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 +/