/* 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} 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; } }