/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is messagingmenu-extension * * The Initial Developer of the Original Code is * Mozilla Messaging, Ltd. * Portions created by the Initial Developer are Copyright (C) 2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Mike Conley * Chris Coulson * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ var EXPORTED_SYMBOLS = ["MessagingMenu"]; const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/ctypes.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource:///modules/mailServices.js"); Cu.import("resource:///modules/iteratorUtils.jsm"); Cu.import("resource://messagingmenu/glib.jsm"); Cu.import("resource://messagingmenu/gobject.jsm"); Cu.import("resource://messagingmenu/dbusmenu.jsm"); Cu.import("resource://messagingmenu/indicate.jsm"); Cu.import("resource://messagingmenu/unity.jsm"); Cu.import("resource://messagingmenu/msgHdrUtils.jsm"); // I need the gdk lib to do focus hacking Cu.import("resource://messagingmenu/gdk.jsm"); ["LOG", "WARN", "ERROR"].forEach(function(aName) { this.__defineGetter__(aName, function() { Components.utils.import("resource://gre/modules/AddonLogging.jsm"); LogManager.getLogger("messagingmenu", this); return this[aName]; }); }, this); const FLDR_UNINTERESTING = Ci.nsMsgFolderFlags.Trash | Ci.nsMsgFolderFlags.Junk | Ci.nsMsgFolderFlags.SentMail | Ci.nsMsgFolderFlags.Drafts | Ci.nsMsgFolderFlags.Templates | Ci.nsMsgFolderFlags.Queue | Ci.nsMsgFolderFlags.Archive; const FOLDER_URL_KEY = "url"; const USER_SHARE_APPLICATIONS = "/usr/share/applications/"; const SYSTEM_LAUNCHER_ENTRIES = "/usr/share/indicators/messages/applications/"; const USER_LAUNCHER_ENTRIES = ".config/indicators/messages/applications/"; const USER_BLACKLIST_ENTRIES = ".config/indicators/messages/applications-blacklist/"; const MAX_INDICATORS = 6; const ADDON_ID = "messagingmenu@mozilla.com"; const PREF_ROOT = "extensions.messagingmenu."; const PREF_INCLUDE_NEWSGROUPS = "includeNewsgroups"; const PREF_INCLUDE_RSS = "includeRSS"; const PREF_ENABLED = "enabled"; const PREF_USER_LAUNCHER_PATH = "userLauncherPath"; const PREF_USER_LAUNCHER_MTIME = "userLauncherMTime"; const PREF_ATTENTION_FOR_ALL = "attentionForAll"; const PREF_INBOX_ONLY = "inboxOnly"; const PREF_ACCOUNTS = "mail.accountmanager.accounts"; const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; var open3PaneCallback; var contactsCallback; var composerCallback; var clickIndicatorCallback; function injectTimestamp(aTimestamp) { let atts = new gdk.GdkWindowAttributes; atts.window_type = 1; atts.x = atts.y = 0; atts.width = atts.height = 1; atts.wclass = 1; atts.event_mask = 0; let win = gdk.gdk_window_new(null, atts.address(), 0); gdk.gdk_x11_window_set_user_time(win, aTimestamp); gdk.gdk_window_destroy(win); } function openWindowByType(aType, aURL, aTimestamp) { if (aTimestamp) { injectTimestamp(aTimestamp); } let win = null; if (aType) { let wm = Cc["@mozilla.org/appshell/window-mediator;1"] .getService(Ci.nsIWindowMediator); if (wm) { win = wm.getMostRecentWindow(aType); } } if (win) { win.focus(); } else { let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] .getService(Ci.nsIWindowWatcher); if (ww) { ww.openWindow(null, aURL, "", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar", null); } } } function openAndFocus3Pane(aInstance, aTimestamp, aUserData) { LOG("Opening 3pane"); openWindowByType("mail:3pane", "chrome://messenger/content/messenger.xul", aTimestamp); } function openAndFocusAddressBook(aInstance, aTimestamp, aUserData) { LOG("Opening addressbook"); openWindowByType("mail:addressbook", "chrome://messenger/content/addressbook/addressbook.xul", aTimestamp); } function openAndFocusComposer(aInstance, aTimestamp, aUserData) { LOG("Opening composer"); injectTimestamp(aTimestamp); let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"] .getService(Ci.nsIWindowWatcher); if (ww) { ww.openWindow(null, "chrome://messenger/content/messengercompose/messengercompose.xul", "_blank", "chrome,extrachrome,menubar,resizable,scrollbars,status,toolbar", null); } } function onClickIndicator(aInstance, aTimestamp, aUserData) { let indicator = ctypes.cast(aInstance, indicate.Indicator.ptr); let folderURL = indicate.indicate_indicator_get_property(indicator, FOLDER_URL_KEY).readString(); LOG("Received click event on indicator for folder " + folderURL); let indicatorEntry = MessagingMenuEngine.mIndicators[folderURL]; if (!indicatorEntry) { WARN("No indicator for folder " + folderURL); return; } // Hide the indicator MessagingMenuEngine.hideIndicator(indicatorEntry); var msg = MessagingMenuEngine.messenger.msgHdrFromURI(indicatorEntry.messageURL); if(!msg) { WARN("Invalid message URI " + indicatorEntry.messageURL); return; } // Focus 3pane openAndFocus3Pane(aInstance, aTimestamp, aUserData); let win = null; let wm = Cc["@mozilla.org/appshell/window-mediator;1"] .getService(Ci.nsIWindowMediator); if (wm) { win = wm.getMostRecentWindow("mail:3pane"); } if (!win) { ERROR("Failed to open 3pane"); return; } win.document.getElementById("tabmail").switchToTab(0); win.gFolderTreeView.selectFolder(msg.folder); win.gFolderDisplay.selectMessage(msg); } function hasMultipleAccounts() { let count = 0; // We don't want to just call Count() on the account nsISupportsArray, as we // want to filter out accounts with "none" as the incoming server type // (eg, for Local Folders) for (let account in fixIterator(MailServices.accounts.accounts, Ci.nsIMsgAccount)) { if (account.incomingServer.type != "none") { count++ } } return count > 1; } // Helper class to wrap a nsIPrefBranch and allow the caller // to specify default values to be used where the pref doesn't exist function Prefs(aBranch) { this.branch = aBranch; } Prefs.prototype = { getBoolPref: function P_getBoolPref(aName, aDefaultValue) { try { return this.branch.getBoolPref(aName); } catch(e) { return aDefaultValue; } }, setCharPref: function P_setCharPref(aName, aValue) { this.branch.setCharPref(aName, aValue); }, getCharPref: function P_getCharPref(aName, aDefaultValue) { try { return this.branch.getCharPref(aName); } catch(e) { return aDefaultValue; } }, setIntPrefAsChar: function P_setIntPrefAsChar(aName, aValue) { this.setCharPref(aName, aValue.toString()); }, getIntPrefFromChar: function P_getIntPrefFromChar(aName, aDefaultValue) { return parseInt(this.getCharPref(aName, aDefaultValue.toString())); }, clearUserPref: function P_clearUserPref(aName) { this.branch.clearUserPref(aName); }, addObserver: function P_addObserver(aDomain, aObserver, aHoldWeak) { this.branch.QueryInterface(Ci.nsIPrefBranch2) .addObserver(aDomain, aObserver, aHoldWeak); }, removeObserver: function P_removeObserver(aDomain, aObserver) { this.branch.QueryInterface(Ci.nsIPrefBranch2) .removeObserver(aDomain, aObserver); } }; function LauncherEntryFind(aDir, aDesktopFile, aCallback) { new LauncherEntryFinder(aDir, aDesktopFile, aCallback); } // Small helper class which takes a directory containing messaging menu // launcher entries and tells the listener whether one of them is ours function LauncherEntryFinder(aDir, aDesktopFile, aCallback) { LOG("Searching for launcher entry for " + aDesktopFile + " in " + aDir.path); if (!aDir.exists() || !aDir.isDirectory()) { LOG(aDir.path + " does not exist or is not a directory"); aCallback(null); return; } this.callback = aCallback; this.desktopFile = aDesktopFile; this.entries = aDir.directoryEntries; this.dir = aDir; this.processNextEntry(); } LauncherEntryFinder.prototype = { processNextEntry: function MMEF_processNextEntry() { if (this.entries.hasMoreElements()) { var entry = this.entries.getNext().QueryInterface(Ci.nsIFile); if (!entry.isFile()) { this.processNextEntry(); } var self = this; NetUtil.asyncFetch(entry, function(inputStream, status) { let data = NetUtil.readInputStreamToString(inputStream, inputStream.available()); if (data.replace(/\n$/,"") == self.desktopFile) { LOG("Found launcher entry " + entry.path); self.callback(entry); } else { self.processNextEntry(); } }); } else { LOG("No launcher entry found"); this.callback(null); } } }; function MMMessageFilterState() { this._shouldIndicate = null; this._shouldRequestAttention = null; } MMMessageFilterState.prototype = { acceptMessageIfTrue: function(aTest, aMsg) { if (this._shouldIndicate !== null) return; if (aTest()) { LOG("Accepting message: " + aMsg); this._shouldIndicate = true; } }, rejectMessageIfTrue: function(aTest, aMsg) { if (this._shouldIndicate !== null) return; if (aTest()) { LOG("Rejecting message: " + aMsg); this._shouldIndicate = false; } }, requestAttentionIfTrue: function(aTest, aMsg) { if (this._shouldIndicate == false || this._shouldRequestAttention != null) return; if (aTest()) { LOG("Requesting attention for message: " + aMsg); this._shouldRequestAttention = true; } }, dontRequestAttentionIfTrue: function(aTest, aMsg) { if (this._shouldIndicate == false || this._shouldRequestAttention != null) return; if (aTest()) { LOG("Not requesting attention for message: " + aMsg); this._shouldRequestAttention = false; } }, get shouldIndicate() { return this._shouldIndicate == true; }, get shouldRequestAttention() { return this._shouldRequestAttention == true; } }; /* Given a particular message header, determines whether or not * it's something that's worth showing in the Messaging Menu. * * @param aItemHeader An nsIMsgDBHdr for a message. * @param aCallback A function to call if the message should * be indicated to the user */ function MMMessageFilter (aItemHeader, aCallback) { // FIXME: This function needs a bit of cleanup - I think the plinko-style // boolean flag stuff could be done a bit better. // See bug 806123: https://bugs.launchpad.net/messagingmenu-extension/+bug/806123 LOG("Applying filter for message " + aItemHeader.folder.getUriForMsg(aItemHeader) + " in " + aItemHeader.folder.folderURL); var state = new MMMessageFilterState(); var folder = aItemHeader.folder; state.rejectMessageIfTrue(function() { let junkScore = aItemHeader.getStringProperty("junkscore"); return (junkScore != "" && junkScore != "0"); }, "Message has junkscore != 0"); state.rejectMessageIfTrue(function() { return (folder.flags & FLDR_UNINTERESTING) != 0; }, "Message in blacklisted folder"); state.rejectMessageIfTrue(function() { return (aItemHeader.flags & Ci.nsMsgMessageFlags.New) == 0; }, "Message is not new"); state.acceptMessageIfTrue(function() { return (folder.flags & Ci.nsMsgFolderFlags.Mail) != 0; }, "Message is mail message"); state.acceptMessageIfTrue(function() { return (MessagingMenuEngine.prefs.getBoolPref(PREF_INCLUDE_NEWSGROUPS, true) && (folder.flags & Ci.nsMsgFolderFlags.Newsgroup) != 0); }, "Message is newsgroup message"); state.acceptMessageIfTrue(function() { return (MessagingMenuEngine.prefs.getBoolPref(PREF_INCLUDE_RSS, true) && folder.server.type == "rss"); }, "Message is from RSS feed"); msgHdrGetHeaders(aItemHeader, function (aHeaders) { state.requestAttentionIfTrue(function() { return MessagingMenuEngine.prefs.getBoolPref(PREF_ATTENTION_FOR_ALL, false); }, "attentionForAll preference is set to true"); state.requestAttentionIfTrue(function() { return aItemHeader.priority >= Ci.nsMsgPriority.high; }, "Message sent with high priority"); state.requestAttentionIfTrue(function() { return aItemHeader.isFlagged; }, "Message is starred"); state.dontRequestAttentionIfTrue(function() { return (aItemHeader.priority <= Ci.nsMsgPriority.low && aItemHeader.priority > Ci.nsMsgPriority.none); }, "Message sent with low priority"); // Use of Precedence is discouraged by RFC 2076, but we use this to catch // message from Launchpad state.dontRequestAttentionIfTrue(function() { return ((aHeaders.has("auto-submitted") && aHeaders.get("auto-submitted") != "no") || (aHeaders.has("precedence") && aHeaders.get("precedence") == "bulk")); }, "Automated message (Auto-Submitted != no || Precedence == bulk)"); state.dontRequestAttentionIfTrue(function() { return aHeaders.has("list-id"); }, "Mailing list message"); state.dontRequestAttentionIfTrue(function() { if (!aHeaders.has("return-path")) return false; let re = /.*<([^>]*)>/; let rp = aHeaders.get("return-path").replace(re, "$1"); let from = aItemHeader.author.replace(re, "$1"); return rp != from; }, "Possible automated message (Return-Path != From)"); state.dontRequestAttentionIfTrue(function() { if (!aHeaders.has("sender")) return false; let re = /.*<([^>]*)>/; let sender = aHeaders.get("sender").replace(re, "$1"); let from = aItemHeader.author.replace(re, "$1"); return sender != from; }, "Possible automated message (Sender != From)"); state.requestAttentionIfTrue(function() { let recipients = aItemHeader.recipients.split(","); let re = /.*<([^>]*)>/; // Convert "Foo " in to "bar" for (let i in recipients) { let recipient = recipients[i].replace(re, "$1"); for (let id in fixIterator(MailServices.accounts.allIdentities, Ci.nsIMsgIdentity)) { if (recipient.indexOf(id.email) != -1) return true; } } return false; }, "Message is addressed directly to us"); if (state.shouldIndicate) aCallback(state.shouldRequestAttention); }); } function MMIndicatorEntry (aFolder) { LOG("Creating indicator for folder " + aFolder.folderURL); this.folder = aFolder; this.indicator = indicate.indicate_indicator_new(); this.refreshLabel(); this.newCount = 0; this.dateInSeconds = 0; this.cancelAttention(); this.hide(); gobject.g_signal_connect(this.indicator, "user-display", clickIndicatorCallback, null); indicate.indicate_indicator_set_property(this.indicator, FOLDER_URL_KEY, aFolder.folderURL); Services.prefs.addObserver(PREF_ACCOUNTS, this, false); MessagingMenuEngine.prefs.addObserver("", this, false); } MMIndicatorEntry.prototype = { get indicator() { if (this._indicator) { return this._indicator; } throw "No IndicateIndicator. Have we been destroyed?"; }, set indicator(aIndicator) { this._indicator = aIndicator; }, requestAttention: function MMIE_requestAttention() { if (!this.active) { WARN("Attempting to request attention for an inactive indicator"); return; } if (this.visible) { this._requestAttention(); } else { LOG("Saving request for attention for folder " + this.label + " until we are visible"); } this._attention = true; }, _requestAttention: function MMIE__requestAttention() { LOG("Requesting attention for folder " + this.label); indicate.indicate_indicator_set_property(this.indicator, indicate.INDICATOR_MESSAGES_PROP_ATTENTION, "true"); }, cancelAttention: function MMIE_cancelAttention() { LOG("Cancelling attention for folder " + this.label); this._cancelAttention(); this._attention = false; }, _cancelAttention: function MMIE__cancelAttention() { indicate.indicate_indicator_set_property(this.indicator, indicate.INDICATOR_MESSAGES_PROP_ATTENTION, "false"); }, set label(aName) { LOG("Setting label for folder " + this.folder.folderURL + " to " + aName); indicate.indicate_indicator_set_property(this.indicator, indicate.INDICATOR_MESSAGES_PROP_NAME, aName); this._label = aName; }, get label() { return this._label; }, set newCount(aCount) { LOG("Setting unread count for folder " + this.label + " to " + aCount.toString()); indicate.indicate_indicator_set_property(this.indicator, indicate.INDICATOR_MESSAGES_PROP_COUNT, aCount.toString()); this._newCount = aCount; }, get newCount() { return this._newCount; }, get active() { return this._newCount > 0; }, show: function MMIE_show() { if (!this.active) { WARN("Attempting to display an inactive indicator"); return; } LOG("Showing indicator for folder " + this.label); indicate.indicate_indicator_show(this.indicator); // Ensure we really request attention now we are being made visible if (this._attention) this._requestAttention(); }, hide: function MMIE_hide() { LOG("Hiding indicator for folder " + this.label); indicate.indicate_indicator_hide(this.indicator); // Cancel our request for attention whilst we are hidden this._cancelAttention(); }, get visible() { return indicate.indicate_indicator_is_visible(this.indicator) != 0; }, get priority() { let score = 0; if (this.folder.flags & Ci.nsMsgFolderFlags.Inbox) { score += 3; } if (this._attention) { score += 1; } return score; }, isInbox: function MMIE_isInbox() { return this.folder.flags & Ci.nsMsgFolderFlags.Inbox; }, // We consider an indicator to be less important if: // 1) It has a lower priority, or // 2) It has the same priority and is more recent hasPriorityOver: function MMIE_hasPriorityOver(aIndicator) { return ((aIndicator.priority < this.priority) || ((aIndicator.dateInSeconds > this.dateInSeconds) && (aIndicator.priority == this.priority))) ? true : false; }, refreshLabel: function MMIE_refreshLabel() { let folder = this.folder; if (MessagingMenuEngine.prefs.getBoolPref("inboxOnly", false) && this.isInbox()) { this.label = folder.server.prettyName; } else if (hasMultipleAccounts()) { this.label = folder.prettiestName + " (" + folder.server.prettyName + ")"; } else { this.label = folder.prettiestName; } }, destroy: function MMIE_destroy() { LOG("Destroying indicator for folder " + this.folder.folderURL); gobject.g_object_unref(this.indicator); this.indicator = null; Services.prefs.removeObserver(PREF_ACCOUNTS, this); MessagingMenuEngine.prefs.removeObserver("", this); }, observe: function MMIE_observe(subject, topic, data) { if (data == PREF_ACCOUNTS) { // An account was added or removed. Note that this observer fires // before nsIMsgAccountManager is up-to-date, so we add the next // bit to the event loop LOG("Account settings updated. Updating label for folder " + this.folder.folderURL); var self = this; let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); if (timer) { var timerGrip = timer; timer.initWithCallback(function() { timerGrip = null; self.refreshLabel(); }, 0, Ci.nsITimer.TYPE_ONE_SHOT); } } else { this.refreshLabel(); } } }; var UnityLauncher = { get entry() { if (this._entry) return this._entry; var appName = Cc["@mozilla.org/xre/app-info;1"]. getService(Ci.nsIXULAppInfo).name.toLowerCase(); this._entry = unity.unity_launcher_entry_get_for_desktop_id(appName + ".desktop"); if (!this._entry) throw "Failed to create UnityLauncherEntry"; Services.obs.addObserver(this, "xpcom-will-shutdown", false); return this._entry; }, set entry(aEntry) { if (this._entry) gobject.g_object_unref(this._entry); this._entry = aEntry; }, observe: function UL_observe(aSubject, aTopic, aData) { if (aTopic == "xpcom-will-shutdown") { this.entry = null; if (unity.available()) { unity.close(); } } }, setCount: function UL_setCount(aCount) { if (!unity.available()) return; if (aCount === null) { unity.unity_launcher_entry_set_count_visible(this.entry, false); } else { unity.unity_launcher_entry_set_count(this.entry, aCount); unity.unity_launcher_entry_set_count_visible(this.entry, true); } } }; var MessagingMenuEngine = { initialized: false, enabled: false, mIndicators: {}, _visibleIndicators: 0, _badgeCount: 0, get messenger() { if (this._messenger) return this._messenger; this._messenger = Cc["@mozilla.org/messenger;1"].createInstance() .QueryInterface(Ci.nsIMessenger); return this._messenger; }, get desktopFile() { if (this._desktopFile) return this._desktopFile; var appName = Services.appinfo.name.toLowerCase(); this._desktopFile = USER_SHARE_APPLICATIONS + appName + ".desktop"; return this._desktopFile; }, get prefs() { if (this._prefs) return this._prefs; this._prefs = new Prefs(Services.prefs.getBranch(PREF_ROOT)); return this._prefs; }, get indicateServer() { if (this._indicateServer) return this._indicateServer; let indicateServer = indicate.indicate_server_ref_default(); indicate.indicate_server_set_type(indicateServer, "message.email"); indicate.indicate_server_set_desktop_file(indicateServer, this.desktopFile); let serverDisplayCB = ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [glib.gpointer, glib.guint, glib.gpointer]).ptr; open3PaneCallback = serverDisplayCB(openAndFocus3Pane); gobject.g_signal_connect(indicateServer, "server-display", open3PaneCallback, null); let bundle = Services.strings.createBundle( "chrome://messagingmenu/locale/messagingmenu.properties"); let server = dbusmenu.dbusmenu_server_new("/messaging/commands"); let root = dbusmenu.dbusmenu_menuitem_new(); let composeMi = dbusmenu.dbusmenu_menuitem_new(); dbusmenu.dbusmenu_menuitem_property_set(composeMi, "label", bundle.GetStringFromName("composeNewMessage")); dbusmenu.dbusmenu_menuitem_property_set_bool(composeMi, "visible", true); let menuItemActivatedCB = ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [glib.gpointer, glib.guint, glib.gpointer]).ptr; composerCallback = menuItemActivatedCB(openAndFocusComposer); gobject.g_signal_connect(composeMi, dbusmenu.MENUITEM_SIGNAL_ITEM_ACTIVATED, composerCallback, null); dbusmenu.dbusmenu_menuitem_child_append(root, composeMi); // I can't believe that this doesn't inherit from GInitiallyUnowned. // It really, really sucks that we need to do this.... gobject.g_object_unref(composeMi); let contactsMi = dbusmenu.dbusmenu_menuitem_new(); dbusmenu.dbusmenu_menuitem_property_set(contactsMi, "label", bundle.GetStringFromName("contacts")); dbusmenu.dbusmenu_menuitem_property_set_bool(contactsMi, "visible", true); contactsCallback = menuItemActivatedCB(openAndFocusAddressBook); gobject.g_signal_connect(contactsMi, dbusmenu.MENUITEM_SIGNAL_ITEM_ACTIVATED, contactsCallback, null); dbusmenu.dbusmenu_menuitem_child_append(root, contactsMi); gobject.g_object_unref(contactsMi); // This too dbusmenu.dbusmenu_server_set_root(server, root); gobject.g_object_unref(root); // And this... indicate.indicate_server_set_menu(indicateServer, server); gobject.g_object_unref(server); let displayCB = ctypes.FunctionType(ctypes.default_abi, ctypes.void_t, [glib.gpointer, glib.guint, glib.gpointer]).ptr; clickIndicatorCallback = displayCB(onClickIndicator); this._indicateServer = indicateServer; return this._indicateServer; }, get available() { return (gobject.available() && gdk.available() && dbusmenu.available() && indicate.available()); }, get badgeCount() { return this._badgeCount; }, set badgeCount(aCount) { LOG("Setting total new count to " + aCount.toString()); this._badgeCount = aCount; if (aCount > 0) { UnityLauncher.setCount(aCount); } else { UnityLauncher.setCount(null); } }, get visibleIndicators() { return this._visibleIndicators; }, set visibleIndicators(aCount) { if (aCount < 0) { ERROR("Invalid visibleIndicators count: " + aCount.toString()); } else if (this._visibleCount != aCount) { LOG("There are now " + aCount.toString() + " visible indicators"); this._visibleIndicators = aCount; } }, createLauncherEntry: function MME_createLauncherEntry(aDir) { if (!aDir.exists()) { aDir.create(Ci.nsILocalFile.DIRECTORY_TYPE, 0755); } let entry = aDir; entry.append(Services.appinfo.name.toLowerCase()); let ostream = FileUtils.openSafeFileOutputStream(entry, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE); let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; let istream = converter.convertToInputStream(this.desktopFile); var self = this; NetUtil.asyncCopy(istream, ostream, function() { self.prefs.setCharPref(PREF_USER_LAUNCHER_PATH, entry.path); self.prefs.setIntPrefAsChar(PREF_USER_LAUNCHER_MTIME, entry.lastModifiedTime); }); }, init: function MME_init() { if (this.initialized) return; LOG("Initializing MessagingMenu"); if (!this.available) { WARN("The required libraries aren't available"); this.initialized = true; return; } // Check if we have a static launcher entry in the messaging menu. If we // don't, then we should add one and also display "Contacts" and "Compose" // menu items. If there is one, then it was probably added by the Thunderbird // package. We don't need to create one or show the extra menu items in that case let sysLauncherEntriesDir = Cc["@mozilla.org/file/local;1"] .createInstance(Ci.nsILocalFile); sysLauncherEntriesDir.initWithPath(SYSTEM_LAUNCHER_ENTRIES); LauncherEntryFind(sysLauncherEntriesDir, this.desktopFile, function(aFile) { if(!aFile) { // There is no system-provided static launcher entry for us in the // messaging menu var userLauncherEntriesDir = Services.dirsvc.get("Home", Ci.nsILocalFile); userLauncherEntriesDir.appendRelativePath(USER_LAUNCHER_ENTRIES); LauncherEntryFind(userLauncherEntriesDir, this.desktopFile, function(aFile) { if (!aFile) { MessagingMenuEngine.createLauncherEntry(userLauncherEntriesDir); } }); } }); AddonManager.addAddonListener(this); this.prefs.addObserver("", this, false); Services.obs.addObserver(this, "xpcom-will-shutdown", false); this.badgeCount = 0; if (this.prefs.getBoolPref(PREF_ENABLED, true)) { this.enable(); } else { this.disableAndHide(); } this.initialized = true; }, enable: function MME_enable() { if (this.enabled) { WARN("Trying to enable more than once"); return; } LOG("Enabling messaging indicator"); if (!this.indicateServer) { Cu.reportError("Could not construct the Messaging Menu server."); return; } indicate.indicate_server_show(this.indicateServer); let userBlacklistDir = Services.dirsvc.get("Home", Ci.nsILocalFile); userBlacklistDir.appendRelativePath(USER_BLACKLIST_ENTRIES); LauncherEntryFind(userBlacklistDir, this.desktopFile, function(aFile) { if (aFile) { LOG("Removing launcher entry " + aFile.path); aFile.remove(false); } }); let notificationFlags = Ci.nsIFolderListener.added | Ci.nsIFolderListener.boolPropertyChanged MailServices.mailSession.AddFolderListener(this, notificationFlags); this.enabled = true; }, disableAndHide: function MME_disableAndHide() { LOG("Hiding messaging indicator"); var userBlacklistDir = Services.dirsvc.get("Home", Ci.nsILocalFile); userBlacklistDir.appendRelativePath(USER_BLACKLIST_ENTRIES); LauncherEntryFind(userBlacklistDir, this.desktopFile, function(aFile) { if (aFile) return; if (!userBlacklistDir.exists()) { userBlacklistDir.create(Ci.nsILocalFile.DIRECTORY_TYPE, 0755); } let entry = userBlacklistDir.clone(); entry.append(Services.appinfo.name.toLowerCase()); let ostream = FileUtils.openSafeFileOutputStream(entry, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE); let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] .createInstance(Ci.nsIScriptableUnicodeConverter); converter.charset = "UTF-8"; let istream = converter.convertToInputStream(this.desktopFile); var self = this; NetUtil.asyncCopy(istream, ostream, null); }); this.disable(); }, cleanup: function MME_cleanup() { // We're being uninstalled or disabled. If we created a launcher // entry in the messaging menu, make sure we clean it up let userLauncherEntry = this.prefs.getCharPref(PREF_USER_LAUNCHER_PATH, null); let userLauncherEntryMTime = this.prefs.getIntPrefFromChar(PREF_USER_LAUNCHER_MTIME, 0); this.prefs.clearUserPref(PREF_USER_LAUNCHER_PATH); this.prefs.clearUserPref(PREF_USER_LAUNCHER_MTIME); if (userLauncherEntry) { let userLauncherEntryFile = Cc["@mozilla.org/file/local;1"] .createInstance(Ci.nsILocalFile); userLauncherEntryFile.initWithPath(userLauncherEntry); if (userLauncherEntryFile.exists() && userLauncherEntryFile.isFile() && userLauncherEntryFile.lastModifiedTime == userLauncherEntryMTime) { LOG("Removing launcher entry at " + userLauncherEntry); userLauncherEntryFile.remove(false); } } }, disable: function MME_disable() { if (!this.enabled) return; LOG("Disabling messaging indicator"); MailServices.mailSession.RemoveFolderListener(this); // Remove references for any leftover indicators for (let key in this.mIndicators) this.mIndicators[key].destroy(); this.mIndicators = {}; this.visibleIndicators = 0; if (this._indicateServer) indicate.indicate_server_hide(this._indicateServer); this.badgeCount = 0; this.enabled = false; }, shutdown: function MME_shutdown() { if (!this.initialized) { WARN("Calling shutdown before we are initialized"); return; } LOG("Shutting down"); this.disable(); if (this._indicateServer) gobject.g_object_unref(this._indicateServer); this._indicateServer = null; AddonManager.removeAddonListener(this); Services.obs.removeObserver(this, "xpcom-will-shutdown"); this.prefs.removeObserver("", this); this.initialized = false; }, refreshBadgeCount: function MME_refreshBadgeCount() { let inboxOnly = this.prefs.getBoolPref(PREF_INBOX_ONLY, false); let accumulator = 0; for (let url in this.mIndicators) { if (!this.mIndicators[url].isInbox() && inboxOnly) continue; accumulator += this.mIndicators[url].newCount; } this.badgeCount = accumulator; }, refreshVisibility: function MME_refreshVisibility() { let inboxOnly = this.prefs.getBoolPref(PREF_INBOX_ONLY, false); for (let url in this.mIndicators) { let indicator = this.mIndicators[url]; if (!indicator.isInbox() && inboxOnly) { if (indicator.visible) { indicator.hide(); this.visibleIndicators--; } } else { this.maybeShowIndicator(indicator); } } }, maybeShowIndicator: function MME_maybeShowIndicator(aIndicator) { LOG("Maybe showing indicator for folder " + aIndicator.label); LOG("Indicator priority is " + aIndicator.priority.toString()); if (!aIndicator.active) { LOG("Not showing inactive indicator"); return; } // Don't show more than MAX_INDICATORS indicators if (this.visibleIndicators < MAX_INDICATORS && !aIndicator.visible) { aIndicator.show(); LOG("Showing indicator"); this.visibleIndicators++; } else { if (aIndicator.visible) { LOG("Indicator is already visible"); return; } // We are already displaying MAX_INDICATORS. Lets see if one of the // current ones can be bumped off, to make way for the new one LOG("There are already " + MAX_INDICATORS.toString() + " visible indicators"); let doomedIndicator = null // This will make your head explode, but basically, what we want to do // is iterate over the currently displayed indicator entries and see if // one of them should make way for the new indicator. for (let url in this.mIndicators) { let existingIndicator = this.mIndicators[url]; // This one already isn't visible, so don't care... if (!existingIndicator.visible) { continue; } let refIndicator = doomedIndicator ? doomedIndicator : aIndicator; if (refIndicator.hasPriorityOver(existingIndicator)) { LOG("Indicator with priority=" + existingIndicator.priority.toString() + " and dateInSeconds=" + existingIndicator.dateInSeconds + " is " + " a candidate for hiding"); doomedIndicator = existingIndicator; } } if (doomedIndicator) { LOG("Showing indicator"); doomedIndicator.hide(); aIndicator.show(); } } }, /* Given a message header, displays an indicator for it's folder * and requests attention * * @param aItemHeader A nsIMsgDBHdr for the message that we're * trying to show in the Messaging Menu. */ doIndication: function MME_doIndication(aItemHeader, aShouldRequestAtt) { let itemFolder = aItemHeader.folder; let folderURL = itemFolder.folderURL; LOG("Doing indication for folder " + folderURL); if (!this.mIndicators[folderURL]) { // Create an indicator for this folder if one doesn't already exist this.mIndicators[folderURL] = new MMIndicatorEntry(itemFolder); } let indicator = this.mIndicators[folderURL]; LOG("Current indicator dateInSeconds = " + indicator.dateInSeconds.toString()); LOG("Message item dateInSeconds = " + aItemHeader.dateInSeconds.toString()); if (indicator.active) { LOG("Indicator for folder is already active"); } if (!indicator.active || (indicator.dateInSeconds > aItemHeader.dateInSeconds)) { indicator.messageURL = itemFolder.getUriForMsg(aItemHeader); indicator.dateInSeconds = aItemHeader.dateInSeconds; } indicator.newCount += 1; if (aShouldRequestAtt) { indicator.requestAttention(); } if (!indicator.isInbox() && this.prefs.getBoolPref("inboxOnly", false)) { LOG("Suppressing non-inbox indicator in inbox-only mode"); return; } this.badgeCount += 1; this.maybeShowIndicator(indicator); }, hideIndicator: function MME_hideIndicator(aIndicator) { if (aIndicator.visible) { aIndicator.hide(); this.visibleIndicators--; } let savedCount = aIndicator.newCount; aIndicator.cancelAttention(); aIndicator.newCount = 0; let oldBadgeCount = this.badgeCount; this.refreshBadgeCount(); if (oldBadgeCount != (this.badgeCount + savedCount)) { WARN("The badge count got out of sync with the actual number of new messages"); } if (this.visibleIndicators >= MAX_INDICATORS) { return; } // Now see if there are any pending indicators to be shown. // If there are more than one, we give priority to the indicator // with the highest priority. If there are more than one of those, // then we give priority to the one which has been waiting the longest for (let url in this.mIndicators) { let indicator = this.mIndicators[url]; if (!indicator.isInbox() && this.prefs.getBoolPref("inboxOnly", false)) { continue; } this.maybeShowIndicator(indicator); } }, /* Observes when items are added to folders, and when * message flags change. Also listens for notifications * sent by uMessagingMenuService for opening messages based * on an Indicator that was clicked. */ OnItemAdded: function MME_OnItemAdded(parentItem, item) { if (item instanceof Ci.nsIMsgDBHdr) { LOG("Item " + item.folder.getUriForMsg(item) + " added to " + item.folder.folderURL); let self = this; MMMessageFilter(item, function(aShouldRequestAtt) { self.doIndication(item, aShouldRequestAtt); }); } }, OnItemBoolPropertyChanged: function MME_OnItemBoolPropertyChanged(item, property, oldValue, newValue) { if (item instanceof Ci.nsIMsgFolder && item.folderURL in this.mIndicators && property.toString() == "NewMessages" && newValue == false) { LOG("Folder " + item.folderURL + " no longer has new messages"); this.hideIndicator(this.mIndicators[item.folderURL]); } }, observe: function(aSubject, aTopic, aData) { if (aTopic == "xpcom-will-shutdown") { LOG("Got shutdown notification"); this.shutdown(); } else if (aTopic == NS_PREFBRANCH_PREFCHANGE_TOPIC_ID) { LOG("Got prefchange notification for " + aData); if (aData == PREF_ENABLED) { let prefs = new Prefs(aSubject.QueryInterface(Ci.nsIPrefBranch)); let enabled = prefs.getBoolPref(aData, true); if (enabled) { this.enable(); } else { this.disableAndHide(); } } else if (aData == PREF_INBOX_ONLY) { this.refreshVisibility(); this.refreshBadgeCount(); } } else { WARN("Observer notification not intended for us: " + aTopic); } }, onUninstalling: function(aAddon, aNeedsRestart) { if (aAddon.id == ADDON_ID) { LOG("Addon is being uninstalled"); this.cleanup(); } }, onDisabling: function(aAddon, aNeedsRestart) { if (aAddon.id == ADDON_ID) { LOG("Addon is being disabled"); this.disableAndHide(); } }, onEnabling: function(aAddon, aNeedsRestart) { if (aAddon.id == ADDON_ID) { LOG("Addon is being enabled"); this.enable(); } } } MessagingMenuEngine.init(); var MessagingMenu = { get available() { return MessagingMenuEngine.available; } };