/* 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/. */ var EXPORTED_SYMBOLS = ["MailUtils"]; var Cc = Components.classes; var Ci = Components.interfaces; Components.utils.import("resource:///modules/iteratorUtils.jsm"); Components.utils.import("resource:///modules/MailConsts.js"); Components.utils.import("resource:///modules/mailServices.js"); Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/PluralForm.jsm"); var MC = MailConsts; /** * This module has several utility functions for use by both core and * third-party code. Some functions are aimed at code that doesn't have a * window context, while others can be used anywhere. */ var MailUtils = { /** * Discover all folders. This is useful during startup, when you have code * that deals with folders and that executes before the main 3pane window is * open (the folder tree wouldn't have been initialized yet). */ discoverFolders: function MailUtils_discoverFolders() { let servers = MailServices.accounts.allServers; for (let server in fixIterator(servers, Ci.nsIMsgIncomingServer)) { // Bug 466311 Sometimes this can throw file not found, we're unsure // why, but catch it and log the fact. try { server.rootFolder.subFolders; } catch (ex) { Services.console.logStringMessage("Discovering folders for account failed with " + "exception: " + ex); } } }, /** * Get the nsIMsgFolder corresponding to this file. This just looks at all * folders and does a direct match. * * One of the places this is used is desktop search integration -- to open * the search result corresponding to a mozeml/wdseml file, we need to figure * out the folder using the file's path. * * @param aFile the nsILocalFile to convert to a folder * @returns the nsIMsgFolder corresponding to aFile, or null if the folder * isn't found */ getFolderForFileInProfile: function MailUtils_getFolderForFileInProfile(aFile) { let folders = MailServices.accounts.allFolders; for (let folder in fixIterator(folders, Ci.nsIMsgFolder)) { if (folder.filePath.equals(aFile)) return folder; } return null; }, /** * Get the nsIMsgFolder corresponding to this URI. This uses the RDF service * to do the work. * * @param aFolderURI the URI to convert into a folder * @param aCheckFolderAttributes whether to check that the folder either has * a parent or isn't a server * @returns the nsIMsgFolder corresponding to this URI, or null if * aCheckFolderAttributes is true and the folder doesn't have a * parent or is a server */ getFolderForURI: function MailUtils_getFolderForURI(aFolderURI, aCheckFolderAttributes) { let folder = null; let rdfService = Cc['@mozilla.org/rdf/rdf-service;1'] .getService(Ci.nsIRDFService); folder = rdfService.GetResource(aFolderURI); // This is going to QI the folder to an nsIMsgFolder as well if (folder && folder instanceof Ci.nsIMsgFolder) { if (aCheckFolderAttributes && !(folder.parent || folder.isServer)) return null; } else { return null; } return folder; }, /** * Display this message header in a new tab, a new window or an existing * window, depending on the preference and whether a 3pane or standalone * window is already open. This function should be called when you'd like to * display a message to the user according to the pref set. * * @note Do not use this if you want to open multiple messages at once. Use * |displayMessages| instead. * * @param aMsgHdr the message header to display * @param [aViewWrapperToClone] a view wrapper to clone. If null or not * given, the message header's folder's default * view will be used * @param [aTabmail] a tabmail element to use in case we need to open tabs. * If null or not given: * - if one or more 3pane windows are open, the most recent * one's tabmail is used * - if no 3pane windows are open, a standalone window is * opened instead of a tab */ displayMessage: function MailUtils_displayMessage(aMsgHdr, aViewWrapperToClone, aTabmail) { this.displayMessages([aMsgHdr], aViewWrapperToClone, aTabmail); }, /** * Display the warning if the number of messages to be displayed is greater than * the limit set in preferences. * @param aNumMessages: number of messages to be displayed * @param aConfirmTitle: title ID * @param aConfirmMsg: message ID * @param aLiitingPref: the name of the pref to retrieve the limit from */ confirmAction: function (aNumMessages, aConfirmTitle, aConfirmMsg, aLimitingPref) { let openWarning = Services.prefs.getIntPref(aLimitingPref); if ((openWarning > 1) && (aNumMessages >= openWarning)) { let bundle = Services.strings.createBundle( "chrome://messenger/locale/messenger.properties"); let title = bundle.GetStringFromName(aConfirmTitle); let message = PluralForm.get(aNumMessages, bundle.GetStringFromName(aConfirmMsg)) .replace("#1", aNumMessages); if (!Services.prompt.confirm(null, title, message)) return true; } return false; }, /** * Display these message headers in new tabs, new windows or existing * windows, depending on the preference, the number of messages, and whether * a 3pane or standalone window is already open. This function should be * called when you'd like to display multiple messages to the user according * to the pref set. * * @param aMsgHdrs an array containing the message headers to display. The * array should contain at least one message header * @param [aViewWrapperToClone] a DB view wrapper to clone for each of the * tabs or windows * @param [aTabmail] a tabmail element to use in case we need to open tabs. * If given, the window containing the tabmail is assumed * to be in front. If null or not given: * - if one or more 3pane windows are open, the most recent * one's tabmail is used, and the window is brought to the * front * - if no 3pane windows are open, standalone windows are * opened instead of tabs */ displayMessages: function MailUtils_displayMessages(aMsgHdrs, aViewWrapperToClone, aTabmail) { let openMessageBehavior = Services.prefs.getIntPref( "mail.openMessageBehavior"); if (openMessageBehavior == MC.OpenMessageBehavior.NEW_WINDOW) { this.openMessagesInNewWindows(aMsgHdrs, aViewWrapperToClone); } else if (openMessageBehavior == MC.OpenMessageBehavior.EXISTING_WINDOW) { // Try reusing an existing window. If we can't, fall back to opening new // windows if (aMsgHdrs.length > 1 || !this.openMessageInExistingWindow(aMsgHdrs[0])) this.openMessagesInNewWindows(aMsgHdrs, aViewWrapperToClone); } else if (openMessageBehavior == MC.OpenMessageBehavior.NEW_TAB) { let mail3PaneWindow = null; if (!aTabmail) { // Try opening new tabs in a 3pane window mail3PaneWindow = Services.wm.getMostRecentWindow("mail:3pane"); if (mail3PaneWindow) aTabmail = mail3PaneWindow.document.getElementById("tabmail"); } if (aTabmail) { if (this.confirmAction(aMsgHdrs.length, "openTabWarningTitle", "openTabWarningConfirmation", "mailnews.open_tab_warning")) return; for (let [i, msgHdr] of aMsgHdrs.entries()) // Open all the tabs in the background, except for the last one aTabmail.openTab("message", {msgHdr: msgHdr, viewWrapperToClone: aViewWrapperToClone, background: (i < (aMsgHdrs.length - 1)), disregardOpener: (aMsgHdrs.length > 1), }); if (mail3PaneWindow) mail3PaneWindow.focus(); } else { // We still haven't found a tabmail, so we'll need to open new windows this.openMessagesInNewWindows(aMsgHdrs, aViewWrapperToClone); } } }, /** * Show this message in an existing window. * * @param aMsgHdr the message header to display * @param [aViewWrapperToClone] a DB view wrapper to clone for the message * window * @returns true if an existing window was found and the message header was * displayed, false otherwise */ openMessageInExistingWindow: function MailUtils_openMessageInExistingWindow(aMsgHdr, aViewWrapperToClone) { let messageWindow = Services.wm.getMostRecentWindow("mail:messageWindow"); if (messageWindow) { messageWindow.displayMessage(aMsgHdr, aViewWrapperToClone); return true; } return false; }, /** * Open a new standalone message window with this header. * * @param aMsgHdr the message header to display * @param [aViewWrapperToClone] a DB view wrapper to clone for the message * window */ openMessageInNewWindow: function MailUtils_openMessageInNewWindow(aMsgHdr, aViewWrapperToClone) { // It sucks that we have to go through XPCOM for this let args = {msgHdr: aMsgHdr, viewWrapperToClone: aViewWrapperToClone}; args.wrappedJSObject = args; Services.ww.openWindow(null, "chrome://messenger/content/messageWindow.xul", "", "all,chrome,dialog=no,status,toolbar", args); }, /** * Open new standalone message windows for these headers. This will prompt * for confirmation if the number of windows to be opened is greater than the * value of the mailnews.open_window_warning preference. * * @param aMsgHdrs an array containing the message headers to display * @param [aViewWrapperToClone] a DB view wrapper to clone for each message * window */ openMessagesInNewWindows: function MailUtils_openMessagesInNewWindows(aMsgHdrs, aViewWrapperToClone) { if (this.confirmAction(aMsgHdrs.length, "openWindowWarningTitle", "openWindowWarningConfirmation", "mailnews.open_window_warning")) return; for (let msgHdr of aMsgHdrs) this.openMessageInNewWindow(msgHdr, aViewWrapperToClone); }, /** * Display this message header in a folder tab in a 3pane window. This is * useful when the message needs to be displayed in the context of its folder * or thread. * * @param aMsgHdr the message header to display */ displayMessageInFolderTab: function MailUtils_displayMessageInFolderTab( aMsgHdr) { // Try opening new tabs in a 3pane window let mail3PaneWindow = Services.wm.getMostRecentWindow("mail:3pane"); if (mail3PaneWindow) { mail3PaneWindow.MsgDisplayMessageInFolderTab(aMsgHdr); } else { let args = {msgHdr: aMsgHdr}; args.wrappedJSObject = args; Services.ww.openWindow(null, "chrome://messenger/content/", "", "all,chrome,dialog=no,status,toolbar", args); } }, /** * The number of milliseconds to wait between loading of folders in * |setStringPropertyOnFolderAndDescendents|. We wait at all because * opening msf databases is a potentially expensive synchronous operation that * can approach the order of a second in pathological cases like gmail's * all mail folder. * * If we did not use a timer or otherwise spin the event loop we would * completely lock up the UI. In theory we would still maintain some degree * of UI responsiveness if we just used postMessage to break up our work so * that the event loop still got a chance to run between our folder openings. * The use of any delay between processing folders is to try and avoid causing * system-wide interactivity problems from dominating the system's available * disk seeks to such an extent that other applications start experiencing * non-trivial I/O waits. * * The specific choice of delay remains an arbitrary one to maintain app * and system responsiveness per the above while also processing as many * folders as quickly as possible. * * This is exposed primarily to allow unit tests to set this to 0 to minimize * throttling. */ INTER_FOLDER_PROCESSING_DELAY_MS: 10, /** * Set a string property on a folder and all of its descendents, taking care * to avoid locking up the main thread and to avoid leaving folder databases * open. To avoid locking up the main thread we operate in an asynchronous * fashion; we invoke a callback when we have completed our work. * * Using this function will write the value into the folder cache * (panacea.dat) as well as the folder itself. Hopefully you want this; if * you do not, keep in mind that the only way to avoid that is to retrieve * the nsIMsgDatabase and then the nsIDbFolderInfo. You would want to avoid * that as much as possible because once those are exposed to you, XPConnect * is going to hold onto them creating a situation where you are going to be * in severe danger of extreme memory bloat unless you force garbage * collections after every time you close a database. * * @param aPropertyName The name of the property to set. * @param aPropertyValue The (string) value of the property to set. * Alternately, you can pass a function that takes the nsIMsgFolder and * returns a string value. * @param aFolder The parent folder; we set the string property on it and all * of its descendents. * @param [aCallback] The optional callback to invoke once we finish our work. * The callback is provided a boolean success value; true means we * managed to set the values on all folders, false means we encountered a * problem. */ setStringPropertyOnFolderAndDescendents: function MailUtils_setStringPropertyOnFolderAndDescendents(aPropertyName, aPropertyValue, aFolder, aCallback) { // We need to add the base folder as it does not get added by ListDescendants. let allFolders = toXPCOMArray([aFolder], Ci.nsIMutableArray); // - get all the descendants aFolder.ListDescendants(allFolders); // - worker function function* folder_string_setter_worker() { for (let folder in fixIterator(allFolders, Ci.nsIMsgFolder)) { // set the property; this may open the database... let value = (typeof aPropertyValue == "function" ? aPropertyValue(folder) : aPropertyValue); folder.setStringProperty(aPropertyName, value); // force the reference to be forgotten. folder.msgDatabase = null; yield undefined; } } let worker = folder_string_setter_worker(); // - driver logic let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); function folder_string_setter_driver() { try { if (worker.next().done) { timer.cancel(); if (aCallback) aCallback(true); } } catch (ex) { // Any type of exception kills the generator. timer.cancel(); if (aCallback) aCallback(false); } } // make sure there is at least 100 ms of not us between doing things. timer.initWithCallback(folder_string_setter_driver, this.INTER_FOLDER_PROCESSING_DELAY_MS, Ci.nsITimer.TYPE_REPEATING_SLACK); }, };