1 /// 2 module imap.session; 3 import imap.defines; 4 import imap.socket; 5 import imap.set; 6 import imap.sil : SILdoc; 7 8 import core.stdc.stdio; 9 import core.stdc.string; 10 import core.stdc.errno; 11 import std.socket; 12 import core.time : Duration; 13 14 import deimos.openssl.ssl; 15 import deimos.openssl.err; 16 import deimos.openssl.sha; 17 18 19 struct SSL_ { 20 SSL* handle; 21 alias handle this; 22 } 23 24 25 /// 26 Set!T removeValues(T)(Set!T set, T[] values) { 27 import std.algorithm : each; 28 Set!T ret; 29 set.values_.byKeyValue.each!(entry => ret[entry.key] = entry.value); 30 foreach (value; values) { 31 if (value in ret) 32 ret.remove(value); 33 } 34 return ret; 35 } 36 37 /// 38 Set!T addValues(T)(Set!T set, T[] values) { 39 import std.algorithm : each; 40 Set!T ret; 41 set.values_.byKeyValue.each!(entry => ret[entry.key] = entry.value); 42 values.each!(value => set.values_[value] = true); 43 return ret; 44 } 45 46 /// 47 Set!T addSet(T)(Set!T lhs, Set!T rhs) { 48 Set!T ret; 49 return lhs.addValues(rhs.values); 50 } 51 52 /// 53 Set!T removeSet(T)(Set!T lhs, Set!T rhs) { 54 Set!T ret; 55 return lhs.removeValues(rhs.values); 56 } 57 58 /// 59 struct ImapServer { 60 string server = "imap.fastmail.com"; // localhost"; 61 string port = "993"; 62 63 string toString() const { 64 import std.format : format; 65 return format!"%s:%s"(server, port); 66 } 67 } 68 69 /// 70 struct ImapLogin { 71 @SILdoc("User's name. It takes a string as a value.") 72 string username = "laeeth@kaleidic.io"; 73 74 @SILdoc("User's secret keyword. If a password wasn't supplied the user will be asked to enter one interactively the first time it will be needed. It takes a string as a value.") 75 string password; 76 77 string toString() const { 78 import std.format : format; 79 return format!"%s:[hidden]"(username); 80 } 81 } 82 83 /// 84 struct Options { 85 import core.time : Duration, seconds, minutes; 86 87 bool debugMode = false; 88 bool verboseOutput = false; 89 bool interactive = false; 90 bool namespace = false; 91 92 @SILdoc("When this option is enabled and the server supports the Challenge-Response Authentication Mechanism (specifically CRAM-MD5), this method will be used for user authentication instead of a plaintext password LOGIN. This variable takes a boolean as a value. Default is false") 93 bool cramMD5 = false; 94 95 bool startTLS = false; 96 bool tryCreate = false; 97 bool recoverAll = true; 98 bool recoverErrors = true; 99 100 @SILdoc("Normally, messages are marked for deletion and are actually deleted when the mailbox is closed. When this option is enabled, messages are expunged immediately after being marked deleted. This variable takes a boolean as a value. Default is false") 101 bool expunge = false; 102 103 @SILdoc("By enabling this option new mailboxes that were automatically created, get also subscribed; they are set active in order for IMAP clients to recognize them. This variable takes a boolean as a value. Default is false") 104 bool subscribe = false; 105 106 bool wakeOnAny = true; 107 108 @SILdoc("The time in minutes before terminating and re-issuing the IDLE command, in order to keep alive the connection, by resetting the inactivity timeout of the server. A standards compliant server must have an inactivity timeout of at least 30 minutes. But it may happen that some IMAP servers don't respect that, or some intermediary network device has a shorter timeout. By setting this option the above problem can be worked around. This variable takes a number as a value. Default is 29 minutes. ") 109 Duration keepAlive = 29.minutes; 110 111 string logFile; 112 string configFile; 113 string oneline; 114 Duration timeout = 20.seconds; 115 } 116 117 /// 118 final class Mailbox { 119 this(Session session, string mailbox) { 120 this(session, [mailbox]); 121 } 122 this(Session session, Mailbox base, string mailbox) { 123 this(session, base.path ~ mailbox); 124 } 125 this(Session session, string[] mailboxes) { 126 if (session.namespaceDelim == '\0') { 127 import std.exception : enforce; 128 import imap.request : list; 129 130 // Fetch the delimiter *once* for the session. We're assuming that INBOX exists, so 131 // there will be at least one entry returned for us to inspect. 132 auto resp = session.list(); 133 enforce(resp.status == ImapStatus.ok, "Failed to get listing in Mailbox()."); 134 session.namespaceDelim = resp.entries[0].hierarchyDelimiter[0]; 135 } 136 path = mailboxes; 137 delim = session.namespaceDelim; 138 } 139 140 /// 141 override string toString() { 142 import std.array : join; 143 import std.string : toUpper, replace; 144 import std.format : format; 145 import imap.namespace; 146 147 if (utf7Path is null) { 148 // XXX Or to we convert to utf-7 before joining? 149 utf7Path = utf8ToUtf7(path.join(delim)); 150 } 151 return utf7Path; 152 } 153 154 private { 155 string[] path; 156 char delim; 157 string utf7Path; 158 } 159 } 160 161 /// 162 final class Session { 163 import imap.defines : ImapStatus; 164 import imap.namespace; 165 Options options; 166 ImapStatus status_; 167 string server; 168 169 @SILdoc("The port to connect to. It takes a number as a value. Default is ''143'' for imap and ''993'' for imaps.") 170 string port; 171 172 package AddressInfo addressInfo; 173 ImapLogin imapLogin; 174 Socket socket; 175 ImapProtocol imapProtocol; 176 Set!Capability capabilities; 177 string namespacePrefix; 178 char namespaceDelim = '\0'; // Use Nullable? 179 Mailbox selected; 180 181 bool useSSL = true; 182 bool noCerts = true; 183 ProtocolSSL sslProtocol = ProtocolSSL.tls1_2; // ssl3; // tls1_2; 184 SSL* sslConnection; 185 SSL_CTX* sslContext; 186 187 override string toString() const { 188 import std.array : Appender; 189 import std.format : formattedWrite; 190 Appender!string ret; 191 ret.formattedWrite!"Session to %s:%s as user %s\n"(server, port, imapLogin.username); 192 ret.formattedWrite!"- useSSL: %s\n"(useSSL); 193 ret.formattedWrite!"- startTLS: %s\n"(options.startTLS); 194 ret.formattedWrite!"- noCerts: %s\n"(noCerts); 195 ret.formattedWrite!"- sslProtocol: %s\n"(sslProtocol); 196 ret.formattedWrite!"- imap protocol: %s\n"(imapProtocol); 197 ret.formattedWrite!" - capabilities: %s\n"(capabilities); 198 ret.formattedWrite!" - namespace: %s/%s\n"(namespacePrefix, [namespaceDelim]); 199 ret.formattedWrite!" - selected mailbox: %s\n"(selected); 200 return ret.data; 201 } 202 203 this(ImapServer imapServer, ImapLogin imapLogin, bool useSSL = true, Options options = Options.init) { 204 import std.exception : enforce; 205 import std.process : environment; 206 this.options = options; 207 this.server = imapServer.server; 208 this.port = imapServer.port; 209 this.useSSL = useSSL; 210 this.imapLogin = imapLogin; 211 } 212 213 Session useStartTLS(bool useTLS = true) { 214 this.options.startTLS = useTLS; 215 return this; 216 } 217 218 Session setSelected(Mailbox mailbox) { 219 this.selected = mailbox; 220 return this; 221 } 222 223 Session setStatus(ImapStatus status) { 224 this.status_ = status; 225 return this; 226 } 227 228 string status() { 229 import std.conv : to; 230 return status_.to!string; 231 } 232 } 233