summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/resources
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/resources
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.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/resources/.eslintrc.js5
-rw-r--r--comm/mailnews/test/resources/IMAPpump.jsm143
-rw-r--r--comm/mailnews/test/resources/LocalAccountUtils.jsm195
-rw-r--r--comm/mailnews/test/resources/MailTestUtils.jsm611
-rw-r--r--comm/mailnews/test/resources/MessageGenerator.jsm1651
-rw-r--r--comm/mailnews/test/resources/MessageInjection.jsm987
-rw-r--r--comm/mailnews/test/resources/NetworkTestUtils.jsm294
-rw-r--r--comm/mailnews/test/resources/POP3pump.js269
-rw-r--r--comm/mailnews/test/resources/PromiseTestUtils.jsm316
-rw-r--r--comm/mailnews/test/resources/abSetup.js80
-rw-r--r--comm/mailnews/test/resources/alertTestUtils.js487
-rw-r--r--comm/mailnews/test/resources/filterTestUtils.js89
-rw-r--r--comm/mailnews/test/resources/folderEventLogHelper.js0
-rw-r--r--comm/mailnews/test/resources/logHelper.js567
-rw-r--r--comm/mailnews/test/resources/mailShutdown.js51
-rw-r--r--comm/mailnews/test/resources/msgFolderListenerSetup.js429
-rw-r--r--comm/mailnews/test/resources/passwordStorage.js23
-rw-r--r--comm/mailnews/test/resources/searchTestUtils.js134
-rw-r--r--comm/mailnews/test/resources/smimeUtils.jsm71
19 files changed, 6402 insertions, 0 deletions
diff --git a/comm/mailnews/test/resources/.eslintrc.js b/comm/mailnews/test/resources/.eslintrc.js
new file mode 100644
index 0000000000..9fad83a7b1
--- /dev/null
+++ b/comm/mailnews/test/resources/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/xpcshell-test", "plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/test/resources/IMAPpump.jsm b/comm/mailnews/test/resources/IMAPpump.jsm
new file mode 100644
index 0000000000..3f4b55266d
--- /dev/null
+++ b/comm/mailnews/test/resources/IMAPpump.jsm
@@ -0,0 +1,143 @@
+/* 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 provides a simple interface to the imap fake server. Demonstration
+ * of its use can be found in test_imapPump.js
+ *
+ * The code that forms the core of this file, in its original incarnation,
+ * was test_imapFolderCopy.js There have been several iterations since
+ * then.
+ */
+
+var EXPORTED_SYMBOLS = ["IMAPPump", "setupIMAPPump", "teardownIMAPPump"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+var { gThreadManager, nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var Imapd = ChromeUtils.import("resource://testing-common/mailnews/Imapd.jsm");
+var { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+
+// define globals
+var IMAPPump = {
+ daemon: null, // the imap fake server daemon
+ server: null, // the imap fake server
+ incomingServer: null, // nsIMsgIncomingServer for the imap server
+ inbox: null, // nsIMsgFolder/nsIMsgImapMailFolder for imap inbox
+ mailbox: null, // imap fake server mailbox
+};
+
+function setupIMAPPump(extensions) {
+ // Create Application info if we need it.
+ updateAppInfo();
+
+ // These are copied from imap's head_server.js to here so we can run
+ // this from any directory.
+ function makeServer(daemon, infoString) {
+ if (infoString in Imapd.configurations) {
+ return makeServer(daemon, Imapd.configurations[infoString].join(","));
+ }
+
+ function createHandler(d) {
+ var handler = new Imapd.IMAP_RFC3501_handler(d);
+ if (!infoString) {
+ infoString = "RFC2195";
+ }
+
+ var parts = infoString.split(/ *, */);
+ for (var part of parts) {
+ Imapd.mixinExtension(handler, Imapd["IMAP_" + part + "_extension"]);
+ }
+ return handler;
+ }
+ var server = new nsMailServer(createHandler, daemon);
+ server.start();
+ return server;
+ }
+
+ function createLocalIMAPServer() {
+ let server = localAccountUtils.create_incoming_server(
+ "imap",
+ IMAPPump.server.port,
+ "user",
+ "password"
+ );
+ server.QueryInterface(Ci.nsIImapIncomingServer);
+ return server;
+ }
+
+ // end copy from head_server.js
+
+ IMAPPump.daemon = new Imapd.ImapDaemon();
+ IMAPPump.server = makeServer(IMAPPump.daemon, extensions);
+
+ IMAPPump.incomingServer = createLocalIMAPServer();
+
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+
+ // We need an identity so that updateFolder doesn't fail
+ let localAccount = MailServices.accounts.createAccount();
+ let identity = MailServices.accounts.createIdentity();
+ localAccount.addIdentity(identity);
+ localAccount.defaultIdentity = identity;
+ localAccount.incomingServer = localAccountUtils.incomingServer;
+
+ // Let's also have another account, using the same identity
+ let imapAccount = MailServices.accounts.createAccount();
+ imapAccount.addIdentity(identity);
+ imapAccount.defaultIdentity = identity;
+ imapAccount.incomingServer = IMAPPump.incomingServer;
+ MailServices.accounts.defaultAccount = imapAccount;
+
+ // The server doesn't support more than one connection
+ Services.prefs.setIntPref("mail.server.default.max_cached_connections", 1);
+ // We aren't interested in downloading messages automatically
+ Services.prefs.setBoolPref("mail.server.default.download_on_biff", false);
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ Services.prefs.setBoolPref("mail.biff.alert.show_preview", false);
+
+ IMAPPump.incomingServer.performExpand(null);
+
+ IMAPPump.inbox = IMAPPump.incomingServer.rootFolder.getChildNamed("INBOX");
+ IMAPPump.mailbox = IMAPPump.daemon.getMailbox("INBOX");
+ IMAPPump.inbox instanceof Ci.nsIMsgImapMailFolder;
+}
+
+// This will clear not only the imap accounts but also local accounts.
+function teardownIMAPPump() {
+ // try to finish any pending operations
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ IMAPPump.inbox = null;
+ try {
+ let serverSink = IMAPPump.incomingServer.QueryInterface(
+ Ci.nsIImapServerSink
+ );
+ serverSink.abortQueuedUrls();
+ IMAPPump.incomingServer.closeCachedConnections();
+ IMAPPump.server.resetTest();
+ IMAPPump.server.stop();
+ MailServices.accounts.removeIncomingServer(IMAPPump.incomingServer, false);
+ IMAPPump.incomingServer = null;
+ localAccountUtils.clearAll();
+ } catch (ex) {
+ dump(ex);
+ }
+}
diff --git a/comm/mailnews/test/resources/LocalAccountUtils.jsm b/comm/mailnews/test/resources/LocalAccountUtils.jsm
new file mode 100644
index 0000000000..11af0d1577
--- /dev/null
+++ b/comm/mailnews/test/resources/LocalAccountUtils.jsm
@@ -0,0 +1,195 @@
+/* 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 = ["localAccountUtils"];
+
+// MailServices
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// Local Mail Folders. Requires prior setup of profile directory
+
+var localAccountUtils = {
+ inboxFolder: undefined,
+ incomingServer: undefined,
+ rootFolder: undefined,
+ msgAccount: undefined,
+
+ _localAccountInitialized: false,
+ _mailboxStoreContractID: undefined,
+
+ pluggableStores: [
+ "@mozilla.org/msgstore/berkeleystore;1",
+ "@mozilla.org/msgstore/maildirstore;1",
+ ],
+
+ clearAll() {
+ this._localAccountInitialized = false;
+ if (this.msgAccount) {
+ MailServices.accounts.removeAccount(this.msgAccount);
+ }
+ this.incomingServer = undefined;
+ this.msgAccount = undefined;
+ this.inboxFolder = undefined;
+ this.rootFolder = undefined;
+ },
+
+ loadLocalMailAccount(storeID) {
+ if (
+ (storeID && storeID == this._mailboxStoreContractID) ||
+ (!storeID && this._localAccountInitialized)
+ ) {
+ return;
+ }
+
+ this.clearAll();
+ if (storeID) {
+ Services.prefs.setCharPref("mail.serverDefaultStoreContractID", storeID);
+ }
+
+ this._mailboxStoreContractID = storeID;
+ MailServices.accounts.createLocalMailAccount();
+
+ this.incomingServer = MailServices.accounts.localFoldersServer;
+ this.msgAccount = MailServices.accounts.FindAccountForServer(
+ this.incomingServer
+ );
+
+ this.rootFolder = this.incomingServer.rootMsgFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+
+ // Note: Inbox is not created automatically when there is no deferred server,
+ // so we need to create it.
+ this.inboxFolder = this.rootFolder
+ .createLocalSubfolder("Inbox")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ // a local inbox should have a Mail flag!
+ this.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
+
+ // Force an initialization of the Inbox folder database.
+ this.inboxFolder.prettyName;
+
+ this._localAccountInitialized = true;
+ },
+
+ /**
+ * Create an nsIMsgIncomingServer and an nsIMsgAccount to go with it.
+ *
+ * @param {string} aType - The type of the server (pop3, imap etc).
+ * @param {integer} aPort - The port the server is on.
+ * @param {string} aUsername - The username for the server.
+ * @param {string} aPassword - The password for the server.
+ * @param {string} aHostname - The hostname for the server (defaults to localhost).
+ * @returns {nsIMsgIncomingServer} The newly-created nsIMsgIncomingServer.
+ */
+ create_incoming_server(
+ aType,
+ aPort,
+ aUsername,
+ aPassword,
+ aHostname = "localhost"
+ ) {
+ let serverAndAccount = localAccountUtils.create_incoming_server_and_account(
+ aType,
+ aPort,
+ aUsername,
+ aPassword,
+ aHostname
+ );
+ return serverAndAccount.server;
+ },
+
+ /**
+ * Create an nsIMsgIncomingServer and an nsIMsgAccount to go with it.
+ * There are no identities created for the account.
+ *
+ * @param {string} aType - The type of the server (pop3, imap etc).
+ * @param {integer} aPort - The port the server is on.
+ * @param {string} aUsername - The username for the server.
+ * @param {string} aPassword - The password for the server.
+ * @param {string} aHostname - The hostname for the server (defaults to localhost).
+ * @returns {object} object
+ * @returns {nsIMsgIncomingServer} object.server Created server
+ * @returns {nsIMsgAccount} object.account Created account
+ */
+ create_incoming_server_and_account(
+ aType,
+ aPort,
+ aUsername,
+ aPassword,
+ aHostname = "localhost"
+ ) {
+ let server = MailServices.accounts.createIncomingServer(
+ aUsername,
+ aHostname,
+ aType
+ );
+ server.port = aPort;
+ if (aUsername != null) {
+ server.username = aUsername;
+ }
+ if (aPassword != null) {
+ server.password = aPassword;
+ }
+
+ server.valid = false;
+
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = server;
+ if (aType == "pop3") {
+ // Several tests expect that mail is deferred to the local folders account,
+ // so do that.
+ this.loadLocalMailAccount();
+ server.QueryInterface(Ci.nsIPop3IncomingServer);
+ server.deferredToAccount = this.msgAccount.key;
+ }
+ server.valid = true;
+
+ return { server, account };
+ },
+
+ /**
+ * Create an outgoing nsISmtpServer with the given parameters.
+ *
+ * @param {integer} aPort - The port the server is on.
+ * @param {string} aUsername - The username for the server
+ * @param {string} aPassword - The password for the server
+ * @param {string} [aHostname=localhost] - The hostname for the server.
+ * @returns {nsISmtpServer} The newly-created nsISmtpServer.
+ */
+ create_outgoing_server(aPort, aUsername, aPassword, aHostname = "localhost") {
+ let server = MailServices.smtp.createServer();
+ server.hostname = aHostname;
+ server.port = aPort;
+ server.authMethod = Ci.nsMsgAuthMethod.none;
+ return server;
+ },
+
+ /**
+ * Associate the given outgoing server with the given account.
+ * It does so by creating a new identity in the account using the given outgoing
+ * server.
+ *
+ * @param {nsIMsgAccount} aIncoming - The account to associate.
+ * @param {nsISmtpServer} aOutgoingServer - The outgoing server to associate.
+ * @param {bool} aSetAsDefault - Whether to set the outgoing server as the
+ * default for the account.
+ */
+ associate_servers(aIncoming, aOutgoingServer, aSetAsDefault = false) {
+ if (!(aIncoming instanceof Ci.nsIMsgAccount)) {
+ throw new Error("aIncoming isn't an account");
+ }
+
+ let identity = MailServices.accounts.createIdentity();
+ identity.smtpServerKey = aOutgoingServer.key;
+
+ aIncoming.addIdentity(identity);
+
+ if (aSetAsDefault) {
+ aIncoming.defaultIdentity = identity;
+ }
+ },
+};
diff --git a/comm/mailnews/test/resources/MailTestUtils.jsm b/comm/mailnews/test/resources/MailTestUtils.jsm
new file mode 100644
index 0000000000..588e21521a
--- /dev/null
+++ b/comm/mailnews/test/resources/MailTestUtils.jsm
@@ -0,0 +1,611 @@
+/* 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 = ["mailTestUtils"];
+
+var { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// See Bug 903946
+function avoidUncaughtExceptionInExternalProtocolService() {
+ try {
+ Services.prefs.setCharPref(
+ "helpers.private_mime_types_file",
+ Services.prefs.getCharPref("helpers.global_mime_types_file")
+ );
+ } catch (ex) {}
+ try {
+ Services.prefs.setCharPref(
+ "helpers.private_mailcap_file",
+ Services.prefs.getCharPref("helpers.global_mailcap_file")
+ );
+ } catch (ex) {}
+}
+avoidUncaughtExceptionInExternalProtocolService();
+
+var mailTestUtils = {
+ // Loads a file to a string
+ // If aCharset is specified, treats the file as being of that charset
+ loadFileToString(aFile, aCharset) {
+ var data = "";
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fstream.init(aFile, -1, 0, 0);
+
+ if (aCharset) {
+ var cstream = Cc[
+ "@mozilla.org/intl/converter-input-stream;1"
+ ].createInstance(Ci.nsIConverterInputStream);
+ cstream.init(fstream, aCharset, 4096, 0x0000);
+ let str = {};
+ while (cstream.readString(4096, str) != 0) {
+ data += str.value;
+ }
+
+ cstream.close();
+ } else {
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+
+ sstream.init(fstream);
+
+ let str = sstream.read(4096);
+ while (str.length > 0) {
+ data += str;
+ str = sstream.read(4096);
+ }
+
+ sstream.close();
+ }
+
+ fstream.close();
+
+ return data;
+ },
+
+ // Loads a message to a string
+ // If aCharset is specified, treats the file as being of that charset
+ loadMessageToString(aFolder, aMsgHdr, aCharset) {
+ var data = "";
+ let reusable = {};
+ let bytesLeft = aMsgHdr.messageSize;
+ let stream = aFolder.getMsgInputStream(aMsgHdr, reusable);
+ if (aCharset) {
+ let cstream = Cc[
+ "@mozilla.org/intl/converter-input-stream;1"
+ ].createInstance(Ci.nsIConverterInputStream);
+ cstream.init(stream, aCharset, 4096, 0x0000);
+ let str = {};
+ let bytesToRead = Math.min(bytesLeft, 4096);
+ while (cstream.readString(bytesToRead, str) != 0) {
+ data += str.value;
+ bytesLeft -= bytesToRead;
+ if (bytesLeft <= 0) {
+ break;
+ }
+ bytesToRead = Math.min(bytesLeft, 4096);
+ }
+ cstream.close();
+ } else {
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+
+ sstream.init(stream);
+
+ let bytesToRead = Math.min(bytesLeft, 4096);
+ var str = sstream.read(bytesToRead);
+ bytesLeft -= str.length;
+ while (str.length > 0) {
+ data += str;
+ if (bytesLeft <= 0) {
+ break;
+ }
+ bytesToRead = Math.min(bytesLeft, 4096);
+ str = sstream.read(bytesToRead);
+ bytesLeft -= str.length;
+ }
+ sstream.close();
+ }
+ stream.close();
+
+ return data;
+ },
+
+ // Loads a message to a UTF-16 string.
+ loadMessageToUTF16String(folder, msgHdr, charset) {
+ let str = this.loadMessageToString(folder, msgHdr, charset);
+ let arr = new Uint8Array(Array.from(str, x => x.charCodeAt(0)));
+ return new TextDecoder().decode(arr);
+ },
+
+ // Gets the first message header in a folder.
+ firstMsgHdr(folder) {
+ let enumerator = folder.msgDatabase.enumerateMessages();
+ let first = enumerator[Symbol.iterator]().next();
+ return first.done ? null : first.value;
+ },
+
+ // Gets message header number N (0 based index) in a folder.
+ getMsgHdrN(folder, n) {
+ let i = 0;
+ for (let next of folder.msgDatabase.enumerateMessages()) {
+ if (i == n) {
+ return next;
+ }
+ i++;
+ }
+ return null;
+ },
+
+ /**
+ * Returns the file system a particular file is on.
+ * Currently supported on Windows only.
+ *
+ * @param {nsIFile} aFile - The file to get the file system for.
+ * @returns {string} The file system a particular file is on, or 'null'
+ * if not on Windows.
+ */
+ get_file_system(aFile) {
+ if (!("@mozilla.org/windows-registry-key;1" in Cc)) {
+ dump("get_file_system() is supported on Windows only.\n");
+ return null;
+ }
+
+ // Win32 type and other constants.
+ const BOOL = ctypes.int32_t;
+ const MAX_PATH = 260;
+
+ let kernel32 = ctypes.open("kernel32.dll");
+
+ try {
+ // Returns the path of the volume a file is on.
+ let GetVolumePathName = kernel32.declare(
+ "GetVolumePathNameW",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ ctypes.char16_t.ptr, // in: lpszFileName
+ ctypes.char16_t.ptr, // out: lpszVolumePathName
+ ctypes.uint32_t // in: cchBufferLength
+ );
+
+ let filePath = aFile.path;
+ // The volume path should be at most 1 greater than than the length of the
+ // path -- add 1 for a trailing backslash if necessary, and 1 for the
+ // terminating null character. Note that the parentheses around the type are
+ // necessary for new to apply correctly.
+ let volumePath = new (ctypes.char16_t.array(filePath.length + 2))();
+
+ if (!GetVolumePathName(filePath, volumePath, volumePath.length)) {
+ throw new Error(
+ "Unable to get volume path for " +
+ filePath +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ // Returns information about the file system for the given volume path. We just need
+ // the file system name.
+ let GetVolumeInformation = kernel32.declare(
+ "GetVolumeInformationW",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ ctypes.char16_t.ptr, // in, optional: lpRootPathName
+ ctypes.char16_t.ptr, // out: lpVolumeNameBuffer
+ ctypes.uint32_t, // in: nVolumeNameSize
+ ctypes.uint32_t.ptr, // out, optional: lpVolumeSerialNumber
+ ctypes.uint32_t.ptr, // out, optional: lpMaximumComponentLength
+ ctypes.uint32_t.ptr, // out, optional: lpFileSystemFlags
+ ctypes.char16_t.ptr, // out: lpFileSystemNameBuffer
+ ctypes.uint32_t // in: nFileSystemNameSize
+ );
+
+ // We're only interested in the name of the file system.
+ let fsName = new (ctypes.char16_t.array(MAX_PATH + 1))();
+
+ if (
+ !GetVolumeInformation(
+ volumePath,
+ null,
+ 0,
+ null,
+ null,
+ null,
+ fsName,
+ fsName.length
+ )
+ ) {
+ throw new Error(
+ "Unable to get volume information for " +
+ volumePath.readString() +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ return fsName.readString();
+ } finally {
+ kernel32.close();
+ }
+ },
+
+ /**
+ * Try marking a region of a file as sparse, so that zeros don't consume
+ * significant amounts of disk space. This is a platform-dependent routine and
+ * is not supported on all platforms. The current status of this function is:
+ * - Windows: Supported, but only on NTFS volumes.
+ * - Mac: Not supported.
+ * - Linux: As long as you seek to a position before writing, happens automatically
+ * on most file systems, so this function is a no-op.
+ *
+ * @param {nsIFile} aFile - The file to mark as sparse.
+ * @param {integer} aRegionStart - The start position of the sparse region,
+ * in bytes.
+ * @param {integer} aRegionBytes - The number of bytes to mark as sparse.
+ * @returns {boolean} Whether the OS and file system supports marking files as
+ * sparse. If this is true, then the file has been marked as sparse.
+ * If this isfalse, then the underlying system doesn't support marking files as
+ * sparse. If an exception is thrown, then the system does support marking
+ * files as sparse, but an error occurred while doing so.
+ *
+ */
+ mark_file_region_sparse(aFile, aRegionStart, aRegionBytes) {
+ let fileSystem = this.get_file_system(aFile);
+ dump(
+ "[mark_file_region_sparse()] File system = " +
+ (fileSystem || "(unknown)") +
+ ", file region = at " +
+ this.toMiBString(aRegionStart) +
+ " for " +
+ this.toMiBString(aRegionBytes) +
+ "\n"
+ );
+
+ if ("@mozilla.org/windows-registry-key;1" in Cc) {
+ // On Windows, check whether the drive is NTFS. If it is, proceed.
+ // If it isn't, then bail out now, because in all probability it is
+ // FAT32, which doesn't support sparse files.
+ if (fileSystem != "NTFS") {
+ return false;
+ }
+
+ // Win32 type and other constants.
+ const BOOL = ctypes.int32_t;
+ const HANDLE = ctypes.voidptr_t;
+ // A BOOLEAN (= BYTE = unsigned char) is distinct from a BOOL.
+ // http://blogs.msdn.com/b/oldnewthing/archive/2004/12/22/329884.aspx
+ const BOOLEAN = ctypes.unsigned_char;
+ const FILE_SET_SPARSE_BUFFER = new ctypes.StructType(
+ "FILE_SET_SPARSE_BUFFER",
+ [{ SetSparse: BOOLEAN }]
+ );
+ // LARGE_INTEGER is actually a type union. We'll use the int64 representation
+ const LARGE_INTEGER = ctypes.int64_t;
+ const FILE_ZERO_DATA_INFORMATION = new ctypes.StructType(
+ "FILE_ZERO_DATA_INFORMATION",
+ [{ FileOffset: LARGE_INTEGER }, { BeyondFinalZero: LARGE_INTEGER }]
+ );
+
+ const GENERIC_WRITE = 0x40000000;
+ const OPEN_ALWAYS = 4;
+ const FILE_ATTRIBUTE_NORMAL = 0x80;
+ const INVALID_HANDLE_VALUE = new ctypes.Int64(-1);
+ const FSCTL_SET_SPARSE = 0x900c4;
+ const FSCTL_SET_ZERO_DATA = 0x980c8;
+ const FILE_BEGIN = 0;
+
+ let kernel32 = ctypes.open("kernel32.dll");
+
+ try {
+ let CreateFile = kernel32.declare(
+ "CreateFileW",
+ ctypes.winapi_abi,
+ HANDLE, // return type: handle to the file
+ ctypes.char16_t.ptr, // in: lpFileName
+ ctypes.uint32_t, // in: dwDesiredAccess
+ ctypes.uint32_t, // in: dwShareMode
+ ctypes.voidptr_t, // in, optional: lpSecurityAttributes (note that
+ // we're cheating here by not declaring a
+ // SECURITY_ATTRIBUTES structure -- that's because
+ // we're going to pass in null anyway)
+ ctypes.uint32_t, // in: dwCreationDisposition
+ ctypes.uint32_t, // in: dwFlagsAndAttributes
+ HANDLE // in, optional: hTemplateFile
+ );
+
+ let filePath = aFile.path;
+ let hFile = CreateFile(
+ filePath,
+ GENERIC_WRITE,
+ 0,
+ null,
+ OPEN_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL,
+ null
+ );
+ let hFileInt = ctypes.cast(hFile, ctypes.intptr_t);
+ if (ctypes.Int64.compare(hFileInt.value, INVALID_HANDLE_VALUE) == 0) {
+ throw new Error(
+ "CreateFile failed for " +
+ filePath +
+ ", error " +
+ ctypes.winLastError
+ );
+ }
+
+ try {
+ let DeviceIoControl = kernel32.declare(
+ "DeviceIoControl",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE, // in: hDevice
+ ctypes.uint32_t, // in: dwIoControlCode
+ ctypes.voidptr_t, // in, optional: lpInBuffer
+ ctypes.uint32_t, // in: nInBufferSize
+ ctypes.voidptr_t, // out, optional: lpOutBuffer
+ ctypes.uint32_t, // in: nOutBufferSize
+ ctypes.uint32_t.ptr, // out, optional: lpBytesReturned
+ ctypes.voidptr_t // inout, optional: lpOverlapped (again, we're
+ // cheating here by not having this as an
+ // OVERLAPPED structure
+ );
+ // bytesReturned needs to be passed in, even though it's meaningless
+ let bytesReturned = new ctypes.uint32_t();
+ let sparseBuffer = new FILE_SET_SPARSE_BUFFER();
+ sparseBuffer.SetSparse = 1;
+
+ // Mark the file as sparse
+ if (
+ !DeviceIoControl(
+ hFile,
+ FSCTL_SET_SPARSE,
+ sparseBuffer.address(),
+ FILE_SET_SPARSE_BUFFER.size,
+ null,
+ 0,
+ bytesReturned.address(),
+ null
+ )
+ ) {
+ throw new Error(
+ "Unable to mark file as sparse, error " + ctypes.winLastError
+ );
+ }
+
+ let zdInfo = new FILE_ZERO_DATA_INFORMATION();
+ zdInfo.FileOffset = aRegionStart;
+ let regionEnd = aRegionStart + aRegionBytes;
+ zdInfo.BeyondFinalZero = regionEnd;
+ // Mark the region as a sparse region
+ if (
+ !DeviceIoControl(
+ hFile,
+ FSCTL_SET_ZERO_DATA,
+ zdInfo.address(),
+ FILE_ZERO_DATA_INFORMATION.size,
+ null,
+ 0,
+ bytesReturned.address(),
+ null
+ )
+ ) {
+ throw new Error(
+ "Unable to mark region as zero, error " + ctypes.winLastError
+ );
+ }
+
+ // Move to past the sparse region and mark it as the end of the file. The
+ // above DeviceIoControl call is useless unless followed by this.
+ let SetFilePointerEx = kernel32.declare(
+ "SetFilePointerEx",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE, // in: hFile
+ LARGE_INTEGER, // in: liDistanceToMove
+ LARGE_INTEGER.ptr, // out, optional: lpNewFilePointer
+ ctypes.uint32_t // in: dwMoveMethod
+ );
+ if (!SetFilePointerEx(hFile, regionEnd, null, FILE_BEGIN)) {
+ throw new Error(
+ "Unable to set file pointer to end, error " + ctypes.winLastError
+ );
+ }
+
+ let SetEndOfFile = kernel32.declare(
+ "SetEndOfFile",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE // in: hFile
+ );
+ if (!SetEndOfFile(hFile)) {
+ throw new Error(
+ "Unable to set end of file, error " + ctypes.winLastError
+ );
+ }
+
+ return true;
+ } finally {
+ let CloseHandle = kernel32.declare(
+ "CloseHandle",
+ ctypes.winapi_abi,
+ BOOL, // return type: 1 indicates success, 0 failure
+ HANDLE // in: hObject
+ );
+ CloseHandle(hFile);
+ }
+ } finally {
+ kernel32.close();
+ }
+ } else if ("nsILocalFileMac" in Ci) {
+ // Macs don't support marking files as sparse.
+ return false;
+ } else {
+ // Assuming Unix here. Unix file systems generally automatically sparsify
+ // files.
+ return true;
+ }
+ },
+
+ /**
+ * Converts a size in bytes into its mebibytes string representation.
+ * NB: 1 MiB = 1024 * 1024 = 1048576 B.
+ *
+ * @param {integer} aSize - The size in bytes.
+ * @returns {string} A string representing the size in mebibytes.
+ */
+ toMiBString(aSize) {
+ return aSize / 1048576 + " MiB";
+ },
+
+ /**
+ * A variant of do_timeout that accepts an actual function instead of
+ * requiring you to pass a string to evaluate. If the function throws an
+ * exception when invoked, we will use do_throw to ensure that the test fails.
+ *
+ * @param {integer} aDelayInMS - The number of milliseconds to wait before firing the timer.
+ * @param {Function} aFunc - The function to invoke when the timer fires.
+ * @param {object} [aFuncThis] - Optional 'this' pointer to use.
+ * @param {*[]} aFuncArgs - Optional list of arguments to pass to the function.
+ */
+ _timer: null,
+ do_timeout_function(aDelayInMS, aFunc, aFuncThis, aFuncArgs) {
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ let wrappedFunc = function () {
+ try {
+ aFunc.apply(aFuncThis, aFuncArgs);
+ } catch (ex) {
+ // we want to make sure that if the thing we call throws an exception,
+ // that this terminates the test.
+ do_throw(ex);
+ }
+ };
+ this._timer.initWithCallback(
+ wrappedFunc,
+ aDelayInMS,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ },
+
+ /**
+ * Ensure the given nsIMsgFolder's database is up-to-date, calling the provided
+ * callback once the folder has been loaded. (This may be instantly or
+ * after a re-parse.)
+ *
+ * @param {nsIMsgFolder} aFolder - The nsIMsgFolder whose database you want
+ * to ensure is up-to-date.
+ * @param {Function} aCallback - The callback function to invoke once the
+ * folder has been loaded.
+ * @param {object} aCallbackThis - The 'this' to use when calling the callback.
+ * Pass null if your callback does not rely on 'this'.
+ * @param {*[]} aCallbackArgs - A list of arguments to pass to the callback
+ * via apply. If you provide [1,2,3], we will effectively call:
+ * aCallbackThis.aCallback(1,2,3);
+ * @param {boolean} [aSomeoneElseWillTriggerTheUpdate=false] If this is true,
+ * we do not trigger the updateFolder call and it is assumed someone else is
+ * taking care of that.
+ */
+ updateFolderAndNotify(
+ aFolder,
+ aCallback,
+ aCallbackThis,
+ aCallbackArgs,
+ aSomeoneElseWillTriggerTheUpdate = false
+ ) {
+ // register for the folder loaded notification ahead of time... even though
+ // we may not need it...
+ let folderListener = {
+ onFolderEvent(aEventFolder, aEvent) {
+ if (aEvent == "FolderLoaded" && aFolder.URI == aEventFolder.URI) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ aCallback.apply(aCallbackThis, aCallbackArgs);
+ }
+ },
+ };
+
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.event
+ );
+
+ if (!aSomeoneElseWillTriggerTheUpdate) {
+ aFolder.updateFolder(null);
+ }
+ },
+
+ /**
+ * For when you want to compare elements non-strictly.
+ */
+ non_strict_index_of(aArray, aElem) {
+ for (let [i, elem] of aArray.entries()) {
+ if (elem == aElem) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Click on a particular cell in a tree. `window` is not defined here in this
+ * file, so we can't provide it as a default argument. Similarly, we pass in
+ * `EventUtils` as an argument because importing it here does not work
+ * because `window` is not defined.
+ *
+ * @param {object} EventUtils - The EventUtils object.
+ * @param {Window} win - The window the tree is in.
+ * @param {Element} tree - The tree element.
+ * @param {number} row - The tree row to click on.
+ * @param {number} column - The tree column to click on.
+ * @param {object} event - The mouse event to synthesize, e.g. `{ clickCount: 2 }`.
+ */
+ treeClick(EventUtils, win, tree, row, column, event) {
+ let coords = tree.getCoordsForCellItem(row, tree.columns[column], "cell");
+ let treeChildren = tree.lastElementChild;
+ EventUtils.synthesizeMouse(
+ treeChildren,
+ coords.x + coords.width / 2,
+ coords.y + coords.height / 2,
+ event,
+ win
+ );
+ },
+
+ /**
+ * For waiting until an element exists in a given document. Pass in the
+ * `MutationObserver` as an argument because importing it here does not work
+ * because `window` is not defined here.
+ *
+ * @param {object} MutationObserver - The MutationObserver object.
+ * @param {Document} doc - Document that contains the elements.
+ * @param {string} observedNodeId - Id of the element to observe.
+ * @param {string} awaitedNodeId - Id of the element that will soon exist.
+ * @returns {Promise.<undefined>} - A promise fulfilled when the element exists.
+ */
+ awaitElementExistence(MutationObserver, doc, observedNodeId, awaitedNodeId) {
+ return new Promise(resolve => {
+ let outerObserver = new MutationObserver((mutationsList, observer) => {
+ for (let mutation of mutationsList) {
+ if (mutation.type == "childList" && mutation.addedNodes.length) {
+ let element = doc.getElementById(awaitedNodeId);
+
+ if (element) {
+ observer.disconnect();
+ resolve();
+ return;
+ }
+ }
+ }
+ });
+
+ let nodeToObserve = doc.getElementById(observedNodeId);
+ outerObserver.observe(nodeToObserve, { childList: true });
+ });
+ },
+};
diff --git a/comm/mailnews/test/resources/MessageGenerator.jsm b/comm/mailnews/test/resources/MessageGenerator.jsm
new file mode 100644
index 0000000000..4e3f8b75f1
--- /dev/null
+++ b/comm/mailnews/test/resources/MessageGenerator.jsm
@@ -0,0 +1,1651 @@
+/* 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 = [
+ "MessageGenerator",
+ "addMessagesToFolder",
+ "MessageScenarioFactory",
+ "SyntheticPartLeaf",
+ "SyntheticDegeneratePartEmpty",
+ "SyntheticPartMulti",
+ "SyntheticPartMultiMixed",
+ "SyntheticPartMultiParallel",
+ "SyntheticPartMultiDigest",
+ "SyntheticPartMultiAlternative",
+ "SyntheticPartMultiRelated",
+ "SyntheticPartMultiSignedSMIME",
+ "SyntheticPartMultiSignedPGP",
+ "SyntheticMessage",
+ "SyntheticMessageSet",
+];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * A list of first names for use by MessageGenerator to create deterministic,
+ * reversible names. To keep things easily reversible, if you add names, make
+ * sure they have no spaces in them!
+ */
+var FIRST_NAMES = [
+ "Andy",
+ "Bob",
+ "Chris",
+ "David",
+ "Emily",
+ "Felix",
+ "Gillian",
+ "Helen",
+ "Idina",
+ "Johnny",
+ "Kate",
+ "Lilia",
+ "Martin",
+ "Neil",
+ "Olof",
+ "Pete",
+ "Quinn",
+ "Rasmus",
+ "Sarah",
+ "Troels",
+ "Ulf",
+ "Vince",
+ "Will",
+ "Xavier",
+ "Yoko",
+ "Zig",
+];
+
+/**
+ * A list of last names for use by MessageGenerator to create deterministic,
+ * reversible names. To keep things easily reversible, if you add names, make
+ * sure they have no spaces in them!
+ */
+var LAST_NAMES = [
+ "Anway",
+ "Bell",
+ "Clarke",
+ "Davol",
+ "Ekberg",
+ "Flowers",
+ "Gilbert",
+ "Hook",
+ "Ivarsson",
+ "Jones",
+ "Kurtz",
+ "Lowe",
+ "Morris",
+ "Nagel",
+ "Orzabal",
+ "Price",
+ "Quinn",
+ "Rolinski",
+ "Stanley",
+ "Tennant",
+ "Ulvaeus",
+ "Vannucci",
+ "Wiggs",
+ "Xavier",
+ "Young",
+ "Zig",
+];
+
+/**
+ * A list of adjectives used to construct a deterministic, reversible subject
+ * by MessageGenerator. To keep things easily reversible, if you add more,
+ * make sure they have no spaces in them! Also, make sure your additions
+ * don't break the secret Monty Python reference!
+ */
+var SUBJECT_ADJECTIVES = [
+ "Big",
+ "Small",
+ "Huge",
+ "Tiny",
+ "Red",
+ "Green",
+ "Blue",
+ "My",
+ "Happy",
+ "Sad",
+ "Grumpy",
+ "Angry",
+ "Awesome",
+ "Fun",
+ "Lame",
+ "Funky",
+];
+
+/**
+ * A list of nouns used to construct a deterministic, reversible subject
+ * by MessageGenerator. To keep things easily reversible, if you add more,
+ * make sure they have no spaces in them! Also, make sure your additions
+ * don't break the secret Monty Python reference!
+ */
+var SUBJECT_NOUNS = [
+ "Meeting",
+ "Party",
+ "Shindig",
+ "Wedding",
+ "Document",
+ "Report",
+ "Spreadsheet",
+ "Hovercraft",
+ "Aardvark",
+ "Giraffe",
+ "Llama",
+ "Velociraptor",
+ "Laser",
+ "Ray-Gun",
+ "Pen",
+ "Sword",
+];
+
+/**
+ * A list of suffixes used to construct a deterministic, reversible subject
+ * by MessageGenerator. These can (clearly) have spaces in them. Make sure
+ * your additions don't break the secret Monty Python reference!
+ */
+var SUBJECT_SUFFIXES = [
+ "Today",
+ "Tomorrow",
+ "Yesterday",
+ "In a Fortnight",
+ "Needs Attention",
+ "Very Important",
+ "Highest Priority",
+ "Full Of Eels",
+ "In The Lobby",
+ "On Your Desk",
+ "In Your Car",
+ "Hiding Behind The Door",
+];
+
+/**
+ * Base class for MIME Part representation.
+ */
+function SyntheticPart(aProperties) {
+ if (aProperties) {
+ if ("contentType" in aProperties) {
+ this._contentType = aProperties.contentType;
+ }
+ if ("charset" in aProperties) {
+ this._charset = aProperties.charset;
+ }
+ if ("format" in aProperties) {
+ this._format = aProperties.format;
+ }
+ if ("filename" in aProperties) {
+ this._filename = aProperties.filename;
+ }
+ if ("boundary" in aProperties) {
+ this._boundary = aProperties.boundary;
+ }
+ if ("encoding" in aProperties) {
+ this._encoding = aProperties.encoding;
+ }
+ if ("contentId" in aProperties) {
+ this._contentId = aProperties.contentId;
+ }
+ if ("disposition" in aProperties) {
+ this._forceDisposition = aProperties.disposition;
+ }
+ if ("extraHeaders" in aProperties) {
+ this._extraHeaders = aProperties.extraHeaders;
+ }
+ }
+}
+SyntheticPart.prototype = {
+ _forceDisposition: null,
+ _encoding: null,
+
+ get contentTypeHeaderValue() {
+ let s = this._contentType;
+ if (this._charset) {
+ s += "; charset=" + this._charset;
+ }
+ if (this._format) {
+ s += "; format=" + this._format;
+ }
+ if (this._filename) {
+ s += ';\r\n name="' + this._filename + '"';
+ }
+ if (this._contentTypeExtra) {
+ for (let [key, value] of Object.entries(this._contentTypeExtra)) {
+ s += ";\r\n " + key + '="' + value + '"';
+ }
+ }
+ if (this._boundary) {
+ s += ';\r\n boundary="' + this._boundary + '"';
+ }
+ return s;
+ },
+ get hasTransferEncoding() {
+ return this._encoding;
+ },
+ get contentTransferEncodingHeaderValue() {
+ return this._encoding;
+ },
+ get hasDisposition() {
+ return this._forceDisposition || this._filename || false;
+ },
+ get contentDispositionHeaderValue() {
+ let s = "";
+ if (this._forceDisposition) {
+ s += this._forceDisposition;
+ } else if (this._filename) {
+ s += 'attachment;\r\n filename="' + this._filename + '"';
+ }
+ return s;
+ },
+ get hasContentId() {
+ return this._contentId || false;
+ },
+ get contentIdHeaderValue() {
+ return "<" + this._contentId + ">";
+ },
+ get hasExtraHeaders() {
+ return this._extraHeaders || false;
+ },
+ get extraHeaders() {
+ return this._extraHeaders || false;
+ },
+};
+
+/**
+ * Leaf MIME part, defaulting to text/plain.
+ */
+function SyntheticPartLeaf(aBody, aProperties) {
+ SyntheticPart.call(this, aProperties);
+ this.body = aBody;
+}
+SyntheticPartLeaf.prototype = {
+ __proto__: SyntheticPart.prototype,
+ _contentType: "text/plain",
+ _charset: "ISO-8859-1",
+ _format: "flowed",
+ _encoding: "7bit",
+ toMessageString() {
+ return this.body;
+ },
+ prettyString(aIndent) {
+ return "Leaf: " + this._contentType;
+ },
+};
+
+/**
+ * A part that tells us to produce NO output in a multipart section. So if our
+ * separator is "--BOB", we might produce "--BOB\n--BOB--\n" instead of having
+ * some headers and actual content in there.
+ * This is not a good idea and probably not legal either, but it happens and
+ * we need to test for it.
+ */
+function SyntheticDegeneratePartEmpty() {}
+SyntheticDegeneratePartEmpty.prototype = {
+ prettyString(aIndent) {
+ return "Degenerate Empty Part";
+ },
+};
+
+/**
+ * Multipart (multipart/*) MIME part base class.
+ */
+function SyntheticPartMulti(aParts, aProperties) {
+ SyntheticPart.call(this, aProperties);
+
+ this._boundary = "--------------CHOPCHOP" + this.BOUNDARY_COUNTER;
+ this.BOUNDARY_COUNTER_HOME.BOUNDARY_COUNTER += 1;
+ this.parts = aParts != null ? aParts : [];
+}
+SyntheticPartMulti.prototype = {
+ __proto__: SyntheticPart.prototype,
+ BOUNDARY_COUNTER: 0,
+ toMessageString() {
+ let s = "This is a multi-part message in MIME format.\r\n";
+ for (let part of this.parts) {
+ s += "--" + this._boundary + "\r\n";
+ if (part instanceof SyntheticDegeneratePartEmpty) {
+ continue;
+ }
+ s += "Content-Type: " + part.contentTypeHeaderValue + "\r\n";
+ if (part.hasTransferEncoding) {
+ s +=
+ "Content-Transfer-Encoding: " +
+ part.contentTransferEncodingHeaderValue +
+ "\r\n";
+ }
+ if (part.hasDisposition) {
+ s +=
+ "Content-Disposition: " + part.contentDispositionHeaderValue + "\r\n";
+ }
+ if (part.hasContentId) {
+ s += "Content-ID: " + part.contentIdHeaderValue + "\r\n";
+ }
+ if (part.hasExtraHeaders) {
+ for (let k in part.extraHeaders) {
+ let v = part.extraHeaders[k];
+ s += k + ": " + v + "\r\n";
+ }
+ }
+ s += "\r\n";
+ s += part.toMessageString() + "\r\n";
+ }
+ s += "--" + this._boundary + "--";
+ return s;
+ },
+ prettyString(aIndent) {
+ let nextIndent = aIndent != null ? aIndent + " " : "";
+
+ let s = "Container: " + this._contentType;
+
+ for (let iPart = 0; iPart < this.parts.length; iPart++) {
+ let part = this.parts[iPart];
+ s +=
+ "\n" + nextIndent + (iPart + 1) + " " + part.prettyString(nextIndent);
+ }
+
+ return s;
+ },
+};
+SyntheticPartMulti.prototype.BOUNDARY_COUNTER_HOME =
+ SyntheticPartMulti.prototype;
+
+/**
+ * Multipart mixed (multipart/mixed) MIME part.
+ */
+function SyntheticPartMultiMixed(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiMixed.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/mixed",
+};
+
+/**
+ * Multipart mixed (multipart/mixed) MIME part.
+ */
+function SyntheticPartMultiParallel(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiParallel.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/parallel",
+};
+
+/**
+ * Multipart digest (multipart/digest) MIME part.
+ */
+function SyntheticPartMultiDigest(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiDigest.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/digest",
+};
+
+/**
+ * Multipart alternative (multipart/alternative) MIME part.
+ */
+function SyntheticPartMultiAlternative(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiAlternative.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/alternative",
+};
+
+/**
+ * Multipart related (multipart/related) MIME part.
+ */
+function SyntheticPartMultiRelated(...aArgs) {
+ SyntheticPartMulti.apply(this, aArgs);
+}
+SyntheticPartMultiRelated.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/related",
+};
+
+var PKCS_SIGNATURE_MIME_TYPE = "application/x-pkcs7-signature";
+/**
+ * Multipart signed (multipart/signed) SMIME part. This is helperish and makes
+ * up a gibberish signature. We wrap the provided parts in the standard
+ * signature idiom
+ *
+ * @param {string} aPart - The content part to wrap. Only one part!
+ * Use a multipart if you need to cram extra stuff in there.
+ * @param {object} aProperties - Properties, propagated to SyntheticPart, see that.
+ */
+function SyntheticPartMultiSignedSMIME(aPart, aProperties) {
+ SyntheticPartMulti.call(this, [aPart], aProperties);
+ this.parts.push(
+ new SyntheticPartLeaf(
+ "I am not really a signature but let's hope no one figures it out.",
+ {
+ contentType: PKCS_SIGNATURE_MIME_TYPE,
+ name: "smime.p7s",
+ }
+ )
+ );
+}
+SyntheticPartMultiSignedSMIME.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/signed",
+ _contentTypeExtra: {
+ protocol: PKCS_SIGNATURE_MIME_TYPE,
+ micalg: "SHA1",
+ },
+};
+
+var PGP_SIGNATURE_MIME_TYPE = "application/pgp-signature";
+/**
+ * Multipart signed (multipart/signed) PGP part. This is helperish and makes
+ * up a gibberish signature. We wrap the provided parts in the standard
+ * signature idiom
+ *
+ * @param {string} aPart - The content part to wrap. Only one part!
+ * Use a multipart if you need to cram extra stuff in there.
+ * @param {object} aProperties - Properties, propagated to SyntheticPart, see that.
+ */
+function SyntheticPartMultiSignedPGP(aPart, aProperties) {
+ SyntheticPartMulti.call(this, [aPart], aProperties);
+ this.parts.push(
+ new SyntheticPartLeaf(
+ "I am not really a signature but let's hope no one figures it out.",
+ {
+ contentType: PGP_SIGNATURE_MIME_TYPE,
+ }
+ )
+ );
+}
+SyntheticPartMultiSignedPGP.prototype = {
+ __proto__: SyntheticPartMulti.prototype,
+ _contentType: "multipart/signed",
+ _contentTypeExtra: {
+ protocol: PGP_SIGNATURE_MIME_TYPE,
+ micalg: "pgp-sha1",
+ },
+};
+
+var _DEFAULT_META_STATES = {
+ junk: false,
+ read: false,
+};
+
+/**
+ * A synthetic message, created by the MessageGenerator. Captures both the
+ * ingredients that went into the synthetic message as well as the rfc822 form
+ * of the message.
+ *
+ * @param {object} [aHeaders] A dictionary of rfc822 header payloads.
+ * The key should be capitalized as you want it to appear in the output.
+ * This requires adherence to convention of this class. You are best to just
+ * use the helpers provided by this class.
+ * @param {object} [aBodyPart] - An instance of one of the many Synthetic part
+ * types available in this file.
+ * @param {object} [aMetaState] - A dictionary of meta-state about the message
+ * that is only relevant to the MessageInjection logic and perhaps some
+ * testing logic.
+ * @param {boolean} [aMetaState.junk=false] Is the method junk?
+ */
+function SyntheticMessage(aHeaders, aBodyPart, aMetaState) {
+ // we currently do not need to call SyntheticPart's constructor...
+ this.headers = aHeaders || {};
+ this.bodyPart = aBodyPart || new SyntheticPartLeaf("");
+ this.metaState = aMetaState || {};
+ for (let key in _DEFAULT_META_STATES) {
+ let value = _DEFAULT_META_STATES[key];
+ if (!(key in this.metaState)) {
+ this.metaState[key] = value;
+ }
+ }
+}
+
+SyntheticMessage.prototype = {
+ __proto__: SyntheticPart.prototype,
+ _contentType: "message/rfc822",
+ _charset: null,
+ _format: null,
+ _encoding: null,
+
+ /** @returns {string} The Message-Id header value. */
+ get messageId() {
+ return this._messageId;
+ },
+ /**
+ * Sets the Message-Id header value.
+ *
+ * @param {string} aMessageId - A unique string without the greater-than and
+ * less-than, we add those for you.
+ */
+ set messageId(aMessageId) {
+ this._messageId = aMessageId;
+ this.headers["Message-Id"] = "<" + aMessageId + ">";
+ },
+
+ /** @returns {Date} The message Date header value. */
+ get date() {
+ return this._date;
+ },
+ /**
+ * Sets the Date header to the given javascript Date object.
+ *
+ * @param {Date} aDate The date you want the message to claim to be from.
+ */
+ set date(aDate) {
+ this._date = aDate;
+ let dateParts = aDate.toString().split(" ");
+ this.headers.Date =
+ dateParts[0] +
+ ", " +
+ dateParts[2] +
+ " " +
+ dateParts[1] +
+ " " +
+ dateParts[3] +
+ " " +
+ dateParts[4] +
+ " " +
+ dateParts[5].substring(3);
+ },
+
+ /** @returns {string} The message subject. */
+ get subject() {
+ return this._subject;
+ },
+ /**
+ * Sets the message subject.
+ *
+ * @param {string} aSubject - A string sans newlines or other illegal characters.
+ */
+ set subject(aSubject) {
+ this._subject = aSubject;
+ this.headers.Subject = aSubject;
+ },
+
+ /**
+ * Given a tuple containing [a display name, an e-mail address], returns a
+ * string suitable for use in a to/from/cc header line.
+ *
+ * @param {string[]} aNameAndAddress - A list with two elements. The first
+ * should be the display name (sans wrapping quotes). The second element
+ * should be the e-mail address (sans wrapping greater-than/less-than).
+ */
+ _formatMailFromNameAndAddress(aNameAndAddress) {
+ // if the name is encoded, do not put it in quotes!
+ if (aNameAndAddress[0].startsWith("=")) {
+ return aNameAndAddress[0] + " <" + aNameAndAddress[1] + ">";
+ }
+ return '"' + aNameAndAddress[0] + '" <' + aNameAndAddress[1] + ">";
+ },
+
+ /**
+ * Given a mailbox, parse out name and email. The mailbox
+ * can (per rfc 2822) be of two forms:
+ * 1) Name <me@example.org>
+ * 2) me@example.org
+ *
+ * @returns {string[]} A tuple of name, email.
+ */
+ _parseMailbox(mailbox) {
+ let matcher = mailbox.match(/(.*)<(.+@.+)>/);
+ if (!matcher) {
+ // no match -> second form
+ return ["", mailbox];
+ }
+
+ let name = matcher[1].trim();
+ let email = matcher[2].trim();
+ return [name, email];
+ },
+
+ /** @returns {string[]} The name-and-address tuple used when setting the From header. */
+ get from() {
+ return this._from;
+ },
+ /**
+ * Sets the From header using the given tuple containing [a display name,
+ * an e-mail address].
+ *
+ * @param {string[]} aNameAndAddress - A list with two elements. The first
+ * should be the display name (sans wrapping quotes). The second element
+ * should be the e-mail address (sans wrapping greater-than/less-than).
+ * Can also be a string, should then be a valid raw From: header value.
+ */
+ set from(aNameAndAddress) {
+ if (typeof aNameAndAddress === "string") {
+ this._from = this._parseMailbox(aNameAndAddress);
+ this.headers.From = aNameAndAddress;
+ return;
+ }
+ this._from = aNameAndAddress;
+ this.headers.From = this._formatMailFromNameAndAddress(aNameAndAddress);
+ },
+
+ /** @returns {string} The display name part of the From header. */
+ get fromName() {
+ return this._from[0];
+ },
+ /** @returns {string} The e-mail address part of the From header (no display name). */
+ get fromAddress() {
+ return this._from[1];
+ },
+
+ /**
+ * For our header storage, we may need to pre-add commas, this does it.
+ *
+ * @param {string[]} aList - A list of strings that is mutated so that every
+ * string in the list except the last one has a comma appended to it.
+ */
+ _commaize(aList) {
+ for (let i = 0; i < aList.length - 1; i++) {
+ aList[i] = aList[i] + ",";
+ }
+ return aList;
+ },
+
+ /**
+ * @returns {string[][]} the comma-ized list of name-and-address tuples used
+ * to set the To header.
+ */
+ get to() {
+ return this._to;
+ },
+ /**
+ * Sets the To header using a list of tuples containing [a display name,
+ * an e-mail address].
+ *
+ * @param {string[][]} aNameAndAddresses - A list of name-and-address tuples.
+ * Each tuple is alist with two elements. The first should be the
+ * display name (sans wrapping quotes). The second element should be the
+ * e-mail address (sans wrapping greater-than/less-than).
+ * Can also be a string, should then be a valid raw To: header value.
+ */
+ set to(aNameAndAddresses) {
+ if (typeof aNameAndAddresses === "string") {
+ this._to = [];
+ let people = aNameAndAddresses.split(",");
+ for (let i = 0; i < people.length; i++) {
+ this._to.push(this._parseMailbox(people[i]));
+ }
+
+ this.headers.To = aNameAndAddresses;
+ return;
+ }
+ this._to = aNameAndAddresses;
+ this.headers.To = this._commaize(
+ aNameAndAddresses.map(nameAndAddr =>
+ this._formatMailFromNameAndAddress(nameAndAddr)
+ )
+ );
+ },
+ /** @returns {string} The display name of the first intended recipient. */
+ get toName() {
+ return this._to[0][0];
+ },
+ /** @returns {string} The email address (no display name) of the first recipient. */
+ get toAddress() {
+ return this._to[0][1];
+ },
+
+ /**
+ * @returns {string[][]} The comma-ized list of name-and-address tuples used
+ * to set the Cc header.
+ */
+ get cc() {
+ return this._cc;
+ },
+ /**
+ * Sets the Cc header using a list of tuples containing [a display name,
+ * an e-mail address].
+ *
+ * @param {string[][]} aNameAndAddresses - A list of name-and-address tuples.
+ * Each tuple is a list with two elements. The first should be the
+ * display name (sans wrapping quotes). The second element should be the
+ * e-mail address (sans wrapping greater-than/less-than).
+ * Can also be a string, should then be a valid raw Cc: header value.
+ */
+ set cc(aNameAndAddresses) {
+ if (typeof aNameAndAddresses === "string") {
+ this._cc = [];
+ let people = aNameAndAddresses.split(",");
+ for (let i = 0; i < people.length; i++) {
+ this._cc.push(this._parseMailbox(people[i]));
+ }
+ this.headers.Cc = aNameAndAddresses;
+ return;
+ }
+ this._cc = aNameAndAddresses;
+ this.headers.Cc = this._commaize(
+ aNameAndAddresses.map(nameAndAddr =>
+ this._formatMailFromNameAndAddress(nameAndAddr)
+ )
+ );
+ },
+
+ get bodyPart() {
+ return this._bodyPart;
+ },
+ set bodyPart(aBodyPart) {
+ this._bodyPart = aBodyPart;
+ this.headers["Content-Type"] = this._bodyPart.contentTypeHeaderValue;
+ },
+
+ /**
+ * Normalizes header values, which may be strings or arrays of strings, into
+ * a suitable string suitable for appending to the header name/key.
+ *
+ * @returns {string} A normalized string representation of the header
+ * value(s), which may include spanning multiple lines.
+ */
+ _formatHeaderValues(aHeaderValues) {
+ // may not be an array
+ if (!(aHeaderValues instanceof Array)) {
+ return aHeaderValues;
+ }
+ // it's an array!
+ if (aHeaderValues.length == 1) {
+ return aHeaderValues[0];
+ }
+ return aHeaderValues.join("\r\n\t");
+ },
+
+ /**
+ * @returns {string} A string uniquely identifying this message, at least
+ * as long as the messageId is set and unique.
+ */
+ toString() {
+ return "msg:" + this._messageId;
+ },
+
+ /**
+ * Convert the message and its hierarchy into a "pretty string". The message
+ * and each MIME part get their own line. The string never ends with a
+ * newline. For a non-multi-part message, only a single line will be
+ * returned.
+ * Messages have their subject displayed, everyone else just shows their
+ * content type.
+ */
+ prettyString(aIndent) {
+ if (aIndent === undefined) {
+ aIndent = "";
+ }
+ let nextIndent = aIndent + " ";
+
+ let s = "Message: " + this.subject;
+ s += "\n" + nextIndent + "1 " + this.bodyPart.prettyString(nextIndent);
+
+ return s;
+ },
+
+ /**
+ * @returns {string} This messages in rfc822 format, or something close enough.
+ */
+ toMessageString() {
+ let lines = Object.keys(this.headers).map(
+ headerKey =>
+ headerKey + ": " + this._formatHeaderValues(this.headers[headerKey])
+ );
+
+ return lines.join("\r\n") + "\r\n\r\n" + this.bodyPart.toMessageString();
+ },
+
+ toMboxString() {
+ return "From " + this._from[1] + "\r\n" + this.toMessageString() + "\r\n";
+ },
+
+ /**
+ * @returns {nsIStringInputStream} This message in rfc822 format in a string stream.
+ */
+ toStream() {
+ let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ let str = this.toMessageString();
+ stream.setData(str, str.length);
+ return stream;
+ },
+
+ /**
+ * Writes this message to an mbox stream. his means adding a "From " line
+ * and making sure we've got a trailing newline.
+ */
+ writeToMboxStream(aStream) {
+ let str = this.toMboxString();
+ aStream.write(str, str.length);
+ },
+};
+
+/**
+ * Write a list of messages to a folder
+ *
+ * @param {SyntheticMessage[]} aMessages - The list of SyntheticMessages instances to write.
+ * @param {nsIMsgFolder} aFolder - The folder to write to.
+ */
+function addMessagesToFolder(aMessages, aFolder) {
+ let localFolder = aFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ for (let message of aMessages) {
+ localFolder.addMessage(message.toMboxString());
+ }
+}
+
+/**
+ * Represents a set of synthetic messages, also supporting insertion into and
+ * tracking of the message folders to which they belong. This then allows
+ * mutations of the messages (in their folders) for testing purposes.
+ *
+ * In general, you would create a synthetic message set by passing in only a
+ * list of synthetic messages, and then add then messages to nsIMsgFolders by
+ * using one of the addMessage* methods. This will populate the aMsgFolders
+ * and aFolderIndices values. (They are primarily intended for reasons of
+ * slicing, but people who know what they are doing can also use them.)
+ *
+ * @param {SyntheticMessage[]} aSynMessages The synthetic messages that should belong to this set.
+ * @param {nsIMsgFolder|nsIMsgFolder[]} [aMsgFolders] Optional nsIMsgFolder or list of folders.
+ * @param {number[]} [aFolderIndices] Optional list where each value is an index into the
+ * msgFolders attribute, specifying what folder the message can be found
+ * in. The value may also be null if the message has not yet been
+ * inserted into a folder.
+ */
+function SyntheticMessageSet(aSynMessages, aMsgFolders, aFolderIndices) {
+ this.synMessages = aSynMessages;
+
+ if (Array.isArray(aMsgFolders)) {
+ this.msgFolders = aMsgFolders;
+ } else if (aMsgFolders) {
+ this.msgFolders = [aMsgFolders];
+ } else {
+ this.msgFolders = [];
+ }
+ if (aFolderIndices == null) {
+ this.folderIndices = aSynMessages.map(_ => null);
+ } else {
+ this.folderIndices = aFolderIndices;
+ }
+}
+SyntheticMessageSet.prototype = {
+ /**
+ * Helper method for messageInjection to use to tell us it is injecting a
+ * message in a given folder. As a convenience, we also return the
+ * synthetic message.
+ *
+ * @protected
+ */
+ _trackMessageAddition(aFolder, aMessageIndex) {
+ let aFolderIndex = this.msgFolders.indexOf(aFolder);
+ if (aFolderIndex == -1) {
+ aFolderIndex = this.msgFolders.push(aFolder) - 1;
+ }
+ this.folderIndices[aMessageIndex] = aFolderIndex;
+ return this.synMessages[aMessageIndex];
+ },
+ /**
+ * Helper method for use by |MessageInjection.async_move_messages| to tell us that it moved
+ * all the messages from aOldFolder to aNewFolder.
+ */
+ _folderSwap(aOldFolder, aNewFolder) {
+ let folderIndex = this.msgFolders.indexOf(aOldFolder);
+ this.msgFolders[folderIndex] = aNewFolder;
+ },
+
+ /**
+ * Union this set with another set and return the (new) result.
+ *
+ * @param {SyntheticMessageSet} aOtherSet - The other synthetic message set.
+ * @returns {SyntheticMessageSet} A new SyntheticMessageSet containing the
+ * union of this set and the other set.
+ */
+ union(aOtherSet) {
+ let messages = this.synMessages.concat(aOtherSet.synMessages);
+ let folders = this.msgFolders.concat();
+ let indices = this.folderIndices.concat();
+
+ let folderUrisToIndices = {};
+ for (let [iFolder, folder] of this.msgFolders.entries()) {
+ folderUrisToIndices[folder.URI] = iFolder;
+ }
+
+ for (let iOther = 0; iOther < aOtherSet.synMessages.length; iOther++) {
+ let folderIndex = aOtherSet.folderIndices[iOther];
+ if (folderIndex == null) {
+ indices.push(folderIndex);
+ } else {
+ let folder = aOtherSet.msgFolders[folderIndex];
+ if (!(folder.URI in folderUrisToIndices)) {
+ folderUrisToIndices[folder.URI] = folders.length;
+ folders.push(folder);
+ }
+ indices.push(folderUrisToIndices[folder.URI]);
+ }
+ }
+
+ return new SyntheticMessageSet(messages, folders, indices);
+ },
+
+ /**
+ * Get the single message header of the message at the given index; use
+ * |msgHdrs| if you want to get all the headers at once.
+ *
+ * @param {integer} aIndex
+ */
+ getMsgHdr(aIndex) {
+ let folder = this.msgFolders[this.folderIndices[aIndex]];
+ let synMsg = this.synMessages[aIndex];
+ return folder.msgDatabase.getMsgHdrForMessageID(synMsg.messageId);
+ },
+
+ /**
+ * Get the URI for the message at the given index.
+ *
+ * @param {integer} aIndex
+ */
+ getMsgURI(aIndex) {
+ let msgHdr = this.getMsgHdr(aIndex);
+ return msgHdr.folder.getUriForMsg(msgHdr);
+ },
+
+ /**
+ * @yields {nsIMsgDBHdr} A JS iterator of the message headers for all
+ * messages inserted into a folder.
+ */
+ *msgHdrs() {
+ // get the databases
+ let msgDatabases = this.msgFolders.map(folder => folder.msgDatabase);
+ for (let [iMsg, synMsg] of this.synMessages.entries()) {
+ let folderIndex = this.folderIndices[iMsg];
+ if (folderIndex != null) {
+ yield msgDatabases[folderIndex].getMsgHdrForMessageID(synMsg.messageId);
+ }
+ }
+ },
+ /**
+ * @returns {nsIMsgDBHdr} A JS list of the message headers for all
+ * messages inserted into a folder.
+ */
+ get msgHdrList() {
+ return Array.from(this.msgHdrs());
+ },
+
+ /**
+ * @returns {object[]} - A list where each item is a list with two elements;
+ * the first is an nsIMsgFolder, and the second is a list of all of the nsIMsgDBHdrs
+ * for the synthetic messages in the set inserted into that folder.
+ */
+ get foldersWithMsgHdrs() {
+ let results = this.msgFolders.map(folder => [folder, []]);
+ for (let [iMsg, synMsg] of this.synMessages.entries()) {
+ let folderIndex = this.folderIndices[iMsg];
+ if (folderIndex != null) {
+ let [folder, msgHdrs] = results[folderIndex];
+ msgHdrs.push(
+ folder.msgDatabase.getMsgHdrForMessageID(synMsg.messageId)
+ );
+ }
+ }
+ return results;
+ },
+ /**
+ * Sets the status of the messages to read/unread.
+ *
+ * @param {boolean} aRead - true/false to set messages as read/unread
+ * @param {nsIMsgDBHdr} aMsgHdr - A message header to work on. If not
+ * specified, mark all messages in the current set.
+ */
+ setRead(aRead, aMsgHdr) {
+ let msgHdrs = aMsgHdr ? [aMsgHdr] : this.msgHdrList;
+ for (let msgHdr of msgHdrs) {
+ msgHdr.markRead(aRead);
+ }
+ },
+ /**
+ * Sets the starred status of the messages.
+ *
+ * @param {boolean} aStarred - Starred status.
+ */
+ setStarred(aStarred) {
+ for (let msgHdr of this.msgHdrs()) {
+ msgHdr.markFlagged(aStarred);
+ }
+ },
+ /**
+ * Adds tag to the messages.
+ *
+ * @param {string} aTagName - Tag to add
+ */
+ addTag(aTagName) {
+ for (let [folder, msgHdrs] of this.foldersWithMsgHdrs) {
+ folder.addKeywordsToMessages(msgHdrs, aTagName);
+ }
+ },
+ /**
+ * Removes tag from the messages.
+ *
+ * @param {string} aTagName - Tag to remove
+ */
+ removeTag(aTagName) {
+ for (let [folder, msgHdrs] of this.foldersWithMsgHdrs) {
+ folder.removeKeywordsFromMessages(msgHdrs, aTagName);
+ }
+ },
+ /**
+ * Sets the junk score for the messages to junk/non-junk. It does not
+ * involve the bayesian classifier because we really don't want it
+ * affecting our unit tests! (Unless we were testing the bayesian
+ * classifier. Which I'm conveniently not. Feel free to add a
+ * "setJunkForRealsies" method if you are.)
+ *
+ * @param {boolean} aIsJunk - true/false to set messages to junk/non-junk
+ * @param {nsIMsgDBHdr} aMsgHdr - A message header to work on. If not
+ * specified, mark all messages in the current set.
+ * Generates a msgsJunkStatusChanged nsIMsgFolderListener notification.
+ */
+ setJunk(aIsJunk, aMsgHdr) {
+ let junkscore = aIsJunk ? "100" : "0";
+ let msgHdrs = aMsgHdr ? [aMsgHdr] : this.msgHdrList;
+ for (let msgHdr of msgHdrs) {
+ msgHdr.setStringProperty("junkscore", junkscore);
+ }
+ MailServices.mfn.notifyMsgsJunkStatusChanged(msgHdrs);
+ },
+
+ /**
+ * Slice the message set using the exact Array.prototype.slice semantics
+ * (because we call Array.prototype.slice).
+ */
+ slice(...aArgs) {
+ let slicedMessages = this.synMessages.slice(...aArgs);
+ let slicedIndices = this.folderIndices.slice(...aArgs);
+ let sliced = new SyntheticMessageSet(
+ slicedMessages,
+ this.msgFolders,
+ slicedIndices
+ );
+ if ("glodaMessages" in this && this.glodaMessages) {
+ sliced.glodaMessages = this.glodaMessages.slice(...aArgs);
+ }
+ return sliced;
+ },
+};
+
+/**
+ * Provides mechanisms for creating vaguely interesting, but at least valid,
+ * SyntheticMessage instances.
+ */
+function MessageGenerator() {
+ this._clock = new Date(2000, 1, 1);
+ this._nextNameNumber = 0;
+ this._nextSubjectNumber = 0;
+ this._nextMessageIdNum = 0;
+}
+
+MessageGenerator.prototype = {
+ /**
+ * The maximum number of unique names makeName can produce.
+ */
+ MAX_VALID_NAMES: FIRST_NAMES.length * LAST_NAMES.length,
+ /**
+ * The maximum number of unique e-mail address makeMailAddress can produce.
+ */
+ MAX_VALID_MAIL_ADDRESSES: FIRST_NAMES.length * LAST_NAMES.length,
+ /**
+ * The maximum number of unique subjects makeSubject can produce.
+ */
+ MAX_VALID_SUBJECTS:
+ SUBJECT_ADJECTIVES.length * SUBJECT_NOUNS.length * SUBJECT_SUFFIXES,
+
+ /**
+ * Generate a consistently determined (and reversible) name from a unique
+ * value. Currently up to 26*26 unique names can be generated, which
+ * should be sufficient for testing purposes, but if your code cares, check
+ * against MAX_VALID_NAMES.
+ *
+ * @param {integer} aNameNumber The 'number' of the name you want which must be less
+ * than MAX_VALID_NAMES.
+ * @returns {string} The unique name corresponding to the name number.
+ */
+ makeName(aNameNumber) {
+ let iFirst = aNameNumber % FIRST_NAMES.length;
+ let iLast =
+ (iFirst + Math.floor(aNameNumber / FIRST_NAMES.length)) %
+ LAST_NAMES.length;
+
+ return FIRST_NAMES[iFirst] + " " + LAST_NAMES[iLast];
+ },
+
+ /**
+ * Generate a consistently determined (and reversible) e-mail address from
+ * a unique value; intended to work in parallel with makeName. Currently
+ * up to 26*26 unique addresses can be generated, but if your code cares,
+ * check against MAX_VALID_MAIL_ADDRESSES.
+ *
+ * @param {integer} aNameNumber - The 'number' of the mail address you want
+ * which must be ess than MAX_VALID_MAIL_ADDRESSES.
+ * @returns {string} The unique name corresponding to the name mail address.
+ */
+ makeMailAddress(aNameNumber) {
+ let iFirst = aNameNumber % FIRST_NAMES.length;
+ let iLast =
+ (iFirst + Math.floor(aNameNumber / FIRST_NAMES.length)) %
+ LAST_NAMES.length;
+
+ return (
+ FIRST_NAMES[iFirst].toLowerCase() +
+ "@" +
+ LAST_NAMES[iLast].toLowerCase() +
+ ".invalid"
+ );
+ },
+
+ /**
+ * Generate a pair of name and e-mail address.
+ *
+ * @param {integer} aNameNumber - The optional 'number' of the name and mail
+ * address you want. If you do not provide a value, we will increment an
+ * internal counter to ensure that a new name is allocated and that will not
+ * be re-used. If you use our automatic number once, you must use it
+ * always, unless you don't mind or can ensure no collisions occur between
+ * our number allocation and your uses. If provided, the number must be
+ * less than MAX_VALID_NAMES.
+ * @returns {string[]} A list containing two elements.
+ * The first is a name produced by a call to makeName, and the second an
+ * e-mail address produced by a call to makeMailAddress.
+ * This representation is used by the SyntheticMessage class when dealing
+ * with names and addresses.
+ */
+ makeNameAndAddress(aNameNumber) {
+ if (aNameNumber === undefined) {
+ aNameNumber = this._nextNameNumber++;
+ }
+ return [this.makeName(aNameNumber), this.makeMailAddress(aNameNumber)];
+ },
+
+ /**
+ * Generate and return multiple pairs of names and e-mail addresses. The
+ * names are allocated using the automatic mechanism as documented on
+ * makeNameAndAddress. You should accordingly not allocate / hard code name
+ * numbers on your own.
+ *
+ * @param {integer} aCount - The number of people you want name and address tuples for.
+ * @returns {string[][]} A list of aCount name-and-address tuples.
+ */
+ makeNamesAndAddresses(aCount) {
+ let namesAndAddresses = [];
+ for (let i = 0; i < aCount; i++) {
+ namesAndAddresses.push(this.makeNameAndAddress());
+ }
+ return namesAndAddresses;
+ },
+
+ /**
+ * Generate a consistently determined (and reversible) subject from a unique
+ * value. Up to MAX_VALID_SUBJECTS can be produced.
+ *
+ * @param {integer} aSubjectNumber - The subject number you want generated,
+ * must be less than MAX_VALID_SUBJECTS.
+ * @returns {string} The subject corresponding to the given subject number.
+ */
+ makeSubject(aSubjectNumber) {
+ if (aSubjectNumber === undefined) {
+ aSubjectNumber = this._nextSubjectNumber++;
+ }
+ let iAdjective = aSubjectNumber % SUBJECT_ADJECTIVES.length;
+ let iNoun =
+ (iAdjective + Math.floor(aSubjectNumber / SUBJECT_ADJECTIVES.length)) %
+ SUBJECT_NOUNS.length;
+ let iSuffix =
+ (iNoun +
+ Math.floor(
+ aSubjectNumber / (SUBJECT_ADJECTIVES.length * SUBJECT_NOUNS.length)
+ )) %
+ SUBJECT_SUFFIXES.length;
+ return (
+ SUBJECT_ADJECTIVES[iAdjective] +
+ " " +
+ SUBJECT_NOUNS[iNoun] +
+ " " +
+ SUBJECT_SUFFIXES[iSuffix]
+ );
+ },
+
+ /**
+ * Fabricate a message-id suitable for the given synthetic message. Although
+ * we don't use the message yet, in theory it would let us tailor the
+ * message id to the server that theoretically might be sending it. Or some
+ * such.
+ *
+ * @param {SyntheticMessage} aSynthMessage - The synthetic message you would
+ * like us to make up a message-id for. We don't set the message-id on the
+ * message, that's up to you.
+ * @returns {string} A Message-Id suitable for the given message.
+ */
+ makeMessageId(aSynthMessage) {
+ let msgId = this._nextMessageIdNum + "@made.up.invalid";
+ this._nextMessageIdNum++;
+ return msgId;
+ },
+
+ /**
+ * Generates a valid date which is after all previously issued dates by this
+ * method, ensuring an apparent ordering of time consistent with the order
+ * in which code is executed / messages are generated.
+ * If you need a precise time ordering or precise times, make them up
+ * yourself.
+ *
+ * @returns {Date} - A made-up time in JavaScript Date object form.
+ */
+ makeDate() {
+ let date = this._clock;
+ // advance time by an hour
+ this._clock = new Date(date.valueOf() + 60 * 60 * 1000);
+ return date;
+ },
+
+ /**
+ * Description for makeMessage options parameter.
+ *
+ * @typedef MakeMessageOptions
+ * @property {number} [age] A dictionary with potential attributes 'minutes',
+ * 'hours', 'days', 'weeks' to specify the message be created that far in
+ * the past.
+ * @property {object} [attachments] A list of dictionaries suitable for passing to
+ * syntheticPartLeaf, plus a 'body' attribute that has already been
+ * encoded. Line chopping is on you FOR NOW.
+ * @property {SyntheticPartLeaf} [body] A dictionary suitable for passing to SyntheticPart plus
+ * a 'body' attribute that has already been encoded (if encoding is
+ * required). Line chopping is on you FOR NOW. Alternately, use
+ * bodyPart.
+ * @property {SyntheticPartLeaf} [bodyPart] A SyntheticPart to uses as the body. If you
+ * provide an attachments value, this part will be wrapped in a
+ * multipart/mixed to also hold your attachments. (You can put
+ * attachments in the bodyPart directly if you want and not use
+ * attachments.)
+ * @property {string} [callerData] A value to propagate to the callerData attribute
+ * on the resulting message.
+ * @property {string[][]} [cc] A list of cc recipients (name and address pairs). If
+ * omitted, no cc is generated.
+ * @property {string[][]} [from] The name and value pair this message should be from.
+ * Defaults to the first recipient if this is a reply, otherwise a new
+ * person is synthesized via |makeNameAndAddress|.
+ * @property {string} [inReplyTo] the SyntheticMessage this message should be in
+ * reply-to. If that message was in reply to another message, we will
+ * appropriately compensate for that. If a SyntheticMessageSet is
+ * provided we will use the first message in the set.
+ * @property {boolean} [replyAll] a boolean indicating whether this should be a
+ * reply-to-all or just to the author of the message. (er, to-only, not
+ * cc.)
+ * @property {string} [subject] subject to use; you are responsible for doing any
+ * encoding before passing it in.
+ * @property {string[][]} [to] The list of recipients for this message, defaults to a
+ * set of toCount newly created persons.
+ * @property {number} [toCount=1] the number of people who the message should be to.
+ * @property {object} [clobberHeaders] An object whose contents will overwrite the
+ * contents of the headers object. This should only be used to construct
+ * illegal header values; general usage should use another explicit
+ * mechanism.
+ * @property {boolean} [junk] Should this message be flagged as junk for the benefit
+ * of the MessageInjection helper so that it can know to flag the message
+ * as junk? We have no concept of marking a message as definitely not
+ * junk at this point.
+ * @property {boolean} [read] Should this message be marked as already read?
+ */
+ /**
+ * Create a SyntheticMessage. All arguments are optional, but allow
+ * additional control. With no arguments specified, a new name/address will
+ * be generated that has not been used before, and sent to a new name/address
+ * that has not been used before.
+ *
+ * @param {MakeMessageOptions} aArgs
+ * @returns {SyntheticMessage} a SyntheticMessage fashioned just to your liking.
+ */
+ makeMessage(aArgs) {
+ aArgs = aArgs || {};
+ let msg = new SyntheticMessage();
+
+ if (aArgs.inReplyTo) {
+ // If inReplyTo is a SyntheticMessageSet, just use the first message in
+ // the set because the caller may be using them.
+ let srcMsg = aArgs.inReplyTo.synMessages
+ ? aArgs.inReplyTo.synMessages[0]
+ : aArgs.inReplyTo;
+
+ msg.parent = srcMsg;
+ msg.parent.children.push(msg);
+
+ msg.subject = srcMsg.subject.startsWith("Re: ")
+ ? srcMsg.subject
+ : "Re: " + srcMsg.subject;
+ if (aArgs.replyAll) {
+ msg.to = [srcMsg.from].concat(srcMsg.to.slice(1));
+ } else {
+ msg.to = [srcMsg.from];
+ }
+ msg.from = srcMsg.to[0];
+
+ // we want the <>'s.
+ msg.headers["In-Reply-To"] = srcMsg.headers["Message-Id"];
+ msg.headers.References = (srcMsg.headers.References || []).concat([
+ srcMsg.headers["Message-Id"],
+ ]);
+ } else {
+ msg.parent = null;
+
+ msg.subject = aArgs.subject || this.makeSubject();
+ msg.from = aArgs.from || this.makeNameAndAddress();
+ msg.to = aArgs.to || this.makeNamesAndAddresses(aArgs.toCount || 1);
+ if (aArgs.cc) {
+ msg.cc = aArgs.cc;
+ }
+ }
+
+ msg.children = [];
+ msg.messageId = this.makeMessageId(msg);
+ if (aArgs.age) {
+ let age = aArgs.age;
+ // start from 'now'
+ let ts = new Date().valueOf();
+ if (age.minutes) {
+ ts -= age.minutes * 60 * 1000;
+ }
+ if (age.hours) {
+ ts -= age.hours * 60 * 60 * 1000;
+ }
+ if (age.days) {
+ ts -= age.days * 24 * 60 * 60 * 1000;
+ }
+ if (age.weeks) {
+ ts -= age.weeks * 7 * 24 * 60 * 60 * 1000;
+ }
+ msg.date = new Date(ts);
+ } else {
+ msg.date = this.makeDate();
+ }
+
+ if ("clobberHeaders" in aArgs) {
+ for (let key in aArgs.clobberHeaders) {
+ let value = aArgs.clobberHeaders[key];
+ if (value === null) {
+ delete msg.headers[key];
+ } else {
+ msg.headers[key] = value;
+ }
+ // clobber helper...
+ if (key == "From") {
+ msg._from = ["", ""];
+ }
+ if (key == "To") {
+ msg._to = [["", ""]];
+ }
+ if (key == "Cc") {
+ msg._cc = [["", ""]];
+ }
+ }
+ }
+
+ if ("junk" in aArgs && aArgs.junk) {
+ msg.metaState.junk = true;
+ }
+ if ("read" in aArgs && aArgs.read) {
+ msg.metaState.read = true;
+ }
+
+ let bodyPart;
+ if (aArgs.bodyPart) {
+ bodyPart = aArgs.bodyPart;
+ } else if (aArgs.body) {
+ bodyPart = new SyntheticPartLeaf(aArgs.body.body, aArgs.body);
+ } else {
+ // Different messages should have a chance at different bodies.
+ bodyPart = new SyntheticPartLeaf("Hello " + msg.toName + "!");
+ }
+
+ // if it has any attachments, create a multipart/mixed to be the body and
+ // have it be the parent of the existing body and all the attachments
+ if (aArgs.attachments) {
+ let parts = [bodyPart];
+ for (let attachDesc of aArgs.attachments) {
+ parts.push(new SyntheticPartLeaf(attachDesc.body, attachDesc));
+ }
+ bodyPart = new SyntheticPartMultiMixed(parts);
+ }
+
+ msg.bodyPart = bodyPart;
+
+ msg.callerData = aArgs.callerData;
+
+ return msg;
+ },
+
+ /**
+ * Create an encrypted SMime message. It's just a wrapper around makeMessage,
+ * that sets the right content-type. Use like makeMessage.
+ *
+ * @param {MakeMessageOptions} aOptions
+ * @returns {SyntheticMessage}
+ */
+ makeEncryptedSMimeMessage(aOptions) {
+ if (!aOptions) {
+ aOptions = {};
+ }
+ aOptions.clobberHeaders = {
+ "Content-Transfer-Encoding": "base64",
+ "Content-Disposition": 'attachment; filename="smime.p7m"',
+ };
+ if (!aOptions.body) {
+ aOptions.body = {};
+ }
+ aOptions.body.contentType = 'application/pkcs7-mime; name="smime.p7m"';
+ let msg = this.makeMessage(aOptions);
+ return msg;
+ },
+
+ /**
+ * Create an encrypted OpenPGP message. It's just a wrapper around makeMessage,
+ * that sets the right content-type. Use like makeMessage.
+ *
+ * @param {MakeMessageOptions} aOptions
+ * @returns {SyntheticMessage}
+ */
+ makeEncryptedOpenPGPMessage(aOptions) {
+ if (!aOptions) {
+ aOptions = {};
+ }
+ aOptions.clobberHeaders = {
+ "Content-Transfer-Encoding": "base64",
+ };
+ if (!aOptions.body) {
+ aOptions.body = {};
+ }
+ aOptions.body.contentType =
+ 'multipart/encrypted; protocol="application/pgp-encrypted"';
+ let msg = this.makeMessage(aOptions);
+ return msg;
+ },
+
+ MAKE_MESSAGES_DEFAULTS: {
+ count: 10,
+ },
+ MAKE_MESSAGES_PROPAGATE: [
+ "attachments",
+ "body",
+ "cc",
+ "from",
+ "inReplyTo",
+ "subject",
+ "to",
+ "clobberHeaders",
+ "junk",
+ "read",
+ ],
+ /**
+ * Given a set definition, produce a list of synthetic messages.
+ *
+ * The set definition supports the following attributes:
+ * count: The number of messages to create.
+ * age: As used by makeMessage.
+ * age_incr: Similar to age, but used to increment the values in the age
+ * dictionary (assuming a value of zero if omitted).
+ *
+ * @param {object} aSetDef - Message properties, see MAKE_MESSAGES_PROPAGATE.
+ * @param {integer} [aSetDef.msgsPerThread=1] The number of messages per thread.
+ * If you want to create direct-reply threads, you can pass a value for this
+ * and have it not be one. If you need fancier reply situations,
+ * directly use a scenario or hook us up to support that.
+ *
+ * Also supported are the following attributes as defined by makeMessage:
+ * attachments, body, from, inReplyTo, subject, to, clobberHeaders, junk
+ *
+ * If omitted, the following defaults are used, but don't depend on this as we
+ * can change these at any time:
+ * - count: 10
+ */
+ makeMessages(aSetDef) {
+ let messages = [];
+
+ let args = {};
+ // zero out all the age_incr fields in age (if present)
+ if (aSetDef.age_incr) {
+ args.age = {};
+ for (let unit of Object.keys(aSetDef.age_incr)) {
+ args.age[unit] = 0;
+ }
+ }
+ // copy over the initial values from age (if present)
+ if (aSetDef.age) {
+ args.age = args.age || {};
+ for (let [unit, value] of Object.entries(aSetDef.age)) {
+ args.age[unit] = value;
+ }
+ }
+ // just copy over any attributes found from MAKE_MESSAGES_PROPAGATE
+ for (let propAttrName of this.MAKE_MESSAGES_PROPAGATE) {
+ if (aSetDef[propAttrName]) {
+ args[propAttrName] = aSetDef[propAttrName];
+ }
+ }
+
+ let count = aSetDef.count || this.MAKE_MESSAGES_DEFAULTS.count;
+ let messagsPerThread = aSetDef.msgsPerThread || 1;
+ let lastMessage = null;
+ for (let iMsg = 0; iMsg < count; iMsg++) {
+ // primitive threading support...
+ if (lastMessage && iMsg % messagsPerThread != 0) {
+ args.inReplyTo = lastMessage;
+ } else if (!("inReplyTo" in aSetDef)) {
+ args.inReplyTo = null;
+ }
+ lastMessage = this.makeMessage(args);
+ messages.push(lastMessage);
+
+ if (aSetDef.age_incr) {
+ for (let [unit, delta] of Object.entries(aSetDef.age_incr)) {
+ args.age[unit] += delta;
+ }
+ }
+ }
+
+ return messages;
+ },
+};
+
+/**
+ * Repository of generative message scenarios. Uses the magic bindMethods
+ * function below to allow you to reference methods/attributes without worrying
+ * about how those methods will get the right 'this' pointer if passed as
+ * simply a function argument to someone. So if you do:
+ * foo = messageScenarioFactory.method, followed by foo(...), it will be
+ * equivalent to having simply called messageScenarioFactory.method(...).
+ * (Normally this would not be the case when using JavaScript.)
+ *
+ * @param {MessageGenerator} [aMessageGenerator] The optional message generator we should use.
+ * If you don't pass one, we create our own. You would want to pass one so
+ * that if you also create synthetic messages directly via the message
+ * generator then the two sources can avoid duplicate use of the same
+ * names/addresses/subjects/message-ids.
+ */
+function MessageScenarioFactory(aMessageGenerator) {
+ if (!aMessageGenerator) {
+ aMessageGenerator = new MessageGenerator();
+ }
+ this._msgGen = aMessageGenerator;
+}
+
+MessageScenarioFactory.prototype = {
+ /** Create a chain of direct-reply messages of the given length. */
+ directReply(aNumMessages) {
+ aNumMessages = aNumMessages || 2;
+ let messages = [this._msgGen.makeMessage()];
+ for (let i = 1; i < aNumMessages; i++) {
+ messages.push(this._msgGen.makeMessage({ inReplyTo: messages[i - 1] }));
+ }
+ return messages;
+ },
+
+ /** Two siblings (present), one parent (missing). */
+ siblingsMissingParent() {
+ let missingParent = this._msgGen.makeMessage();
+ let msg1 = this._msgGen.makeMessage({ inReplyTo: missingParent });
+ let msg2 = this._msgGen.makeMessage({ inReplyTo: missingParent });
+ return [msg1, msg2];
+ },
+
+ /** Present parent, missing child, present grand-child. */
+ missingIntermediary() {
+ let msg1 = this._msgGen.makeMessage();
+ let msg2 = this._msgGen.makeMessage({ inReplyTo: msg1 });
+ let msg3 = this._msgGen.makeMessage({ inReplyTo: msg2 });
+ return [msg1, msg3];
+ },
+
+ /**
+ * The root message and all non-leaf nodes have aChildrenPerParent children,
+ * for a total of aHeight layers. (If aHeight is 1, we have just the root;
+ * if aHeight is 2, the root and his aChildrePerParent children.)
+ */
+ fullPyramid(aChildrenPerParent, aHeight) {
+ let msgGen = this._msgGen;
+ let root = msgGen.makeMessage();
+ let messages = [root];
+ function helper(aParent, aRemDepth) {
+ for (let iChild = 0; iChild < aChildrenPerParent; iChild++) {
+ let child = msgGen.makeMessage({ inReplyTo: aParent });
+ messages.push(child);
+ if (aRemDepth) {
+ helper(child, aRemDepth - 1);
+ }
+ }
+ }
+ if (aHeight > 1) {
+ helper(root, aHeight - 2);
+ }
+ return messages;
+ },
+};
+
+/**
+ * Decorate the given object's methods will python-style method binding. We
+ * create a getter that returns a method that wraps the call, providing the
+ * actual method with the 'this' of the object that was 'this' when the getter
+ * was called.
+ * Note that we don't follow the prototype chain; we only process the object you
+ * immediately pass to us. This does not pose a problem for the 'this' magic
+ * because we are using a getter and 'this' in js always refers to the object
+ * in question (never any part of its prototype chain). As such, you probably
+ * want to invoke us on your prototype object(s).
+ *
+ * @param {object} aObj - The object on whom we want to perform magic binding.
+ * This should probably be your prototype object.
+ */
+function bindMethods(aObj) {
+ for (let [name, ubfunc] of Object.entries(aObj)) {
+ // the variable binding needs to get captured...
+ let realFunc = ubfunc;
+ delete aObj[name];
+ Object.defineProperty(aObj, name, {
+ get() {
+ return realFunc.bind(this);
+ },
+ });
+ }
+}
+
+bindMethods(MessageScenarioFactory.prototype);
diff --git a/comm/mailnews/test/resources/MessageInjection.jsm b/comm/mailnews/test/resources/MessageInjection.jsm
new file mode 100644
index 0000000000..9e1b7c79cb
--- /dev/null
+++ b/comm/mailnews/test/resources/MessageInjection.jsm
@@ -0,0 +1,987 @@
+/* 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 = ["MessageInjection"];
+
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+var { ImapMessage } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+);
+var { IMAPPump, setupIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+
+const SEARCH_TERM_MAP_HELPER = {
+ subject: Ci.nsMsgSearchAttrib.Subject,
+ body: Ci.nsMsgSearchAttrib.Body,
+ from: Ci.nsMsgSearchAttrib.Sender,
+ to: Ci.nsMsgSearchAttrib.To,
+ cc: Ci.nsMsgSearchAttrib.CC,
+ recipient: Ci.nsMsgSearchAttrib.ToOrCC,
+ involves: Ci.nsMsgSearchAttrib.AllAddresses,
+ age: Ci.nsMsgSearchAttrib.AgeInDays,
+ tags: Ci.nsMsgSearchAttrib.Keywords,
+ // If a test uses a custom search term, they must register that term
+ // with the id "mailnews@mozilla.org#test"
+ custom: Ci.nsMsgSearchAttrib.Custom,
+};
+
+/**
+ * Handling for Messages in Folders. Usage of either `local` or `imap`.
+ *
+ * Beware:
+ * Currently only one active instance of MessageInjection is supported due
+ * to a dependency on retrieving an account in the constructor.
+ */
+class MessageInjection {
+ /**
+ * MessageInjectionSetup
+ */
+ _mis = {
+ _nextUniqueFolderId: 0,
+
+ injectionConfig: {
+ mode: "none",
+ },
+ listeners: [],
+ notifyListeners(handlerName, args) {
+ for (let listener of this.listeners) {
+ if (handlerName in listener) {
+ listener[handlerName].apply(listener, args);
+ }
+ }
+ },
+
+ /**
+ * The nsIMsgIncomingServer
+ */
+ incomingServer: null,
+
+ /**
+ * The incoming server's (synthetic) root message folder.
+ */
+ rootFolder: null,
+
+ /**
+ * The nsIMsgFolder that is the inbox.
+ */
+ inboxFolder: null,
+
+ /**
+ * Fakeserver daemon, if applicable.
+ */
+ daemon: null,
+ /**
+ * Fakeserver server instance, if applicable.
+ */
+ server: null,
+ };
+ /**
+ * Creates an environment for tests.
+ * Usage either with "local" or "imap".
+ * An Inbox folder is created. Retrieve it via `getInboxFolder`
+ *
+ * IMAP:
+ * Starts an IMAP Server for you
+ *
+ * @param {object} injectionConfig
+ * @param {"local"|"imap"} injectionConfig.mode mode One of "local", "imap".
+ * @param {boolean} [injectionConfig.offline] Should the folder be marked offline (and
+ * fully downloaded)? Only relevant for IMAP.
+ * @param {MessageGenerator} [msgGen] The MessageGenerator which generates the new
+ * SyntheticMessages. We do not create our own because we would lose track of
+ * messages created from another MessageGenerator.
+ * It's optional as it is used only for a subset of methods.
+ */
+ constructor(injectionConfig, msgGen) {
+ // Set the injection Mode.
+ this._mis.injectionConfig = injectionConfig;
+
+ // Disable new mail notifications.
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ // Set msgGen if given.
+ if (msgGen) {
+ this.msgGen = msgGen;
+ }
+
+ // we need to pull in the notification service so we get events?
+ MailServices.mfn;
+
+ if (this._mis.injectionConfig.mode == "local") {
+ // This does createIncomingServer() and createAccount(), sets the server as
+ // the account's server, then sets the server.
+ try {
+ MailServices.accounts.createLocalMailAccount();
+ } catch (ex) {
+ // This will fail if someone already called this. Like in the mozmill
+ // case.
+ }
+
+ let localAccount = MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+
+ // We need an identity or we get angry warnings.
+ let identity = MailServices.accounts.createIdentity();
+ // We need an email to protect against random code assuming it exists and
+ // throwing exceptions.
+ identity.email = "sender@nul.invalid";
+ localAccount.addIdentity(identity);
+ localAccount.defaultIdentity = identity;
+
+ this._mis.incomingServer = MailServices.accounts.localFoldersServer;
+ // Note: Inbox is not created automatically when there is no deferred server,
+ // so we need to create it.
+ this._mis.rootFolder = this._mis.incomingServer.rootMsgFolder;
+ this._mis.rootFolder.createSubfolder("Inbox", null);
+ this._mis.inboxFolder = this._mis.rootFolder.getChildNamed("Inbox");
+ // a local inbox should have a Mail flag!
+ this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
+ this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Inbox);
+ this._mis.notifyListeners("onRealFolderCreated", [this._mis.inboxFolder]);
+
+ // Force an initialization of the Inbox folder database.
+ this._mis.inboxFolder.prettyName;
+ } else if (this._mis.injectionConfig.mode == "imap") {
+ // Disable autosync in favor of our explicitly forcing downloads of all
+ // messages in a folder. This is being done speculatively because when we
+ // didn't do this we got tripped up by the semaphore being in use and
+ // concern over inability to hang a listener off of the completion of the
+ // download. (Although I'm sure there are various ways we could do it.)
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ // Set the offline property based on the configured setting. This will
+ // affect newly created folders.
+ Services.prefs.setBoolPref(
+ "mail.server.default.offline_download",
+ this._mis.injectionConfig.offline
+ );
+
+ // set up IMAP fakeserver and incoming server
+ setupIMAPPump("");
+ this._mis.daemon = IMAPPump.daemon;
+ this._mis.server = IMAPPump.server;
+ this._mis.incomingServer = IMAPPump.incomingServer;
+ // this.#mis.server._debug = 3;
+
+ // do not log transactions; it's just a memory leak to us
+ this._mis.server._logTransactions = false;
+
+ // We need an identity so that updateFolder doesn't fail
+ let localAccount = MailServices.accounts.defaultAccount;
+ // We need an email to protect against random code assuming it exists and
+ // throwing exceptions.
+ let identity = localAccount.defaultIdentity;
+ identity.email = "sender@nul.invalid";
+
+ // The server doesn't support more than one connection
+ Services.prefs.setIntPref(
+ "mail.server.server1.max_cached_connections",
+ 1
+ );
+ // We aren't interested in downloading messages automatically
+ Services.prefs.setBoolPref("mail.server.server1.download_on_biff", false);
+
+ this._mis.rootFolder = this._mis.incomingServer.rootMsgFolder;
+
+ this._mis.inboxFolder = this._mis.rootFolder.getChildNamed("Inbox");
+ // make sure the inbox's offline state is correct. (may be excessive now
+ // that we set the pref above?)
+ if (this._mis.injectionConfig.offline) {
+ this._mis.inboxFolder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ } else {
+ this._mis.inboxFolder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+ this._mis.notifyListeners("onRealFolderCreated", [this._mis.inboxFolder]);
+
+ this._mis.handleUriToRealFolder = {};
+ this._mis.handleUriToFakeFolder = {};
+ this._mis.realUriToFakeFolder = {};
+ this._mis.realUriToFakeFolder[this._mis.inboxFolder.URI] =
+ this._mis.daemon.getMailbox("INBOX");
+ } else {
+ throw new Error(
+ "Illegal injection config option: " + this._mis.injectionConfig.mode
+ );
+ }
+
+ this._mis.junkHandle = null;
+ this._mis.junkFolder = null;
+
+ this._mis.trashHandle = null;
+ this._mis.trashFolder = null;
+ }
+
+ /**
+ * @returns {nsIMsgFolder}
+ */
+ getInboxFolder() {
+ return this._mis.inboxFolder;
+ }
+ /**
+ * @returns {boolean}
+ */
+ messageInjectionIsLocal() {
+ return this._mis.injectionConfig.mode == "local";
+ }
+ /**
+ * Call this method to finish the use of MessageInjection.
+ * Stops the IMAP server (if used) and stops internal functions.
+ */
+ teardownMessageInjection() {
+ if (this._mis.injectionConfig.mode == "imap") {
+ this._mis.incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish.
+ // (This spins its own event loop...)
+ this._mis.server.stop();
+ }
+
+ // Clean out this.#mis; we don't just null the global because it's conceivable we
+ // might still have some closures floating about.
+ for (let key in this._mis) {
+ delete this._mis[key];
+ }
+ }
+ /**
+ * Register a listener to be notified when interesting things happen involving
+ * calls made to the message injection API.
+ *
+ * @param {object} listener
+ * @param {Function} listener.onVirtualFolderCreated Called when a virtual
+ * folder is created using |makeVirtualFolder|. Takes a nsIMsgFolder
+ * that defines the virtual folder as argument.
+ */
+ registerMessageInjectionListener(listener) {
+ this._mis.listeners.push(listener);
+ }
+ /**
+ * Create and return an empty folder. If you want to delete this folder
+ * you must call |deleteFolder| to kill it! If you want to rename it, you
+ * must implement a method called renameFolder and then call it.
+ *
+ * @param {string} [folderName] A folder name with no support for hierarchy at this
+ * time. A name of the form "gabba#" will be autogenerated if you do not
+ * provide one.
+ * @param {nsMsgFolderFlags[]} [specialFlags] A list of nsMsgFolderFlags bits to set.
+ * @returns {nsIMsgFolder|string} In local mode a nsIMsgFolder is returned.
+ * In imap mode a folder URI is returned.
+ */
+ async makeEmptyFolder(folderName, specialFlags) {
+ if (folderName == null) {
+ folderName = "gabba" + this._mis._nextUniqueFolderId++;
+ }
+ let testFolder;
+
+ if (this._mis.injectionConfig.mode == "local") {
+ let localRoot = this._mis.rootFolder.QueryInterface(
+ Ci.nsIMsgLocalMailFolder
+ );
+ testFolder = localRoot.createLocalSubfolder(folderName);
+ // it seems dumb that we have to set this.
+ testFolder.setFlag(Ci.nsMsgFolderFlags.Mail);
+ if (specialFlags) {
+ for (let flag of specialFlags) {
+ testFolder.setFlag(flag);
+ }
+ }
+ this._mis.notifyListeners("onRealFolderCreated", [testFolder]);
+ } else if (this._mis.injectionConfig.mode == "imap") {
+ // Circumvent this scoping.
+ let mis = this._mis;
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl: (url, exitCode) => {
+ // get the newly created nsIMsgFolder folder
+ let msgFolder = mis.rootFolder.getChildNamed(folderName);
+
+ // XXX there is a bug that causes folders to be reported as ImapPublic
+ // when there is no namespace support by the IMAP server. This is
+ // a temporary workaround.
+ msgFolder.clearFlag(Ci.nsMsgFolderFlags.ImapPublic);
+ msgFolder.setFlag(Ci.nsMsgFolderFlags.ImapPersonal);
+
+ if (specialFlags) {
+ for (let flag of specialFlags) {
+ msgFolder.setFlag(flag);
+ }
+ }
+
+ // get a reference to the fake server folder
+ let fakeFolder = this._mis.daemon.getMailbox(folderName);
+ // establish the mapping
+ mis.handleUriToRealFolder[testFolder] = msgFolder;
+ mis.handleUriToFakeFolder[testFolder] = fakeFolder;
+ mis.realUriToFakeFolder[msgFolder.URI] = fakeFolder;
+
+ // notify listeners
+ mis.notifyListeners("onRealFolderCreated", [msgFolder]);
+ },
+ });
+
+ testFolder = this._mis.rootFolder.URI + "/" + folderName;
+
+ // Tell the IMAP service to create the folder, adding a listener that
+ // hooks up the 'handle' URI -> actual folder mapping.
+ MailServices.imap.createFolder(
+ this._mis.rootFolder,
+ folderName,
+ promiseUrlListener
+ );
+ await promiseUrlListener.promise;
+ }
+
+ return testFolder;
+ }
+ /**
+ * Small helper for moving folder.
+ *
+ * @param {nsIMsgFolder} source
+ * @param {nsIMsgFolder} target
+ */
+ static async moveFolder(source, target) {
+ // we're doing a true move
+ await new Promise((resolve, reject) => {
+ MailServices.copy.copyFolder(
+ MessageInjection.get_nsIMsgFolder(source),
+ MessageInjection.get_nsIMsgFolder(target),
+ true,
+ {
+ /* nsIMsgCopyServiceListener implementation */
+ OnStartCopy() {},
+ OnProgress(progress, progressMax) {},
+ SetMessageKey(key) {},
+ SetMessageId(messageId) {},
+ OnStopCopy(status) {
+ if (Components.isSuccessCode(status)) {
+ resolve();
+ } else {
+ reject();
+ }
+ },
+ },
+ null
+ );
+ });
+ }
+ /**
+ *
+ * Get/create the junk folder handle. Use getRealInjectionFolder if you
+ * need the underlying nsIFolder.
+ *
+ * @returns {nsIMsgFolder}
+ */
+ async getJunkFolder() {
+ if (!this._mis.junkHandle) {
+ this._mis.junkHandle = await this.makeEmptyFolder("Junk", [
+ Ci.nsMsgFolderFlags.Junk,
+ ]);
+ }
+
+ return this._mis.junkHandle;
+ }
+ /**
+ * Get/create the trash folder handle. Use getRealInjectionFolder if you
+ * need the underlying nsIMsgFolder.
+ *
+ * @returns {nsIMsgFolder|string}
+ */
+ async getTrashFolder() {
+ if (!this._mis.trashHandle) {
+ // the folder may have been created and already known...
+ this._mis.trashFolder = this._mis.rootFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Trash
+ );
+ if (this._mis.trashFolder) {
+ this._mis.trashHandle = this._mis.rootFolder.URI + "/Trash";
+ let fakeFolder = this._mis.daemon.getMailbox("Trash");
+ this._mis.handleUriToRealFolder[this._mis.trashHandle] =
+ this._mis.trashFolder;
+ this._mis.handleUriToFakeFolder[this._mis.trashHandle] = fakeFolder;
+ this._mis.realUriToFakeFolder[this._mis.trashFolder.URI] = fakeFolder;
+ } else {
+ this._mis.trashHandle = await this.makeEmptyFolder("Trash", [
+ Ci.nsMsgFolderFlags.Trash,
+ ]);
+ }
+ }
+
+ return this._mis.trashHandle;
+ }
+ /**
+ * Create and return a virtual folder.
+ *
+ * @param {nsIMsgFolder[]} folders The real folders this virtual folder should draw from.
+ * @param {SEARCH_TERM_MAP_HELPER} searchDef The search definition to use
+ * to build the list of search terms that populate this virtual folder.
+ * Keys should be stuff from SEARCH_TERM_MAP_HELPER and values should be
+ * strings to search for within those attribute things.
+ * @param {boolean} [booleanAnd] Should the search terms be and-ed together.
+ * Defaults to false.
+ * @param {string} [folderName] Name to use.
+ * @returns {nsIMsgFolder|string} In local usage returns a nsIMsgFolder
+ * in imap usage returns a Folder URI.
+ */
+ makeVirtualFolder(folders, searchDef, booleanAnd, folderName) {
+ let name = folderName
+ ? folderName
+ : "virt" + this._mis._nextUniqueFolderId++;
+
+ let terms = [];
+ let termCreator = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ for (let key in searchDef) {
+ let val = searchDef[key];
+ let term = termCreator.createTerm();
+ let value = term.value;
+ value.str = val;
+ term.value = value;
+ term.attrib = SEARCH_TERM_MAP_HELPER[key];
+ if (term.attrib == Ci.nsMsgSearchAttrib.Custom) {
+ term.customId = "mailnews@mozilla.org#test";
+ }
+ term.op = Ci.nsMsgSearchOp.Contains;
+ term.booleanAnd = Boolean(booleanAnd);
+ terms.push(term);
+ }
+ // create an ALL case if we didn't add any terms
+ if (terms.length == 0) {
+ let term = termCreator.createTerm();
+ term.matchAll = true;
+ terms.push(term);
+ }
+
+ let wrapped = VirtualFolderHelper.createNewVirtualFolder(
+ name,
+ this._mis.rootFolder,
+ folders,
+ terms,
+ /* online */ false
+ );
+ this._mis.notifyListeners("onVirtualFolderCreated", [
+ wrapped.virtualFolder,
+ ]);
+ return wrapped.virtualFolder;
+ }
+ /**
+ * Mark the folder as offline and force all of its messages to be downloaded.
+ * This is an asynchronous operation that will call resolve once the
+ * download is completed.
+ *
+ * @param {string} folderHandle Folder URI.
+ */
+ async makeFolderAndContentsOffline(folderHandle) {
+ if (this._mis.injectionConfig.mode != "imap") {
+ return;
+ }
+
+ let msgFolder = this.getRealInjectionFolder(folderHandle);
+ msgFolder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ msgFolder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+
+ /**
+ * Create multiple new local folders, populating them with messages according to
+ * the set definitions provided. Differs from makeFolderWithSets by taking
+ * the number of folders to create and return the list of created folders as
+ * the first element in the returned list. This method is simple enough that
+ * the limited code duplication is deemed acceptable in support of readability.
+ *
+ * @param {number} folderCount
+ * @param {MakeMessageOptions[]} synSetDefs A synthetic set
+ * definition, as appropriate to pass to makeNewSetsInFolders.
+ * @returns {Promise<object[]>} A Promise with a list whose first element are
+ * the nsIMsgFolders created and whose subsequent items are the
+ * SyntheticMessageSets used to populate the folder (as returned by
+ * makeNewSetsInFolders). So nsIMsgFolder[], ...SyntheticMessageSet.
+ *
+ * Please note that the folders are either nsIMsgFolder, or folder
+ * URIs, depending on whether we're in local injection mode, or on IMAP. This
+ * should be transparent to you, unless you start trying to inject messages
+ * into a folder that hasn't been created by makeFoldersWithSets. See
+ * test_folder_deletion_nested in base_index_messages.js for an example of
+ * such pain.
+ */
+ async makeFoldersWithSets(folderCount, synSetDefs) {
+ let msgFolders = [];
+ for (let i = 0; i < folderCount; i++) {
+ msgFolders.push(await this.makeEmptyFolder());
+ }
+ let results = await this.makeNewSetsInFolders(msgFolders, synSetDefs);
+ // results may be referenced by addSetsToFolders in an async fashion, so
+ // don't change it.
+ results = results.concat();
+ results.unshift(msgFolders);
+ return results;
+ }
+ /**
+ * Given one or more existing folders, create new message sets and
+ * add them to the folders using.
+ *
+ * @param {nsIMsgFolder[]} msgFolders A list of nsIMsgFolder.
+ * The synthetic messages will be added to the folder(s).
+ * @param {MakeMessageOptions[]} synSetDefs A list of set definition objects as
+ * defined by MessageGenerator.makeMessages.
+ * @param {boolean} [doNotForceUpdate=false] By default we force an updateFolder on IMAP
+ * folders to ensure Thunderbird knows about the newly injected messages.
+ * If you are testing Thunderbird's use of updateFolder itself, you will
+ * not want this and so will want to pass true for this argument.
+ * @returns {SyntheticMessageSet[]} A Promise with a list of SyntheticMessageSet objects,
+ * each corresponding to the entry in synSetDefs (or implied if an integer was passed).
+ */
+ async makeNewSetsInFolders(msgFolders, synSetDefs, doNotForceUpdate) {
+ // - create the synthetic message sets
+ let messageSets = [];
+ for (let synSetDef of synSetDefs) {
+ // Using the getter of the MessageGenerator for error handling.
+ let messages = this.messageGenerator.makeMessages(synSetDef);
+ messageSets.push(new SyntheticMessageSet(messages));
+ }
+
+ // - add the messages to the folders (interleaving them)
+ await this.addSetsToFolders(msgFolders, messageSets, doNotForceUpdate);
+
+ return messageSets;
+ }
+ /**
+ * Spreads the messages in messageSets across the folders in msgFolders. Each
+ * message set is spread in a round-robin fashion across all folders. At the
+ * same time, each message-sets insertion is interleaved with the other message
+ * sets. This distributes message across multiple folders for useful
+ * cross-folder threading testing (via the round robin) while also hopefully
+ * avoiding making things pathologically easy for the code under test (by way
+ * of the interleaving.)
+ *
+ * For example, given the following 2 input message sets:
+ * message set 'lower': [a b c d e f]
+ * message set 'upper': [A B C D E F G H]
+ *
+ * across 2 folders:
+ * folder 1: [a A c C e E G]
+ * folder 2: [b B d D f F H]
+ * across 3 folders:
+ * folder 1: [a A d D G]
+ * folder 2: [b B e E H]
+ * folder 3: [c C f F]
+ *
+ * @param {nsIMsgFolder[]} msgFolders
+ * An nsIMsgFolder to add the message sets to or a list of them.
+ * @param {SyntheticMessageSet[]} messageSets A list of SyntheticMessageSets.
+ * @param {boolean} [doNotForceUpdate=false] By default we force an updateFolder on IMAP
+ * folders to ensure Thunderbird knows about the newly injected messages.
+ * If you are testing Thunderbird's use of updateFolder itself, you will
+ * not want this and so will want to pass true for this argument.
+ */
+ async addSetsToFolders(msgFolders, messageSets, doNotForceUpdate) {
+ let iterFolders;
+
+ this._mis.notifyListeners("onInjectingMessages", []);
+
+ // -- Pre-loop
+ if (this._mis.injectionConfig.mode == "local") {
+ for (let folder of msgFolders) {
+ if (!(folder instanceof Ci.nsIMsgLocalMailFolder)) {
+ throw new Error("All folders in msgFolders must be local folders!");
+ }
+ }
+ } else if (this._mis.injectionConfig.mode == "imap") {
+ // no protection is possible because of our dependency on promises,
+ // although we could check that the fake URL is one we handed out.
+ } else {
+ throw new Error("Message injection is not configured!");
+ }
+
+ if (this._mis.injectionConfig.mode == "local") {
+ // Note: in order to cut down on excessive fsync()s, we do a two-pass
+ // approach. In the first pass we just allocate messages to the folder
+ // we are going to insert them into. In the second pass we insert the
+ // messages into folders in batches and perform any mutations.
+ let folderBatches = msgFolders.map(folder => {
+ return { folder, messages: [] };
+ });
+ iterFolders = this._looperator([...folderBatches.keys()]);
+ let iPerSet = 0,
+ folderNext = iterFolders.next();
+
+ // - allocate messages to folders
+ // loop, incrementing our subscript until all message sets are out of messages
+ let didSomething;
+ do {
+ didSomething = false;
+ // for each message set, if it is not out of messages, add the message
+ for (let messageSet of messageSets) {
+ if (iPerSet < messageSet.synMessages.length) {
+ let synMsg = messageSet._trackMessageAddition(
+ folderBatches[folderNext.value].folder,
+ iPerSet
+ );
+ folderBatches[folderNext.value].messages.push({
+ messageSet,
+ synMsg,
+ index: iPerSet,
+ });
+ didSomething = true;
+ }
+ }
+ iPerSet++;
+ folderNext = iterFolders.next();
+ } while (didSomething);
+
+ // - inject messages
+ for (let folderBatch of folderBatches) {
+ // it is conceivable some folders might not get any messages, skip them.
+ if (!folderBatch.messages.length) {
+ continue;
+ }
+
+ let folder = folderBatch.folder;
+ folder.gettingNewMessages = true;
+ let messageStrings = folderBatch.messages.map(message =>
+ message.synMsg.toMboxString()
+ );
+ folder.addMessageBatch(messageStrings);
+
+ for (let message of folderBatch.messages) {
+ let synMsgState = message.synMsg.metaState;
+ // If we need to mark the message as junk grab the header and do so.
+ if (synMsgState.junk) {
+ message.messageSet.setJunk(
+ true,
+ message.messageSet.getMsgHdr(message.index)
+ );
+ }
+ if (synMsgState.read) {
+ // XXX this will generate an event; I'm not sure if we should be
+ // trying to avoid that or not. This case is really only added
+ // for IMAP where this makes more sense.
+ message.messageSet.setRead(
+ true,
+ message.messageSet.getMsgHdr(message.index)
+ );
+ }
+ }
+ if (folderBatch.messages.length) {
+ let lastMRUTime = Math.floor(
+ Number(folderBatch.messages[0].synMsg.date) / 1000
+ );
+ folder.setStringProperty("MRUTime", lastMRUTime);
+ }
+ folder.gettingNewMessages = false;
+ folder.hasNewMessages = true;
+ folder.setNumNewMessages(
+ folder.getNumNewMessages(false) + messageStrings.length
+ );
+ folder.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NewMail;
+ }
+
+ // make sure that junk filtering gets a turn
+ // XXX we probably need to be doing more in terms of filters here,
+ // although since filters really want to be run on the inbox, there
+ // are separate potential semantic issues involved.
+ for (let folder of msgFolders) {
+ folder.callFilterPlugins(null);
+ }
+ } else if (this._mis.injectionConfig.mode == "imap") {
+ iterFolders = this._looperator(msgFolders);
+ // we need to call updateFolder on all the folders, not just the first
+ // one...
+ let iPerSet = 0,
+ folder = iterFolders.next();
+ let didSomething;
+ do {
+ didSomething = false;
+ for (let messageSet of messageSets) {
+ if (iPerSet < messageSet.synMessages.length) {
+ didSomething = true;
+
+ let realFolder = this._mis.handleUriToRealFolder[folder.value];
+ let fakeFolder = this._mis.handleUriToFakeFolder[folder.value];
+ let synMsg = messageSet._trackMessageAddition(realFolder, iPerSet);
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(synMsg.toMessageString())
+ );
+ let imapMsg = new ImapMessage(
+ msgURI.spec,
+ fakeFolder.uidnext++,
+ []
+ );
+ // If the message's meta-state indicates it is junk, set that flag.
+ // There is also a NotJunk flag, but we're not playing with that
+ // right now; as long as nothing is ever marked as junk, the junk
+ // classifier won't run, so it's moot for now.
+ if (synMsg.metaState.junk) {
+ imapMsg.setFlag("Junk");
+ }
+ if (synMsg.metaState.read) {
+ imapMsg.setFlag("\\Seen");
+ }
+ fakeFolder.addMessage(imapMsg);
+ }
+ }
+ iPerSet++;
+ folder = iterFolders.next();
+ } while (didSomething);
+
+ // We have nothing more to do if we aren't support to force the update.
+ if (doNotForceUpdate) {
+ return;
+ }
+
+ for (let iFolder = 0; iFolder < msgFolders.length; iFolder++) {
+ let realFolder = this._mis.handleUriToRealFolder[msgFolders[iFolder]];
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(realFolder, resolve);
+ });
+
+ // compel download of the messages if appropriate
+ if (realFolder.flags & Ci.nsMsgFolderFlags.Offline) {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ realFolder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+ }
+ }
+ }
+ /**
+ * Return the nsIMsgFolder associated with a folder handle. If the folder has
+ * been created since the last injection and you are using IMAP, you may need
+ * to first resolve the Promises for us to be able to provide
+ * you with a result.
+ *
+ * @param {nsIMsgFolder|string} folderHandle nsIMsgFolder or folder URI.
+ * @returns {nsIMsgFolder}
+ */
+ getRealInjectionFolder(folderHandle) {
+ if (this._mis.injectionConfig.mode == "imap") {
+ return this._mis.handleUriToRealFolder[folderHandle];
+ }
+ return folderHandle;
+ }
+ /**
+ * Move messages in the given set to the destination folder.
+ *
+ * For IMAP moves we force an update of the source folder and then the
+ * destination folder. This ensures that any (pseudo-)offline operations in
+ * the source folder have had a chance to run and that we have seen the changes
+ * in the target folder.
+ * We additionally cause all of the message bodies to be downloaded in the
+ * target folder if the folder has the Offline flag set.
+ *
+ * @param {SyntheticMessageSet} synMessageSet The messages to move.
+ * @param {nsIMsgFolder|string} destFolder The target folder or target folder URI.
+ * @param {boolean} [allowUndo=false] Should we generate undo operations and, as a
+ * side-effect, offline operations? (The code uses undo operations as
+ * a proxy-indicator for it coming from the UI and therefore performing
+ * pseudo-offline operations instead of trying to do things online.)
+ */
+ async moveMessages(synMessageSet, destFolder, allowUndo) {
+ let realDestFolder = this.getRealInjectionFolder(destFolder);
+
+ for (let [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
+ // In the IMAP case tell listeners we are moving messages without
+ // destination headers.
+ if (!this.messageInjectionIsLocal()) {
+ this._mis.notifyListeners("onMovingMessagesWithoutDestHeaders", [
+ realDestFolder,
+ ]);
+ }
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ folder,
+ msgs,
+ realDestFolder,
+ /* move */ true,
+ promiseCopyListener,
+ null,
+ Boolean(allowUndo)
+ );
+ await promiseCopyListener.promise;
+ // update the synthetic message set's folder entry...
+ synMessageSet._folderSwap(folder, realDestFolder);
+
+ // IMAP special case per function doc...
+ if (!this.messageInjectionIsLocal()) {
+ // update the source folder to force it to issue the move
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(folder, resolve);
+ });
+
+ // update the dest folder to see the new header.
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(realDestFolder, resolve);
+ });
+
+ // compel download of messages in dest folder if appropriate
+ if (realDestFolder.flags & Ci.nsMsgFolderFlags.Offline) {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ realDestFolder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+ }
+ }
+ }
+ /**
+ * Move the messages to the trash; do not use this on messages that are already
+ * in the trash, we are not clever enough for that.
+ *
+ * @param {SyntheticMessageSet} synMessageSet The set of messages to trash.
+ * The messages do not all have to be in the same folder,
+ * but we have to trash them folder by folder if they are not.
+ */
+ async trashMessages(synMessageSet) {
+ for (let [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
+ // In the IMAP case tell listeners we are moving messages without
+ // destination headers, since that's what trashing amounts to.
+ if (!this.messageInjectionIsLocal()) {
+ this._mis.notifyListeners("onMovingMessagesWithoutDestHeaders", []);
+ }
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ folder.deleteMessages(
+ msgs,
+ null,
+ false,
+ true,
+ promiseCopyListener,
+ /* do not allow undo, currently leaks */ false
+ );
+ await promiseCopyListener.promise;
+
+ // just like the move case we need to force updateFolder calls for IMAP
+ if (!this.messageInjectionIsLocal()) {
+ // update the source folder to force it to issue the move
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(folder, resolve);
+ });
+
+ // trash folder may not have existed at startup but the deletion
+ // will have created it.
+ let trashFolder = this.getRealInjectionFolder(
+ await this.getTrashFolder()
+ );
+
+ // update the dest folder to see the new header.
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(trashFolder, resolve);
+ });
+
+ // compel download of messages in dest folder if appropriate
+ if (trashFolder.flags & Ci.nsMsgFolderFlags.Offline) {
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ trashFolder.downloadAllForOffline(promiseUrlListener, null);
+ await promiseUrlListener.promise;
+ }
+ }
+ }
+ }
+ /**
+ * Delete all of the messages in a SyntheticMessageSet like the user performed a
+ * shift-delete (or if the messages were already in the trash).
+ *
+ * @param {SyntheticMessageSet} synMessageSet The set of messages to delete.
+ * The messages do not all have to be in the same folder, but we have to
+ * delete them folder by folder if they are not.
+ */
+ static async deleteMessages(synMessageSet) {
+ for (let [folder, msgs] of synMessageSet.foldersWithMsgHdrs) {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ folder.deleteMessages(
+ msgs,
+ null,
+ /* delete storage */ true,
+ /* is move? */ false,
+ promiseCopyListener,
+ /* do not allow undo, currently leaks */ false
+ );
+ await promiseCopyListener.promise;
+ }
+ }
+ /**
+ * Empty the trash.
+ */
+ async emptyTrash() {
+ let trashHandle = await this.getTrashFolder();
+ let trashFolder = this.getRealInjectionFolder(trashHandle);
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ trashFolder.emptyTrash(promiseUrlListener);
+ await promiseUrlListener.promise;
+ }
+ /**
+ * Delete the given folder, removing the storage. We do not move it to the
+ * trash.
+ */
+ deleteFolder(folder) {
+ let realFolder = this.getRealInjectionFolder(folder);
+ realFolder.parent.propagateDelete(realFolder, true);
+ }
+
+ /**
+ * @param {nsIMsgFolder} folder
+ * @returns {nsIMsgFolder}
+ */
+ static get_nsIMsgFolder(folder) {
+ if (!(folder instanceof Ci.nsIMsgFolder)) {
+ return MailUtils.getOrCreateFolder(folder);
+ }
+ return folder;
+ }
+ /**
+ * An iterator that generates an infinite sequence of its argument. So
+ * _looperator(1, 2, 3) will generate the iteration stream: [1, 2, 3, 1, 2, 3,
+ * 1, 2, 3, ...].
+ */
+ *_looperator(list) {
+ if (list.length == 0) {
+ throw new Error("list must have at least one item!");
+ }
+
+ let i = 0,
+ length = list.length;
+ while (true) {
+ yield list[i];
+ i = (i + 1) % length;
+ }
+ }
+
+ get messageGenerator() {
+ if (this.msgGen === undefined) {
+ throw new Error(
+ "MessageInjection.jsm needs a MessageGenerator for new messages. " +
+ "The MessageGenerator helps you with threaded messages. If you use " +
+ "two different MessageGenerators the behaviour with threads are complicated."
+ );
+ }
+ return this.msgGen;
+ }
+ /**
+ * @param {MessageGenerator} msgGen The MessageGenerator which generates the new
+ * SyntheticMessages. We do not create our own because we would lose track of
+ * messages created from another MessageGenerator.
+ */
+ set messageGenerator(msgGen) {
+ this.msgGen = msgGen;
+ }
+}
diff --git a/comm/mailnews/test/resources/NetworkTestUtils.jsm b/comm/mailnews/test/resources/NetworkTestUtils.jsm
new file mode 100644
index 0000000000..131eb9c9eb
--- /dev/null
+++ b/comm/mailnews/test/resources/NetworkTestUtils.jsm
@@ -0,0 +1,294 @@
+/* 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 provides utilities useful in testing more advanced networking
+ * scenarios, such as proxies and SSL connections.
+ */
+
+const EXPORTED_SYMBOLS = ["NetworkTestUtils"];
+
+var CC = Components.Constructor;
+
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+const ServerSocket = CC(
+ "@mozilla.org/network/server-socket;1",
+ "nsIServerSocket",
+ "init"
+);
+const BinaryInputStream = CC(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+// The following code is adapted from network/test/unit/test_socks.js, in order
+// to provide a SOCKS proxy server for our testing code.
+//
+// For more details on how SOCKSv5 works, please read RFC 1928.
+var currentThread = Services.tm.currentThread;
+
+const STATE_WAIT_GREETING = 1;
+const STATE_WAIT_SOCKS5_REQUEST = 2;
+
+/**
+ * A client of a SOCKS connection.
+ *
+ * This doesn't implement all of SOCKSv5, just enough to get a simple proxy
+ * working for the test code.
+ *
+ * @param {nsIInputStream} client_in - The nsIInputStream of the socket.
+ * @param {nsIOutputStream} client_out - The nsIOutputStream of the socket.
+ */
+function SocksClient(client_in, client_out) {
+ this.client_in = client_in;
+ this.client_out = client_out;
+ this.inbuf = [];
+ this.state = STATE_WAIT_GREETING;
+ this.waitRead(this.client_in);
+}
+SocksClient.prototype = {
+ // ... implement nsIInputStreamCallback ...
+ QueryInterface: ChromeUtils.generateQI(["nsIInputStreamCallback"]),
+ onInputStreamReady(input) {
+ var len = input.available();
+ var bin = new BinaryInputStream(input);
+ var data = bin.readByteArray(len);
+ this.inbuf = this.inbuf.concat(data);
+
+ switch (this.state) {
+ case STATE_WAIT_GREETING:
+ this.handleGreeting();
+ break;
+ case STATE_WAIT_SOCKS5_REQUEST:
+ this.handleSocks5Request();
+ break;
+ }
+
+ if (!this.sub_transport) {
+ this.waitRead(input);
+ }
+ },
+
+ // Listen on the input for the next packet
+ waitRead(input) {
+ input.asyncWait(this, 0, 0, currentThread);
+ },
+
+ // Simple handler to write out a binary string (because xpidl sucks here)
+ write(buf) {
+ this.client_out.write(buf, buf.length);
+ },
+
+ // Handle the first SOCKSv5 client message
+ handleGreeting() {
+ if (this.inbuf.length == 0) {
+ return;
+ }
+
+ if (this.inbuf[0] != 5) {
+ dump("Unknown protocol version: " + this.inbuf[0] + "\n");
+ this.close();
+ return;
+ }
+
+ // Some quality checks to make sure we've read the entire greeting.
+ if (this.inbuf.length < 2) {
+ return;
+ }
+ var nmethods = this.inbuf[1];
+ if (this.inbuf.length < 2 + nmethods) {
+ return;
+ }
+ this.inbuf = [];
+
+ // Tell them that we don't log into this SOCKS server.
+ this.state = STATE_WAIT_SOCKS5_REQUEST;
+ this.write("\x05\x00");
+ },
+
+ // Handle the second SOCKSv5 message
+ handleSocks5Request() {
+ if (this.inbuf.length < 4) {
+ return;
+ }
+
+ // Find the address:port requested.
+ var atype = this.inbuf[3];
+ var len, addr;
+ if (atype == 0x01) {
+ // IPv4 Address
+ len = 4;
+ addr = this.inbuf.slice(4, 8).join(".");
+ } else if (atype == 0x03) {
+ // Domain name
+ len = this.inbuf[4];
+ addr = String.fromCharCode.apply(null, this.inbuf.slice(5, 5 + len));
+ len = len + 1;
+ } else if (atype == 0x04) {
+ // IPv6 address
+ len = 16;
+ addr = this.inbuf
+ .slice(4, 20)
+ .map(i => i.toString(16))
+ .join(":");
+ }
+ var port = (this.inbuf[4 + len] << 8) | this.inbuf[5 + len];
+ dump("Requesting " + addr + ":" + port + "\n");
+
+ // Map that data to the port we report.
+ var foundPort = gPortMap.get(addr + ":" + port);
+ dump("This was mapped to " + foundPort + "\n");
+
+ if (foundPort !== undefined) {
+ this.write(
+ "\x05\x00\x00" + // Header for response
+ "\x04" +
+ "\x00".repeat(15) +
+ "\x01" + // IPv6 address ::1
+ String.fromCharCode(foundPort >> 8) +
+ String.fromCharCode(foundPort & 0xff) // Port number
+ );
+ } else {
+ this.write(
+ "\x05\x05\x00" + // Header for failed response
+ "\x04" +
+ "\x00".repeat(15) +
+ "\x01" + // IPv6 address ::1
+ "\x00\x00"
+ );
+ this.close();
+ return;
+ }
+
+ // At this point, we contact the local server on that port and then we feed
+ // the data back and forth. Easiest way to do that is to open the connection
+ // and use the async copy to do it in a background thread.
+ let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
+ Ci.nsISocketTransportService
+ );
+ let trans = sts.createTransport([], "localhost", foundPort, null, null);
+ let tunnelInput = trans.openInputStream(0, 1024, 1024);
+ let tunnelOutput = trans.openOutputStream(0, 1024, 1024);
+ this.sub_transport = trans;
+ NetUtil.asyncCopy(tunnelInput, this.client_out);
+ NetUtil.asyncCopy(this.client_in, tunnelOutput);
+ },
+
+ close() {
+ this.client_in.close();
+ this.client_out.close();
+ if (this.sub_transport) {
+ this.sub_transport.close(Cr.NS_OK);
+ }
+ },
+};
+
+// A SOCKS server that runs on a random port.
+function SocksTestServer() {
+ this.listener = ServerSocket(-1, true, -1);
+ dump("Starting SOCKS server on " + this.listener.port + "\n");
+ this.port = this.listener.port;
+ this.listener.asyncListen(this);
+ this.client_connections = [];
+}
+SocksTestServer.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIServerSocketListener"]),
+
+ onSocketAccepted(socket, trans) {
+ var input = trans.openInputStream(0, 0, 0);
+ var output = trans.openOutputStream(0, 0, 0);
+ var client = new SocksClient(input, output);
+ this.client_connections.push(client);
+ },
+
+ onStopListening(socket) {},
+
+ close() {
+ for (let client of this.client_connections) {
+ client.close();
+ }
+ this.client_connections = [];
+ if (this.listener) {
+ this.listener.close();
+ this.listener = null;
+ }
+ },
+};
+
+var gSocksServer = null;
+// hostname:port -> the port on localhost that the server really runs on.
+var gPortMap = new Map();
+
+var NetworkTestUtils = {
+ /**
+ * Set up a proxy entry such that requesting a connection to hostName:port
+ * will instead cause a connection to localRemappedPort. This will use a SOCKS
+ * proxy (because any other mechanism is too complicated). Since this is
+ * starting up a server, it does behoove you to call shutdownServers when you
+ * no longer need to use the proxy server.
+ *
+ * @param {string} hostName - The DNS name to use for the client.
+ * @param {integer} hostPort - The port number to use for the client.
+ * @param {integer} localRemappedPort - The port number on which the real server sits.
+ */
+ configureProxy(hostName, hostPort, localRemappedPort) {
+ if (gSocksServer == null) {
+ gSocksServer = new SocksTestServer();
+ // Using PAC makes much more sense here. However, it turns out that PAC
+ // appears to be broken with synchronous proxy resolve, so enabling the
+ // PAC mode requires bug 791645 to be fixed first.
+ /*
+ let pac = 'data:text/plain,function FindProxyForURL(url, host) {' +
+ "if (host == 'localhost' || host == '127.0.0.1') {" +
+ 'return "DIRECT";' +
+ '}' +
+ 'return "SOCKS5 127.0.0.1:' + gSocksServer.port + '";' +
+ '}';
+ dump(pac + '\n');
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ Services.prefs.setCharPref("network.proxy.autoconfig_url", pac);
+ */
+
+ // Until then, we'll serve the actual proxy via a proxy filter.
+ let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
+ Ci.nsIProtocolProxyService
+ );
+ let filter = {
+ QueryInterface: ChromeUtils.generateQI(["nsIProtocolProxyFilter"]),
+ applyFilter(aURI, aProxyInfo, aCallback) {
+ if (aURI.host != "localhost" && aURI.host != "127.0.0.1") {
+ aCallback.onProxyFilterResult(
+ pps.newProxyInfo(
+ "socks",
+ "localhost",
+ gSocksServer.port,
+ "",
+ "",
+ Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST,
+ 0,
+ null
+ )
+ );
+ return;
+ }
+ aCallback.onProxyFilterResult(aProxyInfo);
+ },
+ };
+ pps.registerFilter(filter, 0);
+ }
+ dump("Requesting to map " + hostName + ":" + hostPort + "\n");
+ gPortMap.set(hostName + ":" + hostPort, localRemappedPort);
+ },
+
+ /**
+ * Turn off any servers started by this file (e.g., the SOCKS proxy server).
+ */
+ shutdownServers() {
+ if (gSocksServer) {
+ gSocksServer.close();
+ }
+ },
+};
diff --git a/comm/mailnews/test/resources/POP3pump.js b/comm/mailnews/test/resources/POP3pump.js
new file mode 100644
index 0000000000..5a083798b1
--- /dev/null
+++ b/comm/mailnews/test/resources/POP3pump.js
@@ -0,0 +1,269 @@
+/* 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 routine will allow the easy processing of
+ * messages through the fake POP3 server into the local
+ * folder. It uses a single global defined as:
+ *
+ * gPOP3Pump: the main access to the routine
+ * gPOP3Pump.run() function to run to load the messages. Returns promise that
+ * resolves when done.
+ * gPOP3Pump.files: (in) an array of message files to load
+ * gPOP3Pump.onDone: function to execute after completion
+ (optional and deprecated)
+ * gPOP3Pump.fakeServer: (out) the POP3 incoming server
+ * gPOP3Pump.resetPluggableStore(): function to change the pluggable store for the
+ * server to the input parameter's store.
+ * (in) pluggable store contract ID
+ *
+ * adapted from test_pop3GetNewMail.js
+ *
+ * Original Author: Kent James <kent@caspia.com>
+ *
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+// Import the pop3 server scripts
+var { nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var { AuthPLAIN, AuthLOGIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+var {
+ Pop3Daemon,
+ POP3_RFC1939_handler,
+ POP3_RFC2449_handler,
+ POP3_RFC5034_handler,
+} = ChromeUtils.import("resource://testing-common/mailnews/Pop3d.jsm");
+
+function POP3Pump() {
+ // public attributes
+ this.fakeServer = null;
+ this.onDone = null;
+ this.files = null;
+
+ // local private variables
+
+ this.kPOP3_PORT = 1024 + 110;
+ this._server = null;
+ this._daemon = null;
+ this._incomingServer = null;
+ this._firstFile = true;
+ this._tests = [];
+ this._finalCleanup = false;
+ this._expectedResult = Cr.NS_OK;
+ this._actualResult = Cr.NS_ERROR_UNEXPECTED;
+ this._mailboxStoreContractID = Services.prefs.getCharPref(
+ "mail.serverDefaultStoreContractID"
+ );
+}
+
+// nsIUrlListener implementation
+POP3Pump.prototype.OnStartRunningUrl = function (url) {};
+
+POP3Pump.prototype.OnStopRunningUrl = function (aUrl, aResult) {
+ this._actualResult = aResult;
+ if (aResult != Cr.NS_OK) {
+ // If we have an error, clean up nicely.
+ this._server.stop();
+
+ var thread = Services.tm.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ }
+ Assert.equal(aResult, this._expectedResult);
+
+ // Let OnStopRunningUrl return cleanly before doing anything else.
+ do_timeout(0, _checkPumpBusy);
+};
+
+// Setup the daemon and server
+// If the debugOption is set, then it will be applied to the server.
+POP3Pump.prototype._setupServerDaemon = function (aDebugOption) {
+ this._daemon = new Pop3Daemon();
+ function createHandler(d) {
+ return new POP3_RFC1939_handler(d);
+ }
+ this._server = new nsMailServer(createHandler, this._daemon);
+ if (aDebugOption) {
+ this._server.setDebugLevel(aDebugOption);
+ }
+ return [this._daemon, this._server];
+};
+
+POP3Pump.prototype._createPop3ServerAndLocalFolders = function () {
+ if (typeof localAccountUtils.inboxFolder == "undefined") {
+ localAccountUtils.loadLocalMailAccount();
+ }
+
+ if (!this.fakeServer) {
+ this.fakeServer = localAccountUtils.create_incoming_server(
+ "pop3",
+ this.kPOP3_PORT,
+ "fred",
+ "wilma"
+ );
+ }
+
+ return this.fakeServer;
+};
+
+POP3Pump.prototype.resetPluggableStore = function (aStoreContractID) {
+ if (aStoreContractID == this._mailboxStoreContractID) {
+ return;
+ }
+
+ Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ aStoreContractID
+ );
+
+ // Cleanup existing files, server and account instances, if any.
+ if (this._server) {
+ this._server.stop();
+ }
+
+ if (this.fakeServer && this.fakeServer.valid) {
+ this.fakeServer.closeCachedConnections();
+ MailServices.accounts.removeIncomingServer(this.fakeServer, false);
+ }
+
+ this.fakeServer = null;
+ localAccountUtils.clearAll();
+
+ this._incomingServer = this._createPop3ServerAndLocalFolders();
+ this._mailboxStoreContractID = aStoreContractID;
+};
+
+POP3Pump.prototype._checkBusy = function () {
+ if (this._tests.length == 0 && !this._finalCleanup) {
+ this._incomingServer.closeCachedConnections();
+
+ // No more tests, let everything finish
+ this._server.stop();
+ this._finalCleanup = true;
+ do_timeout(20, _checkPumpBusy);
+ return;
+ }
+
+ if (this._finalCleanup) {
+ if (Services.tm.currentThread.hasPendingEvents()) {
+ do_timeout(20, _checkPumpBusy);
+ } else {
+ // exit this module
+ do_test_finished();
+ if (this.onDone) {
+ this._promise.then(this.onDone, this.onDone);
+ }
+ if (this._actualResult == Cr.NS_OK) {
+ this._resolve();
+ } else {
+ this._reject(this._actualResult);
+ }
+ }
+ return;
+ }
+
+ // If the server hasn't quite finished, just delay a little longer.
+ if (this._incomingServer.serverBusy) {
+ do_timeout(20, _checkPumpBusy);
+ return;
+ }
+
+ this._testNext();
+};
+
+POP3Pump.prototype._testNext = function () {
+ let thisFiles = this._tests.shift();
+ if (!thisFiles) {
+ // Exit.
+ this._checkBusy();
+ }
+
+ // Handle the server in a try/catch/finally loop so that we always will stop
+ // the server if something fails.
+ try {
+ if (this._firstFile) {
+ this._firstFile = false;
+
+ // Start the fake POP3 server
+ this._server.start();
+ this.kPOP3_PORT = this._server.port;
+ if (this.fakeServer) {
+ this.fakeServer.port = this.kPOP3_PORT;
+ }
+ } else {
+ this._server.resetTest();
+ }
+
+ // Set up the test
+ this._daemon.setMessages(thisFiles);
+
+ // Now get the mail, get inbox in case it got un-deferred.
+ let inbox = this._incomingServer.rootMsgFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+ MailServices.pop3.GetNewMail(null, this, inbox, this._incomingServer);
+
+ this._server.performTest();
+ } catch (e) {
+ this._server.stop();
+
+ do_throw(e);
+ }
+};
+
+POP3Pump.prototype.run = function (aExpectedResult) {
+ do_test_pending();
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+
+ this._server = this._setupServerDaemon();
+ this._daemon = this._server[0];
+ this._server = this._server[1];
+
+ this._firstFile = true;
+ this._finalCleanup = false;
+
+ if (aExpectedResult) {
+ this._expectedResult = aExpectedResult;
+ }
+
+ // In the default configuration, only a single test is accepted
+ // by this routine. But the infrastructure exists to support
+ // multiple tests, as this was in the original files. We leave that
+ // infrastructure in place, so that if desired this routine could
+ // be easily copied and modified to make multiple passes through
+ // a POP3 server.
+
+ this._tests[0] = this.files;
+
+ this._testNext();
+
+ // This probably does not work with multiple tests, but nobody is using that.
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ return this._promise;
+};
+
+var gPOP3Pump = new POP3Pump();
+gPOP3Pump._incomingServer = gPOP3Pump._createPop3ServerAndLocalFolders();
+
+function _checkPumpBusy() {
+ gPOP3Pump._checkBusy();
+}
diff --git a/comm/mailnews/test/resources/PromiseTestUtils.jsm b/comm/mailnews/test/resources/PromiseTestUtils.jsm
new file mode 100644
index 0000000000..cb40b8c856
--- /dev/null
+++ b/comm/mailnews/test/resources/PromiseTestUtils.jsm
@@ -0,0 +1,316 @@
+/* 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 provides utilities useful in using Promises and Task.jsm
+ * with mailnews tests.
+ */
+
+const EXPORTED_SYMBOLS = ["PromiseTestUtils"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Url listener that can wrap another listener and trigger a callback.
+ *
+ * @param [aWrapped] The nsIUrlListener to pass all notifications through to.
+ * This gets called prior to the callback (or async resumption).
+ */
+
+var PromiseTestUtils = {};
+
+PromiseTestUtils.PromiseUrlListener = function (aWrapped) {
+ this.wrapped = aWrapped;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+};
+
+PromiseTestUtils.PromiseUrlListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIUrlListener"]),
+
+ OnStartRunningUrl(aUrl) {
+ if (this.wrapped && this.wrapped.OnStartRunningUrl) {
+ this.wrapped.OnStartRunningUrl(aUrl);
+ }
+ },
+ OnStopRunningUrl(aUrl, aExitCode) {
+ if (this.wrapped && this.wrapped.OnStopRunningUrl) {
+ this.wrapped.OnStopRunningUrl(aUrl, aExitCode);
+ }
+ if (aExitCode == Cr.NS_OK) {
+ this._resolve();
+ } else {
+ this._reject(aExitCode);
+ }
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+
+/**
+ * Copy listener that can wrap another listener and trigger a callback.
+ *
+ * @param {nsIMsgCopyServiceListener} [aWrapped] - The nsIMsgCopyServiceListener
+ * to pass all notifications through to. This gets called prior to the
+ * callback (or async resumption).
+ */
+PromiseTestUtils.PromiseCopyListener = function (aWrapped) {
+ this.wrapped = aWrapped;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ this._result = { messageKeys: [], messageIds: [] };
+};
+
+PromiseTestUtils.PromiseCopyListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgCopyServiceListener"]),
+ OnStartCopy() {
+ if (this.wrapped && this.wrapped.OnStartCopy) {
+ this.wrapped.OnStartCopy();
+ }
+ },
+ OnProgress(aProgress, aProgressMax) {
+ if (this.wrapped && this.wrapped.OnProgress) {
+ this.wrapped.OnProgress(aProgress, aProgressMax);
+ }
+ },
+ SetMessageKey(aKey) {
+ if (this.wrapped && this.wrapped.SetMessageKey) {
+ this.wrapped.SetMessageKey(aKey);
+ }
+
+ this._result.messageKeys.push(aKey);
+ },
+ SetMessageId(aMessageId) {
+ if (this.wrapped && this.wrapped.SetMessageId) {
+ this.wrapped.SetMessageId(aMessageId);
+ }
+
+ this._result.messageIds.push(aMessageId);
+ },
+ OnStopCopy(aStatus) {
+ if (this.wrapped && this.wrapped.OnStopCopy) {
+ this.wrapped.OnStopCopy(aStatus);
+ }
+
+ if (aStatus == Cr.NS_OK) {
+ this._resolve(this._result);
+ } else {
+ this._reject(aStatus);
+ }
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+
+/**
+ * Stream listener that can wrap another listener and trigger a callback.
+ *
+ * @param {nsIStreamListener} [aWrapped] - The nsIStreamListener to pass all
+ * notifications through to. This gets called prior to the callback
+ * (or async resumption).
+ */
+PromiseTestUtils.PromiseStreamListener = function (aWrapped) {
+ this.wrapped = aWrapped;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+ this._data = null;
+ this._stream = null;
+};
+
+PromiseTestUtils.PromiseStreamListener.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+
+ onStartRequest(aRequest) {
+ if (this.wrapped && this.wrapped.onStartRequest) {
+ this.wrapped.onStartRequest(aRequest);
+ }
+ this._data = "";
+ this._stream = null;
+ },
+
+ onStopRequest(aRequest, aStatusCode) {
+ if (this.wrapped && this.wrapped.onStopRequest) {
+ this.wrapped.onStopRequest(aRequest, aStatusCode);
+ }
+ if (aStatusCode == Cr.NS_OK) {
+ this._resolve(this._data);
+ } else {
+ this._reject(aStatusCode);
+ }
+ },
+
+ onDataAvailable(aRequest, aInputStream, aOff, aCount) {
+ if (this.wrapped && this.wrapped.onDataAvailable) {
+ this.wrapped.onDataAvailable(aRequest, aInputStream, aOff, aCount);
+ }
+ if (!this._stream) {
+ this._stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ this._stream.init(aInputStream);
+ }
+ this._data += this._stream.read(aCount);
+ },
+
+ get promise() {
+ return this._promise;
+ },
+};
+
+/**
+ * Folder listener to resolve a promise when a certain folder event occurs.
+ *
+ * @param {nsIMsgFolder} folder - nsIMsgFolder to listen to
+ * @param {string} event - Event name to listen for. Example event is
+ * "DeleteOrMoveMsgCompleted".
+ * @returns {Promise} Promise that resolves when the event occurs.
+ */
+PromiseTestUtils.promiseFolderEvent = function (folder, event) {
+ return new Promise((resolve, reject) => {
+ let folderListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
+ onFolderEvent(aEventFolder, aEvent) {
+ if (folder === aEventFolder && event == aEvent) {
+ MailServices.mailSession.RemoveFolderListener(folderListener);
+ resolve();
+ }
+ },
+ };
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.event
+ );
+ });
+};
+
+/**
+ * Folder listener to resolve a promise when a certain folder event occurs.
+ *
+ * @param {nsIMsgFolder} folder - nsIMsgFolder to listen to.
+ * @param {string} listenerMethod - string listener method to listen for.
+ * Example listener method is "msgsClassified".
+ * @returns {Promise} Promise that resolves when the event occurs.
+ */
+PromiseTestUtils.promiseFolderNotification = function (folder, listenerMethod) {
+ return new Promise((resolve, reject) => {
+ let mfnListener = {};
+ mfnListener[listenerMethod] = function () {
+ let args = Array.from(arguments);
+ let flag = true;
+ for (let arg of args) {
+ if (folder && arg instanceof Ci.nsIMsgFolder) {
+ if (arg == folder) {
+ flag = true;
+ break;
+ } else {
+ return;
+ }
+ }
+ }
+
+ if (flag) {
+ MailServices.mfn.removeListener(mfnListener);
+ resolve(args);
+ }
+ };
+ MailServices.mfn.addListener(
+ mfnListener,
+ Ci.nsIMsgFolderNotificationService[listenerMethod]
+ );
+ });
+};
+
+/**
+ * Folder listener to resolve a promise when a folder with a certain
+ * name is added.
+ *
+ * @param {string} folderName - folder name to listen for
+ * @returns {Promise<nsIMsgFolder>} Promise that resolves with the new folder
+ * when the folder add completes.
+ */
+PromiseTestUtils.promiseFolderAdded = function (folderName) {
+ return new Promise((resolve, reject) => {
+ var listener = {
+ folderAdded: aFolder => {
+ if (aFolder.name == folderName) {
+ MailServices.mfn.removeListener(listener);
+ resolve(aFolder);
+ }
+ },
+ };
+ MailServices.mfn.addListener(
+ listener,
+ Ci.nsIMsgFolderNotificationService.folderAdded
+ );
+ });
+};
+
+/**
+ * Timer to resolve a promise after a delay
+ *
+ * @param {integer} aDelay - Delay in milliseconds
+ * @returns {Promise} Promise that resolves after the delay.
+ */
+PromiseTestUtils.promiseDelay = function (aDelay) {
+ return new Promise((resolve, reject) => {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(resolve, aDelay, Ci.nsITimer.TYPE_ONE_SHOT);
+ });
+};
+
+/**
+ * Search listener to resolve a promise when a search completes
+ *
+ * @param {nsIMsgSearchSession} aSearchSession - The nsIMsgSearchSession to search
+ * @param {nsIMsgSearchNotify} aWrapped - The nsIMsgSearchNotify to pass all
+ * notifications through to. This gets called prior to the callback
+ * (or async resumption).
+ */
+PromiseTestUtils.PromiseSearchNotify = function (aSearchSession, aWrapped) {
+ this._searchSession = aSearchSession;
+ this._searchSession.registerListener(this);
+ this.wrapped = aWrapped;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+};
+
+PromiseTestUtils.PromiseSearchNotify.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgSearchNotify"]),
+ onSearchHit(aHeader, aFolder) {
+ if (this.wrapped && this.wrapped.onSearchHit) {
+ this.wrapped.onSearchHit(aHeader, aFolder);
+ }
+ },
+ onSearchDone(aResult) {
+ this._searchSession.unregisterListener(this);
+ if (this.wrapped && this.wrapped.onSearchDone) {
+ this.wrapped.onSearchDone(aResult);
+ }
+ if (aResult == Cr.NS_OK) {
+ this._resolve();
+ } else {
+ this._reject(aResult);
+ }
+ },
+ onNewSearch() {
+ if (this.wrapped && this.wrapped.onNewSearch) {
+ this.wrapped.onNewSearch();
+ }
+ },
+ get promise() {
+ return this._promise;
+ },
+};
diff --git a/comm/mailnews/test/resources/abSetup.js b/comm/mailnews/test/resources/abSetup.js
new file mode 100644
index 0000000000..65e5e1bf82
--- /dev/null
+++ b/comm/mailnews/test/resources/abSetup.js
@@ -0,0 +1,80 @@
+/* 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/. */
+
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Sets up the directory service provider to return the app dir as the profile
+ * directory for the address book to use for locating its files during the
+ * tests.
+ *
+ * Note there are further configuration setup items below this.
+ */
+
+/**
+ * General Configuration Data that applies to the address book.
+ */
+
+// Personal Address Book configuration items.
+var kPABData = {
+ URI: "jsaddrbook://abook.sqlite",
+ fileName: "abook.sqlite",
+ dirName: "Personal Address Book",
+ dirType: 101,
+ dirPrefID: "ldap_2.servers.pab",
+ readOnly: false,
+ position: 1,
+};
+
+// Collected Address Book configuration items.
+var kCABData = {
+ URI: "jsaddrbook://history.sqlite",
+ fileName: "history.sqlite",
+ dirName: "Collected Addresses",
+ dirType: 101,
+ dirPrefID: "ldap_2.servers.history",
+ readOnly: false,
+ position: 2,
+};
+
+// This currently applies to all address books of local type.
+var kNormalPropertiesURI =
+ "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml";
+
+/**
+ * Installs a pre-prepared address book file into the profile directory.
+ * This version is for JS/SQLite address books, if you create a new type,
+ * replace this function to test them.
+ *
+ * @param {string} source - Path to the source data, without extension
+ * @param {string} dest - Final file name in the profile, with extension
+ */
+function loadABFile(source, dest) {
+ let sourceFile = do_get_file(`${source}.sql`);
+ let destFile = do_get_profile();
+ destFile.append(dest);
+
+ info(`Creating ${destFile.path} from ${sourceFile.path}`);
+
+ let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ let cstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
+ Ci.nsIConverterInputStream
+ );
+ fstream.init(sourceFile, -1, 0, 0);
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let data = "";
+ let read = 0;
+ do {
+ let str = {};
+ read = cstream.readString(0xffffffff, str);
+ data += str.value;
+ } while (read != 0);
+ cstream.close();
+
+ let conn = Services.storage.openDatabase(destFile);
+ conn.executeSimpleSQL(data);
+ conn.close();
+}
diff --git a/comm/mailnews/test/resources/alertTestUtils.js b/comm/mailnews/test/resources/alertTestUtils.js
new file mode 100644
index 0000000000..2b757480e1
--- /dev/null
+++ b/comm/mailnews/test/resources/alertTestUtils.js
@@ -0,0 +1,487 @@
+/* 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/. */
+
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * This file provides support for writing mailnews tests that require hooking
+ * into the alerts system. Normally these tests would require a UI and fail in
+ * debug mode, but with this method you can hook into the alerts system and
+ * avoid the UI.
+ *
+ * This file registers prompts for nsIWindowWatcher::getNewPrompter and also
+ * registers a nsIPromptService service. nsIWindowWatcher::getNewAuthPrompter
+ * is also implemented but returns the nsILoginManagerPrompter as this would
+ * be expected when running mailnews.
+ *
+ * To register the system:
+ *
+ * function run_test() {
+ * registerAlertTestUtils();
+ * // ...
+ * }
+ *
+ * You can then hook into the alerts just by defining a function of the same
+ * name as the interface function:
+ *
+ * function alert(aDialogTitle, aText) {
+ * // do my check
+ * }
+ *
+ * Interface functions that do not have equivalent functions defined and get
+ * called will be treated as unexpected, and therefore they will call
+ * do_throw().
+ */
+/* globals alert, confirm, prompt */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+var LoginInfo = Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo",
+ "init"
+);
+
+// Wrapper to the nsIPrompt interface.
+// This allows the send code to attempt to display errors to the user without
+// failing.
+var alertUtilsPrompts = {
+ alert(aDialogTitle, aText) {
+ if (typeof alert == "function") {
+ alert(aDialogTitle, aText);
+ return;
+ }
+
+ do_throw("alert unexpectedly called: " + aText + "\n");
+ },
+
+ alertCheck(aDialogTitle, aText, aCheckMsg, aCheckState) {
+ if (typeof alertCheck == "function") {
+ // eslint-disable-next-line no-undef
+ alertCheck(aDialogTitle, aText, aCheckMsg, aCheckState);
+ return;
+ }
+
+ do_throw("alertCheck unexpectedly called: " + aText + "\n");
+ },
+
+ confirm(aDialogTitle, aText) {
+ if (typeof confirm == "function") {
+ return confirm(aDialogTitle, aText);
+ }
+
+ do_throw("confirm unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ confirmCheck(aDialogTitle, aText, aCheckMsg, aCheckState) {
+ if (typeof confirmCheck == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmCheck(aDialogTitle, aText, aCheckMsg, aCheckState);
+ }
+
+ do_throw("confirmCheck unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ confirmEx(
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+ ) {
+ if (typeof confirmEx == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmEx(
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("confirmEx unexpectedly called: " + aText + "\n");
+ return 0;
+ },
+
+ prompt(aDialogTitle, aText, aValue, aCheckMsg, aCheckState) {
+ if (typeof prompt == "function") {
+ return prompt(aDialogTitle, aText, aValue, aCheckMsg, aCheckState);
+ }
+
+ do_throw("prompt unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+ ) {
+ if (typeof promptUsernameAndPassword == "function") {
+ // eslint-disable-next-line no-undef
+ return promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("promptUsernameAndPassword unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ promptPassword(aDialogTitle, aText, aPassword, aCheckMsg, aCheckState) {
+ if (typeof promptPassword == "function") {
+ // eslint-disable-next-line no-undef
+ return promptPassword(
+ aDialogTitle,
+ aText,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("promptPassword unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ select(aDialogTitle, aText, aCount, aSelectList, aOutSelection) {
+ if (typeof select == "function") {
+ // eslint-disable-next-line no-undef
+ return select(aDialogTitle, aText, aCount, aSelectList, aOutSelection);
+ }
+
+ do_throw("select unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIPrompt"]),
+};
+
+var alertUtilsPromptService = {
+ alert(aParent, aDialogTitle, aText) {
+ if (typeof alertPS == "function") {
+ // eslint-disable-next-line no-undef
+ alertPS(aParent, aDialogTitle, aText);
+ return;
+ }
+
+ do_throw("alertPS unexpectedly called: " + aText + "\n");
+ },
+
+ alertCheck(aParent, aDialogTitle, aText, aCheckMsg, aCheckState) {
+ if (typeof alertCheckPS == "function") {
+ // eslint-disable-next-line no-undef
+ alertCheckPS(aParent, aDialogTitle, aText, aCheckMsg, aCheckState);
+ return;
+ }
+
+ do_throw("alertCheckPS unexpectedly called: " + aText + "\n");
+ },
+
+ confirm(aParent, aDialogTitle, aText) {
+ if (typeof confirmPS == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmPS(aParent, aDialogTitle, aText);
+ }
+
+ do_throw("confirmPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ confirmCheck(aParent, aDialogTitle, aText, aCheckMsg, aCheckState) {
+ if (typeof confirmCheckPS == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmCheckPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("confirmCheckPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ confirmEx(
+ aParent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+ ) {
+ if (typeof confirmExPS == "function") {
+ // eslint-disable-next-line no-undef
+ return confirmExPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+ );
+ }
+
+ do_throw("confirmExPS unexpectedly called: " + aText + "\n");
+ return 0;
+ },
+
+ prompt(aParent, aDialogTitle, aText, aValue) {
+ if (typeof promptPS == "function") {
+ // eslint-disable-next-line no-undef
+ return promptPS(aParent, aDialogTitle, aText, aValue);
+ }
+
+ do_throw("promptPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ promptUsernameAndPassword(
+ aParent,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword
+ ) {
+ if (typeof promptUsernameAndPasswordPS == "function") {
+ // eslint-disable-next-line no-undef
+ return promptUsernameAndPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword
+ );
+ }
+
+ do_throw(
+ "promptUsernameAndPasswordPS unexpectedly called: " + aText + "\n"
+ );
+ return false;
+ },
+
+ promptPassword(aParent, aDialogTitle, aText, aPassword) {
+ if (typeof promptPasswordPS == "function") {
+ // eslint-disable-next-line no-undef
+ return promptPasswordPS(aParent, aDialogTitle, aText, aPassword);
+ }
+
+ do_throw("promptPasswordPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ select(aParent, aDialogTitle, aText, aCount, aSelectList, aOutSelection) {
+ if (typeof selectPS == "function") {
+ // eslint-disable-next-line no-undef
+ return selectPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aCount,
+ aSelectList,
+ aOutSelection
+ );
+ }
+
+ do_throw("selectPS unexpectedly called: " + aText + "\n");
+ return false;
+ },
+
+ createInstance(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIPromptService",
+ "nsIPromptService2",
+ ]),
+};
+
+var alertUtilsWindowWatcher = {
+ getNewPrompter(aParent) {
+ return alertUtilsPrompts;
+ },
+
+ getNewAuthPrompter(aParent) {
+ return Cc["@mozilla.org/login-manager/authprompter;1"].getService(
+ Ci.nsIAuthPrompt
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIWindowWatcher"]),
+};
+
+// Special prompt that ensures we get prompted for logins. Calls
+// promptPasswordPS/promptUsernameAndPasswordPS directly, rather than through
+// the prompt service, because the function signature changed and no longer
+// allows a "save password" check box.
+let alertUtilsMsgAuthPrompt = {
+ QueryInterface: ChromeUtils.generateQI(["nsIAuthPrompt"]),
+
+ _getFormattedOrigin(aURI) {
+ let uri;
+ if (aURI instanceof Ci.nsIURI) {
+ uri = aURI;
+ } else {
+ uri = Services.io.newURI(aURI);
+ }
+
+ return uri.scheme + "://" + uri.displayHostPort;
+ },
+
+ _getRealmInfo(aRealmString) {
+ var httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString)) {
+ return [null, null, null];
+ }
+
+ var uri = Services.io.newURI(aRealmString);
+ var pathname = "";
+
+ if (uri.pathQueryRef != "/") {
+ pathname = uri.pathQueryRef;
+ }
+
+ var formattedOrigin = this._getFormattedOrigin(uri);
+
+ return [formattedOrigin, formattedOrigin + pathname, uri.username];
+ },
+
+ promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aUsername,
+ aPassword
+ ) {
+ var checkBox = { value: false };
+ var checkBoxLabel = null;
+ var [origin, realm] = this._getRealmInfo(aPasswordRealm);
+
+ if (typeof promptUsernameAndPasswordPS != "function") {
+ throw new Error(
+ "promptUsernameAndPasswordPS unexpectedly called: " + aText + "\n"
+ );
+ }
+
+ // eslint-disable-next-line no-undef
+ var ok = promptUsernameAndPasswordPS(
+ this._chromeWindow,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (!ok || !checkBox.value || !origin) {
+ return ok;
+ }
+
+ if (!aPassword.value) {
+ return ok;
+ }
+
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ aUsername.value,
+ aPassword.value
+ );
+ Services.logins.addLogin(newLogin);
+
+ return ok;
+ },
+
+ promptPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aPassword
+ ) {
+ var checkBox = { value: false };
+ var checkBoxLabel = null;
+ var [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
+
+ username = decodeURIComponent(username);
+
+ if (typeof promptPasswordPS != "function") {
+ throw new Error("promptPasswordPS unexpectedly called: " + aText + "\n");
+ }
+
+ // eslint-disable-next-line no-undef
+ var ok = promptPasswordPS(
+ this._chromeWindow,
+ aDialogTitle,
+ aText,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (ok && checkBox.value && origin && aPassword.value) {
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ username,
+ aPassword.value
+ );
+
+ Services.logins.addLogin(newLogin);
+ }
+
+ return ok;
+ },
+};
+
+function registerAlertTestUtils() {
+ MockRegistrar.register(
+ "@mozilla.org/embedcomp/window-watcher;1",
+ alertUtilsWindowWatcher
+ );
+ MockRegistrar.register(
+ "@mozilla.org/messenger/msgAuthPrompt;1",
+ alertUtilsMsgAuthPrompt
+ );
+ MockRegistrar.register("@mozilla.org/prompter;1", alertUtilsPromptService);
+ Services.prompt = alertUtilsPromptService;
+}
+
+var gDummyMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(
+ Ci.nsIMsgWindow
+);
diff --git a/comm/mailnews/test/resources/filterTestUtils.js b/comm/mailnews/test/resources/filterTestUtils.js
new file mode 100644
index 0000000000..c34d1147c0
--- /dev/null
+++ b/comm/mailnews/test/resources/filterTestUtils.js
@@ -0,0 +1,89 @@
+/* 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/. */
+
+// Utility functions for testing interactions with filters.
+
+var contains = Ci.nsMsgSearchOp.Contains;
+// This maps strings to a filter attribute (excluding the parameter)
+var ATTRIB_MAP = {
+ // Template : [attrib, op, field of value, otherHeader]
+ subject: [Ci.nsMsgSearchAttrib.Subject, contains, "str", null],
+ from: [Ci.nsMsgSearchAttrib.Sender, contains, "str", null],
+ date: [Ci.nsMsgSearchAttrib.Date, Ci.nsMsgSearchOp.Is, "date", null],
+ size: [Ci.nsMsgSearchAttrib.Size, Ci.nsMsgSearchOp.Is, "size", null],
+ "message-id": [
+ Ci.nsMsgSearchAttrib.OtherHeader + 1,
+ contains,
+ "str",
+ "Message-ID",
+ ],
+ "user-agent": [
+ Ci.nsMsgSearchAttrib.OtherHeader + 2,
+ contains,
+ "str",
+ "User-Agent",
+ ],
+};
+// And this maps strings to filter actions
+var ACTION_MAP = {
+ // Template : [action, auxiliary attribute field, auxiliary value]
+ priority: [Ci.nsMsgFilterAction.ChangePriority, "priority", 6],
+ delete: [Ci.nsMsgFilterAction.Delete],
+ read: [Ci.nsMsgFilterAction.MarkRead],
+ unread: [Ci.nsMsgFilterAction.MarkUnread],
+ kill: [Ci.nsMsgFilterAction.KillThread],
+ watch: [Ci.nsMsgFilterAction.WatchThread],
+ flag: [Ci.nsMsgFilterAction.MarkFlagged],
+ stop: [Ci.nsMsgFilterAction.StopExecution],
+ tag: [Ci.nsMsgFilterAction.AddTag, "strValue", "tag"],
+};
+
+/**
+ * Creates a filter and appends it to the nsIMsgFilterList.
+ *
+ * @param {nsIMsgFilter} list - An nsIMsgFilter to which the new filter will be appended.
+ * @param {string} trigger - A key of ATTRIB_MAP that represents the filter trigger.
+ * @param {string} value - The value of the filter trigger.
+ * @param {nsMsgFilterAction} action - A key of ACTION_MAP that represents the action to be taken.
+ */
+function createFilter(list, trigger, value, action) {
+ var filter = list.createFilter(trigger + action + "Test");
+ filter.filterType = Ci.nsMsgFilterType.NewsRule;
+
+ var searchTerm = filter.createTerm();
+ searchTerm.matchAll = false;
+ if (trigger in ATTRIB_MAP) {
+ let information = ATTRIB_MAP[trigger];
+ searchTerm.attrib = information[0];
+ if (information[3] != null) {
+ searchTerm.arbitraryHeader = information[3];
+ }
+ searchTerm.op = information[1];
+ var oldValue = searchTerm.value;
+ oldValue.attrib = information[0];
+ oldValue[information[2]] = value;
+ searchTerm.value = oldValue;
+ } else {
+ throw new Error("Unknown trigger " + trigger);
+ }
+ searchTerm.booleanAnd = true;
+ filter.appendTerm(searchTerm);
+
+ var filterAction = filter.createAction();
+ if (action in ACTION_MAP) {
+ let information = ACTION_MAP[action];
+ filterAction.type = information[0];
+ if (1 in information) {
+ filterAction[information[1]] = information[2];
+ }
+ } else {
+ throw new Error("Unknown action " + action);
+ }
+ filter.appendAction(filterAction);
+
+ filter.enabled = true;
+
+ // Add to the end
+ list.insertFilterAt(list.filterCount, filter);
+}
diff --git a/comm/mailnews/test/resources/folderEventLogHelper.js b/comm/mailnews/test/resources/folderEventLogHelper.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/comm/mailnews/test/resources/folderEventLogHelper.js
diff --git a/comm/mailnews/test/resources/logHelper.js b/comm/mailnews/test/resources/logHelper.js
new file mode 100644
index 0000000000..88a9616904
--- /dev/null
+++ b/comm/mailnews/test/resources/logHelper.js
@@ -0,0 +1,567 @@
+/* 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/. */
+
+/*
+ * Makes everything awesome if you are Andrew. Some day it will make everything
+ * awesome if you are not awesome too.
+ *
+ * Right now the most meaningful thing to know is that if XPCOM failures happen
+ * (and get reported to the error console), this will induce a unit test
+ * failure. You should think this is awesome no matter whether you are Andrew
+ * or not.
+ */
+
+// eslint-disable-next-line mozilla/reject-importGlobalProperties
+Cu.importGlobalProperties(["Element", "Node"]);
+
+var _mailnewsTestLogger;
+var _xpcshellLogger;
+var _testLoggerContexts = [];
+var _testLoggerContextId = 0;
+var _testLoggerActiveContext;
+
+var _logHelperInterestedListeners = false;
+
+/**
+ * Let test code extend the list of allowed XPCOM errors.
+ */
+var logHelperAllowedErrors = ["NS_ERROR_FAILURE"];
+var logHelperAllowedWarnings = [/Quirks Mode/];
+
+/**
+ * Let other test helping code decide whether to register for potentially
+ * expensive notifications based on whether anyone can even hear those
+ * results.
+ */
+function logHelperHasInterestedListeners() {
+ return _logHelperInterestedListeners;
+}
+
+/**
+ * Tunnel nsIScriptErrors that show up on the error console to ConsoleInstance.
+ * We could send everything but I think only script errors are likely of much
+ * concern. Also, this nicely avoids infinite recursions no matter what you do
+ * since what we publish is not going to end up as an nsIScriptError.
+ *
+ * This is based on my (asuth') exmmad extension.
+ */
+var _errorConsoleTunnel = {
+ initialize() {
+ Services.console.registerListener(this);
+
+ // we need to unregister our listener at shutdown if we don't want explosions
+ Services.obs.addObserver(this, "quit-application");
+ },
+
+ shutdown() {
+ Services.console.unregisterListener(this);
+ Services.obs.removeObserver(this, "quit-application");
+ },
+
+ observe(aMessage, aTopic, aData) {
+ if (aTopic == "quit-application") {
+ this.shutdown();
+ return;
+ }
+
+ try {
+ if (
+ aMessage instanceof Ci.nsIScriptError &&
+ !aMessage.errorMessage.includes("Error console says")
+ ) {
+ // Unfortunately changes to mozilla-central are throwing lots
+ // of console errors during testing, so disable (we hope temporarily)
+ // failing on XPCOM console errors (see bug 1014350).
+ // An XPCOM error aMessage looks like this:
+ // [JavaScript Error: "uncaught exception: 2147500037"]
+ // Capture the number, and allow known XPCOM results.
+ let matches = /JavaScript Error: "(\w+)/.exec(aMessage);
+ let XPCOMresult = null;
+ if (matches) {
+ for (let result in Cr) {
+ if (matches[1] == Cr[result]) {
+ XPCOMresult = result;
+ break;
+ }
+ }
+ let message = XPCOMresult || aMessage;
+ if (logHelperAllowedErrors.some(e => e == matches[1])) {
+ if (XPCOMresult) {
+ info("Ignoring XPCOM error: " + message);
+ }
+ return;
+ }
+ info("Found XPCOM error: " + message);
+ }
+ // Ignore warnings that match a white-listed pattern.
+ if (
+ /JavaScript Warning:/.test(aMessage) &&
+ logHelperAllowedWarnings.some(w => w.test(aMessage))
+ ) {
+ return;
+ }
+ dump(`Error console says: ${aMessage}`);
+ }
+ } catch (ex) {
+ // This is to avoid pathological error loops. we definitely do not
+ // want to propagate an error here.
+ }
+ },
+};
+
+// This defaults to undefined and is for use by test-folder-display-helpers
+// so that it can pre-initialize the value so that when we are evaluated in
+// its subscript loader we see a value of 'true'.
+var _do_not_wrap_xpcshell;
+
+/**
+ * Initialize logging. The idea is to:
+ *
+ * - Always create a dump appender on 'test'.
+ * - Check if there's a desire to use a logsploder style network connection
+ * based on the presence of an appropriate file in 'tmp'. This should be
+ * harmless in cases where there is not such a file.
+ *
+ * We will wrap the interesting xpcshell functions if we believe there is an
+ * endpoint that cares about these things (such as logsploder).
+ */
+function _init_log_helper() {
+ // - dump on test
+ _mailnewsTestLogger = console.createInstance({
+ prefix: "test.test",
+ });
+
+ // - silent category for xpcshell stuff that already gets dump()ed
+ _xpcshellLogger = console.createInstance({
+ prefix: "xpcshell",
+ });
+
+ // Create a console listener reporting thinger in all cases. Since XPCOM
+ // failures will show up via the error console, this allows our test to fail
+ // in more situations where we might otherwise silently be cool with bad
+ // things happening.
+ _errorConsoleTunnel.initialize();
+
+ if (_logHelperInterestedListeners) {
+ if (!_do_not_wrap_xpcshell) {
+ _wrap_xpcshell_functions();
+ }
+
+ // Send a message telling the listeners about the test file being run.
+ _xpcshellLogger.info({
+ _jsonMe: true,
+ _isContext: true,
+ _specialContext: "lifecycle",
+ _id: "start",
+ testFile: _TEST_FILE,
+ });
+ }
+}
+_init_log_helper();
+
+/**
+ * Mark the start of a test. This creates nice console output as well as
+ * setting up logging contexts so that use of other helpers in here
+ * get associated with the context.
+ *
+ * This will likely only be used by the test driver framework, such as
+ * asyncTestUtils.js. However, |mark_sub_test_start| is for user test code.
+ */
+function mark_test_start(aName, aParameter, aDepth) {
+ if (aDepth == null) {
+ aDepth = 0;
+ }
+
+ // clear out any existing contexts
+ mark_test_end(aDepth);
+
+ let term = aDepth == 0 ? "test" : "subtest";
+ _testLoggerActiveContext = {
+ type: term,
+ name: aName,
+ parameter: aParameter,
+ _id: ++_testLoggerContextId,
+ };
+ if (_testLoggerContexts.length) {
+ _testLoggerActiveContext._contextDepth = _testLoggerContexts.length;
+ _testLoggerActiveContext._contextParentId =
+ _testLoggerContexts[_testLoggerContexts.length - 1]._id;
+ }
+ _testLoggerContexts.push(_testLoggerActiveContext);
+
+ _mailnewsTestLogger.info(
+ _testLoggerActiveContext._id,
+ "Starting " + term + ": " + aName + (aParameter ? ", " + aParameter : "")
+ );
+}
+
+/**
+ * Mark the end of a test started by |mark_test_start|.
+ */
+function mark_test_end(aPopTo) {
+ if (aPopTo === undefined) {
+ aPopTo = 0;
+ }
+ // clear out any existing contexts
+ while (_testLoggerContexts.length > aPopTo) {
+ let context = _testLoggerContexts.pop();
+ _mailnewsTestLogger.info(
+ context._id,
+ "Finished " +
+ context.type +
+ ": " +
+ context.name +
+ (context.parameter ? ", " + context.parameter : "")
+ );
+ }
+}
+
+/**
+ * For user test code and test support code to mark sub-regions of tests.
+ *
+ * @param {string} aName The name of the (sub) test.
+ * @param {string} [aParameter=null] The parameter if the test is being parameterized.
+ * @param {boolean} [aNest=false] Should this nest inside other sub-tests?
+ * If you omit orpass false, we will close out any existing sub-tests.
+ * If you pass true, we nest inside the previous test/sub-test and rely on
+ * you to call |mark_sub_test_end|.
+ * Sub tests can lost no longer than their parent.
+ * You should strongly consider using the aNest parameter if you are test
+ * support code.
+ */
+function mark_sub_test_start(aName, aParameter, aNest) {
+ let depth = aNest ? _testLoggerContexts.length : 1;
+ mark_test_start(aName, aParameter, depth);
+}
+
+/**
+ * Mark the end of a sub-test. Because sub-tests can't outlive their parents,
+ * there is no ambiguity about what sub-test we are closing out.
+ */
+function mark_sub_test_end() {
+ if (_testLoggerContexts.length <= 1) {
+ return;
+ }
+ mark_test_end(_testLoggerContexts.length - 1);
+}
+
+/**
+ * Express that all tests were run to completion. This helps the listener
+ * distinguish between successful termination and abort-style termination where
+ * the process just keeled over and on one told us.
+ *
+ * This also tells us to clean up.
+ */
+function mark_all_tests_run() {
+ // make sure all tests get closed out
+ mark_test_end();
+
+ _xpcshellLogger.info("All finished");
+}
+
+function _explode_flags(aFlagWord, aFlagDefs) {
+ let flagList = [];
+
+ for (let flagName in aFlagDefs) {
+ let flagVal = aFlagDefs[flagName];
+ if (flagVal & aFlagWord) {
+ flagList.push(flagName);
+ }
+ }
+
+ return flagList;
+}
+
+var _registered_json_normalizers = [];
+
+/**
+ * Copy natives or objects, deferring to _normalize_for_json for objects.
+ */
+function __value_copy(aObj, aDepthAllowed) {
+ if (aObj == null || typeof aObj != "object") {
+ return aObj;
+ }
+ return _normalize_for_json(aObj, aDepthAllowed, true);
+}
+
+/**
+ * Simple object copier to limit accidentally JSON-ing a ridiculously complex
+ * object graph or getting tripped up by prototypes.
+ *
+ * @param {object} aObj - Input object.
+ * @param {integer} aDepthAllowed - How many times we are allowed to recursively
+ * call ourselves.
+ */
+function __simple_obj_copy(aObj, aDepthAllowed) {
+ let oot = {};
+ let nextDepth = aDepthAllowed - 1;
+ for (let key in aObj) {
+ // avoid triggering getters
+ if (aObj.__lookupGetter__(key)) {
+ oot[key] = "*getter*";
+ continue;
+ }
+ let value = aObj[key];
+
+ if (value == null) {
+ oot[key] = null;
+ } else if (typeof value != "object") {
+ oot[key] = value;
+ } else if (!aDepthAllowed) {
+ // steal control flow if no more depth is allowed
+ oot[key] = "truncated, string rep: " + value.toString();
+ } else if (Array.isArray(value)) {
+ // array? (not directly counted, but we will terminate because the
+ // child copying occurs using nextDepth...)
+ oot[key] = value.map(v => __value_copy(v, nextDepth));
+ } else {
+ // it's another object! woo!
+ oot[key] = _normalize_for_json(value, nextDepth, true);
+ }
+ }
+
+ // let's take advantage of the object's native toString now
+ oot._stringRep = aObj.toString();
+
+ return oot;
+}
+
+var _INTERESTING_MESSAGE_HEADER_PROPERTIES = {
+ "gloda-id": 0,
+ "gloda-dirty": 0,
+ junkscore: "",
+ junkscoreorigin: "",
+ msgOffset: 0,
+ offlineMsgSize: 0,
+};
+
+/**
+ * Given an object, attempt to normalize it into an interesting JSON
+ * representation.
+ *
+ * We transform generally interesting mail objects like:
+ * - nsIMsgFolder
+ * - nsIMsgDBHdr
+ */
+function _normalize_for_json(aObj, aDepthAllowed, aJsonMeNotNeeded) {
+ if (aDepthAllowed === undefined) {
+ aDepthAllowed = 2;
+ }
+
+ // if it's a simple type just return it direct
+ if (typeof aObj != "object") {
+ return aObj;
+ } else if (aObj == null) {
+ return aObj;
+ }
+
+ // recursively transform arrays outright
+ if (Array.isArray(aObj)) {
+ return aObj.map(v => __value_copy(v, aDepthAllowed - 1));
+ }
+
+ // === Mail Specific ===
+ // (but common and few enough to not split out)
+ if (aObj instanceof Ci.nsIMsgFolder) {
+ return {
+ type: "folder",
+ name: aObj.prettyName,
+ uri: aObj.URI,
+ flags: _explode_flags(aObj.flags, Ci.nsMsgFolderFlags),
+ };
+ } else if (aObj instanceof Ci.nsIMsgDBHdr) {
+ let properties = {};
+ for (let name in _INTERESTING_MESSAGE_HEADER_PROPERTIES) {
+ let propType = _INTERESTING_MESSAGE_HEADER_PROPERTIES[name];
+ if (propType === 0) {
+ properties[name] =
+ aObj.getStringProperty(name) != ""
+ ? aObj.getUint32Property(name)
+ : null;
+ } else {
+ properties[name] = aObj.getStringProperty(name);
+ }
+ }
+ return {
+ type: "msgHdr",
+ name: aObj.folder.URI + "#" + aObj.messageKey,
+ subject: aObj.mime2DecodedSubject,
+ from: aObj.mime2DecodedAuthor,
+ to: aObj.mime2DecodedRecipients,
+ messageKey: aObj.messageKey,
+ messageId: aObj.messageId,
+ flags: _explode_flags(aObj.flags, Ci.nsMsgMessageFlags),
+ interestingProperties: properties,
+ };
+ } else if (Node.isInstance(aObj)) {
+ // === Generic ===
+ // DOM nodes, including elements
+ let name = aObj.nodeName;
+ let objAttrs = {};
+
+ if (Element.isInstance(aObj)) {
+ name += "#" + aObj.getAttribute("id");
+ }
+
+ if ("attributes" in aObj) {
+ let nodeAttrs = aObj.attributes;
+ for (let iAttr = 0; iAttr < nodeAttrs.length; iAttr++) {
+ objAttrs[nodeAttrs[iAttr].name] = nodeAttrs[iAttr].value;
+ }
+ }
+
+ let bounds = { left: null, top: null, width: null, height: null };
+ if ("getBoundingClientRect" in aObj) {
+ bounds = aObj.getBoundingClientRect();
+ }
+
+ return {
+ type: "domNode",
+ name,
+ value: aObj.nodeValue,
+ namespace: aObj.namespaceURI,
+ boundingClientRect: bounds,
+ attrs: objAttrs,
+ };
+ } else if (aObj instanceof Ci.nsIDOMWindow) {
+ let winId, title;
+ if (aObj.document && aObj.document.documentElement) {
+ title = aObj.document.title;
+ winId =
+ aObj.document.documentElement.getAttribute("windowtype") ||
+ aObj.document.documentElement.getAttribute("id") ||
+ "unnamed";
+ } else {
+ winId = "n/a";
+ title = "no document";
+ }
+ return {
+ type: "domWindow",
+ id: winId,
+ title,
+ location: "" + aObj.location,
+ coords: { x: aObj.screenX, y: aObj.screenY },
+ dims: { width: aObj.outerWidth, height: aObj.outerHeight },
+ };
+ } else if (aObj instanceof Error) {
+ // Although straight JS exceptions should serialize pretty well, we can
+ // improve things by making "stack" more friendly.
+ return {
+ type: "error",
+ message: aObj.message,
+ fileName: aObj.fileName,
+ lineNumber: aObj.lineNumber,
+ name: aObj.name,
+ stack: aObj.stack ? aObj.stack.split(/\n\r?/g) : null,
+ _stringRep: aObj.message,
+ };
+ } else if (aObj instanceof Ci.nsIException) {
+ return {
+ type: "error",
+ message: "nsIException: " + aObj.name,
+ fileName: aObj.filename, // intentionally lower-case
+ lineNumber: aObj.lineNumber,
+ name: aObj.name,
+ result: aObj.result,
+ stack: null,
+ };
+ } else if (aObj instanceof Ci.nsIStackFrame) {
+ return {
+ type: "stackFrame",
+ name: aObj.name,
+ fileName: aObj.filename, // intentionally lower-case
+ lineNumber: aObj.lineNumber,
+ };
+ } else if (aObj instanceof Ci.nsIScriptError) {
+ return {
+ type: "stackFrame",
+ name: aObj.errorMessage,
+ category: aObj.category,
+ fileName: aObj.sourceName,
+ lineNumber: aObj.lineNumber,
+ };
+ }
+
+ for (let [checkType, handler] of _registered_json_normalizers) {
+ if (aObj instanceof checkType) {
+ return handler(aObj);
+ }
+ }
+
+ // Do not fall into simple object walking if this is an XPCOM interface.
+ // We might run across getters and that leads to nothing good.
+ if (aObj instanceof Ci.nsISupports) {
+ return {
+ type: "XPCOM",
+ name: aObj.toString(),
+ };
+ }
+
+ let simple_obj = __simple_obj_copy(aObj, aDepthAllowed);
+ if (!aJsonMeNotNeeded) {
+ simple_obj._jsonMe = true;
+ }
+ return simple_obj;
+}
+
+function register_json_normalizer(aType, aHandler) {
+ _registered_json_normalizers.push([aType, aHandler]);
+}
+
+/*
+ * Wrap the xpcshell test functions that do interesting things. The idea is
+ * that we clobber these only if we're going to value-add; that decision
+ * gets made up top in the initialization function.
+ *
+ * Since eq/neq fall-through to do_throw in the explosion case, we don't handle
+ * that since the scoping means that we're going to see the resulting
+ * do_throw.
+ */
+
+var _orig_do_throw;
+var _orig_do_check_neq;
+var _orig_do_check_eq;
+// do_check_true is implemented in terms of do_check_eq
+// do_check_false is implemented in terms of do_check_eq
+
+function _CheckAction(aSuccess, aLeft, aRight, aStack) {
+ this.type = "check";
+ this.success = aSuccess;
+ this.left = _normalize_for_json(aLeft);
+ this.right = _normalize_for_json(aRight);
+ this.stack = _normalize_for_json(aStack);
+}
+_CheckAction.prototype = {
+ _jsonMe: true,
+ // we don't need a toString because we should not go out to the console
+};
+
+/**
+ * Representation of a failure from do_throw.
+ */
+function _Failure(aText, aStack) {
+ this.type = "failure";
+ this.text = aText;
+ this.stack = _normalize_for_json(aStack);
+}
+_Failure.prototype = {
+ _jsonMe: true,
+};
+
+function _wrapped_do_throw(text, stack) {
+ if (!stack) {
+ stack = Components.stack.caller;
+ }
+
+ // We need to use an info because otherwise explosion loggers can get angry
+ // and they may be indiscriminate about what they subscribe to.
+ _xpcshellLogger.info(_testLoggerActiveContext, new _Failure(text, stack));
+
+ return _orig_do_throw(text, stack);
+}
+
+function _wrap_xpcshell_functions() {
+ _orig_do_throw = do_throw;
+ do_throw = _wrapped_do_throw; // eslint-disable-line no-global-assign
+}
diff --git a/comm/mailnews/test/resources/mailShutdown.js b/comm/mailnews/test/resources/mailShutdown.js
new file mode 100644
index 0000000000..0a5a5aa092
--- /dev/null
+++ b/comm/mailnews/test/resources/mailShutdown.js
@@ -0,0 +1,51 @@
+/* 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/. */
+
+/* Provides methods to make sure our test shuts down mailnews properly. */
+
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+// Notifies everyone that the we're shutting down. This is needed to make sure
+// that e.g. the account manager closes and cleans up correctly. It is semi-fake
+// because we don't actually do any work to make sure the profile goes away, but
+// it will mimic the behaviour in the app sufficiently.
+//
+// See also http://developer.mozilla.org/en/Observer_Notifications
+function postShutdownNotifications() {
+ // first give everyone a heads up about us shutting down. if someone wants
+ // to cancel this, our test should fail.
+ var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested");
+ if (cancelQuit.data) {
+ do_throw("Cannot shutdown: Someone cancelled the quit request!");
+ }
+
+ // post all notifications in the right order. none of these are cancellable
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNCONFIRMED
+ );
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNNETTEARDOWN
+ );
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWNTEARDOWN
+ );
+ Services.startup.advanceShutdownPhase(
+ Services.startup.SHUTDOWN_PHASE_APPSHUTDOWN
+ );
+
+ // finally, the xpcom-shutdown notification is handled by XPCOM itself.
+}
+
+MockRegistrar.unregisterAll();
+
+// First do a gc to let anything not being referenced be cleaned up.
+gc();
+
+// Now shut everything down.
+postShutdownNotifications();
diff --git a/comm/mailnews/test/resources/msgFolderListenerSetup.js b/comm/mailnews/test/resources/msgFolderListenerSetup.js
new file mode 100644
index 0000000000..72eaedb7a3
--- /dev/null
+++ b/comm/mailnews/test/resources/msgFolderListenerSetup.js
@@ -0,0 +1,429 @@
+/* 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/. */
+
+// ChromeUtils.import should be used for this, but it breaks mozmill.
+// Assume whatever test loaded this file already has mailTestUtils.
+/* globals mailTestUtils */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var allTestedEvents =
+ MailServices.mfn.msgAdded |
+ MailServices.mfn.msgsClassified |
+ MailServices.mfn.msgsJunkStatusChanged |
+ MailServices.mfn.msgsDeleted |
+ MailServices.mfn.msgsMoveCopyCompleted |
+ MailServices.mfn.msgKeyChanged |
+ MailServices.mfn.msgUnincorporatedMoved |
+ MailServices.mfn.folderAdded |
+ MailServices.mfn.folderDeleted |
+ MailServices.mfn.folderMoveCopyCompleted |
+ MailServices.mfn.folderRenamed |
+ MailServices.mfn.folderCompactStart |
+ MailServices.mfn.folderCompactFinish |
+ MailServices.mfn.folderReindexTriggered;
+
+// Current test being executed
+var gTest = 1;
+
+// Which events are expected
+var gExpectedEvents = [];
+
+// The current status (what all has been done)
+var gCurrStatus = 0;
+var kStatus = {
+ notificationsDone: 0x1,
+ onStopCopyDone: 0x2,
+ functionCallDone: 0x4,
+ everythingDone: 0,
+};
+kStatus.everythingDone =
+ kStatus.notificationsDone | kStatus.onStopCopyDone | kStatus.functionCallDone;
+
+// For copyFileMessage: this stores the header that was received
+var gHdrsReceived = [];
+
+var gMsgHdrs = [];
+
+// Our listener, which captures events and verifies them as they are received.
+var gMFListener = {
+ msgAdded(aMsg) {
+ verify([MailServices.mfn.msgAdded, aMsg]);
+ // We might not actually have a header in gHdrsReceived in the IMAP case,
+ // so use the aMsg we got instead
+ gMsgHdrs.push({ hdr: aMsg, ID: aMsg.messageId });
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ } else if (gExpectedEvents[0][0] == MailServices.mfn.msgsClassified) {
+ // XXX this is a hack to deal with limitations of the classification logic
+ // and the new list. We want to issue a call to clear the list once all
+ // the messages have been added, which would be when the next expected
+ // event is msgsClassified. (The limitation is that if we don't do this,
+ // we can end up getting told about this message again later.)
+ aMsg.folder.clearNewMessages();
+ }
+ },
+
+ msgsClassified(aMsgs, aJunkProcessed, aTraitProcessed) {
+ dump("classified id: " + aMsgs[0].messageId + "\n");
+ verify([
+ MailServices.mfn.msgsClassified,
+ aMsgs,
+ aJunkProcessed,
+ aTraitProcessed,
+ ]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgsJunkStatusChanged(messages) {
+ verify([MailServices.mfn.msgsJunkStatusChanged, messages]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgsDeleted(aMsgs) {
+ verify([MailServices.mfn.msgsDeleted, aMsgs]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder, aDestMsgs) {
+ verify([
+ MailServices.mfn.msgsMoveCopyCompleted,
+ aMove,
+ aSrcMsgs,
+ aDestFolder,
+ aDestMsgs,
+ ]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgKeyChanged(aOldKey, aNewMsgHdr) {
+ verify([MailServices.mfn.msgKeyChanged, aOldKey, aNewMsgHdr]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ msgUnincorporatedMoved(srcFolder, msg) {
+ verify([MailServices.mfn.msgUnincorporatedMoved, srcFolder, msg]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderAdded(aFolder) {
+ verify([MailServices.mfn.folderAdded, aFolder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderDeleted(aFolder) {
+ verify([MailServices.mfn.folderDeleted, aFolder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
+ verify([
+ MailServices.mfn.folderMoveCopyCompleted,
+ aMove,
+ aSrcFolder,
+ aDestFolder,
+ ]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderRenamed(aOrigFolder, aNewFolder) {
+ verify([MailServices.mfn.folderRenamed, aOrigFolder, aNewFolder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderCompactStart(folder) {
+ verify([MailServices.mfn.folderCompactStart, folder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderCompactFinish(folder) {
+ verify([MailServices.mfn.folderCompactFinish, folder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+
+ folderReindexTriggered(folder) {
+ verify([MailServices.mfn.folderReindexTriggered, folder]);
+ if (gExpectedEvents.length == 0) {
+ gCurrStatus |= kStatus.notificationsDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ }
+ },
+};
+
+// Copy listener, for proceeding after each operation.
+var copyListener = {
+ // For copyFileMessage: this should be the folder the message is being stored to
+ mFolderStoredIn: null,
+ mMessageId: "",
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ gHdrsReceived.push(this.mFolderStoredIn.GetMessageHeader(aKey));
+ },
+ GetMessageId(aMessageId) {
+ aMessageId = { value: this.mMessageId };
+ },
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ gCurrStatus |= kStatus.onStopCopyDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+ },
+};
+
+function resetStatusAndProceed() {
+ gHdrsReceived.length = 0;
+ gCurrStatus = 0;
+ // Ugly hack: make sure we don't get stuck in a JS->C++->JS->C++... call stack
+ // This can happen with a bunch of synchronous functions grouped together, and
+ // can even cause tests to fail because they're still waiting for the listener
+ // to return
+ do_timeout(0, () => {
+ this.doTest(++gTest);
+ });
+}
+
+// Checks whether the array returned from a function has exactly these elements.
+function hasExactlyElements(array, elements) {
+ // If an nsIArray (it could also be a single header or a folder)
+ if (elements instanceof Ci.nsIArray) {
+ var count = elements.length;
+
+ // Check: array sizes should be equal.
+ Assert.equal(count, array.length);
+
+ for (let i = 0; i < count; i++) {
+ // Check: query element, must be a header or folder and present in the array
+ var currElement;
+ try {
+ currElement = elements.queryElementAt(i, Ci.nsIMsgDBHdr);
+ } catch (e) {}
+ if (!currElement) {
+ try {
+ currElement = elements.queryElementAt(i, Ci.nsIMsgFolder);
+ } catch (e) {}
+ }
+ Assert.equal(typeof currElement, "object");
+ Assert.notEqual(
+ mailTestUtils.non_strict_index_of(array, currElement),
+ -1
+ );
+ }
+ } else if (Array.isArray(elements)) {
+ Assert.equal(elements.length, array.length);
+ for (let el of elements) {
+ Assert.equal(typeof el, "object");
+ Assert.equal(
+ el instanceof Ci.nsIMsgDBHdr || el instanceof Ci.nsIMsgFolder,
+ true
+ );
+ Assert.notEqual(mailTestUtils.non_strict_index_of(array, el), -1);
+ }
+ } else if (
+ elements instanceof Ci.nsIMsgDBHdr ||
+ elements instanceof Ci.nsIMsgFolder
+ ) {
+ // If a single header or a folder
+
+ // Check: there should be only one element in the array.
+ Assert.equal(array.length, 1);
+
+ // Check: the element should be present
+ Assert.notEqual(mailTestUtils.non_strict_index_of(array, elements), -1);
+ } else {
+ // This shouldn't happen
+ do_throw("Unrecognized item returned from listener");
+ }
+}
+
+// Verifies an event
+function verify(event) {
+ // Check: make sure we actually have an item to process
+ Assert.ok(gExpectedEvents.length >= 1);
+ var expected = gExpectedEvents.shift();
+
+ // Check: events match.
+ var eventType = expected[0];
+ Assert.equal(event[0], eventType);
+
+ dump("..... Verifying event type " + eventType + "\n");
+
+ switch (eventType) {
+ case MailServices.mfn.msgAdded:
+ // So for IMAP right now, we aren't able to get the actual nsIMsgDBHdr.
+ // Instead, we'll match up message ids as a (poor?) substitute.
+ if (expected[1].expectedMessageId) {
+ Assert.equal(expected[1].expectedMessageId, event[1].messageId);
+ break;
+ }
+ // If we do have a header, fall through to the case below
+ case MailServices.mfn.msgsDeleted:
+ case MailServices.mfn.folderDeleted:
+ // Check: headers match/folder matches.
+ hasExactlyElements(expected[1], event[1]);
+ break;
+ case MailServices.mfn.msgsClassified:
+ // In the IMAP case expected[1] is a list of mesage-id strings whereas in
+ // the local case (where we are copying from files), we actually have
+ // the headers.
+ if (typeof expected[1][0] == "string") {
+ // IMAP; message id strings
+ // The IMAP case has additional complexity in that the 'new message'
+ // list is not tailored to our needs and so may over-report about
+ // new messagse. So to deal with this we make sure the msgsClassified
+ // event is telling us about at least the N expected events and that
+ // the last N of these events match
+ if (event[1].length < expected[1].length) {
+ do_throw("Not enough reported classified messages.");
+ }
+ let ignoreCount = event[1].length - expected[1].length;
+ for (let i = 0; i < expected[1].length; i++) {
+ let eventHeader = event[1][i + ignoreCount];
+ Assert.equal(expected[1][i], eventHeader.messageId);
+ }
+ } else {
+ // actual headers
+ hasExactlyElements(expected[1], event[1]);
+ }
+ // aJunkProcessed: was the message processed for junk?
+ Assert.equal(expected[2], event[2]);
+ // aTraitProcessed: was the message processed for traits?
+ Assert.equal(expected[3], event[3]);
+ break;
+ case MailServices.mfn.msgsJunkStatusChanged:
+ // Check: same messages?
+ hasExactlyElements(expected[1], event[1]);
+ break;
+ case MailServices.mfn.msgKeyChanged:
+ Assert.equal(expected[1].expectedMessageId, event[2].messageId);
+ break;
+ case MailServices.mfn.msgUnincorporatedMoved:
+ // Check: Same folder?
+ Assert.equal(expected[1].URI, event[1].URI);
+ // Check: message matches?
+ hasExactlyElements(expected[2], event[2]);
+ break;
+ case MailServices.mfn.msgsMoveCopyCompleted:
+ case MailServices.mfn.folderMoveCopyCompleted:
+ // Check: Move or copy as expected.
+ Assert.equal(expected[1], event[1]);
+
+ // Check: headers match/folder matches.
+ hasExactlyElements(expected[2], event[2]);
+
+ // Check: destination folder matches.
+ Assert.equal(expected[3].URI, event[3].URI);
+
+ if (eventType == MailServices.mfn.folderMoveCopyCompleted) {
+ break;
+ }
+
+ // Check: destination headers. We expect these for local and imap folders,
+ // but we will not have heard about the headers ahead of time,
+ // so the best we can do is make sure they match up. To this end,
+ // we check that the message-id header values match up.
+ for (let iMsg = 0; iMsg < event[2].length; iMsg++) {
+ let srcHdr = event[2][iMsg];
+ let destHdr = event[4][iMsg];
+ Assert.equal(srcHdr.messageId, destHdr.messageId);
+ }
+ break;
+ case MailServices.mfn.folderAdded:
+ // Check: parent folder matches
+ Assert.equal(expected[1].URI, event[1].parent.URI);
+
+ // Check: folder name matches
+ Assert.equal(expected[2], event[1].prettyName);
+ Assert.equal(expected[2], event[1].name);
+
+ // Not a check, but call the passed in callback with the new folder,
+ // used e.g. to store this folder somewhere.
+ if (expected[3]) {
+ expected[3](event[1]);
+ }
+ break;
+ case MailServices.mfn.folderRenamed:
+ // Check: source folder matches
+ hasExactlyElements(expected[1], event[1]);
+
+ // Check: destination folder name matches
+ Assert.equal(expected[2], event[2].prettyName);
+ break;
+ case MailServices.mfn.folderCompactStart:
+ case MailServices.mfn.folderCompactFinish:
+ case MailServices.mfn.folderReindexTriggered:
+ // Check: same folder?
+ Assert.equal(expected[1].URI, event[1].URI);
+ break;
+ }
+}
diff --git a/comm/mailnews/test/resources/passwordStorage.js b/comm/mailnews/test/resources/passwordStorage.js
new file mode 100644
index 0000000000..26db74c5a7
--- /dev/null
+++ b/comm/mailnews/test/resources/passwordStorage.js
@@ -0,0 +1,23 @@
+/* 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/. */
+
+/* globals gDEPTH */
+
+if (typeof gDEPTH == "undefined") {
+ do_throw("gDEPTH must be defined when using passwordStorage.js");
+}
+
+/**
+ * Use the given storage database as the current signon database.
+ *
+ * @returns {Promise} Promive for when the storage database is usable.
+ */
+function setupForPassword(storageName) {
+ let keyDB = do_get_file(gDEPTH + "mailnews/data/key4.db");
+ keyDB.copyTo(do_get_profile(), "key4.db");
+
+ let signons = do_get_file(gDEPTH + "mailnews/data/" + storageName);
+ signons.copyTo(do_get_profile(), "logins.json");
+ return Services.logins.initializationPromise;
+}
diff --git a/comm/mailnews/test/resources/searchTestUtils.js b/comm/mailnews/test/resources/searchTestUtils.js
new file mode 100644
index 0000000000..df26824bca
--- /dev/null
+++ b/comm/mailnews/test/resources/searchTestUtils.js
@@ -0,0 +1,134 @@
+/* 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/. */
+
+// Contains various functions commonly used in testing mailnews search.
+
+/**
+ * TestSearch: Class to test number of search hits
+ *
+ * @param {nsIMsgFolder} aFolder - The folder to search
+ * @param {string|integer} aValue - value used for the search
+ * The interpretation of aValue depends on aAttrib. It
+ * defaults to string, but for certain attributes other
+ * types are used.
+ * WARNING: not all attributes have been tested.
+ *
+ * @param {nsMsgSearchAttrib} aAttrib - Attribute for the search (Ci.nsMsgSearchAttrib.Size, etc.)
+ * @param {nsMsgSearchOp} aOp - Operation for the search (Ci.nsMsgSearchOp.Contains, etc.)
+ * @param {integer} aHitCount - Expected number of search hits
+ * @param {Function} onDone - Function to call on completion of search
+ * @param {string} aCustomId - Id string for the custom action, if aAttrib is Custom
+ * @param {string} aArbitraryHeader - For OtherHeader case, header.
+ * @param {string|integer} aHdrProperty - For HdrProperty and Uint32HdrProperty case
+ *
+ */
+function TestSearch(
+ aFolder,
+ aValue,
+ aAttrib,
+ aOp,
+ aHitCount,
+ onDone,
+ aCustomId,
+ aArbitraryHeader,
+ aHdrProperty
+) {
+ var searchListener = {
+ onSearchHit(dbHdr, folder) {
+ hitCount++;
+ },
+ onSearchDone(status) {
+ print("Finished search does " + aHitCount + " equal " + hitCount + "?");
+ searchSession = null;
+ Assert.equal(aHitCount, hitCount);
+ if (onDone) {
+ onDone();
+ }
+ },
+ onNewSearch() {
+ hitCount = 0;
+ },
+ };
+
+ // define and initiate the search session
+
+ var hitCount;
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, aFolder);
+ var searchTerm = searchSession.createTerm();
+ searchTerm.attrib = aAttrib;
+
+ var value = searchTerm.value;
+ // This is tricky - value.attrib must be set before actual values
+ value.attrib = aAttrib;
+ if (aAttrib == Ci.nsMsgSearchAttrib.JunkPercent) {
+ value.junkPercent = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Priority) {
+ value.priority = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Date) {
+ value.date = aValue;
+ } else if (
+ aAttrib == Ci.nsMsgSearchAttrib.MsgStatus ||
+ aAttrib == Ci.nsMsgSearchAttrib.FolderFlag ||
+ aAttrib == Ci.nsMsgSearchAttrib.Uint32HdrProperty
+ ) {
+ value.status = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.MessageKey) {
+ value.msgKey = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.Size) {
+ value.size = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.AgeInDays) {
+ value.age = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.JunkStatus) {
+ value.junkStatus = aValue;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.HasAttachmentStatus) {
+ value.status = Ci.nsMsgMessageFlags.Attachment;
+ } else {
+ value.str = aValue;
+ }
+ searchTerm.value = value;
+ searchTerm.op = aOp;
+ searchTerm.booleanAnd = false;
+ if (aAttrib == Ci.nsMsgSearchAttrib.Custom) {
+ searchTerm.customId = aCustomId;
+ } else if (aAttrib == Ci.nsMsgSearchAttrib.OtherHeader) {
+ searchTerm.arbitraryHeader = aArbitraryHeader;
+ } else if (
+ aAttrib == Ci.nsMsgSearchAttrib.HdrProperty ||
+ aAttrib == Ci.nsMsgSearchAttrib.Uint32HdrProperty
+ ) {
+ searchTerm.hdrProperty = aHdrProperty;
+ }
+
+ searchSession.appendTerm(searchTerm);
+ searchSession.registerListener(searchListener);
+ searchSession.search(null);
+}
+
+/*
+ * Test search validity table Available and Enabled settings
+ *
+ * @param aScope: search scope (Ci.nsMsgSearchScope.offlineMail, etc.)
+ * @param aOp: search operation (Ci.nsMsgSearchOp.Contains, etc.)
+ * @param aAttrib: search attribute (Ci.nsMsgSearchAttrib.Size, etc.)
+ * @param aValue: expected value (true/false) for Available and Enabled
+ */
+const gValidityManager = Cc[
+ "@mozilla.org/mail/search/validityManager;1"
+].getService(Ci.nsIMsgSearchValidityManager);
+
+function testValidityTable(aScope, aOp, aAttrib, aValue) {
+ var validityTable = gValidityManager.getTable(aScope);
+ var isAvailable = validityTable.getAvailable(aAttrib, aOp);
+ var isEnabled = validityTable.getEnabled(aAttrib, aOp);
+ if (aValue) {
+ Assert.ok(isAvailable);
+ Assert.ok(isEnabled);
+ } else {
+ Assert.ok(!isAvailable);
+ Assert.ok(!isEnabled);
+ }
+}
diff --git a/comm/mailnews/test/resources/smimeUtils.jsm b/comm/mailnews/test/resources/smimeUtils.jsm
new file mode 100644
index 0000000000..db7cf2e5c9
--- /dev/null
+++ b/comm/mailnews/test/resources/smimeUtils.jsm
@@ -0,0 +1,71 @@
+/* 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 provides some utilities for helping run S/MIME tests.
+ */
+
+var EXPORTED_SYMBOLS = ["SmimeUtils"];
+
+var { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+const gCertDialogs = {
+ confirmDownloadCACert: (ctx, cert, trust) => {
+ dump("Requesting certificate download\n");
+ trust.value = Ci.nsIX509CertDB.TRUSTED_EMAIL;
+ return true;
+ },
+ setPKCS12FilePassword: (ctx, password) => {
+ throw new Error("Not implemented");
+ },
+ getPKCS12FilePassword: (ctx, password) => {
+ password.value = "";
+ return true;
+ },
+ viewCert: (ctx, cert) => {
+ throw new Error("Not implemented");
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsICertificateDialogs"]),
+};
+
+var SmimeUtils = {
+ ensureNSS() {
+ // Ensure NSS is initialized.
+ Cc["@mozilla.org/psm;1"].getService(Ci.nsISupports);
+
+ // Set up the internal key token so that subsequent code doesn't fail. If
+ // this isn't done, we'll fail to work if the NSS databases didn't already
+ // exist.
+ let keydb = Cc["@mozilla.org/security/pk11tokendb;1"].getService(
+ Ci.nsIPK11TokenDB
+ );
+ try {
+ keydb.getInternalKeyToken().initPassword("");
+ } catch (e) {
+ // In this scenario, the key token already had its password initialized.
+ // Therefore, we don't need to do anything (assuming its password is
+ // empty).
+ }
+
+ MockRegistrar.register("@mozilla.org/nsCertificateDialogs;1", gCertDialogs);
+ },
+
+ loadPEMCertificate(file, certType, loadKey = false) {
+ dump("Loading certificate from " + file.path + "\n");
+ let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ certDB.importCertsFromFile(file, certType);
+ },
+
+ loadCertificateAndKey(file, pw) {
+ dump("Loading key from " + file.path + "\n");
+ let certDB = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+ );
+ certDB.importPKCS12File(file, pw);
+ },
+};