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