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 }