summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/fakeserver/Imapd.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/test/fakeserver/Imapd.jsm')
-rw-r--r--comm/mailnews/test/fakeserver/Imapd.jsm2544
1 files changed, 2544 insertions, 0 deletions
diff --git a/comm/mailnews/test/fakeserver/Imapd.jsm b/comm/mailnews/test/fakeserver/Imapd.jsm
new file mode 100644
index 0000000000..6023bf7b90
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Imapd.jsm
@@ -0,0 +1,2544 @@
+/* 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
+
+var EXPORTED_SYMBOLS = [
+ "ImapDaemon",
+ "ImapMailbox",
+ "ImapMessage",
+ "IMAP_RFC3501_handler",
+ "configurations",
+ "mixinExtension",
+ "IMAP_GMAIL_extension",
+ "IMAP_MOVE_extension",
+ "IMAP_CUSTOM_extension",
+ "IMAP_RFC2197_extension",
+ "IMAP_RFC2342_extension",
+ "IMAP_RFC3348_extension",
+ "IMAP_RFC4315_extension",
+ "IMAP_RFC5258_extension",
+ "IMAP_RFC2195_extension",
+];
+
+// 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.
+
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+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
+ let names = name.split(mailbox.delimiter);
+ names.splice(0, 1);
+ for (let 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
+ let names = name.split(this.inbox.delimiter);
+ mailbox = this.root;
+
+ for (let 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;
+ let 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...
+ let 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;
+ let 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");
+ }
+ }
+}
+
+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;
+ }
+
+ let generator = folder.includes("*") ? "allChildren" : "_children";
+ let possible = matching.reduce(function (arr, elem) {
+ return arr.concat(elem[generator]);
+ }, []);
+
+ if (folder == "*" || folder == "%") {
+ matching = possible;
+ continue;
+ }
+
+ let parts = folder.split(/[*%]/).filter(function (str) {
+ return str.length > 0;
+ });
+ matching = possible.filter(function (mailbox) {
+ let index = 0,
+ 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() {
+ let 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;
+ }
+}
+
+class ImapMessage {
+ constructor(URI, uid, flags) {
+ this._URI = URI;
+ this.uid = uid;
+ this.size = 0;
+ this.flags = [];
+ for (let 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) {
+ let 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) {
+ let 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 == "{") {
+ let end = text.indexOf("}");
+ if (end == -1) {
+ throw new Error("Expected CLOSE_BRACKET");
+ }
+ if (end + 1 != text.length) {
+ throw new Error("Expected CRLF");
+ }
+ let 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 = "";
+ }
+ let 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") {
+ let 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.
+ */
+class IMAP_RFC3501_handler {
+ constructor(daemon) {
+ this.kUsername = "user";
+ this.kPassword = "password";
+ this.kAuthSchemes = []; // Added by RFC2195 extension. Test may modify as needed.
+ 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 (let 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(args) {
+ 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(args) {
+ return "OK Recognized a valid CLIENTID command, used for authentication methods";
+ }
+ LOGOUT(args) {
+ 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(args) {
+ return "OK NOOP completed";
+ }
+ STARTTLS(args) {
+ // 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 (let 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";
+ let returnOptions = args[3] ? args[3].toString().split(" ") : [];
+ if (
+ this.kCapabilities.includes("CHILDREN") &&
+ !returnOptions.includes("CHILDREN")
+ ) {
+ returnOptions.push("CHILDREN");
+ }
+ returnOptions.sort();
+ for (let option of returnOptions) {
+ listFunctionName += "_" + option.replace(/-/g, "_");
+ }
+ }
+ if (!this[listFunctionName]) {
+ return "BAD unknown LIST request options";
+ }
+
+ let 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 (let requestedBox of requestedBoxes) {
+ let people = base.matchKids(requestedBox);
+ for (let 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(args) {
+ this._daemon.synchronize(this._selectedMailbox, false);
+ return "OK CHECK completed";
+ }
+ CLOSE(args) {
+ this._selectedMailbox.expunge();
+ this._daemon.synchronize(this._selectedMailbox, !this._readOnly);
+ this._selectedMailbox = null;
+ this._state = IMAP_STATE_AUTHED;
+ return "OK CLOSE completed";
+ }
+ EXPUNGE(args) {
+ // 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, uid) {
+ if (args[0] == "UNDELETED") {
+ let response = "* SEARCH";
+ let 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\0";
+ }
+ 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 (let 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 (let flag of args[2]) {
+ message.setFlag(flag);
+ }
+ break;
+ case "-FLAGS":
+ for (let 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) {
+ let 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.
+ let now = new Date();
+ let alarm;
+ let startingMSeconds = now.getTime();
+ 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;
+ }
+ let re = /[0-9]/g;
+ let 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]) {
+ let 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": {
+ let joinList = [];
+ let 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": {
+ let joinList = [];
+ let headers = message.getPartHeaders(partNum);
+ for (let 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, query) {
+ 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) {
+ let date = message.date;
+ // Format timestamp as: "%d-%b-%Y %H:%M:%S %z" (%b in English).
+ let year = date.getFullYear().toString();
+ let month = date.toLocaleDateString("en-US", { month: "short" });
+ let day = date.getDate().toString();
+ let hours = date.getHours().toString().padStart(2, "0");
+ let minutes = date.getMinutes().toString().padStart(2, "0");
+ let seconds = date.getSeconds().toString().padStart(2, "0");
+ let offset = date.getTimezoneOffset();
+ let tzoff =
+ Math.floor(Math.abs(offset) / 60) * 100 + (Math.abs(offset) % 60);
+ let 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.
+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"],
+};
+
+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
+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) {
+ let 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
+ let ids = [];
+ let 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++) {
+ let 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++) {
+ let 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"] },
+};
+
+var IMAP_MOVE_extension = {
+ MOVE(args, uid) {
+ let messages = this._parseSequenceSet(args[0], uid);
+
+ let dest = this._daemon.getMailbox(args[1]);
+ if (!dest) {
+ return "NO [TRYCREATE] what mailbox?";
+ }
+
+ for (var message of messages) {
+ let newMessage = new ImapMessage(
+ message._URI,
+ dest.uidnext++,
+ message.flags
+ );
+ newMessage.recent = false;
+ dest.addMessage(newMessage);
+ }
+ let mailbox = this._selectedMailbox;
+ let response = "";
+ for (let i = messages.length - 1; i >= 0; i--) {
+ let 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
+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) {
+ let 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
+ let ids = [];
+ let 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++) {
+ let 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++) {
+ let 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 2197: ID
+var IMAP_RFC2197_extension = {
+ ID(args) {
+ let clientID = "(";
+ for (let i of args) {
+ clientID += '"' + i + '"';
+ }
+
+ clientID += ")";
+ let clientStrings = clientID.split(",");
+ clientID = "";
+ for (let 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)
+var IMAP_RFC2342_extension = {
+ NAMESPACE(args) {
+ var namespaces = [[], [], []];
+ for (let 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 (let 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)
+var IMAP_RFC3348_extension = {
+ kCapabilities: ["CHILDREN"],
+};
+
+// RFC 4315: UIDPLUS
+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) {
+ let mailbox = this._daemon.getMailbox(args[0]);
+ let uid = mailbox.uidnext - 1;
+ response =
+ "OK [APPENDUID " +
+ mailbox.uidvalidity +
+ " " +
+ uid +
+ "]" +
+ response.substring(2);
+ }
+ return response;
+ },
+ COPY(args) {
+ let mailbox = this._daemon.getMailbox(args[0]);
+ if (mailbox) {
+ var first = mailbox.uidnext;
+ }
+ let response = this._preRFC4315COPY(args);
+ if (response.indexOf("OK") == 0) {
+ let last = mailbox.uidnext - 1;
+ response =
+ "OK [COPYUID " +
+ this._selectedMailbox.uidvalidity +
+ " " +
+ args[0] +
+ " " +
+ first +
+ ":" +
+ last +
+ "]" +
+ response.substring(2);
+ }
+ return response;
+ },
+ MOVE(args) {
+ let mailbox = this._daemon.getMailbox(args[1]);
+ if (mailbox) {
+ var first = mailbox.uidnext;
+ }
+ let response = this._preRFC4315MOVE(args);
+ if (response.includes("OK MOVE")) {
+ let 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
+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.
+ */
+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(lineRest) {
+ 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(lineRest) {
+ 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(lineRest) {
+ 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(line) {
+ 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!";
+ },
+};
+
+// 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) {
+ let paramList = [];
+ for (let [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(partNum) {
+ // Grab the headers from before
+ let headers = headerStack.pop();
+ let 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";
+
+ let 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;
+}