summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/fakeserver
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/test/fakeserver
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--comm/mailnews/test/fakeserver/Auth.jsm209
-rw-r--r--comm/mailnews/test/fakeserver/Binaryd.jsm250
-rw-r--r--comm/mailnews/test/fakeserver/Imapd.jsm2544
-rw-r--r--comm/mailnews/test/fakeserver/Ldapd.jsm665
-rw-r--r--comm/mailnews/test/fakeserver/Maild.jsm566
-rw-r--r--comm/mailnews/test/fakeserver/Nntpd.jsm631
-rw-r--r--comm/mailnews/test/fakeserver/Pop3d.jsm454
-rw-r--r--comm/mailnews/test/fakeserver/Smtpd.jsm274
8 files changed, 5593 insertions, 0 deletions
diff --git a/comm/mailnews/test/fakeserver/Auth.jsm b/comm/mailnews/test/fakeserver/Auth.jsm
new file mode 100644
index 0000000000..4bd240b509
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Auth.jsm
@@ -0,0 +1,209 @@
+/* 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 the authentication mechanisms
+ * - AUTH LOGIN
+ * - AUTH PLAIN
+ * - AUTH CRAM-MD5
+ * for all the server implementations, i.e. in a generic way.
+ * In fact, you could use this to implement a real server in JS :-) .
+ *
+ * @author Ben Bucksch <ben.bucksch beonex.com>
+ */
+
+var EXPORTED_SYMBOLS = ["AuthPLAIN", "AuthLOGIN", "AuthCRAM"];
+
+/**
+ * Implements AUTH PLAIN
+ *
+ * @see RFC 4616
+ */
+var AuthPLAIN = {
+ /**
+ * Takes full PLAIN auth line, and decodes it.
+ *
+ * @param line {string}
+ * @returns {Object { username : value, password : value } }
+ * @throws {string} error to return to client
+ */
+ decodeLine(line) {
+ dump("AUTH PLAIN line -" + line + "-\n");
+ line = atob(line); // base64 decode
+ let aap = line.split("\u0000"); // 0-charater is delimiter
+ if (aap.length != 3) {
+ throw new Error("Expected three parts");
+ }
+ /* aap is: authorize-id, authenticate-id, password.
+ Generally, authorize-id = authenticate-id = username.
+ authorize-id may thus be empty and then defaults to authenticate-id. */
+ var result = {};
+ var authzid = aap[0];
+ result.username = aap[1];
+ result.password = aap[2];
+ dump(
+ "authorize-id: -" +
+ authzid +
+ "-, username: -" +
+ result.username +
+ "-, password: -" +
+ result.password +
+ "-\n"
+ );
+ if (authzid && authzid != result.username) {
+ throw new Error(
+ "Expecting a authorize-id that's either the same as authenticate-id or empty"
+ );
+ }
+ return result;
+ },
+
+ /**
+ * Create an AUTH PLAIN line, to allow a client to authenticate to a server.
+ * Useful for tests.
+ */
+ encodeLine(username, password) {
+ username = username.substring(0, 255);
+ password = password.substring(0, 255);
+ return btoa("\u0000" + username + "\u0000" + password); // base64 encode
+ },
+};
+
+var AuthLOGIN = {
+ /**
+ * Takes full LOGIN auth line, and decodes it.
+ * It may contain either username or password,
+ * depending on state/step (first username, then pw).
+ *
+ * @param line {string}
+ * @returns {string} username or password
+ * @throws {string} error to return to client
+ */
+ decodeLine(line) {
+ dump("AUTH LOGIN -" + atob(line) + "-\n");
+ return atob(line); // base64 decode
+ },
+};
+
+/**
+ * Implements AUTH CRAM-MD5
+ *
+ * @see RFC 2195, RFC 2104
+ */
+var AuthCRAM = {
+ /**
+ * First part of CRAM exchange is that the server sends
+ * a challenge to the client. The client response depends on
+ * the challenge. (This prevents replay attacks, I think.)
+ * This function generates the challenge.
+ *
+ * You need to store it, you'll need it to check the client response.
+ *
+ * @param domain {string} - Your hostname or domain,
+ * e.g. "example.com", "mx.example.com" or just "localhost".
+ * @returns {string} The challenge.
+ * It's already base64-encoded. Send it as-is to the client.
+ */
+ createChallenge(domain) {
+ var timestamp = new Date().getTime(); // unixtime
+ var challenge = "<" + timestamp + "@" + domain + ">";
+ dump("CRAM challenge unencoded: " + challenge + "\n");
+ return btoa(challenge);
+ },
+ /**
+ * Takes full CRAM-MD5 auth line, and decodes it.
+ *
+ * Compare the returned |digest| to the result of
+ * encodeCRAMMD5(). If they match, the |username|
+ * returned here is authenticated.
+ *
+ * @param line {string}
+ * @returns {Object { username : value, digest : value } }
+ * @throws {string} error to return to client
+ */
+ decodeLine(line) {
+ dump("AUTH CRAM-MD5 line -" + line + "-\n");
+ line = atob(line);
+ dump("base64 decoded -" + line + "-\n");
+ var sp = line.split(" ");
+ if (sp.length != 2) {
+ throw new Error("Expected one space");
+ }
+ var result = {};
+ result.username = sp[0];
+ result.digest = sp[1];
+ return result;
+ },
+ /**
+ * @param text {string} - server challenge (base64-encoded)
+ * @param key {string} - user's password
+ * @returns {string} digest as hex string
+ */
+ encodeCRAMMD5(text, key) {
+ text = atob(text); // createChallenge() returns it already encoded
+ dump("encodeCRAMMD5(text: -" + text + "-, key: -" + key + "-)\n");
+ const kInputLen = 64;
+ // const kHashLen = 16;
+ const kInnerPad = 0x36; // per spec
+ const kOuterPad = 0x5c;
+
+ key = this.textToNumberArray(key);
+ text = this.textToNumberArray(text);
+ // Make sure key is exactly kDigestLen bytes long. Algo per spec.
+ if (key.length > kInputLen) {
+ // (results in kHashLen)
+ key = this.md5(key);
+ }
+ while (key.length < kInputLen) {
+ // Fill up with zeros.
+ key.push(0);
+ }
+
+ // MD5((key XOR outerpad) + MD5((key XOR innerpad) + text)) , per spec
+ var digest = this.md5(
+ this.xor(key, kOuterPad).concat(
+ this.md5(this.xor(key, kInnerPad).concat(text))
+ )
+ );
+ return this.arrayToHexString(digest);
+ },
+ // Utils
+ xor(binary, value) {
+ var result = [];
+ for (var i = 0; i < binary.length; i++) {
+ result.push(binary[i] ^ value);
+ }
+ return result;
+ },
+ md5(binary) {
+ var md5 = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ md5.init(Ci.nsICryptoHash.MD5);
+ md5.update(binary, binary.length);
+ return this.textToNumberArray(md5.finish(false));
+ },
+ textToNumberArray(text) {
+ var array = [];
+ for (var i = 0; i < text.length; i++) {
+ // Convert string (only lower byte) to array.
+ array.push(text.charCodeAt(i) & 0xff);
+ }
+ return array;
+ },
+ arrayToHexString(binary) {
+ var result = "";
+ for (var i = 0; i < binary.length; i++) {
+ if (binary[i] > 255) {
+ throw new Error("unexpected that value > 255");
+ }
+ let hex = binary[i].toString(16);
+ if (hex.length < 2) {
+ hex = "0" + hex;
+ }
+ result += hex;
+ }
+ return result;
+ },
+};
diff --git a/comm/mailnews/test/fakeserver/Binaryd.jsm b/comm/mailnews/test/fakeserver/Binaryd.jsm
new file mode 100644
index 0000000000..7502ef1e55
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Binaryd.jsm
@@ -0,0 +1,250 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["BinaryServer"];
+
+const CC = Components.Constructor;
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+/**
+ * A binary stream-based server.
+ * Listens on a socket, and whenever a new connection is made it runs
+ * a user-supplied handler function.
+ *
+ * Example:
+ * A trivial echo server (with a null daemon, so no state shared between
+ * connections):
+ *
+ * let echoServer = new BinaryServer(function(conn, daemon) {
+ * while(1) {
+ * let data = conn.read(1);
+ * conn.write(data);
+ * }
+ * }, null);
+ *
+ */
+
+class BinaryServer {
+ /**
+ * The handler function should be of the form:
+ * async function handlerFn(conn, daemon)
+ *
+ * @async
+ * @callback handlerFn
+ * @param {Connection} conn
+ * @param {object} daemon
+ *
+ * The handler function runs as long as it wants - reading and writing bytes
+ * (via methods on conn) until it is finished with the connection.
+ * The handler simply returns to indicate the connection is done, or throws
+ * an exception to indicate that something went wrong.
+ * The daemon is the object which holds the server data/state, shared with
+ * all connection handler. The BinaryServer doesn't do anything with daemon
+ * other than passing it directly on to the handler function.
+ */
+
+ /**
+ * Construct a new BinaryServer.
+ *
+ * @param {handlerFn} handlerFn - Function to call to handle each new connection.
+ * @param {object} daemon - Object to pass on to the handler, to share state
+ * and functionality between across connections.
+ */
+ constructor(handlerFn, daemon) {
+ this._port = -1;
+ this._handlerFn = handlerFn;
+ this._daemon = daemon;
+ this._listener = null; // Listening socket to accept new connections.
+ this._connections = new Set();
+ }
+
+ /**
+ * Starts the server running.
+ *
+ * @param {number} port - The port to run on (or -1 to pick one automatically).
+ */
+ async start(port = -1) {
+ if (this._listener) {
+ throw Components.Exception(
+ "Server already started",
+ Cr.NS_ERROR_ALREADY_INITIALIZED
+ );
+ }
+
+ let socket = new ServerSocket(
+ port,
+ true, // Loopback only.
+ -1 // Default max pending connections.
+ );
+
+ let server = this;
+
+ socket.asyncListen({
+ async onSocketAccepted(socket, transport) {
+ let conn = new Connection(transport);
+ server._connections.add(conn);
+ try {
+ await server._handlerFn(conn, server._daemon);
+ // If we get here, handler completed, without error.
+ } catch (e) {
+ if (conn.isClosed()) {
+ // if we get here, assume the error occurred because we're
+ // shutting down, and ignore it.
+ } else {
+ // if we get here, something went wrong.
+ dump("ERROR " + e.toString());
+ }
+ }
+ conn.close();
+ server._connections.delete(conn);
+ },
+ onStopListening(socket, status) {
+ // Server is stopping, time to close any outstanding connections.
+ server._connections.forEach(conn => conn.close());
+ server._connections.clear();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIServerSocketListener"]),
+ });
+ // We're running!
+ this._listener = socket;
+ }
+
+ /**
+ * Provides port, a read-only attribute to get which port the server
+ * server is listening upon. Behaviour is undefined if server is not
+ * running.
+ */
+ get port() {
+ return this._listener.port;
+ }
+
+ /**
+ * Stops the server, if it is running.
+ */
+ stop() {
+ if (!this._listener) {
+ // Already stopped.
+ return;
+ }
+ this._listener.close();
+ this._listener = null;
+ // We could still be accepting new connections at this point,
+ // so we wait until the onStopListening callback to tear down the
+ // connections.
+ }
+}
+
+/**
+ * Connection wraps a nsITransport with read/write functions that are
+ * javascript async, to simplify writing server handers.
+ * Handlers should only need to use read() and write() from here, leaving
+ * all connection management up to the BinaryServer.
+ */
+class Connection {
+ constructor(transport) {
+ this._transport = transport;
+ this._input = transport.openInputStream(0, 0, 0);
+ let outStream = transport.openOutputStream(0, 0, 0);
+ this._output = new BinaryOutputStream(outStream);
+ }
+
+ /**
+ * @returns true if close() has been called.
+ */
+ isClosed() {
+ return this._transport === null;
+ }
+
+ /**
+ * Closes the connection. Can be safely called multiple times.
+ * The BinaryServer will call this - handlers don't need to worry about
+ * the connection status.
+ */
+ close() {
+ if (this.isClosed()) {
+ return;
+ }
+ this._input.close();
+ this._output.close();
+ this._transport.close(Cr.NS_OK);
+ this._input = null;
+ this._output = null;
+ this._transport = null;
+ }
+
+ /**
+ * Read exactly nBytes from the connection.
+ *
+ * @param {number} nBytes - The number of bytes required.
+ * @returns {Array.<number>} - An array containing the requested bytes.
+ */
+ async read(nBytes) {
+ let conn = this;
+ let buf = [];
+ while (buf.length < nBytes) {
+ let want = nBytes - buf.length;
+ // A slightly odd-looking construct to wrap the listener-based
+ // asyncwait() into a javascript async function.
+ await new Promise((resolve, reject) => {
+ try {
+ conn._input.asyncWait(
+ {
+ onInputStreamReady(stream) {
+ // how many bytes are actually available?
+ let n;
+ try {
+ n = stream.available();
+ } catch (e) {
+ // stream was closed.
+ reject(e);
+ }
+ if (n > want) {
+ n = want;
+ }
+ let chunk = new BinaryInputStream(stream).readByteArray(n);
+ Array.prototype.push.apply(buf, chunk);
+ resolve();
+ },
+ },
+ 0,
+ want,
+ Services.tm.mainThread
+ );
+ } catch (e) {
+ // asyncwait() failed
+ reject(e);
+ }
+ });
+ }
+ return buf;
+ }
+
+ /**
+ * Write data to the connection.
+ *
+ * @param {Array.<number>} data - The bytes to send.
+ */
+ async write(data) {
+ // TODO: need to check outputstream for writeability here???
+ // Might be an issue if we start throwing bigger chunks of data about...
+ await this._output.writeByteArray(data);
+ }
+}
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;
+}
diff --git a/comm/mailnews/test/fakeserver/Ldapd.jsm b/comm/mailnews/test/fakeserver/Ldapd.jsm
new file mode 100644
index 0000000000..2206ebce81
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Ldapd.jsm
@@ -0,0 +1,665 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["LDAPDaemon", "LDAPHandlerFn"];
+
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+/**
+ * This file provides fake LDAP server functionality, just enough to run
+ * our unit tests against.
+ *
+ * Currently:
+ * - it accepts any bind request (no authentication).
+ * - it supports searches, but only some types of filter.
+ * - it supports unbind (quit) requests.
+ * - all other requests are ignored.
+ *
+ * It should be extensible enough that extra features can be added as
+ * required.
+ */
+
+/*
+ * Helpers for application-neutral BER-encoding/decoding.
+ *
+ * BER is self-describing enough to allow us to parse it without knowing
+ * the meaning. So we can break down a binary stream into ints, strings,
+ * sequences etc... and then leave it up to the separate LDAP code to
+ * interpret the meaning of it.
+ *
+ * Clearest BER reference I've read:
+ * https://docs.oracle.com/cd/E19476-01/821-0510/def-basic-encoding-rules.html
+ */
+
+/**
+ * Encodes a BER length, returning an array of (wire-format) bytes.
+ * It's variable length encoding - smaller numbers are encoded with
+ * fewer bytes.
+ *
+ * @param {number} i - The length to encode.
+ */
+function encodeLength(i) {
+ if (i < 128) {
+ return [i];
+ }
+
+ let temp = i;
+ let bytes = [];
+
+ while (temp >= 128) {
+ bytes.unshift(temp & 255);
+ temp >>= 8;
+ }
+ bytes.unshift(temp);
+ bytes.unshift(0x80 | bytes.length);
+ return bytes;
+}
+
+/**
+ * Helper for encoding and decoding BER values.
+ * Each value is notionally a type-length-data triplet, although we just
+ * store type and an array for data (with the array knowing it's length).
+ * BERValue.data is held in raw form (wire format) for non-sequence values.
+ * For sequences and sets (constructed values), .data is empty, and
+ * instead .children is used to hold the contained BERValue objects.
+ */
+class BERValue {
+ constructor(type) {
+ this.type = type;
+ this.children = []; // only for constructed values (sequences)
+ this.data = []; // the raw data (empty for constructed ones)
+ }
+
+ /**
+ * Encode the BERValue to an array of bytes, ready to be written to the wire.
+ *
+ * @returns {Array.<number>} - The encoded bytes.
+ */
+ encode() {
+ let bytes = [];
+ if (this.isConstructed()) {
+ for (let c of this.children) {
+ bytes = bytes.concat(c.encode());
+ }
+ } else {
+ bytes = this.data;
+ }
+ return [this.type].concat(encodeLength(bytes.length), bytes);
+ }
+
+ // Functions to check class (upper two bits of type).
+ isUniversal() {
+ return (this.type & 0xc0) == 0x00;
+ }
+ isApplication() {
+ return (this.type & 0xc0) == 0x40;
+ }
+ isContextSpecific() {
+ return (this.type & 0xc0) == 0x80;
+ }
+ isPrivate() {
+ return (this.type & 0xc0) == 0xc0;
+ }
+
+ /*
+ * @return {boolean} - Is this value a constructed type a sequence or set?
+ * (As encoded in bit 5 of the type)
+ */
+ isConstructed() {
+ return !!(this.type & 0x20);
+ }
+
+ /**
+ * @returns {number} - The tag number of the type (the lower 5 bits).
+ */
+ tag() {
+ return this.type & 0x1f;
+ }
+
+ // Functions to check for some of the core universal types.
+ isNull() {
+ return this.type == 0x05;
+ }
+ isBoolean() {
+ return this.type == 0x01;
+ }
+ isInteger() {
+ return this.type == 0x02;
+ }
+ isOctetString() {
+ return this.type == 0x04;
+ }
+ isEnumerated() {
+ return this.type == 0x0a;
+ }
+
+ // Functions to interpret the value in particular ways.
+ // No type checking is performed, as application/context-specific
+ // types can also use these.
+
+ asBoolean() {
+ return this.data[0] != 0;
+ }
+
+ asInteger() {
+ let i = 0;
+ // TODO: handle negative numbers!
+ for (let b of this.data) {
+ i = (i << 8) | b;
+ }
+ return i;
+ }
+
+ asEnumerated() {
+ return this.asInteger();
+ }
+
+ // Helper to interpret an octet string as an ASCII string.
+ asString() {
+ // TODO: pass in expected encoding?
+ if (this.data.length > 0) {
+ return MailStringUtils.uint8ArrayToByteString(new Uint8Array(this.data));
+ }
+ return "";
+ }
+
+ // Static helpers to construct specific types of BERValue.
+ static newNull() {
+ let ber = new BERValue(0x05);
+ ber.data = [];
+ return ber;
+ }
+
+ static newBoolean(b) {
+ let ber = new BERValue(0x01);
+ ber.data = [b ? 0xff : 0x00];
+ return ber;
+ }
+
+ static newInteger(i) {
+ let ber = new BERValue(0x02);
+ // TODO: does this handle negative correctly?
+ while (i >= 128) {
+ ber.data.unshift(i & 255);
+ i >>= 8;
+ }
+ ber.data.unshift(i);
+ return ber;
+ }
+
+ static newEnumerated(i) {
+ let ber = BERValue.newInteger(i);
+ ber.type = 0x0a; // sneaky but valid.
+ return ber;
+ }
+
+ static newOctetString(bytes) {
+ let ber = new BERValue(0x04);
+ ber.data = bytes;
+ return ber;
+ }
+
+ /**
+ * Create an octet string from an ASCII string.
+ */
+ static newString(str) {
+ let ber = new BERValue(0x04);
+ if (str.length > 0) {
+ ber.data = Array.from(str, c => c.charCodeAt(0));
+ }
+ return ber;
+ }
+
+ /**
+ * Create a new sequence
+ *
+ * @param {number} type - BER type byte
+ * @param {Array.<BERValue>} children - The contents of the sequence.
+ */
+ static newSequence(type, children) {
+ let ber = new BERValue(type);
+ ber.children = children;
+ return ber;
+ }
+
+ /*
+ * A helper to dump out the value (and it's children) in a human-readable
+ * way.
+ */
+ dbug(prefix = "") {
+ let desc = "";
+ switch (this.type) {
+ case 0x01:
+ desc += `BOOLEAN (${this.asBoolean()})`;
+ break;
+ case 0x02:
+ desc += `INTEGER (${this.asInteger()})`;
+ break;
+ case 0x04:
+ desc += `OCTETSTRING ("${this.asString()}")`;
+ break;
+ case 0x05:
+ desc += `NULL`;
+ break;
+ case 0x0a:
+ desc += `ENUMERATED (${this.asEnumerated()})`;
+ break;
+ case 0x30:
+ desc += `SEQUENCE`;
+ break;
+ case 0x31:
+ desc += `SET`;
+ break;
+ default:
+ desc = `0x${this.type.toString(16)}`;
+ if (this.isConstructed()) {
+ desc += " SEQUENCE";
+ }
+ break;
+ }
+
+ switch (this.type & 0xc0) {
+ case 0x00:
+ break; // universal
+ case 0x40:
+ desc += " APPLICATION";
+ break;
+ case 0x80:
+ desc += " CONTEXT-SPECIFIC";
+ break;
+ case 0xc0:
+ desc += " PRIVATE";
+ break;
+ }
+
+ if (this.isConstructed()) {
+ desc += ` ${this.children.length} children`;
+ } else {
+ desc += ` ${this.data.length} bytes`;
+ }
+
+ // Dump out the beginning of the payload as raw bytes.
+ let rawdump = this.data.slice(0, 8).join(" ");
+ if (this.data.length > 8) {
+ rawdump += "...";
+ }
+
+ dump(`${prefix}${desc} ${rawdump}\n`);
+
+ for (let c of this.children) {
+ c.dbug(prefix + " ");
+ }
+ }
+}
+
+/**
+ * Parser to decode BER elements from a Connection.
+ */
+class BERParser {
+ constructor(conn) {
+ this._conn = conn;
+ }
+
+ /**
+ * Helper to fetch the next byte in the stream.
+ *
+ * @returns {number} - The byte.
+ */
+ async _nextByte() {
+ let buf = await this._conn.read(1);
+ return buf[0];
+ }
+
+ /**
+ * Helper to read a BER length field from the connection.
+ *
+ * @returns {Array.<number>} - 2 elements: [length, bytesconsumed].
+ */
+ async _readLength() {
+ let n = await this._nextByte();
+ if ((n & 0x80) == 0) {
+ return [n, 1]; // msb clear => single-byte encoding
+ }
+ // lower 7 bits are number of bytes encoding length (big-endian order).
+ n = n & 0x7f;
+ let len = 0;
+ for (let i = 0; i < n; ++i) {
+ len = (len << 8) + (await this._nextByte());
+ }
+ return [len, 1 + n];
+ }
+
+ /**
+ * Reads a single BERValue from the connection (including any children).
+ *
+ * @returns {Array.<number>} - 2 elements: [value, bytesconsumed].
+ */
+ async decodeBERValue() {
+ // BER values always encoded as TLV (type, length, value) triples,
+ // where type is a single byte, length can be a variable number of bytes
+ // and value is a byte string, of size length.
+ let type = await this._nextByte();
+ let [length, lensize] = await this._readLength();
+
+ let ber = new BERValue(type);
+ if (type & 0x20) {
+ // it's a sequence
+ let cnt = 0;
+ while (cnt < length) {
+ let [child, consumed] = await this.decodeBERValue();
+ cnt += consumed;
+ ber.children.push(child);
+ }
+ if (cnt != length) {
+ // All the bytes in the sequence must be accounted for.
+ // TODO: should define a specific BER error type so handler can
+ // detect and respond to BER decoding issues?
+ throw new Error("Mismatched length in sequence");
+ }
+ } else {
+ ber.data = await this._conn.read(length);
+ }
+ return [ber, 1 + lensize + length];
+ }
+}
+
+/*
+ * LDAP-specific code from here on.
+ */
+
+/*
+ * LDAPDaemon holds our LDAP database and has methods for
+ * searching and manipulating the data.
+ * So tests can set up test data here, shared by any number of LDAPHandlerFn
+ * connections.
+ */
+class LDAPDaemon {
+ constructor() {
+ // An entry is an object of the form:
+ // {dn:"....", attributes: {attr1: [val1], attr2:[val2,val3], ...}}
+ // Note that the attribute values are arrays (attributes can have multiple
+ // values in LDAP).
+ this.entries = {}; // We map dn to entry, to ensure dn is unique.
+ this.debug = false;
+ }
+
+ /**
+ * If set, will dump out assorted debugging info.
+ */
+ setDebug(yesno) {
+ this.debug = yesno;
+ }
+
+ /**
+ * Add entries to the LDAP database.
+ * Overwrites previous entries with same dn.
+ * since attributes can have multiple values, they should be arrays.
+ * For example:
+ * {dn: "...", {cn: ["Bob Smith"], ...}}
+ * But because that can be a pain, non-arrays values will be promoted.
+ * So we'll also accept:
+ * {dn: "...", {cn: "Bob Smith", ...}}
+ */
+ add(...entries) {
+ // Clone the data before munging it.
+ let entriesCopy = JSON.parse(JSON.stringify(entries));
+ for (let e of entriesCopy) {
+ if (e.dn === undefined || e.attributes === undefined) {
+ throw new Error("bad entry");
+ }
+
+ // Convert attr values to arrays, if required.
+ for (let [attr, val] of Object.entries(e.attributes)) {
+ if (!Array.isArray(val)) {
+ e.attributes[attr] = [val];
+ }
+ }
+ this.entries[e.dn] = e;
+ }
+ }
+
+ /**
+ * Find entries in our LDAP db.
+ *
+ * @param {BERValue} berFilter - BERValue containing the filter to apply.
+ * @returns {Array} - The matching entries.
+ */
+ search(berFilter) {
+ let f = this.buildFilter(berFilter);
+ return Object.values(this.entries).filter(f);
+ }
+
+ /**
+ * Recursively build a filter function from a BER-encoded filter.
+ * The resulting function accepts a single entry as parameter, and
+ * returns a bool to say if it passes the filter or not.
+ *
+ * @param {BERValue} ber - The filter.
+ * @returns {Function} - A function to test an entry against the filter.
+ */
+ buildFilter(ber) {
+ if (!ber.isContextSpecific()) {
+ throw new Error("Bad filter");
+ }
+
+ switch (ber.tag()) {
+ case 0: {
+ // and
+ if (ber.children.length < 1) {
+ throw new Error("Bad 'and' filter");
+ }
+ let subFilters = ber.children.map(this.buildFilter);
+ return function (e) {
+ return subFilters.every(filt => filt(e));
+ };
+ }
+ case 1: {
+ // or
+ if (ber.children.length < 1) {
+ throw new Error("Bad 'or' filter");
+ }
+ let subFilters = ber.children.map(this.buildFilter);
+ return function (e) {
+ return subFilters.some(filt => filt(e));
+ };
+ }
+ case 2: {
+ // not
+ if (ber.children.length != 1) {
+ throw new Error("Bad 'not' filter");
+ }
+ let subFilter = this.buildFilter(ber.children[0]); // one child
+ return function (e) {
+ return !subFilter(e);
+ };
+ }
+ case 3: {
+ // equalityMatch
+ if (ber.children.length != 2) {
+ throw new Error("Bad 'equality' filter");
+ }
+ let attrName = ber.children[0].asString().toLowerCase();
+ let attrVal = ber.children[1].asString().toLowerCase();
+ return function (e) {
+ let attrs = Object.keys(e.attributes).reduce(function (c, key) {
+ c[key.toLowerCase()] = e.attributes[key];
+ return c;
+ }, {});
+ return (
+ attrs[attrName] !== undefined &&
+ attrs[attrName].map(val => val.toLowerCase()).includes(attrVal)
+ );
+ };
+ }
+ case 7: {
+ // present
+ let attrName = ber.asString().toLowerCase();
+ return function (e) {
+ let attrs = Object.keys(e.attributes).reduce(function (c, key) {
+ c[key.toLowerCase()] = e.attributes[key];
+ return c;
+ }, {});
+ return attrs[attrName] !== undefined;
+ };
+ }
+ case 4: // substring (Probably need to implement this!)
+ case 5: // greaterOrEqual
+ case 6: // lessOrEqual
+ case 8: // approxMatch
+ case 9: // extensibleMatch
+ // UNSUPPORTED! just match everything.
+ dump("WARNING: unsupported filter\n");
+ return e => true;
+ default:
+ throw new Error("unknown filter");
+ }
+ }
+}
+
+/**
+ * Helper class to help break down LDAP handler into multiple functions.
+ * Used by LDAPHandlerFn, below.
+ * Handler state for a single connection (as opposed to any state common
+ * across all connections, which is handled by LDAPDaemon).
+ */
+class LDAPHandler {
+ constructor(conn, daemon) {
+ this._conn = conn;
+ this._daemon = daemon;
+ }
+
+ // handler run() should exit when done, or throw exception to crash out.
+ async run() {
+ let parser = new BERParser(this._conn);
+
+ while (1) {
+ let [msg] = await parser.decodeBERValue();
+ if (this._daemon.debug) {
+ dump("=== received ===\n");
+ msg.dbug("C: ");
+ }
+
+ if (
+ msg.type != 0x30 ||
+ msg.children.length < 2 ||
+ !msg.children[0].isInteger()
+ ) {
+ // badly formed message - TODO: bail out gracefully...
+ throw new Error("Bad message..");
+ }
+
+ let msgID = msg.children[0].asInteger();
+ let req = msg.children[1];
+
+ // Handle a teeny tiny subset of requests.
+ switch (req.type) {
+ case 0x60:
+ this.handleBindRequest(msgID, req);
+ break;
+ case 0x63:
+ this.handleSearchRequest(msgID, req);
+ break;
+ case 0x42: // unbindRequest (essentially a "quit").
+ return;
+ }
+ }
+ }
+
+ /**
+ * Send out an LDAP message.
+ *
+ * @param {number} msgID - The ID of the message we're responding to.
+ * @param {BERValue} payload - The message content.
+ */
+ async sendLDAPMessage(msgID, payload) {
+ let msg = BERValue.newSequence(0x30, [BERValue.newInteger(msgID), payload]);
+ if (this._daemon.debug) {
+ msg.dbug("S: ");
+ }
+ await this._conn.write(msg.encode());
+ }
+
+ async handleBindRequest(msgID, req) {
+ // Ignore the details, just say "OK!"
+ // TODO: Add some auth support here, would be handy for testing.
+ let bindResponse = new BERValue(0x61);
+ bindResponse.children = [
+ BERValue.newEnumerated(0), // resultCode 0=success
+ BERValue.newString(""), // matchedDN
+ BERValue.newString(""), // diagnosticMessage
+ ];
+
+ if (this._daemon.debug) {
+ dump("=== send bindResponse ===\n");
+ }
+ await this.sendLDAPMessage(msgID, bindResponse);
+ }
+
+ async handleSearchRequest(msgID, req) {
+ // Make sure all the parts we expect are present and of correct type.
+ if (
+ req.children.length < 8 ||
+ !req.children[0].isOctetString() ||
+ !req.children[1].isEnumerated() ||
+ !req.children[2].isEnumerated() ||
+ !req.children[3].isInteger() ||
+ !req.children[4].isInteger() ||
+ !req.children[5].isBoolean()
+ ) {
+ throw new Error("Bad search request!");
+ }
+
+ // Perform search
+ let filt = req.children[6];
+ let matches = this._daemon.search(filt);
+
+ // Send a searchResultEntry for each match
+ for (let match of matches) {
+ let dn = BERValue.newString(match.dn);
+ let attrList = new BERValue(0x30);
+ for (let [key, values] of Object.entries(match.attributes)) {
+ let valueSet = new BERValue(0x31);
+ for (let v of values) {
+ valueSet.children.push(BERValue.newString(v));
+ }
+
+ attrList.children.push(
+ BERValue.newSequence(0x30, [BERValue.newString(key), valueSet])
+ );
+ }
+
+ // 0x64 = searchResultEntry
+ let searchResultEntry = BERValue.newSequence(0x64, [dn, attrList]);
+
+ if (this._daemon.debug) {
+ dump(`=== send searchResultEntry ===\n`);
+ }
+ this.sendLDAPMessage(msgID, searchResultEntry);
+ }
+
+ //SearchResultDone ::= [APPLICATION 5] LDAPResult
+ let searchResultDone = new BERValue(0x65);
+ searchResultDone.children = [
+ BERValue.newEnumerated(0), // resultCode 0=success
+ BERValue.newString(""), // matchedDN
+ BERValue.newString(""), // diagnosticMessage
+ ];
+
+ if (this._daemon.debug) {
+ dump(`=== send searchResultDone ===\n`);
+ }
+ this.sendLDAPMessage(msgID, searchResultDone);
+ }
+}
+
+/**
+ * Handler function to deal with a connection to our LDAP server.
+ */
+async function LDAPHandlerFn(conn, daemon) {
+ let handler = new LDAPHandler(conn, daemon);
+ await handler.run();
+}
diff --git a/comm/mailnews/test/fakeserver/Maild.jsm b/comm/mailnews/test/fakeserver/Maild.jsm
new file mode 100644
index 0000000000..30647e1d56
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Maild.jsm
@@ -0,0 +1,566 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// Much of the original code is taken from netwerk's httpserver implementation
+
+var EXPORTED_SYMBOLS = [
+ "nsMailServer",
+ "gThreadManager", // TODO: kill this export
+ "fsDebugNone",
+ "fsDebugAll",
+ "fsDebugRecv",
+ "fsDebugRecvSend",
+];
+
+var CC = Components.Constructor;
+
+/**
+ * The XPCOM thread manager. This declaration is obsolete and exists only
+ * because deleting it breaks several dozen tests at the moment.
+ */
+var gThreadManager = Services.tm;
+
+var fsDebugNone = 0;
+var fsDebugRecv = 1;
+var fsDebugRecvSend = 2;
+var fsDebugAll = 3;
+
+/**
+ * JavaScript constructors for commonly-used classes; precreating these is a
+ * speedup over doing the same from base principles. See the docs at
+ * http://developer.mozilla.org/en/Components.Constructor for details.
+ */
+var ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+var BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+// Time out after 3 minutes
+var TIMEOUT = 3 * 60 * 1000;
+
+/**
+ * The main server handling class. A fake server consists of three parts, this
+ * server implementation (which handles the network communication), the handler
+ * (which handles the state for a connection), and the daemon (which handles
+ * the state for the logical server). To make a new server, one needs to pass
+ * in a function to create handlers--not the handlers themselves--and the
+ * backend daemon. Since each handler presumably needs access to the logical
+ * server daemon, that is passed into the handler creation function. A new
+ * handler will be constructed for every connection made.
+ *
+ * As the core code is inherently single-threaded, it is guaranteed that all of
+ * the calls to the daemon will be made on the same thread, so you do not have
+ * to worry about reentrancy in daemon calls.
+ *
+ * Typical usage:
+ *
+ * function createHandler(daemon) {
+ * return new handler(daemon);
+ * }
+ * do_test_pending();
+ * var server = new nsMailServer(createHandler, serverDaemon);
+ * // Port to use. I tend to like using 1024 + default port number myself.
+ * server.start(port);
+ *
+ * // Set up a connection the server...
+ * server.performTest();
+ * transaction = server.playTransaction();
+ * // Verify that the transaction is correct...
+ *
+ * server.resetTest();
+ * // Set up second test...
+ * server.performTest();
+ * transaction = server.playTransaction();
+ *
+ * // Finished with tests
+ * server.stop();
+ *
+ * var thread = Services.tm.currentThread;
+ * while (thread.hasPendingEvents())
+ * thread.processNextEvent(true);
+ *
+ * do_test_finished();
+ */
+class nsMailServer {
+ constructor(handlerCreator, daemon) {
+ this._debug = fsDebugNone;
+
+ /** The port on which this server listens. */
+ this._port = -1;
+
+ /** The socket associated with this. */
+ this._socket = null;
+
+ /**
+ * True if the socket in this is closed (and closure notifications have been
+ * sent and processed if the socket was ever opened), false otherwise.
+ */
+ this._socketClosed = true;
+
+ /**
+ * Should we log transactions? This only matters if you want to inspect the
+ * protocol traffic. Defaults to true because this was written for protocol
+ * testing.
+ */
+ this._logTransactions = true;
+
+ this._handlerCreator = handlerCreator;
+ this._daemon = daemon;
+ this._readers = [];
+ this._test = false;
+ this._watchWord = undefined;
+
+ /**
+ * An array to hold refs to all the input streams below, so that they don't
+ * get GCed
+ */
+ this._inputStreams = [];
+ }
+
+ onSocketAccepted(socket, trans) {
+ if (this._debug != fsDebugNone) {
+ dump("Received Connection from " + trans.host + ":" + trans.port + "\n");
+ }
+
+ const SEGMENT_SIZE = 1024;
+ const SEGMENT_COUNT = 1024;
+ var input = trans
+ .openInputStream(0, SEGMENT_SIZE, SEGMENT_COUNT)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ this._inputStreams.push(input);
+
+ var handler = this._handlerCreator(this._daemon);
+ var reader = new nsMailReader(
+ this,
+ handler,
+ trans,
+ this._debug,
+ this._logTransactions
+ );
+ this._readers.push(reader);
+
+ // Note: must use main thread here, or we might get a GC that will cause
+ // threadsafety assertions. We really need to fix XPConnect so that
+ // you can actually do things in multi-threaded JS. :-(
+ input.asyncWait(reader, 0, 0, Services.tm.mainThread);
+ this._test = true;
+ }
+
+ onStopListening(socket, status) {
+ if (this._debug != fsDebugNone) {
+ dump("Connection Lost " + status + "\n");
+ }
+
+ this._socketClosed = true;
+ // We've been killed or we've stopped, reset the handler to the original
+ // state (e.g. to require authentication again).
+ for (var i = 0; i < this._readers.length; i++) {
+ this._readers[i]._handler.resetTest();
+ this._readers[i]._realCloseSocket();
+ }
+ }
+
+ setDebugLevel(debug) {
+ this._debug = debug;
+ for (var i = 0; i < this._readers.length; i++) {
+ this._readers[i].setDebugLevel(debug);
+ }
+ }
+
+ start(port = -1) {
+ if (this._socket) {
+ throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+
+ if (port > 0) {
+ this._port = port;
+ }
+ this._socketClosed = false;
+
+ var socket = new ServerSocket(
+ this._port,
+ true, // loopback only
+ -1
+ ); // default number of pending connections
+
+ socket.asyncListen(this);
+ this._socket = socket;
+ }
+
+ stop() {
+ if (!this._socket) {
+ return;
+ }
+
+ this._socket.close();
+ this._socket = null;
+
+ for (let reader of this._readers) {
+ reader._realCloseSocket();
+ }
+
+ if (this._readers.some(e => e.observer.forced)) {
+ return;
+ }
+
+ // spin an event loop and wait for the socket-close notification
+ let thr = Services.tm.currentThread;
+ while (!this._socketClosed) {
+ // Don't wait for the next event, just in case there isn't one.
+ thr.processNextEvent(false);
+ }
+ }
+ stopTest() {
+ this._test = false;
+ }
+
+ get port() {
+ if (this._port == -1) {
+ this._port = this._socket.port;
+ }
+ return this._port;
+ }
+
+ // NSISUPPORTS
+
+ //
+ // see nsISupports.QueryInterface
+ //
+ QueryInterface = ChromeUtils.generateQI(["nsIServerSocketListener"]);
+
+ // NON-XPCOM PUBLIC API
+
+ /**
+ * Returns true if this server is not running (and is not in the process of
+ * serving any requests still to be processed when the server was last
+ * stopped after being run).
+ */
+ isStopped() {
+ return this._socketClosed;
+ }
+
+ /**
+ * Runs the test. It will not exit until the test has finished.
+ */
+ performTest(watchWord) {
+ this._watchWord = watchWord;
+
+ let thread = Services.tm.currentThread;
+ while (!this.isTestFinished()) {
+ thread.processNextEvent(false);
+ }
+ }
+
+ /**
+ * Returns true if the current processing test has finished.
+ */
+ isTestFinished() {
+ return this._readers.length > 0 && !this._test;
+ }
+
+ /**
+ * Returns the commands run between the server and client.
+ * The return is an object with two variables (us and them), both of which
+ * are arrays returning the commands given by each server.
+ */
+ playTransaction() {
+ if (this._readers.some(e => e.observer.forced)) {
+ throw new Error("Server timed out!");
+ }
+ if (this._readers.length == 1) {
+ return this._readers[0].transaction;
+ }
+ return this._readers.map(e => e.transaction);
+ }
+
+ /**
+ * Prepares for the next test.
+ */
+ resetTest() {
+ this._readers = this._readers.filter(function (reader) {
+ return reader._isRunning;
+ });
+ this._test = true;
+ for (var i = 0; i < this._readers.length; i++) {
+ this._readers[i]._handler.resetTest();
+ }
+ }
+}
+
+function readTo(input, count, arr) {
+ var old = new BinaryInputStream(input).readByteArray(count);
+ Array.prototype.push.apply(arr, old);
+}
+
+/**
+ * The nsMailReader service, which reads and handles the lines.
+ * All specific handling is passed off to the handler, which is responsible
+ * for maintaining its own state. The following commands are required for the
+ * handler object:
+ * onError Called when handler[command] does not exist with both the
+ * command and rest-of-line as arguments
+ * onStartup Called on initialization with no arguments
+ * onMultiline Called when in multiline with the entire line as an argument
+ * postCommand Called after every command with this reader as the argument
+ * [command] An untranslated command with the rest of the line as the
+ * argument. Defined as everything to the first space
+ *
+ * All functions, except onMultiline and postCommand, treat the
+ * returned value as the text to be sent to the client; a newline at the end
+ * may be added if it does not exist, and all lone newlines are converted to
+ * CRLF sequences.
+ *
+ * The return of postCommand is ignored. The return of onMultiline is a bit
+ * complicated: it may or may not return a response string (returning one is
+ * necessary to trigger the postCommand handler).
+ *
+ * This object has the following supplemental functions for use by handlers:
+ * closeSocket Performs a server-side socket closing
+ * setMultiline Sets the multiline mode based on the argument
+ */
+class nsMailReader {
+ constructor(server, handler, transport, debug, logTransaction) {
+ this._debug = debug;
+ this._server = server;
+ this._buffer = [];
+ this._lines = [];
+ this._handler = handler;
+ this._transport = transport;
+ // We don't seem to properly handle large streams when the buffer gets
+ // exhausted, which causes issues trying to test large messages. So just
+ // allow a really big buffer.
+ var output = transport.openOutputStream(
+ Ci.nsITransport.OPEN_BLOCKING,
+ 1024,
+ 4096
+ );
+ this._output = output;
+ if (logTransaction) {
+ this.transaction = { us: [], them: [] };
+ } else {
+ this.transaction = null;
+ }
+
+ // Send response line
+ var response = this._handler.onStartup();
+ response = response.replace(/([^\r])\n/g, "$1\r\n");
+ if (!response.endsWith("\n")) {
+ response = response + "\r\n";
+ }
+ if (this.transaction) {
+ this.transaction.us.push(response);
+ }
+ this._output.write(response, response.length);
+ this._output.flush();
+
+ this._multiline = false;
+
+ this._isRunning = true;
+
+ this.observer = {
+ server,
+ forced: false,
+ notify(timer) {
+ this.forced = true;
+ this.server.stopTest();
+ this.server.stop();
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+ };
+ this.timer = Cc["@mozilla.org/timer;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsITimer);
+ this.timer.initWithCallback(
+ this.observer,
+ TIMEOUT,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+
+ _findLines() {
+ var buf = this._buffer;
+ for (
+ var crlfLoc = buf.indexOf(13);
+ crlfLoc >= 0;
+ crlfLoc = buf.indexOf(13, crlfLoc + 1)
+ ) {
+ if (buf[crlfLoc + 1] == 10) {
+ break;
+ }
+ }
+ if (crlfLoc == -1) {
+ // We failed to find a newline
+ return;
+ }
+
+ var line = String.fromCharCode.apply(null, buf.slice(0, crlfLoc));
+ this._buffer = buf.slice(crlfLoc + 2);
+ this._lines.push(line);
+ this._findLines();
+ }
+
+ onInputStreamReady(stream) {
+ if (this.observer.forced) {
+ return;
+ }
+
+ this.timer.cancel();
+ try {
+ var bytes = stream.available();
+ } catch (e) {
+ // Someone, not us, has closed the stream. This means we can't get any
+ // more data from the stream, so we'll just go and close our socket.
+ this._realCloseSocket();
+ return;
+ }
+ readTo(stream, bytes, this._buffer);
+ this._findLines();
+
+ while (this._lines.length > 0) {
+ var line = this._lines.shift();
+
+ if (this._debug != fsDebugNone) {
+ dump("RECV: " + line + "\n");
+ }
+
+ var response;
+ try {
+ let command;
+ if (this._multiline) {
+ response = this._handler.onMultiline(line);
+
+ if (response === undefined) {
+ continue;
+ }
+ } else {
+ // Record the transaction
+ if (this.transaction) {
+ this.transaction.them.push(line);
+ }
+
+ // Find the command and splice it out...
+ var splitter = line.indexOf(" ");
+ command = splitter == -1 ? line : line.substring(0, splitter);
+ let args = splitter == -1 ? "" : line.substring(splitter + 1);
+
+ // By convention, commands are uppercase
+ command = command.toUpperCase();
+
+ if (this._debug == fsDebugAll) {
+ dump("Received command " + command + "\n");
+ }
+
+ if (command in this._handler) {
+ response = this._handler[command](args);
+ } else {
+ response = this._handler.onError(command, args);
+ }
+ }
+
+ this._preventLFMunge = false;
+ this._handler.postCommand(this);
+
+ if (this.watchWord && command == this.watchWord) {
+ this.stopTest();
+ }
+ } catch (e) {
+ response = this._handler.onServerFault(e);
+ if (e instanceof Error) {
+ dump(e.name + ": " + e.message + "\n");
+ dump("File: " + e.fileName + " Line: " + e.lineNumber + "\n");
+ dump("Stack trace:\n" + e.stack);
+ } else {
+ dump("Exception caught: " + e + "\n");
+ }
+ }
+
+ if (!this._preventLFMunge) {
+ response = response.replaceAll("\r\n", "\n").replaceAll("\n", "\r\n");
+ }
+
+ if (!response.endsWith("\n")) {
+ response = response + "\r\n";
+ }
+
+ if (this._debug == fsDebugRecvSend) {
+ dump("SEND: " + response.split(" ", 1)[0] + "\n");
+ } else if (this._debug == fsDebugAll) {
+ var responses = response.split("\n");
+ responses.forEach(function (line) {
+ dump("SEND: " + line + "\n");
+ });
+ }
+
+ if (this.transaction) {
+ this.transaction.us.push(response);
+ }
+
+ try {
+ this._output.write(response, response.length);
+ this._output.flush();
+ } catch (ex) {
+ if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
+ dump("Stream closed whilst sending, this may be expected\n");
+ this._realCloseSocket();
+ } else {
+ // Some other issue, let the test see it.
+ throw ex;
+ }
+ }
+
+ if (this._signalStop) {
+ this._realCloseSocket();
+ this._signalStop = false;
+ }
+ }
+
+ if (this._isRunning) {
+ stream.asyncWait(this, 0, 0, Services.tm.currentThread);
+ this.timer.initWithCallback(
+ this.observer,
+ TIMEOUT,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+ }
+
+ closeSocket() {
+ this._signalStop = true;
+ }
+ _realCloseSocket() {
+ this._isRunning = false;
+ this._output.close();
+ this._transport.close(Cr.NS_OK);
+ this._server.stopTest();
+ }
+
+ setMultiline(multi) {
+ this._multiline = multi;
+ }
+
+ setDebugLevel(debug) {
+ this._debug = debug;
+ }
+
+ preventLFMunge() {
+ this._preventLFMunge = true;
+ }
+
+ get watchWord() {
+ return this._server._watchWord;
+ }
+
+ stopTest() {
+ this._server.stopTest();
+ }
+
+ QueryInterface = ChromeUtils.generateQI(["nsIInputStreamCallback"]);
+}
diff --git a/comm/mailnews/test/fakeserver/Nntpd.jsm b/comm/mailnews/test/fakeserver/Nntpd.jsm
new file mode 100644
index 0000000000..f6d1be0a48
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Nntpd.jsm
@@ -0,0 +1,631 @@
+/* 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 NNTP servers
+
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+var EXPORTED_SYMBOLS = [
+ "NntpDaemon",
+ "NewsArticle",
+ "NNTP_POSTABLE",
+ "NNTP_REAL_LENGTH",
+ "NNTP_RFC977_handler",
+ "NNTP_RFC2980_handler",
+ "NNTP_RFC3977_handler",
+ "NNTP_Giganews_handler",
+ "NNTP_RFC4643_extension",
+];
+
+class NntpDaemon {
+ constructor(flags) {
+ this._groups = {};
+ this._messages = {};
+ this._flags = flags;
+ }
+ addGroup(group, postable) {
+ var flags = 0;
+ if (postable) {
+ flags |= NNTP_POSTABLE;
+ }
+ this._groups[group] = { keys: [], flags, nextKey: 1 };
+ }
+ addArticle(article) {
+ this._messages[article.messageID] = article;
+ for (let group of article.groups) {
+ if (group in this._groups) {
+ var key = this._groups[group].nextKey++;
+ this._groups[group][key] = article;
+ this._groups[group].keys.push(key);
+ }
+ }
+ }
+ addArticleToGroup(article, group, key) {
+ this._groups[group][key] = article;
+ this._messages[article.messageID] = article;
+ this._groups[group].keys.push(key);
+ if (this._groups[group].nextKey <= key) {
+ this._groups[group].nextKey = key + 1;
+ }
+ }
+ removeArticleFromGroup(groupName, key) {
+ let group = this._groups[groupName];
+ delete group[key];
+ group.keys = group.keys.filter(x => x != key);
+ }
+ getGroup(group) {
+ if (this._groups.hasOwnProperty(group)) {
+ return this._groups[group];
+ }
+ return null;
+ }
+ getGroupStats(group) {
+ if (group.keys.length == 0) {
+ return [0, 0, 0];
+ }
+ var min = 1 << 30;
+ var max = 0;
+ group.keys.forEach(function (key) {
+ if (key < min) {
+ min = key;
+ }
+ if (key > max) {
+ max = key;
+ }
+ });
+
+ var length;
+ if (hasFlag(this._flags, NNTP_REAL_LENGTH)) {
+ length = group.keys.length;
+ } else {
+ length = max - min + 1;
+ }
+
+ return [length, min, max];
+ }
+ getArticle(msgid) {
+ if (msgid in this._messages) {
+ return this._messages[msgid];
+ }
+ return null;
+ }
+}
+
+function NewsArticle(text) {
+ this.headers = new Map();
+ this.body = "";
+ this.messageID = "";
+ this.fullText = text;
+
+ var headerMap;
+ [headerMap, this.body] = MimeParser.extractHeadersAndBody(text);
+ for (var [header, values] of headerMap._rawHeaders) {
+ var value = values[0];
+ this.headers.set(header, value);
+ if (header == "message-id") {
+ var start = value.indexOf("<");
+ var end = value.indexOf(">", start);
+ this.messageID = value.substring(start, end + 1);
+ } else if (header == "newsgroups") {
+ this.groups = value.split(/[ \t]*,[ \t]*/);
+ }
+ }
+
+ // Add in non-existent fields
+ if (!this.headers.has("lines")) {
+ let lines = this.body.split("\n").length;
+ this.headers.set("lines", lines);
+ }
+}
+
+/**
+ * This function converts an NNTP wildmat into a regular expression.
+ *
+ * I don't know how accurate it is wrt i18n characters, but its primary usage
+ * right now is just XPAT, where i18n effects are utterly unspecified, so I am
+ * not too concerned.
+ *
+ * This also neglects cases where special characters are in [] blocks.
+ */
+function wildmat2regex(wildmat) {
+ // Special characters in regex that aren't special in wildmat
+ wildmat = wildmat.replace(/[$+.()|{}^]/, function (str) {
+ return "\\" + str;
+ });
+ wildmat = wildmat.replace(/(\\*)([*?])/, function (str, p1, p2) {
+ // TODO: This function appears to be wrong on closer inspection.
+ if (p1.length % 2 == 0) {
+ return p2 == "*" ? ".*" : ".";
+ }
+ return str;
+ });
+ return new RegExp(wildmat);
+}
+
+// NNTP FLAGS
+var NNTP_POSTABLE = 0x0001;
+
+var NNTP_REAL_LENGTH = 0x0100;
+
+function hasFlag(flags, flag) {
+ return (flags & flag) == flag;
+}
+
+// NNTP TEST SERVERS
+// -----------------
+// To be comprehensive about testing and fallback, we define these varying
+// levels of RFC-compliance:
+// * RFC 977 solely (there's not a lot there!)
+// * RFC 977 + 2980 (note that there are varying levels of this impl)
+// * RFC 3977 bare bones
+// * RFC 3977 full
+// * RFC 3977 + post-3977 extensions
+// * Giganews (Common newsserver for ISP stuff; highest importance)
+// * INN 2.4 (Gold standard common implementation; second highest importance)
+// Note too that we want various levels of brokenness:
+// * Perm errors that require login
+// * "I can't handle that" (e.g., news.mozilla.org only supports XOVER for
+// searching with XHDR)
+// * Naive group counts, missing articles
+// * Limitations on what can be posted
+
+// This handler implements the bare minimum required by RFC 977. Actually, not
+// even that much: IHAVE and SLAVE are not implemented, as those two are
+// explicitly server implementations.
+class NNTP_RFC977_handler {
+ constructor(daemon) {
+ this._daemon = daemon;
+ this.closing = false;
+ this.resetTest();
+ }
+ resetTest() {
+ this.extraCommands = "";
+ this.articleKey = null;
+ this.group = null;
+ }
+ ARTICLE(args) {
+ var info = this._selectArticle(args, 220);
+ if (info[0] == null) {
+ return info[1];
+ }
+
+ var response = info[1] + "\n";
+ response += info[0].fullText.replace(/^\./gm, "..");
+ response += ".";
+ return response;
+ }
+ BODY(args) {
+ var info = this._selectArticle(args, 222);
+ if (info[0] == null) {
+ return info[1];
+ }
+
+ var response = info[1] + "\n";
+ response += info[0].body.replace(/^\./gm, "..");
+ response += ".";
+ return response;
+ }
+ GROUP(args) {
+ var group = this._daemon.getGroup(args);
+ if (group == null) {
+ return "411 no such news group";
+ }
+
+ this.group = group;
+ this.articleKey = 0 in this.group.keys ? this.group.keys[0] : null;
+
+ var stats = this._daemon.getGroupStats(group);
+ return (
+ "211 " +
+ stats[0] +
+ " " +
+ stats[1] +
+ " " +
+ stats[2] +
+ " " +
+ args +
+ " group selected"
+ );
+ }
+ HEAD(args) {
+ var info = this._selectArticle(args, 221);
+ if (info[0] == null) {
+ return info[1];
+ }
+
+ var response = info[1] + "\n";
+ for (let [header, value] of info[0].headers) {
+ response += header + ": " + value + "\n";
+ }
+ response += ".";
+ return response;
+ }
+ HELP(args) {
+ var response = "100 Why certainly, here is my help:\n";
+ response += "Mozilla fake NNTP RFC 977 testing server";
+ response += "Commands supported:\n";
+ response += "\tARTICLE <message-id> | [nnn]\n";
+ response += "\tBODY\n";
+ response += "\tGROUP group\n";
+ response += "\tHEAD\n";
+ response += "\tHELP\n";
+ response += "\tLAST\n";
+ response += "\tLIST\n";
+ response += "\tNEWGROUPS\n";
+ response += "\tNEWNEWS\n";
+ response += "\tNEXT\n";
+ response += "\tPOST\n";
+ response += "\tQUIT\n";
+ response += "\tSTAT\n";
+ response += this.extraCommands;
+ response += ".";
+ return response;
+ }
+ LAST(args) {
+ if (this.group == null) {
+ return "412 no newsgroup selected";
+ }
+ if (this.articleKey == null) {
+ return "420 no current article has been selected";
+ }
+ return "502 Command not implemented";
+ }
+ LIST(args) {
+ var response = "215 list of newsgroup follows\n";
+ for (let groupname in this._daemon._groups) {
+ let group = this._daemon._groups[groupname];
+ let stats = this._daemon.getGroupStats(group);
+ response +=
+ groupname +
+ " " +
+ stats[1] +
+ " " +
+ stats[0] +
+ " " +
+ (hasFlag(group.flags, NNTP_POSTABLE) ? "y" : "n") +
+ "\n";
+ }
+ response += ".";
+ return response;
+ }
+ NEWGROUPS(args) {
+ return "502 Command not implemented";
+ }
+ NEWNEWS(args) {
+ return "502 Command not implemented";
+ }
+ NEXT(args) {
+ if (this.group == null) {
+ return "412 no newsgroup selected";
+ }
+ if (this.articleKey == null) {
+ return "420 no current article has been selected";
+ }
+ return "502 Command not implemented";
+ }
+ POST(args) {
+ this.posting = true;
+ this.post = "";
+ return "340 Please continue";
+ }
+ QUIT(args) {
+ this.closing = true;
+ return "205 closing connection - goodbye!";
+ }
+ STAT(args) {
+ var info = this._selectArticle(args, 223);
+ return info[1];
+ }
+ LISTGROUP(args) {
+ // Yes, I know this isn't RFC 977, but I doubt that mailnews will ever drop
+ // its requirement for this, so I'll stuff it in here anyways...
+ var group = args == "" ? this.group : this._daemon.getGroup(args);
+ if (group == null) {
+ return "411 This newsgroup does not exist";
+ }
+
+ var response = "211 Articles follow:\n";
+ for (let key of group.keys) {
+ response += key + "\n";
+ }
+ response += ".\n";
+ return response;
+ }
+
+ onError(command, args) {
+ return "500 command not recognized";
+ }
+ onServerFault(e) {
+ return "500 internal server error: " + e;
+ }
+ onStartup() {
+ this.closing = false;
+ this.group = null;
+ this.article = null;
+ this.posting = false;
+ return "200 posting allowed";
+ }
+ onMultiline(line) {
+ if (line == ".") {
+ if (this.posting) {
+ var article = new NewsArticle(this.post);
+ this._daemon.addArticle(article);
+ this.posting = false;
+ return "240 Wonderful article, your style is gorgeous!";
+ }
+ }
+
+ if (this.posting) {
+ if (line.startsWith(".")) {
+ line = line.substring(1);
+ }
+
+ this.post += line + "\n";
+ }
+
+ return undefined;
+ }
+ postCommand(reader) {
+ if (this.closing) {
+ reader.closeSocket();
+ }
+ reader.setMultiline(this.posting);
+ }
+
+ /**
+ * Selects an article based on args.
+ *
+ * Returns an array of objects consisting of:
+ * # The selected article (or null if non was selected
+ * # The first line response
+ */
+ _selectArticle(args, responseCode) {
+ var art, key;
+ if (args == "") {
+ if (this.group == null) {
+ return [null, "412 no newsgroup has been selected"];
+ }
+ if (this.articleKey == null) {
+ return [null, "420 no current article has been selected"];
+ }
+
+ art = this.group[this.articleKey];
+ key = this.articleKey;
+ } else if (args.startsWith("<")) {
+ art = this._daemon.getArticle(args);
+ key = 0;
+
+ if (art == null) {
+ return [null, "430 no such article found"];
+ }
+ } else {
+ if (this.group == null) {
+ return [null, "412 no newsgroup has been selected"];
+ }
+
+ key = parseInt(args);
+ if (key in this.group) {
+ this.articleKey = key;
+ art = this.group[key];
+ } else {
+ return [null, "423 no such article number in this group"];
+ }
+ }
+
+ var respCode =
+ responseCode + " " + key + " " + art.messageID + " article selected";
+ return [art, respCode];
+ }
+}
+
+class NNTP_RFC2980_handler extends NNTP_RFC977_handler {
+ DATE(args) {
+ return "502 Command not implemented";
+ }
+ LIST(args) {
+ var index = args.indexOf(" ");
+ var command = index == -1 ? args : args.substring(0, index);
+ args = index == -1 ? "" : args.substring(index + 1);
+ command = command.toUpperCase();
+ if ("LIST_" + command in this) {
+ return this["LIST_" + command](args);
+ }
+ return super.LIST(command + " " + args);
+ }
+ LIST_ACTIVE(args) {
+ return super.LIST(args);
+ }
+ MODE(args) {
+ if (args == "READER") {
+ return this.onStartup();
+ }
+ return "500 What do you think you're trying to pull here?";
+ }
+ XHDR(args) {
+ if (!this.group) {
+ return "412 No group selected";
+ }
+
+ args = args.split(" ");
+ var header = args[0].toLowerCase();
+ var found = false;
+ var response = "221 Headers abound\n";
+ for (let key of this._filterRange(args[1], this.group.keys)) {
+ if (!this.group[key].headers.has(header)) {
+ continue;
+ }
+ found = true;
+ response += key + " " + this.group[key].headers.get(header) + "\n";
+ }
+ if (!found) {
+ return "420 No such article";
+ }
+ response += ".";
+ return response;
+ }
+ XOVER(args) {
+ if (!this.group) {
+ return "412 No group selected";
+ }
+
+ args = args.split(/ +/, 3);
+ var response = "224 List of articles\n";
+ for (let key of this._filterRange(args[0], this.group.keys)) {
+ response += key + "\t";
+ var article = this.group[key];
+ response +=
+ article.headers.get("subject") +
+ "\t" +
+ article.headers.get("from") +
+ "\t" +
+ article.headers.get("date") +
+ "\t" +
+ article.headers.get("message-id") +
+ "\t" +
+ (article.headers.get("references") || "") +
+ "\t" +
+ article.fullText.replace(/\r?\n/, "\r\n").length +
+ "\t" +
+ article.body.split(/\r?\n/).length +
+ "\t" +
+ (article.headers.get("xref") || "") +
+ "\n";
+ }
+ response += ".\n";
+ return response;
+ }
+ XPAT(args) {
+ if (!this.group) {
+ return "412 No group selected";
+ }
+
+ /* XPAT header range ... */
+ args = args.split(/ +/, 3);
+ let header = args[0].toLowerCase();
+ let regex = wildmat2regex(args[2]);
+
+ let response = "221 Results follow\n";
+ for (let key of this._filterRange(args[1], this.group.keys)) {
+ let article = this.group[key];
+ if (
+ article.headers.has(header) &&
+ regex.test(article.headers.get(header))
+ ) {
+ response += key + " " + article.headers.get(header) + "\n";
+ }
+ }
+ return response + ".";
+ }
+
+ _filterRange(range, keys) {
+ let dash = range.indexOf("-");
+ let low, high;
+ if (dash < 0) {
+ low = high = parseInt(range);
+ } else {
+ low = parseInt(range.substring(0, dash));
+ if (dash < range.length - 1) {
+ high = range.substring(dash + 1);
+ } else {
+ // Everything is less than this.
+ high = 1.0 / 0.0;
+ }
+ }
+ return keys.filter(function (e) {
+ return low <= e && e <= high;
+ });
+ }
+}
+
+class NNTP_Giganews_handler extends NNTP_RFC2980_handler {
+ XHDR(args) {
+ var header = args.split(" ")[0].toLowerCase();
+ if (
+ header in ["subject", "from", "xref", "date", "message-id", "references"]
+ ) {
+ return super.XHDR(args);
+ }
+ return "503 unsupported header field";
+ }
+}
+
+class NNTP_RFC4643_extension extends NNTP_RFC2980_handler {
+ constructor(daemon) {
+ super(daemon);
+
+ this.extraCommands += "\tAUTHINFO USER\n";
+ this.extraCommands += "\tAUTHINFO PASS\n";
+ this.expectedUsername = "testnews";
+ this.expectedPassword = "newstest";
+ this.requireBoth = true;
+ this.authenticated = false;
+ this.usernameReceived = false;
+ }
+
+ AUTHINFO(args) {
+ if (this.authenticated) {
+ return "502 Command unavailable";
+ }
+
+ var argSplit = args.split(" ");
+ var action = argSplit[0];
+ var param = argSplit[1];
+
+ if (action == "user") {
+ if (this.usernameReceived) {
+ return "502 Command unavailable";
+ }
+
+ var expectUsername = this.lastGroupTried
+ ? this._daemon.groupCredentials[this.lastGroupTried][0]
+ : this.expectedUsername;
+ if (param != expectUsername) {
+ return "481 Authentication failed";
+ }
+
+ this.usernameReceived = true;
+ if (this.requireBoth) {
+ return "381 Password required";
+ }
+
+ this.authenticated = this.lastGroupTried ? this.lastGroupTried : true;
+ return "281 Authentication Accepted";
+ } else if (action == "pass") {
+ if (!this.requireBoth || !this.usernameReceived) {
+ return "482 Authentication commands issued out of sequence";
+ }
+
+ this.usernameReceived = false;
+
+ var expectPassword = this.lastGroupTried
+ ? this._daemon.groupCredentials[this.lastGroupTried][1]
+ : this.expectedPassword;
+ if (param != expectPassword) {
+ return "481 Authentication failed";
+ }
+
+ this.authenticated = this.lastGroupTried ? this.lastGroupTried : true;
+ return "281 Authentication Accepted";
+ }
+ return "502 Invalid Command";
+ }
+ LIST(args) {
+ if (this.authenticated) {
+ return args ? super.LIST(args) : "502 Invalid command: LIST";
+ }
+ return "480 Authentication required";
+ }
+ GROUP(args) {
+ if (
+ (this._daemon.groupCredentials != null && this.authenticated == args) ||
+ (this._daemon.groupCredentials == null && this.authenticated)
+ ) {
+ return super.GROUP(args);
+ }
+ if (this._daemon.groupCredentials != null) {
+ this.lastGroupTried = args;
+ }
+ return "480 Authentication required";
+ }
+}
diff --git a/comm/mailnews/test/fakeserver/Pop3d.jsm b/comm/mailnews/test/fakeserver/Pop3d.jsm
new file mode 100644
index 0000000000..33a2b06a90
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Pop3d.jsm
@@ -0,0 +1,454 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Contributors:
+ * Ben Bucksch <ben.bucksch beonex.com> <http://business.beonex.com> (RFC 5034 Authentication)
+ */
+/* This file implements test POP3 servers
+ */
+
+var EXPORTED_SYMBOLS = [
+ "Pop3Daemon",
+ "POP3_RFC1939_handler",
+ "POP3_RFC2449_handler",
+ "POP3_RFC5034_handler",
+];
+
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+
+// Since we don't really need to worry about peristence, we can just
+// use a UIDL counter.
+var gUIDLCount = 1;
+
+/**
+ * Read the contents of a file to the string.
+ *
+ * @param fileName A path relative to the current working directory, or
+ * a filename underneath the "data" directory relative to
+ * the cwd.
+ */
+function readFile(fileName) {
+ let cwd = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+
+ // Try to find the file relative to either the data directory or to the
+ // current working directory.
+ let file = cwd.clone();
+ if (fileName.includes("/")) {
+ let parts = fileName.split("/");
+ for (let part of parts) {
+ if (part == "..") {
+ file = file.parent;
+ } else {
+ file.append(part);
+ }
+ }
+ } else {
+ file.append("data");
+ file.append(fileName);
+ }
+
+ if (!file.exists()) {
+ throw new Error("Cannot find file named " + fileName);
+ }
+
+ return mailTestUtils.loadFileToString(file);
+}
+
+class Pop3Daemon {
+ messages = [];
+ _messages = [];
+ _totalMessageSize = 0;
+
+ /**
+ * Set the messages that the POP3 daemon will provide to its clients.
+ *
+ * @param messages An array of either 1) strings that are filenames whose
+ * contents will be loaded from the files or 2) objects with a "fileData"
+ * attribute whose value is the content of the file.
+ */
+ setMessages(messages) {
+ this._messages = [];
+ this._totalMessageSize = 0;
+
+ function addMessage(element) {
+ // if it's a string, then it's a file-name.
+ if (typeof element == "string") {
+ this._messages.push({ fileData: readFile(element), size: -1 });
+ } else {
+ // Otherwise it's an object as dictionary already.
+ this._messages.push(element);
+ }
+ }
+ messages.forEach(addMessage, this);
+
+ for (var i = 0; i < this._messages.length; ++i) {
+ this._messages[i].size = this._messages[i].fileData.length;
+ this._messages[i].uidl = "UIDL" + gUIDLCount++;
+ this._totalMessageSize += this._messages[i].size;
+ }
+ }
+ getTotalMessages() {
+ return this._messages.length;
+ }
+ getTotalMessageSize() {
+ return this._totalMessageSize;
+ }
+}
+
+// POP3 TEST SERVERS
+// -----------------
+
+var kStateAuthNeeded = 1; // Not authenticated yet, need username and password
+var kStateAuthPASS = 2; // got command USER, expecting command PASS
+var kStateTransaction = 3; // Authenticated, can fetch and delete mail
+
+/**
+ * This handler implements the bare minimum required by RFC 1939.
+ * If dropOnAuthFailure is set, the server will drop the connection
+ * on authentication errors, to simulate servers that do the same.
+ */
+class POP3_RFC1939_handler {
+ kUsername = "fred";
+ kPassword = "wilma";
+
+ constructor(daemon) {
+ this._daemon = daemon;
+ this.closing = false;
+ this.dropOnAuthFailure = false;
+ this._multiline = false;
+ this.resetTest();
+ }
+
+ resetTest() {
+ this._state = kStateAuthNeeded;
+ }
+
+ USER(args) {
+ if (this._state != kStateAuthNeeded) {
+ return "-ERR invalid state";
+ }
+
+ if (args == this.kUsername) {
+ this._state = kStateAuthPASS;
+ return "+OK user recognized";
+ }
+
+ return "-ERR sorry, no such mailbox";
+ }
+ PASS(args) {
+ if (this._state != kStateAuthPASS) {
+ return "-ERR invalid state";
+ }
+
+ if (args == this.kPassword) {
+ this._state = kStateTransaction;
+ return "+OK maildrop locked and ready";
+ }
+
+ this._state = kStateAuthNeeded;
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR invalid password";
+ }
+ STAT(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+
+ return (
+ "+OK " +
+ this._daemon.getTotalMessages() +
+ " " +
+ this._daemon.getTotalMessageSize()
+ );
+ }
+ LIST(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+
+ var result = "+OK " + this._daemon._messages.length + " messages\r\n";
+ for (var i = 0; i < this._daemon._messages.length; ++i) {
+ result += i + 1 + " " + this._daemon._messages[i].size + "\r\n";
+ }
+
+ result += ".";
+ return result;
+ }
+ UIDL(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ let result = "+OK\r\n";
+ for (let i = 0; i < this._daemon._messages.length; ++i) {
+ result += i + 1 + " " + this._daemon._messages[i].uidl + "\r\n";
+ }
+
+ result += ".";
+ return result;
+ }
+ TOP(args) {
+ let [messageNumber, numberOfBodyLines] = args.split(" ");
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ let result = "+OK\r\n";
+ let msg = this._daemon._messages[messageNumber - 1].fileData;
+ let index = msg.indexOf("\r\n\r\n");
+ result += msg.slice(0, index);
+ if (numberOfBodyLines) {
+ result += "\r\n\r\n";
+ let bodyLines = msg.slice(index + 4).split("\r\n");
+ result += bodyLines.slice(0, numberOfBodyLines).join("\r\n");
+ }
+ result += "\r\n.";
+ return result;
+ }
+ RETR(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+
+ var result = "+OK " + this._daemon._messages[args - 1].size + "\r\n";
+ result += this._daemon._messages[args - 1].fileData;
+ result += ".";
+ return result;
+ }
+ DELE(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ return "+OK";
+ }
+ NOOP(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ return "+OK";
+ }
+ RSET(args) {
+ if (this._state != kStateTransaction) {
+ return "-ERR invalid state";
+ }
+ this._state = kStateAuthNeeded;
+ return "+OK";
+ }
+ QUIT(args) {
+ // Let the client close the socket
+ // this.closing = true;
+ return "+OK fakeserver signing off";
+ }
+ onStartup() {
+ this.closing = false;
+ this._state = kStateAuthNeeded;
+ return "+OK Fake POP3 server ready";
+ }
+ onError(command, args) {
+ return "-ERR command " + command + " not implemented";
+ }
+ onServerFault(e) {
+ return "-ERR internal server error: " + e;
+ }
+ postCommand(reader) {
+ reader.setMultiline(this._multiline);
+ if (this.closing) {
+ reader.closeSocket();
+ }
+ }
+}
+
+/**
+ * This implements CAPA
+ *
+ * @see RFC 2449
+ */
+class POP3_RFC2449_handler extends POP3_RFC1939_handler {
+ kCapabilities = ["UIDL", "TOP"]; // the test may adapt this as necessary
+
+ CAPA(args) {
+ var capa = "+OK List of our wanna-be capabilities follows:\r\n";
+ for (var i = 0; i < this.kCapabilities.length; i++) {
+ capa += this.kCapabilities[i] + "\r\n";
+ }
+ if (this.capaAdditions) {
+ capa += this.capaAdditions();
+ }
+ capa += "IMPLEMENTATION fakeserver\r\n.";
+ return capa;
+ }
+}
+
+/**
+ * This implements the AUTH command, i.e. authentication using CRAM-MD5 etc.
+ *
+ * @see RFC 5034
+ * @author Ben Bucksch <ben.bucksch beonex.com> <http://business.beonex.com>
+ */
+class POP3_RFC5034_handler extends POP3_RFC2449_handler {
+ kAuthSchemes = ["CRAM-MD5", "PLAIN", "LOGIN"]; // the test may adapt this as necessary
+ _usedCRAMMD5Challenge = null; // not base64-encoded
+
+ constructor(daemon) {
+ super(daemon);
+
+ this._kAuthSchemeStartFunction = {
+ "CRAM-MD5": this.authCRAMStart,
+ PLAIN: this.authPLAINStart,
+ LOGIN: this.authLOGINStart,
+ };
+ }
+
+ // called by this.CAPA()
+ capaAdditions() {
+ var capa = "";
+ if (this.kAuthSchemes.length > 0) {
+ capa += "SASL";
+ for (var i = 0; i < this.kAuthSchemes.length; i++) {
+ capa += " " + this.kAuthSchemes[i];
+ }
+ capa += "\r\n";
+ }
+ return capa;
+ }
+ AUTH(lineRest) {
+ // |lineRest| is a string containing the rest of line after "AUTH "
+ if (this._state != kStateAuthNeeded) {
+ return "-ERR invalid state";
+ }
+
+ // AUTH without arguments returns a list of supported schemes
+ if (!lineRest) {
+ var capa = "+OK I like:\r\n";
+ for (var i = 0; i < this.kAuthSchemes.length; i++) {
+ capa += this.kAuthSchemes[i] + "\r\n";
+ }
+ capa += ".\r\n";
+ return capa;
+ }
+
+ var args = lineRest.split(" ");
+ var scheme = args[0].toUpperCase();
+ // |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 (
+ "-ERR I just pretended to implement AUTH " + scheme + ", but I don't"
+ );
+ }
+ return func.call(this, "1" in args ? args[1] : undefined);
+ }
+
+ onMultiline(line) {
+ if (this._nextAuthFunction) {
+ var func = this._nextAuthFunction;
+ this._multiline = false;
+ this._nextAuthFunction = undefined;
+ if (line == "*") {
+ return "-ERR Okay, as you wish. Chicken";
+ }
+ if (!func || typeof func != "function") {
+ return "-ERR I'm lost. Internal server error during auth";
+ }
+ try {
+ return func.call(this, line);
+ } catch (e) {
+ return "-ERR " + e;
+ }
+ }
+
+ if (super.onMultiline) {
+ // Call parent.
+ return super.onMultiline.call(this, line);
+ }
+ return undefined;
+ }
+
+ 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 = kStateTransaction;
+ return "+OK Hello friend! Friends give friends good advice: Next time, use CRAM-MD5";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR 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 = kStateTransaction;
+ return "+OK Hello friend!";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR 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) {
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR Wrong username or password, crook!";
+ }
+ authLOGINPassword(line) {
+ var req = AuthLOGIN.decodeLine(line);
+ if (req == this.kPassword) {
+ this._state = kStateTransaction;
+ return "+OK Hello friend! Where did you pull out this old auth scheme?";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "-ERR Wrong username or password, crook!";
+ }
+}
diff --git a/comm/mailnews/test/fakeserver/Smtpd.jsm b/comm/mailnews/test/fakeserver/Smtpd.jsm
new file mode 100644
index 0000000000..646b626b86
--- /dev/null
+++ b/comm/mailnews/test/fakeserver/Smtpd.jsm
@@ -0,0 +1,274 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+// This file implements test SMTP servers
+
+var EXPORTED_SYMBOLS = ["SmtpDaemon", "SMTP_RFC2821_handler"];
+
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+class SmtpDaemon {
+ _messages = {};
+}
+
+// SMTP TEST SERVERS
+// -----------------
+
+var kStateAuthNeeded = 0;
+var kStateAuthOptional = 2;
+var kStateAuthenticated = 3;
+
+/**
+ * This handler implements the bare minimum required by RFC 2821.
+ *
+ * @see RFC 2821
+ * If dropOnAuthFailure is set, the server will drop the connection
+ * on authentication errors, to simulate servers that do the same.
+ */
+class SMTP_RFC2821_handler {
+ kAuthRequired = false;
+ kUsername = "testsmtp";
+ kPassword = "smtptest";
+ kAuthSchemes = ["CRAM-MD5", "PLAIN", "LOGIN"];
+ kCapabilities = ["8BITMIME", "SIZE", "CLIENTID"];
+ _nextAuthFunction = undefined;
+
+ constructor(daemon) {
+ this._daemon = daemon;
+ this.closing = false;
+ this.dropOnAuthFailure = false;
+
+ this._kAuthSchemeStartFunction = {
+ "CRAM-MD5": this.authCRAMStart,
+ PLAIN: this.authPLAINStart,
+ LOGIN: this.authLOGINStart,
+ };
+
+ this.resetTest();
+ }
+
+ resetTest() {
+ this._state = this.kAuthRequired ? kStateAuthNeeded : kStateAuthOptional;
+ this._nextAuthFunction = undefined;
+ this._multiline = false;
+ this.expectingData = false;
+ this._daemon.post = "";
+ }
+ EHLO(args) {
+ var capa = "250-fakeserver greets you";
+ if (this.kCapabilities.length > 0) {
+ capa += "\n250-" + this.kCapabilities.join("\n250-");
+ }
+ if (this.kAuthSchemes.length > 0) {
+ capa += "\n250-AUTH " + this.kAuthSchemes.join(" ");
+ }
+ capa += "\n250 HELP"; // the odd one: no "-", per RFC 2821
+ return capa;
+ }
+ CLIENTID(args) {
+ return "250 ok";
+ }
+ AUTH(lineRest) {
+ if (this._state == kStateAuthenticated) {
+ return "503 You're already authenticated";
+ }
+ var args = lineRest.split(" ");
+ var scheme = args[0].toUpperCase();
+ // |scheme| contained in |kAuthSchemes|?
+ if (
+ !this.kAuthSchemes.some(function (s) {
+ return s == scheme;
+ })
+ ) {
+ return "504 AUTH " + scheme + " not supported";
+ }
+ var func = this._kAuthSchemeStartFunction[scheme];
+ if (!func || typeof func != "function") {
+ return (
+ "504 I just pretended to implement AUTH " + scheme + ", but I don't"
+ );
+ }
+ dump("Starting AUTH " + scheme + "\n");
+ return func.call(this, args.length > 1 ? args[1] : undefined);
+ }
+ MAIL(args) {
+ if (this._state == kStateAuthNeeded) {
+ return "530 5.7.0 Authentication required";
+ }
+ return "250 ok";
+ }
+ RCPT(args) {
+ if (this._state == kStateAuthNeeded) {
+ return "530 5.7.0 Authentication required";
+ }
+ return "250 ok";
+ }
+ DATA(args) {
+ if (this._state == kStateAuthNeeded) {
+ return "530 5.7.0 Authentication required";
+ }
+ this.expectingData = true;
+ this._daemon.post = "";
+ return "354 ok\n";
+ }
+ RSET(args) {
+ return "250 ok\n";
+ }
+ VRFY(args) {
+ if (this._state == kStateAuthNeeded) {
+ return "530 5.7.0 Authentication required";
+ }
+ return "250 ok\n";
+ }
+ EXPN(args) {
+ return "250 ok\n";
+ }
+ HELP(args) {
+ return "211 ok\n";
+ }
+ NOOP(args) {
+ return "250 ok\n";
+ }
+ QUIT(args) {
+ this.closing = true;
+ return "221 done";
+ }
+ onStartup() {
+ this.closing = false;
+ return "220 ok";
+ }
+
+ /**
+ * AUTH implementations
+ *
+ * @see RFC 4954
+ */
+ authPLAINStart(lineRest) {
+ if (lineRest) {
+ // all in one command, called initial client response, see RFC 4954
+ return this.authPLAINCred(lineRest);
+ }
+
+ this._nextAuthFunction = this.authPLAINCred;
+ this._multiline = true;
+
+ return "334 ";
+ }
+ authPLAINCred(line) {
+ var req = AuthPLAIN.decodeLine(line);
+ if (req.username == this.kUsername && req.password == this.kPassword) {
+ this._state = kStateAuthenticated;
+ return "235 2.7.0 Hello friend! Friends give friends good advice: Next time, use CRAM-MD5";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "535 5.7.8 Wrong username or password, crook!";
+ }
+
+ authCRAMStart(lineRest) {
+ this._nextAuthFunction = this.authCRAMDigest;
+ this._multiline = true;
+
+ this._usedCRAMMD5Challenge = AuthCRAM.createChallenge("localhost");
+ return "334 " + 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 = kStateAuthenticated;
+ return "235 2.7.0 Hello friend!";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "535 5.7.8 Wrong username or password, crook!";
+ }
+
+ authLOGINStart(lineRest) {
+ this._nextAuthFunction = this.authLOGINUsername;
+ this._multiline = true;
+
+ return "334 " + 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 "334 " + btoa("Password:");
+ }
+ authLOGINBadUsername(line) {
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "535 5.7.8 Wrong username or password, crook!";
+ }
+ authLOGINPassword(line) {
+ var req = AuthLOGIN.decodeLine(line);
+ if (req == this.kPassword) {
+ this._state = kStateAuthenticated;
+ return "235 2.7.0 Hello friend! Where did you pull out this old auth scheme?";
+ }
+ if (this.dropOnAuthFailure) {
+ this.closing = true;
+ }
+ return "535 5.7.8 Wrong username or password, crook!";
+ }
+
+ onError(command, args) {
+ return "500 Command " + command + " not recognized\n";
+ }
+ onServerFault(e) {
+ return "451 Internal server error: " + e;
+ }
+ onMultiline(line) {
+ if (this._nextAuthFunction) {
+ var func = this._nextAuthFunction;
+ this._multiline = false;
+ this._nextAuthFunction = undefined;
+ if (line == "*") {
+ // abort, per RFC 4954 and others
+ return "501 Okay, as you wish. Chicken";
+ }
+ if (!func || typeof func != "function") {
+ return "451 I'm lost. Internal server error during auth";
+ }
+ try {
+ return func.call(this, line);
+ } catch (e) {
+ return "451 " + e;
+ }
+ }
+ if (line == ".") {
+ if (this.expectingData) {
+ this.expectingData = false;
+ return "250 Wonderful article, your style is gorgeous!";
+ }
+ return "503 Huch? How did you get here?";
+ }
+
+ if (this.expectingData) {
+ if (line.startsWith(".")) {
+ line = line.substring(1);
+ }
+ // This uses CR LF to match with the specification
+ this._daemon.post += line + "\r\n";
+ }
+ return undefined;
+ }
+ postCommand(reader) {
+ if (this.closing) {
+ reader.closeSocket();
+ }
+ reader.setMultiline(this._multiline || this.expectingData);
+ }
+}