1 ///
2 module imap.response;
3 import imap.defines;
4 import imap.socket;
5 import imap.session;
6 import imap.set;
7 import imap.sil : SILdoc;
8 
9 import std.typecons : tuple;
10 import core.time : Duration;
11 import arsd.email : IncomingEmailMessage;
12 import core.time : msecs;
13 
14 
15 /**
16         TODO:
17             - add optional response code when parsing statuses
18 */
19 
20 ///
21 alias Tag = int;
22 
23 ///
24 enum ImapResponse {
25     tagged,
26     untagged,
27     capability,
28     authenticate,
29     namespace,
30     status,
31     statusMessages,
32     statusRecent,
33     statusUnseen,
34     statusUidNext,
35     exists,
36     recent,
37     list,
38     search,
39     fetch,
40     fetchFlags,
41     fetchDate,
42     fetchSize,
43     fetchStructure,
44     fetchBody,
45 }
46 
47 
48 @SILdoc("Read data the server sent")
49 auto receiveResponse(Session session, Duration timeout = Duration.init, bool timeoutFail = false) {
50     timeout = (timeout == Duration.init) ? session.options.timeout : timeout;
51     // auto result = session.socketSecureRead(); // timeout,timeoutFail);
52     auto result = session.socketRead(timeout, timeoutFail);
53     if (result.status != Status.success)
54         return tuple(Status.failure, result.value.idup);
55     auto buf = result.value;
56 
57     if (session.options.debugMode) {
58         import std.experimental.logger : infof;
59         infof("getting response (%s):", session.socket);
60         infof("buf: %s", buf);
61     }
62     return tuple(Status.success, buf.idup);
63 }
64 
65 
66 @SILdoc("Search for tagged response in the data that the server sent.")
67 ImapStatus checkTag(Session session, string buf, Tag tag) {
68     import std.algorithm : all, map, filter;
69     import std.ascii : isHexDigit, isWhite;
70     import std.experimental.logger;
71     import std.format : format;
72     import std.string : splitLines, toUpper, strip, split, startsWith;
73     import std.array : array;
74     import std.range : front;
75 
76     if (session.options.debugMode) tracef("checking for tag %s in buf: %s", tag, buf);
77     auto r = ImapStatus.none;
78     auto t = format!"D%04X"(tag);
79     if (session.options.debugMode) tracef("checking for tag %s in buf: %s", t, buf);
80     auto lines = buf.splitLines.map!(line => line.strip).array;
81     auto relevantLines = lines
82         .filter!(line => line.startsWith(t))
83         // && line[t.length].isWhite)
84         .array;
85 
86     foreach (line; relevantLines) {
87         auto token = (line.length > t.length + 1) ? line.toUpper[t.length + 1 .. $].strip.split.front : "";
88         if (token.startsWith("OK")) {
89             r = ImapStatus.ok;
90             break;
91         }
92         if (token.startsWith("NO")) {
93             r = ImapStatus.no;
94             break;
95         }
96         if (token.startsWith("BAD")) {
97             r = ImapStatus.bad;
98             break;
99         }
100     }
101 
102     if (session.options.debugMode) tracef("tag result is status %s for lines: %s", r, relevantLines);
103 
104     if (r != ImapStatus.none && session.options.debugMode)
105         if (session.options.debugMode) tracef("S (%s): %s / %s", session.socket, buf, relevantLines);
106 
107     if (r == ImapStatus.no || r == ImapStatus.bad)
108         if (session.options.debugMode) errorf("IMAP (%s): %s / %s", session.socket, buf, relevantLines);
109 
110     return r;
111 }
112 
113 
114 @SILdoc("Check if server sent a BYE response (connection is closed immediately).")
115 bool checkBye(string buf) {
116     import std.string : toUpper;
117     import std.algorithm : canFind;
118     buf = buf.toUpper;
119     return buf.canFind("* BYE") && !buf.canFind(" LOGOUT ");
120 }
121 
122 
123 @SILdoc("Check if server sent a PREAUTH response (connection already authenticated by external means).")
124 int checkPreAuth(string buf) {
125     import std.string : toUpper;
126     import std.algorithm : canFind;
127     buf = buf.toUpper;
128     return buf.canFind("* PREAUTH");
129 }
130 
131 @SILdoc("Check if the server sent a continuation request.")
132 bool checkContinuation(string buf) {
133     import std.string : startsWith;
134     return buf.length > 2 && (buf[0] == '+' && buf[1] == ' ');
135 }
136 
137 
138 @SILdoc("Check if the server sent a TRYCREATE response.")
139 int checkTryCreate(string buf) {
140     import std.string : toUpper;
141     import std.algorithm : canFind;
142     return buf.toUpper.canFind("[TRYCREATE]");
143 }
144 
145 ///
146 struct ImapResult {
147     ImapStatus status;
148     string value;
149 }
150 
151 @SILdoc("Get server data and make sure there is a tagged response inside them.")
152 ImapResult responseGeneric(Session session, Tag tag, Duration timeout = 2000.msecs) {
153     import std.typecons : Tuple;
154     import std.array : Appender;
155     import core.time : msecs;
156     import std.datetime : MonoTime;
157     Tuple!(Status, string)result;
158     Appender!string buf;
159     ImapStatus r;
160 
161     if (tag == -1)
162         return ImapResult(ImapStatus.unknown, "");
163 
164     MonoTime before = MonoTime.currTime();
165     do
166     {
167         result = session.receiveResponse(timeout, false);
168         if (result[0] == Status.failure)
169             return ImapResult(ImapStatus.unknown, buf.data ~ result[1].idup);
170         buf.put(result[1].idup);
171 
172         if (checkBye(result[1]))
173             return ImapResult(ImapStatus.bye, buf.data);
174 
175         r = session.checkTag(result[1], tag);
176     } while (r == ImapStatus.none);
177 
178     if (r == ImapStatus.no && (checkTryCreate(result[1]) || session.options.tryCreate))
179         return ImapResult(ImapStatus.tryCreate, buf.data);
180 
181     return ImapResult(r, buf.data.decodeMimeHeader);
182 }
183 
184 
185 @SILdoc("Get server data and make sure there is a continuation response inside them.")
186 ImapResult responseContinuation(Session session, Tag tag) {
187     import std.algorithm : any;
188     import std.string : strip, splitLines;
189 
190     string buf;
191     // ImapStatus r;
192     import std.typecons : Tuple;
193     Tuple!(Status, string)result;
194     ImapStatus resTag = ImapStatus.ok;
195     do
196     {
197         result = session.receiveResponse(Duration.init, false);
198         if (result[0] == Status.failure)
199             break;
200         // return ImapResult(ImapStatus.unknown,"");
201         buf ~= result[1];
202 
203         if (checkBye(result[1]))
204             return ImapResult(ImapStatus.bye, result[1]);
205         resTag = session.checkTag(result[1], tag);
206     } while (resTag != ImapStatus.none && resTag != ImapStatus.no
207              && !result[1].strip.splitLines.any!(line => line.strip.checkContinuation));
208 
209     if (resTag == ImapStatus.no && (checkTryCreate(buf) || session.options.tryCreate))
210         return ImapResult(ImapStatus.tryCreate, buf);
211 
212     if (resTag == ImapStatus.none)
213         return ImapResult(ImapStatus.continue_, buf);
214 
215     return ImapResult(resTag, buf);
216 }
217 
218 
219 @SILdoc("Process the greeting that server sends during connection.")
220 ImapResult responseGreeting(Session session) {
221     import std.experimental.logger : tracef;
222 
223     auto res = session.receiveResponse(Duration.init, false);
224     if (res[0] == Status.failure)
225         return ImapResult(ImapStatus.unknown, "");
226 
227     if (session.options.debugMode) tracef("S (%s): %s", session.socket, res);
228 
229     if (checkBye(res[1]))
230         return ImapResult(ImapStatus.bye, res[1]);
231 
232     if (checkPreAuth(res[1]))
233         return ImapResult(ImapStatus.preAuth, res[1]);
234 
235     return ImapResult(ImapStatus.none, res[1]);
236 }
237 
238 T parseEnum(T)(string val, T def = T.init) {
239     import std.traits : EnumMembers;
240     import std.conv : to;
241     static foreach (C; EnumMembers!T) {
242         {
243             enum name = C.to!string;
244             enum udas = __traits(getAttributes, __traits(getMember, T, name));
245             static if (udas.length > 0) {
246                 if (val == udas[$ - 1].to!string) {
247                     return C;
248                 }
249             }
250         }
251     }
252     return def;
253 }
254 
255 
256 @SILdoc("Process the data that server sent due to IMAP CAPABILITY client request.")
257 ImapResult responseCapability(Session session, Tag tag) {
258     import std.experimental.logger : infof, tracef;
259     import std.string : splitLines, join, startsWith, toUpper, strip, split;
260     import std.algorithm : filter, map;
261     import std.array : array;
262     import std.traits : EnumMembers;
263     import std.conv : to;
264 
265     enum CapabilityToken = "* CAPABILITY ";
266 
267     auto res = session.responseGeneric(tag);
268     if (res.status == ImapStatus.unknown || res.status == ImapStatus.bye)
269         return res;
270 
271     auto tokens = res.value
272         .splitLines
273         .filter!(line => line.startsWith(CapabilityToken) && line.length > CapabilityToken.length)
274         .array
275         .map!(line => line[CapabilityToken.length .. $].strip.split
276             .map!(token => token.strip)
277             .array)
278         .join;
279 
280     foreach (token; tokens) {
281         auto capability = parseEnum!Capability(token, Capability.none);
282         if (capability != Capability.none) {
283             session.capabilities = session.capabilities.add(capability);
284         }
285     }
286 
287     if (session.options.debugMode)
288         version (Trace) {
289             tracef("session capabilities: %s", session.capabilities.values);
290             tracef("session protocol: %s", session.imapProtocol);
291         }
292     return res;
293 }
294 
295 
296 @SILdoc("Process the data that server sent due to IMAP AUTHENTICATE client request.")
297 ImapResult responseAuthenticate(Session session, Tag tag) {
298     import std.string : splitLines, join, strip, startsWith;
299     import std.algorithm : filter, map;
300     import std.array : array;
301 
302     auto res = session.responseContinuation(tag);
303     auto challengeLines = res.value
304         .splitLines
305         .filter!(line => line.startsWith("+ "))
306         .array
307         .map!(line => (line.length == 2) ? "" : line[2 .. $].strip)
308         .array
309         .join;
310     if (res.status == ImapStatus.continue_ && challengeLines.length > 0) {
311         return ImapResult(ImapStatus.continue_, challengeLines);
312     } else {
313         return ImapResult(ImapStatus.none, res.value);
314     }
315 }
316 
317 @SILdoc("Process the data that server sent due to IMAP NAMESPACE client request.")
318 ImapResult responseNamespace(Session session, Tag tag) {
319     auto r = session.responseGeneric(tag);
320     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
321         return ImapResult(r.status, r.value);
322     return r;
323 }
324 
325 
326 string coreUDA(string uda) {
327     import std.string : replace, strip;
328     return uda.replace("# ", "").replace(" #", "").strip;
329 }
330 
331 
332 T getValueFromLine(T)(string line, string uda) {
333     import std.conv : to;
334     import std.string : startsWith, strip, split;
335     auto udaToken = uda.coreUDA();
336     auto i = token.indexOf(token, udaToken);
337     auto j = i + udaToken.length + 1;
338 
339     bool isPrefix = uda.startsWith("# ");
340     if (isPrefix) {
341         return token[0 .. i].strip.split.back.to!T;
342     } else {
343         return token[j .. $].strip.to!T;
344     }
345 }
346 
347 string getTokenFromLine(string line, string uda) {
348     return "";
349 }
350 
351 version (None)
352     T parseStruct(T)(T agg, string line) {
353         import std.traits : Fields;
354         import std.conv : to;
355         static foreach (M; __traits(allMembers, T)) {
356             {
357                 enum name = M.to!string;
358                 enum udas = __traits(getAttributes, __traits(getMember, T, name));
359                 alias FT = typeof(__traits(getMember, T, name));
360                 static if (udas.length > 0) {
361                     enum uda = udas[0].to!string;
362                     if (token == uda.coreUDA) {
363                         __traits(getMember, agg, name) = getValueFromToken!FT(line, uda);
364                     }
365                 }
366             }
367         }
368         return agg;
369     }
370 
371 struct StatusResult {
372     ImapStatus status;
373     string value;
374 
375     @("MESSAGES")
376     int messages;
377 
378     @("RECENT")
379     int recent;
380 
381     @("UIDNEXT")
382     int uidNext;
383 
384     @("UNSEEN")
385     int unseen;
386 }
387 
388 T parseUpdateT(T)(T t, string name, string value) {
389     import std.format : format;
390     import std.exception : enforce;
391     import std.conv : to;
392 
393     bool isKnown = false;
394     static foreach (M; __traits(allMembers, T)) {
395         {
396             enum udas = __traits(getAttributes, __traits(getMember, T, M));
397             static if (udas.length > 0) {
398                 if (name == udas[0].to!string) {
399                     alias FieldType = typeof(__traits(getMember, T, M));
400                     __traits(getMember, t, M) = value.to!FieldType;
401                     isKnown = true;
402                 }
403             }
404         }
405     }
406     enforce(isKnown, format!"unknown token for type %s parsing name = %s; value = %s"
407             (__traits(identifier, T), name, value));
408     return t;
409 }
410 
411 private string[][] extractParenthesizedList(string line) {
412     import std.string : indexOf, lastIndexOf, strip, split;
413     import std.format : format;
414     import std.range : chunks;
415     import std.exception : enforce;
416     import std.array : array;
417     import std.algorithm : map;
418 
419     auto i = line.indexOf("(");
420     auto j = line.lastIndexOf(")");
421 
422     if (i == -1 || j == -1)
423         return [][];
424 
425     enforce(j > i, format!"line %s should have a (parenthesized list) but it is malformed"(line));
426     auto cols = line[i + 1 .. j].strip.split;
427     enforce(cols.length % 2 == 0, format!"tokens %s should have an even number of columns but they don't"(cols));
428     return cols.chunks(2).map!(r => r.array).array;
429 }
430 
431 
432 @SILdoc("Process the data that server sent due to IMAP STATUS client request.")
433 StatusResult responseStatus(Session session, int tag, string mailboxName) {
434     import std.exception : enforce;
435     import std.algorithm : map, filter;
436     import std.array : array;
437     import std.string : splitLines, split, strip, toUpper, indexOf, startsWith, isNumeric;
438     import std.range : front;
439     import std.conv : to;
440 
441     // The server response is something like:
442     //   * STATUS INBOX (MESSAGES 10 RECENT 5 UIDNEXT 100 UNSEEN 2)
443     //   D1003 OK Completed
444 
445     auto resp = session.responseGeneric(tag);
446     if (resp.status == ImapStatus.unknown || resp.status == ImapStatus.bye)
447         return StatusResult(resp.status, resp.value);
448 
449     StatusResult ret;
450     ret.status = resp.status;
451     ret.value = resp.value;
452 
453     auto extractMailbox = function string(string line) {
454         auto words = line.split;
455         return (words.length < 3) ? null : words[2].strip;
456     };
457 
458     // Find the 'STATUS' line and extract its statistical key/value pairs.
459     enum StatusToken = "* STATUS ";
460     auto statuses = resp.value.splitLines
461         .map!(line => line.strip)
462         .filter!(line => line.startsWith(StatusToken) && extractMailbox(line) == mailboxName)
463         .map!(line => line.extractParenthesizedList)
464         .array;
465 
466     // If we found it then parse each pair into our status result.
467     if (statuses.length > 0) {
468         foreach (pair; statuses[0]) {
469             ret = parseUpdateT!StatusResult(ret, pair[0], pair[1]);
470         }
471     }
472     return ret;
473 }
474 
475 string[] extractLinesWithPrefix(string buf, string prefix, size_t minimumLength = 0) {
476     import std.string : splitLines, strip, startsWith;
477     import std.algorithm : map, filter;
478     import std.array : array;
479 
480     auto lines = buf.splitLines
481         .map!(line => line.strip)
482         .filter!(line => line.startsWith(prefix) && line.length > minimumLength)
483         .map!(line => line[prefix.length .. $].strip)
484         .array;
485     return lines;
486 }
487 
488 @SILdoc("Process the data that server sent due to IMAP EXAMINE client request.")
489 ImapResult responseExamine(Session session, int tag) {
490     auto r = session.responseGeneric(tag);
491     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
492         return ImapResult(r.status, r.value);
493     return r;
494 }
495 
496 struct SelectResult {
497     ImapStatus status;
498     string value;
499 
500     @("FLAGS (#)")
501     ImapFlag[] flags;
502 
503     @("# EXISTS")
504     int exists;
505 
506     @("# RECENT")
507     int recent;
508 
509     @("OK [UNSEEN #]")
510     int unseen;
511 
512     @("OK [PERMANENTFLAGS #]")
513     ImapFlag[] permanentFlags;
514 
515     @("OK [UIDNEXT #]")
516     int uidNext;
517 
518     @("OK [UIDVALIDITY #]")
519     int uidValidity;
520 }
521 
522 
523 @SILdoc("Process the data that server sent due to IMAP SELECT client request.")
524 SelectResult responseSelect(Session session, int tag) {
525     import std.algorithm : canFind;
526     import std.string : toUpper;
527     SelectResult ret;
528     auto r = session.responseGeneric(tag);
529     ret.status = (r.value.canFind("OK [READ-ONLY] SELECT")) ? ImapStatus.readOnly : r.status;
530     ret.value = r.value;
531 
532     if (ret.status == ImapStatus.unknown || ret.status == ImapStatus.bye)
533         return ret;
534 
535     auto lines = r.value.extractLinesWithPrefix("* ", 3);
536     version (None)
537         foreach (line; lines) {
538             ret = ret.parseStruct(line);
539         }
540     return ret;
541 }
542 
543 @SILdoc("Process the data that server sent due to IMAP MOVE client request.")
544 ImapResult responseMove(Session session, int tag) {
545     auto r = session.responseGeneric(tag);
546     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
547         return ImapResult(r.status, r.value);
548     return r;
549 }
550 
551 
552 ///
553 enum ListNameAttribute {
554     @(`\NoInferiors`)
555     noInferiors,
556 
557     @(`\Noselect`)
558     noSelect,
559 
560     @(`\Marked`)
561     marked,
562 
563     @(`\UnMarked`)
564     unMarked,
565 
566     @(`\HasChildren`)
567     hasChildren,
568 
569     @(`\HasNoChildren`)
570     hasNoChildren,
571 }
572 
573 
574 ///
575 string stripQuotes(string s) {
576     import std.range : front, back;
577     if (s.length < 2)
578         return s;
579     if (s.front == '"' && s.back == '"')
580         return s[1 .. $ - 1];
581     return s;
582 }
583 
584 ///
585 string stripBrackets(string s) {
586     import std.range : front, back;
587     if (s.length < 2)
588         return s;
589     if (s.front == '(' && s.back == ')')
590         return s[1 .. $ - 1];
591     return s;
592 }
593 
594 ///
595 struct ListEntry {
596     ListNameAttribute[] attributes;
597     string hierarchyDelimiter;
598     string path;
599 }
600 
601 ///
602 struct ListResponse {
603     ImapStatus status;
604     string value;
605     ListEntry[] entries;
606 }
607 
608 @SILdoc("Process the data that server sent due to IMAP LIST or IMAP LSUB client request.")
609 ListResponse responseList(Session session, Tag tag) {
610 //          list:           "\\* (LIST|LSUB) \\(([[:print:]]*)\\) (\"[[:print:]]\"|NIL) " ~
611 //                            "(\"([[:print:]]+)\"|([[:print:]]+)|\\{([[:digit:]]+)\\} *\r+\n+([[:print:]]*))\r+\n+",
612 
613     // Mailbox[] mailboxes;
614     // string[] folders;
615     import std.array : array;
616     import std.algorithm : map, filter;
617     import std.string : indexOf, splitLines, split, strip, startsWith;
618     import std.traits : EnumMembers;
619     import std.conv : to;
620     import std.exception : enforce;
621 
622     import imap.namespace;
623 
624     auto result = session.responseGeneric(tag);
625     if (result.status == ImapStatus.unknown || result.status == ImapStatus.bye)
626         return ListResponse(result.status, result.value);
627 
628     ListEntry[] listEntries;
629 
630     foreach (line; result.value.splitLines
631              .map!(line => line.strip)
632              .filter!(line => line.startsWith("* LIST ") || line.startsWith("* LSUB"))) {
633 
634         // There can be multiple attributes within parentheses.  Find the parenthesised substring,
635         // split it into words and parse the attributes out.
636         auto attribsStartIdx = line.indexOf("(");
637         auto attribsEndIdx = line.indexOf(")");
638         enforce(attribsStartIdx != -1 && attribsEndIdx != -1
639                 && attribsEndIdx > attribsStartIdx, "LIST response parse error.");
640         auto attribsLine = line[attribsStartIdx + 1 .. attribsEndIdx];
641 
642         ListEntry listEntry;
643         static foreach (A; EnumMembers!ListNameAttribute) {
644             {
645                 enum name = A.to!string;
646                 enum udas = __traits(getAttributes, __traits(getMember, ListNameAttribute, name));
647                 static if (udas.length > 0) {
648                     foreach (attrib; attribsLine.split) {
649                         if (attrib == udas[0].to!string) {
650                             listEntry.attributes ~= A;
651                         }
652                     }
653                 }
654             }
655         }
656 
657         auto nonAttribFields = line[attribsEndIdx + 1 .. $].split;
658         listEntry.hierarchyDelimiter = nonAttribFields[0].strip.stripQuotes;
659         listEntry.path = utf7ToUtf8(nonAttribFields[1].strip);
660         listEntries ~= listEntry;
661     }
662     return ListResponse(ImapStatus.ok, result.value, listEntries);
663 }
664 
665 
666 ///
667 struct SearchResult {
668     ImapStatus status;
669     string value;
670     long[] ids;
671 }
672 
673 @SILdoc("Process the data that server sent due to IMAP SEARCH client request.")
674 SearchResult responseEsearch(Session session, int tag) {
675     return responseSearch(session, tag, "* ESEARCH ");
676 }
677 
678 @SILdoc("Process the data that server sent due to IMAP SEARCH client request.")
679 SearchResult responseSearch(Session session, int tag, string searchToken = "* SEARCH ") {
680     import std.algorithm : filter, map, each;
681     import std.array : array, Appender;
682     import std.string : startsWith, strip, isNumeric, splitLines, split;
683     import std.conv : to;
684 
685     SearchResult ret;
686     Appender!(long[])ids;
687     auto r = session.responseGeneric(tag);
688     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
689         return SearchResult(r.status, r.value);
690 
691     auto lines = r.value.splitLines.filter!(line => line.strip.startsWith(searchToken)).array
692         .map!(line => line[searchToken.length - 1 .. $]
693             .strip
694             .split
695             .map!(token => token.strip)
696             .filter!(token => token.isNumeric)
697             .map!(token => token.to!long));
698 
699     lines.each!(line => line.each!(val => ids.put(val)));
700     return SearchResult(r.status, r.value, ids.data);
701 }
702 
703 @SILdoc("Process the data that server sent due to IMAP ESEARCH (really multi-search) client request.")
704 SearchResult responseMultiSearch(Session session, int tag) {
705     import std.algorithm : filter, map, each;
706     import std.array : array, Appender;
707     import std.string : startsWith, strip, isNumeric, splitLines, split;
708     import std.conv : to;
709 
710     SearchResult ret;
711     Appender!(long[])ids;
712     enum SearchToken = "* ESEARCH ";
713     auto r = session.responseGeneric(tag);
714     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
715         return SearchResult(r.status, r.value);
716 
717     auto lines = r.value.splitLines.filter!(line => line.strip.startsWith(SearchToken)).array
718         .map!(line => line[SearchToken.length - 1 .. $]
719             .strip
720             .split
721             .map!(token => token.strip)
722             .filter!(token => token.isNumeric)
723             .map!(token => token.to!long));
724 
725     lines.each!(line => line.each!(val => ids.put(val)));
726     return SearchResult(r.status, r.value, ids.data);
727 }
728 
729 @SILdoc("Process the data that server sent due to IMAP FETCH FAST client request.")
730 ImapResult responseFetchFast(Session session, int tag) {
731     auto r = session.responseGeneric(tag);
732     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
733         return ImapResult(r.status, r.value);
734     return r;
735 }
736 
737 
738 ///
739 struct FlagResult {
740     ImapStatus status;
741     string value;
742     long[]  ids;
743     ImapFlag[] flags;
744 }
745 
746 @SILdoc("Process the data that server sent due to IMAP FETCH FLAGS client request.")
747 FlagResult responseFetchFlags(Session session, Tag tag) {
748     import std.experimental.logger : infof;
749     import std.string : splitLines, join, startsWith, toUpper, strip, split, isNumeric, indexOf;
750     import std.algorithm : filter, map, canFind;
751     import std.array : array;
752     import std.traits : EnumMembers;
753     import std.conv : to;
754     import std.exception : enforce;
755 
756     enum FlagsToken = "* FLAGS ";
757 
758     long[] ids;
759     ImapFlag[] flags;
760     auto r = session.responseGeneric(tag);
761     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
762         return FlagResult(r.status, r.value);
763 
764     auto lines = r.value
765         .splitLines
766         .map!(line => line.strip)
767         .array
768         .filter!(line => line.startsWith("* ") && line.canFind("FETCH (FLAGS ("))
769         .array
770         .map!(line => line["* ".length .. $].strip.split
771             .map!(token => token.strip)
772             .array);
773 
774     foreach (line; lines) {
775         enforce(line[0].isNumeric);
776         ids ~= line[0].to!long;
777         enforce(line[1] == "FETCH");
778         enforce(line[2].startsWith("(FLAGS"));
779         auto token = line[3 .. $].join;
780         auto i = token.indexOf(")");
781         enforce(i != -1);
782         token = token[0 .. i + 1].stripBrackets;
783         bool isKnown = false;
784         static foreach (F; EnumMembers!ImapFlag) {
785             {
786                 enum name = F.to!string;
787                 enum udas = __traits(getAttributes, __traits(getMember, ImapFlag, name));
788                 static if (udas.length > 0) {
789                     if (token.to!string == udas[0].to!string) {
790                         flags ~= F;
791                         isKnown = true;
792                     }
793                 }
794             }
795         }
796         if (!isKnown && session.options.debugMode) {
797             infof("unknown flag: %s", token);
798         }
799     }
800     return FlagResult(ImapStatus.ok, r.value, ids, flags);
801 }
802 
803 ///
804 ImapResult responseFetchDate(Session session, Tag tag) {
805     auto r = session.responseGeneric(tag);
806     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
807         return ImapResult(r.status, r.value);
808     return r;
809 }
810 
811 ///
812 struct ResponseSize {
813     ImapStatus status;
814     string value;
815 }
816 
817 
818 @SILdoc("Process the data that server sent due to IMAP FETCH RFC822.SIZE client request.")
819 ImapResult responseFetchSize(Session session, Tag tag) {
820     auto r = session.responseGeneric(tag);
821     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
822         return ImapResult(r.status, r.value);
823     return r;
824 }
825 
826 
827 @SILdoc("Process the data that server sent due to IMAP FETCH BODYSTRUCTURE client request.")
828 ImapResult responseFetchStructure(Session session, int tag) {
829     auto r = session.responseGeneric(tag);
830     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
831         return ImapResult(r.status, r.value);
832     return r;
833 }
834 
835 ///
836 struct BodyResponse {
837     import arsd.email : MimePart, IncomingEmailMessage;
838     ImapStatus status;
839     string value;
840     string[] lines;
841     IncomingEmailMessage message;
842     MimeAttachment[] attachments;
843 }
844 
845 struct MimeAttachment {
846     string type;
847     string filename;
848     string content;
849     string id;
850 }
851 
852 @SILdoc("SIL cannot handle void[], so ...")
853 MimeAttachment[] attachments(IncomingEmailMessage message) {
854     import std.algorithm : map;
855     import std.array : array;
856     return message.attachments.map!(a => MimeAttachment(a.type, a.filename, cast(string) a.content.idup, a.id)).array;
857 }
858 
859 ///
860 BodyResponse responseFetchBody(Session session, Tag tag) {
861     import arsd.email : MimePart, IncomingEmailMessage;
862     import std.string : splitLines, join;
863     import std.exception : enforce;
864     import std.range : front;
865     auto r = session.responseGeneric(tag);
866     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
867         return BodyResponse(r.status, r.value);
868     auto parsed = r.value.extractLiterals;
869 
870     if (parsed[1].length >= 2)
871         return BodyResponse(r.status, r.value.decodeMimeHeader, parsed[1]);
872     auto bodyText = (parsed[1].length == 0) ? r.value.decodeMimeHeader : parsed[1][0].decodeMimeHeader;
873     auto bodyLines = bodyText.splitLines;
874     if (bodyLines.length > 0 && bodyLines.front.length == 0)
875         bodyLines = bodyLines[1 .. $];
876     // return BodyResponse(r.status,r.value,new IncomingEmailMessage(bodyLines));
877     auto bodyLinesEmail = cast(immutable(ubyte)[][]) bodyLines.idup;
878     auto incomingEmail = new IncomingEmailMessage(bodyLinesEmail, false);
879     auto attach = attachments(incomingEmail);
880     return BodyResponse(r.status, r.value, bodyLines, incomingEmail, attach);
881 }
882 /+
883 //  Process the data that server sent due to IMAP FETCH BODY[] client request,
884 //   ie. FETCH BODY[HEADER], FETCH BODY[TEXT], FETCH BODY[HEADER.FIELDS (<fields>)], FETCH BODY[<part>].
885 ImapResult fetchBody(Session session, Tag tag)
886 {
887     import std.experimental.logger : infof;
888     import std.string : splitLines, join, startsWith, toUpper, strip, split;
889     import std.algorithm : filter, map;
890     import std.array : array;
891     import std.traits: EnumMembers;
892     import std.conv : to;
893 
894     enum FlagsToken = "* FLAGS ";
895 
896     Flag[] flags;
897     auto r = session.responseGeneric(tag);
898     if (r.status == ImapStatus.unknown || r.status == ImapStatus.bye)
899         return ImapResult(r.status,r.value);
900 
901     auto lines = res.value
902                     .splitLines
903                     .filter!(line => line.startsWith(CapabilityToken) && line.length > CapabilityToken.length)
904                     .array
905                     .map!(line => line[CapabilityToken.length ..$].strip.split
906                                 .map!(token => token.strip.stripBrackets.split)
907 }
908 +/
909 
910 
911 ///
912 bool isTagged(ImapStatus status) {
913     return (status == ImapStatus.ok) || (status == ImapStatus.bad) || (status == ImapStatus.no);
914 }
915 
916 @SILdoc("Process the data that server sent due to IMAP IDLE client request.")
917 ImapResult responseIdle(Session session, Tag tag) {
918     import std.experimental.logger : tracef;
919     import std.string : toUpper, startsWith, strip;
920     import std.algorithm : canFind;
921     import std.typecons : Tuple;
922     Tuple!(Status, string)result;
923     // untagged:       "\\* [[:digit:]]+ ([[:graph:]]*)[^[:cntrl:]]*\r+\n+",
924     while (true) {
925         result = session.receiveResponse(session.options.keepAlive, false);
926         result[1] = result[1].strip;
927         // if (result[0] == Status.failure)
928         // return ImapResult(ImapStatus.unknown,result[1]);
929 
930         if (session.options.debugMode) tracef("S (%s): %s", session.socket, result[1]);
931         auto bufUpper = result[1].toUpper;
932 
933         if (checkBye(result[1]))
934             return ImapResult(ImapStatus.bye, result[1]);
935 
936         auto checkedTag = session.checkTag(result[1], tag);
937         if (checkedTag == ImapStatus.bad || ImapStatus.no) {
938             return ImapResult(checkedTag, result[1]);
939         }
940         if (checkedTag == ImapStatus.ok && bufUpper.canFind("IDLE TERMINATED"))
941             return ImapResult(ImapStatus.untagged, result[1]);
942 
943         bool hasNewInfo = (result[1].startsWith("* ") && result[1].canFind("\n"));
944         if (hasNewInfo) {
945             if (session.options.wakeOnAny)
946                 break;
947             if (bufUpper.canFind("RECENT") || bufUpper.canFind("EXISTS"))
948                 break;
949         }
950     }
951 
952     return ImapResult(ImapStatus.untagged, result[1]);
953 }
954 
955 bool isControlChar(char c) {
956     return c >= 1 && c < 32;
957 }
958 
959 bool isSpecialChar(char c) {
960     import std.algorithm : canFind;
961     return " ()%[".canFind(c);
962 }
963 
964 bool isWhiteSpace(char c) {
965     return (c == '\t') || (c == '\r') || (c == '\n');
966 }
967 
968 enum Backslash = '\\';
969 enum LSquare = '[';
970 enum RSquare = ']';
971 enum DoubleQuote = '"';
972 
973 struct LiteralInfo {
974     ptrdiff_t i;
975     ptrdiff_t j;
976     ptrdiff_t length;
977 }
978 
979 LiteralInfo findLiteral(string buf) {
980     import std.string : indexOf, isNumeric;
981     import std.conv : to;
982     ptrdiff_t i, j, len;
983     bool hasLength;
984     do
985     {
986         i = buf[j .. $].indexOf("{");
987         i = (i == -1) ? i : i + j;
988         j = ((i == -1) || (i + 1 == buf.length)) ? -1 : buf[i + 1 .. $].indexOf("}");
989         j = (j == -1) ? j : (i + 1) + j;
990         hasLength = (i != -1 && j != -1) && buf[i + 1 .. j].isNumeric;
991         len = hasLength ? buf[i + 1 .. j].to!ptrdiff_t : -1;
992     } while (i != -1 && j != -1 && !hasLength);
993     return LiteralInfo(i, j, len);
994 }
995 
996 auto extractLiterals(string buf) {
997     import std.array : Appender;
998     import std.typecons : tuple;
999     import std.stdio;
1000 
1001     Appender!(string[])nonLiterals;
1002     Appender!(string[])literals;
1003     LiteralInfo literalInfo;
1004     do
1005     {
1006         literalInfo = findLiteral(buf);
1007         if (literalInfo.length > 0 && buf.length > literalInfo.j + 1 + literalInfo.length) {
1008             string literal = buf[literalInfo.j + 1 .. literalInfo.j + 1 + literalInfo.length];
1009             literals.put(literal);
1010             nonLiterals.put(buf[0 .. literalInfo.i]);
1011             buf = buf[literalInfo.j + 2 + literalInfo.length .. $];
1012         } else {
1013             nonLiterals.put(buf);
1014             buf.length = 0;
1015         }
1016     } while (buf.length > 0 && literalInfo.length > 0);
1017     return tuple(nonLiterals.data, literals.data);
1018 }
1019 
1020 /+
1021 "* 51045 FETCH (UID 70290 BODY[TEXT] {67265}
1022 
1023 )
1024 D1009 OK Completed (0.002 sec)
1025 +/
1026 
1027 // See RFC2047 - https://tools.ietf.org/html/rfc2047
1028 //
1029 // In summary, message headers may be encoded:
1030 // - grammar: '=?' CHARSET '?' ENCODING '?' TEXT '?='
1031 //   - CHARSET and ENCODING are case insensitive.
1032 //   - CHARSET can be any of the MIME charsets for a "text/plain" body part, or any character set name
1033 //     for a MIME text/plain content-type.
1034 //   - ENCODING is either 'Q' or 'B'.
1035 //     - B is base64.
1036 //     - Q is (similar to) quoted-printable.
1037 //
1038 // NOTE: Assumes strings are well formed after decoded as they're just casted from ubyte[] to either
1039 // UTF-8 string or Latin1String.
1040 
1041 string decodeMimeHeader(string header) {
1042     import std.array : split;
1043     import std.base64 : Base64;
1044     import std.exception : enforce;
1045     import std.format : format;
1046     import std.string : toUpper;
1047 
1048     import arsd.email : decodeQuotedPrintable;
1049 
1050     string[] elements = header.split("?");
1051     if (elements.length != 5 || elements[0] != "=" || elements[4] != "=") {
1052         return header;
1053     }
1054 
1055     // Utility to decode first (Q or B) and then convert from Latin1 to string.
1056     string decodeMimeWithEncodingFromLatin1(alias decoder)() {
1057         import std.encoding : Latin1String, transcode;
1058 
1059         string result;
1060         transcode(cast(Latin1String) decoder(elements[3]), result);
1061         return result;
1062     }
1063 
1064     // For the charset we only support UTF-8 and ISO-8859-1 for now.
1065     string decodeMimeWithEncoding(alias decoder)() {
1066         switch (elements[1].toUpper) {
1067             case "UTF-8":
1068                 return cast(string) decoder(elements[3]);
1069 
1070             case "ISO-8859-1":
1071                 return decodeMimeWithEncodingFromLatin1!(decoder)();
1072 
1073             default:
1074                 enforce(false, format!"Unsupported charset in MIME header: '%s'"(elements[1].toUpper));
1075         }
1076         assert(0);
1077     }
1078 
1079     // The encoding is either 'Q' or 'B'.
1080     switch (elements[2].toUpper) {
1081         case "Q":
1082             return decodeMimeWithEncoding!decodeQuotedPrintable();
1083 
1084         case "B":
1085             return decodeMimeWithEncoding!(Base64.decode)();
1086 
1087         default:
1088             enforce(false, format!"MIME header encoding should be 'Q' or 'B': '%s'"(elements[2].toUpper));
1089     }
1090 
1091     assert(0);
1092 }
1093 
1094 unittest {
1095     // Taken from searches on the net, then converted/confirmed using Python:
1096     //
1097     // Python 3.9.1 (default, Dec 13 2020, 11:55:53)
1098     // [GCC 10.2.0] on linux
1099     // Type "help", "copyright", "credits" or "license" for more information.
1100     // >>> from email.header import decode_header
1101     // >>> decode_header("=?UTF-8?Q?This is a horsey: =F0=9F=90=8E?=")[0][0].decode('utf-8')
1102     // 'This is a horsey: 🐎'
1103     // >>> decode_header("=?UTF-8?B?VGhpcyBpcyBhIGhvcnNleTog8J+Qjg==?=")[0][0].decode('utf-8')
1104     // 'This is a horsey: 🐎'
1105     // >>> decode_header("=?iso-8859-1?Q?WG=3A_Mobilit=E4t_verschlechtert_--=3E_174?=")[0][0].decode('iso-8859-1')
1106     // 'WG: Mobilität verschlechtert --> 174'
1107 
1108     void decodeTest(string encoded, string expected) {
1109         import std.stdio : writeln;
1110 
1111         auto got = decodeMimeHeader(encoded);
1112         if (got != expected) {
1113             writeln("FAILED TO DECODE MIME HEADER:");
1114             writeln("input: ", encoded);
1115             writeln("expecting: ", expected);
1116             writeln("got:       ", got);
1117         }
1118         assert(got == expected);
1119     }
1120 
1121     decodeTest("=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?=",
1122                "Keld Jørn Simonsen");
1123     decodeTest("=?ISO-8859-1?Q?Informaci=F3n_Apartamento_a_la_Venta?=",
1124                "Información Apartamento a la Venta");
1125     decodeTest("=?iso-8859-1?Q?WG=3A_Mobilit=E4t_verschlechtert_--=3E_174?=",
1126                "WG: Mobilität verschlechtert --> 174");
1127 
1128     decodeTest("=?utf-8?Q?Watch_now_=e2=80=93_We_Need_to_Talk_About_New_Zealand?=",
1129                "Watch now – We Need to Talk About New Zealand");
1130     decodeTest("=?UTF-8?Q?This is a horsey: =F0=9F=90=8E?=",
1131                "This is a horsey: 🐎");
1132 
1133     decodeTest("=?UTF-8?B?VGhpcyBpcyBhIGhvcnNleTog8J+Qjg==?=",
1134                "This is a horsey: 🐎");
1135     decodeTest("=?utf-8?B?2LPZhNin2YU=?=",
1136                "سلام");
1137 }