1 ///
2 module imap.request;
3 
4 import imap.socket;
5 import imap.session;
6 import imap.namespace;
7 import imap.defines;
8 import imap.auth;
9 import imap.response;
10 import imap.sil : SILdoc;
11 
12 /// Every IMAP command is preceded with a unique string
13 static int g_tag = 0x1000;
14 
15 
16 auto imapTry(alias F, Args...)(Session session, Args args) {
17     import std.traits : isInstanceOf;
18     import std.conv : to;
19     enum noThrow = true;
20     auto result = F(session, args);
21     alias ResultT = typeof(result);
22     static if (is(ResultT == int)) {
23         auto status = result;
24     } else static if (is(ResultT == ImapResult)) {
25         auto status = result.status;
26     } else static if (isInstanceOf!(Result, ResultT)) {
27         auto status = result.status;
28     } else static if (is(ResultT == ListResponse) || is(ResultT == FlagResult)) {
29         auto status = result.status;
30     } else { static assert(0, "unknown type :" ~ ResultT.stringof); }
31 
32     static if (is(ResultT == int)) {
33         return result;
34     } else {
35         if (status == ImapStatus.unknown) {
36             if (session.options.recoverAll || session.options.recoverErrors) {
37                 session.login();
38                 return F(session, args);
39             } else {
40                 if (!noThrow)
41                     throw new Exception("unknown result " ~ result.to!string);
42                 return result; // ImapResult(ImapStatus.unknown,"");
43             }
44         } else if (status == ImapStatus.bye) {
45             session.closeConnection();
46             if (session.options.recoverAll) {
47                 session.login();
48                 return F(session, args);
49                 // return ImapResult(ImapStatus.none,"");
50             }
51         } else {
52             if (!noThrow)
53                 throw new Exception("IMAP error : " ~ result.to!string ~ "when calling " ~ __traits(identifier, F)  ~ " with args " ~ args.to!string);
54             return result;
55         }
56     }
57     assert(0);
58 }
59 
60 bool isLoginRequest(string value) {
61     import std.string : strip, toUpper, startsWith;
62     return value.strip.toUpper.startsWith("LOGIN");
63 }
64 
65 @SILdoc("Sends to server data; a command")
66 int sendRequest(Session session, string value) {
67     import std.format : format;
68     import std.exception : enforce;
69     import std.experimental.logger : infof, tracef;
70     import std.stdio;
71     import std.string : endsWith;
72 
73     enforce(session.socket, "not connected to server");
74 
75     if (session.options.debugMode) {
76         if (value.isLoginRequest) {
77             infof("sending command (%s):\n\n%s\n\n", session.socket, value.length - session.imapLogin.password.length - "\"\"\r\n".length, value);
78             tracef("C (%s): %s\n", session.socket, value.length, session.imapLogin.password.length - "\"\"\r\n".length, value);
79         } else {
80             infof("sending command (%s):\n\n%s\n", session.socket, value);
81             tracef("C (%s): %s", session.socket, value);
82         }
83     }
84 
85     int tag = g_tag;
86     g_tag++;
87     if (g_tag > 0xffff)
88         g_tag = 0x1000;
89 
90     auto taggedValue = format!"D%04X %s%s"(tag, value, value.endsWith("\n") ? "" : "\r\n");
91     version (Trace) stderr.writefln("sending %s", taggedValue);
92     if (session.socketWrite(taggedValue) == -1)
93         return -1;
94 
95     return tag;
96 }
97 
98 @SILdoc("Sends a response to a command continuation request.")
99 int sendContinuation(Session session, string data) {
100     import std.exception : enforce;
101     enforce(session.socket, "not connected to server");
102     session.socketWrite(data ~ "\r\n");
103     return 1;
104 }
105 
106 
107 @SILdoc("Reset any inactivity autologout timer on the server")
108 void noop(Session session) {
109     auto t = session.sendRequest("NOOP");
110     session.check!responseGeneric(t);
111 }
112 
113 ///
114 auto check(alias F, Args...)(Session session, Args args) {
115     import std.exception : enforce;
116     import std.format : format;
117     import std.traits : isInstanceOf;
118     import imap.response : ImapResult;
119 
120     auto v = F(session, args);
121     alias ResultT = typeof(v);
122     static if (is(ResultT == ImapStatus)) {
123         auto status = v;
124     } else static if (isInstanceOf!(ResultT, Result) || is(ResultT == ImapResult)) {
125         auto status = v.status;
126     } else {
127         auto status = v;
128     }
129     if (status == ImapStatus.bye) {
130         session.closeConnection();
131     }
132     enforce(status != -1 && status != ImapStatus.bye, format!"error calling %s"(__traits(identifier, F)));
133     return v;
134 }
135 
136 
137 @SILdoc("Connect to the server, login to the IMAP server, get its capabilities, get the namespace of the mailboxes.")
138 Session login(Session session) {
139     import std.format : format;
140     import std.exception : enforce;
141     import std.experimental.logger : errorf, infof;
142     import std.string : strip;
143     import std.stdio;
144 
145     int t;
146     ImapResult res;
147     ImapStatus rl = ImapStatus.unknown;
148     auto login = session.imapLogin;
149 
150     scope (failure)
151         closeConnection(session);
152     if (session.socket is null || !session.socket.isAlive()) {
153         if (session.options.debugMode) infof("login called with dead socket, so trying to reconnect");
154         session = openConnection(session);
155     }
156     enforce(session.socket.isAlive(), "not connected to server");
157 
158     auto rg = session.check!responseGreeting();
159     version (Trace) stderr.writefln("got login first stage: %s", rg);
160     /+
161     if (session.options.debugMode)
162     {
163         t = session.check!sendRequest("NOOP");
164         session.check!responseGeneric(t);
165     }
166     +/
167     t = session.check!sendRequest("CAPABILITY");
168     version (Trace) stderr.writefln("sent capability request");
169     res = session.check!responseCapability(t);
170     version (Trace) stderr.writefln("got capabilities: %s", res);
171 
172     bool needsStartTLS =   (session.sslProtocol != ProtocolSSL.none)
173         && session.capabilities.has(Capability.startTLS)
174         && session.options.startTLS;
175     if (needsStartTLS) {
176         version (Trace) stderr.writeln("sending StartTLS");
177         t = session.check!sendRequest("STARTTLS");
178         version (Trace) stderr.writeln("checking for StartTLS response ");
179         res = session.check!responseGeneric(t);
180         // enforce(res.status == ImapStatus.ok, "received bad response: " ~ res.to!string);
181         version (Trace) stderr.writeln("opening secure connection");
182         session.openSecureConnection();
183         version (Trace) stderr.writeln("opened secure connection; check capabilties");
184         t = session.check!sendRequest("CAPABILITY");
185         version (Trace) stderr.writeln("sent capabilties request");
186         res = session.check!responseCapability(t);
187         version (Trace) stderr.writeln("got capabilities response");
188         version (Trace) stderr.writeln(res);
189     }
190 
191     if (rg.status == ImapStatus.preAuth) {
192         rl = ImapStatus.preAuth;
193     } else {
194         if (session.capabilities.has(Capability.cramMD5) && session.options.cramMD5) {
195             version (Trace) stderr.writefln("cram");
196             t = session.check!sendRequest("AUTHENTICATE CRAM-MD5");
197             res = session.check!responseAuthenticate(t);
198             version (Trace) stderr.writefln("authenticate cram first response: %s", res);
199             enforce(res.status == ImapStatus.continue_, "login failure");
200             auto hash = authCramMD5(login.username, login.password, res.value.strip);
201             stderr.writefln("hhash: %s", hash);
202             t = session.check!sendContinuation(hash);
203             res = session.check!responseGeneric(t);
204             version (Trace) stderr.writefln("response: %s", res);
205             rl = res.status;
206         }
207         if (rl != ImapStatus.ok) {
208             t = session.check!sendRequest(format!"LOGIN \"%s\" \"%s\""(login.username, login.password));
209             res = session.check!responseGeneric(t);
210             rl = res.status;
211         }
212         if (rl == ImapStatus.no) {
213             auto err = format!"username %s or password rejected at %s\n"(login.username, session.server);
214             if (session.options.debugMode) errorf("username %s or password rejected at %s\n", login.username, session.server);
215             session.closeConnection();
216             throw new Exception(err);
217         }
218     }
219 
220     t = session.check!sendRequest("CAPABILITY");
221     res = session.check!responseCapability(t);
222 
223     if (session.capabilities.has(Capability.namespace) && session.options.namespace) {
224         t = session.check!sendRequest("NAMESPACE");
225         res = session.check!responseNamespace(t);
226         rl = res.status;
227     }
228 
229     if (session.capabilities.has(Capability.imap4Rev1)) {
230         session.imapProtocol = ImapProtocol.imap4Rev1;
231     } else if (session.capabilities.has(Capability.imap4)) {
232         session.imapProtocol = ImapProtocol.imap4;
233     } else {
234         session.imapProtocol = ImapProtocol.init;
235     }
236 
237     if (session.selected !is null) {
238         t = session.check!sendRequest(format!"SELECT \"%s\""(session.selected.toString));
239         auto selectResult = session.responseSelect(t);
240         enforce(selectResult.status == ImapStatus.ok);
241         rl = selectResult.status;
242     }
243 
244     return session.setStatus(rl);
245 }
246 
247 
248 @SILdoc("Logout from the IMAP server and disconnect")
249 int logout(Session session) {
250 
251     if (responseGeneric(session, sendRequest(session, "LOGOUT")).status == ImapStatus.unknown) {
252         // sessionDestroy(session);
253     } else {
254         closeConnection(session);
255         // sessionDestroy(session);
256     }
257     return ImapStatus.ok;
258 }
259 
260 @SILdoc("IMAP examine command for mailbox mbox")
261 auto examine(Session session, Mailbox mbox) {
262     import std.format : format;
263     auto request = format!`EXAMINE "%s"`(mbox);
264     auto id = session.sendRequest(request);
265     return session.responseExamine(id);
266 }
267 
268 
269 @SILdoc("Get mailbox status")
270 auto status(Session session, Mailbox mbox) {
271     import std.format : format;
272     import std.exception : enforce;
273     enforce(session.imapProtocol == ImapProtocol.imap4Rev1, "status only implemented for Imap4Rev1 - try using examine");
274     auto mailbox = mbox.toString();
275     auto request = format!`STATUS "%s" (MESSAGES RECENT UNSEEN UIDNEXT)`(mailbox);
276     auto id = session.sendRequest(request);
277     return session.responseStatus(id, mailbox);
278 }
279 
280 @SILdoc("Open mailbox in read-write mode.")
281 auto select(Session session, Mailbox mailbox) {
282     import std.format : format;
283     auto request = format!`SELECT "%s"`(mailbox.toString);
284     auto id = session.sendRequest(request);
285     auto ret = session.responseSelect(id);
286     if (ret.status == ImapStatus.ok)
287         session.selected = mailbox;
288     return ret;
289 }
290 
291 
292 @SILdoc("Close examined/selected mailbox")
293 ImapResult close(Session session) {
294     enum request = "CLOSE";
295     auto id = sendRequest(session, request);
296     auto response = responseGeneric(session, id);
297     if (response.status == ImapStatus.ok) {
298         session.selected = Mailbox.init;
299     }
300     return response;
301 }
302 
303 @SILdoc("Remove all messages marked for deletion from selected mailbox")
304 ImapResult expunge(Session session) {
305     enum request = "EXPUNGE";
306     auto id = sendRequest(session, request);
307     return session.responseGeneric(id);
308 }
309 
310 ///
311 struct MailboxList {
312     string[] mailboxes;
313     string[] folders;
314 }
315 
316 @SILdoc(`List available mailboxes:
317 	The LIST command returns a subset of names from the complete set
318 	of all names available to the client.  Zero or more untagged LIST
319 	replies are returned, containing the name attributes, hierarchy
320 	delimiter, and name.
321 
322 	The reference and mailbox name arguments are interpreted into a
323 	canonical form that represents an unambiguous left-to-right
324 	hierarchy. 
325 
326 	Here are some examples of how references and mailbox names might
327 	be interpreted on a UNIX-based server:
328 
329 	   Reference     Mailbox Name  Interpretation
330 	   ------------  ------------  --------------
331 	   ~smith/Mail/  foo.*         ~smith/Mail/foo.*
332 	   archive/      %             archive/%
333 	   #news.        comp.mail.*   #news.comp.mail.*
334 	   ~smith/Mail/  /usr/doc/foo  /usr/doc/foo
335 	   archive/      ~fred/Mail/*  ~fred/Mail/*
336 
337 	The first three examples demonstrate interpretations in
338 	the context of the reference argument.  Note that
339 	"~smith/Mail" SHOULD NOT be transformed into something
340 	like "/u2/users/smith/Mail", or it would be impossible
341 	for the client to determine that the interpretation was
342 	in the context of the reference.
343 
344 	The character "*" is a wildcard, and matches zero or more
345 	characters at this position.  The character "%" is similar to "*",
346 	but it does not match a hierarchy delimiter.  If the "%" wildcard
347 	is the last character of a mailbox name argument, matching levels
348 	of hierarchy are also returned.  If these levels of hierarchy are
349 	not also selectable mailboxes, they are returned with the
350 	\Noselect mailbox name attribute (see the description of the LIST
351 	response for more details).
352 
353 	Params:
354 		session - current IMAP session
355 		referenceName
356 		mailboxName
357 `)
358 auto list(Session session, string referenceName = "", string mailboxName = "*") {
359     import std.format : format;
360     auto request = format!`LIST "%s" "%s"`(referenceName, mailboxName);
361     auto id = session.sendRequest(request);
362     return session.responseList(id);
363 }
364 
365 
366 @SILdoc("List subscribed mailboxes")
367 auto lsub(Session session, string refer = "", string name = "*") {
368     import std.format : format;
369     auto request = format!`LSUB "%s" "%s"`(refer, name);
370     auto id = session.imapTry!sendRequest(request);
371     return session.responseList(id);
372 }
373 
374 @SILdoc("Search selected mailbox according to the supplied search criteria")
375 auto search(Session session, string criteria, string charset = null) {
376     import std.format : format;
377     string s;
378 
379     s = (charset.length > 0) ? format!`UID SEARCH CHARSET "%s" %s`(charset, criteria)
380         : format!`UID SEARCH %s`(criteria);
381 
382     auto t = session.imapTry!sendRequest(s);
383     auto r = session.responseSearch(t);
384     return r;
385 }
386 
387 enum SearchResultType {
388     min,
389     max,
390     count,
391     all,
392 }
393 
394 string toString(SearchResultType[] resultTypes) {
395     import std.string : toUpper, join;
396     import std.format : format;
397     import std.algorithm : map;
398     import std.conv : to;
399     import std.array : array;
400 
401     if (resultTypes.length == 0)
402         return null;
403     return format!"RETURN (%s) "(resultTypes.map!(t => t.to!string.toUpper).array.join(" "));
404 }
405 
406 private string createSearchMailboxList(string[] mailboxes, string[] subtrees, bool subtreeOne = false) {
407     import std.array : Appender;
408     import std.algorithm : map;
409     Appender!string ret;
410     import std.format : format;
411     import std.algorithm : map;
412     import std.string : join, strip;
413     auto subtreeTerm = subtreeOne ? "subtree-one" : "subtree";
414 
415     // FIXME = should add "subscribed", "inboxes" and maybe "selected" and "selected-delayed"
416     if (mailboxes.length == 0 && subtrees.length == 0)
417         return `IN ("personal") `;
418     if (mailboxes.length > 0)
419         ret.put(format!"mailboxes %s "(mailboxes.map!(m => format!`"%s"`(m)).join(" ")));
420     if (subtrees.length > 0)
421         ret.put(format!"%s %s "(subtreeTerm, subtrees.map!(t => format!`"%s"`(t)).join(" ")));
422     return format!"IN (%s) "(ret.data.strip);
423 }
424 
425 
426 @SILdoc("Search selected mailbox according to the supplied search criteria.")
427 auto esearch(Session session, string criteria, SearchResultType[] resultTypes = [], string charset = null) {
428     import std.format : format;
429     import std.string : strip;
430     string s;
431 
432     s = (charset.length > 0) ? format!`UID SEARCH %sCHARSET "%s" %s`(resultTypes.toString(), charset, criteria)
433         : format!`UID SEARCH %s%s`(resultTypes.toString(), criteria);
434     s = s.strip;
435     import std.stdio;
436     stderr.writeln(s);
437     auto t = session.imapTry!sendRequest(s);
438     auto r = session.responseEsearch(t);
439     return r;
440 }
441 
442 @SILdoc("Search selected mailboxes and subtrees according to the supplied search criteria.")
443 auto multiSearch(Session session, string criteria, SearchResultType[] resultTypes = [], string[] mailboxes = [], string[] subtrees = [], string charset = null, bool subtreeOne = false) {
444     import std.format : format;
445     import std.string : strip;
446     string s;
447 
448     s = (charset.length > 0) ? format!`ESEARCH %s%sCHARSET "%s" %s`(
449         createSearchMailboxList(mailboxes, subtrees, subtreeOne),
450         resultTypes.toString(), charset, criteria)
451         : format!`ESEARCH %s%s%s`(
452             createSearchMailboxList(mailboxes, subtrees, subtreeOne),
453             resultTypes.toString(), criteria);
454     s = s.strip;
455     auto t = session.imapTry!sendRequest(s);
456     auto r = session.responseMultiSearch(t);
457     return r;
458 }
459 
460 private int sendFetchRequest(Session session, string id, string itemSpec) {
461     import std.format : format;
462 
463     // Does the id start with '#'?
464     if (id.length > 1 && id[0] == '#') {
465         // A mailbox sequence id which should be in the range 1 to mailbox-message-count.
466         return session.sendRequest(format!"FETCH %s %s"(id[1 .. $], itemSpec));
467     }
468 
469     // Otherwise it's a mailbox uid.
470     return session.sendRequest(format!"UID FETCH %s %s"(id, itemSpec));
471 }
472 
473 @SILdoc("Fetch the FLAGS, INTERNALDATE and RFC822.SIZE of the messages")
474 auto fetchFast(Session session, string mesg) {
475     auto t = session.imapTry!sendFetchRequest(mesg, "FAST");
476     return session.responseFetchFast(t);
477 }
478 
479 @SILdoc("Fetch the FLAGS of the messages")
480 auto fetchFlags(Session session, string mesg) {
481     auto t = session.imapTry!sendFetchRequest(mesg, "FLAGS");
482     return session.responseFetchFlags(t);
483 }
484 
485 @SILdoc("Fetch the INTERNALDATE of the messages")
486 auto fetchDate(Session session, string mesg) {
487     auto id = session.imapTry!sendFetchRequest(mesg, "INTERNALDATE");
488     return session.responseFetchDate(id);
489 }
490 
491 @SILdoc("Fetch the RFC822.SIZE of the messages")
492 auto fetchSize(Session session, string mesg) {
493     auto id = session.imapTry!sendFetchRequest(mesg, "RFC822.SIZE");
494     return session.responseFetchSize(id);
495 }
496 
497 @SILdoc("Fetch the BODYSTRUCTURE of the messages")
498 auto fetchStructure(Session session, string mesg) {
499     auto id = session.imapTry!sendFetchRequest(mesg, "BODYSTRUCTURE");
500     return session.responseFetchStructure(id);
501 }
502 
503 @SILdoc("Fetch the BODY[HEADER] of the messages")
504 auto fetchHeader(Session session, string mesg) {
505     auto id  = session.imapTry!sendFetchRequest(mesg, "BODY.PEEK[HEADER]");
506     return session.responseFetchBody(id);
507 }
508 
509 @SILdoc("Fetch the entire message text, ie. RFC822, of the messages")
510 auto fetchRFC822(Session session, string mesg) {
511     auto id  = session.imapTry!sendFetchRequest(mesg, "RFC822");
512     return session.responseFetchBody(id);
513 }
514 
515 @SILdoc("Fetch the text, ie. BODY[TEXT], of the messages")
516 auto fetchText(Session session, string mesg) {
517     auto id  = session.imapTry!sendFetchRequest(mesg, "BODY.PEEK[TEXT]");
518     return session.responseFetchBody(id);
519 }
520 
521 @SILdoc("Fetch the specified header fields, ie. BODY[HEADER.FIELDS (<fields>)], of the messages.")
522 auto fetchFields(Session session, string mesg, string headerFields) {
523     import std.format : format;
524     auto itemSpec = format!`BODY.PEEK[HEADER.FIELDS (%s)]`(headerFields);
525     auto id  = session.imapTry!sendFetchRequest(mesg, itemSpec);
526     return session.responseFetchBody(id);
527 }
528 
529 @SILdoc("Fetch the specified message part, ie. BODY[<part>], of the messages")
530 auto fetchPart(Session session, string mesg, string part) {
531     import std.format : format;
532     auto itemSpec = format!`BODY.PEEK[%s]`(part);
533     auto id  = session.imapTry!sendFetchRequest(mesg, itemSpec);
534     return session.responseFetchBody(id);
535 }
536 
537 enum StoreMode {
538     replace,
539     add,
540     remove,
541 }
542 
543 private string modeString(StoreMode mode) {
544     final switch (mode) with (StoreMode)
545         {
546             case replace: return "";
547             case add: return "+";
548             case remove: return "-";
549         }
550     assert(0);
551 }
552 
553 private string formatRequestWithId(alias fmt, Args...)(string id, Args args) {
554     import std.format : format;
555 
556     if (id.length > 1 && id[0] == '#') {
557         return format!(fmt)(id[1 .. $], args);
558     }
559     return format!("UID " ~ fmt)(id, args);
560 }
561 
562 @SILdoc("Add, remove or replace the specified flags of the messages.")
563 auto store(Session session, string mesg, StoreMode mode, string flags) {
564     import std.format : format;
565     import std.algorithm : canFind;
566     import std.string : toLower, startsWith;
567     import std.format : format;
568     auto t = session.imapTry!sendRequest(formatRequestWithId!"STORE %s %sFLAGS.SILENT (%s)"(mesg, mode.modeString, flags));
569     auto r = session.responseGeneric(t);
570 
571     if (canFind(flags, `\Deleted`) && mode != StoreMode.remove && session.options.expunge) {
572         if (session.capabilities.has(Capability.uidPlus)) {
573             t = session.imapTry!sendRequest(formatRequestWithId!"EXPUNGE %s"(mesg));
574             session.responseGeneric(t);
575         } else {
576             t = session.imapTry!sendRequest("EXPUNGE");
577             session.responseGeneric(t);
578         }
579     }
580     return r;
581 }
582 
583 @SILdoc("Copy the specified messages to another mailbox.")
584 auto copy(Session session, string mesg, Mailbox mailbox) {
585     import std.format : format;
586 
587     auto t = session.imapTry!sendRequest(formatRequestWithId!`COPY %s "%s"`(mesg, mailbox.toString));
588     auto r = session.imapTry!responseGeneric(t);
589     if (r.status == ImapStatus.tryCreate) {
590         t = session.imapTry!sendRequest(format!`CREATE "%s"`(mailbox.toString));
591         session.imapTry!responseGeneric(t);
592         if (session.options.subscribe) {
593             t = session.imapTry!sendRequest(format!`SUBSCRIBE "%s"`(mailbox.toString));
594             session.imapTry!responseGeneric(t);
595         }
596         t = session.imapTry!sendRequest(formatRequestWithId!`COPY %s "%s"`(mesg, mailbox.toString));
597         r = session.imapTry!responseGeneric(t);
598     }
599     return r;
600 }
601 
602 @SILdoc("Move the specified message to another mailbox.")
603 auto move(Session session, long uid, string mailbox) {
604     import std.conv : text;
605     return multiMove(session, text(uid), new Mailbox(session, mailbox));
606 }
607 
608 @SILdoc("Move the specified messages to another mailbox.")
609 auto moveUIDs(Session session, long[] uids, string mailbox) {
610     import std.conv : text;
611     import std.algorithm : map;
612     import std.array : array;
613     import std.string : join;
614     return multiMove(session, uids.map!(uid => text(uid)).array.join(","), new Mailbox(session, mailbox));
615 }
616 
617 @SILdoc("Move the specified messages to another mailbox.")
618 auto multiMove(Session session, string mesg, Mailbox mailbox) {
619     import std.exception : enforce;
620     import std.format : format;
621     import std.conv : to;
622     version (MoveSanity) {
623         auto t = session.imapTry!sendRequest(format!`UID MOVE %s %s`(mesg, mailbox.toString));
624         auto r = session.imapTry!responseMove(t);
625         if (r.status == ImapStatus.tryCreate) {
626             t = session.imapTry!sendRequest(format!`CREATE "%s"`(mailbox.toString));
627             session.imapTry!responseGeneric(t);
628             if (session.options.subscribe) {
629                 t = session.imapTry!sendRequest(format!`SUBSCRIBE "%s"`(mailbox.toString));
630                 session.imapTry!responseGeneric(t);
631             }
632             t = session.imapTry!sendRequest(format!`UID MOVE %s %s`(mesg, mailbox.toString));
633             r = session.imapTry!responseMove(t);
634         }
635         enforce(r.status == ImapStatus.ok, "imap error when moving : " ~ r.to!string);
636         return r;
637     } else {
638         auto result = copy(session, mesg, mailbox);
639         enforce(result.status == ImapStatus.ok, format!"unable to copy message %s to %s as first stage of move:%s"(mesg, mailbox, result));
640         result = store(session, mesg, StoreMode.add, `\Deleted`);
641         // enforce(result.status == ImapStatus.ok, format!"unable to set deleted flags for message %s as second stage of move:%s"(mesg,result));
642         return result;
643     }
644     assert(0);
645 }
646 
647 // NOTE: the date string must follow the standard grammar taken from the RFC, without the
648 // surrounding double quotes:
649 //
650 // date-time       = DQUOTE date-day-fixed "-" date-month "-" date-year SP time SP zone DQUOTE
651 // date-day-fixed  = (SP DIGIT) / 2DIGIT
652 // date-month      = "Jan" / "Feb" / "Mar" / "Apr" / "May" / "Jun" / "Jul" / "Aug" / "Sep" / "Oct" / "Nov" / "Dec"
653 // date-year       = 4DIGIT
654 // time            = 2DIGIT ":" 2DIGIT ":" 2DIGIT
655 // zone            = ("+" / "-") 4DIGIT
656 //
657 // e.g., " 5-Nov-2020 14:19:28 +1100"
658 
659 @SILdoc(`Append supplied message to the specified mailbox.`)
660 auto append(Session session, Mailbox mbox, string[] mesgLines, string[] flags = [], string date = string.init) {
661     import std.format : format;
662     import std.algorithm : fold;
663     import std.array : join;
664 
665     string flagsStr = "";
666     if (flags.length > 0) {
667         flagsStr = " (" ~ join(flags, " ") ~ ")";
668     }
669 
670     string dateStr = "";
671     if (date) {
672         dateStr = ` "` ~ date ~ `"`;
673     }
674 
675     // TODO: We're making assumptions about the format of the data sent, i.e., '\r\n' suffixes for
676     // lines, which should be better abstracted away.
677 
678     // Each line in the message has a 2 char '\r\n' suffix added when sent to the server.
679     size_t mesgSize = fold!((size, line) => size + line.length + 2)(mesgLines, 0.size_t);
680     int cmdTag = session.imapTry!sendRequest(format!`APPEND "%s"%s%s {%u}`(mbox, flagsStr, dateStr, mesgSize));
681 
682     auto resp = session.imapTry!responseContinuation(cmdTag);
683     if (resp.status != ImapStatus.continue_) {
684         return resp;
685     }
686 
687     // Send each line individually -- the server will wait for exactly mesgSize bytes as the literal
688     // string.  Then send an empty line (which will actually be '\r\n') to end the APPEND command.
689     foreach (line; mesgLines) {
690         session.sendContinuation(line);
691     }
692     session.sendContinuation("");
693 
694     return session.responseGeneric(cmdTag);
695 }
696 
697 @SILdoc("Create the specified mailbox")
698 auto create(Session session, Mailbox mailbox) {
699     import std.format : format;
700     auto request = format!`CREATE "%s"`(mailbox.toString);
701     auto id = session.sendRequest(request);
702     return session.responseGeneric(id);
703 }
704 
705 
706 @SILdoc("Delete the specified mailbox")
707 auto delete_(Session session, Mailbox mailbox) {
708     import std.format : format;
709     auto request = format!`DELETE "%s"`(mailbox.toString);
710     auto id = session.sendRequest(request);
711     return session.responseGeneric(id);
712 }
713 
714 @SILdoc("Rename a mailbox")
715 auto rename(Session session, Mailbox oldmbox, Mailbox newmbox) {
716     import std.format : format;
717     auto request = format!`RENAME "%s" "%s"`(oldmbox.toString, newmbox.toString);
718     auto id = session.sendRequest(request);
719     return session.responseGeneric(id);
720 }
721 
722 @SILdoc("Subscribe to the specified mailbox")
723 auto subscribe(Session session, Mailbox mailbox) {
724     import std.format : format;
725     auto request = format!`SUBSCRIBE "%s"`(mailbox.toString);
726     auto id = session.sendRequest(request);
727     return session.responseGeneric(id);
728 }
729 
730 
731 @SILdoc("Unsubscribe from the specified mailbox.")
732 auto unsubscribe(Session session, Mailbox mailbox) {
733     import std.format : format;
734     auto request = format!`UNSUBSCRIBE "%s"`(mailbox.toString);
735     auto id = session.sendRequest(request);
736     return session.responseGeneric(id);
737 }
738 
739 @SILdoc("IMAP ENABLE command.")
740 auto enable(Session session, string command) {
741     import std.format : format;
742     auto request = format!`ENABLE %s`(command);
743     auto id = session.sendRequest(request);
744     return session.responseGeneric(id);
745 }
746 
747 @SILdoc("IMAP raw command.")
748 auto raw(Session session, string command) {
749     import std.format : format;
750     auto id = session.sendRequest(command);
751     return session.responseGeneric(id);
752 }
753 
754 @SILdoc(`IMAP idle command`)
755 auto idle(Session session) {
756     import std.stdio;
757     Tag t;
758     ImapResult r, ri;
759 
760     if (!session.capabilities.has(Capability.idle))
761         return ImapResult(ImapStatus.bad, "");
762 
763     version (Trace) stderr.writefln("inner loop for idle");
764     t = session.sendRequest("IDLE");
765     ri = session.responseIdle(t);
766     r = session.responseContinuation(t);
767     version (Trace) stderr.writefln("sendRequest - responseContinuation was %s", r);
768     if (r.status == ImapStatus.continue_) {
769         ri = session.responseIdle(t);
770         version (Trace) stderr.writefln("responseIdle result was %s", ri);
771         session.sendContinuation("DONE");
772         version (Trace) stderr.writefln("continuation result was %s", ri);
773         r = session.responseGeneric(t);
774         version (Trace) stderr.writefln("reponseGenericresult was %s", r);
775     }
776     version (Trace) stderr.writefln("returning %s", ri);
777 
778     return ri;
779 }
780 
781 ///
782 enum SearchField {
783     all,
784     and,
785     or,
786     not,
787     old,
788     answered,
789     deleted,
790     draft,
791     flagged,
792     header,
793     body_,
794     bcc,
795     cc,
796     from,
797     to,
798     subject,
799     text,
800     uid,
801     unanswered,
802     undeleted,
803     undraft,
804     unflagged,
805     unkeyword,
806     unseen,
807     larger,
808     smaller,
809     sentBefore,
810     sentOn,
811     sentSince,
812     keyword,
813     messageNumbers,
814     uidNumbers,
815     resultMin,
816     resultMax,
817     resultAll,
818     resultCount,
819     resultRemoveFrom,
820     resultPartial,
821     sourceMailbox,
822     sourceSubtree,
823     sourceTag,
824     sourceUidValidity,
825     contextCount,
826     context
827 }
828 
829 struct SearchParameter {
830     string fieldName;
831     // Variable value;
832 }
833