summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/MailGlue.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/MailGlue.jsm')
-rw-r--r--comm/mail/components/MailGlue.jsm1380
1 files changed, 1380 insertions, 0 deletions
diff --git a/comm/mail/components/MailGlue.jsm b/comm/mail/components/MailGlue.jsm
new file mode 100644
index 0000000000..f14237a55a
--- /dev/null
+++ b/comm/mail/components/MailGlue.jsm
@@ -0,0 +1,1380 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["MailGlue", "MailTelemetryForTests"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+const lazy = {};
+
+// lazy module getter
+
+XPCOMUtils.defineLazyGetter(lazy, "gMailBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+});
+
+if (AppConstants.NIGHTLY_BUILD) {
+ XPCOMUtils.defineLazyGetter(
+ lazy,
+ "WeaveService",
+ () => Cc["@mozilla.org/weave/service;1"].getService().wrappedJSObject
+ );
+}
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ActorManagerParent: "resource://gre/modules/ActorManagerParent.sys.mjs",
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ ChatCore: "resource:///modules/chatHandler.sys.mjs",
+
+ LightweightThemeConsumer:
+ "resource://gre/modules/LightweightThemeConsumer.sys.mjs",
+
+ OsEnvironment: "resource://gre/modules/OsEnvironment.sys.mjs",
+ PdfJs: "resource://pdf.js/PdfJs.sys.mjs",
+
+ RemoteSecuritySettings:
+ "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ cal: "resource:///modules/calendar/calUtils.jsm",
+ ExtensionSupport: "resource:///modules/ExtensionSupport.jsm",
+ MailMigrator: "resource:///modules/MailMigrator.jsm",
+ MailServices: "resource:///modules/MailServices.jsm",
+ MailUsageTelemetry: "resource:///modules/MailUsageTelemetry.jsm",
+ OAuth2Providers: "resource:///modules/OAuth2Providers.jsm",
+ TBDistCustomizer: "resource:///modules/TBDistCustomizer.jsm",
+});
+
+if (AppConstants.MOZ_UPDATER) {
+ ChromeUtils.defineESModuleGetters(lazy, {
+ UpdateListener: "resource://gre/modules/UpdateListener.sys.mjs",
+ });
+}
+
+const listeners = {
+ observers: {},
+
+ observe(subject, topic, data) {
+ for (let module of this.observers[topic]) {
+ try {
+ lazy[module].observe(subject, topic, data);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ },
+
+ init() {
+ for (let observer of Object.keys(this.observers)) {
+ Services.obs.addObserver(this, observer);
+ }
+ },
+};
+if (AppConstants.MOZ_UPDATER) {
+ listeners.observers["update-downloading"] = ["UpdateListener"];
+ listeners.observers["update-staged"] = ["UpdateListener"];
+ listeners.observers["update-downloaded"] = ["UpdateListener"];
+ listeners.observers["update-available"] = ["UpdateListener"];
+ listeners.observers["update-error"] = ["UpdateListener"];
+ listeners.observers["update-swap"] = ["UpdateListener"];
+}
+
+const PREF_PDFJS_ISDEFAULT_CACHE_STATE = "pdfjs.enabledCache.state";
+
+let JSWINDOWACTORS = {
+ ChatAction: {
+ matches: ["chrome://chat/content/conv.html"],
+ parent: {
+ esModuleURI: "resource:///actors/ChatActionParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource:///actors/ChatActionChild.sys.mjs",
+ events: {
+ contextmenu: { mozSystemGroup: true },
+ },
+ },
+ },
+
+ ContextMenu: {
+ parent: {
+ esModuleURI: "resource:///actors/ContextMenuParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource:///actors/ContextMenuChild.sys.mjs",
+ events: {
+ contextmenu: { mozSystemGroup: true },
+ },
+ },
+ allFrames: true,
+ },
+
+ // As in ActorManagerParent.sys.mjs, but with single-site and single-page
+ // message manager groups added.
+ FindBar: {
+ parent: {
+ esModuleURI: "resource://gre/actors/FindBarParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://gre/actors/FindBarChild.sys.mjs",
+ events: {
+ keypress: { mozSystemGroup: true },
+ },
+ },
+
+ allFrames: true,
+ messageManagerGroups: [
+ "browsers",
+ "single-site",
+ "single-page",
+ "test",
+ "",
+ ],
+ },
+
+ LinkClickHandler: {
+ parent: {
+ moduleURI: "resource:///actors/LinkClickHandlerParent.jsm",
+ },
+ child: {
+ moduleURI: "resource:///actors/LinkClickHandlerChild.jsm",
+ events: {
+ click: {},
+ },
+ },
+ messageManagerGroups: ["single-site", "webext-browsers"],
+ allFrames: true,
+ },
+
+ LinkHandler: {
+ parent: {
+ esModuleURI: "resource:///actors/LinkHandlerParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource:///actors/LinkHandlerChild.sys.mjs",
+ events: {
+ DOMHeadElementParsed: {},
+ DOMLinkAdded: {},
+ DOMLinkChanged: {},
+ pageshow: {},
+ // The `pagehide` event is only used to clean up state which will not be
+ // present if the actor hasn't been created.
+ pagehide: { createActor: false },
+ },
+ },
+
+ messageManagerGroups: ["browsers", "single-site", "single-page"],
+ },
+
+ // As in ActorManagerParent.sys.mjs, but with single-site and single-page
+ // message manager groups added.
+ LoginManager: {
+ parent: {
+ esModuleURI: "resource://gre/modules/LoginManagerParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://gre/modules/LoginManagerChild.sys.mjs",
+ events: {
+ DOMDocFetchSuccess: {},
+ DOMFormBeforeSubmit: {},
+ DOMFormHasPassword: {},
+ DOMInputPasswordAdded: {},
+ },
+ },
+
+ allFrames: true,
+ messageManagerGroups: [
+ "browsers",
+ "single-site",
+ "single-page",
+ "webext-browsers",
+ "",
+ ],
+ },
+
+ MailLink: {
+ parent: {
+ moduleURI: "resource:///actors/MailLinkParent.jsm",
+ },
+ child: {
+ moduleURI: "resource:///actors/MailLinkChild.jsm",
+ events: {
+ click: {},
+ },
+ },
+ allFrames: true,
+ },
+
+ Pdfjs: {
+ parent: {
+ esModuleURI: "resource://pdf.js/PdfjsParent.sys.mjs",
+ },
+ child: {
+ esModuleURI: "resource://pdf.js/PdfjsChild.sys.mjs",
+ },
+ enablePreference: PREF_PDFJS_ISDEFAULT_CACHE_STATE,
+ allFrames: true,
+ },
+
+ Prompt: {
+ parent: {
+ moduleURI: "resource:///actors/PromptParent.jsm",
+ },
+ includeChrome: true,
+ allFrames: true,
+ },
+
+ StrictLinkClickHandler: {
+ parent: {
+ moduleURI: "resource:///actors/LinkClickHandlerParent.jsm",
+ },
+ child: {
+ moduleURI: "resource:///actors/LinkClickHandlerChild.jsm",
+ events: {
+ click: {},
+ },
+ },
+ messageManagerGroups: ["single-page"],
+ allFrames: true,
+ },
+
+ VCard: {
+ parent: {
+ moduleURI: "resource:///actors/VCardParent.jsm",
+ },
+ child: {
+ moduleURI: "resource:///actors/VCardChild.jsm",
+ events: {
+ click: {},
+ },
+ },
+ allFrames: true,
+ },
+};
+
+// Seconds of idle time before the late idle tasks will be scheduled.
+const LATE_TASKS_IDLE_TIME_SEC = 20;
+
+// Time after we stop tracking startup crashes.
+const STARTUP_CRASHES_END_DELAY_MS = 30 * 1000;
+
+/**
+ * Glue code that should be executed before any windows are opened. Any
+ * window-independent helper methods (a la nsBrowserGlue.js) should go in
+ * MailUtils.jsm instead.
+ */
+
+function MailGlue() {
+ XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "_userIdleService",
+ "@mozilla.org/widget/useridleservice;1",
+ "nsIUserIdleService"
+ );
+ this._init();
+}
+
+// This should match the constant of the same name in devtools
+// (devtools/client/framework/browser-toolbox/Launcher.sys.mjs). Otherwise the logic
+// in command-line-startup will fail. We have a test to ensure it matches, at
+// mail/base/test/unit/test_devtools_url.js.
+MailGlue.BROWSER_TOOLBOX_WINDOW_URL =
+ "chrome://devtools/content/framework/browser-toolbox/window.html";
+
+// A Promise that is resolved by an idle task after most start-up operations.
+MailGlue.afterStartUp = new Promise(resolve => {
+ MailGlue.resolveAfterStartUp = resolve;
+});
+
+MailGlue.prototype = {
+ _isNewProfile: undefined,
+
+ // init (called at app startup)
+ _init() {
+ // Start-up notifications, in order.
+ // app-startup happens first, registered in components.conf.
+ Services.obs.addObserver(this, "command-line-startup");
+ Services.obs.addObserver(this, "final-ui-startup");
+ Services.obs.addObserver(this, "quit-application-granted");
+ Services.obs.addObserver(this, "mail-startup-done");
+
+ // Shut-down notifications.
+ Services.obs.addObserver(this, "xpcom-shutdown");
+
+ // General notifications.
+ Services.obs.addObserver(this, "intl:app-locales-changed");
+ Services.obs.addObserver(this, "handle-xul-text-link");
+ Services.obs.addObserver(this, "chrome-document-global-created");
+ Services.obs.addObserver(this, "document-element-inserted");
+ Services.obs.addObserver(this, "handlersvc-store-initialized");
+
+ // Call the lazy getter to ensure ActorManagerParent is initialized.
+ lazy.ActorManagerParent;
+
+ // FindBar and LoginManager actors are included in JSWINDOWACTORS as they
+ // also apply to the single-site and single-page message manager groups.
+ // First we must unregister them to avoid errors.
+ ChromeUtils.unregisterWindowActor("FindBar");
+ ChromeUtils.unregisterWindowActor("LoginManager");
+
+ lazy.ActorManagerParent.addJSWindowActors(JSWINDOWACTORS);
+ },
+
+ // cleanup (called at shutdown)
+ _dispose() {
+ Services.obs.removeObserver(this, "command-line-startup");
+ Services.obs.removeObserver(this, "final-ui-startup");
+ Services.obs.removeObserver(this, "quit-application-granted");
+ // mail-startup-done is removed by its handler.
+
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+
+ Services.obs.removeObserver(this, "intl:app-locales-changed");
+ Services.obs.removeObserver(this, "handle-xul-text-link");
+ Services.obs.removeObserver(this, "chrome-document-global-created");
+ Services.obs.removeObserver(this, "document-element-inserted");
+ Services.obs.removeObserver(this, "handlersvc-store-initialized");
+
+ lazy.ExtensionSupport.unregisterWindowListener(
+ "Thunderbird-internal-BrowserConsole"
+ );
+
+ lazy.MailUsageTelemetry.uninit();
+
+ if (this._lateTasksIdleObserver) {
+ this._userIdleService.removeIdleObserver(
+ this._lateTasksIdleObserver,
+ LATE_TASKS_IDLE_TIME_SEC
+ );
+ delete this._lateTasksIdleObserver;
+ }
+ },
+
+ // nsIObserver implementation
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "app-startup":
+ // Record the previously started version. This is used to check for
+ // extensions that were disabled by an application update. We need to
+ // read this pref before the Add-Ons Manager changes it.
+ this.previousVersion = Services.prefs.getCharPref(
+ "extensions.lastAppVersion",
+ "0"
+ );
+ break;
+ case "command-line-startup":
+ // Check if this process is the developer toolbox process, and if it
+ // is, stop MailGlue from doing anything more. Also sets a flag that
+ // can be checked to see if this is the toolbox process.
+ let isToolboxProcess = false;
+ let commandLine = aSubject.QueryInterface(Ci.nsICommandLine);
+ let flagIndex = commandLine.findFlag("chrome", true) + 1;
+ if (
+ flagIndex > 0 &&
+ flagIndex < commandLine.length &&
+ commandLine.getArgument(flagIndex) ===
+ MailGlue.BROWSER_TOOLBOX_WINDOW_URL
+ ) {
+ isToolboxProcess = true;
+ }
+
+ MailGlue.__defineGetter__("isToolboxProcess", () => isToolboxProcess);
+
+ if (isToolboxProcess) {
+ // Clean up all of the listeners.
+ this._dispose();
+ }
+ break;
+ case "final-ui-startup":
+ // Initialise the permission manager. If this happens before telling
+ // the folder service that strings are available, it's a *much* less
+ // expensive operation than if it happens afterwards, because if
+ // strings are available, some types of mail URL go looking for things
+ // in message databases, causing massive amounts of I/O.
+ Services.perms.all;
+
+ Cc["@mozilla.org/msgFolder/msgFolderService;1"]
+ .getService(Ci.nsIMsgFolderService)
+ .initializeFolderStrings();
+ Cc["@mozilla.org/msgDBView/msgDBViewService;1"]
+ .getService(Ci.nsIMsgDBViewService)
+ .initializeDBViewStrings();
+ this._beforeUIStartup();
+ break;
+ case "quit-application-granted":
+ Services.startup.trackStartupCrashEnd();
+ if (AppConstants.MOZ_UPDATER) {
+ lazy.UpdateListener.reset();
+ }
+ break;
+ case "mail-startup-done":
+ this._onFirstWindowLoaded();
+ Services.obs.removeObserver(this, "mail-startup-done");
+ break;
+ case "xpcom-shutdown":
+ this._dispose();
+ break;
+ case "intl:app-locales-changed":
+ Cc["@mozilla.org/msgFolder/msgFolderService;1"]
+ .getService(Ci.nsIMsgFolderService)
+ .initializeFolderStrings();
+ Cc["@mozilla.org/msgDBView/msgDBViewService;1"]
+ .getService(Ci.nsIMsgDBViewService)
+ .initializeDBViewStrings();
+ let windows = Services.wm.getEnumerator("mail:3pane");
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext();
+ win.document.getElementById("threadTree")?.invalidate();
+ }
+ // Refresh the folder tree.
+ let fls = Cc["@mozilla.org/mail/folder-lookup;1"].getService(
+ Ci.nsIFolderLookupService
+ );
+ fls.setPrettyNameFromOriginalAllFolders();
+ break;
+ case "handle-xul-text-link":
+ this._handleLink(aSubject, aData);
+ break;
+ case "chrome-document-global-created":
+ // Set up lwt, but only if the "lightweightthemes" attr is set on the root
+ // (i.e. in messenger.xhtml).
+ aSubject.addEventListener(
+ "DOMContentLoaded",
+ () => {
+ if (
+ aSubject.document.documentElement.hasAttribute(
+ "lightweightthemes"
+ )
+ ) {
+ new lazy.LightweightThemeConsumer(aSubject.document);
+ }
+ },
+ { once: true }
+ );
+ break;
+ case "document-element-inserted":
+ let doc = aSubject;
+ if (
+ doc.nodePrincipal.isSystemPrincipal &&
+ (doc.contentType == "application/xhtml+xml" ||
+ doc.contentType == "text/html") &&
+ // People shouldn't be using our built-in custom elements in
+ // system-principal about:blank anyway, and trying to support that
+ // causes responsiveness regressions. So let's not support it.
+ doc.URL != "about:blank"
+ ) {
+ Services.scriptloader.loadSubScript(
+ "chrome://messenger/content/customElements.js",
+ doc.ownerGlobal
+ );
+ }
+ break;
+ case "handlersvc-store-initialized": {
+ // Initialize PdfJs when running in-process and remote. This only
+ // happens once since PdfJs registers global hooks. If the PdfJs
+ // extension is installed the init method below will be overridden
+ // leaving initialization to the extension.
+ // parent only: configure default prefs, set up pref observers, register
+ // pdf content handler, and initializes parent side message manager
+ // shim for privileged api access.
+ lazy.PdfJs.init(this._isNewProfile);
+ break;
+ }
+ }
+ },
+
+ // Runs on startup, before the first command line handler is invoked
+ // (i.e. before the first window is opened).
+ _beforeUIStartup() {
+ lazy.TBDistCustomizer.applyPrefDefaults();
+
+ const UI_VERSION_PREF = "mail.ui-rdf.version";
+ this._isNewProfile = !Services.prefs.prefHasUserValue(UI_VERSION_PREF);
+
+ // handle any migration work that has to happen at profile startup
+ lazy.MailMigrator.migrateAtProfileStartup();
+
+ if (!Services.prefs.prefHasUserValue(PREF_PDFJS_ISDEFAULT_CACHE_STATE)) {
+ lazy.PdfJs.checkIsDefault(this._isNewProfile);
+ }
+
+ // Inject scripts into some devtools windows.
+ function _setupBrowserConsole(domWindow) {
+ // Browser Console is an XHTML document.
+ domWindow.document.title =
+ lazy.gMailBundle.GetStringFromName("errorConsoleTitle");
+ Services.scriptloader.loadSubScript(
+ "chrome://global/content/viewSourceUtils.js",
+ domWindow
+ );
+ }
+
+ lazy.ExtensionSupport.registerWindowListener(
+ "Thunderbird-internal-BrowserConsole",
+ {
+ chromeURLs: ["chrome://devtools/content/webconsole/index.html"],
+ onLoadWindow: _setupBrowserConsole,
+ }
+ );
+
+ // check if we're in safe mode
+ if (Services.appinfo.inSafeMode) {
+ Services.ww.openWindow(
+ null,
+ "chrome://messenger/content/troubleshootMode.xhtml",
+ "_blank",
+ "chrome,centerscreen,modal,resizable=no",
+ null
+ );
+ }
+
+ lazy.AddonManager.maybeInstallBuiltinAddon(
+ "thunderbird-compact-light@mozilla.org",
+ "1.2",
+ "resource://builtin-themes/light/"
+ );
+ lazy.AddonManager.maybeInstallBuiltinAddon(
+ "thunderbird-compact-dark@mozilla.org",
+ "1.2",
+ "resource://builtin-themes/dark/"
+ );
+
+ if (AppConstants.MOZ_UPDATER) {
+ listeners.init();
+ }
+ },
+
+ _checkForOldBuildUpdates() {
+ // check for update if our build is old
+ if (
+ AppConstants.MOZ_UPDATER &&
+ Services.prefs.getBoolPref("app.update.checkInstallTime")
+ ) {
+ let buildID = Services.appinfo.appBuildID;
+ let today = new Date().getTime();
+ /* eslint-disable no-multi-spaces */
+ let buildDate = new Date(
+ buildID.slice(0, 4), // year
+ buildID.slice(4, 6) - 1, // months are zero-based.
+ buildID.slice(6, 8), // day
+ buildID.slice(8, 10), // hour
+ buildID.slice(10, 12), // min
+ buildID.slice(12, 14)
+ ) // ms
+ .getTime();
+ /* eslint-enable no-multi-spaces */
+
+ const millisecondsIn24Hours = 86400000;
+ let acceptableAge =
+ Services.prefs.getIntPref("app.update.checkInstallTime.days") *
+ millisecondsIn24Hours;
+
+ if (buildDate + acceptableAge < today) {
+ Cc["@mozilla.org/updates/update-service;1"]
+ .getService(Ci.nsIApplicationUpdateService)
+ .checkForBackgroundUpdates();
+ }
+ }
+ },
+
+ _onFirstWindowLoaded() {
+ // Start these services.
+ Cc["@mozilla.org/chat/logger;1"].getService(Ci.imILogger);
+
+ this._checkForOldBuildUpdates();
+
+ // On Windows 7 and above, initialize the jump list module.
+ const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
+ if (
+ WINTASKBAR_CONTRACTID in Cc &&
+ Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available
+ ) {
+ const { WinTaskbarJumpList } = ChromeUtils.import(
+ "resource:///modules/WindowsJumpLists.jsm"
+ );
+ WinTaskbarJumpList.startup();
+ }
+
+ const { ExtensionsUI } = ChromeUtils.import(
+ "resource:///modules/ExtensionsUI.jsm"
+ );
+ ExtensionsUI.init();
+
+ // If the application has been updated, check all installed extensions for
+ // updates.
+ let currentVersion = Services.appinfo.version;
+ if (this.previousVersion != "0" && this.previousVersion != currentVersion) {
+ let { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+ );
+ let { XPIDatabase } = ChromeUtils.import(
+ "resource://gre/modules/addons/XPIDatabase.jsm"
+ );
+ let addons = XPIDatabase.getAddons();
+ for (let addon of addons) {
+ if (addon.permissions() & AddonManager.PERM_CAN_UPGRADE) {
+ AddonManager.getAddonByID(addon.id).then(addon => {
+ if (!AddonManager.shouldAutoUpdate(addon)) {
+ return;
+ }
+ addon.findUpdates(
+ {
+ onUpdateFinished() {},
+ onUpdateAvailable(addon, install) {
+ install.install();
+ },
+ },
+ AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED
+ );
+ });
+ }
+ }
+ }
+
+ if (AppConstants.ASAN_REPORTER) {
+ var { AsanReporter } = ChromeUtils.importESModule(
+ "resource://gre/modules/AsanReporter.sys.mjs"
+ );
+ AsanReporter.init();
+ }
+
+ // Check if Sync is configured
+ if (
+ AppConstants.NIGHTLY_BUILD &&
+ Services.prefs.prefHasUserValue("services.sync.username")
+ ) {
+ lazy.WeaveService.init();
+ }
+
+ this._scheduleStartupIdleTasks();
+ this._lateTasksIdleObserver = (idleService, topic, data) => {
+ if (topic == "idle") {
+ idleService.removeIdleObserver(
+ this._lateTasksIdleObserver,
+ LATE_TASKS_IDLE_TIME_SEC
+ );
+ delete this._lateTasksIdleObserver;
+ this._scheduleBestEffortUserIdleTasks();
+ }
+ };
+ this._userIdleService.addIdleObserver(
+ this._lateTasksIdleObserver,
+ LATE_TASKS_IDLE_TIME_SEC
+ );
+
+ lazy.MailUsageTelemetry.init();
+ },
+
+ /**
+ * Use this function as an entry point to schedule tasks that
+ * need to run only once after startup, and can be scheduled
+ * by using an idle callback.
+ *
+ * The functions scheduled here will fire from idle callbacks
+ * once every window has finished being restored by session
+ * restore, and it's guaranteed that they will run before
+ * the equivalent per-window idle tasks
+ * (from _schedulePerWindowIdleTasks in browser.js).
+ *
+ * If you have something that can wait even further than the
+ * per-window initialization, and is okay with not being run in some
+ * sessions, please schedule them using
+ * _scheduleBestEffortUserIdleTasks.
+ * Don't be fooled by thinking that the use of the timeout parameter
+ * will delay your function: it will just ensure that it potentially
+ * happens _earlier_ than expected (when the timeout limit has been reached),
+ * but it will not make it happen later (and out of order) compared
+ * to the other ones scheduled together.
+ */
+ _scheduleStartupIdleTasks() {
+ const idleTasks = [
+ {
+ task() {
+ // This module needs to be loaded so it registers to receive
+ // FormAutoComplete:GetSelectedIndex messages and respond
+ // appropriately, otherwise we get error messages like the one
+ // reported in bug 1635422.
+ ChromeUtils.importESModule(
+ "resource://gre/actors/AutoCompleteParent.sys.mjs"
+ );
+ },
+ },
+ {
+ task() {
+ // Make sure Gloda's up and running.
+ ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm");
+ },
+ },
+ {
+ task() {
+ MailGlue.resolveAfterStartUp();
+ },
+ },
+ {
+ task() {
+ let { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+ );
+ setTimeout(function () {
+ Services.tm.idleDispatchToMainThread(
+ Services.startup.trackStartupCrashEnd
+ );
+ }, STARTUP_CRASHES_END_DELAY_MS);
+ },
+ },
+ {
+ condition: AppConstants.NIGHTLY_BUILD,
+ task: async () => {
+ // Register our sync engines.
+ await lazy.WeaveService.whenLoaded();
+ let Weave = lazy.WeaveService.Weave;
+
+ for (let [moduleName, engineName] of [
+ ["accounts", "AccountsEngine"],
+ ["addressBooks", "AddressBooksEngine"],
+ ["calendars", "CalendarsEngine"],
+ ["identities", "IdentitiesEngine"],
+ ]) {
+ let ns = ChromeUtils.importESModule(
+ `resource://services-sync/engines/${moduleName}.sys.mjs`
+ );
+ await Weave.Service.engineManager.register(ns[engineName]);
+ Weave.Service.engineManager
+ .get(moduleName.toLowerCase())
+ .startTracking();
+ }
+
+ if (lazy.WeaveService.enabled) {
+ // Schedule a sync (if enabled).
+ Weave.Service.scheduler.autoConnect();
+ }
+ },
+ },
+ {
+ condition: Services.prefs.getBoolPref("mail.chat.enabled"),
+ task() {
+ lazy.ChatCore.idleStart();
+ ChromeUtils.importESModule("resource:///modules/index_im.sys.mjs");
+ },
+ },
+ {
+ condition: AppConstants.MOZ_UPDATER,
+ task: () => {
+ lazy.UpdateListener.maybeShowUnsupportedNotification();
+ },
+ },
+ {
+ task() {
+ // Use idleDispatch a second time to run this after the per-window
+ // idle tasks.
+ ChromeUtils.idleDispatch(() => {
+ Services.obs.notifyObservers(
+ null,
+ "mail-startup-idle-tasks-finished"
+ );
+ });
+ },
+ },
+ // Do NOT add anything after idle tasks finished.
+ ];
+
+ for (let task of idleTasks) {
+ if ("condition" in task && !task.condition) {
+ continue;
+ }
+
+ ChromeUtils.idleDispatch(
+ () => {
+ if (!Services.startup.shuttingDown) {
+ let startTime = Cu.now();
+ try {
+ task.task();
+ } catch (ex) {
+ console.error(ex);
+ } finally {
+ ChromeUtils.addProfilerMarker("startupIdleTask", startTime);
+ }
+ }
+ },
+ task.timeout ? { timeout: task.timeout } : undefined
+ );
+ }
+ },
+
+ /**
+ * Use this function as an entry point to schedule tasks that we hope
+ * to run once per session, at any arbitrary point in time, and which we
+ * are okay with sometimes not running at all.
+ *
+ * This function will be called from an idle observer. Check the value of
+ * LATE_TASKS_IDLE_TIME_SEC to see the current value for this idle
+ * observer.
+ *
+ * Note: this function may never be called if the user is never idle for the
+ * requisite time (LATE_TASKS_IDLE_TIME_SEC). Be certain before adding
+ * something here that it's okay that it never be run.
+ */
+ _scheduleBestEffortUserIdleTasks() {
+ const idleTasks = [
+ // Certificates revocation list, etc.
+ () => lazy.RemoteSecuritySettings.init(),
+ // If we haven't already, ensure the address book manager is ready.
+ // This must happen at some point so that CardDAV address books sync.
+ () => lazy.MailServices.ab.directories,
+ // Telemetry.
+ async () => {
+ lazy.OsEnvironment.reportAllowedAppSources();
+ reportAccountTypes();
+ reportAddressBookTypes();
+ reportAccountSizes();
+ await reportCalendars();
+ reportPreferences();
+ reportUIConfiguration();
+ },
+ ];
+
+ for (let task of idleTasks) {
+ ChromeUtils.idleDispatch(async () => {
+ if (!Services.startup.shuttingDown) {
+ let startTime = Cu.now();
+ try {
+ await task();
+ } catch (ex) {
+ console.error(ex);
+ } finally {
+ ChromeUtils.addProfilerMarker("startupLateIdleTask", startTime);
+ }
+ }
+ });
+ }
+ },
+
+ _handleLink(aSubject, aData) {
+ let linkHandled = aSubject.QueryInterface(Ci.nsISupportsPRBool);
+ if (!linkHandled.data) {
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ aData = JSON.parse(aData);
+ let tabParams = { url: aData.href, linkHandler: null };
+ if (win) {
+ let tabmail = win.document.getElementById("tabmail");
+ if (tabmail) {
+ tabmail.openTab("contentTab", tabParams);
+ win.focus();
+ linkHandled.data = true;
+ return;
+ }
+ }
+
+ // If we didn't have an open 3 pane window, try and open one.
+ Services.ww.openWindow(
+ null,
+ "chrome://messenger/content/messenger.xhtml",
+ "_blank",
+ "chrome,dialog=no,all",
+ {
+ type: "contentTab",
+ tabParams,
+ }
+ );
+ linkHandled.data = true;
+ }
+ },
+
+ // for XPCOM
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+};
+
+/**
+ * Report account types to telemetry. For im accounts, use `im_protocol` as
+ * scalar key name.
+ */
+function reportAccountTypes() {
+ // Init all count with 0, so that when an account was set up before but
+ // removed now, we reset it in telemetry report.
+ let report = {
+ pop3: 0,
+ imap: 0,
+ nntp: 0,
+ exchange: 0,
+ rss: 0,
+ im_gtalk: 0,
+ im_irc: 0,
+ im_jabber: 0,
+ im_matrix: 0,
+ im_odnoklassniki: 0,
+ };
+
+ const providerReport = {
+ google: 0,
+ microsoft: 0,
+ yahoo_aol: 0,
+ other: 0,
+ };
+
+ for (let account of lazy.MailServices.accounts.accounts) {
+ const incomingServer = account.incomingServer;
+
+ let type = incomingServer.type;
+ if (type == "none") {
+ // Reporting one Local Folders account is not that useful. Skip it.
+ continue;
+ }
+
+ if (type === "im") {
+ let protocol =
+ incomingServer.wrappedJSObject.imAccount.protocol.normalizedName;
+ type = `im_${protocol}`;
+ }
+
+ // It's still possible to report other types not explicitly specified due to
+ // account types that used to exist, but no longer -- e.g. im_yahoo.
+ if (!report[type]) {
+ report[type] = 0;
+ }
+
+ report[type]++;
+
+ // Collect a rough understanding of the frequency of various OAuth
+ // providers.
+ if (incomingServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
+ const hostnameDetails = lazy.OAuth2Providers.getHostnameDetails(
+ incomingServer.hostName
+ );
+
+ if (!hostnameDetails || hostnameDetails.length == 0) {
+ // Not a valid OAuth2 configuration; skip it
+ continue;
+ }
+
+ const host = hostnameDetails[0];
+
+ switch (host) {
+ case "accounts.google.com":
+ providerReport.google++;
+ break;
+ case "login.microsoftonline.com":
+ providerReport.microsoft++;
+ break;
+ case "login.yahoo.com":
+ case "login.aol.com":
+ providerReport.yahoo_aol++;
+ break;
+ default:
+ providerReport.other++;
+ }
+ }
+ }
+
+ for (let [type, count] of Object.entries(report)) {
+ Services.telemetry.keyedScalarSet("tb.account.count", type, count);
+ }
+
+ for (const [provider, count] of Object.entries(providerReport)) {
+ Services.telemetry.keyedScalarSet(
+ "tb.account.oauth2_provider_count",
+ provider,
+ count
+ );
+ }
+}
+
+/**
+ * Report size on disk and messages count of each type of folder to telemetry.
+ */
+function reportAccountSizes() {
+ let keys = [
+ "Inbox",
+ "Drafts",
+ "Trash",
+ "SentMail",
+ "Templates",
+ "Junk",
+ "Archive",
+ "Queue",
+ ];
+ for (let key of keys) {
+ Services.telemetry.keyedScalarSet("tb.account.total_messages", key, 0);
+ }
+ Services.telemetry.keyedScalarSet("tb.account.total_messages", "Other", 0);
+ Services.telemetry.keyedScalarSet("tb.account.total_messages", "Total", 0);
+
+ for (let server of lazy.MailServices.accounts.allServers) {
+ if (
+ server instanceof Ci.nsIPop3IncomingServer &&
+ server.deferredToAccount
+ ) {
+ // Skip deferred accounts
+ continue;
+ }
+
+ for (let folder of server.rootFolder.descendants) {
+ let key =
+ keys.find(x => folder.getFlag(Ci.nsMsgFolderFlags[x])) || "Other";
+ let totalMessages = folder.getTotalMessages(false);
+ if (totalMessages > 0) {
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.total_messages",
+ key,
+ totalMessages
+ );
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.total_messages",
+ "Total",
+ totalMessages
+ );
+ }
+ let sizeOnDisk = folder.sizeOnDisk;
+ if (sizeOnDisk > 0) {
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.size_on_disk",
+ key,
+ sizeOnDisk
+ );
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.size_on_disk",
+ "Total",
+ sizeOnDisk
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Report addressbook count and contact count to telemetry, keyed by addressbook
+ * type. Type is one of ["jsaddrbook", "jscarddav", "moz-abldapdirectory"], see
+ * AddrBookManager.jsm for more details.
+ *
+ * NOTE: We didn't use `dir.dirType` because it's just an integer, instead we
+ * use the scheme of `dir.URI` as the type.
+ */
+function reportAddressBookTypes() {
+ let report = {};
+ for (let dir of lazy.MailServices.ab.directories) {
+ let type = dir.URI.split(":")[0];
+
+ if (!report[type]) {
+ report[type] = { count: 0, contactCount: 0 };
+ }
+ report[type].count++;
+
+ try {
+ report[type].contactCount += dir.childCardCount;
+ } catch (ex) {
+ // Directories may throw NS_ERROR_NOT_IMPLEMENTED.
+ }
+ }
+
+ for (let [type, { count, contactCount }] of Object.entries(report)) {
+ Services.telemetry.keyedScalarSet(
+ "tb.addressbook.addressbook_count",
+ type,
+ count
+ );
+ Services.telemetry.keyedScalarSet(
+ "tb.addressbook.contact_count",
+ type,
+ contactCount
+ );
+ }
+}
+
+/**
+ * A telemetry probe to report calendar count and read only calendar count.
+ */
+async function reportCalendars() {
+ let telemetryReport = {};
+ let home = lazy.cal.l10n.getCalString("homeCalendarName");
+
+ for (let calendar of lazy.cal.manager.getCalendars()) {
+ if (calendar.name == home && calendar.type == "storage") {
+ // Ignore the "Home" calendar if it is disabled or unused as it's
+ // automatically added.
+ if (calendar.getProperty("disabled")) {
+ continue;
+ }
+ let items = await calendar.getItemsAsArray(
+ Ci.calICalendar.ITEM_FILTER_ALL_ITEMS,
+ 1,
+ null,
+ null
+ );
+ if (!items.length) {
+ continue;
+ }
+ }
+ if (!telemetryReport[calendar.type]) {
+ telemetryReport[calendar.type] = { count: 0, readOnlyCount: 0 };
+ }
+ telemetryReport[calendar.type].count++;
+ if (calendar.readOnly) {
+ telemetryReport[calendar.type].readOnlyCount++;
+ }
+ }
+
+ for (let [type, { count, readOnlyCount }] of Object.entries(
+ telemetryReport
+ )) {
+ Services.telemetry.keyedScalarSet(
+ "tb.calendar.calendar_count",
+ type.toLowerCase(),
+ count
+ );
+ Services.telemetry.keyedScalarSet(
+ "tb.calendar.read_only_calendar_count",
+ type.toLowerCase(),
+ readOnlyCount
+ );
+ }
+}
+
+function reportPreferences() {
+ let booleanPrefs = [
+ // General
+ "browser.cache.disk.smart_size.enabled",
+ "privacy.clearOnShutdown.cache",
+ "general.autoScroll",
+ "general.smoothScroll",
+ "intl.regional_prefs.use_os_locales",
+ "layers.acceleration.disabled",
+ "mail.biff.play_sound",
+ "mail.close_message_window.on_delete",
+ "mail.delete_matches_sort_order",
+ "mail.display_glyph",
+ "mail.mailnews.scroll_to_new_message",
+ "mail.prompt_purge_threshhold",
+ "mail.purge.ask",
+ "mail.showCondensedAddresses",
+ "mailnews.database.global.indexer.enabled",
+ "mailnews.mark_message_read.auto",
+ "mailnews.mark_message_read.delay",
+ "mailnews.reuse_message_window",
+ "mailnews.start_page.enabled",
+ "searchintegration.enable",
+
+ // Fonts
+ "mail.fixed_width_messages",
+
+ // Colors
+ "browser.display.use_system_colors",
+ "browser.underline_anchors",
+
+ // Read receipts
+ "mail.mdn.report.enabled",
+ "mail.receipt.request_return_receipt_on",
+
+ // Connection
+ "network.proxy.share_proxy_settings",
+ "network.proxy.socks_remote_dns",
+ "pref.advanced.proxies.disable_button.reload",
+ "signon.autologin.proxy",
+
+ // Offline
+ "offline.autoDetect",
+
+ // Compose
+ "ldap_2.autoComplete.useDirectory",
+ "mail.collect_email_address_outgoing",
+ "mail.compose.attachment_reminder",
+ "mail.compose.autosave",
+ "mail.compose.big_attachments.notify",
+ "mail.compose.default_to_paragraph",
+ "mail.e2ee.auto_enable",
+ "mail.e2ee.auto_disable",
+ "mail.e2ee.notify_on_auto_disable",
+ "mail.enable_autocomplete",
+ "mail.forward_add_extension",
+ "mail.SpellCheckBeforeSend",
+ "mail.spellcheck.inline",
+ "mail.warn_on_send_accel_key",
+ "msgcompose.default_colors",
+ "pref.ldap.disable_button.edit_directories",
+
+ // Send options
+ "mailnews.sendformat.auto_downgrade",
+
+ // Privacy
+ "browser.safebrowsing.enabled",
+ "mail.phishing.detection.enabled",
+ "mail.spam.logging.enabled",
+ "mail.spam.manualMark",
+ "mail.spam.markAsReadOnSpam",
+ "mailnews.downloadToTempFile",
+ "mailnews.message_display.disable_remote_image",
+ "network.cookie.blockFutureCookies",
+ "places.history.enabled",
+ "pref.privacy.disable_button.cookie_exceptions",
+ "pref.privacy.disable_button.view_cookies",
+ "pref.privacy.disable_button.view_passwords",
+ "privacy.donottrackheader.enabled",
+ "security.disable_button.openCertManager",
+ "security.disable_button.openDeviceManager",
+
+ // Chat
+ "messenger.options.getAttentionOnNewMessages",
+ "messenger.status.reportIdle",
+ "messenger.status.awayWhenIdle",
+ "mail.chat.enabled",
+ "mail.chat.play_sound",
+ "mail.chat.show_desktop_notifications",
+ "purple.conversations.im.send_typing",
+ "purple.logging.log_chats",
+ "purple.logging.log_ims",
+ "purple.logging.log_system",
+
+ // Calendar views
+ "calendar.view.showLocation",
+ "calendar.view-minimonth.showWeekNumber",
+ "calendar.week.d0sundaysoff",
+ "calendar.week.d1mondaysoff",
+ "calendar.week.d2tuesdaysoff",
+ "calendar.week.d3wednesdaysoff",
+ "calendar.week.d4thursdaysoff",
+ "calendar.week.d5fridaysoff",
+ "calendar.week.d6saturdaysoff",
+
+ // Calendar general
+ "calendar.item.editInTab",
+ "calendar.item.promptDelete",
+ "calendar.timezone.useSystemTimezone",
+
+ // Alarms
+ "calendar.alarms.playsound",
+ "calendar.alarms.show",
+ "calendar.alarms.showmissed",
+
+ // Unlisted
+ "mail.operate_on_msgs_in_collapsed_threads",
+ ];
+
+ let integerPrefs = [
+ // Mail UI
+ "mail.pane_config.dynamic",
+ "mail.ui.display.dateformat.default",
+ "mail.ui.display.dateformat.thisweek",
+ "mail.ui.display.dateformat.today",
+ ];
+
+ // Platform-specific preferences
+ if (AppConstants.platform === "win") {
+ booleanPrefs.push("mail.biff.show_tray_icon", "mail.minimizeToTray");
+ }
+
+ if (AppConstants.platform !== "macosx") {
+ booleanPrefs.push(
+ "mail.biff.show_alert",
+ "mail.biff.use_system_alert",
+
+ // Notifications
+ "mail.biff.alert.show_preview",
+ "mail.biff.alert.show_sender",
+ "mail.biff.alert.show_subject"
+ );
+ }
+
+ // Compile-time flag-dependent preferences
+ if (AppConstants.HAVE_SHELL_SERVICE) {
+ booleanPrefs.push("mail.shell.checkDefaultClient");
+ }
+
+ if (AppConstants.MOZ_WIDGET_GTK) {
+ booleanPrefs.push("widget.gtk.overlay-scrollbars.enabled");
+ }
+
+ if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ booleanPrefs.push("app.update.service.enabled");
+ }
+
+ if (AppConstants.MOZ_DATA_REPORTING) {
+ booleanPrefs.push("datareporting.healthreport.uploadEnabled");
+ }
+
+ if (AppConstants.MOZ_CRASHREPORTER) {
+ booleanPrefs.push("browser.crashReports.unsubmittedCheck.autoSubmit2");
+ }
+
+ // Fetch and report preference values
+ for (let prefName of booleanPrefs) {
+ let prefValue = Services.prefs.getBoolPref(prefName, false);
+
+ Services.telemetry.keyedScalarSet(
+ "tb.preferences.boolean",
+ prefName,
+ prefValue
+ );
+ }
+
+ for (let prefName of integerPrefs) {
+ let prefValue = Services.prefs.getIntPref(prefName, 0);
+
+ Services.telemetry.keyedScalarSet(
+ "tb.preferences.integer",
+ prefName,
+ prefValue
+ );
+ }
+}
+
+function reportUIConfiguration() {
+ let docURL = "chrome://messenger/content/messenger.xhtml";
+
+ let folderTreeMode = Services.xulStore.getValue(docURL, "folderTree", "mode");
+ if (folderTreeMode) {
+ let folderTreeCompact = Services.xulStore.getValue(
+ docURL,
+ "folderTree",
+ "compact"
+ );
+ if (folderTreeCompact === "true") {
+ folderTreeMode += " (compact)";
+ }
+ Services.telemetry.scalarSet(
+ "tb.ui.configuration.folder_tree_modes",
+ folderTreeMode
+ );
+ }
+
+ let headerLayout = Services.xulStore.getValue(
+ docURL,
+ "messageHeader",
+ "layout"
+ );
+ if (headerLayout) {
+ headerLayout = JSON.parse(headerLayout);
+ for (let [key, value] of Object.entries(headerLayout)) {
+ if (key == "buttonStyle") {
+ value = { default: 0, "only-icons": 1, "only-text": 2 }[value];
+ }
+ Services.telemetry.keyedScalarSet(
+ "tb.ui.configuration.message_header",
+ key,
+ value
+ );
+ }
+ }
+}
+
+/**
+ * Export these functions so we can test them. This object shouldn't be
+ * accessed outside of a test (hence the name).
+ */
+var MailTelemetryForTests = {
+ reportAccountTypes,
+ reportAccountSizes,
+ reportAddressBookTypes,
+ reportCalendars,
+ reportPreferences,
+ reportUIConfiguration,
+};