/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file implements test IMAP servers

// IMAP DAEMON ORGANIZATION
// ------------------------
// The large numbers of RFCs all induce some implicit assumptions as to the
// organization of an IMAP server. Ideally, we'd like to be as inclusive as
// possible so that we can guarantee that it works for every type of server.
// Unfortunately, such all-accepting setups make generic algorithms hard to
// use; given their difficulty in a generic framework, it seems unlikely that
// a server would implement such characteristics. It also seems likely that
// if mailnews had a problem with the implementation, then most clients would
// see similar problems, so as to make the server widely unusable. In any
// case, if someone complains about not working on bugzilla, it can be added
// to the test suite.
// So, with that in mind, this is the basic layout of the daemon:
// DAEMON
// + Namespaces: parentless mailboxes whose names are the namespace name. The
//     type of the namespace is specified by the type attribute.
// + Mailboxes: ImapMailbox objects with several properties. If a mailbox
// | |   property begins with a '_', then it should not be serialized because
// | |   it can be discovered from other means; in particular, a '_' does not
// | |   necessarily mean that it is a private property that should not be
// | |   accessed. The parent of a top-level mailbox is null, not "".
// | + I18N names: RFC 3501 specifies a modified UTF-7 form for names.
// | |     However, a draft RFC makes the names UTF-8; it is expected to be
// | |     completed and implemented "soon". Therefore, the correct usage is
// | |     to specify the mailbox names as one normally does in JS and the
// | |     protocol will take care of conversion itself.
// | + Case-sensitivity: RFC 3501 takes no position on this issue, only that
// | |     a case-insensitive server must treat the base-64 parts of mailbox
// | |     names as case-sensitive. The draft UTF8 RFC says nothing on this
// | |     topic, but Crispin recommends using Unicode case-insensitivity. We
// | |     therefore treat names in such manner (if the case-insensitive flag
// | |     is set), in technical violation of RFC 3501.
// | + Flags: Flags are (as confirmed by Crispin) case-insensitive. Internal
// |       flag equality, though, uses case-sensitive checks. Therefore they
// |       should be normalized to a title-case form (e.g., \Noselect).
// + Synchronization: On certain synchronizing commands, the daemon will call
// |   a synchronizing function to allow manipulating code the chance to
// |   perform various (potentially expensive) actions.
// + Messages: A message is represented internally as an annotated URI.

import { MimeParser } from "resource:///modules/mimeParser.sys.mjs";

import {
  AuthPLAIN,
  AuthLOGIN,
  AuthCRAM,
} from "resource://testing-common/mailnews/Auth.sys.mjs";

export class ImapDaemon {
  constructor(flags, syncFunc) {
    this._flags = flags;

    this.namespaces = [];
    this.idResponse = "NIL";
    this.root = new ImapMailbox("", null, { type: IMAP_NAMESPACE_PERSONAL });
    this.uidvalidity = Math.round(Date.now() / 1000);
    this.inbox = new ImapMailbox("INBOX", null, this.uidvalidity++);
    this.root.addMailbox(this.inbox);
    this.namespaces.push(this.root);
    this.syncFunc = syncFunc;
    // This can be used to cause the artificial failure of any given command.
    this.commandToFail = "";
    // This can be used to simulate timeouts on large copies
    this.copySleep = 0;
  }
  synchronize(mailbox, update) {
    if (this.syncFunc) {
      this.syncFunc.call(null, this);
    }
    if (update) {
      for (var message of mailbox._messages) {
        message.recent = false;
      }
    }
  }
  getNamespace(name) {
    for (var namespace of this.namespaces) {
      if (
        name.indexOf(namespace.name) == 0 &&
        name[namespace.name.length] == namespace.delimiter
      ) {
        return namespace;
      }
    }
    return this.root;
  }
  createNamespace(name, type) {
    var newbox = this.createMailbox(name, { type });
    this.namespaces.push(newbox);
  }
  getMailbox(name) {
    if (name == "") {
      return this.root;
    }
    // INBOX is case-insensitive, no matter what
    if (name.toUpperCase().startsWith("INBOX")) {
      name = "INBOX" + name.substr(5);
    }
    // We want to find a child who has the same name, but we don't quite know
    // what the delimiter is. The convention is that different namespaces use a
    // name starting with '#', so that's how we'll work it out.
    let mailbox;
    if (name.startsWith("#")) {
      for (mailbox of this.root._children) {
        if (
          mailbox.name.indexOf(name) == 0 &&
          name[mailbox.name.length] == mailbox.delimiter
        ) {
          break;
        }
      }
      if (!mailbox) {
        return null;
      }

      // Now we continue like normal
      const names = name.split(mailbox.delimiter);
      names.splice(0, 1);
      for (const part of names) {
        mailbox = mailbox.getChild(part);
        if (!mailbox || mailbox.nonExistent) {
          return null;
        }
      }
    } else {
      // This is easy, just split it up using the inbox's delimiter
      const names = name.split(this.inbox.delimiter);
      mailbox = this.root;

      for (const part of names) {
        mailbox = mailbox.getChild(part);
        if (!mailbox || mailbox.nonExistent) {
          return null;
        }
      }
    }
    return mailbox;
  }
  createMailbox(name, oldBox) {
    var namespace = this.getNamespace(name);
    if (namespace.name != "") {
      name = name.substring(namespace.name.length + 1);
    }
    var prefixes = name.split(namespace.delimiter);
    var subName;
    if (prefixes[prefixes.length - 1] == "") {
      subName = prefixes.splice(prefixes.length - 2, 2)[0];
    } else {
      subName = prefixes.splice(prefixes.length - 1, 1)[0];
    }
    var box = namespace;
    for (var component of prefixes) {
      box = box.getChild(component);
      // Yes, we won't autocreate intermediary boxes
      if (box == null || box.flags.includes("\\NoInferiors")) {
        return false;
      }
    }
    // If this is an ImapMailbox...
    if (oldBox && oldBox._children) {
      // Only delete now so we don't screw ourselves up if creation fails
      this.deleteMailbox(oldBox);
      oldBox._parent = box == this.root ? null : box;
      const newBox = new ImapMailbox(subName, box, this.uidvalidity++);
      newBox._messages = oldBox._messages;
      box.addMailbox(newBox);

      // And if oldBox is an INBOX, we need to recreate that
      if (oldBox.name == "INBOX") {
        this.inbox = new ImapMailbox("INBOX", null, this.uidvalidity++);
        this.root.addMailbox(this.inbox);
      }
      oldBox.name = subName;
    } else if (oldBox) {
      // oldBox is a regular {} object, so it contains mailbox data but is not
      // a mailbox itself. Pass it into the constructor and let that deal with
      // it...
      const childBox = new ImapMailbox(
        subName,
        box == this.root ? null : box,
        oldBox
      );
      box.addMailbox(childBox);
      // And return the new mailbox, since this is being used by people setting
      // up the daemon.
      return childBox;
    } else {
      var creatable = hasFlag(this._flags, IMAP_FLAG_NEEDS_DELIMITER)
        ? name[name.length - 1] == namespace.delimiter
        : true;
      const childBox = new ImapMailbox(subName, box == this.root ? null : box, {
        flags: creatable ? [] : ["\\NoInferiors"],
        uidvalidity: this.uidvalidity++,
      });
      box.addMailbox(childBox);
    }
    return true;
  }
  deleteMailbox(mailbox) {
    if (mailbox._children.length == 0) {
      // We don't preserve the subscribed state for deleted mailboxes
      var parentBox = mailbox._parent == null ? this.root : mailbox._parent;
      parentBox._children.splice(parentBox._children.indexOf(mailbox), 1);
    } else {
      // clear mailbox
      mailbox._messages = [];
      mailbox.flags.push("\\Noselect");
    }
  }
}

export class ImapMailbox {
  constructor(name, parent, state) {
    this.name = name;
    this._parent = parent;
    this._children = [];
    this._messages = [];
    this._updates = [];

    // Shorthand for uidvalidity
    if (typeof state == "number") {
      this.uidvalidity = state;
      state = {};
    }

    if (!state) {
      state = {};
    }

    for (var prop in state) {
      this[prop] = state[prop];
    }

    this.setDefault("subscribed", false);
    this.setDefault("nonExistent", false);
    this.setDefault("delimiter", "/");
    this.setDefault("flags", []);
    this.setDefault("specialUseFlag", "");
    this.setDefault("uidnext", 1);
    this.setDefault("msgflags", [
      "\\Seen",
      "\\Answered",
      "\\Flagged",
      "\\Deleted",
      "\\Draft",
    ]);
    this.setDefault("permflags", [
      "\\Seen",
      "\\Answered",
      "\\Flagged",
      "\\Deleted",
      "\\Draft",
      "\\*",
    ]);
  }
  setDefault(prop, def) {
    this[prop] = prop in this ? this[prop] : def;
  }
  addMailbox(mailbox) {
    this._children.push(mailbox);
  }
  getChild(name) {
    for (var mailbox of this._children) {
      if (name == mailbox.name) {
        return mailbox;
      }
    }
    return null;
  }
  matchKids(pattern) {
    if (pattern == "") {
      return this._parent ? this._parent.matchKids("") : [this];
    }

    var portions = pattern.split(this.delimiter);
    var matching = [this];
    for (var folder of portions) {
      if (folder.length == 0) {
        continue;
      }

      const generator = folder.includes("*") ? "allChildren" : "_children";
      const possible = matching.reduce(function (arr, elem) {
        return arr.concat(elem[generator]);
      }, []);

      if (folder == "*" || folder == "%") {
        matching = possible;
        continue;
      }

      const parts = folder.split(/[*%]/).filter(function (str) {
        return str.length > 0;
      });
      matching = possible.filter(function (mailbox) {
        let index = 0;
        const name = mailbox.fullName;
        for (var part of parts) {
          index = name.indexOf(part, index);
          if (index == -1) {
            return false;
          }
        }
        return true;
      });
    }
    return matching;
  }
  get fullName() {
    return (
      (this._parent ? this._parent.fullName + this.delimiter : "") + this.name
    );
  }
  get displayName() {
    const manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
      Ci.nsICharsetConverterManager
    );
    // Escape backslash and double-quote with another backslash before encoding.
    return manager.unicodeToMutf7(this.fullName.replace(/([\\"])/g, "\\$1"));
  }
  get allChildren() {
    return this._children.reduce(function (arr, elem) {
      return arr.concat(elem._allChildrenInternal);
    }, []);
  }
  get _allChildrenInternal() {
    return this._children.reduce(
      function (arr, elem) {
        return arr.concat(elem._allChildrenInternal);
      },
      [this]
    );
  }
  addMessage(message) {
    this._messages.push(message);
    if (message.uid >= this.uidnext) {
      this.uidnext = message.uid + 1;
    }
    if (!this._updates.includes("EXISTS")) {
      this._updates.push("EXISTS");
    }
    if ("__highestuid" in this && message.uid > this.__highestuid) {
      this.__highestuid = message.uid;
    }
  }
  get _highestuid() {
    if ("__highestuid" in this) {
      return this.__highestuid;
    }
    var highest = 0;
    for (var message of this._messages) {
      if (message.uid > highest) {
        highest = message.uid;
      }
    }
    this.__highestuid = highest;
    return highest;
  }
  expunge() {
    var response = "";
    for (var i = 0; i < this._messages.length; i++) {
      if (this._messages[i].flags.includes("\\Deleted")) {
        response += "* " + (i + 1) + " EXPUNGE\0";
        this._messages.splice(i--, 1);
      }
    }
    if (response.length > 0) {
      delete this.__highestuid;
    }
    return response;
  }
}

export class ImapMessage {
  constructor(URI, uid, flags) {
    this._URI = URI;
    this.uid = uid;
    this.size = 0;
    this.flags = [];
    for (const flag in flags) {
      this.flags.push(flag);
    }
    this.recent = false;
  }
  get channel() {
    return Services.io.newChannel(
      this._URI,
      null,
      null,
      null,
      Services.scriptSecurityManager.getSystemPrincipal(),
      null,
      Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
      Ci.nsIContentPolicy.TYPE_OTHER
    );
  }
  setFlag(flag) {
    if (!this.flags.includes(flag)) {
      this.flags.push(flag);
    }
  }
  // This allows us to simulate servers that approximate the rfc822 size.
  setSize(size) {
    this.size = size;
  }
  clearFlag(flag) {
    const index = this.flags.indexOf(flag);
    if (index != -1) {
      this.flags.splice(index, 1);
    }
  }
  getText(start, length) {
    if (!start) {
      start = 0;
    }
    if (!length) {
      length = -1;
    }
    var channel = this.channel;
    var istream = channel.open();
    var bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
      Ci.nsIBinaryInputStream
    );
    bstream.setInputStream(istream);
    var str = bstream.readBytes(start);
    if (str.length != start) {
      throw new Error("Erm, we didn't just pass through 8-bit");
    }
    length = length == -1 ? istream.available() : length;
    if (length > istream.available()) {
      length = istream.available();
    }
    str = bstream.readBytes(length);
    return str;
  }

  get _partMap() {
    if (this.__partMap) {
      return this.__partMap;
    }
    var partMap = {};
    var emitter = {
      startPart(partNum, headers) {
        var imapPartNum = partNum.replace("$", "");
        // If there are multiple imap parts that this represents, we'll
        // overwrite with the latest. This is what we want (most deeply nested).
        partMap[imapPartNum] = [partNum, headers];
      },
    };
    MimeParser.parseSync(this.getText(), emitter, {
      bodyformat: "none",
      stripcontinuations: false,
    });
    return (this.__partMap = partMap);
  }
  getPartHeaders(partNum) {
    return this._partMap[partNum][1];
  }
  getPartBody(partNum) {
    var body = "";
    var emitter = {
      deliverPartData(partNum, data) {
        body += data;
      },
    };
    var mimePartNum = this._partMap[partNum][0];
    MimeParser.parseSync(this.getText(), emitter, {
      pruneat: mimePartNum,
      bodyformat: "raw",
    });
    return body;
  }
}

// IMAP FLAGS
// If you don't specify any flag, no flags are set.

/**
 * This flag represents whether or not CREATE hierarchies need a delimiter.
 *
 * If this flag is off, <tt>CREATE a<br />CREATE a/b</tt> fails where
 * <tt>CREATE a/<br />CREATE a/b</tt> would succeed (assuming the delimiter is
 * '/').
 */
var IMAP_FLAG_NEEDS_DELIMITER = 2;

function hasFlag(flags, flag) {
  return (flags & flag) == flag;
}

// IMAP Namespaces
var IMAP_NAMESPACE_PERSONAL = 0;
// var IMAP_NAMESPACE_OTHER_USERS = 1;
// var IMAP_NAMESPACE_SHARED = 2;

// IMAP server helpers
var IMAP_STATE_NOT_AUTHED = 0;
var IMAP_STATE_AUTHED = 1;
var IMAP_STATE_SELECTED = 2;

function parseCommand(text, partial) {
  var args = [];
  var current = args;
  var stack = [];
  if (partial) {
    args = partial.args;
    current = partial.current;
    stack = partial.stack;
    current.push(partial.text);
  }
  var atom = "";
  while (text.length > 0) {
    const c = text[0];

    if (c == '"') {
      let index = 1;
      let s = "";
      while (index < text.length && text[index] != '"') {
        if (text[index] == "\\") {
          index++;
          if (text[index] != '"' && text[index] != "\\") {
            throw new Error("Expected quoted character");
          }
        }
        s += text[index++];
      }
      if (index == text.length) {
        throw new Error("Expected DQUOTE");
      }
      current.push(s);
      text = text.substring(index + 1);
      continue;
    } else if (c == "{") {
      const end = text.indexOf("}");
      if (end == -1) {
        throw new Error("Expected CLOSE_BRACKET");
      }
      if (end + 1 != text.length) {
        throw new Error("Expected CRLF");
      }
      const length = parseInt(text.substring(1, end));
      // Usable state
      // eslint-disable-next-line no-throw-literal
      throw { length, current, args, stack, text: "" };
    } else if (c == "(") {
      stack.push(current);
      current = [];
    } else if (c == ")") {
      if (atom.length > 0) {
        current.push(atom);
        atom = "";
      }
      const hold = current;
      current = stack.pop();
      if (current == undefined) {
        throw new Error("Unexpected CLOSE_PAREN");
      }
      current.push(hold);
    } else if (c == " ") {
      if (atom.length > 0) {
        current.push(atom);
        atom = "";
      }
    } else if (
      text.toUpperCase().startsWith("NIL") &&
      (text.length == 3 || text[3] == " ")
    ) {
      current.push(null);
      text = text.substring(4);
      continue;
    } else {
      atom += c;
    }
    text = text.substring(1);
  }
  if (stack.length != 0) {
    throw new Error("Expected CLOSE_PAREN!");
  }
  if (atom.length > 0) {
    args.push(atom);
  }
  return args;
}

function formatArg(argument, spec) {
  // Get NILs out of the way quickly
  var nilAccepted = false;
  if (spec.startsWith("n") && spec[1] != "u") {
    spec = spec.substring(1);
    nilAccepted = true;
  }
  if (argument == null) {
    if (!nilAccepted) {
      throw new Error("Unexpected NIL!");
    }

    return null;
  }

  // array!
  if (spec.startsWith("(")) {
    // typeof array is object. Don't ask me why.
    if (!Array.isArray(argument)) {
      throw new Error("Expected list!");
    }
    // Strip the '(' and ')'...
    spec = spec.substring(1, spec.length - 1);
    // ... and apply to the rest
    return argument.map(function (item) {
      return formatArg(item, spec);
    });
  }

  // or!
  var pipe = spec.indexOf("|");
  if (pipe > 0) {
    var first = spec.substring(0, pipe);
    try {
      return formatArg(argument, first);
    } catch (e) {
      return formatArg(argument, spec.substring(pipe + 1));
    }
  }

  // By now, we know that the input should be generated from an atom or string.
  if (typeof argument != "string") {
    throw new Error("Expected argument of type " + spec + "!");
  }

  if (spec == "atom") {
    argument = argument.toUpperCase();
  } else if (spec == "mailbox") {
    const manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
      Ci.nsICharsetConverterManager
    );
    argument = manager.mutf7ToUnicode(argument);
  } else if (spec == "string") {
    // Do nothing
  } else if (spec == "flag") {
    argument = argument.toLowerCase();
    if (
      !("a" <= argument[0] && argument[0] <= "z") &&
      !("A" <= argument[0] && argument[0] <= "Z")
    ) {
      argument = argument[0] + argument[1].toUpperCase() + argument.substr(2);
    } else {
      argument = argument[0].toUpperCase() + argument.substr(1);
    }
  } else if (spec == "number") {
    if (argument == parseInt(argument)) {
      argument = parseInt(argument);
    }
  } else if (spec == "date") {
    if (
      !/^\d{1,2}-[A-Z][a-z]{2}-\d{4}( \d{2}(:\d{2}){2} [+-]\d{4})?$/.test(
        argument
      )
    ) {
      throw new Error("Expected date!");
    }
    argument = new Date(Date.parse(argument.replace(/-(?!\d{4}$)/g, " ")));
  } else {
    throw new Error("Unknown spec " + spec);
  }

  return argument;
}

// IMAP TEST SERVERS
// -----------------
// Because of IMAP and the LEMONADE RFCs, we have a myriad of different
// server configurations that we should ideally be supporting. We handle them
// by defining a core RFC 3501 implementation and then have different server
// extensions subclass the server through functions below. However, we also
// provide standard configurations for best handling.
// Configurations:
// * Barebones RFC 3501
// * Cyrus
// * UW IMAP
// * Courier
// * Exchange
// * Dovecot
// * Zimbra
// * GMail
// KNOWN DEVIATIONS FROM RFC 3501:
// + The autologout timer is 3 minutes, not 30 minutes. A test with a logout
//   of 30 minutes would take a very long time if it failed.
// + SEARCH (except for UNDELETED) and STARTTLS are not supported,
//   nor is all of FETCH.
// + Concurrent mailbox access is probably compliant with a rather liberal
//   implementation of RFC 3501, although probably not what one would expect,
//   and certainly not what the Dovecot IMAP server tests expect.

/* IMAP Fakeserver operates in a different manner than the rest of fakeserver
 * because of some differences in the protocol. Commands are dispatched through
 * onError, which parses the message into components. Like other fakeserver
 * implementations, the command property will be called, but this time with an
 * argument that is an array of data items instead of a string representing the
 * rest of the line.
 */
export class IMAP_RFC3501_handler {
  constructor(
    daemon,
    { username = "user", password = "password", authSchemes = [] } = {}
  ) {
    this.kUsername = username;
    this.kPassword = password;
    this.kAuthSchemes = authSchemes; // Added by RFC2195 extension.
    this.kCapabilities = [
      /* "LOGINDISABLED", "STARTTLS", */
      "CLIENTID",
    ]; // Test may modify as needed.
    this.kUidCommands = ["FETCH", "STORE", "SEARCH", "COPY"];

    this._daemon = daemon;
    this.closing = false;
    this.dropOnStartTLS = false;
    // map: property = auth scheme {String}, value = start function on this obj
    this._kAuthSchemeStartFunction = {};

    this._enabledCommands = {
      // IMAP_STATE_NOT_AUTHED
      0: [
        "CAPABILITY",
        "NOOP",
        "LOGOUT",
        "STARTTLS",
        "CLIENTID",
        "AUTHENTICATE",
        "LOGIN",
      ],
      // IMAP_STATE_AUTHED
      1: [
        "CAPABILITY",
        "NOOP",
        "LOGOUT",
        "SELECT",
        "EXAMINE",
        "CREATE",
        "DELETE",
        "RENAME",
        "SUBSCRIBE",
        "UNSUBSCRIBE",
        "LIST",
        "LSUB",
        "STATUS",
        "APPEND",
      ],
      // IMAP_STATE_SELECTED
      2: [
        "CAPABILITY",
        "NOOP",
        "LOGOUT",
        "SELECT",
        "EXAMINE",
        "CREATE",
        "DELETE",
        "RENAME",
        "SUBSCRIBE",
        "UNSUBSCRIBE",
        "LIST",
        "LSUB",
        "STATUS",
        "APPEND",
        "CHECK",
        "CLOSE",
        "EXPUNGE",
        "SEARCH",
        "FETCH",
        "STORE",
        "COPY",
        "UID",
      ],
    };
    // Format explanation:
    // atom -> UPPERCASE
    // string -> don't touch!
    // mailbox -> Apply ->UTF16 transformation with case-insensitivity stuff
    // flag -> Titlecase (or \Titlecase, $Titlecase, etc.)
    // date -> Make it a JSDate object
    // number -> Make it a number, if possible
    // ( ) -> list, apply flags as specified
    // [ ] -> optional argument.
    // x|y -> either x or y format.
    // ... -> variable args, don't parse
    this._argFormat = {
      CAPABILITY: [],
      NOOP: [],
      LOGOUT: [],
      STARTTLS: [],
      CLIENTID: ["string", "string"],
      AUTHENTICATE: ["atom", "..."],
      LOGIN: ["string", "string"],
      SELECT: ["mailbox"],
      EXAMINE: ["mailbox"],
      CREATE: ["mailbox"],
      DELETE: ["mailbox"],
      RENAME: ["mailbox", "mailbox"],
      SUBSCRIBE: ["mailbox"],
      UNSUBSCRIBE: ["mailbox"],
      LIST: ["mailbox", "mailbox"],
      LSUB: ["mailbox", "mailbox"],
      STATUS: ["mailbox", "(atom)"],
      APPEND: ["mailbox", "[(flag)]", "[date]", "string"],
      CHECK: [],
      CLOSE: [],
      EXPUNGE: [],
      SEARCH: ["atom", "..."],
      FETCH: ["number", "atom|(atom|(atom))"],
      STORE: ["number", "atom", "flag|(flag)"],
      COPY: ["number", "mailbox"],
      UID: ["atom", "..."],
    };

    this.resetTest();
  }
  resetTest() {
    this._state = IMAP_STATE_NOT_AUTHED;
    this._multiline = false;
    this._nextAuthFunction = undefined; // should be in RFC2195_ext, but too lazy
  }
  onStartup() {
    this._state = IMAP_STATE_NOT_AUTHED;
    return "* OK IMAP4rev1 Fakeserver started up";
  }

  // CENTRALIZED DISPATCH FUNCTIONS

  // IMAP sends commands in the form of "tag command args", but fakeserver
  // parsing tries to call the tag, which doesn't exist. Instead, we use this
  // error method to do the actual command dispatch. Mailnews uses numbers for
  // tags, which won't impede on actual commands.
  onError(tag, realLine) {
    this._tag = tag;
    var space = realLine.indexOf(" ");
    var command = space == -1 ? realLine : realLine.substring(0, space);
    realLine = space == -1 ? "" : realLine.substring(space + 1);

    // Now parse realLine into an array of atoms, etc.
    try {
      var args = parseCommand(realLine);
    } catch (state) {
      if (typeof state == "object") {
        this._partial = state;
        this._partial.command = command;
        this._multiline = true;
        return "+ More!";
      }

      return this._tag + " BAD " + state;
    }

    // If we're here, we have a command with arguments. Dispatch!
    return this._dispatchCommand(command, args);
  }
  onMultiline(line) {
    // A multiline arising form a literal being passed
    if (this._partial) {
      // There are two cases to be concerned with:
      // 1. The CRLF is internal or end (we want more)
      // 1a. The next line is the actual command stuff!
      // 2. The CRLF is in the middle (rest of the line is args)
      if (this._partial.length >= line.length + 2) {
        // Case 1
        this._partial.text += line + "\r\n";
        this._partial.length -= line.length + 2;
        return undefined;
      } else if (this._partial.length != 0) {
        this._partial.text += line.substring(0, this._partial.length);
        line = line.substring(this._partial.length);
      }
      var command = this._partial.command;
      var args;
      try {
        args = parseCommand(line, this._partial);
      } catch (state) {
        if (typeof state == "object") {
          // Yet another literal coming around...
          this._partial = state;
          this._partial.command = command;
          return "+ I'll be needing more text";
        }

        this._multiline = false;
        return this.tag + " BAD parse error: " + state;
      }

      this._partial = undefined;
      this._multiline = false;
      return this._dispatchCommand(command, args);
    }

    if (this._nextAuthFunction) {
      var func = this._nextAuthFunction;
      this._multiline = false;
      this._nextAuthFunction = undefined;
      if (line == "*") {
        return this._tag + " BAD Okay, as you wish. Chicken";
      }
      if (!func || typeof func != "function") {
        return this._tag + " BAD I'm lost. Internal server error during auth";
      }
      try {
        return this._tag + " " + func.call(this, line);
      } catch (e) {
        return this._tag + " BAD " + e;
      }
    }
    return undefined;
  }
  _dispatchCommand(command, args) {
    this.sendingLiteral = false;
    command = command.toUpperCase();
    if (command == this._daemon.commandToFail.toUpperCase()) {
      return this._tag + " NO " + command + " failed";
    }
    var response;
    if (command in this) {
      this._lastCommand = command;
      // Are we allowed to execute this command?
      if (!this._enabledCommands[this._state].includes(command)) {
        return (
          this._tag + " BAD illegal command for current state " + this._state
        );
      }

      try {
        // Format the arguments nicely
        args = this._treatArgs(args, command);

        // UID command by itself is not useful for PerformTest
        if (command == "UID") {
          this._lastCommand += " " + args[0];
        }

        // Finally, run the thing
        response = this[command](args);
      } catch (e) {
        if (typeof e == "string") {
          response = e;
        } else {
          throw e;
        }
      }
    } else {
      response = "BAD " + command + " not implemented";
    }

    // Add status updates
    if (this._selectedMailbox) {
      for (var update of this._selectedMailbox._updates) {
        let line;
        switch (update) {
          case "EXISTS":
            line = "* " + this._selectedMailbox._messages.length + " EXISTS";
            break;
        }
        response = line + "\0" + response;
      }
    }

    var lines = response.split("\0");
    response = "";
    for (const line of lines) {
      if (!line.startsWith("+") && !line.startsWith("*")) {
        response += this._tag + " ";
      }
      response += line + "\r\n";
    }
    return response;
  }
  _treatArgs(args, command) {
    var format = this._argFormat[command];
    var treatedArgs = [];
    for (var i = 0; i < format.length; i++) {
      var spec = format[i];

      if (spec == "...") {
        treatedArgs = treatedArgs.concat(args);
        args = [];
        break;
      }

      if (args.length == 0) {
        if (spec.startsWith("[")) {
          // == optional arg
          continue;
        } else {
          throw new Error("BAD not enough arguments");
        }
      }

      if (spec.startsWith("[")) {
        // We have an optional argument. See if the format matches and move on
        // if it doesn't. Ideally, we'd rethink our decision if a later
        // application turns out to be wrong, but that's ugly to do
        // iteratively. Should any IMAP extension require it, we'll have to
        // come back and change this assumption, though.
        spec = spec.substr(1, spec.length - 2);
        try {
          var out = formatArg(args[0], spec);
        } catch (e) {
          continue;
        }
        treatedArgs.push(out);
        args.shift();
        continue;
      }
      try {
        treatedArgs.push(formatArg(args.shift(), spec));
      } catch (e) {
        throw new Error("BAD " + e);
      }
    }
    if (args.length != 0) {
      throw new Error("BAD Too many arguments");
    }
    return treatedArgs;
  }

  // PROTOCOL COMMANDS (ordered as in spec)

  CAPABILITY() {
    var capa = "* CAPABILITY IMAP4rev1 " + this.kCapabilities.join(" ");
    if (this.kAuthSchemes.length > 0) {
      capa += " AUTH=" + this.kAuthSchemes.join(" AUTH=");
    }
    capa += "\0OK CAPABILITY completed";
    return capa;
  }
  CLIENTID() {
    return "OK Recognized a valid CLIENTID command, used for authentication methods";
  }
  LOGOUT() {
    this.closing = true;
    if (this._selectedMailbox) {
      this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
    }
    this._state = IMAP_STATE_NOT_AUTHED;
    return "* BYE IMAP4rev1 Logging out\0OK LOGOUT completed";
  }
  NOOP() {
    return "OK NOOP completed";
  }
  STARTTLS() {
    // simulate annoying server that drops connection on STARTTLS
    if (this.dropOnStartTLS) {
      this.closing = true;
      return "";
    }
    return "BAD maild doesn't support TLS ATM";
  }
  _nextAuthFunction = undefined;
  AUTHENTICATE(args) {
    var scheme = args[0]; // already uppercased by type "atom"
    // |scheme| contained in |kAuthSchemes|?
    if (
      !this.kAuthSchemes.some(function (s) {
        return s == scheme;
      })
    ) {
      return "-ERR AUTH " + scheme + " not supported";
    }

    var func = this._kAuthSchemeStartFunction[scheme];
    if (!func || typeof func != "function") {
      return (
        "BAD I just pretended to implement AUTH " + scheme + ", but I don't"
      );
    }
    return func.apply(this, args.slice(1));
  }
  LOGIN(args) {
    if (
      this.kCapabilities.some(function (c) {
        return c == "LOGINDISABLED";
      })
    ) {
      return "BAD old-style LOGIN is disabled, use AUTHENTICATE";
    }
    if (args[0] == this.kUsername && args[1] == this.kPassword) {
      this._state = IMAP_STATE_AUTHED;
      return "OK authenticated";
    }
    return "BAD invalid password, I won't authenticate you";
  }
  SELECT(args) {
    var box = this._daemon.getMailbox(args[0]);
    if (!box) {
      return "NO no such mailbox";
    }

    if (this._selectedMailbox) {
      this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
    }
    this._state = IMAP_STATE_SELECTED;
    this._selectedMailbox = box;
    this._readOnly = false;

    var response = "* FLAGS (" + box.msgflags.join(" ") + ")\0";
    response += "* " + box._messages.length + " EXISTS\0* ";
    response += box._messages.reduce(function (count, message) {
      return count + (message.recent ? 1 : 0);
    }, 0);
    response += " RECENT\0";
    for (var i = 0; i < box._messages.length; i++) {
      if (!box._messages[i].flags.includes("\\Seen")) {
        response += "* OK [UNSEEN " + (i + 1) + "]\0";
        break;
      }
    }
    response += "* OK [PERMANENTFLAGS (" + box.permflags.join(" ") + ")]\0";
    response += "* OK [UIDNEXT " + box.uidnext + "]\0";
    if ("uidvalidity" in box) {
      response += "* OK [UIDVALIDITY " + box.uidvalidity + "]\0";
    }
    return response + "OK [READ-WRITE] SELECT completed";
  }
  EXAMINE(args) {
    var box = this._daemon.getMailbox(args[0]);
    if (!box) {
      return "NO no such mailbox";
    }

    if (this._selectedMailbox) {
      this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
    }
    this._state = IMAP_STATE_SELECTED;
    this._selectedMailbox = box;
    this._readOnly = true;

    var response = "* FLAGS (" + box.msgflags.join(" ") + ")\0";
    response += "* " + box._messages.length + " EXISTS\0* ";
    response += box._messages.reduce(function (count, message) {
      return count + (message.recent ? 1 : 0);
    }, 0);
    response += " RECENT\0";
    for (var i = 0; i < box._messages.length; i++) {
      if (!box._messages[i].flags.includes("\\Seen")) {
        response += "* OK [UNSEEN " + (i + 1) + "]\0";
        break;
      }
    }
    response += "* OK [PERMANENTFLAGS (" + box.permflags.join(" ") + ")]\0";
    response += "* OK [UIDNEXT " + box.uidnext + "]\0";
    response += "* OK [UIDVALIDITY " + box.uidvalidity + "]\0";
    return response + "OK [READ-ONLY] EXAMINE completed";
  }
  CREATE(args) {
    if (this._daemon.getMailbox(args[0])) {
      return "NO mailbox already exists";
    }
    if (!this._daemon.createMailbox(args[0])) {
      return "NO cannot create mailbox";
    }
    return "OK CREATE completed";
  }
  DELETE(args) {
    var mbox = this._daemon.getMailbox(args[0]);
    if (!mbox || mbox.name == "") {
      return "NO no such mailbox";
    }
    if (mbox._children.length > 0) {
      for (let i = 0; i < mbox.flags.length; i++) {
        if (mbox.flags[i] == "\\Noselect") {
          return "NO cannot delete mailbox";
        }
      }
    }
    this._daemon.deleteMailbox(mbox);
    return "OK DELETE completed";
  }
  RENAME(args) {
    var mbox = this._daemon.getMailbox(args[0]);
    if (!mbox || mbox.name == "") {
      return "NO no such mailbox";
    }
    if (!this._daemon.createMailbox(args[1], mbox)) {
      return "NO cannot rename mailbox";
    }
    return "OK RENAME completed";
  }
  SUBSCRIBE(args) {
    var mailbox = this._daemon.getMailbox(args[0]);
    if (!mailbox) {
      return "NO error in subscribing";
    }
    mailbox.subscribed = true;
    return "OK SUBSCRIBE completed";
  }
  UNSUBSCRIBE(args) {
    var mailbox = this._daemon.getMailbox(args[0]);
    if (mailbox) {
      mailbox.subscribed = false;
    }
    return "OK UNSUBSCRIBE completed";
  }
  LIST(args) {
    // even though this is the LIST function for RFC 3501, code for
    // LIST-EXTENDED (RFC 5258) is included here to keep things simple and
    // avoid duplication. We can get away with this because the _treatArgs
    // function filters out invalid args for servers that don't support
    // LIST-EXTENDED before they even get here.

    let listFunctionName = "_LIST";
    // check for optional list selection options argument used by LIST-EXTENDED
    // and other related RFCs
    if (args.length == 3 || (args.length > 3 && args[3] == "RETURN")) {
      let selectionOptions = args.shift();
      selectionOptions = selectionOptions.toString().split(" ");
      selectionOptions.sort();
      for (const option of selectionOptions) {
        listFunctionName += "_" + option.replace(/-/g, "_");
      }
    }
    // check for optional list return options argument used by LIST-EXTENDED
    // and other related RFCs
    if (
      (args.length > 2 && args[2] == "RETURN") ||
      this.kCapabilities.includes("CHILDREN")
    ) {
      listFunctionName += "_RETURN";
      const returnOptions = args[3] ? args[3].toString().split(" ") : [];
      if (
        this.kCapabilities.includes("CHILDREN") &&
        !returnOptions.includes("CHILDREN")
      ) {
        returnOptions.push("CHILDREN");
      }
      returnOptions.sort();
      for (const option of returnOptions) {
        listFunctionName += "_" + option.replace(/-/g, "_");
      }
    }
    if (!this[listFunctionName]) {
      return "BAD unknown LIST request options";
    }

    const base = this._daemon.getMailbox(args[0]);
    if (!base) {
      return "NO no such mailbox";
    }
    let requestedBoxes;
    // check for multiple mailbox patterns used by LIST-EXTENDED
    // and other related RFCs
    if (args[1].startsWith("(")) {
      requestedBoxes = parseCommand(args[1])[0];
    } else {
      requestedBoxes = [args[1]];
    }
    let response = "";
    for (const requestedBox of requestedBoxes) {
      const people = base.matchKids(requestedBox);
      for (const box of people) {
        response += this[listFunctionName](box);
      }
    }
    return response + "OK LIST completed";
  }
  // _LIST is the standard LIST command response
  _LIST(aBox) {
    if (aBox.nonExistent) {
      return "";
    }
    return (
      "* LIST (" +
      aBox.flags.join(" ") +
      ') "' +
      aBox.delimiter +
      '" "' +
      aBox.displayName +
      '"\0'
    );
  }
  LSUB(args) {
    var base = this._daemon.getMailbox(args[0]);
    if (!base) {
      return "NO no such mailbox";
    }
    var people = base.matchKids(args[1]);
    var response = "";
    for (var box of people) {
      if (box.subscribed) {
        response +=
          '* LSUB () "' + box.delimiter + '" "' + box.displayName + '"\0';
      }
    }
    return response + "OK LSUB completed";
  }
  STATUS(args) {
    var box = this._daemon.getMailbox(args[0]);
    if (!box) {
      return "NO no such mailbox exists";
    }
    for (let i = 0; i < box.flags.length; i++) {
      if (box.flags[i] == "\\Noselect") {
        return "NO STATUS not allowed on Noselect folder";
      }
    }
    var parts = [];
    for (var status of args[1]) {
      var line = status + " ";
      switch (status) {
        case "MESSAGES":
          line += box._messages.length;
          break;
        case "RECENT":
          line += box._messages.reduce(function (count, message) {
            return count + (message.recent ? 1 : 0);
          }, 0);
          break;
        case "UIDNEXT":
          line += box.uidnext;
          break;
        case "UIDVALIDITY":
          line += box.uidvalidity;
          break;
        case "UNSEEN":
          line += box._messages.reduce(function (count, message) {
            return count + (message.flags.includes("\\Seen") ? 0 : 1);
          }, 0);
          break;
        default:
          return "BAD unknown status flag: " + status;
      }
      parts.push(line);
    }
    return (
      '* STATUS "' +
      args[0] +
      '" (' +
      parts.join(" ") +
      ")\0OK STATUS completed"
    );
  }
  APPEND(args) {
    var mailbox = this._daemon.getMailbox(args[0]);
    if (!mailbox) {
      return "NO [TRYCREATE] no such mailbox";
    }
    var flags, date, text;
    if (args.length == 3) {
      if (args[1] instanceof Date) {
        flags = [];
        date = args[1];
      } else {
        flags = args[1];
        date = Date.now();
      }
      text = args[2];
    } else if (args.length == 4) {
      flags = args[1];
      date = args[2];
      text = args[3];
    } else {
      flags = [];
      date = Date.now();
      text = args[1];
    }
    var msg = new ImapMessage(
      "data:text/plain," + encodeURI(text),
      mailbox.uidnext++,
      flags
    );
    msg.recent = true;
    msg.date = date;
    mailbox.addMessage(msg);
    return "OK APPEND complete";
  }
  CHECK() {
    this._daemon.synchronize(this._selectedMailbox, false);
    return "OK CHECK completed";
  }
  CLOSE() {
    this._selectedMailbox.expunge();
    this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
    this._selectedMailbox = null;
    this._state = IMAP_STATE_AUTHED;
    return "OK CLOSE completed";
  }
  EXPUNGE() {
    // Will be either empty or LF-terminated already
    var response = this._selectedMailbox.expunge();
    this._daemon.synchronize(this._selectedMailbox);
    return response + "OK EXPUNGE completed";
  }
  SEARCH(args) {
    if (args[0] == "UNDELETED") {
      let response = "* SEARCH";
      const messages = this._selectedMailbox._messages;
      for (let i = 0; i < messages.length; i++) {
        if (!messages[i].flags.includes("\\Deleted")) {
          response += " " + messages[i].uid;
        }
      }
      response += "\0";
      return response + "OK SEARCH COMPLETED";
    }
    return "BAD not here yet";
  }
  FETCH(args, uid) {
    // Step 1: Get the messages to fetch
    var ids = [];
    var messages = this._parseSequenceSet(args[0], uid, ids);

    // Step 2: Ensure that the fetching items are in a neat format
    if (typeof args[1] == "string") {
      if (args[1] in this.fetchMacroExpansions) {
        args[1] = this.fetchMacroExpansions[args[1]];
      } else {
        args[1] = [args[1]];
      }
    }
    if (uid && !args[1].includes("UID")) {
      args[1].push("UID");
    }

    // Step 2.1: Preprocess the item fetch stack
    var items = [],
      prefix = undefined;
    for (let item of args[1]) {
      if (item.indexOf("[") > 0 && !item.includes("]")) {
        // We want to append everything into an item until we find a ']'
        prefix = item + " ";
        continue;
      }
      if (prefix !== undefined) {
        if (typeof item != "string" || !item.includes("]")) {
          prefix +=
            (typeof item == "string" ? item : "(" + item.join(" ") + ")") + " ";
          continue;
        }
        // Replace superfluous space with a ']'.
        prefix = prefix.substr(0, prefix.length - 1) + "]";
        item = prefix;
        prefix = undefined;
      }
      item = item.toUpperCase();
      if (!items.includes(item)) {
        items.push(item);
      }
    }

    // Step 3: Fetch time!
    var response = "";
    for (var i = 0; i < messages.length; i++) {
      response += "* " + ids[i] + " FETCH (";
      var parts = [];
      const flagsBefore = messages[i].flags.slice();
      for (const item of items) {
        // Brief explanation: an item like BODY[]<> can't be hardcoded easily,
        // so we go for the initial alphanumeric substring, passing in the
        // actual string as an optional second part.
        var front = item.split(/[^A-Z0-9-]/, 1)[0];
        var functionName = "_FETCH_" + front.replace(/-/g, "_");

        if (!(functionName in this)) {
          return "BAD can't fetch " + front;
        }
        try {
          parts.push(this[functionName](messages[i], item));
        } catch (ex) {
          return "BAD error in fetching: " + ex;
        }
      }
      const flagsAfter = messages[i].flags;
      if (
        !items.includes("FLAGS") &&
        (flagsAfter.length != flagsBefore.length ||
          flagsAfter.some((f, i) => f != flagsBefore[i]))
      ) {
        // Flags changed, send them too, even though they weren't requested.
        parts.push(this._FETCH_FLAGS(messages[i], "FLAGS"));
      }
      response += parts.join(" ") + ")\0";
    }
    return response + "OK FETCH completed";
  }
  STORE(args, uid) {
    var ids = [];
    var messages = this._parseSequenceSet(args[0], uid, ids);

    args[1] = args[1].toUpperCase();
    var silent = args[1].includes(".SILENT", 1);
    if (silent) {
      args[1] = args[1].substring(0, args[1].indexOf("."));
    }

    if (typeof args[2] != "object") {
      args[2] = [args[2]];
    }

    var response = "";
    for (var i = 0; i < messages.length; i++) {
      var message = messages[i];
      switch (args[1]) {
        case "FLAGS":
          message.flags = args[2];
          break;
        case "+FLAGS":
          for (const flag of args[2]) {
            message.setFlag(flag);
          }
          break;
        case "-FLAGS":
          for (const flag of args[2]) {
            var index;
            if ((index = message.flags.indexOf(flag)) != -1) {
              message.flags.splice(index, 1);
            }
          }
          break;
        default:
          return "BAD change what now?";
      }
      response += "* " + ids[i] + " FETCH (FLAGS (";
      response += message.flags.join(" ");
      response += "))\0";
    }
    if (silent) {
      response = "";
    }
    return response + "OK STORE completed";
  }
  COPY(args, uid) {
    var messages = this._parseSequenceSet(args[0], uid);

    var dest = this._daemon.getMailbox(args[1]);
    if (!dest) {
      return "NO [TRYCREATE] what mailbox?";
    }

    for (var message of messages) {
      const newMessage = new ImapMessage(
        message._URI,
        dest.uidnext++,
        message.flags
      );
      newMessage.recent = false;
      dest.addMessage(newMessage);
    }
    if (this._daemon.copySleep > 0) {
      // spin rudely for copyTimeout milliseconds.
      const now = new Date();
      let alarm;
      const startingMSeconds = now.getTime();
      // @see https://github.com/eslint/eslint/issues/17807
      // eslint-disable-next-line no-constant-condition
      while (true) {
        alarm = new Date();
        if (alarm.getTime() - startingMSeconds > this._daemon.copySleep) {
          break;
        }
      }
    }
    return "OK COPY completed";
  }
  UID(args) {
    var name = args.shift();
    if (!this.kUidCommands.includes(name)) {
      return "BAD illegal command " + name;
    }

    args = this._treatArgs(args, name);
    return this[name](args, true);
  }

  postCommand(reader) {
    if (this.closing) {
      this.closing = false;
      reader.closeSocket();
    }
    if (this.sendingLiteral) {
      reader.preventLFMunge();
    }
    reader.setMultiline(this._multiline);
    if (this._lastCommand == reader.watchWord) {
      reader.stopTest();
    }
  }
  onServerFault(e) {
    return (
      ("_tag" in this ? this._tag : "*") + " BAD Internal server error: " + e
    );
  }

  // FETCH sub commands and helpers

  fetchMacroExpansions = {
    ALL: ["FLAGS", "INTERNALDATE", "RFC822.SIZE" /* , "ENVELOPE" */],
    FAST: ["FLAGS", "INTERNALDATE", "RFC822.SIZE"],
    FULL: ["FLAGS", "INTERNALDATE", "RFC822.SIZE" /* , "ENVELOPE", "BODY" */],
  };
  _parseSequenceSet(set, uid, ids /* optional */) {
    if (typeof set == "number") {
      if (uid) {
        for (let i = 0; i < this._selectedMailbox._messages.length; i++) {
          var message = this._selectedMailbox._messages[i];
          if (message.uid == set) {
            if (ids) {
              ids.push(i + 1);
            }
            return [message];
          }
        }
        return [];
      }
      if (!(set - 1 in this._selectedMailbox._messages)) {
        return [];
      }
      if (ids) {
        ids.push(set);
      }
      return [this._selectedMailbox._messages[set - 1]];
    }

    var daemon = this;
    function part2num(part) {
      if (part == "*") {
        if (uid) {
          return daemon._selectedMailbox._highestuid;
        }
        return daemon._selectedMailbox._messages.length;
      }
      const re = /[0-9]/g;
      const num = part.match(re);
      if (!num || num.length != part.length) {
        throw new Error("BAD invalid UID " + part);
      }
      return parseInt(part);
    }

    var elements = set.split(/,/);
    set = [];
    for (var part of elements) {
      if (!part.includes(":")) {
        set.push(part2num(part));
      } else {
        var range = part.split(/:/);
        range[0] = part2num(range[0]);
        range[1] = part2num(range[1]);
        if (range[0] > range[1]) {
          const temp = range[1];
          range[1] = range[0];
          range[0] = temp;
        }
        for (let i = range[0]; i <= range[1]; i++) {
          set.push(i);
        }
      }
    }
    set.sort();
    for (let i = set.length - 1; i > 0; i--) {
      if (set[i] == set[i - 1]) {
        set.splice(i, 0);
      }
    }

    if (!ids) {
      ids = [];
    }
    var messages;
    if (uid) {
      messages = this._selectedMailbox._messages.filter(function (msg, i) {
        if (!set.includes(msg.uid)) {
          return false;
        }
        ids.push(i + 1);
        return true;
      });
    } else {
      messages = [];
      for (var id of set) {
        if (id - 1 in this._selectedMailbox._messages) {
          ids.push(id);
          messages.push(this._selectedMailbox._messages[id - 1]);
        }
      }
    }
    return messages;
  }
  _FETCH_BODY(message, query) {
    if (query == "BODY") {
      return "BODYSTRUCTURE " + bodystructure(message.getText(), false);
    }
    // parts = [ name, section, empty, {, partial, empty } ]
    var parts = query.split(/[[\]<>]/);

    if (parts[0] != "BODY.PEEK" && !this._readOnly) {
      message.setFlag("\\Seen");
    }

    if (parts[3]) {
      parts[3] = parts[3].split(/\./).map(function (e) {
        return parseInt(e);
      });
    }

    if (parts[1].length == 0) {
      // Easy case: we have BODY[], just send the message...
      let response = "BODY[]";
      var text;
      if (parts[3]) {
        response += "<" + parts[3][0] + ">";
        text = message.getText(parts[3][0], parts[3][1]);
      } else {
        text = message.getText();
      }
      response += " {" + text.length + "}\r\n";
      response += text;
      return response;
    }

    // What's inside the command?
    var data = /((?:\d+\.)*\d+)(?:\.([^ ]+))?/.exec(parts[1]);
    var partNum;
    if (data) {
      partNum = data[1];
      query = data[2];
    } else {
      partNum = "";
      if (parts[1].includes(" ", 1)) {
        query = parts[1].substring(0, parts[1].indexOf(" "));
      } else {
        query = parts[1];
      }
    }
    var queryArgs;
    if (parts[1].includes(" ", 1)) {
      queryArgs = parseCommand(parts[1].substr(parts[1].indexOf(" ")))[0];
    } else {
      queryArgs = [];
    }

    // Now we have three parameters representing the part number (empty for top-
    // level), the subportion representing what we want to find (empty for the
    // body), and an array of arguments if we have a subquery. If we made an
    // error here, it will pop until it gets to FETCH, which will just pop at a
    // BAD response, which is what should happen if the query is malformed.
    // Now we dump it all off onto ImapMessage to mess with.

    // Start off the response
    let response = "BODY[" + parts[1] + "]";
    if (parts[3]) {
      response += "<" + parts[3][0] + ">";
    }
    response += " ";

    data = "";
    switch (query) {
      case "":
      case "TEXT":
        data += message.getPartBody(partNum);
        break;
      case "HEADER": // I believe this specifies mime for an RFC822 message only
        data += message.getPartHeaders(partNum).rawHeaderText + "\r\n";
        break;
      case "MIME":
        data += message.getPartHeaders(partNum).rawHeaderText + "\r\n\r\n";
        break;
      case "HEADER.FIELDS": {
        const joinList = [];
        const headers = message.getPartHeaders(partNum);
        for (let header of queryArgs) {
          header = header.toLowerCase();
          if (headers.has(header)) {
            joinList.push(
              headers
                .getRawHeader(header)
                .map(value => `${header}: ${value}`)
                .join("\r\n")
            );
          }
        }
        data += joinList.join("\r\n") + "\r\n";
        break;
      }
      case "HEADER.FIELDS.NOT": {
        const joinList = [];
        const headers = message.getPartHeaders(partNum);
        for (const header of headers) {
          if (!(header in queryArgs)) {
            joinList.push(
              headers
                .getRawHeader(header)
                .map(value => `${header}: ${value}`)
                .join("\r\n")
            );
          }
        }
        data += joinList.join("\r\n") + "\r\n";
        break;
      }
      default:
        data += message.getPartBody(partNum);
    }

    this.sendingLiteral = true;
    response += "{" + data.length + "}\r\n";
    response += data;
    return response;
  }
  _FETCH_BODYSTRUCTURE(message) {
    return "BODYSTRUCTURE " + bodystructure(message.getText(), true);
  }
  // _FETCH_ENVELOPE,
  _FETCH_FLAGS(message) {
    var response = "FLAGS (";
    response += message.flags.join(" ");
    if (message.recent) {
      response += " \\Recent";
    }
    response += ")";
    return response;
  }
  _FETCH_INTERNALDATE(message) {
    const date = message.date;
    // Format timestamp as: "%d-%b-%Y %H:%M:%S %z" (%b in English).
    const year = date.getFullYear().toString();
    const month = date.toLocaleDateString("en-US", { month: "short" });
    const day = date.getDate().toString();
    const hours = date.getHours().toString().padStart(2, "0");
    const minutes = date.getMinutes().toString().padStart(2, "0");
    const seconds = date.getSeconds().toString().padStart(2, "0");
    const offset = date.getTimezoneOffset();
    const tzoff =
      Math.floor(Math.abs(offset) / 60) * 100 + (Math.abs(offset) % 60);
    const timeZone =
      (offset < 0 ? "+" : "-") + tzoff.toString().padStart(4, "0");

    let response = 'INTERNALDATE "';
    response += `${day}-${month}-${year} ${hours}:${minutes}:${seconds} ${timeZone}`;
    response += '"';
    return response;
  }
  _FETCH_RFC822(message, query) {
    if (query == "RFC822") {
      return this._FETCH_BODY(message, "BODY[]").replace("BODY[]", "RFC822");
    }
    if (query == "RFC822.HEADER") {
      return this._FETCH_BODY(message, "BODY.PEEK[HEADER]").replace(
        "BODY[HEADER]",
        "RFC822.HEADER"
      );
    }
    if (query == "RFC822.TEXT") {
      return this._FETCH_BODY(message, "BODY[TEXT]").replace(
        "BODY[TEXT]",
        "RFC822.TEXT"
      );
    }

    if (query == "RFC822.SIZE") {
      var channel = message.channel;
      var length = message.size ? message.size : channel.contentLength;
      if (length == -1) {
        var inputStream = channel.open();
        length = inputStream.available();
        inputStream.close();
      }
      return "RFC822.SIZE " + length;
    }
    throw new Error("Unknown item " + query);
  }
  _FETCH_UID(message) {
    return "UID " + message.uid;
  }
}

// IMAP4 RFC extensions
// --------------------
// Since there are so many extensions to IMAP, and since these extensions are
// not strictly hierarchical (e.g., an RFC 2342-compliant server can also be
// RFC 3516-compliant, but a server might only implement one of them), they
// must be handled differently from other fakeserver implementations.
// An extension is defined as follows: it is an object (not a function and
// prototype pair!). This object is "mixed" into the handler via the helper
// function mixinExtension, which applies appropriate magic to make the
// handler compliant to the extension. Functions are added untransformed, but
// both arrays and objects are handled by appending the values onto the
// original state of the handler. Semantics apply as for the base itself.

// Note that UIDPLUS (RFC4315) should be mixed in last (or at least after the
// MOVE extension) because it changes behavior of that extension.
export var configurations = {
  Cyrus: ["RFC2342", "RFC2195", "RFC5258"],
  UW: ["RFC2342", "RFC2195"],
  Dovecot: ["RFC2195", "RFC5258"],
  Zimbra: ["RFC2197", "RFC2342", "RFC2195", "RFC5258"],
  Exchange: ["RFC2342", "RFC2195"],
  LEMONADE: ["RFC2342", "RFC2195"],
  CUSTOM1: ["MOVE", "RFC4315", "CUSTOM"],
  GMail: ["GMAIL", "RFC2197", "RFC2342", "RFC3348", "RFC4315"],
};

export function mixinExtension(handler, extension) {
  if (extension.preload) {
    extension.preload(handler);
  }

  for (var property in extension) {
    if (property == "preload") {
      continue;
    }
    if (typeof extension[property] == "function") {
      // This is a function, so we add it to the handler
      handler[property] = extension[property];
    } else if (extension[property] instanceof Array) {
      // This is an array, so we append the values
      if (!(property in handler)) {
        handler[property] = [];
      }
      handler[property] = handler[property].concat(extension[property]);
    } else if (property in handler) {
      // This is an object, so we add in the values
      // Hack to make arrays et al. work recursively
      mixinExtension(handler[property], extension[property]);
    } else {
      handler[property] = extension[property];
    }
  }
}

// Support for Gmail extensions: XLIST and X-GM-EXT-1
export var IMAP_GMAIL_extension = {
  preload(toBeThis) {
    toBeThis._preGMAIL_STORE = toBeThis.STORE;
    toBeThis._preGMAIL_STORE_argFormat = toBeThis._argFormat.STORE;
    toBeThis._argFormat.STORE = ["number", "atom", "..."];
    toBeThis._DEFAULT_LIST = toBeThis.LIST;
  },
  XLIST(args) {
    // XLIST is really just SPECIAL-USE that does not conform to RFC 6154
    return this.LIST(args);
  },
  LIST(args) {
    // XLIST was deprecated, LIST implies SPECIAL-USE for Gmail.
    args.push("RETURN");
    args.push("SPECIAL-USE");
    return this._DEFAULT_LIST(args);
  },
  _LIST_RETURN_CHILDREN(aBox) {
    return IMAP_RFC5258_extension._LIST_RETURN_CHILDREN(aBox);
  },
  _LIST_RETURN_CHILDREN_SPECIAL_USE(aBox) {
    if (aBox.nonExistent) {
      return "";
    }

    let result = "* LIST (" + aBox.flags.join(" ");
    if (aBox._children.length > 0) {
      if (aBox.flags.length > 0) {
        result += " ";
      }
      result += "\\HasChildren";
    } else if (!aBox.flags.includes("\\NoInferiors")) {
      if (aBox.flags.length > 0) {
        result += " ";
      }
      result += "\\HasNoChildren";
    }
    if (aBox.specialUseFlag && aBox.specialUseFlag.length > 0) {
      result += " " + aBox.specialUseFlag;
    }
    result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0';
    return result;
  },
  STORE(args, uid) {
    const regex = /[+-]?FLAGS.*/;
    if (regex.test(args[1])) {
      // if we are storing flags, use the method that was overridden
      this._argFormat = this._preGMAIL_STORE_argFormat;
      args = this._treatArgs(args, "STORE");
      return this._preGMAIL_STORE(args, uid);
    }
    // otherwise, handle gmail specific cases
    const ids = [];
    const messages = this._parseSequenceSet(args[0], uid, ids);
    args[2] = formatArg(args[2], "string|(string)");
    for (let i = 0; i < args[2].length; i++) {
      if (args[2][i].includes(" ")) {
        args[2][i] = '"' + args[2][i] + '"';
      }
    }
    let response = "";
    for (let i = 0; i < messages.length; i++) {
      const message = messages[i];
      switch (args[1]) {
        case "X-GM-LABELS":
          if (message.xGmLabels) {
            message.xGmLabels = args[2];
          } else {
            return "BAD can't store X-GM-LABELS";
          }
          break;
        case "+X-GM-LABELS":
          if (message.xGmLabels) {
            message.xGmLabels = message.xGmLabels.concat(args[2]);
          } else {
            return "BAD can't store X-GM-LABELS";
          }
          break;
        case "-X-GM-LABELS":
          if (message.xGmLabels) {
            for (let i = 0; i < args[2].length; i++) {
              const idx = message.xGmLabels.indexOf(args[2][i]);
              if (idx != -1) {
                message.xGmLabels.splice(idx, 1);
              }
            }
          } else {
            return "BAD can't store X-GM-LABELS";
          }
          break;
        default:
          return "BAD change what now?";
      }
      response += "* " + ids[i] + " FETCH (X-GM-LABELS (";
      response += message.xGmLabels.join(" ");
      response += "))\0";
    }
    return response + "OK STORE completed";
  },
  _FETCH_X_GM_MSGID(message) {
    if (message.xGmMsgid) {
      return "X-GM-MSGID " + message.xGmMsgid;
    }
    return "BAD can't fetch X-GM-MSGID";
  },
  _FETCH_X_GM_THRID(message) {
    if (message.xGmThrid) {
      return "X-GM-THRID " + message.xGmThrid;
    }
    return "BAD can't fetch X-GM-THRID";
  },
  _FETCH_X_GM_LABELS(message) {
    if (message.xGmLabels) {
      return "X-GM-LABELS " + message.xGmLabels;
    }
    return "BAD can't fetch X-GM-LABELS";
  },
  kCapabilities: ["XLIST", "X-GM-EXT-1"],
  _argFormat: { XLIST: ["mailbox", "mailbox"] },
  // Enabled in AUTHED and SELECTED states
  _enabledCommands: { 1: ["XLIST"], 2: ["XLIST"] },
};

export var IMAP_MOVE_extension = {
  MOVE(args, uid) {
    const messages = this._parseSequenceSet(args[0], uid);

    const dest = this._daemon.getMailbox(args[1]);
    if (!dest) {
      return "NO [TRYCREATE] what mailbox?";
    }

    for (var message of messages) {
      const newMessage = new ImapMessage(
        message._URI,
        dest.uidnext++,
        message.flags
      );
      newMessage.recent = false;
      dest.addMessage(newMessage);
    }
    const mailbox = this._selectedMailbox;
    let response = "";
    for (let i = messages.length - 1; i >= 0; i--) {
      const msgIndex = mailbox._messages.indexOf(messages[i]);
      if (msgIndex != -1) {
        response += "* " + (msgIndex + 1) + " EXPUNGE\0";
        mailbox._messages.splice(msgIndex, 1);
      }
    }
    if (response.length > 0) {
      delete mailbox.__highestuid;
    }

    return response + "OK MOVE completed";
  },
  kCapabilities: ["MOVE"],
  kUidCommands: ["MOVE"],
  _argFormat: { MOVE: ["number", "mailbox"] },
  // Enabled in SELECTED state
  _enabledCommands: { 2: ["MOVE"] },
};

// Provides methods for testing fetchCustomAttribute and issueCustomCommand
export var IMAP_CUSTOM_extension = {
  preload(toBeThis) {
    toBeThis._preCUSTOM_STORE = toBeThis.STORE;
    toBeThis._preCUSTOM_STORE_argFormat = toBeThis._argFormat.STORE;
    toBeThis._argFormat.STORE = ["number", "atom", "..."];
  },
  STORE(args, uid) {
    const regex = /[+-]?FLAGS.*/;
    if (regex.test(args[1])) {
      // if we are storing flags, use the method that was overridden
      this._argFormat = this._preCUSTOM_STORE_argFormat;
      args = this._treatArgs(args, "STORE");
      return this._preCUSTOM_STORE(args, uid);
    }
    // otherwise, handle custom attribute
    const ids = [];
    const messages = this._parseSequenceSet(args[0], uid, ids);
    args[2] = formatArg(args[2], "string|(string)");
    for (let i = 0; i < args[2].length; i++) {
      if (args[2][i].includes(" ")) {
        args[2][i] = '"' + args[2][i] + '"';
      }
    }
    let response = "";
    for (let i = 0; i < messages.length; i++) {
      const message = messages[i];
      switch (args[1]) {
        case "X-CUSTOM-VALUE":
          if (message.xCustomValue && args[2].length == 1) {
            message.xCustomValue = args[2][0];
          } else {
            return "BAD can't store X-CUSTOM-VALUE";
          }
          break;
        case "X-CUSTOM-LIST":
          if (message.xCustomList) {
            message.xCustomList = args[2];
          } else {
            return "BAD can't store X-CUSTOM-LIST";
          }
          break;
        case "+X-CUSTOM-LIST":
          if (message.xCustomList) {
            message.xCustomList = message.xCustomList.concat(args[2]);
          } else {
            return "BAD can't store X-CUSTOM-LIST";
          }
          break;
        case "-X-CUSTOM-LIST":
          if (message.xCustomList) {
            for (let i = 0; i < args[2].length; i++) {
              const idx = message.xCustomList.indexOf(args[2][i]);
              if (idx != -1) {
                message.xCustomList.splice(idx, 1);
              }
            }
          } else {
            return "BAD can't store X-CUSTOM-LIST";
          }
          break;
        default:
          return "BAD change what now?";
      }
      response += "* " + ids[i] + " FETCH (X-CUSTOM-LIST (";
      response += message.xCustomList.join(" ");
      response += "))\0";
    }
    return response + "OK STORE completed";
  },
  _FETCH_X_CUSTOM_VALUE(message) {
    if (message.xCustomValue) {
      return "X-CUSTOM-VALUE " + message.xCustomValue;
    }
    return "BAD can't fetch X-CUSTOM-VALUE";
  },
  _FETCH_X_CUSTOM_LIST(message) {
    if (message.xCustomList) {
      return "X-CUSTOM-LIST (" + message.xCustomList.join(" ") + ")";
    }
    return "BAD can't fetch X-CUSTOM-LIST";
  },
  kCapabilities: ["X-CUSTOM1"],
};

// RFC 2087: Quota (incomplete implementation)
export var IMAP_RFC2087_extension = {
  GETQUOTAROOT(args) {
    const mailbox = this._daemon.getMailbox(args[0]);
    const quota = mailbox.quota ?? {};
    const response = [`* QUOTAROOT INBOX ""`];
    for (const [name, { usage, limit }] of Object.entries(quota)) {
      response.push(`* QUOTA "" (${name} ${usage} ${limit})`);
    }
    response.push("OK Getquota completed");
    return response.join("\0");
  },
  kCapabilities: ["QUOTA"],
  _argFormat: { GETQUOTAROOT: ["mailbox"] },
  _enabledCommands: { 1: ["GETQUOTAROOT"], 2: ["GETQUOTAROOT"] },
};

// RFC 2197: ID
export var IMAP_RFC2197_extension = {
  ID(args) {
    let clientID = "(";
    for (const i of args) {
      clientID += '"' + i + '"';
    }

    clientID += ")";
    const clientStrings = clientID.split(",");
    clientID = "";
    for (const i of clientStrings) {
      clientID += '"' + i + '" ';
    }
    clientID = clientID.slice(1, clientID.length - 3);
    clientID += ")";
    this._daemon.clientID = clientID;
    return "* ID " + this._daemon.idResponse + "\0OK Success";
  },
  kCapabilities: ["ID"],
  _argFormat: { ID: ["(string)"] },
  _enabledCommands: { 1: ["ID"], 2: ["ID"] },
};

// RFC 2342: IMAP4 Namespace (NAMESPACE)
export var IMAP_RFC2342_extension = {
  NAMESPACE() {
    var namespaces = [[], [], []];
    for (const namespace of this._daemon.namespaces) {
      namespaces[namespace.type].push(namespace);
    }

    var response = "* NAMESPACE";
    for (var type of namespaces) {
      if (type.length == 0) {
        response += " NIL";
        continue;
      }
      response += " (";
      for (const namespace of type) {
        response += '("';
        response += namespace.displayName;
        response += '" "';
        response += namespace.delimiter;
        response += '")';
      }
      response += ")";
    }
    response += "\0OK NAMESPACE command completed";
    return response;
  },
  kCapabilities: ["NAMESPACE"],
  _argFormat: { NAMESPACE: [] },
  // Enabled in AUTHED and SELECTED states
  _enabledCommands: { 1: ["NAMESPACE"], 2: ["NAMESPACE"] },
};

// RFC 3348 Child Mailbox (CHILDREN)
export var IMAP_RFC3348_extension = {
  kCapabilities: ["CHILDREN"],
};

// RFC 4315: UIDPLUS
export var IMAP_RFC4315_extension = {
  preload(toBeThis) {
    toBeThis._preRFC4315UID = toBeThis.UID;
    toBeThis._preRFC4315APPEND = toBeThis.APPEND;
    toBeThis._preRFC4315COPY = toBeThis.COPY;
    toBeThis._preRFC4315MOVE = toBeThis.MOVE;
  },
  UID(args) {
    // XXX: UID EXPUNGE is not supported.
    return this._preRFC4315UID(args);
  },
  APPEND(args) {
    let response = this._preRFC4315APPEND(args);
    if (response.indexOf("OK") == 0) {
      const mailbox = this._daemon.getMailbox(args[0]);
      const uid = mailbox.uidnext - 1;
      response =
        "OK [APPENDUID " +
        mailbox.uidvalidity +
        " " +
        uid +
        "]" +
        response.substring(2);
    }
    return response;
  },
  COPY(args) {
    const mailbox = this._daemon.getMailbox(args[0]);
    if (mailbox) {
      var first = mailbox.uidnext;
    }
    let response = this._preRFC4315COPY(args);
    if (response.indexOf("OK") == 0) {
      const last = mailbox.uidnext - 1;
      response =
        "OK [COPYUID " +
        this._selectedMailbox.uidvalidity +
        " " +
        args[0] +
        " " +
        first +
        ":" +
        last +
        "]" +
        response.substring(2);
    }
    return response;
  },
  MOVE(args) {
    const mailbox = this._daemon.getMailbox(args[1]);
    if (mailbox) {
      var first = mailbox.uidnext;
    }
    let response = this._preRFC4315MOVE(args);
    if (response.includes("OK MOVE")) {
      const last = mailbox.uidnext - 1;
      response = response.replace(
        "OK MOVE",
        "OK [COPYUID " +
          this._selectedMailbox.uidvalidity +
          " " +
          args[0] +
          " " +
          first +
          ":" +
          last +
          "]"
      );
    }
    return response;
  },
  kCapabilities: ["UIDPLUS"],
};

// RFC 5258: LIST-EXTENDED
export var IMAP_RFC5258_extension = {
  preload(toBeThis) {
    toBeThis._argFormat.LIST = [
      "[(atom)]",
      "mailbox",
      "mailbox|(mailbox)",
      "[atom]",
      "[(atom)]",
    ];
  },
  _LIST_SUBSCRIBED(aBox) {
    if (!aBox.subscribed) {
      return "";
    }

    let result = "* LIST (" + aBox.flags.join(" ");
    if (aBox.flags.length > 0) {
      result += " ";
    }
    result += "\\Subscribed";
    if (aBox.nonExistent) {
      result += " \\NonExistent";
    }
    result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0';
    return result;
  },
  _LIST_RETURN_CHILDREN(aBox) {
    if (aBox.nonExistent) {
      return "";
    }

    let result = "* LIST (" + aBox.flags.join(" ");
    if (aBox._children.length > 0) {
      if (aBox.flags.length > 0) {
        result += " ";
      }
      result += "\\HasChildren";
    } else if (!aBox.flags.includes("\\NoInferiors")) {
      if (aBox.flags.length > 0) {
        result += " ";
      }
      result += "\\HasNoChildren";
    }
    result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0';
    return result;
  },
  _LIST_RETURN_SUBSCRIBED(aBox) {
    if (aBox.nonExistent) {
      return "";
    }

    let result = "* LIST (" + aBox.flags.join(" ");
    if (aBox.subscribed) {
      if (aBox.flags.length > 0) {
        result += " ";
      }
      result += "\\Subscribed";
    }
    result += ') "' + aBox.delimiter + '" "' + aBox.displayName + '"\0';
    return result;
  },
  // TODO implement _LIST_REMOTE, _LIST_RECURSIVEMATCH, _LIST_RETURN_SUBSCRIBED
  // and all valid combinations thereof. Currently, nsImapServerResponseParser
  // does not support any of these responses anyway.

  kCapabilities: ["LIST-EXTENDED"],
};

/**
 * This implements AUTH schemes. Could be moved into RFC3501 actually.
 * The test can en-/disable auth schemes by modifying kAuthSchemes.
 */
export var IMAP_RFC2195_extension = {
  kAuthSchemes: ["CRAM-MD5", "PLAIN", "LOGIN"],

  preload(handler) {
    handler._kAuthSchemeStartFunction["CRAM-MD5"] = this.authCRAMStart;
    handler._kAuthSchemeStartFunction.PLAIN = this.authPLAINStart;
    handler._kAuthSchemeStartFunction.LOGIN = this.authLOGINStart;
  },

  authPLAINStart() {
    this._nextAuthFunction = this.authPLAINCred;
    this._multiline = true;

    return "+";
  },
  authPLAINCred(line) {
    var req = AuthPLAIN.decodeLine(line);
    if (req.username == this.kUsername && req.password == this.kPassword) {
      this._state = IMAP_STATE_AUTHED;
      return "OK Hello friend! Friends give friends good advice: Next time, use CRAM-MD5";
    }
    return "BAD Wrong username or password, crook!";
  },

  authCRAMStart() {
    this._nextAuthFunction = this.authCRAMDigest;
    this._multiline = true;

    this._usedCRAMMD5Challenge = AuthCRAM.createChallenge("localhost");
    return "+ " + this._usedCRAMMD5Challenge;
  },
  authCRAMDigest(line) {
    var req = AuthCRAM.decodeLine(line);
    var expectedDigest = AuthCRAM.encodeCRAMMD5(
      this._usedCRAMMD5Challenge,
      this.kPassword
    );
    if (req.username == this.kUsername && req.digest == expectedDigest) {
      this._state = IMAP_STATE_AUTHED;
      return "OK Hello friend!";
    }
    return "BAD Wrong username or password, crook!";
  },

  authLOGINStart() {
    this._nextAuthFunction = this.authLOGINUsername;
    this._multiline = true;

    return "+ " + btoa("Username:");
  },
  authLOGINUsername(line) {
    var req = AuthLOGIN.decodeLine(line);
    if (req == this.kUsername) {
      this._nextAuthFunction = this.authLOGINPassword;
    } else {
      // Don't return error yet, to not reveal valid usernames
      this._nextAuthFunction = this.authLOGINBadUsername;
    }
    this._multiline = true;
    return "+ " + btoa("Password:");
  },
  authLOGINBadUsername() {
    return "BAD Wrong username or password, crook!";
  },
  authLOGINPassword(line) {
    var req = AuthLOGIN.decodeLine(line);
    if (req == this.kPassword) {
      this._state = IMAP_STATE_AUTHED;
      return "OK Hello friend! Where did you pull out this old auth scheme?";
    }
    return "BAD Wrong username or password, crook!";
  },
};

/**
 * Implements XOAUTH2 authentication.
 */
export var IMAP_OAUTH2_extension = {
  kAuthSchemes: ["XOAUTH2"],

  preload(handler) {
    handler._kAuthSchemeStartFunction.XOAUTH2 = this.authXOAUTH2Start;
  },

  authXOAUTH2Start(lineRest) {
    const [user, auth] = atob(lineRest).split("\u0001");
    if (
      user == `user=${this.kUsername}` &&
      auth == `auth=Bearer ${this.kPassword}`
    ) {
      this._state = IMAP_STATE_AUTHED;
      return "OK Yeah, that's the right access token.";
    }
    return "BAD Yeah, nah, that's the wrong access token.";
  },
};

// FETCH BODYSTRUCTURE
function bodystructure(msg, extension) {
  if (!msg || msg == "") {
    return "";
  }

  // Use the mime parser emitter to generate body structure data. Most of the
  // string will be built as we exit a part. Currently not working:
  // 1. Some of the fields return NIL instead of trying to calculate them.
  // 2. MESSAGE is missing the ENVELOPE and the lines at the end.
  var bodystruct = "";
  function paramToString(params) {
    const paramList = [];
    for (const [param, value] of params) {
      paramList.push('"' + param.toUpperCase() + '" "' + value + '"');
    }
    return paramList.length == 0 ? "NIL" : "(" + paramList.join(" ") + ")";
  }
  var headerStack = [];
  var BodyStructureEmitter = {
    startPart(partNum, headers) {
      bodystruct += "(";
      headerStack.push(headers);
      this.numLines = 0;
      this.length = 0;
    },
    deliverPartData(partNum, data) {
      this.length += data.length;
      this.numLines += Array.from(data).filter(x => x == "\n").length;
    },
    endPart() {
      // Grab the headers from before
      const headers = headerStack.pop();
      const contentType = headers.contentType;
      if (contentType.mediatype == "multipart") {
        bodystruct += ' "' + contentType.subtype.toUpperCase() + '"';
        if (extension) {
          bodystruct += " " + paramToString(contentType);
          // XXX: implement the rest
          bodystruct += " NIL NIL NIL";
        }
      } else {
        bodystruct +=
          '"' +
          contentType.mediatype.toUpperCase() +
          '" "' +
          contentType.subtype.toUpperCase() +
          '"';
        bodystruct += " " + paramToString(contentType);

        // XXX: Content ID, Content description
        bodystruct += " NIL NIL";

        const cte = headers.has("content-transfer-encoding")
          ? headers.get("content-transfer-encoding")
          : "7BIT";
        bodystruct += ' "' + cte + '"';

        bodystruct += " " + this.length;
        if (contentType.mediatype == "text") {
          bodystruct += " " + this.numLines;
        }

        // XXX: I don't want to implement these yet
        if (extension) {
          bodystruct += " NIL NIL NIL NIL";
        }
      }
      bodystruct += ")";
    },
  };
  MimeParser.parseSync(msg, BodyStructureEmitter, {});
  return bodystruct;
}
