summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/MailNotificationManager.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base/src/MailNotificationManager.jsm')
-rw-r--r--comm/mailnews/base/src/MailNotificationManager.jsm478
1 files changed, 478 insertions, 0 deletions
diff --git a/comm/mailnews/base/src/MailNotificationManager.jsm b/comm/mailnews/base/src/MailNotificationManager.jsm
new file mode 100644
index 0000000000..c16e37eb2f
--- /dev/null
+++ b/comm/mailnews/base/src/MailNotificationManager.jsm
@@ -0,0 +1,478 @@
+/* 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 = ["MailNotificationManager"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ WinUnreadBadge: "resource:///modules/WinUnreadBadge.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () => new Localization(["messenger/messenger.ftl"])
+);
+
+/**
+ * A module that listens to folder change events, and show notifications for new
+ * mails if necessary.
+ */
+class MailNotificationManager {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIFolderListener",
+ "mozINewMailListener",
+ ]);
+
+ constructor() {
+ this._systemAlertAvailable = true;
+ this._unreadChatCount = 0;
+ this._unreadMailCount = 0;
+ // @type {Map<nsIMsgFolder, number>} - A map of folder and its last biff time.
+ this._folderBiffTime = new Map();
+ // @type {Set<nsIMsgFolder>} - A set of folders to show alert for.
+ this._pendingFolders = new Set();
+
+ this._logger = console.createInstance({
+ prefix: "mail.notification",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.notification.loglevel",
+ });
+ this._bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ MailServices.mailSession.AddFolderListener(
+ this,
+ Ci.nsIFolderListener.intPropertyChanged
+ );
+
+ // Ensure that OS integration is defined before we attempt to initialize the
+ // system tray icon.
+ XPCOMUtils.defineLazyGetter(this, "_osIntegration", () => {
+ try {
+ return Cc["@mozilla.org/messenger/osintegration;1"].getService(
+ Ci.nsIMessengerOSIntegration
+ );
+ } catch (e) {
+ // We don't have OS integration on all platforms.
+ return null;
+ }
+ });
+
+ if (["macosx", "win"].includes(AppConstants.platform)) {
+ // We don't have indicator for unread count on Linux yet.
+ Cc["@mozilla.org/newMailNotificationService;1"]
+ .getService(Ci.mozINewMailNotificationService)
+ .addListener(this, Ci.mozINewMailNotificationService.count);
+
+ Services.obs.addObserver(this, "unread-im-count-changed");
+ Services.obs.addObserver(this, "profile-before-change");
+ }
+
+ if (AppConstants.platform == "macosx") {
+ Services.obs.addObserver(this, "new-directed-incoming-message");
+ }
+
+ if (AppConstants.platform == "win") {
+ Services.obs.addObserver(this, "windows-refresh-badge-tray");
+ Services.prefs.addObserver("mail.biff.show_badge", this);
+ Services.prefs.addObserver("mail.biff.show_tray_icon_always", this);
+ }
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "alertclickcallback":
+ // Display the associated message when an alert is clicked.
+ let msgHdr = Cc["@mozilla.org/messenger;1"]
+ .getService(Ci.nsIMessenger)
+ .msgHdrFromURI(data);
+ lazy.MailUtils.displayMessageInFolderTab(msgHdr, true);
+ return;
+ case "unread-im-count-changed":
+ this._logger.log(
+ `Unread chat count changed to ${this._unreadChatCount}`
+ );
+ this._unreadChatCount = parseInt(data, 10) || 0;
+ this._updateUnreadCount();
+ return;
+ case "new-directed-incoming-messenger":
+ this._animateDockIcon();
+ return;
+ case "windows-refresh-badge-tray":
+ this._updateUnreadCount();
+ return;
+ case "profile-before-change":
+ this._osIntegration?.onExit();
+ return;
+ case "newmailalert-closed":
+ // newmailalert.xhtml is closed, try to show the next queued folder.
+ this._customizedAlertShown = false;
+ this._showCustomizedAlert();
+ return;
+ case "nsPref:changed":
+ if (
+ data == "mail.biff.show_badge" ||
+ data == "mail.biff.show_tray_icon_always"
+ ) {
+ this._updateUnreadCount();
+ }
+ }
+ }
+
+ /**
+ * Following are nsIFolderListener interfaces. Do nothing about them.
+ */
+ onFolderAdded() {}
+ onMessageAdded() {}
+ onFolderRemoved() {}
+ onMessageRemoved() {}
+ onFolderPropertyChanged() {}
+ /**
+ * The only nsIFolderListener interface we care about.
+ *
+ * @see nsIFolderListener
+ */
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ if (!Services.prefs.getBoolPref("mail.biff.show_alert")) {
+ return;
+ }
+
+ this._logger.debug(
+ `onFolderIntPropertyChanged; property=${property}: ${oldValue} => ${newValue}, folder.URI=${folder.URI}`
+ );
+
+ switch (property) {
+ case "BiffState":
+ if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) {
+ // The folder argument is a root folder.
+ this._fillAlertInfo(folder);
+ }
+ break;
+ case "NewMailReceived":
+ // The folder argument is a real folder.
+ this._fillAlertInfo(folder);
+ break;
+ }
+ }
+ onFolderBoolPropertyChanged() {}
+ onFolderUnicharPropertyChanged() {}
+ onFolderPropertyFlagChanged() {}
+ onFolderEvent() {}
+
+ /**
+ * @see mozINewMailNotificationService
+ */
+ onCountChanged(count) {
+ this._logger.log(`Unread mail count changed to ${count}`);
+ this._unreadMailCount = count;
+ this._updateUnreadCount();
+ }
+
+ /**
+ * Show an alert according to the changed folder.
+ *
+ * @param {nsIMsgFolder} changedFolder - The folder that emitted the change
+ * event, can be a root folder or a real folder.
+ */
+ async _fillAlertInfo(changedFolder) {
+ let folder = this._getFirstRealFolderWithNewMail(changedFolder);
+ if (!folder) {
+ return;
+ }
+
+ let newMsgKeys = this._getNewMsgKeysNotNotified(folder);
+ let numNewMessages = newMsgKeys.length;
+ if (!numNewMessages) {
+ return;
+ }
+
+ this._logger.debug(
+ `Filling alert info; folder.URI=${folder.URI}, numNewMessages=${numNewMessages}`
+ );
+ let firstNewMsgHdr = folder.msgDatabase.getMsgHdrForKey(newMsgKeys[0]);
+
+ let title = this._getAlertTitle(folder, numNewMessages);
+ let body;
+ try {
+ body = await this._getAlertBody(folder, firstNewMsgHdr);
+ } catch (e) {
+ this._logger.error(e);
+ }
+ if (!title || !body) {
+ return;
+ }
+ this._showAlert(firstNewMsgHdr, title, body);
+ this._animateDockIcon();
+ }
+
+ /**
+ * Iterate the subfolders of changedFolder, return the first real folder with
+ * new mail.
+ *
+ * @param {nsIMsgFolder} changedFolder - The folder that emitted the change event.
+ * @returns {nsIMsgFolder} The first real folder.
+ */
+ _getFirstRealFolderWithNewMail(changedFolder) {
+ let folders = changedFolder.descendants;
+ folders.unshift(changedFolder);
+
+ for (let folder of folders) {
+ let flags = folder.flags;
+ if (
+ !(flags & Ci.nsMsgFolderFlags.Inbox) &&
+ flags & (Ci.nsMsgFolderFlags.SpecialUse | Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ // Do not notify if the folder is not Inbox but one of
+ // Drafts|Trash|SentMail|Templates|Junk|Archive|Queue or Virtual.
+ continue;
+ }
+
+ if (folder.getNumNewMessages(false) > 0) {
+ return folder;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the title for the alert.
+ *
+ * @param {nsIMsgFolder} folder - The changed folder.
+ * @param {number} numNewMessages - The count of new messages.
+ * @returns {string} The alert title.
+ */
+ _getAlertTitle(folder, numNewMessages) {
+ return this._bundle.formatStringFromName(
+ numNewMessages == 1
+ ? "newMailNotification_message"
+ : "newMailNotification_messages",
+ [folder.server.prettyName, numNewMessages.toString()]
+ );
+ }
+
+ /**
+ * Get the body for the alert.
+ *
+ * @param {nsIMsgFolder} folder - The changed folder.
+ * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the first new messages.
+ * @returns {string} The alert body.
+ */
+ async _getAlertBody(folder, msgHdr) {
+ await new Promise((resolve, reject) => {
+ let isAsync = folder.fetchMsgPreviewText([msgHdr.messageKey], {
+ OnStartRunningUrl() {},
+ // @see nsIUrlListener
+ OnStopRunningUrl(url, exitCode) {
+ Components.isSuccessCode(exitCode) ? resolve() : reject();
+ },
+ });
+ if (!isAsync) {
+ resolve();
+ }
+ });
+
+ let alertBody = "";
+
+ let subject = Services.prefs.getBoolPref("mail.biff.alert.show_subject")
+ ? msgHdr.mime2DecodedSubject
+ : "";
+ let author = "";
+ if (Services.prefs.getBoolPref("mail.biff.alert.show_sender")) {
+ let addressObjects = MailServices.headerParser.makeFromDisplayAddress(
+ msgHdr.mime2DecodedAuthor
+ );
+ let { name, email } = addressObjects[0] || {};
+ author = name || email;
+ }
+ if (subject && author) {
+ alertBody += this._bundle.formatStringFromName(
+ "newMailNotification_messagetitle",
+ [subject, author]
+ );
+ } else if (subject) {
+ alertBody += subject;
+ } else if (author) {
+ alertBody += author;
+ }
+ let showPreview = Services.prefs.getBoolPref(
+ "mail.biff.alert.show_preview"
+ );
+ if (showPreview) {
+ let previewLength = Services.prefs.getIntPref(
+ "mail.biff.alert.preview_length",
+ 40
+ );
+ let preview = msgHdr.getStringProperty("preview").slice(0, previewLength);
+ if (preview) {
+ alertBody += (alertBody ? "\n" : "") + preview;
+ }
+ }
+ return alertBody;
+ }
+
+ /**
+ * Show the alert.
+ *
+ * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the first new messages.
+ * @param {string} title - The alert title.
+ * @param {string} body - The alert body.
+ */
+ _showAlert(msgHdr, title, body) {
+ let folder = msgHdr.folder;
+
+ // Try to use system alert first.
+ if (
+ Services.prefs.getBoolPref("mail.biff.use_system_alert", true) &&
+ this._systemAlertAvailable
+ ) {
+ let alertsService = Cc["@mozilla.org/system-alerts-service;1"].getService(
+ Ci.nsIAlertsService
+ );
+ let cookie = folder.generateMessageURI(msgHdr.messageKey);
+ try {
+ let alert = Cc["@mozilla.org/alert-notification;1"].createInstance(
+ Ci.nsIAlertNotification
+ );
+ alert.init(
+ cookie, // name
+ "chrome://messenger/skin/icons/new-mail-alert.png",
+ title,
+ body,
+ true, // clickable
+ cookie
+ );
+ alertsService.showAlert(alert, this);
+ return;
+ } catch (e) {
+ this._logger.error(e);
+ this._systemAlertAvailable = false;
+ }
+ }
+
+ // The use_system_alert pref is false or showAlert somehow failed, use the
+ // customized alert window.
+ this._showCustomizedAlert(folder);
+ }
+
+ /**
+ * Show a customized alert window (newmailalert.xhtml), if there is already
+ * one showing, do not show another one, because the newer one will block the
+ * older one. Instead, save the folder and newMsgKeys to this._pendingFolders.
+ *
+ * @param {nsIMsgFolder} [folder] - The folder containing new messages.
+ */
+ _showCustomizedAlert(folder) {
+ if (this._customizedAlertShown) {
+ // Queue the folder.
+ this._pendingFolders.add(folder);
+ return;
+ }
+ if (!folder) {
+ // Get the next folder from the queue.
+ folder = this._pendingFolders.keys().next().value;
+ if (!folder) {
+ return;
+ }
+ this._pendingFolders.delete(folder);
+ }
+
+ let newMsgKeys = this._getNewMsgKeysNotNotified(folder);
+ if (!newMsgKeys.length) {
+ // No NEW message in the current folder, try the next queued folder.
+ this._showCustomizedAlert();
+ return;
+ }
+
+ let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ args.appendElement(folder);
+ args.appendElement({
+ wrappedJSObject: newMsgKeys,
+ });
+ args.appendElement(this);
+ Services.ww.openWindow(
+ null,
+ "chrome://messenger/content/newmailalert.xhtml",
+ "_blank",
+ "chrome,dialog=yes,titlebar=no,popup=yes",
+ args
+ );
+ this._customizedAlertShown = true;
+ this._folderBiffTime.set(folder, Date.now());
+ }
+
+ /**
+ * Get all NEW messages from a folder that we received after last biff time.
+ *
+ * @param {nsIMsgFolder} folder - The message folder to check.
+ * @returns {number[]} An array of message keys.
+ */
+ _getNewMsgKeysNotNotified(folder) {
+ let msgDb = folder.msgDatabase;
+ let lastBiffTime = this._folderBiffTime.get(folder) || 0;
+ return msgDb
+ .getNewList()
+ .slice(-folder.getNumNewMessages(false))
+ .filter(key => {
+ let msgHdr = msgDb.getMsgHdrForKey(key);
+ return msgHdr.dateInSeconds * 1000 > lastBiffTime;
+ });
+ }
+
+ async _updateUnreadCount() {
+ if (this._updatingUnreadCount) {
+ // _updateUnreadCount can be triggered faster than we finish rendering the
+ // badge. When that happens, set a flag and return.
+ this._pendingUpdate = true;
+ return;
+ }
+ this._updatingUnreadCount = true;
+
+ this._logger.debug(
+ `Update unreadMailCount=${this._unreadMailCount}, unreadChatCount=${this._unreadChatCount}`
+ );
+ let count = this._unreadMailCount + this._unreadChatCount;
+ let tooltip = "";
+ if (AppConstants.platform == "win") {
+ if (!Services.prefs.getBoolPref("mail.biff.show_badge", true)) {
+ count = 0;
+ }
+ if (count > 0) {
+ tooltip = await lazy.l10n.formatValue("unread-messages-os-tooltip", {
+ count,
+ });
+ }
+ await lazy.WinUnreadBadge.updateUnreadCount(count, tooltip);
+ }
+ this._osIntegration?.updateUnreadCount(count, tooltip);
+
+ this._updatingUnreadCount = false;
+ if (this._pendingUpdate) {
+ // There was at least one _updateUnreadCount call while we were rendering
+ // the badge. Render one more time will ensure the badge reflects the
+ // current state.
+ this._pendingUpdate = false;
+ this._updateUnreadCount();
+ }
+ }
+
+ _animateDockIcon() {
+ if (Services.prefs.getBoolPref("mail.biff.animate_dock_icon", false)) {
+ Services.wm.getMostRecentWindow("mail:3pane")?.getAttention();
+ }
+ }
+}