summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/test/resources/MessageInjection.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/test/resources/MessageInjection.jsm')
-rw-r--r--comm/mailnews/test/resources/MessageInjection.jsm987
1 files changed, 987 insertions, 0 deletions
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;
+ }
+}