1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4// This file implements test IMAP servers 5 6var EXPORTED_SYMBOLS = [ 7 "imapDaemon", 8 "imapMailbox", 9 "imapMessage", 10 "IMAP_RFC3501_handler", 11 "configurations", 12 "mixinExtension", 13 "IMAP_GMAIL_extension", 14 "IMAP_MOVE_extension", 15 "IMAP_CUSTOM_extension", 16 "IMAP_RFC2197_extension", 17 "IMAP_RFC2342_extension", 18 "IMAP_RFC3348_extension", 19 "IMAP_RFC4315_extension", 20 "IMAP_RFC5258_extension", 21 "IMAP_RFC2195_extension", 22]; 23 24// IMAP DAEMON ORGANIZATION 25// ------------------------ 26// The large numbers of RFCs all induce some implicit assumptions as to the 27// organization of an IMAP server. Ideally, we'd like to be as inclusive as 28// possible so that we can guarantee that it works for every type of server. 29// Unfortunately, such all-accepting setups make generic algorithms hard to 30// use; given their difficulty in a generic framework, it seems unlikely that 31// a server would implement such characteristics. It also seems likely that 32// if mailnews had a problem with the implementation, then most clients would 33// see similar problems, so as to make the server widely unusable. In any 34// case, if someone complains about not working on bugzilla, it can be added 35// to the test suite. 36// So, with that in mind, this is the basic layout of the daemon: 37// DAEMON 38// + Namespaces: parentless mailboxes whose names are the namespace name. The 39// type of the namespace is specified by the type attribute. 40// + Mailboxes: imapMailbox objects with several properties. If a mailbox 41// | | property begins with a '_', then it should not be serialized because 42// | | it can be discovered from other means; in particular, a '_' does not 43// | | necessarily mean that it is a private property that should not be 44// | | accessed. The parent of a top-level mailbox is null, not "". 45// | + I18N names: RFC 3501 specifies a modified UTF-7 form for names. 46// | | However, a draft RFC makes the names UTF-8; it is expected to be 47// | | completed and implemented "soon". Therefore, the correct usage is 48// | | to specify the mailbox names as one normally does in JS and the 49// | | protocol will take care of conversion itself. 50// | + Case-sensitivity: RFC 3501 takes no position on this issue, only that 51// | | a case-insensitive server must treat the base-64 parts of mailbox 52// | | names as case-sensitive. The draft UTF8 RFC says nothing on this 53// | | topic, but Crispin recommends using Unicode case-insensitivity. We 54// | | therefore treat names in such manner (if the case-insensitive flag 55// | | is set), in technical violation of RFC 3501. 56// | + Flags: Flags are (as confirmed by Crispin) case-insensitive. Internal 57// | flag equality, though, uses case-sensitive checks. Therefore they 58// | should be normalized to a title-case form (e.g., \Noselect). 59// + Synchronization: On certain synchronizing commands, the daemon will call 60// | a synchronizing function to allow manipulating code the chance to 61// | perform various (potentially expensive) actions. 62// + Messages: A message is represented internally as an annotated URI. 63 64var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 65const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); 66var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import( 67 "resource://testing-common/mailnews/Auth.jsm" 68); 69 70function imapDaemon(flags, syncFunc) { 71 this._flags = flags; 72 73 this.namespaces = []; 74 this.idResponse = "NIL"; 75 this.root = new imapMailbox("", null, { type: IMAP_NAMESPACE_PERSONAL }); 76 this.uidvalidity = Math.round(Date.now() / 1000); 77 this.inbox = new imapMailbox("INBOX", null, this.uidvalidity++); 78 this.root.addMailbox(this.inbox); 79 this.namespaces.push(this.root); 80 this.syncFunc = syncFunc; 81 // This can be used to cause the artificial failure of any given command. 82 this.commandToFail = ""; 83 // This can be used to simulate timeouts on large copies 84 this.copySleep = 0; 85} 86imapDaemon.prototype = { 87 synchronize(mailbox, update) { 88 if (this.syncFunc) { 89 this.syncFunc.call(null, this); 90 } 91 if (update) { 92 for (var message of mailbox._messages) { 93 message.recent = false; 94 } 95 } 96 }, 97 getNamespace(name) { 98 for (var namespace of this.namespaces) { 99 if ( 100 name.indexOf(namespace.name) == 0 && 101 name[namespace.name.length] == namespace.delimiter 102 ) { 103 return namespace; 104 } 105 } 106 return this.root; 107 }, 108 createNamespace(name, type) { 109 var newbox = this.createMailbox(name, { type }); 110 this.namespaces.push(newbox); 111 }, 112 getMailbox(name) { 113 if (name == "") { 114 return this.root; 115 } 116 // INBOX is case-insensitive, no matter what 117 if (name.toUpperCase().startsWith("INBOX")) { 118 name = "INBOX" + name.substr(5); 119 } 120 // We want to find a child who has the same name, but we don't quite know 121 // what the delimiter is. The convention is that different namespaces use a 122 // name starting with '#', so that's how we'll work it out. 123 let mailbox; 124 if (name.startsWith("#")) { 125 for (mailbox of this.root._children) { 126 if ( 127 mailbox.name.indexOf(name) == 0 && 128 name[mailbox.name.length] == mailbox.delimiter 129 ) { 130 break; 131 } 132 } 133 if (!mailbox) { 134 return null; 135 } 136 137 // Now we continue like normal 138 let names = name.split(mailbox.delimiter); 139 names.splice(0, 1); 140 for (let part of names) { 141 mailbox = mailbox.getChild(part); 142 if (!mailbox || mailbox.nonExistent) { 143 return null; 144 } 145 } 146 } else { 147 // This is easy, just split it up using the inbox's delimiter 148 let names = name.split(this.inbox.delimiter); 149 mailbox = this.root; 150 151 for (let part of names) { 152 mailbox = mailbox.getChild(part); 153 if (!mailbox || mailbox.nonExistent) { 154 return null; 155 } 156 } 157 } 158 return mailbox; 159 }, 160 createMailbox(name, oldBox) { 161 var namespace = this.getNamespace(name); 162 if (namespace.name != "") { 163 name = name.substring(namespace.name.length + 1); 164 } 165 var prefixes = name.split(namespace.delimiter); 166 var subName; 167 if (prefixes[prefixes.length - 1] == "") { 168 subName = prefixes.splice(prefixes.length - 2, 2)[0]; 169 } else { 170 subName = prefixes.splice(prefixes.length - 1, 1)[0]; 171 } 172 var box = namespace; 173 for (var component of prefixes) { 174 box = box.getChild(component); 175 // Yes, we won't autocreate intermediary boxes 176 if (box == null || box.flags.includes("\\NoInferiors")) { 177 return false; 178 } 179 } 180 // If this is an imapMailbox... 181 if (oldBox && oldBox._children) { 182 // Only delete now so we don't screw ourselves up if creation fails 183 this.deleteMailbox(oldBox); 184 oldBox._parent = box == this.root ? null : box; 185 let newBox = new imapMailbox(subName, box, this.uidvalidity++); 186 newBox._messages = oldBox._messages; 187 box.addMailbox(newBox); 188 189 // And if oldBox is an INBOX, we need to recreate that 190 if (oldBox.name == "INBOX") { 191 this.inbox = new imapMailbox("INBOX", null, this.uidvalidity++); 192 this.root.addMailbox(this.inbox); 193 } 194 oldBox.name = subName; 195 } else if (oldBox) { 196 // oldBox is a regular {} object, so it contains mailbox data but is not 197 // a mailbox itself. Pass it into the constructor and let that deal with 198 // it... 199 let childBox = new imapMailbox( 200 subName, 201 box == this.root ? null : box, 202 oldBox 203 ); 204 box.addMailbox(childBox); 205 // And return the new mailbox, since this is being used by people setting 206 // up the daemon. 207 return childBox; 208 } else { 209 var creatable = hasFlag(this._flags, IMAP_FLAG_NEEDS_DELIMITER) 210 ? name[name.length - 1] == namespace.delimiter 211 : true; 212 let childBox = new imapMailbox(subName, box == this.root ? null : box, { 213 flags: creatable ? [] : ["\\NoInferiors"], 214 uidvalidity: this.uidvalidity++, 215 }); 216 box.addMailbox(childBox); 217 } 218 return true; 219 }, 220 deleteMailbox(mailbox) { 221 if (mailbox._children.length == 0) { 222 // We don't preserve the subscribed state for deleted mailboxes 223 var parentBox = mailbox._parent == null ? this.root : mailbox._parent; 224 parentBox._children.splice(parentBox._children.indexOf(mailbox), 1); 225 } else { 226 // clear mailbox 227 mailbox._messages = []; 228 mailbox.flags.push("\\Noselect"); 229 } 230 }, 231}; 232 233function imapMailbox(name, parent, state) { 234 this.name = name; 235 this._parent = parent; 236 this._children = []; 237 this._messages = []; 238 this._updates = []; 239 240 // Shorthand for uidvalidity 241 if (typeof state == "number") { 242 this.uidvalidity = state; 243 state = {}; 244 } 245 246 if (!state) { 247 state = {}; 248 } 249 250 for (var prop in state) { 251 this[prop] = state[prop]; 252 } 253 254 this.setDefault("subscribed", false); 255 this.setDefault("nonExistent", false); 256 this.setDefault("delimiter", "/"); 257 this.setDefault("flags", []); 258 this.setDefault("specialUseFlag", ""); 259 this.setDefault("uidnext", 1); 260 this.setDefault("msgflags", [ 261 "\\Seen", 262 "\\Answered", 263 "\\Flagged", 264 "\\Deleted", 265 "\\Draft", 266 ]); 267 this.setDefault("permflags", [ 268 "\\Seen", 269 "\\Answered", 270 "\\Flagged", 271 "\\Deleted", 272 "\\Draft", 273 "\\*", 274 ]); 275} 276imapMailbox.prototype = { 277 setDefault(prop, def) { 278 this[prop] = prop in this ? this[prop] : def; 279 }, 280 addMailbox(mailbox) { 281 this._children.push(mailbox); 282 }, 283 getChild(name) { 284 for (var mailbox of this._children) { 285 if (name == mailbox.name) { 286 return mailbox; 287 } 288 } 289 return null; 290 }, 291 matchKids(pattern) { 292 if (pattern == "") { 293 return this._parent ? this._parent.matchKids("") : [this]; 294 } 295 296 var portions = pattern.split(this.delimiter); 297 var matching = [this]; 298 for (var folder of portions) { 299 if (folder.length == 0) { 300 continue; 301 } 302 303 let generator = folder.includes("*") ? "allChildren" : "_children"; 304 let possible = matching.reduce(function(arr, elem) { 305 return arr.concat(elem[generator]); 306 }, []); 307 308 if (folder == "*" || folder == "%") { 309 matching = possible; 310 continue; 311 } 312 313 let parts = folder.split(/[*%]/).filter(function(str) { 314 return str.length > 0; 315 }); 316 matching = possible.filter(function(mailbox) { 317 let index = 0, 318 name = mailbox.fullName; 319 for (var part of parts) { 320 index = name.indexOf(part, index); 321 if (index == -1) { 322 return false; 323 } 324 } 325 return true; 326 }); 327 } 328 return matching; 329 }, 330 get fullName() { 331 return ( 332 (this._parent ? this._parent.fullName + this.delimiter : "") + this.name 333 ); 334 }, 335 get displayName() { 336 let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService( 337 Ci.nsICharsetConverterManager 338 ); 339 // Escape backslash and double-quote with another backslash before encoding. 340 return manager.unicodeToMutf7(this.fullName.replace(/([\\"])/g, "\\$1")); 341 }, 342 get allChildren() { 343 return this._children.reduce(function(arr, elem) { 344 return arr.concat(elem._allChildrenInternal); 345 }, []); 346 }, 347 get _allChildrenInternal() { 348 return this._children.reduce( 349 function(arr, elem) { 350 return arr.concat(elem._allChildrenInternal); 351 }, 352 [this] 353 ); 354 }, 355 addMessage(message) { 356 this._messages.push(message); 357 if (message.uid >= this.uidnext) { 358 this.uidnext = message.uid + 1; 359 } 360 if (!this._updates.includes("EXISTS")) { 361 this._updates.push("EXISTS"); 362 } 363 if ("__highestuid" in this && message.uid > this.__highestuid) { 364 this.__highestuid = message.uid; 365 } 366 }, 367 get _highestuid() { 368 if ("__highestuid" in this) { 369 return this.__highestuid; 370 } 371 var highest = 0; 372 for (var message of this._messages) { 373 if (message.uid > highest) { 374 highest = message.uid; 375 } 376 } 377 this.__highestuid = highest; 378 return highest; 379 }, 380 expunge() { 381 var response = ""; 382 for (var i = 0; i < this._messages.length; i++) { 383 if (this._messages[i].flags.includes("\\Deleted")) { 384 response += "* " + (i + 1) + " EXPUNGE\0"; 385 this._messages.splice(i--, 1); 386 } 387 } 388 if (response.length > 0) { 389 delete this.__highestuid; 390 } 391 return response; 392 }, 393}; 394 395function imapMessage(URI, uid, flags) { 396 this._URI = URI; 397 this.uid = uid; 398 this.size = 0; 399 this.flags = []; 400 for (let flag in flags) { 401 this.flags.push(flag); 402 } 403 this.recent = false; 404} 405imapMessage.prototype = { 406 get channel() { 407 return Services.io.newChannel( 408 this._URI, 409 null, 410 null, 411 null, 412 Services.scriptSecurityManager.getSystemPrincipal(), 413 null, 414 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, 415 Ci.nsIContentPolicy.TYPE_OTHER 416 ); 417 }, 418 setFlag(flag) { 419 if (!this.flags.includes(flag)) { 420 this.flags.push(flag); 421 } 422 }, 423 // This allows us to simulate servers that approximate the rfc822 size. 424 setSize(size) { 425 this.size = size; 426 }, 427 clearFlag(flag) { 428 let index = this.flags.indexOf(flag); 429 if (index != -1) { 430 this.flags.splice(index, 1); 431 } 432 }, 433 getText(start, length) { 434 if (!start) { 435 start = 0; 436 } 437 if (!length) { 438 length = -1; 439 } 440 var channel = this.channel; 441 var istream = channel.open(); 442 var bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( 443 Ci.nsIBinaryInputStream 444 ); 445 bstream.setInputStream(istream); 446 var str = bstream.readBytes(start); 447 if (str.length != start) { 448 throw new Error("Erm, we didn't just pass through 8-bit"); 449 } 450 length = length == -1 ? istream.available() : length; 451 if (length > istream.available()) { 452 length = istream.available(); 453 } 454 str = bstream.readBytes(length); 455 return str; 456 }, 457 458 get _partMap() { 459 if (this.__partMap) { 460 return this.__partMap; 461 } 462 var partMap = {}; 463 var emitter = { 464 startPart(partNum, headers) { 465 var imapPartNum = partNum.replace("$", ""); 466 // If there are multiple imap parts that this represents, we'll 467 // overwrite with the latest. This is what we want (most deeply nested). 468 partMap[imapPartNum] = [partNum, headers]; 469 }, 470 }; 471 MimeParser.parseSync(this.getText(), emitter, { 472 bodyformat: "none", 473 stripcontinuations: false, 474 }); 475 return (this.__partMap = partMap); 476 }, 477 getPartHeaders(partNum) { 478 return this._partMap[partNum][1]; 479 }, 480 getPartBody(partNum) { 481 var body = ""; 482 var emitter = { 483 deliverPartData(partNum, data) { 484 body += data; 485 }, 486 }; 487 var mimePartNum = this._partMap[partNum][0]; 488 MimeParser.parseSync(this.getText(), emitter, { 489 pruneat: mimePartNum, 490 bodyformat: "raw", 491 }); 492 return body; 493 }, 494}; 495 496// IMAP FLAGS 497// If you don't specify any flag, no flags are set. 498 499/** 500 * This flag represents whether or not CREATE hierarchies need a delimiter. 501 * 502 * If this flag is off, <tt>CREATE a<br />CREATE a/b</tt> fails where 503 * <tt>CREATE a/<br />CREATE a/b</tt> would succeed (assuming the delimiter is 504 * '/'). 505 */ 506var IMAP_FLAG_NEEDS_DELIMITER = 2; 507 508function hasFlag(flags, flag) { 509 return (flags & flag) == flag; 510} 511 512// IMAP Namespaces 513var IMAP_NAMESPACE_PERSONAL = 0; 514// var IMAP_NAMESPACE_OTHER_USERS = 1; 515// var IMAP_NAMESPACE_SHARED = 2; 516 517// IMAP server helpers 518var IMAP_STATE_NOT_AUTHED = 0; 519var IMAP_STATE_AUTHED = 1; 520var IMAP_STATE_SELECTED = 2; 521 522function parseCommand(text, partial) { 523 var args = []; 524 var current = args; 525 var stack = []; 526 if (partial) { 527 args = partial.args; 528 current = partial.current; 529 stack = partial.stack; 530 current.push(partial.text); 531 } 532 var atom = ""; 533 while (text.length > 0) { 534 let c = text[0]; 535 536 if (c == '"') { 537 let index = 1; 538 let s = ""; 539 while (index < text.length && text[index] != '"') { 540 if (text[index] == "\\") { 541 index++; 542 if (text[index] != '"' && text[index] != "\\") { 543 throw new Error("Expected quoted character"); 544 } 545 } 546 s += text[index++]; 547 } 548 if (index == text.length) { 549 throw new Error("Expected DQUOTE"); 550 } 551 current.push(s); 552 text = text.substring(index + 1); 553 continue; 554 } else if (c == "{") { 555 let end = text.indexOf("}"); 556 if (end == -1) { 557 throw new Error("Expected CLOSE_BRACKET"); 558 } 559 if (end + 1 != text.length) { 560 throw new Error("Expected CRLF"); 561 } 562 let length = parseInt(text.substring(1, end)); 563 // Usable state 564 // eslint-disable-next-line no-throw-literal 565 throw { length, current, args, stack, text: "" }; 566 } else if (c == "(") { 567 stack.push(current); 568 current = []; 569 } else if (c == ")") { 570 if (atom.length > 0) { 571 current.push(atom); 572 atom = ""; 573 } 574 let hold = current; 575 current = stack.pop(); 576 if (current == undefined) { 577 throw new Error("Unexpected CLOSE_PAREN"); 578 } 579 current.push(hold); 580 } else if (c == " ") { 581 if (atom.length > 0) { 582 current.push(atom); 583 atom = ""; 584 } 585 } else if ( 586 text.toUpperCase().startsWith("NIL") && 587 (text.length == 3 || text[3] == " ") 588 ) { 589 current.push(null); 590 text = text.substring(4); 591 continue; 592 } else { 593 atom += c; 594 } 595 text = text.substring(1); 596 } 597 if (stack.length != 0) { 598 throw new Error("Expected CLOSE_PAREN!"); 599 } 600 if (atom.length > 0) { 601 args.push(atom); 602 } 603 return args; 604} 605 606function formatArg(argument, spec) { 607 // Get NILs out of the way quickly 608 var nilAccepted = false; 609 if (spec.startsWith("n") && spec[1] != "u") { 610 spec = spec.substring(1); 611 nilAccepted = true; 612 } 613 if (argument == null) { 614 if (!nilAccepted) { 615 throw new Error("Unexpected NIL!"); 616 } 617 618 return null; 619 } 620 621 // array! 622 if (spec.startsWith("(")) { 623 // typeof array is object. Don't ask me why. 624 if (!Array.isArray(argument)) { 625 throw new Error("Expected list!"); 626 } 627 // Strip the '(' and ')'... 628 spec = spec.substring(1, spec.length - 1); 629 // ... and apply to the rest 630 return argument.map(function(item) { 631 return formatArg(item, spec); 632 }); 633 } 634 635 // or! 636 var pipe = spec.indexOf("|"); 637 if (pipe > 0) { 638 var first = spec.substring(0, pipe); 639 try { 640 return formatArg(argument, first); 641 } catch (e) { 642 return formatArg(argument, spec.substring(pipe + 1)); 643 } 644 } 645 646 // By now, we know that the input should be generated from an atom or string. 647 if (typeof argument != "string") { 648 throw new Error("Expected argument of type " + spec + "!"); 649 } 650 651 if (spec == "atom") { 652 argument = argument.toUpperCase(); 653 } else if (spec == "mailbox") { 654 let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService( 655 Ci.nsICharsetConverterManager 656 ); 657 argument = manager.mutf7ToUnicode(argument); 658 } else if (spec == "string") { 659 // Do nothing 660 } else if (spec == "flag") { 661 argument = argument.toLowerCase(); 662 if ( 663 !("a" <= argument[0] && argument[0] <= "z") && 664 !("A" <= argument[0] && argument[0] <= "Z") 665 ) { 666 argument = argument[0] + argument[1].toUpperCase() + argument.substr(2); 667 } else { 668 argument = argument[0].toUpperCase() + argument.substr(1); 669 } 670 } else if (spec == "number") { 671 if (argument == parseInt(argument)) { 672 argument = parseInt(argument); 673 } 674 } else if (spec == "date") { 675 if ( 676 !/^\d{1,2}-[A-Z][a-z]{2}-\d{4}( \d{2}(:\d{2}){2} [+-]\d{4})?$/.test( 677 argument 678 ) 679 ) { 680 throw new Error("Expected date!"); 681 } 682 argument = new Date(Date.parse(argument.replace(/-(?!\d{4}$)/g, " "))); 683 } else { 684 throw new Error("Unknown spec " + spec); 685 } 686 687 return argument; 688} 689 690// IMAP TEST SERVERS 691// ----------------- 692// Because of IMAP and the LEMONADE RFCs, we have a myriad of different 693// server configurations that we should ideally be supporting. We handle them 694// by defining a core RFC 3501 implementation and then have different server 695// extensions subclass the server through functions below. However, we also 696// provide standard configurations for best handling. 697// Configurations: 698// * Barebones RFC 3501 699// * Cyrus 700// * UW IMAP 701// * Courier 702// * Exchange 703// * Dovecot 704// * Zimbra 705// * GMail 706// KNOWN DEVIATIONS FROM RFC 3501: 707// + The autologout timer is 3 minutes, not 30 minutes. A test with a logout 708// of 30 minutes would take a very long time if it failed. 709// + SEARCH (except for UNDELETED) and STARTTLS are not supported, 710// nor is all of FETCH. 711// + Concurrent mailbox access is probably compliant with a rather liberal 712// implementation of RFC 3501, although probably not what one would expect, 713// and certainly not what the Dovecot IMAP server tests expect. 714 715/* IMAP Fakeserver operates in a different manner than the rest of fakeserver 716 * because of some differences in the protocol. Commands are dispatched through 717 * onError, which parses the message into components. Like other fakeserver 718 * implementations, the command property will be called, but this time with an 719 * argument that is an array of data items instead of a string representing the 720 * rest of the line. 721 */ 722function IMAP_RFC3501_handler(daemon) { 723 this.kUsername = "user"; 724 this.kPassword = "password"; 725 this.kAuthSchemes = []; // Added by RFC2195 extension. Test may modify as needed. 726 this.kCapabilities = [ 727 /* "LOGINDISABLED", "STARTTLS", */ 728 "CLIENTID", 729 ]; // Test may modify as needed. 730 this.kUidCommands = ["FETCH", "STORE", "SEARCH", "COPY"]; 731 732 this._daemon = daemon; 733 this.closing = false; 734 this.dropOnStartTLS = false; 735 // map: property = auth scheme {String}, value = start function on this obj 736 this._kAuthSchemeStartFunction = {}; 737 738 this._enabledCommands = { 739 // IMAP_STATE_NOT_AUTHED 740 0: [ 741 "CAPABILITY", 742 "NOOP", 743 "LOGOUT", 744 "STARTTLS", 745 "CLIENTID", 746 "AUTHENTICATE", 747 "LOGIN", 748 ], 749 // IMAP_STATE_AUTHED 750 1: [ 751 "CAPABILITY", 752 "NOOP", 753 "LOGOUT", 754 "SELECT", 755 "EXAMINE", 756 "CREATE", 757 "DELETE", 758 "RENAME", 759 "SUBSCRIBE", 760 "UNSUBSCRIBE", 761 "LIST", 762 "LSUB", 763 "STATUS", 764 "APPEND", 765 ], 766 // IMAP_STATE_SELECTED 767 2: [ 768 "CAPABILITY", 769 "NOOP", 770 "LOGOUT", 771 "SELECT", 772 "EXAMINE", 773 "CREATE", 774 "DELETE", 775 "RENAME", 776 "SUBSCRIBE", 777 "UNSUBSCRIBE", 778 "LIST", 779 "LSUB", 780 "STATUS", 781 "APPEND", 782 "CHECK", 783 "CLOSE", 784 "EXPUNGE", 785 "SEARCH", 786 "FETCH", 787 "STORE", 788 "COPY", 789 "UID", 790 ], 791 }; 792 // Format explanation: 793 // atom -> UPPERCASE 794 // string -> don't touch! 795 // mailbox -> Apply ->UTF16 transformation with case-insensitivity stuff 796 // flag -> Titlecase (or \Titlecase, $Titlecase, etc.) 797 // date -> Make it a JSDate object 798 // number -> Make it a number, if possible 799 // ( ) -> list, apply flags as specified 800 // [ ] -> optional argument. 801 // x|y -> either x or y format. 802 // ... -> variable args, don't parse 803 this._argFormat = { 804 CAPABILITY: [], 805 NOOP: [], 806 LOGOUT: [], 807 STARTTLS: [], 808 CLIENTID: ["string", "string"], 809 AUTHENTICATE: ["atom", "..."], 810 LOGIN: ["string", "string"], 811 SELECT: ["mailbox"], 812 EXAMINE: ["mailbox"], 813 CREATE: ["mailbox"], 814 DELETE: ["mailbox"], 815 RENAME: ["mailbox", "mailbox"], 816 SUBSCRIBE: ["mailbox"], 817 UNSUBSCRIBE: ["mailbox"], 818 LIST: ["mailbox", "mailbox"], 819 LSUB: ["mailbox", "mailbox"], 820 STATUS: ["mailbox", "(atom)"], 821 APPEND: ["mailbox", "[(flag)]", "[date]", "string"], 822 CHECK: [], 823 CLOSE: [], 824 EXPUNGE: [], 825 SEARCH: ["atom", "..."], 826 FETCH: ["number", "atom|(atom|(atom))"], 827 STORE: ["number", "atom", "flag|(flag)"], 828 COPY: ["number", "mailbox"], 829 UID: ["atom", "..."], 830 }; 831 832 this.resetTest(); 833} 834IMAP_RFC3501_handler.prototype = { 835 resetTest() { 836 this._state = IMAP_STATE_NOT_AUTHED; 837 this._multiline = false; 838 this._nextAuthFunction = undefined; // should be in RFC2195_ext, but too lazy 839 }, 840 onStartup() { 841 this._state = IMAP_STATE_NOT_AUTHED; 842 return "* OK IMAP4rev1 Fakeserver started up"; 843 }, 844 845 // CENTRALIZED DISPATCH FUNCTIONS 846 847 // IMAP sends commands in the form of "tag command args", but fakeserver 848 // parsing tries to call the tag, which doesn't exist. Instead, we use this 849 // error method to do the actual command dispatch. Mailnews uses numbers for 850 // tags, which won't impede on actual commands. 851 onError(tag, realLine) { 852 this._tag = tag; 853 var space = realLine.indexOf(" "); 854 var command = space == -1 ? realLine : realLine.substring(0, space); 855 realLine = space == -1 ? "" : realLine.substring(space + 1); 856 857 // Now parse realLine into an array of atoms, etc. 858 try { 859 var args = parseCommand(realLine); 860 } catch (state) { 861 if (typeof state == "object") { 862 this._partial = state; 863 this._partial.command = command; 864 this._multiline = true; 865 return "+ More!"; 866 } 867 868 return this._tag + " BAD " + state; 869 } 870 871 // If we're here, we have a command with arguments. Dispatch! 872 return this._dispatchCommand(command, args); 873 }, 874 onMultiline(line) { 875 // A multiline arising form a literal being passed 876 if (this._partial) { 877 // There are two cases to be concerned with: 878 // 1. The CRLF is internal or end (we want more) 879 // 1a. The next line is the actual command stuff! 880 // 2. The CRLF is in the middle (rest of the line is args) 881 if (this._partial.length >= line.length + 2) { 882 // Case 1 883 this._partial.text += line + "\r\n"; 884 this._partial.length -= line.length + 2; 885 return undefined; 886 } else if (this._partial.length != 0) { 887 this._partial.text += line.substring(0, this._partial.length); 888 line = line.substring(this._partial.length); 889 } 890 var command = this._partial.command; 891 var args; 892 try { 893 args = parseCommand(line, this._partial); 894 } catch (state) { 895 if (typeof state == "object") { 896 // Yet another literal coming around... 897 this._partial = state; 898 this._partial.command = command; 899 return "+ I'll be needing more text"; 900 } 901 902 this._multiline = false; 903 return this.tag + " BAD parse error: " + state; 904 } 905 906 this._partial = undefined; 907 this._multiline = false; 908 return this._dispatchCommand(command, args); 909 } 910 911 if (this._nextAuthFunction) { 912 var func = this._nextAuthFunction; 913 this._multiline = false; 914 this._nextAuthFunction = undefined; 915 if (line == "*") { 916 return this._tag + " BAD Okay, as you wish. Chicken"; 917 } 918 if (!func || typeof func != "function") { 919 return this._tag + " BAD I'm lost. Internal server error during auth"; 920 } 921 try { 922 return this._tag + " " + func.call(this, line); 923 } catch (e) { 924 return this._tag + " BAD " + e; 925 } 926 } 927 return undefined; 928 }, 929 _dispatchCommand(command, args) { 930 this.sendingLiteral = false; 931 command = command.toUpperCase(); 932 if (command == this._daemon.commandToFail.toUpperCase()) { 933 return this._tag + " NO " + command + " failed"; 934 } 935 var response; 936 if (command in this) { 937 this._lastCommand = command; 938 // Are we allowed to execute this command? 939 if (!this._enabledCommands[this._state].includes(command)) { 940 return ( 941 this._tag + " BAD illegal command for current state " + this._state 942 ); 943 } 944 945 try { 946 // Format the arguments nicely 947 args = this._treatArgs(args, command); 948 949 // UID command by itself is not useful for PerformTest 950 if (command == "UID") { 951 this._lastCommand += " " + args[0]; 952 } 953 954 // Finally, run the thing 955 response = this[command](args); 956 } catch (e) { 957 if (typeof e == "string") { 958 response = e; 959 } else { 960 throw e; 961 } 962 } 963 } else { 964 response = "BAD " + command + " not implemented"; 965 } 966 967 // Add status updates 968 if (this._selectedMailbox) { 969 for (var update of this._selectedMailbox._updates) { 970 let line; 971 switch (update) { 972 case "EXISTS": 973 line = "* " + this._selectedMailbox._messages.length + " EXISTS"; 974 break; 975 } 976 response = line + "\0" + response; 977 } 978 } 979 980 var lines = response.split("\0"); 981 response = ""; 982 for (let line of lines) { 983 if (!line.startsWith("+") && !line.startsWith("*")) { 984 response += this._tag + " "; 985 } 986 response += line + "\r\n"; 987 } 988 return response; 989 }, 990 _treatArgs(args, command) { 991 var format = this._argFormat[command]; 992 var treatedArgs = []; 993 for (var i = 0; i < format.length; i++) { 994 var spec = format[i]; 995 996 if (spec == "...") { 997 treatedArgs = treatedArgs.concat(args); 998 args = []; 999 break; 1000 } 1001 1002 if (args.length == 0) { 1003 if (spec.startsWith("[")) { 1004 // == optional arg 1005 continue; 1006 } else { 1007 throw new Error("BAD not enough arguments"); 1008 } 1009 } 1010 1011 if (spec.startsWith("[")) { 1012 // We have an optional argument. See if the format matches and move on 1013 // if it doesn't. Ideally, we'd rethink our decision if a later 1014 // application turns out to be wrong, but that's ugly to do 1015 // iteratively. Should any IMAP extension require it, we'll have to 1016 // come back and change this assumption, though. 1017 spec = spec.substr(1, spec.length - 2); 1018 try { 1019 var out = formatArg(args[0], spec); 1020 } catch (e) { 1021 continue; 1022 } 1023 treatedArgs.push(out); 1024 args.shift(); 1025 continue; 1026 } 1027 try { 1028 treatedArgs.push(formatArg(args.shift(), spec)); 1029 } catch (e) { 1030 throw new Error("BAD " + e); 1031 } 1032 } 1033 if (args.length != 0) { 1034 throw new Error("BAD Too many arguments"); 1035 } 1036 return treatedArgs; 1037 }, 1038 1039 // PROTOCOL COMMANDS (ordered as in spec) 1040 1041 CAPABILITY(args) { 1042 var capa = "* CAPABILITY IMAP4rev1 " + this.kCapabilities.join(" "); 1043 if (this.kAuthSchemes.length > 0) { 1044 capa += " AUTH=" + this.kAuthSchemes.join(" AUTH="); 1045 } 1046 capa += "\0OK CAPABILITY completed"; 1047 return capa; 1048 }, 1049 CLIENTID(args) { 1050 return "OK Recognized a valid CLIENTID command, used for authentication methods"; 1051 }, 1052 LOGOUT(args) { 1053 this.closing = true; 1054 if (this._selectedMailbox) { 1055 this._daemon.synchronize(this._selectedMailbox, !this._readOnly); 1056 } 1057 this._state = IMAP_STATE_NOT_AUTHED; 1058 return "* BYE IMAP4rev1 Logging out\0OK LOGOUT completed"; 1059 }, 1060 NOOP(args) { 1061 return "OK NOOP completed"; 1062 }, 1063 STARTTLS(args) { 1064 // simulate annoying server that drops connection on STARTTLS 1065 if (this.dropOnStartTLS) { 1066 this.closing = true; 1067 return ""; 1068 } 1069 return "BAD maild doesn't support TLS ATM"; 1070 }, 1071 _nextAuthFunction: undefined, 1072 AUTHENTICATE(args) { 1073 var scheme = args[0]; // already uppercased by type "atom" 1074 // |scheme| contained in |kAuthSchemes|? 1075 if ( 1076 !this.kAuthSchemes.some(function(s) { 1077 return s == scheme; 1078 }) 1079 ) { 1080 return "-ERR AUTH " + scheme + " not supported"; 1081 } 1082 1083 var func = this._kAuthSchemeStartFunction[scheme]; 1084 if (!func || typeof func != "function") { 1085 return ( 1086 "BAD I just pretended to implement AUTH " + scheme + ", but I don't" 1087 ); 1088 } 1089 return func.apply(this, args.slice(1)); 1090 }, 1091 LOGIN(args) { 1092 if ( 1093 this.kCapabilities.some(function(c) { 1094 return c == "LOGINDISABLED"; 1095 }) 1096 ) { 1097 return "BAD old-style LOGIN is disabled, use AUTHENTICATE"; 1098 } 1099 if (args[0] == this.kUsername && args[1] == this.kPassword) { 1100 this._state = IMAP_STATE_AUTHED; 1101 return "OK authenticated"; 1102 } 1103 return "BAD invalid password, I won't authenticate you"; 1104 }, 1105 SELECT(args) { 1106 var box = this._daemon.getMailbox(args[0]); 1107 if (!box) { 1108 return "NO no such mailbox"; 1109 } 1110 1111 if (this._selectedMailbox) { 1112 this._daemon.synchronize(this._selectedMailbox, !this._readOnly); 1113 } 1114 this._state = IMAP_STATE_SELECTED; 1115 this._selectedMailbox = box; 1116 this._readOnly = false; 1117 1118 var response = "* FLAGS (" + box.msgflags.join(" ") + ")\0"; 1119 response += "* " + box._messages.length + " EXISTS\0* "; 1120 response += box._messages.reduce(function(count, message) { 1121 return count + (message.recent ? 1 : 0); 1122 }, 0); 1123 response += " RECENT\0"; 1124 for (var i = 0; i < box._messages.length; i++) { 1125 if (!box._messages[i].flags.includes("\\Seen")) { 1126 response += "* OK [UNSEEN " + (i + 1) + "]\0"; 1127 break; 1128 } 1129 } 1130 response += "* OK [PERMANENTFLAGS (" + box.permflags.join(" ") + ")]\0"; 1131 response += "* OK [UIDNEXT " + box.uidnext + "]\0"; 1132 if ("uidvalidity" in box) { 1133 response += "* OK [UIDVALIDITY " + box.uidvalidity + "]\0"; 1134 } 1135 return response + "OK [READ-WRITE] SELECT completed"; 1136 }, 1137 EXAMINE(args) { 1138 var box = this._daemon.getMailbox(args[0]); 1139 if (!box) { 1140 return "NO no such mailbox"; 1141 } 1142 1143 if (this._selectedMailbox) { 1144 this._daemon.synchronize(this._selectedMailbox, !this._readOnly); 1145 } 1146 this._state = IMAP_STATE_SELECTED; 1147 this._selectedMailbox = box; 1148 this._readOnly = true; 1149 1150 var response = "* FLAGS (" + box.msgflags.join(" ") + ")\0"; 1151 response += "* " + box._messages.length + " EXISTS\0* "; 1152 response += box._messages.reduce(function(count, message) { 1153 return count + (message.recent ? 1 : 0); 1154 }, 0); 1155 response += " RECENT\0"; 1156 for (var i = 0; i < box._messages.length; i++) { 1157 if (!box._messages[i].flags.includes("\\Seen")) { 1158 response += "* OK [UNSEEN " + (i + 1) + "]\0"; 1159 break; 1160 } 1161 } 1162 response += "* OK [PERMANENTFLAGS (" + box.permflags.join(" ") + ")]\0"; 1163 response += "* OK [UIDNEXT " + box.uidnext + "]\0"; 1164 response += "* OK [UIDVALIDITY " + box.uidvalidity + "]\0"; 1165 return response + "OK [READ-ONLY] EXAMINE completed"; 1166 }, 1167 CREATE(args) { 1168 if (this._daemon.getMailbox(args[0])) { 1169 return "NO mailbox already exists"; 1170 } 1171 if (!this._daemon.createMailbox(args[0])) { 1172 return "NO cannot create mailbox"; 1173 } 1174 return "OK CREATE completed"; 1175 }, 1176 DELETE(args) { 1177 var mbox = this._daemon.getMailbox(args[0]); 1178 if (!mbox || mbox.name == "") { 1179 return "NO no such mailbox"; 1180 } 1181 if (mbox._children.length > 0) { 1182 for (let i = 0; i < mbox.flags.length; i++) { 1183 if (mbox.flags[i] == "\\Noselect") { 1184 return "NO cannot delete mailbox"; 1185 } 1186 } 1187 } 1188 this._daemon.deleteMailbox(mbox); 1189 return "OK DELETE completed"; 1190 }, 1191 RENAME(args) { 1192 var mbox = this._daemon.getMailbox(args[0]); 1193 if (!mbox || mbox.name == "") { 1194 return "NO no such mailbox"; 1195 } 1196 if (!this._daemon.createMailbox(args[1], mbox)) { 1197 return "NO cannot rename mailbox"; 1198 } 1199 return "OK RENAME completed"; 1200 }, 1201 SUBSCRIBE(args) { 1202 var mailbox = this._daemon.getMailbox(args[0]); 1203 if (!mailbox) { 1204 return "NO error in subscribing"; 1205 } 1206 mailbox.subscribed = true; 1207 return "OK SUBSCRIBE completed"; 1208 }, 1209 UNSUBSCRIBE(args) { 1210 var mailbox = this._daemon.getMailbox(args[0]); 1211 if (mailbox) { 1212 mailbox.subscribed = false; 1213 } 1214 return "OK UNSUBSCRIBE completed"; 1215 }, 1216 LIST(args) { 1217 // even though this is the LIST function for RFC 3501, code for 1218 // LIST-EXTENDED (RFC 5258) is included here to keep things simple and 1219 // avoid duplication. We can get away with this because the _treatArgs 1220 // function filters out invalid args for servers that don't support 1221 // LIST-EXTENDED before they even get here. 1222 1223 let listFunctionName = "_LIST"; 1224 // check for optional list selection options argument used by LIST-EXTENDED 1225 // and other related RFCs 1226 if (args.length == 3 || (args.length > 3 && args[3] == "RETURN")) { 1227 let selectionOptions = args.shift(); 1228 selectionOptions = selectionOptions.toString().split(" "); 1229 selectionOptions.sort(); 1230 for (let option of selectionOptions) { 1231 listFunctionName += "_" + option.replace(/-/g, "_"); 1232 } 1233 } 1234 // check for optional list return options argument used by LIST-EXTENDED 1235 // and other related RFCs 1236 if ( 1237 (args.length > 2 && args[2] == "RETURN") || 1238 this.kCapabilities.includes("CHILDREN") 1239 ) { 1240 listFunctionName += "_RETURN"; 1241 let returnOptions = args[3] ? args[3].toString().split(" ") : []; 1242 if ( 1243 this.kCapabilities.includes("CHILDREN") && 1244 !returnOptions.includes("CHILDREN") 1245 ) { 1246 returnOptions.push("CHILDREN"); 1247 } 1248 returnOptions.sort(); 1249 for (let option of returnOptions) { 1250 listFunctionName += "_" + option.replace(/-/g, "_"); 1251 } 1252 } 1253 if (!this[listFunctionName]) { 1254 return "BAD unknown LIST request options"; 1255 } 1256 1257 let base = this._daemon.getMailbox(args[0]); 1258 if (!base) { 1259 return "NO no such mailbox"; 1260 } 1261 let requestedBoxes; 1262 // check for multiple mailbox patterns used by LIST-EXTENDED 1263 // and other related RFCs 1264 if (args[1].startsWith("(")) { 1265 requestedBoxes = parseCommand(args[1])[0]; 1266 } else { 1267 requestedBoxes = [args[1]]; 1268 } 1269 let response = ""; 1270 for (let requestedBox of requestedBoxes) { 1271 let people = base.matchKids(requestedBox); 1272 for (let box of people) { 1273 response += this[listFunctionName](box); 1274 } 1275 } 1276 return response + "OK LIST completed"; 1277 }, 1278 // _LIST is the standard LIST command response 1279 _LIST(aBox) { 1280 if (aBox.nonExistent) { 1281 return ""; 1282 } 1283 return ( 1284 "* LIST (" + 1285 aBox.flags.join(" ") + 1286 ') "' + 1287 aBox.delimiter + 1288 '" "' + 1289 aBox.displayName + 1290 '"\0' 1291 ); 1292 }, 1293 LSUB(args) { 1294 var base = this._daemon.getMailbox(args[0]); 1295 if (!base) { 1296 return "NO no such mailbox"; 1297 } 1298 var people = base.matchKids(args[1]); 1299 var response = ""; 1300 for (var box of people) { 1301 if (box.subscribed) { 1302 response += 1303 '* LSUB () "' + box.delimiter + '" "' + box.displayName + '"\0'; 1304 } 1305 } 1306 return response + "OK LSUB completed"; 1307 }, 1308 STATUS(args) { 1309 var box = this._daemon.getMailbox(args[0]); 1310 if (!box) { 1311 return "NO no such mailbox exists"; 1312 } 1313 for (let i = 0; i < box.flags.length; i++) { 1314 if (box.flags[i] == "\\Noselect") { 1315 return "NO STATUS not allowed on Noselect folder"; 1316 } 1317 } 1318 var parts = []; 1319 for (var status of args[1]) { 1320 var line = status + " "; 1321 switch (status) { 1322 case "MESSAGES": 1323 line += box._messages.length; 1324 break; 1325 case "RECENT": 1326 line += box._messages.reduce(function(count, message) { 1327 return count + (message.recent ? 1 : 0); 1328 }, 0); 1329 break; 1330 case "UIDNEXT": 1331 line += box.uidnext; 1332 break; 1333 case "UIDVALIDITY": 1334 line += box.uidvalidity; 1335 break; 1336 case "UNSEEN": 1337 line += box._messages.reduce(function(count, message) { 1338 return count + (message.flags.includes("\\Seen") ? 0 : 1); 1339 }, 0); 1340 break; 1341 default: 1342 return "BAD unknown status flag: " + status; 1343 } 1344 parts.push(line); 1345 } 1346 return ( 1347 '* STATUS "' + 1348 args[0] + 1349 '" (' + 1350 parts.join(" ") + 1351 ")\0OK STATUS completed" 1352 ); 1353 }, 1354 APPEND(args) { 1355 var mailbox = this._daemon.getMailbox(args[0]); 1356 if (!mailbox) { 1357 return "NO [TRYCREATE] no such mailbox"; 1358 } 1359 var flags, date, text; 1360 if (args.length == 3) { 1361 if (args[1] instanceof Date) { 1362 flags = []; 1363 date = args[1]; 1364 } else { 1365 flags = args[1]; 1366 date = Date.now(); 1367 } 1368 text = args[2]; 1369 } else if (args.length == 4) { 1370 flags = args[1]; 1371 date = args[2]; 1372 text = args[3]; 1373 } else { 1374 flags = []; 1375 date = Date.now(); 1376 text = args[1]; 1377 } 1378 var msg = new imapMessage( 1379 "data:text/plain," + encodeURI(text), 1380 mailbox.uidnext++, 1381 flags 1382 ); 1383 msg.recent = true; 1384 msg.date = date; 1385 mailbox.addMessage(msg); 1386 return "OK APPEND complete"; 1387 }, 1388 CHECK(args) { 1389 this._daemon.synchronize(this._selectedMailbox, false); 1390 return "OK CHECK completed"; 1391 }, 1392 CLOSE(args) { 1393 this._selectedMailbox.expunge(); 1394 this._daemon.synchronize(this._selectedMailbox, !this._readOnly); 1395 this._selectedMailbox = null; 1396 this._state = IMAP_STATE_AUTHED; 1397 return "OK CLOSE completed"; 1398 }, 1399 EXPUNGE(args) { 1400 // Will be either empty or LF-terminated already 1401 var response = this._selectedMailbox.expunge(); 1402 this._daemon.synchronize(this._selectedMailbox); 1403 return response + "OK EXPUNGE completed"; 1404 }, 1405 SEARCH(args, uid) { 1406 if (args[0] == "UNDELETED") { 1407 let response = "* SEARCH"; 1408 let messages = this._selectedMailbox._messages; 1409 for (let i = 0; i < messages.length; i++) { 1410 if (!messages[i].flags.includes("\\Deleted")) { 1411 response += " " + messages[i].uid; 1412 } 1413 } 1414 response += "\0"; 1415 return response + "OK SEARCH COMPLETED\0"; 1416 } 1417 return "BAD not here yet"; 1418 }, 1419 FETCH(args, uid) { 1420 // Step 1: Get the messages to fetch 1421 var ids = []; 1422 var messages = this._parseSequenceSet(args[0], uid, ids); 1423 1424 // Step 2: Ensure that the fetching items are in a neat format 1425 if (typeof args[1] == "string") { 1426 if (args[1] in this.fetchMacroExpansions) { 1427 args[1] = this.fetchMacroExpansions[args[1]]; 1428 } else { 1429 args[1] = [args[1]]; 1430 } 1431 } 1432 if (uid && !args[1].includes("UID")) { 1433 args[1].push("UID"); 1434 } 1435 1436 // Step 2.1: Preprocess the item fetch stack 1437 var items = [], 1438 prefix = undefined; 1439 for (let item of args[1]) { 1440 if (item.indexOf("[") > 0 && !item.includes("]")) { 1441 // We want to append everything into an item until we find a ']' 1442 prefix = item + " "; 1443 continue; 1444 } 1445 if (prefix !== undefined) { 1446 if (typeof item != "string" || !item.includes("]")) { 1447 prefix += 1448 (typeof item == "string" ? item : "(" + item.join(" ") + ")") + " "; 1449 continue; 1450 } 1451 // Replace superfluous space with a ']'. 1452 prefix = prefix.substr(0, prefix.length - 1) + "]"; 1453 item = prefix; 1454 prefix = undefined; 1455 } 1456 item = item.toUpperCase(); 1457 if (!items.includes(item)) { 1458 items.push(item); 1459 } 1460 } 1461 1462 // Step 3: Fetch time! 1463 var response = ""; 1464 for (var i = 0; i < messages.length; i++) { 1465 response += "* " + ids[i] + " FETCH ("; 1466 var parts = []; 1467 for (let item of items) { 1468 // Brief explanation: an item like BODY[]<> can't be hardcoded easily, 1469 // so we go for the initial alphanumeric substring, passing in the 1470 // actual string as an optional second part. 1471 var front = item.split(/[^A-Z0-9-]/, 1)[0]; 1472 var functionName = "_FETCH_" + front.replace(/-/g, "_"); 1473 1474 if (!(functionName in this)) { 1475 return "BAD can't fetch " + front; 1476 } 1477 try { 1478 parts.push(this[functionName](messages[i], item)); 1479 } catch (ex) { 1480 return "BAD error in fetching: " + ex; 1481 } 1482 } 1483 response += parts.join(" ") + ")\0"; 1484 } 1485 return response + "OK FETCH completed"; 1486 }, 1487 STORE(args, uid) { 1488 var ids = []; 1489 var messages = this._parseSequenceSet(args[0], uid, ids); 1490 1491 args[1] = args[1].toUpperCase(); 1492 var silent = args[1].includes(".SILENT", 1); 1493 if (silent) { 1494 args[1] = args[1].substring(0, args[1].indexOf(".")); 1495 } 1496 1497 if (typeof args[2] != "object") { 1498 args[2] = [args[2]]; 1499 } 1500 1501 var response = ""; 1502 for (var i = 0; i < messages.length; i++) { 1503 var message = messages[i]; 1504 switch (args[1]) { 1505 case "FLAGS": 1506 message.flags = args[2]; 1507 break; 1508 case "+FLAGS": 1509 for (let flag of args[2]) { 1510 message.setFlag(flag); 1511 } 1512 break; 1513 case "-FLAGS": 1514 for (let flag of args[2]) { 1515 var index; 1516 if ((index = message.flags.indexOf(flag)) != -1) { 1517 message.flags.splice(index, 1); 1518 } 1519 } 1520 break; 1521 default: 1522 return "BAD change what now?"; 1523 } 1524 response += "* " + ids[i] + " FETCH (FLAGS ("; 1525 response += message.flags.join(" "); 1526 response += "))\0"; 1527 } 1528 if (silent) { 1529 response = ""; 1530 } 1531 return response + "OK STORE completed"; 1532 }, 1533 COPY(args, uid) { 1534 var messages = this._parseSequenceSet(args[0], uid); 1535 1536 var dest = this._daemon.getMailbox(args[1]); 1537 if (!dest) { 1538 return "NO [TRYCREATE] what mailbox?"; 1539 } 1540 1541 for (var message of messages) { 1542 let newMessage = new imapMessage( 1543 message._URI, 1544 dest.uidnext++, 1545 message.flags 1546 ); 1547 newMessage.recent = false; 1548 dest.addMessage(newMessage); 1549 } 1550 if (this._daemon.copySleep > 0) { 1551 // spin rudely for copyTimeout milliseconds. 1552 let now = new Date(); 1553 let alarm; 1554 let startingMSeconds = now.getTime(); 1555 while (true) { 1556 alarm = new Date(); 1557 if (alarm.getTime() - startingMSeconds > this._daemon.copySleep) { 1558 break; 1559 } 1560 } 1561 } 1562 return "OK COPY completed"; 1563 }, 1564 UID(args) { 1565 var name = args.shift(); 1566 if (!this.kUidCommands.includes(name)) { 1567 return "BAD illegal command " + name; 1568 } 1569 1570 args = this._treatArgs(args, name); 1571 return this[name](args, true); 1572 }, 1573 1574 postCommand(reader) { 1575 if (this.closing) { 1576 this.closing = false; 1577 reader.closeSocket(); 1578 } 1579 if (this.sendingLiteral) { 1580 reader.preventLFMunge(); 1581 } 1582 reader.setMultiline(this._multiline); 1583 if (this._lastCommand == reader.watchWord) { 1584 reader.stopTest(); 1585 } 1586 }, 1587 onServerFault(e) { 1588 return ( 1589 ("_tag" in this ? this._tag : "*") + " BAD Internal server error: " + e 1590 ); 1591 }, 1592 1593 // FETCH sub commands and helpers 1594 1595 fetchMacroExpansions: { 1596 ALL: ["FLAGS", "INTERNALDATE", "RFC822.SIZE" /* , "ENVELOPE" */], 1597 FAST: ["FLAGS", "INTERNALDATE", "RFC822.SIZE"], 1598 FULL: ["FLAGS", "INTERNALDATE", "RFC822.SIZE" /* , "ENVELOPE", "BODY" */], 1599 }, 1600 _parseSequenceSet(set, uid, ids /* optional */) { 1601 if (typeof set == "number") { 1602 if (uid) { 1603 for (let i = 0; i < this._selectedMailbox._messages.length; i++) { 1604 var message = this._selectedMailbox._messages[i]; 1605 if (message.uid == set) { 1606 if (ids) { 1607 ids.push(i + 1); 1608 } 1609 return [message]; 1610 } 1611 } 1612 return []; 1613 } 1614 if (!(set - 1 in this._selectedMailbox._messages)) { 1615 return []; 1616 } 1617 if (ids) { 1618 ids.push(set); 1619 } 1620 return [this._selectedMailbox._messages[set - 1]]; 1621 } 1622 1623 var daemon = this; 1624 function part2num(part) { 1625 if (part == "*") { 1626 if (uid) { 1627 return daemon._selectedMailbox._highestuid; 1628 } 1629 return daemon._selectedMailbox._messages.length; 1630 } 1631 let re = /[0-9]/g; 1632 let num = part.match(re); 1633 if (!num || num.length != part.length) { 1634 throw new Error("BAD invalid UID " + part); 1635 } 1636 return parseInt(part); 1637 } 1638 1639 var elements = set.split(/,/); 1640 set = []; 1641 for (var part of elements) { 1642 if (!part.includes(":")) { 1643 set.push(part2num(part)); 1644 } else { 1645 var range = part.split(/:/); 1646 range[0] = part2num(range[0]); 1647 range[1] = part2num(range[1]); 1648 if (range[0] > range[1]) { 1649 let temp = range[1]; 1650 range[1] = range[0]; 1651 range[0] = temp; 1652 } 1653 for (let i = range[0]; i <= range[1]; i++) { 1654 set.push(i); 1655 } 1656 } 1657 } 1658 set.sort(); 1659 for (let i = set.length - 1; i > 0; i--) { 1660 if (set[i] == set[i - 1]) { 1661 set.splice(i, 0); 1662 } 1663 } 1664 1665 if (!ids) { 1666 ids = []; 1667 } 1668 var messages; 1669 if (uid) { 1670 messages = this._selectedMailbox._messages.filter(function(msg, i) { 1671 if (!set.includes(msg.uid)) { 1672 return false; 1673 } 1674 ids.push(i + 1); 1675 return true; 1676 }); 1677 } else { 1678 messages = []; 1679 for (var id of set) { 1680 if (id - 1 in this._selectedMailbox._messages) { 1681 ids.push(id); 1682 messages.push(this._selectedMailbox._messages[id - 1]); 1683 } 1684 } 1685 } 1686 return messages; 1687 }, 1688 _FETCH_BODY(message, query) { 1689 if (query == "BODY") { 1690 return "BODYSTRUCTURE " + bodystructure(message.getText(), false); 1691 } 1692 // parts = [ name, section, empty, {, partial, empty } ] 1693 var parts = query.split(/[[\]<>]/); 1694 1695 if (parts[0] != "BODY.PEEK" && !this._readOnly) { 1696 message.setFlag("\\Seen"); 1697 } 1698 1699 if (parts[3]) { 1700 parts[3] = parts[3].split(/\./).map(function(e) { 1701 return parseInt(e); 1702 }); 1703 } 1704 1705 if (parts[1].length == 0) { 1706 // Easy case: we have BODY[], just send the message... 1707 let response = "BODY[]"; 1708 var text; 1709 if (parts[3]) { 1710 response += "<" + parts[3][0] + ">"; 1711 text = message.getText(parts[3][0], parts[3][1]); 1712 } else { 1713 text = message.getText(); 1714 } 1715 response += " {" + text.length + "}\r\n"; 1716 response += text; 1717 return response; 1718 } 1719 1720 // What's inside the command? 1721 var data = /((?:\d+\.)*\d+)(?:\.([^ ]+))?/.exec(parts[1]); 1722 var partNum; 1723 if (data) { 1724 partNum = data[1]; 1725 query = data[2]; 1726 } else { 1727 partNum = ""; 1728 if (parts[1].includes(" ", 1)) { 1729 query = parts[1].substring(0, parts[1].indexOf(" ")); 1730 } else { 1731 query = parts[1]; 1732 } 1733 } 1734 var queryArgs; 1735 if (parts[1].includes(" ", 1)) { 1736 queryArgs = parseCommand(parts[1].substr(parts[1].indexOf(" ")))[0]; 1737 } else { 1738 queryArgs = []; 1739 } 1740 1741 // Now we have three parameters representing the part number (empty for top- 1742 // level), the subportion representing what we want to find (empty for the 1743 // body), and an array of arguments if we have a subquery. If we made an 1744 // error here, it will pop until it gets to FETCH, which will just pop at a 1745 // BAD response, which is what should happen if the query is malformed. 1746 // Now we dump it all off onto imapMessage to mess with. 1747 1748 // Start off the response 1749 let response = "BODY[" + parts[1] + "]"; 1750 if (parts[3]) { 1751 response += "<" + parts[3][0] + ">"; 1752 } 1753 response += " "; 1754 1755 data = ""; 1756 switch (query) { 1757 case "": 1758 case "TEXT": 1759 data += message.getPartBody(partNum); 1760 break; 1761 case "HEADER": // I believe this specifies mime for an RFC822 message only 1762 data += message.getPartHeaders(partNum).rawHeaderText + "\r\n"; 1763 break; 1764 case "MIME": 1765 data += message.getPartHeaders(partNum).rawHeaderText + "\r\n\r\n"; 1766 break; 1767 case "HEADER.FIELDS": { 1768 let joinList = []; 1769 let headers = message.getPartHeaders(partNum); 1770 for (let header of queryArgs) { 1771 header = header.toLowerCase(); 1772 if (headers.has(header)) { 1773 joinList.push( 1774 headers 1775 .getRawHeader(header) 1776 .map(value => `${header}: ${value}`) 1777 .join("\r\n") 1778 ); 1779 } 1780 } 1781 data += joinList.join("\r\n") + "\r\n"; 1782 break; 1783 } 1784 case "HEADER.FIELDS.NOT": { 1785 let joinList = []; 1786 let headers = message.getPartHeaders(partNum); 1787 for (let header of headers) { 1788 if (!(header in queryArgs)) { 1789 joinList.push( 1790 headers 1791 .getRawHeader(header) 1792 .map(value => `${header}: ${value}`) 1793 .join("\r\n") 1794 ); 1795 } 1796 } 1797 data += joinList.join("\r\n") + "\r\n"; 1798 break; 1799 } 1800 default: 1801 data += message.getPartBody(partNum); 1802 } 1803 1804 this.sendingLiteral = true; 1805 response += "{" + data.length + "}\r\n"; 1806 response += data; 1807 return response; 1808 }, 1809 _FETCH_BODYSTRUCTURE(message, query) { 1810 return "BODYSTRUCTURE " + bodystructure(message.getText(), true); 1811 }, 1812 // _FETCH_ENVELOPE, 1813 _FETCH_FLAGS(message) { 1814 var response = "FLAGS ("; 1815 response += message.flags.join(" "); 1816 if (message.recent) { 1817 response += " \\Recent"; 1818 } 1819 response += ")"; 1820 return response; 1821 }, 1822 _FETCH_INTERNALDATE(message) { 1823 let date = message.date; 1824 // Format timestamp as: "%d-%b-%Y %H:%M:%S %z" (%b in English). 1825 let year = date.getFullYear().toString(); 1826 let month = date.toLocaleDateString("en-US", { month: "short" }); 1827 let day = date.getDate().toString(); 1828 let hours = date 1829 .getHours() 1830 .toString() 1831 .padStart(2, "0"); 1832 let minutes = date 1833 .getMinutes() 1834 .toString() 1835 .padStart(2, "0"); 1836 let seconds = date 1837 .getSeconds() 1838 .toString() 1839 .padStart(2, "0"); 1840 let offset = date.getTimezoneOffset(); 1841 let tzoff = 1842 Math.floor(Math.abs(offset) / 60) * 100 + (Math.abs(offset) % 60); 1843 let timeZone = (offset < 0 ? "+" : "-") + tzoff.toString().padStart(4, "0"); 1844 1845 let response = 'INTERNALDATE "'; 1846 response += `${day}-${month}-${year} ${hours}:${minutes}:${seconds} ${timeZone}`; 1847 response += '"'; 1848 return response; 1849 }, 1850 _FETCH_RFC822(message, query) { 1851 if (query == "RFC822") { 1852 return this._FETCH_BODY(message, "BODY[]").replace("BODY[]", "RFC822"); 1853 } 1854 if (query == "RFC822.HEADER") { 1855 return this._FETCH_BODY(message, "BODY.PEEK[HEADER]").replace( 1856 "BODY[HEADER]", 1857 "RFC822.HEADER" 1858 ); 1859 } 1860 if (query == "RFC822.TEXT") { 1861 return this._FETCH_BODY(message, "BODY[TEXT]").replace( 1862 "BODY[TEXT]", 1863 "RFC822.TEXT" 1864 ); 1865 } 1866 1867 if (query == "RFC822.SIZE") { 1868 var channel = message.channel; 1869 var length = message.size ? message.size : channel.contentLength; 1870 if (length == -1) { 1871 var inputStream = channel.open(); 1872 length = inputStream.available(); 1873 inputStream.close(); 1874 } 1875 return "RFC822.SIZE " + length; 1876 } 1877 throw new Error("Unknown item " + query); 1878 }, 1879 _FETCH_UID(message) { 1880 return "UID " + message.uid; 1881 }, 1882}; 1883 1884// IMAP4 RFC extensions 1885// -------------------- 1886// Since there are so many extensions to IMAP, and since these extensions are 1887// not strictly hierarchical (e.g., an RFC 2342-compliant server can also be 1888// RFC 3516-compliant, but a server might only implement one of them), they 1889// must be handled differently from other fakeserver implementations. 1890// An extension is defined as follows: it is an object (not a function and 1891// prototype pair!). This object is "mixed" into the handler via the helper 1892// function mixinExtension, which applies appropriate magic to make the 1893// handler compliant to the extension. Functions are added untransformed, but 1894// both arrays and objects are handled by appending the values onto the 1895// original state of the handler. Semantics apply as for the base itself. 1896 1897// Note that UIDPLUS (RFC4315) should be mixed in last (or at least after the 1898// MOVE extension) because it changes behavior of that extension. 1899var configurations = { 1900 Cyrus: ["RFC2342", "RFC2195", "RFC5258"], 1901 UW: ["RFC2342", "RFC2195"], 1902 Dovecot: ["RFC2195", "RFC5258"], 1903 Zimbra: ["RFC2197", "RFC2342", "RFC2195", "RFC5258"], 1904 Exchange: ["RFC2342", "RFC2195"], 1905 LEMONADE: ["RFC2342", "RFC2195"], 1906 CUSTOM1: ["MOVE", "RFC4315", "CUSTOM"], 1907 GMail: ["GMAIL", "RFC2197", "RFC2342", "RFC3348", "RFC4315"], 1908}; 1909 1910function mixinExtension(handler, extension) { 1911 if (extension.preload) { 1912 extension.preload(handler); 1913 } 1914 1915 for (var property in extension) { 1916 if (property == "preload") { 1917 continue; 1918 } 1919 if (typeof extension[property] == "function") { 1920 // This is a function, so we add it to the handler 1921 handler[property] = extension[property]; 1922 } else if (extension[property] instanceof Array) { 1923 // This is an array, so we append the values 1924 if (!(property in handler)) { 1925 handler[property] = []; 1926 } 1927 handler[property] = handler[property].concat(extension[property]); 1928 } else if (property in handler) { 1929 // This is an object, so we add in the values 1930 // Hack to make arrays et al. work recursively 1931 mixinExtension(handler[property], extension[property]); 1932 } else { 1933 handler[property] = extension[property]; 1934 } 1935 } 1936} 1937 1938// Support for Gmail extensions: XLIST and X-GM-EXT-1 1939var IMAP_GMAIL_extension = { 1940 preload(toBeThis) { 1941 toBeThis._preGMAIL_STORE = toBeThis.STORE; 1942 toBeThis._preGMAIL_STORE_argFormat = toBeThis._argFormat.STORE; 1943 toBeThis._argFormat.STORE = ["number", "atom", "..."]; 1944 }, 1945 XLIST(args) { 1946 // XLIST is really just SPECIAL-USE that does not conform to RFC 6154 1947 args.push("RETURN"); 1948 args.push("SPECIAL-USE"); 1949 return this.LIST(args); 1950 }, 1951 _LIST_RETURN_CHILDREN(aBox) { 1952 return IMAP_RFC5258_extension._LIST_RETURN_CHILDREN(aBox); 1953 }, 1954 _LIST_RETURN_CHILDREN_SPECIAL_USE(aBox) { 1955 if (aBox.nonExistent) { 1956 return ""; 1957 } 1958 1959 let result = "* LIST (" + aBox.flags.join(" "); 1960 if (aBox._children.length > 0) { 1961 if (aBox.flags.length > 0) { 1962 result += " "; 1963 } 1964 result += "\\HasChildren"; 1965 } else if (!aBox.flags.includes("\\NoInferiors")) { 1966 if (aBox.flags.length > 0) { 1967 result += " "; 1968 } 1969 result += "\\HasNoChildren"; 1970 } 1971 if (aBox.specialUseFlag && aBox.specialUseFlag.length > 0) { 1972 result += " " + aBox.specialUseFlag; 1973 } 1974 result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0'; 1975 return result; 1976 }, 1977 STORE(args, uid) { 1978 let regex = /[+-]?FLAGS.*/; 1979 if (regex.test(args[1])) { 1980 // if we are storing flags, use the method that was overridden 1981 this._argFormat = this._preGMAIL_STORE_argFormat; 1982 args = this._treatArgs(args, "STORE"); 1983 return this._preGMAIL_STORE(args, uid); 1984 } 1985 // otherwise, handle gmail specific cases 1986 let ids = []; 1987 let messages = this._parseSequenceSet(args[0], uid, ids); 1988 args[2] = formatArg(args[2], "string|(string)"); 1989 for (let i = 0; i < args[2].length; i++) { 1990 if (args[2][i].includes(" ")) { 1991 args[2][i] = '"' + args[2][i] + '"'; 1992 } 1993 } 1994 let response = ""; 1995 for (let i = 0; i < messages.length; i++) { 1996 let message = messages[i]; 1997 switch (args[1]) { 1998 case "X-GM-LABELS": 1999 if (message.xGmLabels) { 2000 message.xGmLabels = args[2]; 2001 } else { 2002 return "BAD can't store X-GM-LABELS"; 2003 } 2004 break; 2005 case "+X-GM-LABELS": 2006 if (message.xGmLabels) { 2007 message.xGmLabels = message.xGmLabels.concat(args[2]); 2008 } else { 2009 return "BAD can't store X-GM-LABELS"; 2010 } 2011 break; 2012 case "-X-GM-LABELS": 2013 if (message.xGmLabels) { 2014 for (let i = 0; i < args[2].length; i++) { 2015 let idx = message.xGmLabels.indexOf(args[2][i]); 2016 if (idx != -1) { 2017 message.xGmLabels.splice(idx, 1); 2018 } 2019 } 2020 } else { 2021 return "BAD can't store X-GM-LABELS"; 2022 } 2023 break; 2024 default: 2025 return "BAD change what now?"; 2026 } 2027 response += "* " + ids[i] + " FETCH (X-GM-LABELS ("; 2028 response += message.xGmLabels.join(" "); 2029 response += "))\0"; 2030 } 2031 return response + "OK STORE completed"; 2032 }, 2033 _FETCH_X_GM_MSGID(message) { 2034 if (message.xGmMsgid) { 2035 return "X-GM-MSGID " + message.xGmMsgid; 2036 } 2037 return "BAD can't fetch X-GM-MSGID"; 2038 }, 2039 _FETCH_X_GM_THRID(message) { 2040 if (message.xGmThrid) { 2041 return "X-GM-THRID " + message.xGmThrid; 2042 } 2043 return "BAD can't fetch X-GM-THRID"; 2044 }, 2045 _FETCH_X_GM_LABELS(message) { 2046 if (message.xGmLabels) { 2047 return "X-GM-LABELS " + message.xGmLabels; 2048 } 2049 return "BAD can't fetch X-GM-LABELS"; 2050 }, 2051 kCapabilities: ["XLIST", "X-GM-EXT-1"], 2052 _argFormat: { XLIST: ["mailbox", "mailbox"] }, 2053 // Enabled in AUTHED and SELECTED states 2054 _enabledCommands: { 1: ["XLIST"], 2: ["XLIST"] }, 2055}; 2056 2057var IMAP_MOVE_extension = { 2058 MOVE(args, uid) { 2059 let messages = this._parseSequenceSet(args[0], uid); 2060 2061 let dest = this._daemon.getMailbox(args[1]); 2062 if (!dest) { 2063 return "NO [TRYCREATE] what mailbox?"; 2064 } 2065 2066 for (var message of messages) { 2067 let newMessage = new imapMessage( 2068 message._URI, 2069 dest.uidnext++, 2070 message.flags 2071 ); 2072 newMessage.recent = false; 2073 dest.addMessage(newMessage); 2074 } 2075 let mailbox = this._selectedMailbox; 2076 let response = ""; 2077 for (let i = messages.length - 1; i >= 0; i--) { 2078 let msgIndex = mailbox._messages.indexOf(messages[i]); 2079 if (msgIndex != -1) { 2080 response += "* " + (msgIndex + 1) + " EXPUNGE\0"; 2081 mailbox._messages.splice(msgIndex, 1); 2082 } 2083 } 2084 if (response.length > 0) { 2085 delete mailbox.__highestuid; 2086 } 2087 2088 return response + "OK MOVE completed"; 2089 }, 2090 kCapabilities: ["MOVE"], 2091 kUidCommands: ["MOVE"], 2092 _argFormat: { MOVE: ["number", "mailbox"] }, 2093 // Enabled in SELECTED state 2094 _enabledCommands: { 2: ["MOVE"] }, 2095}; 2096 2097// Provides methods for testing fetchCustomAttribute and issueCustomCommand 2098var IMAP_CUSTOM_extension = { 2099 preload(toBeThis) { 2100 toBeThis._preCUSTOM_STORE = toBeThis.STORE; 2101 toBeThis._preCUSTOM_STORE_argFormat = toBeThis._argFormat.STORE; 2102 toBeThis._argFormat.STORE = ["number", "atom", "..."]; 2103 }, 2104 STORE(args, uid) { 2105 let regex = /[+-]?FLAGS.*/; 2106 if (regex.test(args[1])) { 2107 // if we are storing flags, use the method that was overridden 2108 this._argFormat = this._preCUSTOM_STORE_argFormat; 2109 args = this._treatArgs(args, "STORE"); 2110 return this._preCUSTOM_STORE(args, uid); 2111 } 2112 // otherwise, handle custom attribute 2113 let ids = []; 2114 let messages = this._parseSequenceSet(args[0], uid, ids); 2115 args[2] = formatArg(args[2], "string|(string)"); 2116 for (let i = 0; i < args[2].length; i++) { 2117 if (args[2][i].includes(" ")) { 2118 args[2][i] = '"' + args[2][i] + '"'; 2119 } 2120 } 2121 let response = ""; 2122 for (let i = 0; i < messages.length; i++) { 2123 let message = messages[i]; 2124 switch (args[1]) { 2125 case "X-CUSTOM-VALUE": 2126 if (message.xCustomValue && args[2].length == 1) { 2127 message.xCustomValue = args[2][0]; 2128 } else { 2129 return "BAD can't store X-CUSTOM-VALUE"; 2130 } 2131 break; 2132 case "X-CUSTOM-LIST": 2133 if (message.xCustomList) { 2134 message.xCustomList = args[2]; 2135 } else { 2136 return "BAD can't store X-CUSTOM-LIST"; 2137 } 2138 break; 2139 case "+X-CUSTOM-LIST": 2140 if (message.xCustomList) { 2141 message.xCustomList = message.xCustomList.concat(args[2]); 2142 } else { 2143 return "BAD can't store X-CUSTOM-LIST"; 2144 } 2145 break; 2146 case "-X-CUSTOM-LIST": 2147 if (message.xCustomList) { 2148 for (let i = 0; i < args[2].length; i++) { 2149 let idx = message.xCustomList.indexOf(args[2][i]); 2150 if (idx != -1) { 2151 message.xCustomList.splice(idx, 1); 2152 } 2153 } 2154 } else { 2155 return "BAD can't store X-CUSTOM-LIST"; 2156 } 2157 break; 2158 default: 2159 return "BAD change what now?"; 2160 } 2161 response += "* " + ids[i] + " FETCH (X-CUSTOM-LIST ("; 2162 response += message.xCustomList.join(" "); 2163 response += "))\0"; 2164 } 2165 return response + "OK STORE completed"; 2166 }, 2167 _FETCH_X_CUSTOM_VALUE(message) { 2168 if (message.xCustomValue) { 2169 return "X-CUSTOM-VALUE " + message.xCustomValue; 2170 } 2171 return "BAD can't fetch X-CUSTOM-VALUE"; 2172 }, 2173 _FETCH_X_CUSTOM_LIST(message) { 2174 if (message.xCustomList) { 2175 return "X-CUSTOM-LIST (" + message.xCustomList.join(" ") + ")"; 2176 } 2177 return "BAD can't fetch X-CUSTOM-LIST"; 2178 }, 2179 kCapabilities: ["X-CUSTOM1"], 2180}; 2181 2182// RFC 2197: ID 2183var IMAP_RFC2197_extension = { 2184 ID(args) { 2185 let clientID = "("; 2186 for (let i of args) { 2187 clientID += '"' + i + '"'; 2188 } 2189 2190 clientID += ")"; 2191 let clientStrings = clientID.split(","); 2192 clientID = ""; 2193 for (let i of clientStrings) { 2194 clientID += '"' + i + '" '; 2195 } 2196 clientID = clientID.slice(1, clientID.length - 3); 2197 clientID += ")"; 2198 this._daemon.clientID = clientID; 2199 return "* ID " + this._daemon.idResponse + "\0OK Success"; 2200 }, 2201 kCapabilities: ["ID"], 2202 _argFormat: { ID: ["(string)"] }, 2203 _enabledCommands: { 1: ["ID"], 2: ["ID"] }, 2204}; 2205 2206// RFC 2342: IMAP4 Namespace (NAMESPACE) 2207var IMAP_RFC2342_extension = { 2208 NAMESPACE(args) { 2209 var namespaces = [[], [], []]; 2210 for (let namespace of this._daemon.namespaces) { 2211 namespaces[namespace.type].push(namespace); 2212 } 2213 2214 var response = "* NAMESPACE"; 2215 for (var type of namespaces) { 2216 if (type.length == 0) { 2217 response += " NIL"; 2218 continue; 2219 } 2220 response += " ("; 2221 for (let namespace of type) { 2222 response += '("'; 2223 response += namespace.displayName; 2224 response += '" "'; 2225 response += namespace.delimiter; 2226 response += '")'; 2227 } 2228 response += ")"; 2229 } 2230 response += "\0OK NAMESPACE command completed"; 2231 return response; 2232 }, 2233 kCapabilities: ["NAMESPACE"], 2234 _argFormat: { NAMESPACE: [] }, 2235 // Enabled in AUTHED and SELECTED states 2236 _enabledCommands: { 1: ["NAMESPACE"], 2: ["NAMESPACE"] }, 2237}; 2238 2239// RFC 3348 Child Mailbox (CHILDREN) 2240var IMAP_RFC3348_extension = { 2241 kCapabilities: ["CHILDREN"], 2242}; 2243 2244// RFC 4315: UIDPLUS 2245var IMAP_RFC4315_extension = { 2246 preload(toBeThis) { 2247 toBeThis._preRFC4315UID = toBeThis.UID; 2248 toBeThis._preRFC4315APPEND = toBeThis.APPEND; 2249 toBeThis._preRFC4315COPY = toBeThis.COPY; 2250 toBeThis._preRFC4315MOVE = toBeThis.MOVE; 2251 }, 2252 UID(args) { 2253 // XXX: UID EXPUNGE is not supported. 2254 return this._preRFC4315UID(args); 2255 }, 2256 APPEND(args) { 2257 let response = this._preRFC4315APPEND(args); 2258 if (response.indexOf("OK") == 0) { 2259 let mailbox = this._daemon.getMailbox(args[0]); 2260 let uid = mailbox.uidnext - 1; 2261 response = 2262 "OK [APPENDUID " + 2263 mailbox.uidvalidity + 2264 " " + 2265 uid + 2266 "]" + 2267 response.substring(2); 2268 } 2269 return response; 2270 }, 2271 COPY(args) { 2272 let mailbox = this._daemon.getMailbox(args[0]); 2273 if (mailbox) { 2274 var first = mailbox.uidnext; 2275 } 2276 let response = this._preRFC4315COPY(args); 2277 if (response.indexOf("OK") == 0) { 2278 let last = mailbox.uidnext - 1; 2279 response = 2280 "OK [COPYUID " + 2281 this._selectedMailbox.uidvalidity + 2282 " " + 2283 args[0] + 2284 " " + 2285 first + 2286 ":" + 2287 last + 2288 "]" + 2289 response.substring(2); 2290 } 2291 return response; 2292 }, 2293 MOVE(args) { 2294 let mailbox = this._daemon.getMailbox(args[1]); 2295 if (mailbox) { 2296 var first = mailbox.uidnext; 2297 } 2298 let response = this._preRFC4315MOVE(args); 2299 if (response.includes("OK MOVE")) { 2300 let last = mailbox.uidnext - 1; 2301 response = response.replace( 2302 "OK MOVE", 2303 "OK [COPYUID " + 2304 this._selectedMailbox.uidvalidity + 2305 " " + 2306 args[0] + 2307 " " + 2308 first + 2309 ":" + 2310 last + 2311 "]" 2312 ); 2313 } 2314 return response; 2315 }, 2316 kCapabilities: ["UIDPLUS"], 2317}; 2318 2319// RFC 5258: LIST-EXTENDED 2320var IMAP_RFC5258_extension = { 2321 preload(toBeThis) { 2322 toBeThis._argFormat.LIST = [ 2323 "[(atom)]", 2324 "mailbox", 2325 "mailbox|(mailbox)", 2326 "[atom]", 2327 "[(atom)]", 2328 ]; 2329 }, 2330 _LIST_SUBSCRIBED(aBox) { 2331 if (!aBox.subscribed) { 2332 return ""; 2333 } 2334 2335 let result = "* LIST (" + aBox.flags.join(" "); 2336 if (aBox.flags.length > 0) { 2337 result += " "; 2338 } 2339 result += "\\Subscribed"; 2340 if (aBox.nonExistent) { 2341 result += " \\NonExistent"; 2342 } 2343 result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0'; 2344 return result; 2345 }, 2346 _LIST_RETURN_CHILDREN(aBox) { 2347 if (aBox.nonExistent) { 2348 return ""; 2349 } 2350 2351 let result = "* LIST (" + aBox.flags.join(" "); 2352 if (aBox._children.length > 0) { 2353 if (aBox.flags.length > 0) { 2354 result += " "; 2355 } 2356 result += "\\HasChildren"; 2357 } else if (!aBox.flags.includes("\\NoInferiors")) { 2358 if (aBox.flags.length > 0) { 2359 result += " "; 2360 } 2361 result += "\\HasNoChildren"; 2362 } 2363 result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0'; 2364 return result; 2365 }, 2366 _LIST_RETURN_SUBSCRIBED(aBox) { 2367 if (aBox.nonExistent) { 2368 return ""; 2369 } 2370 2371 let result = "* LIST (" + aBox.flags.join(" "); 2372 if (aBox.subscribed) { 2373 if (aBox.flags.length > 0) { 2374 result += " "; 2375 } 2376 result += "\\Subscribed"; 2377 } 2378 result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0'; 2379 return result; 2380 }, 2381 // TODO implement _LIST_REMOTE, _LIST_RECURSIVEMATCH, _LIST_RETURN_SUBSCRIBED 2382 // and all valid combinations thereof. Currently, nsImapServerResponseParser 2383 // does not support any of these responses anyway. 2384 2385 kCapabilities: ["LIST-EXTENDED"], 2386}; 2387 2388/** 2389 * This implements AUTH schemes. Could be moved into RFC3501 actually. 2390 * The test can en-/disable auth schemes by modifying kAuthSchemes. 2391 */ 2392var IMAP_RFC2195_extension = { 2393 kAuthSchemes: ["CRAM-MD5", "PLAIN", "LOGIN"], 2394 2395 preload(handler) { 2396 handler._kAuthSchemeStartFunction["CRAM-MD5"] = this.authCRAMStart; 2397 handler._kAuthSchemeStartFunction.PLAIN = this.authPLAINStart; 2398 handler._kAuthSchemeStartFunction.LOGIN = this.authLOGINStart; 2399 }, 2400 2401 authPLAINStart(lineRest) { 2402 this._nextAuthFunction = this.authPLAINCred; 2403 this._multiline = true; 2404 2405 return "+"; 2406 }, 2407 authPLAINCred(line) { 2408 var req = AuthPLAIN.decodeLine(line); 2409 if (req.username == this.kUsername && req.password == this.kPassword) { 2410 this._state = IMAP_STATE_AUTHED; 2411 return "OK Hello friend! Friends give friends good advice: Next time, use CRAM-MD5"; 2412 } 2413 return "BAD Wrong username or password, crook!"; 2414 }, 2415 2416 authCRAMStart(lineRest) { 2417 this._nextAuthFunction = this.authCRAMDigest; 2418 this._multiline = true; 2419 2420 this._usedCRAMMD5Challenge = AuthCRAM.createChallenge("localhost"); 2421 return "+ " + this._usedCRAMMD5Challenge; 2422 }, 2423 authCRAMDigest(line) { 2424 var req = AuthCRAM.decodeLine(line); 2425 var expectedDigest = AuthCRAM.encodeCRAMMD5( 2426 this._usedCRAMMD5Challenge, 2427 this.kPassword 2428 ); 2429 if (req.username == this.kUsername && req.digest == expectedDigest) { 2430 this._state = IMAP_STATE_AUTHED; 2431 return "OK Hello friend!"; 2432 } 2433 return "BAD Wrong username or password, crook!"; 2434 }, 2435 2436 authLOGINStart(lineRest) { 2437 this._nextAuthFunction = this.authLOGINUsername; 2438 this._multiline = true; 2439 2440 return "+ " + btoa("Username:"); 2441 }, 2442 authLOGINUsername(line) { 2443 var req = AuthLOGIN.decodeLine(line); 2444 if (req == this.kUsername) { 2445 this._nextAuthFunction = this.authLOGINPassword; 2446 } else { 2447 // Don't return error yet, to not reveal valid usernames 2448 this._nextAuthFunction = this.authLOGINBadUsername; 2449 } 2450 this._multiline = true; 2451 return "+ " + btoa("Password:"); 2452 }, 2453 authLOGINBadUsername(line) { 2454 return "BAD Wrong username or password, crook!"; 2455 }, 2456 authLOGINPassword(line) { 2457 var req = AuthLOGIN.decodeLine(line); 2458 if (req == this.kPassword) { 2459 this._state = IMAP_STATE_AUTHED; 2460 return "OK Hello friend! Where did you pull out this old auth scheme?"; 2461 } 2462 return "BAD Wrong username or password, crook!"; 2463 }, 2464}; 2465 2466// FETCH BODYSTRUCTURE 2467function bodystructure(msg, extension) { 2468 if (!msg || msg == "") { 2469 return ""; 2470 } 2471 2472 // Use the mime parser emitter to generate body structure data. Most of the 2473 // string will be built as we exit a part. Currently not working: 2474 // 1. Some of the fields return NIL instead of trying to calculate them. 2475 // 2. MESSAGE is missing the ENVELOPE and the lines at the end. 2476 var bodystruct = ""; 2477 function paramToString(params) { 2478 let paramList = []; 2479 for (let [param, value] of params) { 2480 paramList.push('"' + param.toUpperCase() + '" "' + value + '"'); 2481 } 2482 return paramList.length == 0 ? "NIL" : "(" + paramList.join(" ") + ")"; 2483 } 2484 var headerStack = []; 2485 var BodyStructureEmitter = { 2486 startPart(partNum, headers) { 2487 bodystruct += "("; 2488 headerStack.push(headers); 2489 this.numLines = 0; 2490 this.length = 0; 2491 }, 2492 deliverPartData(partNum, data) { 2493 this.length += data.length; 2494 this.numLines += Array.from(data).filter(x => x == "\n").length; 2495 }, 2496 endPart(partNum) { 2497 // Grab the headers from before 2498 let headers = headerStack.pop(); 2499 let contentType = headers.contentType; 2500 if (contentType.mediatype == "multipart") { 2501 bodystruct += ' "' + contentType.subtype.toUpperCase() + '"'; 2502 if (extension) { 2503 bodystruct += " " + paramToString(contentType); 2504 // XXX: implement the rest 2505 bodystruct += " NIL NIL NIL"; 2506 } 2507 } else { 2508 bodystruct += 2509 '"' + 2510 contentType.mediatype.toUpperCase() + 2511 '" "' + 2512 contentType.subtype.toUpperCase() + 2513 '"'; 2514 bodystruct += " " + paramToString(contentType); 2515 2516 // XXX: Content ID, Content description 2517 bodystruct += " NIL NIL"; 2518 2519 let cte = headers.has("content-transfer-encoding") 2520 ? headers.get("content-transfer-encoding") 2521 : "7BIT"; 2522 bodystruct += ' "' + cte + '"'; 2523 2524 bodystruct += " " + this.length; 2525 if (contentType.mediatype == "text") { 2526 bodystruct += " " + this.numLines; 2527 } 2528 2529 // XXX: I don't want to implement these yet 2530 if (extension) { 2531 bodystruct += " NIL NIL NIL NIL"; 2532 } 2533 } 2534 bodystruct += ")"; 2535 }, 2536 }; 2537 MimeParser.parseSync(msg, BodyStructureEmitter, {}); 2538 return bodystruct; 2539} 2540