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