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