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