diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/test/resources | |
parent | Initial commit. (diff) | |
download | thunderbird-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 '')
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); + }, +}; |