summaryrefslogtreecommitdiffstats
path: root/comm/suite/mailnews
diff options
context:
space:
mode:
Diffstat (limited to 'comm/suite/mailnews')
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abCardOverlay.js1397
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abCardOverlay.xul515
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abCardViewOverlay.js527
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abCommon.js1185
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abEditCardDialog.xul18
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abEditListDialog.xul22
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abListOverlay.xul86
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abMailListDialog.xul34
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abNewCardDialog.xul35
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abResultsPaneOverlay.xul94
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.js399
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.xul92
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abTrees.js332
-rw-r--r--comm/suite/mailnews/components/addrbook/content/addressbook-panel.js119
-rw-r--r--comm/suite/mailnews/components/addrbook/content/addressbook-panel.xul96
-rw-r--r--comm/suite/mailnews/components/addrbook/content/addressbook.js581
-rw-r--r--comm/suite/mailnews/components/addrbook/content/addressbook.xul722
-rw-r--r--comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.js24
-rw-r--r--comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.xul92
-rw-r--r--comm/suite/mailnews/components/addrbook/jar.mn24
-rw-r--r--comm/suite/mailnews/components/addrbook/moz.build8
-rw-r--r--comm/suite/mailnews/components/calendar/content/suite-overlay-addons.xhtml39
-rw-r--r--comm/suite/mailnews/components/calendar/content/suite-overlay-preferences.xhtml66
-rw-r--r--comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.js47
-rw-r--r--comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.xhtml39
-rw-r--r--comm/suite/mailnews/components/compose/content/MsgComposeCommands.js3936
-rw-r--r--comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js1167
-rw-r--r--comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul16
-rw-r--r--comm/suite/mailnews/components/compose/content/messengercompose.xul720
-rw-r--r--comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul23
-rw-r--r--comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js30
-rw-r--r--comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul212
-rw-r--r--comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js151
-rw-r--r--comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul120
-rw-r--r--comm/suite/mailnews/components/compose/jar.mn14
-rw-r--r--comm/suite/mailnews/components/compose/moz.build6
-rw-r--r--comm/suite/mailnews/components/moz.build11
-rw-r--r--comm/suite/mailnews/components/prefs/content/mailPrefsOverlay.xul102
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-character_encoding.js41
-rwxr-xr-xcomm/suite/mailnews/components/prefs/content/pref-character_encoding.xul111
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-junk.js45
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-junk.xul134
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-mailnews.js25
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-mailnews.xul141
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-notifications.js91
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-notifications.xul187
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-offline.js19
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-offline.xul121
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-receipts.js28
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-receipts.xul146
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-tags.js478
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-tags.xul83
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-viewing_messages.js26
-rw-r--r--comm/suite/mailnews/components/prefs/content/pref-viewing_messages.xul174
-rw-r--r--comm/suite/mailnews/components/prefs/jar.mn23
-rw-r--r--comm/suite/mailnews/components/prefs/moz.build6
-rw-r--r--comm/suite/mailnews/components/smime/content/msgCompSMIMEOverlay.js354
-rw-r--r--comm/suite/mailnews/components/smime/content/msgHdrViewSMIMEOverlay.js258
-rw-r--r--comm/suite/mailnews/components/smime/jar.mn15
-rw-r--r--comm/suite/mailnews/components/smime/moz.build6
-rw-r--r--comm/suite/mailnews/content/ABSearchDialog.js327
-rw-r--r--comm/suite/mailnews/content/ABSearchDialog.xul99
-rw-r--r--comm/suite/mailnews/content/FilterListDialog.js1037
-rw-r--r--comm/suite/mailnews/content/FilterListDialog.xul197
-rw-r--r--comm/suite/mailnews/content/SearchDialog.js729
-rw-r--r--comm/suite/mailnews/content/SearchDialog.xul178
-rw-r--r--comm/suite/mailnews/content/browserRequest.js107
-rw-r--r--comm/suite/mailnews/content/browserRequest.xul34
-rw-r--r--comm/suite/mailnews/content/commandglue.js989
-rw-r--r--comm/suite/mailnews/content/folderDisplay.js142
-rw-r--r--comm/suite/mailnews/content/folderPane.js2221
-rw-r--r--comm/suite/mailnews/content/folderPane.xul169
-rw-r--r--comm/suite/mailnews/content/mail-offline.js164
-rw-r--r--comm/suite/mailnews/content/mail3PaneWindowCommands.js1057
-rw-r--r--comm/suite/mailnews/content/mailCommands.js415
-rw-r--r--comm/suite/mailnews/content/mailContextMenus.js828
-rw-r--r--comm/suite/mailnews/content/mailEditorOverlay.xul61
-rw-r--r--comm/suite/mailnews/content/mailKeysOverlay.xul64
-rw-r--r--comm/suite/mailnews/content/mailOverlay.js30
-rw-r--r--comm/suite/mailnews/content/mailOverlay.xul29
-rw-r--r--comm/suite/mailnews/content/mailTasksOverlay.js250
-rw-r--r--comm/suite/mailnews/content/mailTasksOverlay.xul64
-rw-r--r--comm/suite/mailnews/content/mailViewList.js161
-rw-r--r--comm/suite/mailnews/content/mailViewList.xul79
-rw-r--r--comm/suite/mailnews/content/mailViewSetup.js119
-rw-r--r--comm/suite/mailnews/content/mailViewSetup.xul51
-rw-r--r--comm/suite/mailnews/content/mailWidgets.xml1946
-rw-r--r--comm/suite/mailnews/content/mailWindow.js593
-rw-r--r--comm/suite/mailnews/content/mailWindowOverlay.js2695
-rw-r--r--comm/suite/mailnews/content/mailWindowOverlay.xul1929
-rw-r--r--comm/suite/mailnews/content/messageWindow.js1044
-rw-r--r--comm/suite/mailnews/content/messageWindow.xul141
-rw-r--r--comm/suite/mailnews/content/messenger.css236
-rw-r--r--comm/suite/mailnews/content/messenger.xul275
-rw-r--r--comm/suite/mailnews/content/msgFolderPickerOverlay.js100
-rw-r--r--comm/suite/mailnews/content/msgHdrViewOverlay.js1971
-rw-r--r--comm/suite/mailnews/content/msgHdrViewOverlay.xul273
-rw-r--r--comm/suite/mailnews/content/msgMail3PaneWindow.js1265
-rw-r--r--comm/suite/mailnews/content/msgViewNavigation.js243
-rw-r--r--comm/suite/mailnews/content/msgViewPickerOverlay.js413
-rw-r--r--comm/suite/mailnews/content/nsDragAndDrop.js595
-rw-r--r--comm/suite/mailnews/content/phishingDetector.js173
-rw-r--r--comm/suite/mailnews/content/searchBar.js432
-rw-r--r--comm/suite/mailnews/content/searchTermOverlay.xul64
-rw-r--r--comm/suite/mailnews/content/start.xhtml69
-rw-r--r--comm/suite/mailnews/content/tabmail.js969
-rw-r--r--comm/suite/mailnews/content/tabmail.xml1583
-rw-r--r--comm/suite/mailnews/content/threadPane.js598
-rw-r--r--comm/suite/mailnews/content/threadPane.xul91
-rw-r--r--comm/suite/mailnews/jar.mn70
-rw-r--r--comm/suite/mailnews/modules/MailUtils.js100
-rw-r--r--comm/suite/mailnews/modules/moz.build8
-rw-r--r--comm/suite/mailnews/moz.build11
113 files changed, 43101 insertions, 0 deletions
diff --git a/comm/suite/mailnews/components/addrbook/content/abCardOverlay.js b/comm/suite/mailnews/components/addrbook/content/abCardOverlay.js
new file mode 100644
index 0000000000..06167ca240
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abCardOverlay.js
@@ -0,0 +1,1397 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const kNonVcardFields =
+ ["NickNameContainer", "SecondaryEmailContainer", "ScreenNameContainer",
+ "customFields", "preferDisplayName"];
+
+const kPhoneticFields =
+ ["PhoneticLastName", "PhoneticLabel1", "PhoneticSpacer1",
+ "PhoneticFirstName", "PhoneticLabel2", "PhoneticSpacer2"];
+
+// Item is |[dialogField, cardProperty]|.
+const kVcardFields =
+ [ // Contact > Name
+ ["FirstName", "FirstName"],
+ ["LastName", "LastName"],
+ ["DisplayName", "DisplayName"],
+ ["NickName", "NickName"],
+ // Contact > Internet
+ ["PrimaryEmail", "PrimaryEmail"],
+ ["SecondEmail", "SecondEmail"],
+ // Contact > Phones
+ ["WorkPhone", "WorkPhone"],
+ ["HomePhone", "HomePhone"],
+ ["FaxNumber", "FaxNumber"],
+ ["PagerNumber", "PagerNumber"],
+ ["CellularNumber", "CellularNumber"],
+ // Address > Home
+ ["HomeAddress", "HomeAddress"],
+ ["HomeAddress2", "HomeAddress2"],
+ ["HomeCity", "HomeCity"],
+ ["HomeState", "HomeState"],
+ ["HomeZipCode", "HomeZipCode"],
+ ["HomeCountry", "HomeCountry"],
+ ["WebPage2", "WebPage2"],
+ // Address > Work
+ ["JobTitle", "JobTitle"],
+ ["Department", "Department"],
+ ["Company", "Company"],
+ ["WorkAddress", "WorkAddress"],
+ ["WorkAddress2", "WorkAddress2"],
+ ["WorkCity", "WorkCity"],
+ ["WorkState", "WorkState"],
+ ["WorkZipCode", "WorkZipCode"],
+ ["WorkCountry", "WorkCountry"],
+ ["WebPage1", "WebPage1"],
+ // Other > (custom)
+ ["Custom1", "Custom1"],
+ ["Custom2", "Custom2"],
+ ["Custom3", "Custom3"],
+ ["Custom4", "Custom4"],
+ // Other > Notes
+ ["Notes", "Notes"],
+ // Chat
+ ["Yahoo", "_Yahoo"],
+ ["Skype", "_Skype"],
+ ["QQ", "_QQ"],
+ ["MSN", "_MSN"],
+ ["ICQ", "_ICQ"],
+ ["XMPP", "_JabberId"],
+ ["IRC", "_IRC"]
+ ];
+
+var gEditCard;
+var gOnSaveListeners = [];
+var gOnLoadListeners = [];
+var gOkCallback = null;
+var gHideABPicker = false;
+var gPhotoHandlers = {};
+// If any new photos were added to the card, this stores the name of the original
+// and any temporary new filenames used to store photos of the card.
+// 'null' is a valid value when there was no photo (e.g. the generic photo).
+var gOldPhotos = [];
+// If a new photo was added, the name is stored here.
+var gNewPhoto = null;
+
+function OnLoadNewCard()
+{
+ InitEditCard();
+
+ gEditCard.card =
+ (("arguments" in window) && (window.arguments.length > 0) &&
+ (window.arguments[0] instanceof Ci.nsIAbCard))
+ ? window.arguments[0]
+ : Cc["@mozilla.org/addressbook/cardproperty;1"]
+ .createInstance(Ci.nsIAbCard);
+ gEditCard.titleProperty = "newContactTitle";
+ gEditCard.selectedAB = "";
+
+ if ("arguments" in window && window.arguments[0])
+ {
+ gEditCard.selectedAB = kPersonalAddressbookURI;
+
+ if ("selectedAB" in window.arguments[0] &&
+ (window.arguments[0].selectedAB != kAllDirectoryRoot + "?")) {
+ // check if selected ab is a mailing list
+ var abURI = window.arguments[0].selectedAB;
+
+ var directory = GetDirectoryFromURI(abURI);
+ if (directory.isMailList) {
+ var parentURI = GetParentDirectoryFromMailingListURI(abURI);
+ if (parentURI)
+ gEditCard.selectedAB = parentURI;
+ }
+ else if (!directory.readOnly)
+ gEditCard.selectedAB = window.arguments[0].selectedAB;
+ }
+
+ // we may have been given properties to pre-initialize the window with....
+ // we'll fill these in here...
+ if ("primaryEmail" in window.arguments[0])
+ gEditCard.card.primaryEmail = window.arguments[0].primaryEmail;
+ if ("displayName" in window.arguments[0]) {
+ gEditCard.card.displayName = window.arguments[0].displayName;
+ // if we've got a display name, don't generate
+ // a display name (and stomp on the existing display name)
+ // when the user types a first or last name
+ if (gEditCard.card.displayName)
+ gEditCard.generateDisplayName = false;
+ }
+ if ("okCallback" in window.arguments[0])
+ gOkCallback = window.arguments[0].okCallback;
+
+ if ("escapedVCardStr" in window.arguments[0]) {
+ // hide non vcard values
+ HideNonVcardFields();
+ gEditCard.card = Cc["@mozilla.org/addressbook/msgvcardservice;1"]
+ .getService(Ci.nsIMsgVCardService)
+ .escapedVCardToAbCard(window.arguments[0].escapedVCardStr);
+ }
+
+ if ("titleProperty" in window.arguments[0])
+ gEditCard.titleProperty = window.arguments[0].titleProperty;
+
+ if ("hideABPicker" in window.arguments[0])
+ gHideABPicker = window.arguments[0].hideABPicker;
+ }
+
+ // set popup with address book names
+ var abPopup = document.getElementById('abPopup');
+ abPopup.value = gEditCard.selectedAB || kPersonalAddressbookURI;
+
+ if (gHideABPicker && abPopup) {
+ abPopup.hidden = true;
+ document.getElementById("abPopupLabel").hidden = true;
+ }
+
+ SetCardDialogTitle(gEditCard.card.displayName);
+
+ GetCardValues(gEditCard.card, document);
+
+ // FIX ME - looks like we need to focus on both the text field and the tab widget
+ // probably need to do the same in the addressing widget
+
+ // focus on first or last name based on the pref
+ var focus = document.getElementById(gEditCard.displayLastNameFirst
+ ? "LastName" : "FirstName");
+ if (focus) {
+ // XXX Using the setTimeout hack until bug 103197 is fixed
+ setTimeout( function(firstTextBox) { firstTextBox.focus(); }, 0, focus );
+ }
+}
+
+/**
+ * Get the source directory containing the card we are editing.
+ */
+function getContainingDirectory() {
+ let directory = GetDirectoryFromURI(gEditCard.abURI);
+ // If the source directory is "All Address Books", find the parent
+ // address book of the card being edited and reflect the changes in it.
+ if (directory.URI == kAllDirectoryRoot + "?") {
+ let dirId =
+ gEditCard.card.directoryId
+ .substring(0, gEditCard.card.directoryId.indexOf("&"));
+ directory = MailServices.ab.getDirectoryFromId(dirId);
+ }
+ return directory;
+}
+
+function EditCardOKButton()
+{
+ if (!CheckCardRequiredDataPresence(document))
+ return false; // don't close window
+
+ // See if this card is in any mailing list
+ // if so then we need to update the addresslists of those mailing lists
+ let directory = getContainingDirectory();
+
+ // if the directory is a mailing list we need to search all the mailing lists
+ // in the parent directory if the card exists.
+ if (directory.isMailList) {
+ var parentURI = GetParentDirectoryFromMailingListURI(gEditCard.abURI);
+ directory = GetDirectoryFromURI(parentURI);
+ }
+
+ var listDirectoriesCount = directory.addressLists.length;
+ var foundDirectories = [];
+
+ // create a list of mailing lists and the index where the card is at.
+ for (let i = 0; i < listDirectoriesCount; i++)
+ {
+ var subdirectory = directory.addressLists.queryElementAt(i, Ci.nsIAbDirectory);
+ if (subdirectory.isMailList)
+ {
+ // See if any card in this list is the one we edited.
+ // Must compare card contents using .equals() instead of .indexOf()
+ // because gEditCard is not really a member of the .addressLists array.
+ let listCardsCount = subdirectory.addressLists.length;
+ for (let index = 0; index < listCardsCount; index++)
+ {
+ let card = subdirectory.addressLists.queryElementAt(index, Ci.nsIAbCard);
+ if (card.equals(gEditCard.card))
+ foundDirectories.push({directory:subdirectory, cardIndex:index});
+ }
+ }
+ }
+
+ CheckAndSetCardValues(gEditCard.card, document, false);
+
+ directory.modifyCard(gEditCard.card);
+
+ while (foundDirectories.length)
+ {
+ // Update the addressLists item for this card
+ let foundItem = foundDirectories.pop();
+ foundItem.directory.addressLists.replaceElementAt(gEditCard.card, foundItem.cardIndex);
+ }
+
+ NotifySaveListeners(directory);
+
+ // callback to allow caller to update
+ if (gOkCallback)
+ gOkCallback();
+
+ return true; // close the window
+}
+
+function EditCardCancelButton()
+{
+ // If a new photo was created, remove it now as it won't be used.
+ purgeOldPhotos(false);
+}
+
+function OnLoadEditCard()
+{
+ InitEditCard();
+
+ gEditCard.titleProperty = "editContactTitle";
+
+ if (window.arguments && window.arguments[0])
+ {
+ if ( window.arguments[0].card )
+ gEditCard.card = window.arguments[0].card;
+ if ( window.arguments[0].okCallback )
+ gOkCallback = window.arguments[0].okCallback;
+ if ( window.arguments[0].abURI )
+ gEditCard.abURI = window.arguments[0].abURI;
+ }
+
+ // set global state variables
+ // if first or last name entered, disable generateDisplayName
+ if (gEditCard.generateDisplayName &&
+ (gEditCard.card.firstName.length +
+ gEditCard.card.lastName.length +
+ gEditCard.card.displayName.length > 0))
+ {
+ gEditCard.generateDisplayName = false;
+ }
+
+ GetCardValues(gEditCard.card, document);
+
+ SetCardDialogTitle(gEditCard.card.displayName);
+
+ // check if selectedAB is a writeable
+ // if not disable all the fields
+ if ("arguments" in window && window.arguments[0])
+ {
+ if ("abURI" in window.arguments[0]) {
+ var abURI = window.arguments[0].abURI;
+ var directory = GetDirectoryFromURI(abURI);
+
+ if (directory.readOnly)
+ {
+ // Set all the editable vcard fields to read only
+ for (var i = kVcardFields.length; i-- > 0; )
+ document.getElementById(kVcardFields[i][0]).readOnly = true;
+
+ // the birthday fields
+ document.getElementById("Birthday").readOnly = true;
+ document.getElementById("BirthYear").readOnly = true;
+ document.getElementById("Age").readOnly = true;
+
+ // the photo field and buttons
+ document.getElementById("PhotoType").disabled = true;
+ document.getElementById("GenericPhotoList").disabled = true;
+ document.getElementById("PhotoURI").disabled = true;
+ document.getElementById("PhotoURI").placeholder = "";
+ document.getElementById("BrowsePhoto").disabled = true;
+ document.getElementById("UpdatePhoto").disabled = true;
+
+ // And the phonetic fields
+ document.getElementById(kPhoneticFields[0]).readOnly = true;
+ document.getElementById(kPhoneticFields[3]).readOnly = true;
+
+ // Also disable the mail format popup.
+ document.getElementById("PreferMailFormatPopup").disabled = true;
+
+ // And the "prefer display name" checkbox.
+ document.getElementById("preferDisplayName").disabled = true;
+
+ document.documentElement.buttons = "accept";
+ document.documentElement.removeAttribute("ondialogaccept");
+ }
+ }
+ }
+}
+
+/* Registers functions that are called when loading the card
+ * values into the contact editor dialog. This is useful if
+ * extensions have added extra fields to the nsIAbCard, and
+ * need to display them in the contact editor.
+ */
+function RegisterLoadListener(aFunc)
+{
+ gOnLoadListeners.push(aFunc);
+}
+
+function UnregisterLoadListener(aFunc)
+{
+ var fIndex = gOnLoadListeners.indexOf(aFunc);
+ if (fIndex != -1)
+ gOnLoadListeners.splice(fIndex, 1);
+}
+
+// Notifies load listeners that an nsIAbCard is being loaded.
+function NotifyLoadListeners(aCard, aDoc)
+{
+ if (!gOnLoadListeners.length)
+ return;
+
+ for (let listener of gOnLoadListeners)
+ listener(aCard, aDoc);
+}
+
+/* Registers functions that are called when saving the card
+ * values. This is useful if extensions have added extra
+ * fields to the user interface, and need to set those values
+ * in their nsIAbCard.
+ */
+function RegisterSaveListener(aFunc)
+{
+ gOnSaveListeners.push(aFunc);
+}
+
+function UnregisterSaveListener(aFunc)
+{
+ var fIndex = gOnSaveListeners.indexOf(aFunc);
+ if (fIndex != -1)
+ gOnSaveListeners.splice(fIndex, 1);
+}
+
+// Notifies save listeners that an nsIAbCard is being saved.
+function NotifySaveListeners(directory)
+{
+ if (!gOnSaveListeners.length)
+ return;
+
+ for (let listener of gOnSaveListeners)
+ listener(gEditCard.card, document);
+
+ // the save listeners might have tweaked the card
+ // in which case we need to commit it.
+ directory.modifyCard(gEditCard.card);
+}
+
+function InitPhoneticFields()
+{
+ var showPhoneticFields =
+ Services.prefs.getComplexValue("mail.addr_book.show_phonetic_fields",
+ Ci.nsIPrefLocalizedString).data;
+
+ // show phonetic fields if indicated by the pref
+ if (showPhoneticFields == "true")
+ {
+ for (var i = kPhoneticFields.length; i-- > 0; )
+ document.getElementById(kPhoneticFields[i]).hidden = false;
+ }
+}
+
+function InitEditCard()
+{
+ InitPhoneticFields();
+
+ InitCommonJS();
+ // Create gEditCard object that contains global variables for the current js
+ // file.
+ gEditCard = new Object();
+
+ // get specific prefs that gEditCard will need
+ try {
+ var displayLastNameFirst =
+ Services.prefs.getComplexValue("mail.addr_book.displayName.lastnamefirst",
+ Ci.nsIPrefLocalizedString).data;
+ gEditCard.displayLastNameFirst = (displayLastNameFirst == "true");
+ gEditCard.generateDisplayName =
+ Services.prefs.getBoolPref("mail.addr_book.displayName.autoGeneration");
+ }
+ catch (ex) {
+ dump("ex: failed to get pref" + ex + "\n");
+ }
+}
+
+function NewCardOKButton()
+{
+ if (gOkCallback)
+ {
+ if (!CheckAndSetCardValues(gEditCard.card, document, true))
+ return false; // don't close window
+
+ gOkCallback(gEditCard.card.translateTo("vcard"));
+ return true; // close the window
+ }
+
+ var popup = document.getElementById('abPopup');
+ if ( popup )
+ {
+ var uri = popup.value;
+
+ // FIX ME - hack to avoid crashing if no ab selected because of blank option bug from template
+ // should be able to just remove this if we are not seeing blank lines in the ab popup
+ if ( !uri )
+ return false; // don't close window
+ // -----
+
+ if (gEditCard.card)
+ {
+ if (!CheckAndSetCardValues(gEditCard.card, document, true))
+ return false; // don't close window
+
+ // replace gEditCard.card with the card we added
+ // so that save listeners can get / set attributes on
+ // the card that got created.
+ var directory = GetDirectoryFromURI(uri);
+ gEditCard.card = directory.addCard(gEditCard.card);
+ NotifySaveListeners(directory);
+ }
+ }
+
+ return true; // close the window
+}
+
+function NewCardCancelButton()
+{
+ // If a new photo was created, remove it now as it won't be used.
+ purgeOldPhotos(false);
+}
+
+// Move the data from the cardproperty to the dialog
+function GetCardValues(cardproperty, doc)
+{
+ if (!cardproperty)
+ return;
+
+ // Pass the nsIAbCard and the Document through the listeners
+ // to give extensions a chance to populate custom fields.
+ NotifyLoadListeners(cardproperty, doc);
+
+ for (var i = kVcardFields.length; i-- > 0; ) {
+ doc.getElementById(kVcardFields[i][0]).value =
+ cardproperty.getProperty(kVcardFields[i][1], "");
+ }
+
+ var birthday = doc.getElementById("Birthday");
+ modifyDatepicker(birthday);
+
+ // Get the year first, so that the following month/day
+ // calculations can take leap years into account.
+ var year = cardproperty.getProperty("BirthYear", null);
+ var birthYear = doc.getElementById("BirthYear");
+ // set the year in the datepicker to the stored year
+ // if the year isn't present, default to 2000 (a leap year)
+ birthday.year = saneBirthYear(year);
+ birthYear.value = year;
+
+ // get the month of the year (1 - 12)
+ var month = cardproperty.getProperty("BirthMonth", null);
+ if (month > 0 && month < 13)
+ birthday.month = month - 1;
+ else
+ birthday.monthField.value = null;
+
+ // get the date of the month (1 - 31)
+ var date = cardproperty.getProperty("BirthDay", null);
+ if (date > 0 && date < 32)
+ birthday.date = date;
+ else
+ birthday.dateField.value = null;
+
+ // get the current age
+ calculateAge(null, birthYear);
+ // when the birth year changes, update the datepicker's year to the new value
+ // or to kDefaultYear if the value is null
+ birthYear.onchange = calculateAge;
+ birthday.onchange = calculateAge;
+ var age = doc.getElementById("Age");
+ age.onchange = calculateYear;
+
+ var popup = document.getElementById("PreferMailFormatPopup");
+ if (popup)
+ popup.value = cardproperty.getProperty("PreferMailFormat", "");
+
+ var preferDisplayNameEl = document.getElementById("preferDisplayName");
+ if (preferDisplayNameEl)
+ // getProperty may return a "1" or "0" string, we want a boolean
+ preferDisplayNameEl.checked = cardproperty.getProperty("PreferDisplayName", true) != false;
+
+ // get phonetic fields if exist
+ try {
+ doc.getElementById("PhoneticFirstName").value = cardproperty.getProperty("PhoneticFirstName", "");
+ doc.getElementById("PhoneticLastName").value = cardproperty.getProperty("PhoneticLastName", "");
+ }
+ catch (ex) {}
+
+ // Select the type if there is a valid value stored for that type, otherwise
+ // select the generic photo
+ loadPhoto(cardproperty);
+
+ updateChatName();
+}
+
+// when the ab card dialog is being loaded to show a vCard,
+// hide the fields which aren't supported
+// by vCard so the user does not try to edit them.
+function HideNonVcardFields()
+{
+ document.getElementById("homeTabButton").hidden = true;
+ document.getElementById("photoTabButton").hidden = true;
+ var i;
+ for (i = kNonVcardFields.length; i-- > 0; )
+ document.getElementById(kNonVcardFields[i]).collapsed = true;
+ for (i = kPhoneticFields.length; i-- > 0; )
+ document.getElementById(kPhoneticFields[i]).collapsed = true;
+}
+
+// Move the data from the dialog to the cardproperty to be stored in the database
+// @Returns false - Some required data are missing (card values were not set);
+// true - Card values were set, or there is no card to set values on.
+function CheckAndSetCardValues(cardproperty, doc, check)
+{
+ // If requested, check the required data presence.
+ if (check && !CheckCardRequiredDataPresence(document))
+ return false;
+
+ if (!cardproperty)
+ return true;
+
+ for (var i = kVcardFields.length; i-- > 0; )
+ cardproperty.setProperty(kVcardFields[i][1],
+ doc.getElementById(kVcardFields[i][0]).value);
+
+ // get the birthday information from the dialog
+ var birthdayElem = doc.getElementById("Birthday");
+ var birthMonth = birthdayElem.monthField.value;
+ var birthDay = birthdayElem.dateField.value;
+ var birthYear = doc.getElementById("BirthYear").value;
+
+ // set the birth day, month, and year properties
+ cardproperty.setProperty("BirthDay", birthDay);
+ cardproperty.setProperty("BirthMonth", birthMonth);
+ cardproperty.setProperty("BirthYear", birthYear);
+
+ var popup = document.getElementById("PreferMailFormatPopup");
+ if (popup)
+ cardproperty.setProperty("PreferMailFormat", popup.value);
+
+ var preferDisplayNameEl = document.getElementById("preferDisplayName");
+ if (preferDisplayNameEl)
+ cardproperty.setProperty("PreferDisplayName", preferDisplayNameEl.checked);
+
+ // set phonetic fields if exist
+ try {
+ cardproperty.setProperty("PhoneticFirstName", doc.getElementById("PhoneticFirstName").value);
+ cardproperty.setProperty("PhoneticLastName", doc.getElementById("PhoneticLastName").value);
+ }
+ catch (ex) {}
+
+ let photoType = doc.getElementById("PhotoType").value;
+ if (gPhotoHandlers[photoType]) {
+ if (!gPhotoHandlers[photoType].onSave(cardproperty, doc)) {
+ photoType = "generic";
+ onSwitchPhotoType("generic");
+ gPhotoHandlers[photoType].onSave(cardproperty, doc);
+ }
+ }
+ cardproperty.setProperty("PhotoType", photoType);
+ purgeOldPhotos(true);
+
+ // Remove obsolete chat names.
+ try {
+ cardproperty.setProperty("_GoogleTalk", "");
+ }
+ catch (ex) {}
+ try {
+ cardproperty.setProperty("_AimScreenName", "");
+ }
+ catch (ex) {}
+
+ return true;
+}
+
+function CleanUpWebPage(webPage)
+{
+ // no :// yet so we should add something
+ if ( webPage.length && webPage.search("://") == -1 )
+ {
+ // check for missing / on http://
+ if ( webPage.substr(0, 6) == "http:/" )
+ return( "http://" + webPage.substr(6) );
+ else
+ return( "http://" + webPage );
+ }
+ else
+ return(webPage);
+}
+
+// @Returns false - Some required data are missing;
+// true - All required data are present.
+function CheckCardRequiredDataPresence(doc)
+{
+ // Bug 314995 - We require at least one of the following fields to be
+ // filled in: email address, first name, last name, display name,
+ // organization (company name).
+ var primaryEmail = doc.getElementById("PrimaryEmail");
+ if (primaryEmail.textLength == 0 &&
+ doc.getElementById("FirstName").textLength == 0 &&
+ doc.getElementById("LastName").textLength == 0 &&
+ doc.getElementById("DisplayName").textLength == 0 &&
+ doc.getElementById("Company").textLength == 0)
+ {
+ Services.prompt.alert(window,
+ gAddressBookBundle.getString("cardRequiredDataMissingTitle"),
+ gAddressBookBundle.getString("cardRequiredDataMissingMessage"));
+
+ return false;
+ }
+
+ // Simple checks that the primary email should be of the form |user@host|.
+ // Note: if the length of the primary email is 0 then we skip the check
+ // as some other field must have something as per the check above.
+ if (primaryEmail.textLength != 0 && !/.@./.test(primaryEmail.value))
+ {
+ Services.prompt.alert(window,
+ gAddressBookBundle.getString("incorrectEmailAddressFormatTitle"),
+ gAddressBookBundle.getString("incorrectEmailAddressFormatMessage"));
+
+ // Focus the dialog field, to help the user.
+ document.getElementById("abTabs").selectedIndex = 0;
+ primaryEmail.focus();
+
+ return false;
+ }
+
+ return true;
+}
+
+function GenerateDisplayName()
+{
+ if (!gEditCard.generateDisplayName)
+ return;
+
+ var displayName;
+
+ var firstNameValue = document.getElementById("FirstName").value;
+ var lastNameValue = document.getElementById("LastName").value;
+ if (lastNameValue && firstNameValue) {
+ displayName = (gEditCard.displayLastNameFirst)
+ ? gAddressBookBundle.getFormattedString("lastFirstFormat", [lastNameValue, firstNameValue])
+ : gAddressBookBundle.getFormattedString("firstLastFormat", [firstNameValue, lastNameValue]);
+ }
+ else {
+ // one (or both) of these is empty, so this works.
+ displayName = firstNameValue + lastNameValue;
+ }
+
+ document.getElementById("DisplayName").value = displayName;
+
+ SetCardDialogTitle(displayName);
+}
+
+function DisplayNameChanged()
+{
+ // turn off generateDisplayName if the user changes the display name
+ gEditCard.generateDisplayName = false;
+
+ SetCardDialogTitle(document.getElementById("DisplayName").value);
+}
+
+function SetCardDialogTitle(displayName)
+{
+ document.title = displayName
+ ? gAddressBookBundle.getFormattedString(gEditCard.titleProperty + "WithDisplayName", [displayName])
+ : gAddressBookBundle.getString(gEditCard.titleProperty);
+}
+
+/**
+ * Calculates the duration of time between an event and now and updates the year
+ * of whichever element did not call this function.
+ * @param aEvent The event calling this method.
+ * @param aElement Optional, but required if this function is not called from an
+ * element's event listener. The element that would call this.
+ */
+function calculateAge(aEvent, aElement) {
+ var datepicker, yearElem, ageElem;
+ if (aEvent)
+ aElement = this;
+ if (aElement.id == "BirthYear" || aElement.id == "Birthday") {
+ datepicker = document.getElementById("Birthday");
+ yearElem = document.getElementById("BirthYear");
+ ageElem = document.getElementById("Age");
+ }
+ if (!datepicker || !yearElem || !ageElem)
+ return;
+
+ // if the datepicker was updated, update the year element
+ if (aElement == datepicker && !(datepicker.year == kDefaultYear && !yearElem.value))
+ yearElem.value = datepicker.year;
+ var year = yearElem.value;
+ // if the year element's value is invalid set the year and age elements to null
+ if (isNaN(year) || year < kMinYear || year > kMaxYear) {
+ yearElem.value = null;
+ ageElem.value = null;
+ datepicker.year = kDefaultYear;
+ return;
+ }
+ else if (aElement == yearElem)
+ datepicker.year = year;
+ // calculate the length of time between the event and now
+ try {
+ var event = new Date(datepicker.year, datepicker.month, datepicker.date);
+ // if the year is only 2 digits, then the year won't be set correctly
+ // using setFullYear fixes this issue
+ event.setFullYear(datepicker.year);
+ // get the difference between today and the event
+ var age = new Date(new Date() - event);
+ // get the number of years of the difference and subtract 1970 (epoch)
+ ageElem.value = age.getFullYear() - 1970;
+ }
+ catch(e) {
+ datepicker.year = kDefaultYear;
+ // if there was an error (like invalid year) set the year and age to null
+ yearElem.value = null;
+ ageElem.value = null;
+ }
+}
+
+/**
+ * Calculates the year an event ocurred based on the number of years, months,
+ * and days since the event and updates the relevant element.
+ * @param aEvent The event calling this method.
+ * @param aElement Optional, but required if this function is not called from an
+ * element's event listener. The element that would call this.
+ */
+function calculateYear(aEvent, aElement) {
+ var yearElem, datepicker;
+ if (aEvent)
+ aElement = this;
+ if (aElement.id == "Age") {
+ datepicker = document.getElementById("Birthday");
+ yearElem = document.getElementById("BirthYear");
+ }
+ if (!datepicker || !yearElem)
+ return;
+
+ // if the age is null, remove the year from the year element, and set the
+ // datepicker to the default year
+ if (!aElement.value) {
+ datepicker.year = kDefaultYear;
+ yearElem.value = null;
+ return;
+ }
+ var today = new Date();
+ try {
+ var date = new Date(aElement.value, datepicker.month, datepicker.date);
+ date.setFullYear(aElement.value);
+ // get the difference between today and the age (the year is offset by 1970)
+ var difference = new Date(today - date);
+ datepicker.year = yearElem.value = difference.getFullYear() - 1970;
+ }
+ // the above code may throw an invalid year exception. If that happens, set
+ // the year to kDefaultYear and set the year element's value to 0
+ catch (e) {
+ datepicker.year = kDefaultYear;
+ // if there was an error (like invalid year) set the year and age to null
+ yearElem.value = null;
+ let ageElem = document.getElementById("Age");
+ if (ageElem)
+ ageElem.value = null;
+ }
+}
+
+/**
+ * Modifies a datepicker in the following ways:
+ * - Removes the scroll arrows
+ * - Hides the year
+ * - Allows the day and month to be blank
+ * NOTE:
+ * The datepicker's date, month, year, and dateValue properties are not always
+ * what appear physically to the user in the datepicker fields.
+ * If any field is blank, the corresponding property is either the previous
+ * value if there was one since the card was opened or the relevant portion of
+ * the current date.
+ *
+ * To get the displayed values, get the value of the individual field, such as
+ * datepicker.yyyyField.value where yyyy is "year", "month", or "date" for the
+ * year, month, and day, respectively.
+ * If the value is null, then the field is blank and vice versa.
+ * @param aDatepicker The datepicker to modify.
+ */
+function modifyDatepicker(aDatepicker) {
+ // collapse the year field and separator
+ aDatepicker.yearField.parentNode.collapsed = true;
+ if (aDatepicker.yearField == aDatepicker._fieldThree ||
+ aDatepicker.yearField == aDatepicker._fieldTwo)
+ aDatepicker._separatorSecond.collapsed = true;
+ else
+ aDatepicker._separatorFirst.collapsed = true;
+ // collapse the spinner element
+ document.getAnonymousElementByAttribute(aDatepicker, "anonid", "buttons")
+ .collapsed = true;
+ // this modified constrain value function ignores values less than the minimum
+ // to let the value be blank (null)
+ // from: mozilla/toolkit/content/widgets/datetimepicker.xml#759
+ aDatepicker._constrainValue = function newConstrainValue(aField, aValue, aNoWrap) {
+ // if the value is less than one, make the field's value null
+ if (aValue < 1) {
+ aField.value = null;
+ return null;
+ }
+ if (aNoWrap && aField == this.monthField)
+ aValue--;
+ // make sure the date is valid for the given month
+ if (aField == this.dateField) {
+ var currentMonth = this.month;
+ var dt = new Date(this.year, currentMonth, aValue);
+ return dt.getMonth() != currentMonth ? 1 : aValue;
+ }
+ var min = (aField == this.monthField) ? 0 : 1;
+ var max = (aField == this.monthField) ? 11 : kMaxYear;
+ // make sure the value isn't too high
+ if (aValue > max)
+ return aNoWrap ? max : min;
+ return aValue;
+ }
+ // sets the specified field to the given value, but allows blank fields
+ // from: mozilla/toolkit/content/widgets/datetimepicker.xml#698
+ aDatepicker._setFieldValue = function setValue(aField, aValue) {
+ if (aField == this.yearField && aValue >= kMinYear && aValue <= kMaxYear) {
+ var oldDate = this._dateValue;
+ this._dateValue.setFullYear(aValue);
+ if (oldDate != this._dateValue) {
+ this._dateValue.setDate(0);
+ this._updateUI(this.dateField, this.date);
+ }
+ }
+ // update the month if the value isn't null
+ else if (aField == this.monthField && aValue != null) {
+ var oldDate = this.date;
+ this._dateValue.setMonth(aValue);
+ if (oldDate != this.date)
+ this._dateValue.setDate(0);
+ this._updateUI(this.dateField, this.date);
+ var date = this._dateValue.getDate();
+ this.dateField.value = date < 10 && this.dateLeadingZero ? "0" + date : date;
+ var month = this._dateValue.getMonth() + 1;
+ this.monthField.value = month < 10 && this.monthLeadingZero ? "0" + month : month;
+ }
+ // update the date if the value isn't null
+ else if (aField == this.dateField && aValue != null) {
+ this._dateValue.setDate(aValue);
+ this._updateUI(this.dateField, this.date);
+ var date = this._dateValue.getDate();
+ this.dateField.value = date < 10 && this.dateLeadingZero ? "0" + date : date;
+ var month = this._dateValue.getMonth() + 1;
+ this.monthField.value = month < 10 && this.monthLeadingZero ? "0" + month : month;
+ }
+ this.setAttribute("value", this.value);
+
+ if (this.attachedControl)
+ this.attachedControl._setValueNoSync(this._dateValue);
+ // if the aField's value is null or 0, set both field's values to null
+ if (!aField.value && aField != this.yearField) {
+ this.dateField.value = null;
+ this.monthField.value = null;
+ }
+ // make the field's value null if aValue is null and the field's value isn't
+ if (aValue == null && aField.value != null)
+ aField.value = null;
+ }
+}
+
+var chatNameFieldIds =
+ ["Yahoo", "Skype", "QQ", "MSN", "ICQ", "XMPP", "IRC"];
+
+/**
+ * Show the 'Chat' tab and focus the first field that has a value, or
+ * the first field if none of them has a value.
+ */
+function showChat()
+{
+ document.getElementById('abTabPanels').parentNode.selectedTab =
+ document.getElementById('chatTabButton');
+ for (let id of chatNameFieldIds) {
+ let elt = document.getElementById(id);
+ if (elt.value) {
+ elt.focus();
+ return;
+ }
+ }
+ document.getElementById(chatNameFieldIds[0]).focus();
+}
+
+/**
+ * Fill in the value of the ChatName readonly field with the first
+ * value of the fields in the Chat tab.
+ */
+function updateChatName()
+{
+ let value = "";
+ for (let id of chatNameFieldIds) {
+ let val = document.getElementById(id).value;
+ if (val) {
+ value = val;
+ break;
+ }
+ }
+ document.getElementById("ChatName").value = value;
+}
+
+/**
+ * Extract the photo information from an nsIAbCard, and populate
+ * the appropriate input fields in the contact editor. If the
+ * nsIAbCard returns an unrecognized PhotoType, the generic
+ * display photo is switched to.
+ *
+ * @param aCard The nsIAbCard to extract the information from.
+ *
+ */
+function loadPhoto(aCard) {
+ var type = aCard.getProperty("PhotoType", "");
+
+ if (!gPhotoHandlers[type] || !gPhotoHandlers[type].onLoad(aCard, document)) {
+ type = "generic";
+ gPhotoHandlers[type].onLoad(aCard, document);
+ }
+
+ document.getElementById("PhotoType").value = type;
+ gPhotoHandlers[type].onShow(aCard, document, "photo");
+}
+
+/**
+ * Event handler for when the user switches the type of
+ * photo for the nsIAbCard being edited. Tries to initiate a
+ * photo download.
+ *
+ * @param aPhotoType {string} The type to switch to
+ * @param aEvent {Event} The event object if used as an event handler
+ */
+function onSwitchPhotoType(aPhotoType, aEvent) {
+ if (!gEditCard)
+ return;
+
+ // Stop event propagation to the radiogroup command event in case that the
+ // child button is pressed. Otherwise, the download is started twice in a row.
+ if (aEvent) {
+ aEvent.stopPropagation();
+ }
+
+ if (aPhotoType) {
+ if (aPhotoType != document.getElementById("PhotoType").value) {
+ document.getElementById("PhotoType").value = aPhotoType;
+ }
+ } else {
+ aPhotoType = document.getElementById("PhotoType").value;
+ }
+
+ if (gPhotoHandlers[aPhotoType]) {
+ if (!gPhotoHandlers[aPhotoType].onRead(gEditCard.card, document)) {
+ onSwitchPhotoType("generic");
+ }
+ }
+}
+
+/**
+ * Removes the photo file at the given path, if present.
+ *
+ * @param aName The name of the photo to remove from the Photos directory.
+ * 'null' value is allowed and means to remove no file.
+ *
+ * @return {boolean} True if the file was deleted, false otherwise.
+ */
+function removePhoto(aName) {
+ if (!aName)
+ return false;
+ // Get the directory with all the photos
+ var file = getPhotosDir();
+ // Get the photo (throws an exception for invalid names)
+ try {
+ file.append(aName);
+ file.remove(false);
+ return true;
+ }
+ catch (e) {}
+ return false;
+}
+
+/**
+ * Remove previous and temporary photo files from the Photos directory.
+ *
+ * @param aSaved {boolean} Whether the new card is going to be saved/committed.
+ */
+function purgeOldPhotos(aSaved = true) {
+ // If photo was changed, the array contains at least one member, the original photo.
+ while (gOldPhotos.length > 0) {
+ let photoName = gOldPhotos.pop();
+ if (!aSaved && (gOldPhotos.length == 0)) {
+ // If the saving was cancelled, we want to keep the original photo of the card.
+ break;
+ }
+ removePhoto(photoName);
+ }
+
+ if (aSaved) {
+ // The new photo should stay so we clear the reference to it.
+ gNewPhoto = null;
+ } else {
+ // Changes to card not saved, we don't need the new photo.
+ // It may be null when there was no change of it.
+ removePhoto(gNewPhoto);
+ }
+}
+
+/**
+ * Opens a file picker with image filters to look for a contact photo.
+ * If the user selects a file and clicks OK then the PhotoURI textbox is set
+ * with a file URI pointing to that file and updatePhoto is called.
+ *
+ * @param aEvent {Event} The event object if used as an event handler.
+ */
+function browsePhoto(aEvent) {
+ // Stop event propagation to the radiogroup command event in case that the
+ // child button is pressed. Otherwise, the download is started twice in a row.
+ if (aEvent)
+ aEvent.stopPropagation();
+
+ let fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(Ci.nsIFilePicker);
+ fp.init(window, gAddressBookBundle.getString("browsePhoto"),
+ Ci.nsIFilePicker.modeOpen);
+
+ // Open the directory of the currently chosen photo (if any)
+ let currentPhotoFile = document.getElementById("PhotoFile").file
+ if (currentPhotoFile) {
+ fp.displayDirectory = currentPhotoFile.parent;
+ }
+
+ // Add All Files & Image Files filters and select the latter
+ fp.appendFilters(Ci.nsIFilePicker.filterImages);
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK) {
+ return;
+ }
+ document.getElementById("PhotoFile").file = fp.file;
+ onSwitchPhotoType("file");
+ });
+}
+
+/**
+ * Handlers to add drag and drop support.
+ */
+function checkDropPhoto(aEvent) {
+ // Just allow anything to be dropped. Different types of data are handled
+ // in doDropPhoto() below.
+ aEvent.preventDefault();
+}
+
+function doDropPhoto(aEvent) {
+ aEvent.preventDefault();
+
+ let photoType = "";
+
+ // Check if a file has been dropped.
+ let file = aEvent.dataTransfer.mozGetDataAt("application/x-moz-file", 0);
+ if (file instanceof Ci.nsIFile) {
+ photoType = "file";
+ document.getElementById("PhotoFile").file = file;
+ } else {
+ // Check if a URL has been dropped.
+ let link = aEvent.dataTransfer.getData("URL");
+ if (link) {
+ photoType = "web";
+ document.getElementById("PhotoURI").value = link;
+ } else {
+ // Check if dropped text is a URL.
+ link = aEvent.dataTransfer.getData("text/plain");
+ if (/^(ftps?|https?):\/\//i.test(link)) {
+ photoType = "web";
+ document.getElementById("PhotoURI").value = link;
+ }
+ }
+ }
+
+ onSwitchPhotoType(photoType);
+}
+
+/**
+ * Self-contained object to manage the user interface used for downloading
+ * and storing contact photo images.
+ */
+var gPhotoDownloadUI = (function() {
+ // UI DOM elements
+ let elProgressbar;
+ let elProgressLabel;
+ let elPhotoType;
+ let elProgressContainer;
+
+ window.addEventListener("load", function load(event) {
+ if (!elProgressbar)
+ elProgressbar = document.getElementById("PhotoDownloadProgress");
+ if (!elProgressLabel)
+ elProgressLabel = document.getElementById("PhotoStatus");
+ if (!elPhotoType)
+ elPhotoType = document.getElementById("PhotoType");
+ if (!elProgressContainer)
+ elProgressContainer = document.getElementById("ProgressContainer");
+ }, false);
+
+ function onStart() {
+ elProgressContainer.setAttribute("class", "expanded");
+ elProgressLabel.value = "";
+ elProgressbar.hidden = false;
+ elProgressbar.value = 3; // Start with a tiny visible progress
+ }
+
+ function onSuccess() {
+ elProgressLabel.value = "";
+ elProgressContainer.setAttribute("class", "");
+ }
+
+ function onError(state) {
+ let msg;
+ switch (state) {
+ case gImageDownloader.ERROR_INVALID_URI:
+ msg = gAddressBookBundle.getString("errorInvalidUri");
+ break;
+ case gImageDownloader.ERROR_UNAVAILABLE:
+ msg = gAddressBookBundle.getString("errorNotAvailable");
+ break;
+ case gImageDownloader.ERROR_INVALID_IMG:
+ msg = gAddressBookBundle.getString("errorInvalidImage");
+ break;
+ case gImageDownloader.ERROR_SAVE:
+ msg = gAddressBookBundle.getString("errorSaveOperation");
+ break;
+ }
+ if (msg) {
+ elProgressLabel.value = msg;
+ elProgressbar.hidden = true;
+ onSwitchPhotoType("generic");
+ }
+ }
+
+ function onProgress(state, percent) {
+ elProgressbar.value = percent;
+ elProgressLabel.value = gAddressBookBundle.getString("stateImageSave");
+ }
+
+ return {
+ onStart: onStart,
+ onSuccess: onSuccess,
+ onError: onError,
+ onProgress: onProgress
+ }
+})();
+
+/* A photo handler defines the behaviour of the contact editor
+ * for a particular photo type. Each photo handler must implement
+ * the following interface:
+ *
+ * onLoad: function(aCard, aDocument):
+ * Called when the editor wants to populate the contact editor
+ * input fields with information about aCard's photo. Note that
+ * this does NOT make aCard's photo appear in the contact editor -
+ * this is left to the onShow function. Returns true on success.
+ * If the function returns false, the generic photo handler onLoad
+ * function will be called.
+ *
+ * onShow: function(aCard, aDocument, aTargetID):
+ * Called when the editor wants to show this photo type.
+ * The onShow method should take the input fields in the document,
+ * and render the requested photo in the IMG tag with id
+ * aTargetID. Note that onShow does NOT save the photo for aCard -
+ * this job is left to the onSave function. Returns true on success.
+ * If the function returns false, the generic photo handler onShow
+ * function will be called.
+ *
+ * onRead: function(aCard, aDocument)
+ * Called when the editor wants to read the user supplied new photo.
+ * The onRead method is responsible for analyzing the photo of this
+ * type requested by the user, and storing it, as well as the
+ * other fields required by onLoad/onShow to retrieve and display
+ * the photo again. Returns true on success. If the function
+ * returns false, the generic photo handler onRead function will
+ * be called.
+ *
+ * onSave: function(aCard, aDocument)
+ * Called when the editor wants to save this photo type to the card.
+ * Returns true on success.
+ */
+
+var gGenericPhotoHandler = {
+ onLoad: function(aCard, aDocument) {
+ return true;
+ },
+
+ onShow: function(aCard, aDocument, aTargetID) {
+ // XXX TODO: this ignores any other value from the generic photos
+ // menulist than "default".
+ aDocument.getElementById(aTargetID)
+ .setAttribute("src", defaultPhotoURI);
+ return true;
+ },
+
+ onRead: function(aCard, aDocument) {
+ gPhotoDownloadUI.onSuccess();
+
+ newPhotoAdded("", aCard);
+
+ gGenericPhotoHandler.onShow(aCard, aDocument, "photo");
+ return true;
+ },
+
+ onSave: function(aCard, aDocument) {
+ // XXX TODO: this ignores any other value from the generic photos
+ // menulist than "default".
+
+ // Update contact
+ aCard.setProperty("PhotoName", "");
+ aCard.setProperty("PhotoURI", "");
+ return true;
+ }
+};
+
+var gFilePhotoHandler = {
+
+ onLoad: function(aCard, aDocument) {
+ let photoURI = aCard.getProperty("PhotoURI", "");
+ let file;
+ try {
+ // The original file may not exist anymore, but we still display it.
+ file = Services.io.newURI(photoURI)
+ .QueryInterface(Ci.nsIFileURL)
+ .file;
+ } catch (e) {}
+
+ if (!file)
+ return false;
+
+ aDocument.getElementById("PhotoFile").file = file;
+ return true;
+ },
+
+ onShow: function(aCard, aDocument, aTargetID) {
+ let photoName = gNewPhoto || aCard.getProperty("PhotoName", null);
+ let photoURI = getPhotoURI(photoName);
+ aDocument.getElementById(aTargetID).setAttribute("src", photoURI);
+ return true;
+ },
+
+ onRead: function(aCard, aDocument) {
+ let file = aDocument.getElementById("PhotoFile").file;
+ if (!file)
+ return false;
+
+ // If the local file has been removed/renamed, keep the current photo as is.
+ if (!file.exists() || !file.isFile())
+ return false;
+
+ let photoURI = Services.io.newFileURI(file).spec;
+
+ gPhotoDownloadUI.onStart();
+
+ let cbSuccess = function(newPhotoName) {
+ gPhotoDownloadUI.onSuccess();
+
+ newPhotoAdded(newPhotoName, aCard);
+ aDocument.getElementById("PhotoFile").setAttribute("PhotoURI", photoURI);
+
+ gFilePhotoHandler.onShow(aCard, aDocument, "photo");
+ };
+
+ gImageDownloader.savePhoto(photoURI, cbSuccess,
+ gPhotoDownloadUI.onError,
+ gPhotoDownloadUI.onProgress);
+ return true;
+ },
+
+ onSave: function(aCard, aDocument) {
+ // Update contact
+ if (gNewPhoto) {
+ // The file may not be valid unless the photo has changed.
+ let photoURI = aDocument.getElementById("PhotoFile").getAttribute("PhotoURI");
+ aCard.setProperty("PhotoName", gNewPhoto);
+ aCard.setProperty("PhotoURI", photoURI);
+ }
+ return true;
+ }
+};
+
+var gWebPhotoHandler = {
+ onLoad: function(aCard, aDocument) {
+ let photoURI = aCard.getProperty("PhotoURI", null);
+
+ if (!photoURI)
+ return false;
+
+ aDocument.getElementById("PhotoURI").value = photoURI;
+ return true;
+ },
+
+ onShow: function(aCard, aDocument, aTargetID) {
+ let photoName = gNewPhoto || aCard.getProperty("PhotoName", null);
+ if (!photoName)
+ return false;
+
+ let photoURI = getPhotoURI(photoName);
+
+ aDocument.getElementById(aTargetID).setAttribute("src", photoURI);
+ return true;
+ },
+
+ onRead: function(aCard, aDocument) {
+ let photoURI = aDocument.getElementById("PhotoURI").value;
+ if (!photoURI)
+ return false;
+
+ gPhotoDownloadUI.onStart();
+
+ let cbSuccess = function(newPhotoName) {
+ gPhotoDownloadUI.onSuccess();
+
+ newPhotoAdded(newPhotoName, aCard);
+
+ gWebPhotoHandler.onShow(aCard, aDocument, "photo");
+
+ }
+
+ gImageDownloader.savePhoto(photoURI, cbSuccess,
+ gPhotoDownloadUI.onError,
+ gPhotoDownloadUI.onProgress);
+ return true;
+ },
+
+ onSave: function(aCard, aDocument) {
+ // Update contact
+ if (gNewPhoto) {
+ let photoURI = aDocument.getElementById("PhotoURI").value;
+ aCard.setProperty("PhotoName", gNewPhoto);
+ aCard.setProperty("PhotoURI", photoURI);
+ }
+ return true;
+ }
+};
+
+function newPhotoAdded(aPhotoName, aCard) {
+ // If we had the photo saved locally, shedule it for removal if card is saved.
+ gOldPhotos.push(gNewPhoto !== null ? gNewPhoto : aCard.getProperty("PhotoName", null));
+ gNewPhoto = aPhotoName;
+}
+
+/* In order for other photo handlers to be recognized for
+ * a particular type, they must be registered through this
+ * function.
+ * @param aType the type of photo to handle
+ * @param aPhotoHandler the photo handler to register
+ */
+function registerPhotoHandler(aType, aPhotoHandler)
+{
+ gPhotoHandlers[aType] = aPhotoHandler;
+}
+
+registerPhotoHandler("generic", gGenericPhotoHandler);
+registerPhotoHandler("web", gWebPhotoHandler);
+registerPhotoHandler("file", gFilePhotoHandler);
diff --git a/comm/suite/mailnews/components/addrbook/content/abCardOverlay.xul b/comm/suite/mailnews/components/addrbook/content/abCardOverlay.xul
new file mode 100644
index 0000000000..40287e8411
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abCardOverlay.xul
@@ -0,0 +1,515 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/cardDialog.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/addressbook/abCardOverlay.dtd">
+
+<overlay id="editcardOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+</stringbundleset>
+
+<script src="chrome://messenger/content/addressbook/abCommon.js"/>
+<script src="chrome://messenger/content/addressbook/abCardOverlay.js"/>
+
+<vbox id="editcard">
+ <tabbox>
+ <tabs id="abTabs">
+ <tab id="contactTabButton" label="&Contact.tab;"
+ accesskey="&Contact.accesskey;"/>
+ <tab id="homeTabButton" label="&Home.tab;" accesskey="&Home.accesskey;"/>
+ <tab id="workTabButton" label="&Work.tab;" accesskey="&Work.accesskey;"/>
+ <tab id="otherTabButton" label="&Other.tab;" accesskey="&Other.accesskey;"/>
+ <tab id="chatTabButton" label="&Chat.tab;" accesskey="&Chat.accesskey;"/>
+ <tab id="photoTabButton" label="&Photo.tab;" accesskey="&Photo.accesskey;"/>
+ </tabs>
+
+ <tabpanels id="abTabPanels" flex="1">
+ <!-- ** Name Tab ** -->
+ <!-- The following vbox contains two hboxes
+ top: Name/Email/Phonenumber bottom: Email prefs. -->
+ <vbox id="abNameTab" >
+ <!-- This hbox contains two vboxes
+ left: Name/Email, right: Phonenumbers -->
+ <hbox>
+ <vbox id="namesAndEmailAddresses">
+ <!-- This box contains the Names and Emailnames -->
+
+ <!-- LOCALIZATION NOTE:
+ NameField1, NameField2, PhoneticField1, PhoneticField2
+ those fields are either LN or FN depends on the target country.
+ They are configurable in the .dtd file.
+ -->
+
+ <hbox id="NameField1Container" align="center">
+ <spacer flex="1"/>
+ <label control="&NameField1.id;" value="&NameField1.label;"
+ accesskey="&NameField1.accesskey;"/>
+ <hbox class="CardEditWidth" align="center">
+ <textbox id="&NameField1.id;" flex="1"
+ oninput="GenerateDisplayName()"/>
+
+ <!-- LOCALIZATION NOTE:
+ Fields for phonetic are disabled as default and can be
+ enabled by^editing "mail.addr_book.show_phonetic_fields"
+ -->
+
+ <spacer id="PhoneticSpacer1" flex="1" hidden="true"/>
+ <label id="PhoneticLabel1" control="&PhoneticField1.id;"
+ value="&PhoneticField1.label;" hidden="true"/>
+ <textbox id="&PhoneticField1.id;" flex="1" hidden="true"/>
+ </hbox>
+ </hbox>
+ <hbox id="NameField2Container" align="center">
+ <spacer flex="1"/>
+ <label control="&NameField2.id;" value="&NameField2.label;"
+ accesskey="&NameField2.accesskey;"/>
+ <hbox class="CardEditWidth" align="center">
+ <textbox id="&NameField2.id;" flex="1"
+ oninput="GenerateDisplayName()"/>
+
+ <!-- LOCALIZATION NOTE:
+ Fields for phonetic are disabled as default and can be
+ enabled by editing "mail.addr_book.show_phonetic_fields"
+ -->
+
+ <spacer id="PhoneticSpacer2" flex="1" hidden="true"/>
+ <label id="PhoneticLabel2" control="&PhoneticField2.id;"
+ value="&PhoneticField2.label;" hidden="true"/>
+ <textbox id="&PhoneticField2.id;" flex="1" hidden="true"/>
+ </hbox>
+ </hbox>
+ <hbox id="DisplayNameContainer" align="center">
+ <spacer flex="1"/>
+ <label control="DisplayName" value="&DisplayName.label;"
+ accesskey="&DisplayName.accesskey;" />
+ <hbox class="CardEditWidth">
+ <textbox id="DisplayName" flex="1"
+ oninput="DisplayNameChanged()"/>
+ </hbox>
+ </hbox>
+ <hbox id="PreferDisplayNameContainer" align="center">
+ <spacer flex="1"/>
+ <hbox class="CardEditWidth">
+ <checkbox id="preferDisplayName"
+ label="&preferDisplayName.label;"
+ accesskey="&preferDisplayName2.accesskey;"/>
+ </hbox>
+ </hbox>
+
+ <hbox id="NickNameContainer" align="center">
+ <spacer flex="1"/>
+ <label control="NickName" value="&NickName.label;"
+ accesskey="&NickName.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="NickName" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="PrimaryEmailContainer" align="center">
+ <spacer flex="1"/>
+ <label control="PrimaryEmail" value="&PrimaryEmail.label;"
+ accesskey="&PrimaryEmail.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="PrimaryEmail" flex="1" class="uri-element"/>
+ </hbox>
+ </hbox>
+ <hbox id="SecondaryEmailContainer" align="center">
+ <spacer flex="1"/>
+ <label control="SecondEmail" value="&SecondEmail.label;"
+ accesskey="&SecondEmail.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="SecondEmail" flex="1" class="uri-element"/>
+ </hbox>
+ </hbox>
+ <hbox id="ScreenNameContainer" align="center">
+ <spacer flex="1"/>
+ <label class="text-link" value="&chatName.label;"
+ onclick="showChat();"/>
+ <hbox class="CardEditWidth">
+ <textbox id="ChatName" readonly="true" flex="1"
+ onclick="showChat();"/>
+ </hbox>
+ </hbox>
+ </vbox> <!-- End of Name/Email -->
+ <!-- Phone Number section -->
+ <vbox id="PhoneNumbers">
+ <hbox id="WorkPhoneContainer" align="center">
+ <spacer flex="1"/>
+ <label control="WorkPhone" value="&WorkPhone.label;"
+ accesskey="&WorkPhone.accesskey;" />
+ <textbox id="WorkPhone" class="PhoneEditWidth"/>
+ </hbox>
+ <hbox id="HomePhoneContainer" align="center">
+ <spacer flex="1"/>
+ <label control="HomePhone" value="&HomePhone.label;"
+ accesskey="&HomePhone.accesskey;"/>
+ <textbox id="HomePhone" class="PhoneEditWidth"/>
+ </hbox>
+ <hbox id="FaxNumberContainer" align="center">
+ <spacer flex="1"/>
+ <label control="FaxNumber" value="&FaxNumber.label;"
+ accesskey="&FaxNumber.accesskey;"/>
+ <textbox id="FaxNumber" class="PhoneEditWidth"/>
+ </hbox>
+ <hbox id="PagerNumberContainer" align="center">
+ <spacer flex="1"/>
+ <label control="PagerNumber" value="&PagerNumber.label;"
+ accesskey="&PagerNumber.accesskey;"/>
+ <textbox id="PagerNumber" class="PhoneEditWidth"/>
+ </hbox>
+ <hbox id="CellularNumberContainer" align="center">
+ <spacer flex="1"/>
+ <label control="CellularNumber" value="&CellularNumber.label;"
+ accesskey="&CellularNumber.accesskey;"/>
+ <textbox id="CellularNumber" class="PhoneEditWidth"/>
+ </hbox>
+ </vbox> <!-- End of Phonenumbers -->
+ </hbox> <!-- End of Name/Email/Phonenumbers -->
+ <!-- Email Preferences -->
+ <hbox>
+ <vbox valign="middle">
+ <label control="PreferMailFormatPopup"
+ value="&PreferMailFormat.label;"
+ accesskey="&PreferMailFormat.accesskey;"/>
+ </vbox>
+ <menulist id="PreferMailFormatPopup">
+ <menupopup>
+ <!-- 0,1,2 come from nsIAbPreferMailFormat in nsIAbCard.idl -->
+ <menuitem value="0" label="&Unknown.label;"/>
+ <menuitem value="1" label="&PlainText.label;"/>
+ <menuitem value="2" label="&HTML.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </vbox> <!-- End of Name Tab -->
+
+ <!-- ** Home Address Tab ** -->
+ <vbox id="abHomeTab" >
+ <hbox align="center">
+ <spacer flex="1"/>
+ <label control="HomeAddress" value="&HomeAddress.label;"
+ accesskey="&HomeAddress.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="HomeAddress" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox align="center">
+ <spacer flex="1"/>
+ <label control="HomeAddress2" value="&HomeAddress2.label;"
+ accesskey="&HomeAddress2.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="HomeAddress2" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="HomeCityContainer" align="center">
+ <spacer flex="1"/>
+ <label control="HomeCity" value="&HomeCity.label;"
+ accesskey="&HomeCity.accesskey;"/>
+ <hbox id="HomeCityFieldContainer"
+ class="AddressCardEditWidth"
+ align="center">
+ <textbox id="HomeCity" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox align="center">
+ <spacer flex="1"/>
+ <label control="HomeState" value="&HomeState.label;"
+ accesskey="&HomeState.accesskey;"/>
+ <hbox align="center" class="AddressCardEditWidth">
+ <textbox id="HomeState" flex="1"/>
+ <spacer class="stateZipSpacer"/>
+ <label control="HomeZipCode" value="&HomeZipCode.label;"
+ accesskey="&HomeZipCode.accesskey;"/>
+ <textbox id="HomeZipCode" class="ZipWidth"/>
+ </hbox>
+ </hbox>
+ <hbox align="center">
+ <spacer flex="1"/>
+ <label control="HomeCountry" value="&HomeCountry.label;"
+ accesskey="&HomeCountry.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="HomeCountry" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="WebPage2Container" align="center">
+ <spacer flex="1"/>
+ <label control="WebPage2" value="&HomeWebPage.label;"
+ accesskey="&HomeWebPage.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="WebPage2" flex="1" class="uri-element"/>
+ </hbox>
+ </hbox>
+ <hbox id="birthdayField" align="center">
+ <spacer flex="1"/>
+ <label control="Birthday" value="&Birthday.label;"
+ accesskey="&Birthday.accesskey;"/>
+ <hbox class="AddressCardEditWidth" align="center">
+ <!-- NOTE: This datepicker is modified.
+ See abCardOverlay.js for details-->
+ <datepicker id="Birthday" type="popup"/>
+ <label value="&In.label;"/>
+ <textbox id="BirthYear" maxlength="4"
+ placeholder="&Year.placeholder;" class="YearWidth" />
+ <label control="Age" value="&Or.value;"/>
+ <textbox id="Age" maxlength="4"
+ placeholder="&Age.placeholder;" class="YearWidth" />
+ <label value="&YearsOld.label;"/>
+ <spacer flex="1"/>
+ </hbox>
+ </hbox>
+ </vbox>
+
+ <!-- ** Business Address Tab ** -->
+ <vbox id="abBusinessTab" >
+ <hbox id="JobTitleDepartmentContainer" align="center">
+ <spacer flex="1"/>
+ <label control="JobTitle" value="&JobTitle.label;"
+ accesskey="&JobTitle.accesskey;"/>
+ <hbox class="AddressCardEditWidth" align="center">
+ <textbox id="JobTitle" flex="1"/>
+ <label control="Department" value="&Department.label;"
+ accesskey="&Department.accesskey;"/>
+ <textbox id="Department" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="CompanyContainer" align="center">
+ <spacer flex="1"/>
+ <label control="Company" value="&Company.label;"
+ accesskey="&Company.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="Company" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="WorkAddressContainer" align="center">
+ <spacer flex="1"/>
+ <label control="WorkAddress" value="&WorkAddress.label;"
+ accesskey="&WorkAddress.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="WorkAddress" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="WorkAddress2Container" align="center">
+ <spacer flex="1"/>
+ <label control="WorkAddress2" value="&WorkAddress2.label;"
+ accesskey="&WorkAddress2.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="WorkAddress2" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="WorkCityContainer" align="center">
+ <spacer flex="1"/>
+ <label control="WorkCity" value="&WorkCity.label;"
+ accesskey="&WorkCity.accesskey;"/>
+ <hbox id="WorkCityFieldContainer"
+ class="AddressCardEditWidth"
+ align="center">
+ <textbox id="WorkCity" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="WorkStateZipContainer" align="center">
+ <spacer flex="1"/>
+ <label control="WorkState" value="&WorkState.label;"
+ accesskey="&WorkState.accesskey;"/>
+ <hbox class="AddressCardEditWidth" align="center">
+ <textbox id="WorkState" flex="1"/>
+ <spacer class="stateZipSpacer"/>
+ <label control="WorkZipCode" value="&WorkZipCode.label;"
+ accesskey="&WorkZipCode.accesskey;"/>
+ <textbox id="WorkZipCode" class="ZipWidth"/>
+ </hbox>
+ </hbox>
+ <hbox id="WorkCountryContainer" align="center">
+ <spacer flex="1"/>
+ <label control="WorkCountry" value="&WorkCountry.label;"
+ accesskey="&WorkCountry.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="WorkCountry" flex="1"/>
+ </hbox>
+ </hbox>
+ <hbox id="WebPage1Container" align="center">
+ <spacer flex="1"/>
+ <label control="WebPage1" value="&WorkWebPage.label;"
+ accesskey="&WorkWebPage.accesskey;"/>
+ <hbox class="AddressCardEditWidth">
+ <textbox id="WebPage1" flex="1" class="uri-element"/>
+ </hbox>
+ </hbox>
+ </vbox>
+
+ <!-- ** Other Tab ** -->
+ <vbox id="abOtherTab" >
+ <vbox id="customFields">
+ <hbox flex="1" align="center">
+ <label control="Custom1" value="&Custom1.label;"
+ accesskey="&Custom1.accesskey;"/>
+ <textbox id="Custom1" flex="1"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <label control="Custom2" value="&Custom2.label;"
+ accesskey="&Custom2.accesskey;"/>
+ <textbox id="Custom2" flex="1"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <label control="Custom3" value="&Custom3.label;"
+ accesskey="&Custom3.accesskey;"/>
+ <textbox id="Custom3" flex="1"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <label control="Custom4" value="&Custom4.label;"
+ accesskey="&Custom4.accesskey;"/>
+ <textbox id="Custom4" flex="1"/>
+ </hbox>
+ </vbox>
+ <label control="Notes" value="&Notes.label;"
+ accesskey="&Notes.accesskey;"/>
+ <textbox id="Notes" multiline="true" wrap="virtual" flex="1"/>
+ </vbox>
+
+ <!-- ** Chat Tab ** -->
+ <hbox id="abChatTab">
+ <vbox>
+ <hbox id="YahooContainer" align="center">
+ <spacer flex="1"/>
+ <label control="Yahoo" value="&Yahoo.label;"
+ accesskey="&Yahoo.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="Yahoo" flex="1" onchange="updateChatName();"/>
+ </hbox>
+ </hbox>
+ <hbox id="SkypeContainer" align="center">
+ <spacer flex="1"/>
+ <label control="Skype" value="&Skype.label;"
+ accesskey="&Skype.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="Skype" flex="1" onchange="updateChatName();"/>
+ </hbox>
+ </hbox>
+ <hbox id="QQContainer" align="center">
+ <spacer flex="1"/>
+ <label control="QQ" value="&QQ.label;"
+ accesskey="&QQ.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="QQ" flex="1" onchange="updateChatName();"/>
+ </hbox>
+ </hbox>
+ <hbox id="MSNContainer" align="center">
+ <spacer flex="1"/>
+ <label control="MSN" value="&MSN.label;"
+ accesskey="&MSN.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="MSN" flex="1" onchange="updateChatName();"/>
+ </hbox>
+ </hbox>
+ <hbox id="ICQContainer" align="center">
+ <spacer flex="1"/>
+ <label control="ICQ" value="&ICQ.label;"
+ accesskey="&ICQ.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="ICQ" flex="1" onchange="updateChatName();"/>
+ </hbox>
+ </hbox>
+ <hbox id="XMPPContainer" align="center">
+ <spacer flex="1"/>
+ <label control="XMPP" value="&XMPP.label;"
+ accesskey="&XMPP.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="XMPP" flex="1" onchange="updateChatName();"/>
+ </hbox>
+ </hbox>
+ <hbox id="IRCContainer" align="center">
+ <spacer flex="1"/>
+ <label control="IRC" value="&IRC.label;"
+ accesskey="&IRC.accesskey;"/>
+ <hbox class="CardEditWidth">
+ <textbox id="IRC" flex="1" onchange="updateChatName();"/>
+ </hbox>
+ </hbox>
+ </vbox>
+ </hbox>
+
+ <!-- ** Photo Tab ** -->
+ <hbox id="abPhotoTab">
+ <vbox align="center" id="PhotoContainer"
+ style="height: 25ch; width: 25ch;"
+ ondrop="doDropPhoto(event);"
+ ondragenter="checkDropPhoto(event);"
+ ondragover="checkDropPhoto(event);">
+ <image id="photo" style="max-height: 25ch; max-width: 25ch;"/>
+ <hbox id="PhotoDropTarget" flex="1" pack="center" align="center">
+ <description>&PhotoDropTarget.label;</description>
+ </hbox>
+ </vbox>
+
+ <vbox flex="1">
+ <radiogroup id="PhotoType" oncommand="onSwitchPhotoType();">
+ <vbox id="GenericPhotoContainer">
+ <radio id="GenericPhotoType"
+ value="generic"
+ label="&GenericPhoto.label;"
+ accesskey="&GenericPhoto.accesskey;"/>
+ <menulist id="GenericPhotoList"
+ class="indent"
+ flex="1"
+ oncommand="onSwitchPhotoType('generic', event);">
+ <menupopup>
+ <menuitem label="&DefaultPhoto.label;"
+ selected="true"
+ value="default"/>
+ </menupopup>
+ </menulist>
+ </vbox>
+
+ <vbox id="FilePhotoContainer">
+ <radio id="FilePhotoType"
+ value="file"
+ label="&PhotoFile.label;"
+ accesskey="&PhotoFile.accesskey;"/>
+ <hbox class="indent">
+ <filefield id="PhotoFile"
+ readonly="true"
+ maxlength="255"
+ flex="1"/>
+ <button id="BrowsePhoto"
+ label="&BrowsePhoto.label;"
+ accesskey="&BrowsePhoto.accesskey;"
+ oncommand="browsePhoto(event);"/>
+ </hbox>
+ </vbox>
+
+ <vbox id="WebPhotoContainer">
+ <radio id="WebPhotoType"
+ value="web"
+ label="&PhotoURL.label;"
+ accesskey="&PhotoURL.accesskey;"/>
+ <hbox class="indent">
+ <textbox id="PhotoURI"
+ maxlength="255"
+ flex="1"
+ placeholder="&PhotoURL.placeholder;"/>
+ <button id="UpdatePhoto"
+ label="&UpdatePhoto.label;"
+ accesskey="&UpdatePhoto.accesskey;"
+ oncommand="onSwitchPhotoType('web', event);"/>
+ </hbox>
+ </vbox>
+ </radiogroup>
+ <hbox id="ProgressContainer" align="begin">
+ <label id="PhotoStatus"/>
+ <spacer flex="2"/>
+ <progressmeter id="PhotoDownloadProgress"
+ value="0"
+ mode="determined"
+ hidden="true"
+ flex="1"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ </tabpanels>
+ </tabbox>
+</vbox>
+</overlay>
diff --git a/comm/suite/mailnews/components/addrbook/content/abCardViewOverlay.js b/comm/suite/mailnews/components/addrbook/content/abCardViewOverlay.js
new file mode 100644
index 0000000000..e9edfbfbe4
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abCardViewOverlay.js
@@ -0,0 +1,527 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+//NOTE: gAddressBookBundle must be defined and set or this Overlay won't work
+
+var gMapItURLFormat;
+
+var gPhotoDisplayHandlers = {};
+
+var zListName;
+var zPrimaryEmail;
+var zSecondaryEmail;
+var zNickname;
+var zDisplayName;
+var zWork;
+var zHome;
+var zFax;
+var zCellular;
+var zPager;
+var zBirthday;
+var zCustom1;
+var zCustom2;
+var zCustom3;
+var zCustom4;
+var zYahoo;
+var zSkype;
+var zQQ;
+var zMSN;
+var zICQ;
+var zXMPP;
+var zIRC;
+
+var cvData;
+
+function OnLoadCardView()
+{
+ gMapItURLFormat = GetLocalizedStringPref("mail.addr_book.mapit_url.format");
+
+ zPrimaryEmail = gAddressBookBundle.getString("propertyPrimaryEmail");
+ zSecondaryEmail = gAddressBookBundle.getString("propertySecondaryEmail");
+ zNickname = gAddressBookBundle.getString("propertyNickname");
+ zDisplayName = gAddressBookBundle.getString("propertyDisplayName");
+ zListName = gAddressBookBundle.getString("propertyListName");
+ zWork = gAddressBookBundle.getString("propertyWork");
+ zHome = gAddressBookBundle.getString("propertyHome");
+ zFax = gAddressBookBundle.getString("propertyFax");
+ zCellular = gAddressBookBundle.getString("propertyCellular");
+ zPager = gAddressBookBundle.getString("propertyPager");
+ zBirthday = gAddressBookBundle.getString("propertyBirthday");
+ zCustom1 = gAddressBookBundle.getString("propertyCustom1");
+ zCustom2 = gAddressBookBundle.getString("propertyCustom2");
+ zCustom3 = gAddressBookBundle.getString("propertyCustom3");
+ zCustom4 = gAddressBookBundle.getString("propertyCustom4");
+ zYahoo = gAddressBookBundle.getString("propertyYahoo");
+ zSkype = gAddressBookBundle.getString("propertySkype");
+ zQQ = gAddressBookBundle.getString("propertyQQ");
+ zMSN = gAddressBookBundle.getString("propertyMSN");
+ zICQ = gAddressBookBundle.getString("propertyICQ");
+ zXMPP = gAddressBookBundle.getString("propertyXMPP");
+ zIRC = gAddressBookBundle.getString("propertyIRC");
+
+ var doc = document;
+
+ /* data for address book, prefixes: "cvb" = card view box
+ "cvh" = crad view header
+ "cv" = card view (normal fields) */
+ cvData = new Object;
+
+ // Card View Box
+ cvData.CardViewBox = doc.getElementById("CardViewInnerBox");
+ // Title
+ cvData.CardTitle = doc.getElementById("CardTitle");
+ // Name section
+ cvData.cvbContact = doc.getElementById("cvbContact");
+ cvData.cvhContact = doc.getElementById("cvhContact");
+ cvData.cvNickname = doc.getElementById("cvNickname");
+ cvData.cvDisplayName = doc.getElementById("cvDisplayName");
+ cvData.cvEmail1Box = doc.getElementById("cvEmail1Box");
+ cvData.cvEmail1 = doc.getElementById("cvEmail1");
+ cvData.cvBuddyIcon = doc.getElementById("cvBuddyIcon");
+ cvData.cvListNameBox = doc.getElementById("cvListNameBox");
+ cvData.cvListName = doc.getElementById("cvListName");
+ cvData.cvEmail2Box = doc.getElementById("cvEmail2Box");
+ cvData.cvEmail2 = doc.getElementById("cvEmail2");
+ // Home section
+ cvData.cvbHome = doc.getElementById("cvbHome");
+ cvData.cvhHome = doc.getElementById("cvhHome");
+ cvData.cvHomeAddress = doc.getElementById("cvHomeAddress");
+ cvData.cvHomeAddress2 = doc.getElementById("cvHomeAddress2");
+ cvData.cvHomeCityStZip = doc.getElementById("cvHomeCityStZip");
+ cvData.cvHomeCountry = doc.getElementById("cvHomeCountry");
+ cvData.cvbHomeMapItBox = doc.getElementById("cvbHomeMapItBox");
+ cvData.cvHomeMapIt = doc.getElementById("cvHomeMapIt");
+ cvData.cvHomeWebPageBox = doc.getElementById("cvHomeWebPageBox");
+ cvData.cvHomeWebPage = doc.getElementById("cvHomeWebPage");
+ // Other section
+ cvData.cvbOther = doc.getElementById("cvbOther");
+ cvData.cvBirthday = doc.getElementById("cvBirthday");
+ cvData.cvhOther = doc.getElementById("cvhOther");
+ cvData.cvCustom1 = doc.getElementById("cvCustom1");
+ cvData.cvCustom2 = doc.getElementById("cvCustom2");
+ cvData.cvCustom3 = doc.getElementById("cvCustom3");
+ cvData.cvCustom4 = doc.getElementById("cvCustom4");
+ cvData.cvNotes = doc.getElementById("cvNotes");
+ // Description section (mailing lists only)
+ cvData.cvbDescription = doc.getElementById("cvbDescription");
+ cvData.cvhDescription = doc.getElementById("cvhDescription");
+ cvData.cvDescription = doc.getElementById("cvDescription");
+ // Addresses section (mailing lists only)
+ cvData.cvbAddresses = doc.getElementById("cvbAddresses");
+ cvData.cvhAddresses = doc.getElementById("cvhAddresses");
+ cvData.cvAddresses = doc.getElementById("cvAddresses");
+ // Phone section
+ cvData.cvbPhone = doc.getElementById("cvbPhone");
+ cvData.cvhPhone = doc.getElementById("cvhPhone");
+ cvData.cvPhWork = doc.getElementById("cvPhWork");
+ cvData.cvPhHome = doc.getElementById("cvPhHome");
+ cvData.cvPhFax = doc.getElementById("cvPhFax");
+ cvData.cvPhCellular = doc.getElementById("cvPhCellular");
+ cvData.cvPhPager = doc.getElementById("cvPhPager");
+ // Work section
+ cvData.cvbWork = doc.getElementById("cvbWork");
+ cvData.cvhWork = doc.getElementById("cvhWork");
+ cvData.cvJobTitle = doc.getElementById("cvJobTitle");
+ cvData.cvDepartment = doc.getElementById("cvDepartment");
+ cvData.cvCompany = doc.getElementById("cvCompany");
+ cvData.cvWorkAddress = doc.getElementById("cvWorkAddress");
+ cvData.cvWorkAddress2 = doc.getElementById("cvWorkAddress2");
+ cvData.cvWorkCityStZip = doc.getElementById("cvWorkCityStZip");
+ cvData.cvWorkCountry = doc.getElementById("cvWorkCountry");
+ cvData.cvbWorkMapItBox = doc.getElementById("cvbWorkMapItBox");
+ cvData.cvWorkMapIt = doc.getElementById("cvWorkMapIt");
+ cvData.cvWorkWebPageBox = doc.getElementById("cvWorkWebPageBox");
+ cvData.cvWorkWebPage = doc.getElementById("cvWorkWebPage");
+ cvData.cvbPhoto = doc.getElementById("cvbPhoto");
+ cvData.cvPhoto = doc.getElementById("cvPhoto");
+ // Chat section
+ cvData.cvbChat = doc.getElementById("cvbChat");
+ cvData.cvhChat = doc.getElementById("cvhChat");
+ cvData.cvYahoo = doc.getElementById("cvYahoo");
+ cvData.cvSkype = doc.getElementById("cvSkype");
+ cvData.cvQQ = doc.getElementById("cvQQ");
+ cvData.cvMSN = doc.getElementById("cvMSN");
+ cvData.cvICQ = doc.getElementById("cvICQ");
+ cvData.cvXMPP = doc.getElementById("cvXMPP");
+ cvData.cvIRC = doc.getElementById("cvIRC");
+}
+
+// XXX todo
+// some similar code (in spirit) already exists, see OnLoadEditList()
+// perhaps we could combine and put in abCommon.js?
+function GetAddressesFromURI(uri)
+{
+ var addresses = "";
+
+ var editList = GetDirectoryFromURI(uri);
+ var addressList = editList.addressLists;
+ if (addressList) {
+ var total = addressList.length;
+ if (total > 0)
+ addresses = addressList.queryElementAt(0, Ci.nsIAbCard).primaryEmail;
+ for (var i = 1; i < total; i++ ) {
+ addresses += ", " + addressList.queryElementAt(i, Ci.nsIAbCard).primaryEmail;
+ }
+ }
+ return addresses;
+}
+
+function DisplayCardViewPane(realCard)
+{
+ var generatedName = realCard.generateName(Services.prefs.getIntPref("mail.addr_book.lastnamefirst"));
+
+ let data = top.cvData;
+ let visible = false;
+
+ let card = { getProperty : function (prop) {
+ return realCard.getProperty(prop, "");
+ },
+ primaryEmail : realCard.primaryEmail,
+ displayName : realCard.displayName,
+ isMailList : realCard.isMailList,
+ mailListURI : realCard.mailListURI
+ };
+
+ // Contact photo
+ displayPhoto(card, cvData.cvPhoto);
+
+ let titleString;
+ if (generatedName == "")
+ titleString = card.primaryEmail; // if no generatedName, use email
+ else
+ titleString = generatedName;
+
+ // set fields in card view pane
+ if (card.isMailList)
+ cvSetNode(data.CardTitle, gAddressBookBundle.getFormattedString("viewListTitle", [generatedName]));
+ else
+ cvSetNode(data.CardTitle, titleString);
+
+ // Contact section
+ cvSetNodeWithLabel(data.cvNickname, zNickname, card.getProperty("NickName"));
+
+ if (card.isMailList) {
+ // email1 and display name always hidden when a mailing list.
+ cvSetVisible(data.cvDisplayName, false);
+ cvSetVisible(data.cvEmail1Box, false);
+
+ visible = HandleLink(data.cvListName, zListName, card.displayName, data.cvListNameBox, "mailto:" + encodeURIComponent(GenerateAddressFromCard(card)));
+ }
+ else {
+ // listname always hidden if not a mailing list
+ cvSetVisible(data.cvListNameBox, false);
+
+ cvSetNodeWithLabel(data.cvDisplayName, zDisplayName, card.displayName);
+
+ visible = HandleLink(data.cvEmail1, zPrimaryEmail, card.primaryEmail, data.cvEmail1Box, "mailto:" + card.primaryEmail);
+ }
+
+ visible = HandleLink(data.cvEmail2, zSecondaryEmail, card.getProperty("SecondEmail"), data.cvEmail2Box, "mailto:" + card.getProperty("SecondEmail")) || visible;
+
+ // Home section
+ visible = cvSetNode(data.cvHomeAddress, card.getProperty("HomeAddress"));
+ visible = cvSetNode(data.cvHomeAddress2, card.getProperty("HomeAddress2")) || visible;
+ visible = cvSetCityStateZip(data.cvHomeCityStZip, card.getProperty("HomeCity"), card.getProperty("HomeState"), card.getProperty("HomeZipCode")) || visible;
+ visible = cvSetNode(data.cvHomeCountry, card.getProperty("HomeCountry")) || visible;
+
+ let mapURLList = data.cvHomeMapIt.firstChild;
+ if (visible)
+ mapURLList.initMapAddressFromCard(card, "Home");
+
+ cvSetVisible(data.cvbHomeMapItBox, visible && !!mapURLList.mapURL);
+
+ visible = HandleLink(data.cvHomeWebPage, "", card.getProperty("WebPage2"), data.cvHomeWebPageBox, card.getProperty("WebPage2")) || visible;
+
+ cvSetVisible(data.cvhHome, visible);
+ cvSetVisible(data.cvbHome, visible);
+ if (card.isMailList) {
+ // Description section
+ visible = cvSetNode(data.cvDescription, card.getProperty("Notes"))
+ cvSetVisible(data.cvbDescription, visible);
+
+ // Addresses section
+ visible = cvAddAddressNodes(data.cvAddresses, card.mailListURI);
+ cvSetVisible(data.cvbAddresses, visible);
+
+ // Other and Chat sections, not shown for mailing lists.
+ cvSetVisible(data.cvbOther, false);
+ cvSetVisible(data.cvbChat, false);
+ }
+ else {
+ // Other section
+ /// setup the birthday information
+ let day = card.getProperty("BirthDay", null);
+ let month = card.getProperty("BirthMonth", null);
+ let year = card.getProperty("BirthYear", null);
+ let dateStr;
+ if (day > 0 && day < 32 && month > 0 && month < 13) {
+ let date;
+ let formatter;
+ if (year) {
+ // use UTC-based calculations to avoid off-by-one day
+ // due to time zone/dst discontinuity
+ date = new Date(Date.UTC(year, month - 1, day));
+ date.setUTCFullYear(year); // to handle two-digit years properly
+ formatter = new Services.intl.DateTimeFormat(undefined,
+ { dateStyle: "long", timeZone: "UTC" });
+ }
+ // if the year doesn't exist, display Month DD (ex. January 1)
+ else {
+ date = new Date(Date.UTC(saneBirthYear(year), month - 1, day));
+ formatter = new Services.intl.DateTimeFormat(undefined,
+ { month: "long", day: "numeric", timeZone: "UTC" });
+ }
+ dateStr = formatter.format(date);
+ }
+ else if (year) {
+ dateStr = year;
+ }
+
+ visible = cvSetNodeWithLabel(data.cvBirthday, zBirthday, dateStr);
+
+ visible = cvSetNodeWithLabel(data.cvCustom1, zCustom1, card.getProperty("Custom1")) || visible;
+ visible = cvSetNodeWithLabel(data.cvCustom2, zCustom2, card.getProperty("Custom2")) || visible;
+ visible = cvSetNodeWithLabel(data.cvCustom3, zCustom3, card.getProperty("Custom3")) || visible;
+ visible = cvSetNodeWithLabel(data.cvCustom4, zCustom4, card.getProperty("Custom4")) || visible;
+ visible = cvSetNode(data.cvNotes, card.getProperty("Notes")) || visible;
+
+ cvSetVisible(data.cvhOther, visible);
+ cvSetVisible(data.cvbOther, visible);
+
+ // Chat section
+ visible = cvSetNodeWithLabel(data.cvYahoo, zYahoo,
+ card.getProperty("_Yahoo"));
+ visible = cvSetNodeWithLabel(data.cvSkype, zSkype,
+ card.getProperty("_Skype")) || visible;
+ visible = cvSetNodeWithLabel(data.cvQQ, zQQ,
+ card.getProperty("_QQ")) || visible;
+ visible = cvSetNodeWithLabel(data.cvMSN, zMSN,
+ card.getProperty("_MSN")) || visible;
+ visible = cvSetNodeWithLabel(data.cvICQ, zICQ,
+ card.getProperty("_ICQ")) || visible;
+ visible = cvSetNodeWithLabel(data.cvXMPP, zXMPP,
+ card.getProperty("_JabberId")) || visible;
+ visible = cvSetNodeWithLabel(data.cvIRC, zIRC,
+ card.getProperty("_IRC")) || visible;
+ cvSetVisible(data.cvhChat, visible);
+ cvSetVisible(data.cvbChat, visible);
+
+ // hide description section, not show for non-mailing lists
+ cvSetVisible(data.cvbDescription, false);
+
+ // hide addresses section, not show for non-mailing lists
+ cvSetVisible(data.cvbAddresses, false);
+ }
+
+ // Phone section
+ visible = cvSetNodeWithLabel(data.cvPhWork, zWork, card.getProperty("WorkPhone"));
+ visible = cvSetNodeWithLabel(data.cvPhHome, zHome, card.getProperty("HomePhone")) || visible;
+ visible = cvSetNodeWithLabel(data.cvPhFax, zFax, card.getProperty("FaxNumber")) || visible;
+ visible = cvSetNodeWithLabel(data.cvPhCellular, zCellular, card.getProperty("CellularNumber")) || visible;
+ visible = cvSetNodeWithLabel(data.cvPhPager, zPager, card.getProperty("PagerNumber")) || visible;
+ cvSetVisible(data.cvhPhone, visible);
+ cvSetVisible(data.cvbPhone, visible);
+
+ // Work section
+ visible = cvSetNode(data.cvJobTitle, card.getProperty("JobTitle"));
+ visible = cvSetNode(data.cvDepartment, card.getProperty("Department")) || visible;
+ visible = cvSetNode(data.cvCompany, card.getProperty("Company")) || visible;
+
+ let addressVisible = cvSetNode(data.cvWorkAddress, card.getProperty("WorkAddress"));
+ addressVisible = cvSetNode(data.cvWorkAddress2, card.getProperty("WorkAddress2")) || addressVisible;
+ addressVisible = cvSetCityStateZip(data.cvWorkCityStZip, card.getProperty("WorkCity"), card.getProperty("WorkState"), card.getProperty("WorkZipCode")) || addressVisible;
+ addressVisible = cvSetNode(data.cvWorkCountry, card.getProperty("WorkCountry")) || addressVisible;
+
+ mapURLList = data.cvWorkMapIt.firstChild;
+ if (addressVisible)
+ mapURLList.initMapAddressFromCard(card, "Work");
+
+ cvSetVisible(data.cvbWorkMapItBox, addressVisible && !!mapURLList.mapURL);
+
+ visible = HandleLink(data.cvWorkWebPage, "", card.getProperty("WebPage1"), data.cvWorkWebPageBox, card.getProperty("WebPage1")) || addressVisible || visible;
+
+ cvSetVisible(data.cvhWork, visible);
+ cvSetVisible(data.cvbWork, visible);
+
+ // make the card view box visible
+ cvSetVisible(top.cvData.CardViewBox, true);
+}
+
+function ClearCardViewPane()
+{
+ cvSetVisible(top.cvData.CardViewBox, false);
+}
+
+function cvSetNodeWithLabel(node, label, text)
+{
+ if (text) {
+ if (label)
+ return cvSetNode(node, label + ": " + text);
+ else
+ return cvSetNode(node, text);
+ }
+ else
+ return cvSetNode(node, "");
+}
+
+function cvSetCityStateZip(node, city, state, zip)
+{
+ let text = "";
+
+ if (city && state && zip)
+ text = gAddressBookBundle.getFormattedString("cityAndStateAndZip",
+ [city, state, zip]);
+ else if (city && state && !zip)
+ text = gAddressBookBundle.getFormattedString("cityAndStateNoZip",
+ [city, state]);
+ else if (zip && ((!city && state) || (city && !state)))
+ text = gAddressBookBundle.getFormattedString("cityOrStateAndZip",
+ [city + state, zip]);
+ else {
+ // Only one of the strings is non-empty so contatenating them produces that string.
+ text = city + state + zip;
+ }
+
+ return cvSetNode(node, text);
+}
+
+function cvSetNode(node, text)
+{
+ if (!node)
+ return false;
+
+ node.textContent = text;
+ let visible = !!text;
+ cvSetVisible(node, visible);
+
+ return visible;
+}
+
+function cvAddAddressNodes(node, uri)
+{
+ var visible = false;
+
+ if (node) {
+ var editList = GetDirectoryFromURI(uri);
+ var addressList = editList.addressLists;
+
+ if (addressList) {
+ var total = addressList.length;
+ if (total > 0) {
+ while (node.hasChildNodes()) {
+ node.lastChild.remove();
+ }
+ for (i = 0; i < total; i++ ) {
+ var descNode = document.createElement("description");
+ var card = addressList.queryElementAt(i, Ci.nsIAbCard);
+
+ descNode.setAttribute("class", "CardViewLink");
+ node.appendChild(descNode);
+
+ var linkNode = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
+ linkNode.setAttribute("id", "addr#" + i);
+ linkNode.setAttribute("href", "mailto:" + card.primaryEmail);
+ descNode.appendChild(linkNode);
+
+ var textNode = document.createTextNode(card.displayName + " <" + card.primaryEmail + ">");
+ linkNode.appendChild(textNode);
+ }
+ visible = true;
+ }
+ }
+ cvSetVisible(node, visible);
+ }
+ return visible;
+}
+
+function cvSetVisible(node, visible)
+{
+ if ( visible )
+ node.removeAttribute("collapsed");
+ else
+ node.setAttribute("collapsed", "true");
+}
+
+function HandleLink(node, label, value, box, link)
+{
+ var visible = cvSetNodeWithLabel(node, label, value);
+ if (visible)
+ node.setAttribute('href', link);
+ cvSetVisible(box, visible);
+
+ return visible;
+}
+
+function openLink(aEvent)
+{
+ openAsExternal(aEvent.target.getAttribute("href"));
+ // return false, so we don't load the href in the addressbook window
+ return false;
+}
+
+function openLinkWithUrl(aUrl)
+{
+ if (aUrl)
+ openAsExternal(aUrl);
+ // return false, so we don't load the href in the addressbook window
+ return false;
+}
+
+/* Display the contact photo from the nsIAbCard in the IMG element.
+ * If the photo cannot be displayed, show the generic contact
+ * photo.
+ */
+function displayPhoto(aCard, aImg)
+{
+ var type = aCard.getProperty("PhotoType", "");
+ if (!gPhotoDisplayHandlers[type] ||
+ !gPhotoDisplayHandlers[type](aCard, aImg))
+ gPhotoDisplayHandlers["generic"](aCard, aImg);
+}
+
+/* In order to display the contact photos in the card view, there
+ * must be a registered photo display handler for the card photo
+ * type. The generic, file, and web photo types are handled
+ * by default.
+ *
+ * A photo display handler is a function that behaves as follows:
+ *
+ * function(aCard, aImg):
+ * The function is responsible for determining how to retrieve
+ * the photo from nsIAbCard aCard, and for displaying it in img
+ * img element aImg. Returns true if successful. If it returns
+ * false, the generic photo display handler will be called.
+ *
+ * The following display handlers are for the generic, file and
+ * web photo types.
+ */
+
+var gGenericPhotoDisplayHandler = function(aCard, aImg)
+{
+ aImg.setAttribute("src", defaultPhotoURI);
+ return true;
+};
+
+var gPhotoNameDisplayHandler = function(aCard, aImg)
+{
+ var photoSrc = getPhotoURI(aCard.getProperty("PhotoName"));
+ aImg.setAttribute("src", photoSrc);
+ return true;
+};
+
+/* In order for a photo display handler to be registered for
+ * a particular photo type, it must be registered here.
+ */
+function registerPhotoDisplayHandler(aType, aPhotoDisplayHandler)
+{
+ if (!gPhotoDisplayHandlers[aType])
+ gPhotoDisplayHandlers[aType] = aPhotoDisplayHandler;
+}
+
+registerPhotoDisplayHandler("generic", gGenericPhotoDisplayHandler);
+// File and Web are treated the same, and therefore use the
+// same handler.
+registerPhotoDisplayHandler("file", gPhotoNameDisplayHandler);
+registerPhotoDisplayHandler("web", gPhotoNameDisplayHandler);
diff --git a/comm/suite/mailnews/components/addrbook/content/abCommon.js b/comm/suite/mailnews/components/addrbook/content/abCommon.js
new file mode 100644
index 0000000000..a8e9d37c21
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abCommon.js
@@ -0,0 +1,1185 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const { IOUtils } = ChromeUtils.import("resource:///modules/IOUtils.js");
+const { MailServices } =
+ ChromeUtils.import("resource:///modules/MailServices.jsm");
+const { FileUtils } =
+ ChromeUtils.import("resource://gre/modules/FileUtils.jsm");
+const { PrivateBrowsingUtils } =
+ ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+
+var gDirTree = null;
+var abList = null;
+var gAbResultsTree = null;
+var gAbView = null;
+var gAddressBookBundle;
+// A boolean variable determining whether AB column should be shown in AB
+// sidebar in compose window.
+var gShowAbColumnInComposeSidebar = false;
+
+const kDefaultSortColumn = "GeneratedName";
+const kDefaultAscending = "ascending";
+const kDefaultDescending = "descending";
+// kDefaultYear will be used in birthday calculations when no year is given;
+// this is a leap year so that Feb 29th works.
+const kDefaultYear = nearestLeap(new Date().getFullYear());
+const kMaxYear = 9999;
+const kMinYear = 1;
+const kAllDirectoryRoot = "moz-abdirectory://";
+const kLdapUrlPrefix = "moz-abldapdirectory://";
+const kPersonalAddressbookURI = "moz-abmdbdirectory://abook.mab";
+const kCollectedAddressbookURI = "moz-abmdbdirectory://history.mab";
+// The default, generic contact image is displayed via CSS when the photoURI is
+// blank.
+var defaultPhotoURI = "";
+
+// Controller object for Dir Pane
+var DirPaneController =
+{
+ supportsCommand: function(command)
+ {
+ switch (command) {
+ case "cmd_selectAll":
+ case "cmd_delete":
+ case "button_delete":
+ case "cmd_properties":
+ case "cmd_printcard":
+ case "cmd_printcardpreview":
+ case "cmd_print":
+ case "cmd_printpreview":
+ case "cmd_newlist":
+ case "cmd_newCard":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled: function(command)
+ {
+ switch (command) {
+ case "cmd_selectAll":
+ // The gDirTree pane only handles single selection, but normally we
+ // enable cmd_selectAll as it will get forwarded to the results pane.
+ // But if there is no gAbView, disable as we can't forward to anywhere.
+ return (gAbView != null);
+ case "cmd_delete":
+ case "button_delete": {
+ let selectedDir = getSelectedDirectory();
+ if (!selectedDir)
+ return false;
+ let selectedDirURI = selectedDir.URI;
+
+ // Context-sensitive labels for Edit > Delete menuitem.
+ if (command == "cmd_delete") {
+ goSetMenuValue(command, selectedDir.isMailList ?
+ "valueList" : "valueAddressBook");
+ }
+
+ // If it's one of these special ABs, return false to disable deletion.
+ if (selectedDirURI == kPersonalAddressbookURI ||
+ selectedDirURI == kCollectedAddressbookURI ||
+ selectedDirURI == (kAllDirectoryRoot + "?"))
+ return false;
+
+ // If the directory is a mailing list, and it is read-only,
+ // return false to disable deletion.
+ if (selectedDir.isMailList && selectedDir.readOnly)
+ return false;
+
+ // If the selected directory is an ldap directory,
+ // and if the prefs for this directory are locked,
+ // return false to disable deletion.
+ if (selectedDirURI.startsWith(kLdapUrlPrefix)) {
+ let disable = false;
+ try {
+ let prefName = selectedDirURI.substr(kLdapUrlPrefix.length);
+ disable = Services.prefs.getBoolPref(prefName + ".disable_delete");
+ }
+ catch(ex) {
+ // If this preference is not set, that's ok.
+ }
+ if (disable)
+ return false;
+ }
+
+ // Else return true to enable deletion (default).
+ return true;
+ }
+ case "cmd_printcard":
+ case "cmd_printcardpreview":
+ return (GetSelectedCardIndex() != -1);
+ case "cmd_print":
+ case "cmd_printpreview":
+ document.querySelectorAll("[command=cmd_print]").forEach(e => {
+ e.disabled = false;
+ });
+ return true;
+ case "cmd_properties":
+ return (getSelectedDirectoryURI() != null);
+ case "cmd_newlist":
+ case "cmd_newCard":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function(command)
+ {
+ switch (command) {
+ case "cmd_printcard":
+ case "cmd_printcardpreview":
+ case "cmd_selectAll":
+ SendCommandToResultsPane(command);
+ break;
+ case "cmd_print":
+ AbPrintAddressBook();
+ break;
+ case "cmd_printpreview":
+ AbPrintPreviewAddressBook();
+ break;
+ case "cmd_delete":
+ case "button_delete":
+ if (gDirTree)
+ AbDeleteSelectedDirectory();
+ break;
+ case "cmd_properties":
+ AbEditSelectedDirectory();
+ break;
+ case "cmd_newlist":
+ AbNewList();
+ break;
+ case "cmd_newCard":
+ AbNewCard();
+ break;
+ }
+ },
+
+ onEvent: function(event)
+ {
+ // on blur events set the menu item texts back to the normal values
+ if (event == "blur")
+ goSetMenuValue("cmd_delete", "valueDefault");
+ }
+};
+
+function SendCommandToResultsPane(command)
+{
+ ResultsPaneController.doCommand(command);
+
+ // if we are sending the command so the results pane
+ // we should focus the results pane
+ gAbResultsTree.focus();
+}
+
+function AbNewLDAPDirectory()
+{
+ window.openDialog("chrome://messenger/content/addressbook/pref-directory-add.xul",
+ "",
+ "chrome,modal,resizable=no,centerscreen",
+ null);
+}
+
+function AbNewAddressBook()
+{
+ window.openDialog("chrome://messenger/content/addressbook/abAddressBookNameDialog.xul",
+ "",
+ "chrome,modal,resizable=no,centerscreen",
+ null);
+}
+
+function AbEditSelectedDirectory()
+{
+ let selectedDir = getSelectedDirectory();
+ if (!selectedDir)
+ return;
+
+ if (selectedDir.isMailList) {
+ goEditListDialog(null, selectedDir.URI);
+ } else {
+ window.openDialog(selectedDir.propertiesChromeURI,
+ "",
+ "chrome,modal,resizable=no,centerscreen",
+ {selectedDirectory: selectedDir});
+ }
+}
+
+function AbDeleteSelectedDirectory()
+{
+ let selectedDirURI = getSelectedDirectoryURI();
+ if (!selectedDirURI)
+ return;
+
+ AbDeleteDirectory(selectedDirURI);
+}
+
+function AbDeleteDirectory(aURI)
+{
+ // Determine strings for smart and context-sensitive user prompts
+ // for confirming deletion.
+ let directory = GetDirectoryFromURI(aURI);
+ let confirmDeleteTitleID;
+ let confirmDeleteTitle;
+ let confirmDeleteMessageID;
+ let confirmDeleteMessage;
+ let brandShortName;
+ let clearCollectionPrefs = false;
+
+ if (directory.isMailList) {
+ // It's a mailing list.
+ confirmDeleteMessageID = "confirmDeleteThisMailingList";
+ confirmDeleteTitleID = "confirmDeleteThisMailingListTitle";
+ } else {
+ // It's an address book: check which type.
+ if (Services.prefs.getCharPref("mail.collect_addressbook") == aURI &&
+ (Services.prefs.getBoolPref("mail.collect_email_address_outgoing") ||
+ Services.prefs.getBoolPref("mail.collect_email_address_incoming") ||
+ Services.prefs.getBoolPref("mail.collect_email_address_newsgroup"))) {
+ // It's a collection address book: let's be clear about the consequences.
+ brandShortName = document.getElementById("bundle_brand").getString("brandShortName");
+ confirmDeleteMessageID = "confirmDeleteThisCollectionAddressbook";
+ confirmDeleteTitleID = "confirmDeleteThisCollectionAddressbookTitle";
+ clearCollectionPrefs = true;
+ } else if (directory.URI.startsWith(kLdapUrlPrefix)) {
+ // It's an LDAP directory, so we only delete our offline copy.
+ confirmDeleteMessageID = "confirmDeleteThisLDAPDir";
+ confirmDeleteTitleID = "confirmDeleteThisLDAPDirTitle";
+ } else {
+ // It's a normal personal address book: we'll delete its contacts, too.
+ confirmDeleteMessageID = "confirmDeleteThisAddressbook";
+ confirmDeleteTitleID = "confirmDeleteThisAddressbookTitle";
+ }
+ }
+
+ // Get the raw strings with placeholders.
+ confirmDeleteTitle = gAddressBookBundle.getString(confirmDeleteTitleID);
+ confirmDeleteMessage = gAddressBookBundle.getString(confirmDeleteMessageID);
+
+ // Substitute placeholders as required.
+ // Replace #1 with the name of the selected address book or mailing list.
+ confirmDeleteMessage = confirmDeleteMessage.replace("#1", directory.dirName);
+ if (brandShortName) {
+ // For a collection address book, replace #2 with the brandShortName.
+ confirmDeleteMessage = confirmDeleteMessage.replace("#2", brandShortName);
+ }
+
+ // Ask for confirmation before deleting
+ if (!Services.prompt.confirm(window, confirmDeleteTitle,
+ confirmDeleteMessage)) {
+ // Deletion cancelled by user.
+ return;
+ }
+
+ // If we're about to delete the collection AB, update the respective prefs.
+ if (clearCollectionPrefs) {
+ Services.prefs.setBoolPref("mail.collect_email_address_outgoing", false);
+ Services.prefs.setBoolPref("mail.collect_email_address_incoming", false);
+ Services.prefs.setBoolPref("mail.collect_email_address_newsgroup", false);
+
+ // Change the collection AB pref to "Personal Address Book" so that we
+ // don't get a blank item in prefs dialog when collection is re-enabled.
+ Services.prefs.setCharPref("mail.collect_addressbook",
+ kPersonalAddressbookURI);
+ }
+
+ MailServices.ab.deleteAddressBook(aURI);
+}
+
+function InitCommonJS()
+{
+ gDirTree = document.getElementById("dirTree");
+ abList = document.getElementById("addressbookList");
+ gAddressBookBundle = document.getElementById("bundle_addressBook");
+
+ // Make an entry for "All Address Books".
+ if (abList) {
+ abList.insertItemAt(0, gAddressBookBundle.getString("allAddressBooks"),
+ kAllDirectoryRoot + "?");
+ }
+}
+
+function UpgradeAddressBookResultsPaneUI(prefName)
+{
+ // placeholder in case any new columns get added to the address book
+ // var resultsPaneUIVersion = Services.prefs.getIntPref(prefName);
+}
+
+function AbDelete()
+{
+ let types = GetSelectedCardTypes();
+ if (types == kNothingSelected)
+ return;
+
+ // Determine strings for smart and context-sensitive user prompts
+ // for confirming deletion.
+ let confirmDeleteTitleID;
+ let confirmDeleteTitle;
+ let confirmDeleteMessageID;
+ let confirmDeleteMessage;
+ let itemName;
+ let containingListName;
+ let selectedDir = getSelectedDirectory();
+ let numSelectedItems = gAbView.selection.count;
+
+ switch(types) {
+ case kListsAndCards:
+ confirmDeleteMessageID = "confirmDelete2orMoreContactsAndLists";
+ confirmDeleteTitleID = "confirmDelete2orMoreContactsAndListsTitle";
+ break;
+ case kSingleListOnly:
+ // Set item name for single mailing list.
+ let theCard = GetSelectedAbCards()[0];
+ itemName = theCard.displayName;
+ confirmDeleteMessageID = "confirmDeleteThisMailingList";
+ confirmDeleteTitleID = "confirmDeleteThisMailingListTitle";
+ break;
+ case kMultipleListsOnly:
+ confirmDeleteMessageID = "confirmDelete2orMoreMailingLists";
+ confirmDeleteTitleID = "confirmDelete2orMoreMailingListsTitle";
+ break;
+ case kCardsOnly:
+ if (selectedDir.isMailList) {
+ // Contact(s) in mailing lists will be removed from the list, not deleted.
+ if (numSelectedItems == 1) {
+ confirmDeleteMessageID = "confirmRemoveThisContact";
+ confirmDeleteTitleID = "confirmRemoveThisContactTitle";
+ } else {
+ confirmDeleteMessageID = "confirmRemove2orMoreContacts";
+ confirmDeleteTitleID = "confirmRemove2orMoreContactsTitle";
+ }
+ // For removing contacts from mailing list, set placeholder value
+ containingListName = selectedDir.dirName;
+ } else {
+ // Contact(s) in address books will be deleted.
+ if (numSelectedItems == 1) {
+ confirmDeleteMessageID = "confirmDeleteThisContact";
+ confirmDeleteTitleID = "confirmDeleteThisContactTitle";
+ } else {
+ confirmDeleteMessageID = "confirmDelete2orMoreContacts";
+ confirmDeleteTitleID = "confirmDelete2orMoreContactsTitle";
+ }
+ }
+ if (numSelectedItems == 1) {
+ // Set item name for single contact.
+ let theCard = GetSelectedAbCards()[0];
+ let nameFormatFromPref = Services.prefs.getIntPref("mail.addr_book.lastnamefirst");
+ itemName = theCard.generateName(nameFormatFromPref);
+ }
+ break;
+ }
+
+ // Get the raw model strings.
+ // For numSelectedItems == 1, it's simple strings.
+ // For messages with numSelectedItems > 1, it's multi-pluralform string sets.
+ // confirmDeleteMessage has placeholders for some forms.
+ confirmDeleteTitle = gAddressBookBundle.getString(confirmDeleteTitleID);
+ confirmDeleteMessage = gAddressBookBundle.getString(confirmDeleteMessageID);
+
+ // Get plural form where applicable; substitute placeholders as required.
+ if (numSelectedItems == 1) {
+ // If single selected item, substitute itemName.
+ confirmDeleteMessage = confirmDeleteMessage.replace("#1", itemName);
+ } else {
+ // If multiple selected items, get the right plural string from the
+ // localized set, then substitute numSelectedItems.
+ confirmDeleteMessage = PluralForm.get(numSelectedItems, confirmDeleteMessage);
+ confirmDeleteMessage = confirmDeleteMessage.replace("#1", numSelectedItems);
+ }
+ // If contact(s) in a mailing list, substitute containingListName.
+ if (containingListName)
+ confirmDeleteMessage = confirmDeleteMessage.replace("#2", containingListName);
+
+ // Finally, show our smart confirmation message, and act upon it!
+ if (!Services.prompt.confirm(window, confirmDeleteTitle,
+ confirmDeleteMessage)) {
+ // Deletion cancelled by user.
+ return;
+ }
+
+ if (selectedDir.URI == (kAllDirectoryRoot + "?")) {
+ // Delete cards from "All Address Books" view.
+ let cards = GetSelectedAbCards();
+ for (let i = 0; i < cards.length; i++) {
+ let dirId = cards[i].directoryId
+ .substring(0, cards[i].directoryId.indexOf("&"));
+ let directory = MailServices.ab.getDirectoryFromId(dirId);
+
+ let cardArray =
+ Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ cardArray.appendElement(cards[i], false);
+ if (directory)
+ directory.deleteCards(cardArray);
+ }
+ SetAbView(kAllDirectoryRoot + "?");
+ } else {
+ // Delete cards from address books or mailing lists.
+ gAbView.deleteSelectedCards();
+ }
+}
+
+function AbNewCard()
+{
+ goNewCardDialog(getSelectedDirectoryURI());
+}
+
+function AbEditCard(card)
+{
+ // Need a card,
+ if (!card)
+ return;
+
+ if (card.isMailList) {
+ goEditListDialog(card, card.mailListURI);
+ } else {
+ goEditCardDialog(getSelectedDirectoryURI(), card);
+ }
+}
+
+function AbNewMessage()
+{
+ let params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(Ci.nsIMsgComposeParams);
+ if (params) {
+ let composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(Ci.nsIMsgCompFields);
+ if (composeFields) {
+ params.type = Ci.nsIMsgCompType.New;
+ params.format = Ci.nsIMsgCompFormat.Default;
+ if (DirPaneHasFocus()) {
+ let selectedDir = getSelectedDirectory();
+ let hidesRecipients = false;
+ try {
+ // This is a bit of hackery so that extensions can have mailing lists
+ // where recipients are sent messages via BCC.
+ hidesRecipients = selectedDir.getBoolValue("HidesRecipients", false);
+ } catch(e) {
+ // Standard Thunderbird mailing lists do not have preferences
+ // associated with them, so we'll silently eat the error.
+ }
+
+ if (selectedDir && selectedDir.isMailList && hidesRecipients)
+ // Bug 669301 (https://bugzilla.mozilla.org/show_bug.cgi?id=669301)
+ // We're using BCC right now to hide recipients from one another.
+ // We should probably use group syntax, but that's broken
+ // right now, so this will have to do.
+ composeFields.bcc = GetSelectedAddressesFromDirTree();
+ else
+ composeFields.to = GetSelectedAddressesFromDirTree();
+ } else {
+ composeFields.to = GetSelectedAddresses();
+ }
+ params.composeFields = composeFields;
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+ }
+ }
+}
+
+function AbCopyAddress()
+{
+ var cards = GetSelectedAbCards();
+ if (!cards)
+ return;
+
+ var count = cards.length;
+ if (!count)
+ return;
+
+ var addresses = cards[0].primaryEmail;
+ for (var i = 1; i < count; i++)
+ addresses += "," + cards[i].primaryEmail;
+
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(addresses);
+}
+
+/**
+ * Set up items in the View > Layout menupopup. This function is responsible
+ * for updating the menu items' state to reflect reality.
+ *
+ * @param aEvent the event that caused the View > Layout menupopup to be shown
+ */
+function InitViewLayoutMenuPopup(aEvent)
+{
+ let dirTreeVisible = document.getElementById("dirTree-splitter")
+ .getAttribute("state") != "collapsed";
+ document.getElementById("menu_showDirectoryPane")
+ .setAttribute("checked", dirTreeVisible);
+
+ let cardPaneVisible = document.getElementById("results-splitter")
+ .getAttribute("state") != "collapsed";
+ document.getElementById("menu_showCardPane")
+ .setAttribute("checked", cardPaneVisible);
+}
+
+// Generate a list of cards from the selected mailing list
+// and get a comma separated list of card addresses. If the
+// item selected in the directory pane is not a mailing list,
+// an empty string is returned.
+function GetSelectedAddressesFromDirTree()
+{
+ let selectedDir = getSelectedDirectory();
+
+ if (!selectedDir || !selectedDir.isMailList)
+ return "";
+
+ let listCardsCount = selectedDir.addressLists.length;
+ let cards = new Array(listCardsCount);
+ for (let i = 0; i < listCardsCount; ++i)
+ cards[i] = selectedDir.addressLists
+ .queryElementAt(i, Ci.nsIAbCard);
+ return GetAddressesForCards(cards);
+}
+
+// Generate a comma separated list of addresses from a given
+// set of cards.
+function GetAddressesForCards(cards)
+{
+ var addresses = "";
+
+ if (!cards)
+ return addresses;
+
+ var count = cards.length;
+ for (var i = 0; i < count; ++i) {
+ var generatedAddress = GenerateAddressFromCard(cards[i]);
+ if (generatedAddress) {
+ // If it's not the first address in the list, add a comma separator.
+ if (addresses)
+ addresses += ",";
+ addresses += generatedAddress;
+ }
+ }
+
+ return addresses;
+}
+
+
+function SelectFirstAddressBook()
+{
+ if (gDirectoryTreeView.selection.currentIndex != 0) {
+ gDirectoryTreeView.selection.select(0);
+ ChangeDirectoryByURI(getSelectedDirectoryURI());
+ }
+ gAbResultsTree.focus();
+}
+
+function DirPaneClick(event)
+{
+ // we only care about left button events
+ if (event.button != 0)
+ return;
+
+ // if the user clicks on the header / trecol, do nothing
+ if (event.originalTarget.localName == "treecol") {
+ event.stopPropagation();
+ return;
+ }
+}
+
+function DirPaneDoubleClick(event)
+{
+ // We only care about left button events.
+ if (event.button != 0)
+ return;
+
+ // Ignore double clicking on invalid rows.
+ let row = gDirTree.treeBoxObject.getRowAt(event.clientX, event.clientY);
+ if (row == -1 || row >= gDirectoryTreeView.rowCount)
+ return;
+
+ // Default action for double click is expand/collapse which ships with the tree.
+ // For convenience, allow double-click to edit the properties of mailing
+ // lists in directory tree.
+ if (gDirTree && gDirTree.view.selection &&
+ gDirTree.view.selection.count == 1 &&
+ getSelectedDirectory().isMailList) {
+ AbEditSelectedDirectory();
+ }
+}
+
+function DirPaneSelectionChange()
+{
+ let uri = getSelectedDirectoryURI();
+ // clear out the search box when changing folders...
+ onAbClearSearch(false);
+ if (gDirectoryTreeView.selection &&
+ gDirectoryTreeView.selection.count == 1) {
+ ChangeDirectoryByURI(uri);
+ document.getElementById("localResultsOnlyMessage")
+ .setAttribute("hidden",
+ !gDirectoryTreeView.hasRemoteAB ||
+ uri != kAllDirectoryRoot + "?");
+ }
+}
+
+function ChangeDirectoryByURI(uri = kPersonalAddressbookURI)
+{
+ SetAbView(uri);
+
+ // Actively de-selecting if there are any pre-existing selections
+ // in the results list.
+ if (gAbView && gAbView.getCardFromRow(0))
+ gAbView.selection.clearSelection();
+ else
+ // the selection changes if we were switching directories.
+ ResultsPaneSelectionChanged()
+}
+
+function AbNewList()
+{
+ goNewListDialog(getSelectedDirectoryURI());
+}
+
+function goNewListDialog(selectedAB)
+{
+ window.openDialog("chrome://messenger/content/addressbook/abMailListDialog.xul",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ {selectedAB:selectedAB});
+}
+
+function goEditListDialog(abCard, listURI)
+{
+ let params = {
+ abCard: abCard,
+ listURI: listURI,
+ refresh: false, // This is an out param, true if OK in dialog is clicked.
+ };
+
+ window.openDialog("chrome://messenger/content/addressbook/abEditListDialog.xul",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ params);
+
+ if (params.refresh) {
+ ChangeDirectoryByURI(listURI); // force refresh
+ }
+}
+
+function goNewCardDialog(selectedAB)
+{
+ window.openDialog("chrome://messenger/content/addressbook/abNewCardDialog.xul",
+ "",
+ "chrome,modal,resizable=no,centerscreen",
+ {selectedAB:selectedAB});
+}
+
+function goEditCardDialog(abURI, card)
+{
+ window.openDialog("chrome://messenger/content/addressbook/abEditCardDialog.xul",
+ "",
+ "chrome,modal,resizable=no,centerscreen",
+ {abURI:abURI, card:card});
+}
+
+function setSortByMenuItemCheckState(id, value)
+{
+ var menuitem = document.getElementById(id);
+ if (menuitem) {
+ menuitem.setAttribute("checked", value);
+ }
+}
+
+function InitViewSortByMenu()
+{
+ var sortColumn = kDefaultSortColumn;
+ var sortDirection = kDefaultAscending;
+
+ if (gAbView) {
+ sortColumn = gAbView.sortColumn;
+ sortDirection = gAbView.sortDirection;
+ }
+
+ // this approach is necessary to support generic columns that get overlayed.
+ let elements = document.querySelectorAll('[name="sortas"]');
+ for (let i = 0; i < elements.length; i++) {
+ let cmd = elements[i].id;
+ let columnForCmd = cmd.substr(10); // everything right of cmd_SortBy
+ setSortByMenuItemCheckState(cmd, (sortColumn == columnForCmd));
+ }
+
+ setSortByMenuItemCheckState("sortAscending", (sortDirection == kDefaultAscending));
+ setSortByMenuItemCheckState("sortDescending", (sortDirection == kDefaultDescending));
+}
+
+function GenerateAddressFromCard(card)
+{
+ if (!card)
+ return "";
+
+ var email;
+
+ if (card.isMailList)
+ {
+ var directory = GetDirectoryFromURI(card.mailListURI);
+ email = directory.description || card.displayName;
+ }
+ else
+ email = card.primaryEmail;
+
+ return MailServices.headerParser.makeMimeAddress(card.displayName, email);
+}
+
+function GetDirectoryFromURI(uri)
+{
+ return MailServices.ab.getDirectory(uri);
+}
+
+// returns null if abURI is not a mailing list URI
+function GetParentDirectoryFromMailingListURI(abURI)
+{
+ var abURIArr = abURI.split("/");
+ /*
+ turn turn "moz-abmdbdirectory://abook.mab/MailList6"
+ into ["moz-abmdbdirectory:","","abook.mab","MailList6"]
+ then, turn ["moz-abmdbdirectory:","","abook.mab","MailList6"]
+ into "moz-abmdbdirectory://abook.mab"
+ */
+ if (abURIArr.length == 4 && abURIArr[0] == "moz-abmdbdirectory:" && abURIArr[3] != "") {
+ return abURIArr[0] + "/" + abURIArr[1] + "/" + abURIArr[2];
+ }
+
+ return null;
+}
+
+/**
+ * Return true if the directory pane has focus, otherwise false.
+ */
+function DirPaneHasFocus()
+{
+ return (top.document.commandDispatcher.focusedElement == gDirTree);
+}
+
+/**
+ * Get the selected directory object.
+ *
+ * @return The object of the currently selected directory
+ */
+function getSelectedDirectory()
+{
+ // Select Addresses Dialog
+ if (abList)
+ return MailServices.ab.getDirectory(abList.value);
+
+ // Main Address Book
+ if (gDirTree.currentIndex < 0)
+ return null;
+ return gDirectoryTreeView.getDirectoryAtIndex(gDirTree.currentIndex);
+}
+
+/**
+ * Get the URI of the selected directory.
+ *
+ * @return The URI of the currently selected directory
+ */
+function getSelectedDirectoryURI()
+{
+ // Select Addresses Dialog
+ if (abList)
+ return abList.value;
+
+ // Main Address Book
+ if (gDirTree.currentIndex < 0)
+ return null;
+ return gDirectoryTreeView.getDirectoryAtIndex(gDirTree.currentIndex).URI;
+}
+
+/**
+ * DEPRECATED legacy function wrapper for addon compatibility;
+ * use getSelectedDirectoryURI() instead!
+ * Return the URI of the selected directory.
+ */
+function GetSelectedDirectory()
+{
+ return getSelectedDirectoryURI();
+}
+
+/**
+ * Clears the contents of the search input field,
+ * possibly causing refresh of results.
+ *
+ * @param aRefresh Set to false if the refresh isn't needed,
+ * e.g. window/AB is going away so user will not see anything.
+ */
+function onAbClearSearch(aRefresh = true)
+{
+ let searchInput = document.getElementById("searchInput");
+ if (!searchInput || !searchInput.value)
+ return;
+
+ searchInput.value = "";
+ if (aRefresh)
+ onEnterInSearchBar();
+}
+
+/**
+ * Returns an nsIFile of the directory in which contact photos are stored.
+ * This will create the directory if it does not yet exist.
+ */
+function getPhotosDir() {
+ var file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ // Get the Photos directory
+ file.append("Photos");
+ if (!file.exists() || !file.isDirectory())
+ file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0777", 8));
+ return file;
+}
+
+/**
+ * Returns a URI specifying the location of a photo based on its name.
+ * If the name is blank, or if the photo with that name is not in the Photos
+ * directory then the default photo URI is returned.
+ *
+ * @param aPhotoName The name of the photo from the Photos folder, if any.
+ *
+ * @return A URI pointing to a photo.
+ */
+function getPhotoURI(aPhotoName) {
+ if (!aPhotoName)
+ return defaultPhotoURI;
+ var file = getPhotosDir();
+ try {
+ file.append(aPhotoName);
+ }
+ catch (e) {
+ return defaultPhotoURI;
+ }
+ if (!file.exists())
+ return defaultPhotoURI;
+ return Services.io.newFileURI(file).spec;
+}
+
+/**
+ * Generates a unique filename to be used for a local copy of a contact's photo.
+ *
+ * @param aPath The path to the folder in which the photo will be saved.
+ * @param aExtension The file extension of the photo.
+ *
+ * @return A unique filename in the given path.
+ */
+function makePhotoFile(aDir, aExtension) {
+ var filename, newFile;
+ // Find a random filename for the photo that doesn't exist yet
+ do {
+ filename = new String(Math.random()).replace("0.", "") + "." + aExtension;
+ newFile = aDir.clone();
+ newFile.append(filename);
+ } while (newFile.exists());
+ return newFile;
+}
+
+/**
+ * Public self-contained object for image transfers.
+ * Responsible for file transfer, validating the image and downscaling.
+ * Attention: It is the responsibility of the caller to remove the old photo
+ * and update the card!
+ */
+var gImageDownloader = (function() {
+ let downloadInProgress = false;
+
+ // Current instance of nsIWebBrowserPersist. It is used two times, during
+ // the actual download and for saving canvas data.
+ let downloader;
+
+ // Temporary nsIFile used for download.
+ let tempFile;
+
+ // Images are downsized to this size while keeping the aspect ratio.
+ const maxSize = 300;
+
+ // Callback function for various states
+ let callbackSuccess;
+ let callbackError;
+ let callbackProgress;
+
+ // Start at 4% to show a slight progress initially.
+ const initProgress = 4;
+
+ // Constants indicating error and file transfer status
+ const STATE_TRANSFERRING = 0;
+ const STATE_RESIZING = 1;
+ const STATE_OK = 2;
+ // The URI does not have a valid format.
+ const ERROR_INVALID_URI = 0;
+ // In case of HTTP transfers: server did not answer with a 200 status code.
+ const ERROR_UNAVAILABLE = 1;
+ // The file type is not supported. Only jpeg, png and gif are.
+ const ERROR_INVALID_IMG = 2;
+ // An error occurred while saving the image to the hard drive.
+ const ERROR_SAVE = 4;
+
+
+ /**
+ * Saves a target photo in the profile's photo directory. Only one concurrent
+ * file transfer is supported. Starting a new transfer while another is still
+ * in progress will cancel the former file transfer.
+ *
+ * @param aURI {string} URI pointing to the photo.
+ * @param cbSuccess(photoName) {function} A callback function which is called
+ * on success.
+ * The photo file name is passed in.
+ * @param cbError(state) {function} A callback function which is called
+ * in case of an error. The error
+ * state is passed in.
+ * @param cbProgress(errcode, percent) {function} A callback function which
+ * provides progress report.
+ * An error code (see above)
+ * and the progress percentage
+ * (0-100) is passed in.
+ * State transitions: STATE_TRANSFERRING -> STATE_RESIZING -> STATE_OK (100%)
+ */
+ function savePhoto(aURI, aCBSuccess, aCBError, aCBProgress) {
+ callbackSuccess = typeof aCBSuccess == "function" ? aCBSuccess : null;
+ callbackError = typeof aCBError == "function" ? aCBError : null;
+ callbackProgress = typeof aCBProgress == "function" ? aCBProgress : null;
+
+ // Make sure that there is no running download.
+ cancelSave();
+ downloadInProgress = true;
+
+ if (callbackProgress) {
+ callbackProgress(STATE_TRANSFERRING, initProgress);
+ }
+
+ downloader = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+ downloader.persistFlags =
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_CLEANUP_ON_FAILURE;
+ downloader.progressListener = {
+ onProgressChange(aWebProgress, aRequest, aCurSelfProgress,
+ aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
+ if (aMaxTotalProgress > -1 && callbackProgress) {
+ // Download progress is 0-90%, 90-100% is verifying and scaling the
+ // image.
+ let percent =
+ Math.round(initProgress +
+ (aCurTotalProgress / aMaxTotalProgress) *
+ (90 - initProgress));
+ callbackProgress(STATE_TRANSFERRING, percent);
+ }
+ },
+ onStateChange(aWebProgress, aRequest, aStateFlag, aStatus) {
+ // Check if the download successfully finished.
+ if ((aStateFlag & Ci.nsIWebProgressListener.STATE_STOP) &&
+ !(aStateFlag & Ci.nsIWebProgressListener.STATE_IS_REQUEST)) {
+ try {
+ // Check the response code in case of an HTTP request to catch 4xx
+ // errors.
+ let http = aRequest.QueryInterface(Ci.nsIHttpChannel);
+ if (http.responseStatus == 200) {
+ verifyImage();
+ } else if (callbackError) {
+ callbackError(ERROR_UNAVAILABLE);
+ }
+ } catch (err) {
+ // The nsIHttpChannel interface is not available - just proceed.
+ verifyImage();
+ }
+ }
+ },
+ };
+
+ let source;
+ try {
+ source = Services.io.newURI(aURI);
+ } catch (err) {
+ if (callbackError) {
+ callbackError(ERROR_INVALID_URI);
+ }
+ return;
+ }
+
+ // Start the transfer to a temporary file.
+ tempFile = FileUtils.getFile("TmpD",
+ ["tb-photo-" + new Date().getTime() + ".tmp"]);
+ tempFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ try {
+ // Obtain the privacy context of the browser window that the URL
+ // we are downloading comes from. If, and only if, the URL is not
+ // related to a window, null should be used instead.
+ let privacy = PrivateBrowsingUtils.privacyContextFromWindow(window);
+ downloader.saveURI(source, null, null, null, null, null, tempFile,
+ privacy);
+ } catch (err) {
+ cleanup();
+ if (callbackError) {
+ callbackError(ERROR_SAVE);
+ }
+ }
+ }
+
+ /**
+ * Verifies the downloaded file to be an image.
+ * Scales the image and starts the saving operation.
+ */
+ function verifyImage() {
+ let img = new Image();
+ img.onerror = function() {
+ cleanup();
+ if (callbackError) {
+ callbackError(ERROR_INVALID_IMG);
+ }
+ };
+ img.onload = function() {
+ if (callbackProgress) {
+ callbackProgress(STATE_RESIZING, 95);
+ }
+
+ // Images are scaled down in two steps to improve quality. Resizing
+ // ratios larger than 2 use a different interpolation algorithm than
+ // small ratios. Resize three times (instead of just two steps) to
+ // improve the final quality.
+ let canvas = downscale(img, 3.8 * maxSize);
+ canvas = downscale(canvas, 1.9 * maxSize);
+ canvas = downscale(canvas, maxSize);
+
+ saveCanvas(canvas);
+
+ if (callbackProgress) {
+ callbackProgress(STATE_OK, 100);
+ }
+
+ // Remove the temporary file.
+ cleanup();
+ };
+
+ if (callbackProgress) {
+ callbackProgress(STATE_RESIZING, 92);
+ }
+
+ img.src = Services.io.newFileURI(tempFile).spec;
+ }
+
+ /**
+ * Scale a graphics object down to a specified maximum dimension while
+ * preserving the aspect ratio. Does not upscale an image.
+ *
+ * @param aGraphicsObject {image | canvas} Image or canvas object
+ * @param aMaxDimension {integer} The maximal allowed width or
+ * height
+ *
+ * @return A canvas object.
+ */
+ function downscale(aGraphicsObject, aMaxDimension) {
+ let w = aGraphicsObject.width;
+ let h = aGraphicsObject.height;
+
+ if (w > h && w > aMaxDimension) {
+ h = Math.round(aMaxDimension * h / w);
+ w = aMaxDimension;
+ } else if (h > aMaxDimension) {
+ w = Math.round(aMaxDimension * w / h);
+ h = aMaxDimension;
+ }
+
+ let canvas = document.createElementNS("http://www.w3.org/1999/xhtml",
+ "canvas");
+ canvas.width = w;
+ canvas.height = h;
+
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(aGraphicsObject, 0, 0, w, h);
+ return canvas;
+ }
+
+ /**
+ * Cancel a running download (if any).
+ */
+ function cancelSave() {
+ if (!downloadInProgress) {
+ return;
+ }
+
+ // Cancel the nsIWebBrowserPersist file transfer.
+ if (downloader) {
+ downloader.cancelSave();
+ }
+ cleanup();
+ }
+
+ /**
+ * Remove the temporary file and reset internal status.
+ */
+ function cleanup() {
+ if (tempFile) {
+ try {
+ if (tempFile.exists()) {
+ tempFile.remove(false);
+ }
+ } catch (err) {}
+ tempFile = null;
+ }
+
+ downloadInProgress = false;
+ }
+
+ /**
+ * Save the contents of a canvas to the photos directory of the profile.
+ */
+ function saveCanvas(aCanvas) {
+ // Get the photos directory and check that it exists.
+ let file = getPhotosDir();
+ file = makePhotoFile(file, "png");
+
+ // Create a data url from the canvas and then create URIs of the source and
+ // targets.
+ let source = Services.io.newURI(aCanvas.toDataURL("image/png", ""), "UTF8");
+ let target = Services.io.newFileURI(file);
+
+ downloader = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(Ci.nsIWebBrowserPersist);
+ downloader.persistFlags =
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
+ Ci.nsIWebBrowserPersist.PERSIST_FLAGS_CLEANUP_ON_FAILURE;
+ downloader.progressListener = {
+ onStateChange(aWebProgress, aRequest, aFlag, aStatus) {
+ if ((aFlag & Ci.nsIWebProgressListener.STATE_STOP) &&
+ !(aFlag & Ci.nsIWebProgressListener.STATE_IS_REQUEST)) {
+ if (callbackSuccess) {
+ callbackSuccess(file.leafName);
+ }
+ }
+ },
+ };
+
+ // Obtain the privacy context of the browser window that the URL
+ // we are downloading comes from. If, and only if, the URL is not
+ // related to a window, null should be used instead.
+ let privacy = PrivateBrowsingUtils.privacyContextFromWindow(window);
+ downloader.saveURI(source, null, null, null, null, null, target, privacy);
+ }
+
+ // Publicly accessible methods.
+ return { cancelSave, savePhoto, STATE_TRANSFERRING, STATE_RESIZING, STATE_OK,
+ ERROR_UNAVAILABLE, ERROR_INVALID_URI, ERROR_INVALID_IMG,
+ ERROR_SAVE, };
+})();
+
+/**
+ * Validates the given year and returns it, if it looks sane.
+ * Returns kDefaultYear (a leap year), if no valid date is given.
+ * This ensures that month/day calculations still work.
+ */
+function saneBirthYear(aYear) {
+ return aYear && (aYear <= kMaxYear) && (aYear >= kMinYear) ? aYear : kDefaultYear;
+}
+
+/**
+ * Returns the nearest leap year before aYear.
+ */
+function nearestLeap(aYear) {
+ for (let year = aYear; year > 0; year--) {
+ if (new Date(year, 1, 29).getMonth() == 1)
+ return year;
+ }
+ return 2000;
+}
diff --git a/comm/suite/mailnews/components/addrbook/content/abEditCardDialog.xul b/comm/suite/mailnews/components/addrbook/content/abEditCardDialog.xul
new file mode 100644
index 0000000000..752934becb
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abEditCardDialog.xul
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/addressbook/abCardOverlay.xul"?>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="abcardWindow"
+ onload="OnLoadEditCard()"
+ ondialogaccept="return EditCardOKButton();"
+ ondialogcancel="return EditCardCancelButton();">
+
+ <stringbundleset id="stringbundleset"/>
+ <vbox id="editcard"/>
+</dialog>
diff --git a/comm/suite/mailnews/components/addrbook/content/abEditListDialog.xul b/comm/suite/mailnews/components/addrbook/content/abEditListDialog.xul
new file mode 100644
index 0000000000..3162664e99
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abEditListDialog.xul
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/addressbook/abListOverlay.xul"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/addressbook/abMailListDialog.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ id="ablistWindow"
+ onload="OnLoadEditList()"
+ ondialogaccept="return EditListOKButton();"
+ ondragover="DragOverAddressListTree(event);"
+ ondrop="DropOnAddressListTree(event);">
+
+ <stringbundleset id="stringbundleset"/>
+ <vbox id="editlist" flex="1"/>
+</dialog>
diff --git a/comm/suite/mailnews/components/addrbook/content/abListOverlay.xul b/comm/suite/mailnews/components/addrbook/content/abListOverlay.xul
new file mode 100644
index 0000000000..9f8c07f010
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abListOverlay.xul
@@ -0,0 +1,86 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/addressingWidget.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/cardDialog.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/addressbook/abMailListDialog.dtd">
+
+<overlay id="editListOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+ </stringbundleset>
+
+ <script src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"/>
+ <script src="chrome://messenger/content/addressbook/abCommon.js"/>
+ <script src="chrome://messenger/content/addressbook/abMailListDialog.js"/>
+
+<vbox id="editlist">
+ <grid>
+ <columns>
+ <column/>
+ <column class="CardEditWidth" flex="1"/>
+ </columns>
+
+ <rows>
+ <row id="ListNameContainer" align="center">
+ <label control="ListName"
+ class="CardEditLabel"
+ value="&ListName.label;"
+ accesskey="&ListName.accesskey;"/>
+ <textbox id="ListName"/>
+ </row>
+
+ <row id="ListNickNameContainer" align="center">
+ <label control="ListNickName"
+ class="CardEditLabel"
+ value="&ListNickName.label;"
+ accesskey="&ListNickName.accesskey;"/>
+ <textbox id="ListNickName"/>
+ </row>
+
+ <row id="ListDescriptionContainer" align="center">
+ <label control="ListDescription"
+ class="CardEditLabel"
+ value="&ListDescription.label;"
+ accesskey="&ListDescription.accesskey;"/>
+ <textbox id="ListDescription"/>
+ </row>
+ </rows>
+ </grid>
+
+ <spacer style="height:1em"/>
+ <label control="addressCol1#1"
+ value="&AddressTitle.label;"
+ accesskey="&AddressTitle.accesskey;"/>
+
+ <spacer style="height:0.1em"/>
+
+ <listbox id="addressingWidget" style="height: 15em;"
+ onclick="awClickEmptySpace(event.target, true)">
+ <listitem class="addressingWidgetItem" allowevents="true">
+ <listcell class="addressingWidgetCell">
+ <textbox id="addressCol1#1"
+ class="plain textbox-addressingWidget uri-element"
+ type="autocomplete"
+ flex="1"
+ autocompletesearch="addrbook ldap"
+ autocompletesearchparam="{}" timeout="300" maxrows="4"
+ completedefaultindex="true" forcecomplete="true"
+ minresultsforpopup="3"
+ ontextentered="awRecipientTextCommand(eventParam, this)"
+ onkeydown="awRecipientKeyDown(event, this);"
+ onclick="awNotAnEmptyArea(event);">
+ <image onclick="awNotAnEmptyArea(event)" class="person-icon"/>
+ </textbox>
+ </listcell>
+ </listitem>
+ </listbox>
+</vbox>
+
+</overlay>
+
diff --git a/comm/suite/mailnews/components/addrbook/content/abMailListDialog.xul b/comm/suite/mailnews/components/addrbook/content/abMailListDialog.xul
new file mode 100644
index 0000000000..f335bfdce3
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abMailListDialog.xul
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/addressbook/abListOverlay.xul"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/addressbook/abMailListDialog.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="ablistWindow"
+ title="&mailListWindow.title;"
+ ondialogaccept="return MailListOKButton();"
+ onload="OnLoadNewMailList()"
+ ondragover="DragOverAddressListTree(event);"
+ ondrop="DropOnAddressListTree(event);">
+
+ <stringbundleset id="stringbundleset"/>
+
+ <hbox align="center" valign="center">
+ <label control="abPopup" value="&addToAddressBook.label;" accesskey="&addToAddressBook.accesskey;"/>
+ <menulist id="abPopup">
+ <menupopup id="abPopup-menupopup" class="addrbooksPopup" writable="true"
+ supportsmaillists="true"/>
+ </menulist>
+ </hbox>
+
+ <spacer style="height:1em"/>
+
+ <vbox id="editlist"/>
+
+</dialog>
diff --git a/comm/suite/mailnews/components/addrbook/content/abNewCardDialog.xul b/comm/suite/mailnews/components/addrbook/content/abNewCardDialog.xul
new file mode 100644
index 0000000000..80f4e578e3
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abNewCardDialog.xul
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/addressbook/abCardOverlay.xul"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/addressbook/abNewCardDialog.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="abcardWindow"
+ windowtype="mailnews:newcarddialog"
+ onload="OnLoadNewCard()"
+ ondialogaccept="return NewCardOKButton();"
+ ondialogcancel="return NewCardCancelButton();">
+
+ <stringbundleset id="stringbundleset"/>
+
+ <hbox align="center">
+
+ <label id="abPopupLabel" control="abPopup" value="&chooseAddressBook.label;" accesskey="&chooseAddressBook.accesskey;"/>
+
+ <menulist id="abPopup">
+ <menupopup id="abPopup-menupopup" class="addrbooksPopup" writable="true"/>
+ </menulist>
+
+ </hbox>
+
+ <spacer style="height:1em"/>
+
+ <vbox id="editcard"/>
+
+</dialog>
diff --git a/comm/suite/mailnews/components/addrbook/content/abResultsPaneOverlay.xul b/comm/suite/mailnews/components/addrbook/content/abResultsPaneOverlay.xul
new file mode 100644
index 0000000000..08af2de3fc
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abResultsPaneOverlay.xul
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/abResultsPane.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/addressbook/abResultsPaneOverlay.dtd">
+
+<overlay
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script src="chrome://messenger/content/addressbook/abResultsPane.js"/>
+<script src="chrome://global/content/nsDragAndDrop.js"/>
+<script src="chrome://messenger/content/addressbook/abDragDrop.js"/>
+
+<tree id="abResultsTree" flex="1" enableColumnDrag="true" class="plain focusring"
+ onclick="AbResultsPaneOnClick(event);"
+ onselect="this.view.selectionChanged(); document.commandDispatcher.updateCommands('addrbook-select');"
+ sortCol="GeneratedName"
+ persist="sortCol height">
+
+ <treecols id="abResultsTreeCols">
+ <!-- these column ids must match up to the mork column names, except for GeneratedName and ChatName, see nsIAddrDatabase.idl -->
+ <treecol id="GeneratedName"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&GeneratedName.label;" primary="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="PrimaryEmail"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&PrimaryEmail.label;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="ChatName"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&ChatName.label;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="Company"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&Company.label;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="NickName"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&NickName.label;" hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="SecondEmail"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&SecondEmail.label;" hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="Department"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&Department.label;" hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="JobTitle"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&JobTitle.label;" hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="CellularNumber"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&CellularNumber.label;" hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="PagerNumber"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&PagerNumber.label;" hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="FaxNumber"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&FaxNumber.label;" hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="HomePhone"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&HomePhone.label;" hidden="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="WorkPhone"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&WorkPhone.label;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="addrbook"
+ persist="hidden ordinal width sortDirection" flex="1"
+ hidden="true"
+ label="&Addrbook.label;"/>
+ <!-- LOCALIZATION NOTE: _PhoneticName may be enabled for Japanese builds. -->
+ <!--
+ <splitter class="tree-splitter"/>
+ <treecol id="_PhoneticName"
+ persist="hidden ordinal width sortDirection" flex="1"
+ label="&_PhoneticName.label;" hidden="true"/>
+ -->
+
+ </treecols>
+ <treechildren ondragstart="nsDragAndDrop.startDrag(event, abResultsPaneObserver);"/>
+</tree>
+
+</overlay>
+
diff --git a/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.js b/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.js
new file mode 100644
index 0000000000..7e23a0b89d
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.js
@@ -0,0 +1,399 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const {encodeABTermValue, getModelQuery} = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm");
+
+var addressbook = 0;
+var composeWindow = 0;
+var msgCompFields = 0;
+var editCardCallback = 0;
+
+var gSearchInput;
+var gSearchTimer = null;
+var gQueryURIFormat = null;
+
+// localization strings
+var prefixTo;
+var prefixCc;
+var prefixBcc;
+
+var gToButton;
+var gCcButton;
+var gBccButton;
+
+var gActivatedButton;
+
+var gDragService = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService);
+
+var gSelectAddressesAbViewListener = {
+ onSelectionChanged: function() {
+ ResultsPaneSelectionChanged();
+ },
+ onCountChanged: function(total) {
+ // do nothing
+ }
+};
+
+function GetAbViewListener()
+{
+ return gSelectAddressesAbViewListener;
+}
+
+function OnLoadSelectAddress()
+{
+ InitCommonJS();
+
+ prefixTo = gAddressBookBundle.getString("prefixTo") + ": ";
+ prefixCc = gAddressBookBundle.getString("prefixCc") + ": ";
+ prefixBcc = gAddressBookBundle.getString("prefixBcc") + ": ";
+
+ UpgradeAddressBookResultsPaneUI("mailnews.ui.select_addresses_results.version");
+
+ var toAddress="", ccAddress="", bccAddress="";
+
+ // look in arguments[0] for parameters
+ if (window.arguments && window.arguments[0])
+ {
+ // keep parameters in global for later
+ if ( window.arguments[0].composeWindow )
+ top.composeWindow = window.arguments[0].composeWindow;
+ if ( window.arguments[0].msgCompFields )
+ top.msgCompFields = window.arguments[0].msgCompFields;
+ if ( window.arguments[0].toAddress )
+ toAddress = window.arguments[0].toAddress;
+ if ( window.arguments[0].ccAddress )
+ ccAddress = window.arguments[0].ccAddress;
+ if ( window.arguments[0].bccAddress )
+ bccAddress = window.arguments[0].bccAddress;
+
+ // put the addresses into the bucket
+ AddAddressFromComposeWindow(toAddress, prefixTo);
+ AddAddressFromComposeWindow(ccAddress, prefixCc);
+ AddAddressFromComposeWindow(bccAddress, prefixBcc);
+ }
+
+ gSearchInput = document.getElementById("searchInput");
+
+ // Reselect the persisted address book if possible, if not just select the
+ // first in the list.
+ var temp = abList.value;
+ abList.selectedItem = null;
+ abList.value = temp;
+ if (!abList.selectedItem)
+ abList.selectedIndex = 0;
+
+ ChangeDirectoryByURI(abList.value);
+
+ DialogBucketPaneSelectionChanged();
+
+ var workPhoneCol = document.getElementById("WorkPhone");
+ workPhoneCol.setAttribute("hidden", "true");
+
+ var companyCol = document.getElementById("Company");
+ companyCol.setAttribute("hidden", "true");
+
+ gToButton = document.getElementById("toButton");
+ gCcButton = document.getElementById("ccButton");
+ gBccButton = document.getElementById("bccButton");
+
+ gAbResultsTree.focus();
+
+ gActivatedButton = gToButton;
+
+ document.documentElement.addEventListener("keypress", OnReturnHit, true);
+}
+
+function OnUnloadSelectAddress()
+{
+ CloseAbView();
+}
+
+function AddAddressFromComposeWindow(addresses, prefix)
+{
+ if ( addresses )
+ {
+ var emails = {};
+ var names = {};
+ var fullNames = {};
+ var numAddresses = MailServices.headerParser.parseHeadersWithArray(addresses, emails, names, fullNames);
+
+ for ( var index = 0; index < numAddresses; index++ )
+ {
+ AddAddressIntoBucket(prefix, fullNames.value[index], emails.value[index]);
+ }
+ }
+}
+
+function SelectAddressOKButton()
+{
+ // Empty email checks are now done in AddAddressIntoBucket below.
+ var body = document.getElementById('bucketBody');
+ var item, row, cell, prefix, address, email;
+ var toAddress="", ccAddress="", bccAddress="", emptyEmail="";
+
+ for ( var index = 0; index < body.childNodes.length; index++ )
+ {
+ item = body.childNodes[index];
+ if ( item.childNodes && item.childNodes.length )
+ {
+ row = item.childNodes[0];
+ if ( row.childNodes && row.childNodes.length )
+ {
+ cell = row.childNodes[0];
+ prefix = cell.getAttribute('prefix');
+ address = cell.getAttribute('address');
+ email = cell.getAttribute('email');
+ if ( prefix )
+ {
+ switch ( prefix )
+ {
+ case prefixTo:
+ if ( toAddress )
+ toAddress += ", ";
+ toAddress += address;
+ break;
+ case prefixCc:
+ if ( ccAddress )
+ ccAddress += ", ";
+ ccAddress += address;
+ break;
+ case prefixBcc:
+ if ( bccAddress )
+ bccAddress += ", ";
+ bccAddress += address;
+ break;
+ }
+ }
+ }
+ }
+ }
+ // reset the UI in compose window
+ msgCompFields.to = toAddress;
+ msgCompFields.cc = ccAddress;
+ msgCompFields.bcc = bccAddress;
+ top.composeWindow.CompFields2Recipients(top.msgCompFields);
+
+ return true;
+}
+
+function SelectAddressToButton()
+{
+ AddSelectedAddressesIntoBucket(prefixTo);
+ gActivatedButton = gToButton;
+}
+
+function SelectAddressCcButton()
+{
+ AddSelectedAddressesIntoBucket(prefixCc);
+ gActivatedButton = gCcButton;
+}
+
+function SelectAddressBccButton()
+{
+ AddSelectedAddressesIntoBucket(prefixBcc);
+ gActivatedButton = gBccButton;
+}
+
+function AddSelectedAddressesIntoBucket(prefix)
+{
+ var cards = GetSelectedAbCards();
+ var count = cards.length;
+
+ for (var i = 0; i < count; i++) {
+ AddCardIntoBucket(prefix, cards[i]);
+ }
+}
+
+function AddCardIntoBucket(prefix, card)
+{
+ var address = GenerateAddressFromCard(card);
+ if (card.isMailList) {
+ AddAddressIntoBucket(prefix, address, card.displayName);
+ }
+ else {
+ AddAddressIntoBucket(prefix, address, card.primaryEmail);
+ }
+}
+
+function AddAddressIntoBucket(prefix, address, email)
+{
+ if (!email)
+ {
+ Services.prompt.alert(window,
+ gAddressBookBundle.getString("emptyEmailAddCardTitle"),
+ gAddressBookBundle.getString("emptyEmailAddCard"));
+ }
+ else
+ {
+ var body = document.getElementById("bucketBody");
+
+ var item = document.createElement('treeitem');
+ var row = document.createElement('treerow');
+ var cell = document.createElement('treecell');
+ cell.setAttribute('label', prefix + address);
+ cell.setAttribute('prefix', prefix);
+ cell.setAttribute('address', address);
+ cell.setAttribute('email', email);
+
+ row.appendChild(cell);
+ item.appendChild(row);
+ body.appendChild(item);
+ }
+}
+
+function RemoveSelectedFromBucket()
+{
+ var bucketTree = document.getElementById("addressBucket");
+ if ( bucketTree )
+ {
+ var body = document.getElementById("bucketBody");
+ var selection = bucketTree.view.selection;
+ var rangeCount = selection.getRangeCount();
+
+ for (var i = rangeCount-1; i >= 0; --i)
+ {
+ var start = {}, end = {};
+ selection.getRangeAt(i,start,end);
+ for (var j = end.value; j >= start.value; --j)
+ {
+ bucketTree.contentView.getItemAtIndex(j).remove();
+ }
+ }
+ }
+}
+
+/* Function: ResultsPaneSelectionChanged()
+ * Callers : OnLoadSelectAddress(), abCommon.js:ResultsPaneSelectionChanged()
+ * -------------------------------------------------------------------------
+ * This function is used to grab the selection state of the results tree to maintain
+ * the appropriate enabled/disabled states of the "Edit", "To:", "CC:", and "Bcc:" buttons.
+ * If an entry is selected in the results Tree, then the "disabled" attribute is removed.
+ * Otherwise, if nothing is selected, "disabled" is set to true.
+ */
+
+function ResultsPaneSelectionChanged()
+{;
+ var editButton = document.getElementById("edit");
+ var toButton = document.getElementById("toButton");
+ var ccButton = document.getElementById("ccButton");
+ var bccButton = document.getElementById("bccButton");
+
+ var numSelected = GetNumSelectedCards();
+ if (numSelected > 0)
+ {
+ if (numSelected == 1)
+ editButton.removeAttribute("disabled");
+ else
+ editButton.setAttribute("disabled", "true");
+
+ toButton.removeAttribute("disabled");
+ ccButton.removeAttribute("disabled");
+ bccButton.removeAttribute("disabled");
+ }
+ else
+ {
+ editButton.setAttribute("disabled", "true");
+ toButton.setAttribute("disabled", "true");
+ ccButton.setAttribute("disabled", "true");
+ bccButton.setAttribute("disabled", "true");
+ }
+}
+
+/* Function: DialogBucketPaneSelectionChanged()
+ * Callers : OnLoadSelectAddress(), abSelectAddressesDialog.xul:id="addressBucket"
+ * -------------------------------------------------------------------------------
+ * This function is used to grab the selection state of the bucket tree to maintain
+ * the appropriate enabled/disabled states of the "Remove" button.
+ * If an entry is selected in the bucket Tree, then the "disabled" attribute is removed.
+ * Otherwise, if nothing is selected, "disabled" is set to true.
+ */
+
+function DialogBucketPaneSelectionChanged()
+{
+ var bucketTree = document.getElementById("addressBucket");
+ var removeButton = document.getElementById("remove");
+
+ removeButton.disabled = bucketTree.view.selection.count == 0;
+}
+
+function AbResultsPaneDoubleClick(card)
+{
+ AddCardIntoBucket(prefixTo, card);
+}
+
+function OnClickedCard(card)
+{
+ // in the select address dialog, do nothing on click
+}
+
+function UpdateCardView()
+{
+ // in the select address dialog, do nothing
+}
+
+function DropRecipient(address)
+{
+ AddAddressFromComposeWindow(address, prefixTo);
+}
+
+function OnReturnHit(event)
+{
+ if (event.keyCode == 13) {
+ var focusedElement = document.commandDispatcher.focusedElement;
+ if (focusedElement && (focusedElement.id == "addressBucket"))
+ return;
+ event.stopPropagation();
+ if (focusedElement && (focusedElement.id == "abResultsTree"))
+ gActivatedButton.doCommand();
+ }
+}
+
+
+function onEnterInSearchBar()
+{
+ var selectedNode = abList.selectedItem;
+
+ if (!selectedNode)
+ return;
+
+ if (!gQueryURIFormat) {
+ // Get model query from pref. We don't want the query starting with "?"
+ // as we have to prefix "?and" to this format.
+ gQueryURIFormat = getModelQuery("mail.addr_book.quicksearchquery.format");
+ }
+
+ var searchURI = selectedNode.value;
+
+ // Use helper method to split up search query to multi-word search
+ // query against multiple fields.
+ let searchWords = getSearchTokens(gSearchInput.value);
+ searchURI += generateQueryURI(gQueryURIFormat, searchWords);
+
+ SetAbView(searchURI);
+
+ SelectFirstCard();
+}
+
+function DirPaneSelectionChangeMenulist()
+{
+ if (abList && abList.selectedItem) {
+ if (gSearchInput.value && (gSearchInput.value != ""))
+ onEnterInSearchBar();
+ else
+ ChangeDirectoryByURI(abList.value);
+ }
+
+ // Hide the addressbook column if the selected addressbook isn't
+ // "All address books". Since the column is redundant in all other cases.
+ let addrbookColumn = document.getElementById("addrbook");
+ if (abList.value.startsWith(kAllDirectoryRoot + "?")) {
+ addrbookColumn.hidden = !gShowAbColumnInComposeSidebar;
+ addrbookColumn.removeAttribute("ignoreincolumnpicker");
+ } else {
+ addrbookColumn.hidden = true;
+ addrbookColumn.setAttribute("ignoreincolumnpicker", "true");
+ }
+}
diff --git a/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.xul b/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.xul
new file mode 100644
index 0000000000..067139e7ec
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.xul
@@ -0,0 +1,92 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/selectAddressesDialog.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/addressbook/abResultsPaneOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % abSelectAddressesDialogDTD SYSTEM "chrome://messenger/locale/addressbook/abSelectAddressesDialog.dtd" >
+%abSelectAddressesDialogDTD;
+<!ENTITY % abResultsPaneOverlayDTD SYSTEM "chrome://messenger/locale/addressbook/abResultsPaneOverlay.dtd" >
+%abResultsPaneOverlayDTD;
+]>
+
+<dialog id="selectAddressesWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&selectAddressWindow.title;"
+ style="width: 50em; height: 35em;"
+ persist="width height screenX screenY"
+ buttons="accept,cancel"
+ ondialogaccept="return SelectAddressOKButton();"
+ onload="OnLoadSelectAddress();"
+ onunload="OnUnloadSelectAddress();">
+
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+ <stringbundle id="bundle_composeMsgs" src="chrome://messenger/locale/messengercompose/composeMsgs.properties"/>
+
+ <script src="chrome://messenger/content/addressbook/abCommon.js"/>
+ <script src="chrome://messenger/content/addressbook/abSelectAddressesDialog.js"/>
+ <script src="chrome://messenger/content/addressbook/abDragDrop.js"/>
+ <script src="chrome://messenger/content/messengercompose/MsgComposeCommands.js"/>
+ <script src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"/>
+ <script src="chrome://global/content/globalOverlay.js"/>
+
+ <vbox flex="1">
+
+ <hbox id="topBox" align="center">
+ <label value="&lookIn.label;" accesskey="&lookIn.accesskey;" control="addressbookList"/>
+ <menulist id="addressbookList"
+ oncommand="DirPaneSelectionChangeMenulist(); document.commandDispatcher.updateCommands('addrbook-select');">
+ <menupopup id="addressbookList-menupopup" class="addrbooksPopup"/>
+ </menulist>
+ <label value="&for.label;" accesskey="&for.accesskey;" control="searchInput"/>
+ <textbox id="searchInput" flex="1" type="search"
+ aria-controls="abResultsTree"
+ placeholder="&for.placeholder;"
+ oncommand="onEnterInSearchBar();" clickSelectsAll="true"/>
+ </hbox>
+
+ <hbox flex="1">
+
+ <vbox id="resultsBox" flex="4">
+ <tree id="abResultsTree" flex="1" persist="height"/>
+ </vbox>
+
+ <vbox id="addToBucketButtonBox">
+ <spacer flex="1"/>
+ <button id="toButton" label="&toButton.label;" accesskey="&toButton.accesskey;" oncommand="SelectAddressToButton()"/>
+ <spacer class="middle-button-spacer"/>
+ <button id="ccButton" label="&ccButton.label;" accesskey="&ccButton.accesskey;" oncommand="SelectAddressCcButton()"/>
+ <spacer class="middle-button-spacer"/>
+ <button id="bccButton" label="&bccButton.label;" accesskey="&bccButton.accesskey;" oncommand="SelectAddressBccButton()"/>
+ <spacer class="above-remove-spacer"/>
+ <button id="remove" label="&removeButton.label;" accesskey="&removeButton.accesskey;" class="dialog" oncommand="RemoveSelectedFromBucket()"/>
+ <spacer flex="1"/>
+ </vbox>
+
+ <vbox id="bucketBox" flex="1">
+ <label value="&addressMessageTo.label;" control="addressBucket"/>
+ <tree id="addressBucket" flex="1" hidecolumnpicker="true"
+ ondragover="DragAddressOverTargetControl(event);"
+ ondrop="DropOnAddressingTarget(event, false);"
+ onselect="DialogBucketPaneSelectionChanged();">
+ <treecols>
+ <treecol id="addressCol" flex="1" hideheader="true"/>
+ </treecols>
+ <treechildren id="bucketBody" flex="1"/>
+ </tree>
+ </vbox>
+
+ </hbox>
+
+ <hbox id="newEditButtonBox">
+ <button id="new" label="&newButton.label;" accesskey="&newButton.accesskey;" tooltiptext="&addressPickerNewButton.tooltip;" oncommand="AbNewCard();"/>
+ <button id="edit" label="&editButton.label;" accesskey="&editButton.accesskey;" tooltiptext="&addressPickerEditButton.tooltip;" oncommand="AbEditSelectedCard()"/>
+ </hbox>
+ </vbox>
+
+</dialog>
diff --git a/comm/suite/mailnews/components/addrbook/content/abTrees.js b/comm/suite/mailnews/components/addrbook/content/abTrees.js
new file mode 100644
index 0000000000..ac52d3464d
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/abTrees.js
@@ -0,0 +1,332 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/**
+ * This file contains our implementation for various addressbook trees. It
+ * depends on jsTreeView.js being loaded before this script is loaded.
+ */
+
+const {IOUtils} = ChromeUtils.import("resource:///modules/IOUtils.js");
+
+// Tree Sort helper methods.
+var AB_ORDER = ["aab", "pab", "mork", "ldap", "mapi+other", "anyab", "cab"];
+
+function getDirectoryValue(aDir, aKey) {
+ if (aKey == "ab_type") {
+ if (aDir._directory.URI == kAllDirectoryRoot + "?")
+ return "aab";
+ if (aDir._directory.URI == kPersonalAddressbookURI)
+ return "pab";
+ if (aDir._directory.URI == kCollectedAddressbookURI)
+ return "cab";
+ if (aDir._directory instanceof Ci.nsIAbMDBDirectory)
+ return "mork";
+ if (aDir._directory instanceof Ci.nsIAbLDAPDirectory)
+ return "ldap";
+
+ // If there is any other AB type.
+ return "mapi+other";
+ } else if (aKey == "ab_name") {
+ return aDir._directory.dirName;
+ }
+
+ // This should never happen.
+ return null;
+}
+
+function abNameCompare(a, b) {
+ return a.localeCompare(b);
+}
+
+function abTypeCompare(a, b) {
+ return (AB_ORDER.indexOf(a) - AB_ORDER.indexOf(b));
+}
+
+var SORT_PRIORITY = ["ab_type", "ab_name"];
+var SORT_FUNCS = [abTypeCompare, abNameCompare];
+
+function abSort(a, b) {
+ for (let i = 0; i < SORT_FUNCS.length; i++) {
+ let sortBy = SORT_PRIORITY[i];
+ let aValue = getDirectoryValue(a, sortBy);
+ let bValue = getDirectoryValue(b, sortBy);
+
+ if (!aValue && !bValue)
+ return 0;
+ if (!aValue)
+ return -1;
+ if (!bValue)
+ return 1;
+ if (aValue != bValue) {
+ let result = SORT_FUNCS[i](aValue, bValue);
+
+ if (result != 0)
+ return result;
+ }
+ }
+ return 0;
+}
+
+/**
+ * Each abDirTreeItem corresponds to one row in the tree view.
+ */
+function abDirTreeItem(aDirectory)
+{
+ this._directory = aDirectory;
+}
+
+abDirTreeItem.prototype =
+{
+ getText: function atv_getText()
+ {
+ return this._directory.dirName;
+ },
+
+ get id()
+ {
+ return this._directory.URI;
+ },
+
+ _open: false,
+ get open()
+ {
+ return this._open;
+ },
+
+ _level: 0,
+ get level()
+ {
+ return this._level;
+ },
+
+ _children: null,
+ get children()
+ {
+ if (!this._children)
+ {
+ this._children = [];
+ let myEnum;
+ if (this._directory.URI == (kAllDirectoryRoot + "?"))
+ myEnum = MailServices.ab.directories;
+ else
+ myEnum = this._directory.childNodes;
+
+ while (myEnum.hasMoreElements())
+ {
+ var abItem = new abDirTreeItem(
+ myEnum.getNext().QueryInterface(Ci.nsIAbDirectory));
+ if (gDirectoryTreeView&&
+ this.id == kAllDirectoryRoot + "?" &&
+ getDirectoryValue(abItem, "ab_type") == "ldap")
+ gDirectoryTreeView.hasRemoteAB = true;
+
+ abItem._level = this._level + 1;
+ abItem._parent = this;
+ this._children.push(abItem);
+ }
+
+ this._children.sort(abSort);
+ }
+ return this._children;
+ },
+
+ getProperties: function atv_getProps()
+ {
+ var properties = []
+ if (this._directory.isMailList)
+ properties.push("IsMailList-true");
+ if (this._directory.isRemote)
+ properties.push("IsRemote-true");
+ if (this._directory.isSecure)
+ properties.push("IsSecure-true");
+ return properties.join(" ");
+ }
+};
+
+/**
+ * Our actual implementation of nsITreeView.
+ */
+function directoryTreeView() {}
+directoryTreeView.prototype =
+{
+ __proto__: new PROTO_TREE_VIEW(),
+
+ hasRemoteAB: false,
+
+ init: function dtv_init(aTree, aJSONFile)
+ {
+ if (aJSONFile) {
+ // Parse our persistent-open-state json file
+ let data = IOUtils.loadFileToString(aJSONFile);
+ if (data) {
+ this._persistOpenMap = JSON.parse(data);
+ }
+ }
+
+ this._rebuild();
+ aTree.view = this;
+ },
+
+ shutdown: function dtv_shutdown(aJSONFile)
+ {
+ // Write out the persistOpenMap to our JSON file.
+ if (aJSONFile)
+ {
+ // Write out our json file...
+ let data = JSON.stringify(this._persistOpenMap);
+ IOUtils.saveStringToFile(aJSONFile, data);
+ }
+ },
+
+ // Override the dnd methods for those functions in abDragDrop.js
+ canDrop: function dtv_canDrop(aIndex, aOrientation, dataTransfer)
+ {
+ return abDirTreeObserver.canDrop(aIndex, aOrientation, dataTransfer);
+ },
+
+ drop: function dtv_drop(aRow, aOrientation, dataTransfer)
+ {
+ abDirTreeObserver.onDrop(aRow, aOrientation, dataTransfer);
+ },
+
+ getDirectoryAtIndex: function dtv_getDirForIndex(aIndex)
+ {
+ return this._rowMap[aIndex]._directory;
+ },
+
+ getIndexOfDirectory: function dtv_getIndexOfDir(aItem)
+ {
+ for (var i = 0; i < this._rowMap.length; i++)
+ if (this._rowMap[i]._directory == aItem)
+ return i;
+
+ return -1;
+ },
+
+ // Override jsTreeView's isContainer, since we want to be able
+ // to react to drag-drop events for all items in the directory
+ // tree.
+ isContainer: function dtv_isContainer(aIndex)
+ {
+ return true;
+ },
+
+ /**
+ * NOTE: This function will result in indeterminate rows being selected.
+ * Callers should take care to re-select a desired row after calling
+ * this function.
+ */
+ _rebuild: function dtv__rebuild() {
+ this._rowMap = [];
+
+ // Make an entry for All Address Books.
+ let rootAB = MailServices.ab.getDirectory(kAllDirectoryRoot + "?");
+ rootAB.dirName = gAddressBookBundle.getString("allAddressBooks");
+ this._rowMap.push(new abDirTreeItem(rootAB));
+
+ // Sort our addressbooks now.
+ this._rowMap.sort(abSort);
+
+ this._restoreOpenStates();
+ },
+
+ getIndexForId: function(aId) {
+ for (let i = 0; i < this._rowMap.length; i++) {
+ if (this._rowMap[i].id == aId)
+ return i;
+ }
+
+ return -1;
+ },
+
+ // nsIAbListener interfaces
+ onItemAdded: function dtv_onItemAdded(aParent, aItem)
+ {
+ if (!(aItem instanceof Ci.nsIAbDirectory))
+ return;
+
+ var oldCount = this._rowMap.length;
+ var tree = this._tree;
+ this._tree = null;
+ this._rebuild();
+ if (!tree)
+ return;
+
+ this._tree = tree;
+ var itemIndex = this.getIndexOfDirectory(aItem);
+ tree.rowCountChanged(itemIndex, this._rowMap.length - oldCount);
+ var parentIndex = this.getIndexOfDirectory(aParent);
+ if (parentIndex > -1)
+ tree.invalidateRow(parentIndex);
+ },
+
+ onItemRemoved: function dtv_onItemRemoved(aParent, aItem)
+ {
+ if (!(aItem instanceof Ci.nsIAbDirectory))
+ return;
+
+ var itemIndex = this.getIndexOfDirectory(aItem);
+ var oldCount = this._rowMap.length;
+ var tree = this._tree;
+ this._tree = null;
+ this._rebuild();
+ if (!tree)
+ return;
+
+ this._tree = tree;
+ tree.rowCountChanged(itemIndex, this._rowMap.length - oldCount);
+
+ // This does not currently work, see Bug 1323563.
+ // If we're deleting a top-level address-book, just select the first book.
+ // if (aParent.URI == kAllDirectoryRoot ||
+ // aParent.URI == kAllDirectoryRoot + "?") {
+ // this.selection.select(0);
+ // return;
+ // }
+
+ var parentIndex = this.getIndexOfDirectory(aParent);
+ if (parentIndex > -1)
+ tree.invalidateRow(parentIndex);
+
+ if (!this.selection.count)
+ {
+ // The previously selected item was a member of the deleted subtree.
+ // Select the parent of the subtree.
+ // If there is no parent, select the next item.
+ // If there is no next item, select the first item.
+ var newIndex = parentIndex;
+ if (newIndex < 0)
+ newIndex = itemIndex;
+ if (newIndex >= this._rowMap.length)
+ newIndex = 0;
+
+ this.selection.select(newIndex);
+ }
+ },
+
+ onItemPropertyChanged: function dtv_onItemProp(aItem, aProp, aOld, aNew)
+ {
+ if (!(aItem instanceof Ci.nsIAbDirectory))
+ return;
+
+ var index = this.getIndexOfDirectory(aItem);
+ var current = this.getDirectoryAtIndex(this.selection.currentIndex);
+ var tree = this._tree;
+ this._tree = null;
+ this._rebuild();
+ this._tree = tree;
+ this.selection.select(this.getIndexOfDirectory(current));
+
+ if (index > -1) {
+ var newIndex = this.getIndexOfDirectory(aItem);
+ if (newIndex >= index)
+ this._tree.invalidateRange(index, newIndex);
+ else
+ this._tree.invalidateRange(newIndex, index);
+ }
+ }
+};
+
+var gDirectoryTreeView = new directoryTreeView();
diff --git a/comm/suite/mailnews/components/addrbook/content/addressbook-panel.js b/comm/suite/mailnews/components/addrbook/content/addressbook-panel.js
new file mode 100644
index 0000000000..9fa0a125e0
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/addressbook-panel.js
@@ -0,0 +1,119 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var gIsMsgCompose = false;
+
+function GetAbViewListener()
+{
+ // the ab panel doesn't care if the total changes, or if the selection changes
+ return null;
+}
+
+var mutationObs = null;
+
+function AbPanelLoad()
+{
+ InitCommonJS();
+
+ UpgradeAddressBookResultsPaneUI("mailnews.ui.addressbook_panel_results.version");
+
+ var abPopup = document.getElementById('addressbookList');
+
+ // Reselect the persisted address book if possible, if not just select the
+ // first in the list.
+ var temp = abPopup.value;
+ abPopup.selectedItem = null;
+ abPopup.value = temp;
+ if (!abPopup.selectedItem)
+ abPopup.selectedIndex = 0;
+
+ ChangeDirectoryByURI(abPopup.value);
+
+ mutationObs = new MutationObserver(function(aMutations) {
+ aMutations.forEach(function(mutation) {
+ if (getSelectedDirectoryURI() == (kAllDirectoryRoot + "?") &&
+ mutation.type == "attributes" &&
+ mutation.attributeName == "hidden") {
+ let curState = document.getElementById("addrbook").hidden;
+ gShowAbColumnInComposeSidebar = !curState;
+ }
+ });
+ });
+
+ document.getElementById("addrbook").hidden = !gShowAbColumnInComposeSidebar;
+
+ mutationObs.observe(document.getElementById("addrbook"),
+ { attributes: true, childList: true });
+
+ gSearchInput = document.getElementById("searchInput");
+
+ // for the compose window we want to show To, Cc, Bcc and a separator
+ // for all other windows we want to show Compose Mail To
+ var popup = document.getElementById("composeMail");
+ gIsMsgCompose = parent.document
+ .documentElement
+ .getAttribute("windowtype") == "msgcompose";
+ for (var i = 0; i < 4; i++)
+ popup.childNodes[i].hidden = !gIsMsgCompose;
+ popup.childNodes[4].hidden = gIsMsgCompose;
+}
+
+function AbPanelUnload()
+{
+ mutationObs.disconnect();
+
+ CloseAbView();
+}
+
+function AbPanelAdd(addrtype)
+{
+ var cards = GetSelectedAbCards();
+ var count = cards.length;
+
+ for (var i = 0; i < count; i++) {
+ // turn each card into a properly formatted address
+ var address = GenerateAddressFromCard(cards[i]);
+ if (address)
+ top.awAddRecipient(addrtype, address);
+ else
+ Services.prompt.alert(window,
+ gAddressBookBundle.getString("emptyEmailAddCardTitle"),
+ gAddressBookBundle.getString("emptyEmailAddCard"));
+ }
+}
+
+function AbPanelNewCard()
+{
+ goNewCardDialog(abList.value);
+}
+
+function AbPanelNewList()
+{
+ goNewListDialog(abList.value);
+}
+
+function ResultsPaneSelectionChanged()
+{
+ // do nothing for ab panel
+}
+
+function OnClickedCard()
+{
+ // do nothing for ab panel
+}
+
+function AbResultsPaneDoubleClick(card)
+{
+ // double click for ab panel means "send mail to this person / list"
+ if (gIsMsgCompose)
+ AbPanelAdd('addr_to');
+ else
+ AbNewMessage();
+}
+
+function UpdateCardView()
+{
+ // do nothing for ab panel
+}
diff --git a/comm/suite/mailnews/components/addrbook/content/addressbook-panel.xul b/comm/suite/mailnews/components/addrbook/content/addressbook-panel.xul
new file mode 100644
index 0000000000..72f650ffdb
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/addressbook-panel.xul
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: xml; indent-tabs-mode: nil; -*-
+ - 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/sidebarPanel.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/addressPanes.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/abResultsPane.css" type="text/css"?>
+
+<!DOCTYPE page [
+<!ENTITY % abSelectAddressesDialogDTD SYSTEM "chrome://messenger/locale/addressbook/abSelectAddressesDialog.dtd" >
+%abSelectAddressesDialogDTD;
+<!ENTITY % abResultsPaneOverlayDTD SYSTEM "chrome://messenger/locale/addressbook/abResultsPaneOverlay.dtd" >
+%abResultsPaneOverlayDTD;
+]>
+
+<page id="addressbook-panel"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="AbPanelLoad();"
+ onunload="AbPanelUnload();"
+ title="&selectAddressWindow.title;"
+ selectedaddresses="true">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+ </stringbundleset>
+
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://global/content/nsDragAndDrop.js"/>
+ <script src="chrome://messenger/content/addressbook/addressbook.js"/>
+ <script src="chrome://messenger/content/addressbook/abCommon.js"/>
+ <script src="chrome://messenger/content/addressbook/abDragDrop.js"/>
+ <script src="chrome://messenger/content/addressbook/abResultsPane.js"/>
+ <script src="chrome://messenger/content/addressbook/abSelectAddressesDialog.js"/>
+ <script src="chrome://messenger/content/addressbook/addressbook-panel.js"/>
+ <script src="chrome://communicator/content/utilityOverlay.js"/>
+
+ <commandset id="addressbook-panel-commandset">
+ <command id="cmd_newlist" oncommand="AbNewList();"/>
+ <command id="cmd_properties" oncommand="goDoCommand('cmd_properties');"/>
+ </commandset>
+
+ <menupopup id="composeMail" onpopupshowing="CommandUpdate_AddressBook();">
+ <menuitem label="&toButton.label;" accesskey="&toButton.accesskey;" oncommand="AbPanelAdd('addr_to');" default="true"/>
+ <menuitem label="&ccButton.label;" accesskey="&ccButton.accesskey;" oncommand="AbPanelAdd('addr_cc');"/>
+ <menuitem label="&bccButton.label;" accesskey="&bccButton.accesskey;" oncommand="AbPanelAdd('addr_bcc');"/>
+ <menuseparator/>
+ <menuitem label="&composeEmail.label;" accesskey="&composeEmail.accesskey;" oncommand="AbNewMessage();" default="true"/>
+ <menuitem label="&copyAddress.label;" accesskey="&copyAddress.accesskey;" oncommand="AbCopyAddress();"/>
+ <menuitem label="&deleteAddrBookCard.label;" accesskey="&deleteAddrBookCard.accesskey;" oncommand="AbDelete();"/>
+ <menuseparator/>
+ <menuitem label="&newAddrBookCard.label;" accesskey="&newAddrBookCard.accesskey;" oncommand="AbPanelNewCard();"/>
+ <menuitem label="&newAddrBookMailingList.label;" accesskey="&newAddrBookMailingList.accesskey;" command="cmd_newlist"/>
+ <menuseparator/>
+ <menuitem label="&addrBookCardProperties.label;" accesskey="&addrBookCardProperties.accesskey;" command="cmd_properties"/>
+ </menupopup>
+ <vbox id="results_box" flex="1">
+ <hbox id="panel-bar" class="toolbar" align="center">
+ <label value="&lookIn.label;" control="addressbookList" id="lookInLabel"/>
+ <menulist id="addressbookList"
+ oncommand="DirPaneSelectionChangeMenulist();" flex="1"
+ persist="value">
+ <menupopup id="addressbookList-menupopup" class="addrbooksPopup"/>
+ </menulist>
+ </hbox>
+ <hbox align="center">
+ <label value="&for.label;" id="forLabel" control="searchInput"/>
+ <textbox id="searchInput" flex="1" type="search"
+ aria-labelledby="lookInLabel addressbookList forLabel"
+ aria-controls="abResultsTree"
+ placeholder="&for.placeholder;"
+ oncommand="onEnterInSearchBar();" clickSelectsAll="true"/>
+ </hbox>
+
+ <tree id="abResultsTree" flex="1" context="composeMail" onclick="AbResultsPaneOnClick(event);" class="plain"
+ sortCol="GeneratedName" persist="sortCol">
+ <treecols>
+ <!-- these column ids must match up to the mork column names, see nsIAddrDatabase.idl -->
+ <treecol id="GeneratedName"
+ persist="hidden ordinal width sortDirection" flex="1" label="&GeneratedName.label;" primary="true"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="addrbook"
+ persist="hidden ordinal width sortDirection" hidden="true"
+ flex="1" label="&Addrbook.label;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="PrimaryEmail"
+ persist="hidden ordinal width sortDirection"
+ hiddenbydefault="true"
+ flex="1" label="&PrimaryEmail.label;"/>
+ </treecols>
+ <treechildren ondragstart="nsDragAndDrop.startDrag(event, abResultsPaneObserver);"/>
+</tree>
+
+ </vbox>
+</page>
diff --git a/comm/suite/mailnews/components/addrbook/content/addressbook.js b/comm/suite/mailnews/components/addrbook/content/addressbook.js
new file mode 100644
index 0000000000..e1bce6a868
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/addressbook.js
@@ -0,0 +1,581 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const {encodeABTermValue, getModelQuery} = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm");
+const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const nsIAbListener = Ci.nsIAbListener;
+const kPrefMailAddrBookLastNameFirst = "mail.addr_book.lastnamefirst";
+const kPersistCollapseMapStorage = "directoryTree.json";
+
+var gSearchTimer = null;
+var gStatusText = null;
+var gQueryURIFormat = null;
+var gSearchInput;
+var gSearchBox;
+var gCardViewBox;
+var gCardViewBoxEmail1;
+
+var msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"]
+ .createInstance(Ci.nsIMsgWindow);
+
+// Constants that correspond to choices
+// in Address Book->View -->Show Name as
+const kDisplayName = 0;
+const kLastNameFirst = 1;
+const kFirstNameFirst = 2;
+const kLDAPDirectory = 0; // defined in nsDirPrefs.h
+const kPABDirectory = 2; // defined in nsDirPrefs.h
+
+function OnUnloadAddressBook()
+{
+ MailServices.ab.removeAddressBookListener(gDirectoryTreeView);
+
+ // Shutdown the tree view - this will also save the open/collapsed
+ // state of the tree view to a JSON file.
+ gDirectoryTreeView.shutdown(kPersistCollapseMapStorage);
+
+ MailServices.mailSession.RemoveMsgWindow(msgWindow);
+
+ CloseAbView();
+}
+
+var gAddressBookAbViewListener = {
+ onSelectionChanged: function() {
+ ResultsPaneSelectionChanged();
+ },
+ onCountChanged: function(total) {
+ SetStatusText(total);
+ }
+};
+
+function GetAbViewListener()
+{
+ return gAddressBookAbViewListener;
+}
+
+function OnLoadAddressBook()
+{
+ gSearchInput = document.getElementById("searchInput");
+
+ verifyAccounts(null, false); // this will do migration, if we need to.
+
+ InitCommonJS();
+
+ UpgradeAddressBookResultsPaneUI("mailnews.ui.addressbook_results.version");
+
+ GetCurrentPrefs();
+
+ // FIX ME - later we will be able to use onload from the overlay
+ OnLoadCardView();
+
+ // Before and after callbacks for the customizeToolbar code
+ var abToolbox = getAbToolbox();
+ abToolbox.customizeInit = AbToolboxCustomizeInit;
+ abToolbox.customizeDone = AbToolboxCustomizeDone;
+ abToolbox.customizeChange = AbToolboxCustomizeChange;
+
+ // Initialize the Address Book tree view
+ gDirectoryTreeView.init(gDirTree, kPersistCollapseMapStorage);
+
+ SelectFirstAddressBook();
+
+ // if the pref is locked disable the menuitem New->LDAP directory
+ if (Services.prefs.prefIsLocked("ldap_2.disable_button_add"))
+ document.getElementById("addLDAP").setAttribute("disabled", "true");
+
+ // Add a listener, so we can switch directories if the current directory is
+ // deleted. This listener cares when a directory (= address book), or a
+ // directory item is/are removed. In the case of directory items, we are
+ // only really interested in mailing list changes and not cards but we have
+ // to have both.
+ MailServices.ab.addAddressBookListener(gDirectoryTreeView, nsIAbListener.all);
+
+ gDirTree.controllers.appendController(DirPaneController);
+
+ // Ensure we don't load xul error pages into the main window
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .useErrorPages = false;
+
+ MailServices.mailSession.AddMsgWindow(msgWindow);
+}
+
+function GetCurrentPrefs()
+{
+ // check "Show Name As" menu item based on pref
+ var menuitemID;
+ switch (Services.prefs.getIntPref(kPrefMailAddrBookLastNameFirst))
+ {
+ case kFirstNameFirst:
+ menuitemID = 'firstLastCmd';
+ break;
+ case kLastNameFirst:
+ menuitemID = 'lastFirstCmd';
+ break;
+ case kDisplayName:
+ default:
+ menuitemID = 'displayNameCmd';
+ break;
+ }
+
+ var menuitem = top.document.getElementById(menuitemID);
+ if ( menuitem )
+ menuitem.setAttribute('checked', 'true');
+
+ // show phonetic fields if indicated by the pref
+ if (GetLocalizedStringPref("mail.addr_book.show_phonetic_fields") == "true")
+ document.getElementById("cmd_SortBy_PhoneticName")
+ .setAttribute("hidden", "false");
+}
+
+function SetNameColumn(cmd)
+{
+ var prefValue;
+
+ switch (cmd)
+ {
+ case 'firstLastCmd':
+ prefValue = kFirstNameFirst;
+ break;
+ case 'lastFirstCmd':
+ prefValue = kLastNameFirst;
+ break;
+ case 'displayNameCmd':
+ prefValue = kDisplayName;
+ break;
+ }
+
+ Services.prefs.setIntPref(kPrefMailAddrBookLastNameFirst, prefValue);
+}
+
+function CommandUpdate_AddressBook()
+{
+ goUpdateCommand('cmd_delete');
+ goUpdateCommand('button_delete');
+ goUpdateCommand('cmd_printcardpreview');
+ goUpdateCommand('cmd_printcard');
+ goUpdateCommand('cmd_print');
+ goUpdateCommand('cmd_properties');
+ goUpdateCommand('cmd_newlist');
+}
+
+function ResultsPaneSelectionChanged()
+{
+ UpdateCardView();
+}
+
+function UpdateCardView()
+{
+ var cards = GetSelectedAbCards();
+
+ // display the selected card, if exactly one card is selected.
+ // either no cards, or more than one card is selected, clear the pane.
+ if (cards.length == 1)
+ OnClickedCard(cards[0])
+ else
+ ClearCardViewPane();
+}
+
+function OnClickedCard(card)
+{
+ if (card)
+ DisplayCardViewPane(card);
+ else
+ ClearCardViewPane();
+}
+
+function AbClose()
+{
+ top.close();
+}
+
+function AbPrintCardInternal(doPrintPreview, msgType)
+{
+ var selectedItems = GetSelectedAbCards();
+ var numSelected = selectedItems.length;
+
+ if (!numSelected)
+ return;
+
+ let statusFeedback;
+ statusFeedback = Cc["@mozilla.org/messenger/statusfeedback;1"].createInstance();
+ statusFeedback = statusFeedback.QueryInterface(Ci.nsIMsgStatusFeedback);
+
+ let selectionArray = [];
+
+ for (let i = 0; i < numSelected; i++) {
+ let card = selectedItems[i];
+ let printCardUrl = CreatePrintCardUrl(card);
+ if (printCardUrl) {
+ selectionArray.push(printCardUrl);
+ }
+ }
+
+ printEngineWindow = window.openDialog("chrome://messenger/content/msgPrintEngine.xul",
+ "",
+ "chrome,dialog=no,all",
+ selectionArray.length, selectionArray,
+ statusFeedback, doPrintPreview, msgType);
+
+ return;
+}
+
+function AbPrintCard()
+{
+ AbPrintCardInternal(false, Ci.nsIMsgPrintEngine.MNAB_PRINT_AB_CARD);
+}
+
+function AbPrintPreviewCard()
+{
+ AbPrintCardInternal(true, Ci.nsIMsgPrintEngine.MNAB_PRINTPREVIEW_AB_CARD);
+}
+
+function CreatePrintCardUrl(card)
+{
+ return "data:application/xml;base64," + card.translateTo("base64xml");
+}
+
+function AbPrintAddressBookInternal(doPrintPreview, msgType)
+{
+ let uri = getSelectedDirectoryURI();
+ if (!uri)
+ return;
+
+ var statusFeedback;
+ statusFeedback = Cc["@mozilla.org/messenger/statusfeedback;1"].createInstance();
+ statusFeedback = statusFeedback.QueryInterface(Ci.nsIMsgStatusFeedback);
+
+ /*
+ turn "moz-abmdbdirectory://abook.mab" into
+ "addbook://moz-abmdbdirectory/abook.mab?action=print"
+ */
+
+ var abURIArr = uri.split("://");
+ var printUrl = "addbook://" + abURIArr[0] + "/" + abURIArr[1] + "?action=print"
+
+ printEngineWindow = window.openDialog("chrome://messenger/content/msgPrintEngine.xul",
+ "",
+ "chrome,dialog=no,all",
+ 1, [printUrl],
+ statusFeedback, doPrintPreview, msgType);
+
+ return;
+}
+
+function AbPrintAddressBook()
+{
+ AbPrintAddressBookInternal(false, Ci.nsIMsgPrintEngine.MNAB_PRINT_ADDRBOOK);
+}
+
+function AbPrintPreviewAddressBook()
+{
+ AbPrintAddressBookInternal(true, Ci.nsIMsgPrintEngine.MNAB_PRINTPREVIEW_ADDRBOOK);
+}
+
+/**
+ * Export the currently selected addressbook.
+ */
+function AbExportSelection() {
+ let selectedDirURI = getSelectedDirectoryURI();
+ if (!selectedDirURI)
+ return;
+
+ if (selectedDirURI == (kAllDirectoryRoot + "?"))
+ AbExportAll();
+ else
+ AbExport(selectedDirURI);
+}
+
+/**
+ * Export all found addressbooks, each in a separate file.
+ */
+function AbExportAll()
+{
+ let directories = MailServices.ab.directories;
+
+ while (directories.hasMoreElements()) {
+ let directory = directories.getNext();
+ // Do not export LDAP ABs.
+ if (directory.URI.startsWith(kLdapUrlPrefix))
+ continue;
+
+ AbExport(directory.URI);
+ }
+}
+
+/**
+ * Export the specified addressbook to a file.
+ *
+ * @param aSelectedDirURI The URI of the addressbook to export.
+ */
+function AbExport(aSelectedDirURI)
+{
+ if (!aSelectedDirURI)
+ return;
+
+ try {
+ let directory = GetDirectoryFromURI(aSelectedDirURI);
+ MailServices.ab.exportAddressBook(window, directory);
+ }
+ catch (ex) {
+ var message;
+ switch (ex.result) {
+ case Cr.NS_ERROR_FILE_ACCESS_DENIED:
+ message = gAddressBookBundle.getString("failedToExportMessageFileAccessDenied");
+ break;
+ case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE:
+ message = gAddressBookBundle.getString("failedToExportMessageNoDeviceSpace");
+ break;
+ default:
+ message = ex.message;
+ break;
+ }
+
+ Services.prompt.alert(window,
+ gAddressBookBundle.getString("failedToExportTitle"),
+ message);
+ }
+}
+
+function SetStatusText(total)
+{
+ if (!gStatusText)
+ gStatusText = document.getElementById('statusText');
+
+ try {
+ let statusText;
+
+ if (gSearchInput.value) {
+ if (total == 0) {
+ statusText = gAddressBookBundle.getString("noMatchFound");
+ } else {
+ statusText = PluralForm
+ .get(total, gAddressBookBundle.getString("matchesFound1"))
+ .replace("#1", total);
+ }
+ }
+ else
+ statusText =
+ gAddressBookBundle.getFormattedString(
+ "totalContactStatus",
+ [getSelectedDirectory().dirName, total]);
+
+ gStatusText.setAttribute("label", statusText);
+ }
+ catch(ex) {
+ dump("failed to set status text: " + ex + "\n");
+ }
+}
+
+function AbResultsPaneDoubleClick(card)
+{
+ AbEditCard(card);
+}
+
+function onAdvancedAbSearch()
+{
+ let selectedDirURI = getSelectedDirectoryURI();
+ if (!selectedDirURI)
+ return;
+
+ var existingSearchWindow = Services.wm.getMostRecentWindow("mailnews:absearch");
+ if (existingSearchWindow)
+ existingSearchWindow.focus();
+ else
+ window.openDialog("chrome://messenger/content/ABSearchDialog.xul", "",
+ "chrome,resizable,status,centerscreen,dialog=no",
+ {directory: selectedDirURI});
+}
+
+function onEnterInSearchBar()
+{
+ ClearCardViewPane();
+ if (!gQueryURIFormat) {
+ // Get model query from pref. We don't want the query starting with "?"
+ // as we have to prefix "?and" to this format.
+ gQueryURIFormat = getModelQuery("mail.addr_book.quicksearchquery.format");
+ }
+
+ let searchURI = getSelectedDirectoryURI();
+ if (!searchURI) return;
+
+ /*
+ XXX todo, handle the case where the LDAP url
+ already has a query, like
+ moz-abldapdirectory://nsdirectory.netscape.com:389/ou=People,dc=netscape,dc=com?(or(Department,=,Applications))
+ */
+ // Use helper method to split up search query to multi-word search
+ // query against multiple fields.
+ let searchWords = getSearchTokens(gSearchInput.value);
+ searchURI += generateQueryURI(gQueryURIFormat, searchWords);
+
+ if (searchURI == kAllDirectoryRoot)
+ searchURI += "?";
+
+ document.getElementById("localResultsOnlyMessage")
+ .setAttribute("hidden",
+ !gDirectoryTreeView.hasRemoteAB ||
+ searchURI != kAllDirectoryRoot + "?");
+
+ SetAbView(searchURI);
+
+ // XXX todo
+ // this works for synchronous searches of local addressbooks,
+ // but not for LDAP searches
+ SelectFirstCard();
+}
+
+function SwitchPaneFocus(event)
+{
+ var focusedElement = WhichPaneHasFocus();
+ var cardViewBox = GetCardViewBox();
+ var cardViewBoxEmail1 = GetCardViewBoxEmail1();
+ var searchBox = GetSearchBox();
+ var dirTree = GetDirTree();
+
+ if (event && event.shiftKey)
+ {
+ if (focusedElement == gAbResultsTree && searchBox.getAttribute('hidden') != 'true')
+ searchInput.focus();
+ else if ((focusedElement == gAbResultsTree || focusedElement == searchBox) && !IsDirPaneCollapsed())
+ dirTree.focus();
+ else if (focusedElement != cardViewBox && !IsCardViewAndAbResultsPaneSplitterCollapsed())
+ {
+ if(cardViewBoxEmail1)
+ cardViewBoxEmail1.focus();
+ else
+ cardViewBox.focus();
+ }
+ else
+ gAbResultsTree.focus();
+ }
+ else
+ {
+ if (focusedElement == searchBox)
+ gAbResultsTree.focus();
+ else if (focusedElement == gAbResultsTree && !IsCardViewAndAbResultsPaneSplitterCollapsed())
+ {
+ if(cardViewBoxEmail1)
+ cardViewBoxEmail1.focus();
+ else
+ cardViewBox.focus();
+ }
+ else if (focusedElement != dirTree && !IsDirPaneCollapsed())
+ dirTree.focus();
+ else if (searchBox.getAttribute('hidden') != 'true')
+ gSearchInput.focus();
+ else
+ gAbResultsTree.focus();
+ }
+}
+
+function WhichPaneHasFocus()
+{
+ var cardViewBox = GetCardViewBox();
+ var searchBox = GetSearchBox();
+ var dirTree = GetDirTree();
+
+ var currentNode = top.document.commandDispatcher.focusedElement;
+ while (currentNode)
+ {
+ var nodeId = currentNode.getAttribute('id');
+
+ if(currentNode == gAbResultsTree ||
+ currentNode == cardViewBox ||
+ currentNode == searchBox ||
+ currentNode == dirTree)
+ return currentNode;
+
+ currentNode = currentNode.parentNode;
+ }
+
+ return null;
+}
+
+function GetDirTree()
+{
+ if (!gDirTree)
+ gDirTree = document.getElementById('dirTree');
+ return gDirTree;
+}
+
+function GetSearchBox()
+{
+ if (!gSearchBox)
+ gSearchBox = document.getElementById('searchBox');
+ return gSearchBox;
+}
+
+function GetCardViewBox()
+{
+ if (!gCardViewBox)
+ gCardViewBox = document.getElementById('CardViewBox');
+ return gCardViewBox;
+}
+
+function GetCardViewBoxEmail1()
+{
+ if (!gCardViewBoxEmail1)
+ {
+ try {
+ gCardViewBoxEmail1 = document.getElementById('cvEmail1');
+ }
+ catch (ex) {
+ gCardViewBoxEmail1 = null;
+ }
+ }
+ return gCardViewBoxEmail1;
+}
+
+function IsDirPaneCollapsed()
+{
+ var dirPaneBox = GetDirTree().parentNode;
+ return dirPaneBox.getAttribute("collapsed") == "true" ||
+ dirPaneBox.getAttribute("hidden") == "true";
+}
+
+function IsCardViewAndAbResultsPaneSplitterCollapsed()
+{
+ var cardViewBox = document.getElementById('CardViewOuterBox');
+ try {
+ return (cardViewBox.getAttribute("collapsed") == "true");
+ }
+ catch (ex) {
+ return false;
+ }
+}
+
+function LaunchUrl(url)
+{
+ // Doesn't matter if this bit fails, window.location contains its own prompts
+ try {
+ window.location = url;
+ }
+ catch (ex) {}
+}
+
+function getAbToolbox()
+{
+ return document.getElementById("ab-toolbox");
+}
+
+function AbToolboxCustomizeInit()
+{
+ toolboxCustomizeInit("ab-menubar");
+}
+
+function AbToolboxCustomizeDone(aToolboxChanged)
+{
+ toolboxCustomizeDone("ab-menubar", getAbToolbox(), aToolboxChanged);
+}
+
+function AbToolboxCustomizeChange(event)
+{
+ toolboxCustomizeChange(getAbToolbox(), event);
+}
diff --git a/comm/suite/mailnews/components/addrbook/content/addressbook.xul b/comm/suite/mailnews/components/addrbook/content/addressbook.xul
new file mode 100644
index 0000000000..698fbe62c5
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/addressbook.xul
@@ -0,0 +1,722 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/addressbook.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/addressbook/addressPanes.css"
+ type="text/css"?>
+
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/contentAreaContextOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/addressbook/abResultsPaneOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % abMainWindowDTD SYSTEM "chrome://messenger/locale/addressbook/abMainWindow.dtd" >
+%abMainWindowDTD;
+<!ENTITY % abResultsPaneOverlayDTD SYSTEM "chrome://messenger/locale/addressbook/abResultsPaneOverlay.dtd" >
+%abResultsPaneOverlayDTD;
+<!ENTITY % mailOverlayDTD SYSTEM "chrome://messenger/locale/mailOverlay.dtd">
+%mailOverlayDTD;
+<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
+%messengerDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ id="addressbookWindow"
+ height="450"
+ width="750"
+ title="&addressbookWindow.title;"
+ lightweightthemes="true"
+ lightweightthemesfooter="status-bar"
+ windowtype="mail:addressbook"
+ macanimationtype="document"
+ drawtitle="true"
+ persist="width height screenX screenY sizemode"
+ toggletoolbar="true"
+ onload="OnLoadAddressBook()"
+ onunload="OnUnloadAddressBook()">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ </stringbundleset>
+
+<script src="chrome://messenger/content/jsTreeView.js"/>
+<script src="chrome://messenger/content/addressbook/abTrees.js"/>
+<script src="chrome://messenger/content/accountUtils.js"/>
+<script src="chrome://messenger/content/addressbook/addressbook.js"/>
+<script src="chrome://messenger/content/addressbook/abCommon.js"/>
+<script src="chrome://communicator/content/contentAreaClick.js"/>
+<script src="chrome://global/content/printUtils.js"/>
+<script src="chrome://messenger/content/msgPrintEngine.js"/>
+<script src="chrome://messenger/content/addressbook/abCardViewOverlay.js"/>
+
+ <commandset id="addressBook">
+ <commandset id="CommandUpdate_AddressBook"
+ commandupdater="true"
+ events="focus,addrbook-select"
+ oncommandupdate="CommandUpdate_AddressBook()"/>
+ <commandset id="selectEditMenuItems"/>
+ <commandset id="undoEditMenuItems"/>
+ <commandset id="globalEditMenuItems"/>
+ <command id="cmd_newNavigator"/>
+ <command id="cmd_newPrivateWindow"/>
+ <command id="cmd_newEditor"/>
+ <command id="cmd_newlist" oncommand="AbNewList();"/>
+ <command id="cmd_newCard" oncommand="goDoCommand('cmd_newCard');"/>
+ <command id="cmd_newMessage" oncommand="AbNewMessage();"/>
+ <command id="cmd_printSetup" oncommand="PrintUtils.showPageSetup()"/>
+ <command id="cmd_print" oncommand="AbPrintCard();"/>
+ <command id="cmd_printpreview" oncommand="AbPrintPreviewCard();"/>
+ <command id="cmd_printcard" oncommand="AbPrintCard()"/>
+ <command id="cmd_printcardpreview" oncommand="AbPrintPreviewCard()"/>
+ <command id="cmd_printAddressBook" oncommand="AbPrintAddressBook()"/>
+ <command id="cmd_printPreviewAddressBook" oncommand="AbPrintPreviewAddressBook()"/>
+ <command id="cmd_close" oncommand="AbClose()"/>
+ <command id="cmd_properties" oncommand="goDoCommand('cmd_properties');"/>
+ <command id="cmd_undo"/>
+ <command id="cmd_redo"/>
+ <command id="cmd_copy"/>
+ <command id="cmd_paste"/>
+ <command id="cmd_cut"/>
+ <command id="cmd_delete"
+ valueAddressBook="&deleteAbCmd.label;"
+ valueCard="&deleteContactCmd.label;"
+ valueCards="&deleteContactsCmd.label;"
+ valueList="&deleteListCmd.label;"
+ valueLists="&deleteListsCmd.label;"
+ valueItems="&deleteItemsCmd.label;"/>
+ <command id="cmd_selectAll"/>
+ <command id="button_delete" oncommand="goDoCommand('button_delete');"/>
+ <command id="cmd_swapFirstNameLastName" oncommand="AbSwapFirstNameLastName()"/>
+ <commandset id="tasksCommands"/>
+ </commandset>
+
+<broadcasterset id="abBroadcasters">
+ <broadcaster id="Communicator:WorkMode"/>
+</broadcasterset>
+
+<broadcasterset id="mainBroadcasterSet"/>
+
+<keyset id="tasksKeys">
+ <!-- File Menu -->
+ <key id="key_newNavigator"/>
+ <key id="key_newPrivateWindow"/>
+ <key id="key_newBlankPage"/>
+#ifdef XP_MACOSX
+ <key id="key_newMessage" key="&newMessageCmd.key;"
+ modifiers="accel,shift" command="cmd_newMessage"/>
+#else
+ <key id="key_newMessage" key="&newMessageCmd.key;"
+ modifiers="accel" command="cmd_newMessage"/>
+#endif
+ <key id="key_newCard" key="&newContact.key;" modifiers="accel"
+ command="cmd_newCard"/>
+ <key id="key_printCard" key="&printContactViewCmd.key;"
+ command="cmd_printcard" modifiers="accel"/>
+ <key id="key_close"/>
+ <!-- Edit Menu -->
+ <key id="key_delete"/>
+ <key id="key_delete2"/> <!-- secondary delete key -->
+ <key id="key_undo"/>
+ <key id="key_redo"/>
+ <key id="key_cut"/>
+ <key id="key_copy"/>
+ <key id="key_paste"/>
+ <key id="key_selectAll"/>
+ <key id="key_properties" key="&propertiesCmd.key;" command="cmd_properties" modifiers="accel"/>
+
+ <!-- View Menu -->
+#ifndef XP_MACOSX
+ <key id="key_toggleDirectoryPane"
+ keycode="VK_F9"
+ oncommand="togglePaneSplitter('dirTree-splitter');"/>
+#else
+ <key id="key_toggleDirectoryPane"
+ key="&toggleDirectoryPaneCmd.key;"
+ modifiers="accel,alt"
+ oncommand="togglePaneSplitter('dirTree-splitter');"/>
+#endif
+
+ <!-- Tab/F6 Keys -->
+ <key keycode="VK_TAB" oncommand="SwitchPaneFocus(event);" modifiers="control,shift"/>
+ <key keycode="VK_TAB" oncommand="SwitchPaneFocus(event);" modifiers="control"/>
+ <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="control,shift"/>
+ <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="control"/>
+ <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="shift"/>
+ <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);"/>
+
+ <!-- Search field -->
+ <key key="&focusSearchInput.key;"
+ modifiers="accel"
+ oncommand="focusElement(document.getElementById('searchInput'));"/>
+
+</keyset>
+
+<menupopup id="dirTreeContext">
+ <menuitem id="dirTreeContext-properties"
+ label="&editItemButton.label;"
+ accesskey="&editItemButton.accesskey;"
+ command="cmd_properties"/>
+ <menuseparator/>
+ <menuitem id="dirTreeContext-newcard"
+ label="&newContactButton.label;"
+ accesskey="&newContactButton.accesskey;"
+ command="cmd_newCard"/>
+ <menuitem id="dirTreeContext-newlist"
+ label="&newlistButton.label;"
+ accesskey="&newlistButton.accesskey;"
+ command="cmd_newlist"/>
+ <menuseparator/>
+ <menuitem id="dirTreeContext-print"
+ label="&printButton.label;"
+ accesskey="&printButton.accesskey;"
+ command="cmd_printAddressBook"/>
+ <menuitem id="dirTreeContext-delete"
+ label="&deleteItemButton.label;"
+ accesskey="&deleteItemButton.accesskey;"
+ command="button_delete"/>
+</menupopup>
+
+<menupopup id="abResultsTreeContext">
+ <menuitem id="abResultsTreeContext-properties"
+ label="&editItemButton.label;"
+ accesskey="&editItemButton.accesskey;"
+ command="cmd_properties"/>
+ <menuseparator/>
+ <menuitem id="abResultsTreeContext-newmessage"
+ label="&newmsgButton.label;"
+ accesskey="&newmsgButton.accesskey;"
+ command="cmd_newMessage"/>
+ <menuseparator/>
+ <menuitem id="abResultsTreeContext-print"
+ label="&printButton.label;"
+ accesskey="&printButton.accesskey;"
+ command="cmd_printcard"/>
+ <menuitem id="abResultsTreeContext-delete"
+ label="&deleteItemButton.label;"
+ accesskey="&deleteItemButton.accesskey;"
+ command="button_delete"/>
+</menupopup>
+
+<menupopup id="toolbar-context-menu"/>
+
+<vbox id="titlebar"/>
+
+<toolbox id="ab-toolbox"
+ mode="full"
+ defaultmode="full"
+ class="toolbox-top">
+ <toolbar type="menubar"
+ id="addrbook-toolbar-menubar2"
+ class="chromeclass-menubar"
+ persist="collapsed"
+ grippytooltiptext="&menuBar.tooltip;"
+ customizable="true"
+ defaultset="menubar-items"
+ mode="icons"
+ iconsize="small"
+ defaultmode="icons"
+ defaulticonsize="small"
+ context="toolbar-context-menu">
+ <toolbaritem id="menubar-items"
+ class="menubar-items"
+ align="center">
+ <menubar id="ab-menubar">
+ <menu id="menu_File">
+ <menupopup id="menu_FilePopup">
+ <menu id="menu_New">
+ <menupopup id="menu_NewPopup">
+ <menuitem id="menu_newContact"
+ label="&newContact.label;"
+ accesskey="&newContact.accesskey;"
+ key="key_newCard"
+ command="cmd_newCard"/>
+ <menuitem id="menu_newList"
+ label="&newListCmd.label;"
+ accesskey="&newListCmd.accesskey;"
+ command="cmd_newlist"/>
+ <menuitem id="menu_newAddrbook"
+ label="&newAddressBookCmd.label;"
+ accesskey="&newAddressBookCmd.accesskey;"
+ oncommand="AbNewAddressBook();"/>
+ <menuitem id="addLDAP"
+ label="&newLDAPDirectoryCmd.label;"
+ accesskey="&newLDAPDirectoryCmd.accesskey;"
+ oncommand="AbNewLDAPDirectory();"/>
+ <menuseparator/>
+ <menuitem id="menu_newNavigator"/>
+ <menuitem id="menu_newPrivateWindow"/>
+ <menuitem id="menu_newMessage"
+ label="&newMessageCmd.label;"
+ accesskey="&newMessageCmd.accesskey;"
+ key="key_newMessage"
+ command="cmd_newMessage"/>
+ <menuitem id="menu_newEditor"/>
+ </menupopup>
+ </menu>
+ <menuitem id="menu_close"/>
+ <menuseparator/>
+#ifndef XP_MACOSX
+ <menuitem id="printPreviewMenuItem"
+ label="&printPreviewContactViewCmd.label;"
+ accesskey="&printPreviewContactViewCmd.accesskey;"
+ command="cmd_printcardpreview"/>
+#endif
+ <menuitem id="printMenuItem" label="&printContactViewCmd.label;"
+ accesskey="&printContactViewCmd.accesskey;"
+ key="key_printCard"
+ command="cmd_printcard"/>
+ <menuseparator id="menu_PageSetupSeparator"/>
+ <menuitem id="menu_printSetup"/>
+ <menuseparator id="menu_PrintAddrbookSeparator"/>
+#ifndef XP_MACOSX
+ <menuitem id="printPreviewAddressBook"
+ label="&printPreviewAddressBook.label;"
+ accesskey="&printPreviewAddressBook.accesskey;"
+ command="cmd_printPreviewAddressBook"/>
+#endif
+ <menuitem id="printAddressBook"
+ label="&printAddressBook.label;"
+ accesskey="&printAddressBook.accesskey;"
+ command="cmd_printAddressBook"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_Edit">
+ <menupopup id="menu_EditPopup">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+ <menuseparator/>
+ <menuitem id="menu_cut"/>
+ <menuitem id="menu_copy"/>
+ <menuitem id="menu_paste"/>
+ <menuitem id="menu_delete"/>
+ <menuseparator/>
+ <menuitem id="menu_selectAll"/>
+ <menuseparator/>
+ <!-- LOCALIZATION NOTE: set "hideSwapFnLnUI" to false in .dtd to enable the UI -->
+ <menuitem label="&swapFirstNameLastNameCmd.label;"
+ accesskey="&swapFirstNameLastNameCmd.accesskey;"
+ hidden="&hideSwapFnLnUI;"
+ command="cmd_swapFirstNameLastName"/>
+ <menuitem label="&propertiesCmd.label;"
+ accesskey="&propertiesCmd.accesskey;"
+ key="key_properties"
+ command="cmd_properties"/>
+ <menuitem id="menu_preferences"
+ oncommand="goPreferences('addressing_pane')"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_View">
+ <menupopup id="menu_View_Popup">
+ <menu id="menu_Toolbars">
+ <menupopup id="view_toolbars_popup"
+ onpopupshowing="onViewToolbarsPopupShowing(event);"
+ oncommand="onViewToolbarCommand(event);">
+ <menuitem id="menu_showTaskbar"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_Layout"
+ label="&layoutMenu.label;"
+ accesskey="&layoutMenu.accesskey;">
+ <menupopup id="view_layout_popup"
+ onpopupshowing="InitViewLayoutMenuPopup(event);">
+ <menuitem id="menu_showDirectoryPane"
+ label="&showDirectoryPane.label;"
+ key="key_toggleDirectoryPane"
+ accesskey="&showDirectoryPane.accesskey;"
+ oncommand="togglePaneSplitter('dirTree-splitter');"
+ checked="true"
+ type="checkbox"/>
+ <menuitem id="menu_showCardPane"
+ label="&showContactPane2.label;"
+ accesskey="&showContactPane2.accesskey;"
+ oncommand="togglePaneSplitter('results-splitter');"
+ checked="true"
+ type="checkbox"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="viewMenuAfterLayoutSeparator"/>
+ <menu id="menu_ShowNameAs" label="&menu_ShowNameAs.label;" accesskey="&menu_ShowNameAs.accesskey;">
+ <menupopup id="menuShowNameAsPopup">
+ <menuitem type="radio" name="shownameas"
+ id="firstLastCmd"
+ label="&firstLastCmd.label;"
+ accesskey="&firstLastCmd.accesskey;"
+ oncommand="SetNameColumn('firstLastCmd')"/>
+ <menuitem type="radio" name="shownameas"
+ id="lastFirstCmd"
+ label="&lastFirstCmd.label;"
+ accesskey="&lastFirstCmd.accesskey;"
+ oncommand="SetNameColumn('lastFirstCmd')"/>
+ <menuitem type="radio" name="shownameas"
+ id="displayNameCmd"
+ label="&displayNameCmd.label;"
+ accesskey="&displayNameCmd.accesskey;"
+ oncommand="SetNameColumn('displayNameCmd')"/>
+ </menupopup>
+ </menu>
+ <menu id="sortMenu" label="&sortMenu.label;" accesskey="&sortMenu.accesskey;">
+ <menupopup id="sortMenuPopup" onpopupshowing="InitViewSortByMenu()">
+ <menuitem label="&GeneratedName.label;"
+ id="cmd_SortByGeneratedName"
+ accesskey="&GeneratedName.accesskey;"
+ oncommand="SortResultPane('GeneratedName');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&PrimaryEmail.label;"
+ id="cmd_SortByPrimaryEmail"
+ accesskey="&PrimaryEmail.accesskey;"
+ oncommand="SortResultPane('PrimaryEmail');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&ChatName.label;"
+ id="cmd_SortByChatName"
+ accesskey="&ChatName.accesskey;"
+ oncommand="SortResultPane('ChatName');"
+ name="sortas"
+ type="radio"
+ checked="true"/>
+ <menuitem label="&Company.label;"
+ id="cmd_SortByCompany"
+ accesskey="&Company.accesskey;"
+ oncommand="SortResultPane('Company');" name="sortas" type="radio" checked="true"/>
+ <!-- LOCALIZATION NOTE:
+ Fields for phonetic are disabled as default and can be enabled by
+ editing "mail.addr_book.show_phonetic_fields"
+ -->
+ <menuitem label="&_PhoneticName.label;"
+ id="cmd_SortBy_PhoneticName"
+ hidden="true"
+ accesskey="&_PhoneticName.accesskey;"
+ oncommand="SortResultPane('_PhoneticName');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&NickName.label;"
+ id="cmd_SortByNickName"
+ accesskey="&NickName.accesskey;"
+ oncommand="SortResultPane('NickName');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&SecondEmail.label;"
+ id="cmd_SortBySecondEmail"
+ accesskey="&SecondEmail.accesskey;"
+ oncommand="SortResultPane('SecondEmail');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&Department.label;"
+ id="cmd_SortByDepartment"
+ accesskey="&Department.accesskey;"
+ oncommand="SortResultPane('Department');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&JobTitle.label;"
+ id="cmd_SortByJobTitle"
+ accesskey="&JobTitle.accesskey;"
+ oncommand="SortResultPane('JobTitle');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&CellularNumber.label;"
+ id="cmd_SortByCellularNumber"
+ accesskey="&CellularNumber.accesskey;"
+ oncommand="SortResultPane('CellularNumber');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&PagerNumber.label;"
+ id="cmd_SortByPagerNumber"
+ accesskey="&PagerNumber.accesskey;"
+ oncommand="SortResultPane('PagerNumber');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&FaxNumber.label;"
+ id="cmd_SortByFaxNumber"
+ accesskey="&FaxNumber.accesskey;"
+ oncommand="SortResultPane('FaxNumber');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&HomePhone.label;"
+ id="cmd_SortByHomePhone"
+ accesskey="&HomePhone.accesskey;"
+ oncommand="SortResultPane('HomePhone');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&WorkPhone.label;"
+ id="cmd_SortByWorkPhone"
+ accesskey="&WorkPhone.accesskey;"
+ oncommand="SortResultPane('WorkPhone');" name="sortas" type="radio" checked="true"/>
+ <menuitem label="&Addrbook.label;"
+ id="cmd_SortByaddrbook"
+ accesskey="&Addrbook.accesskey;"
+ oncommand="SortResultPane('addrbook');"
+ name="sortas"
+ type="radio"
+ checked="true"/>
+ <menuseparator/>
+ <menuitem id="sortAscending" type="radio" name="sortdirection" label="&sortAscending.label;" accesskey="&sortAscending.accesskey;" oncommand="AbSortAscending()"/>
+ <menuitem id="sortDescending" type="radio" name="sortdirection" label="&sortDescending.label;" accesskey="&sortDescending.accesskey;" oncommand="AbSortDescending()"/>
+ </menupopup>
+ </menu>
+ </menupopup>
+ </menu>
+ <menu id="tasksMenu">
+ <menupopup id="taskPopup">
+ <menuitem label="&searchAddressesCmd.label;"
+ accesskey="&searchAddressesCmd.accesskey;"
+ id="menu_search_addresses"
+ oncommand="onAdvancedAbSearch();"/>
+ <menuitem label="&importCmd.label;" accesskey="&importCmd.accesskey;" oncommand="toImport()"/>
+ <menuitem label="&exportCmd.label;"
+ accesskey="&exportCmd.accesskey;"
+ oncommand="AbExportSelection();"/>
+ <menuseparator/>
+ </menupopup>
+ </menu>
+
+ <menu id="windowMenu"/>
+ <menu id="menu_Help"/>
+ <spacer flex="1"/>
+ </menubar>
+ </toolbaritem>
+ </toolbar>
+ <toolbar class="chromeclass-toolbar toolbar-primary"
+ id="ab-bar2"
+ persist="collapsed"
+ customizable="true"
+ grippytooltiptext="&addressbookToolbar.tooltip;"
+ toolbarname="&showAbToolbarCmd.label;"
+ accesskey="&showAbToolbarCmd.accesskey;"
+ defaultset="button-newcard,button-newlist,separator,button-editcard,button-newmessage,button-abdelete,spring,searchBox,throbber-box"
+ context="toolbar-context-menu">
+ <toolbarbutton id="button-newcard"
+ class="toolbarbutton-1"
+ label="&newContactButton.label;"
+ tooltiptext="&newContactButton.tooltip;"
+ removable="true"
+ command="cmd_newCard"/>
+ <toolbarbutton id="button-newlist"
+ class="toolbarbutton-1"
+ label="&newlistButton.label;"
+ tooltiptext="&newlistButton.tooltip;"
+ removable="true"
+ command="cmd_newlist"/>
+ <toolbarbutton id="button-editcard"
+ class="toolbarbutton-1"
+ label="&editItemButton.label;"
+ tooltiptext="&editItemButton.tooltip;"
+ removable="true"
+ command="cmd_properties"/>
+ <toolbarbutton id="button-newmessage"
+ class="toolbarbutton-1"
+ label="&newmsgButton.label;"
+ tooltiptext="&newmsgButton.tooltip;"
+ removable="true"
+ oncommand="AbNewMessage();"/>
+ <toolbarbutton id="button-abdelete"
+ class="toolbarbutton-1"
+ label="&deleteItemButton.label;"
+ tooltiptext="&deleteItemButton.tooltip;"
+ removable="true"
+ oncommand="goDoCommand('button_delete');"/>
+ <toolbarbutton id="print-button"
+ label="&printButton.label;"
+ tooltiptext="&printButton.tooltip;"
+ removable="true"/>
+ <toolbaritem id="searchBox"
+ flex="1"
+ title="&searchBox.title;"
+ removable="true"
+ align="center"
+ class="toolbaritem-noline chromeclass-toolbar-additional">
+ <textbox id="searchInput"
+ flex="1"
+ type="search"
+ aria-controls="abResultsTree"
+ placeholder="&searchNameAndEmail.placeholder;"
+ clickSelectsAll="true"
+ oncommand="onEnterInSearchBar();"
+ onkeypress="if (event.keyCode == KeyEvent.DOM_VK_RETURN) this.select();"/>
+ </toolbaritem>
+
+ <toolbaritem id="throbber-box"/>
+ </toolbar>
+ <toolbarpalette id="AbToolbarPalette"/>
+ <toolbarset id="customToolbars" context="toolbar-context-menu"/>
+</toolbox>
+
+ <!-- The main address book three pane -->
+ <hbox flex="1">
+ <vbox id="dirTreeBox" persist="width collapsed">
+ <tree id="dirTree"
+ class="abDirectory plain"
+ seltype="single"
+ style="min-width: 150px;"
+ flex="1"
+ persist="width"
+ hidecolumnpicker="true"
+ context="dirTreeContext"
+ onselect="DirPaneSelectionChange(); document.commandDispatcher.updateCommands('addrbook-select');"
+ ondblclick="DirPaneDoubleClick(event);"
+ onclick="DirPaneClick(event);"
+ onblur="goOnEvent(this, 'blur');">
+ <treecols>
+ <treecol id="DirCol"
+ flex="1"
+ primary="true"
+ label="&dirTreeHeader.label;"
+ crop="center"
+ persist="width"/>
+ </treecols>
+ <treechildren/>
+ </tree>
+ </vbox>
+
+ <splitter id="dirTree-splitter" collapse="before" persist="state">
+ <grippy/>
+ </splitter>
+
+ <vbox flex="1" style="min-width:100px">
+ <description id="localResultsOnlyMessage"
+ value="&localResultsOnlyMessage.label;"/>
+ <vbox id="blankResultsPaneMessageBox"
+ flex="1"
+ pack="center"
+ align="center">
+ <description id="blankResultsPaneMessage"
+ value="&blankResultsPaneMessage.label;"/>
+ </vbox>
+ <!-- results pane -->
+ <tree id="abResultsTree" context="abResultsTreeContext" flex="1" />
+
+ <splitter id="results-splitter" collapse="after" persist="state">
+ <grippy/>
+ </splitter>
+
+ <!-- card view -->
+ <hbox id="CardViewOuterBox" flex="1" persist="height">
+ <vbox id="CardViewBox"
+ flex="1"
+ style="height:170px; min-height:1px; min-width:1px">
+ <vbox id="CardViewInnerBox" collapsed="true" flex="1">
+ <description id="CardTitle"/>
+ <hbox style="width:100%" flex="1">
+ <vbox id="cvbPhoto"
+ class="cardViewGroup"
+ align="center"
+ style="min-width: 10ch; max-width: 10ch;">
+ <image id="cvPhoto" style="max-width: 10ch; max-height: 10ch;"/>
+ </vbox>
+ <hbox flex="1" equalsize="always">
+ <vbox flex="1" class="cardViewColumn">
+ <vbox id="cvbContact" class="cardViewGroup">
+ <description class="CardViewHeading" id="cvhContact">&contact.heading;</description>
+ <description class="CardViewLink" id="cvListNameBox">
+ <html:p><html:a href="" id="cvListName"/></html:p>
+ </description>
+ <description class="CardViewText" id="cvDisplayName"/>
+ <description class="CardViewText" id="cvNickname"/>
+ <description class="CardViewLink" id="cvEmail1Box">
+ <html:a href="" id="cvEmail1"/>
+ </description>
+ <description class="CardViewLink" id="cvEmail2Box">
+ <html:a href="" id="cvEmail2"/>
+ </description>
+ </vbox>
+ <vbox id="cvbHome" class="cardViewGroup">
+ <description class="CardViewHeading" id="cvhHome">&home.heading;</description>
+ <hbox>
+ <vbox flex="1">
+ <description class="CardViewText" id="cvHomeAddress"/>
+ <description class="CardViewText" id="cvHomeAddress2"/>
+ <description class="CardViewText" id="cvHomeCityStZip"/>
+ <description class="CardViewText" id="cvHomeCountry"/>
+ </vbox>
+ <vbox id="cvbHomeMapItBox" pack="end">
+ <button id="cvHomeMapIt"
+ label="&mapItButton.label;"
+ type="menu-button"
+ oncommand="openLinkWithUrl(this.firstChild.mapURL);"
+ tooltiptext="&mapIt.tooltip;">
+ <menupopup class="map-list"/>
+ </button>
+ </vbox>
+ </hbox>
+ <description class="CardViewLink" id="cvHomeWebPageBox">
+ <html:a onclick="return openLink(event);"
+ href=""
+ id="cvHomeWebPage"/>
+ </description>
+ </vbox>
+ <vbox id="cvbOther" class="cardViewGroup">
+ <description class="CardViewHeading" id="cvhOther">&other.heading;</description>
+ <description class="CardViewText" id="cvBirthday"/>
+ <description class="CardViewText" id="cvCustom1"/>
+ <description class="CardViewText" id="cvCustom2"/>
+ <description class="CardViewText" id="cvCustom3"/>
+ <description class="CardViewText" id="cvCustom4"/>
+ <description class="CardViewText" id="cvNotes"
+ style="white-space: pre-wrap;"/>
+ <hbox>
+ <image id="cvBuddyIcon"/>
+ </hbox>
+ </vbox>
+ <vbox id="cvbChat" class="cardViewGroup">
+ <description class="CardViewHeading" id="cvhChat">&chat.heading;</description>
+ <description class="CardViewText" id="cvYahoo"/>
+ <description class="CardViewText" id="cvSkype"/>
+ <description class="CardViewText" id="cvQQ"/>
+ <description class="CardViewText" id="cvMSN"/>
+ <description class="CardViewText" id="cvICQ"/>
+ <description class="CardViewText" id="cvXMPP"/>
+ <description class="CardViewText" id="cvIRC"/>
+ </vbox>
+ <!-- the description and addresses groups are only for
+ mailing lists -->
+ <vbox id="cvbDescription" class="cardViewGroup">
+ <description class="CardViewHeading" id="cvhDescription">&description.heading;</description>
+ <description class="CardViewText" id="cvDescription"/>
+ </vbox>
+ <vbox id="cvbAddresses" class="cardViewGroup">
+ <description class="CardViewHeading" id="cvhAddresses">&addresses.heading;</description>
+ <vbox id="cvAddresses"/>
+ </vbox>
+ </vbox>
+
+ <vbox flex="1" class="cardViewColumn">
+ <vbox id="cvbPhone" class="cardViewGroup">
+ <description class="CardViewHeading" id="cvhPhone">&phone.heading;</description>
+ <description class="CardViewText" id="cvPhWork"/>
+ <description class="CardViewText" id="cvPhHome"/>
+ <description class="CardViewText" id="cvPhFax"/>
+ <description class="CardViewText" id="cvPhCellular"/>
+ <description class="CardViewText" id="cvPhPager"/>
+ </vbox>
+ <vbox id="cvbWork" class="cardViewGroup">
+ <description class="CardViewHeading" id="cvhWork">&work.heading;</description>
+ <description class="CardViewText" id="cvJobTitle"/>
+ <description class="CardViewText" id="cvDepartment"/>
+ <description class="CardViewText" id="cvCompany"/>
+ <hbox>
+ <vbox flex="1">
+ <description class="CardViewText" id="cvWorkAddress"/>
+ <description class="CardViewText" id="cvWorkAddress2"/>
+ <description class="CardViewText" id="cvWorkCityStZip"/>
+ <description class="CardViewText" id="cvWorkCountry"/>
+ </vbox>
+ <vbox id="cvbWorkMapItBox" pack="end">
+ <button id="cvWorkMapIt"
+ label="&mapItButton.label;"
+ type="menu-button"
+ oncommand="openLinkWithUrl(this.firstChild.mapURL);"
+ tooltiptext="&mapIt.tooltip;">
+ <menupopup class="map-list"/>
+ </button>
+ </vbox>
+ </hbox>
+ <description class="CardViewLink" id="cvWorkWebPageBox">
+ <html:a onclick="return openLink(event);"
+ href=""
+ id="cvWorkWebPage"/>
+ </description>
+ </vbox>
+ </vbox>
+ </hbox>
+ </hbox>
+ </vbox>
+ </vbox>
+ </hbox>
+ </vbox>
+ </hbox>
+
+ <panel id="customizeToolbarSheetPopup"/>
+ <statusbar id="status-bar" class="chromeclass-status">
+ <statusbarpanel id="component-bar"/>
+ <statusbarpanel id="statusText" flex="1" value="&statusText.label;"/>
+ <statusbarpanel id="offline-status" class="statusbarpanel-iconic"/>
+ </statusbar>
+</window>
diff --git a/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.js b/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.js
new file mode 100644
index 0000000000..bfc741c97f
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.js
@@ -0,0 +1,24 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+function Startup()
+{
+ enableAutocomplete();
+}
+
+function onEditDirectories()
+{
+ window.openDialog("chrome://messenger/content/addressbook/pref-editdirectories.xul",
+ "editDirectories", "chrome,modal=yes,resizable=no", null);
+}
+
+function enableAutocomplete()
+{
+ var acLDAPValue = document.getElementById("ldap_2.autoComplete.useDirectory")
+ .value;
+
+ EnableElementById("directoriesList", acLDAPValue, false);
+ EnableElementById("editButton", acLDAPValue, false);
+}
diff --git a/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.xul b/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.xul
new file mode 100644
index 0000000000..af4bed750b
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.xul
@@ -0,0 +1,92 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/addressbook/pref-addressing.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="addressing_pane" label="&pref.addressing.title;"
+ script="chrome://messenger/content/addressbook/pref-addressing.js">
+
+ <stringbundle id="bundle_addressBook"
+ src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+
+ <preferences id="addressing_preferences">
+ <preference id="mail.collect_email_address_outgoing"
+ name="mail.collect_email_address_outgoing"
+ type="bool"/>
+ <preference id="mail.collect_addressbook"
+ name="mail.collect_addressbook"
+ type="string"/>
+ <preference id="mail.autoComplete.highlightNonMatches"
+ name="mail.autoComplete.highlightNonMatches"
+ type="bool"/>
+ <preference id="mail.enable_autocomplete"
+ name="mail.enable_autocomplete"
+ type="bool"/>
+ <preference id="ldap_2.autoComplete.useDirectory"
+ name="ldap_2.autoComplete.useDirectory"
+ onchange="enableAutocomplete();" type="bool"/>
+ <preference id="ldap_2.autoComplete.directoryServer"
+ name="ldap_2.autoComplete.directoryServer"
+ type="string"/>
+ <preference id="pref.ldap.disable_button.edit_directories"
+ name="pref.ldap.disable_button.edit_directories"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&emailCollectiontitle.label;"/>
+ <description>&emailCollectiontext.label;</description>
+ <hbox align="center">
+ <checkbox id="emailCollectionOutgoing"
+ label="&emailCollectionPicker.label;"
+ accesskey="&emailCollectionPicker.accesskey;"
+ preference="mail.collect_email_address_outgoing"/>
+ <menulist id="localDirectoriesList" flex="1"
+ aria-labelledby="emailCollectionOutgoing"
+ preference="mail.collect_addressbook">
+ <menupopup id="localDirectoriesPopup" class="addrbooksPopup"
+ localonly="true" writable="true"/>
+ </menulist>
+ </hbox>
+ </groupbox>
+ <groupbox id="addressAutocompletion">
+ <caption label="&addressingTitle.label;"/>
+ <hbox align="center">
+ <checkbox id="highlightNonMatches" label="&highlightNonMatches.label;"
+ preference="mail.autoComplete.highlightNonMatches"
+ accesskey="&highlightNonMatches.accesskey;"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <description>&autocompleteText.label;</description>
+ <hbox align="center">
+ <checkbox id="addressingAutocomplete" label="&addressingEnable.label;"
+ preference="mail.enable_autocomplete"
+ accesskey="&addressingEnable.accesskey;"/>
+ </hbox>
+ <hbox align="center">
+ <checkbox id="autocompleteLDAP" label="&directories.label;"
+ preference="ldap_2.autoComplete.useDirectory"
+ accesskey="&directories.accesskey;"/>
+ <menulist id="directoriesList" flex="1"
+ aria-labelledby="autocompleteLDAP"
+ preference="ldap_2.autoComplete.directoryServer">
+ <menupopup id="directoriesListPopup" class="addrbooksPopup"
+ none="&directoriesNone.label;"
+ remoteonly="true" value="dirPrefId"/>
+ </menulist>
+ <button id="editButton" label="&editDirectories.label;"
+ oncommand="onEditDirectories();"
+ accesskey="&editDirectories.accesskey;"
+ preference="pref.ldap.disable_button.edit_directories"/>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/addrbook/jar.mn b/comm/suite/mailnews/components/addrbook/jar.mn
new file mode 100644
index 0000000000..cc2cec4e13
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/jar.mn
@@ -0,0 +1,24 @@
+# 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/.
+
+messenger.jar:
+ content/messenger/addressbook/addressbook.js (content/addressbook.js)
+* content/messenger/addressbook/addressbook.xul (content/addressbook.xul)
+ content/messenger/addressbook/abCommon.js (content/abCommon.js)
+ content/messenger/addressbook/abCardOverlay.js (content/abCardOverlay.js)
+ content/messenger/addressbook/abCardOverlay.xul (content/abCardOverlay.xul)
+ content/messenger/addressbook/abCardViewOverlay.js (content/abCardViewOverlay.js)
+ content/messenger/addressbook/abEditCardDialog.xul (content/abEditCardDialog.xul)
+ content/messenger/addressbook/abNewCardDialog.xul (content/abNewCardDialog.xul)
+ content/messenger/addressbook/abResultsPaneOverlay.xul (content/abResultsPaneOverlay.xul)
+ content/messenger/addressbook/abMailListDialog.xul (content/abMailListDialog.xul)
+ content/messenger/addressbook/abEditListDialog.xul (content/abEditListDialog.xul)
+ content/messenger/addressbook/abListOverlay.xul (content/abListOverlay.xul)
+ content/messenger/addressbook/abSelectAddressesDialog.js (content/abSelectAddressesDialog.js)
+ content/messenger/addressbook/abSelectAddressesDialog.xul (content/abSelectAddressesDialog.xul)
+ content/messenger/addressbook/abTrees.js (content/abTrees.js)
+ content/messenger/addressbook/addressbook-panel.xul (content/addressbook-panel.xul)
+ content/messenger/addressbook/addressbook-panel.js (content/addressbook-panel.js)
+ content/messenger/addressbook/pref-addressing.js (content/prefs/pref-addressing.js)
+ content/messenger/addressbook/pref-addressing.xul (content/prefs/pref-addressing.xul)
diff --git a/comm/suite/mailnews/components/addrbook/moz.build b/comm/suite/mailnews/components/addrbook/moz.build
new file mode 100644
index 0000000000..77788c7f3e
--- /dev/null
+++ b/comm/suite/mailnews/components/addrbook/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+DEFINES["TOOLKIT_DIR"] = "%s/toolkit" % (CONFIG["topsrcdir"],)
diff --git a/comm/suite/mailnews/components/calendar/content/suite-overlay-addons.xhtml b/comm/suite/mailnews/components/calendar/content/suite-overlay-addons.xhtml
new file mode 100644
index 0000000000..c1ccec4917
--- /dev/null
+++ b/comm/suite/mailnews/components/calendar/content/suite-overlay-addons.xhtml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<overlay id="suiteAddonsOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script><![CDATA[
+ var lightningPrefs = {
+ guid: "{e2fda1a4-762b-4020-b5ad-a41df1933103}",
+ handleEvent: function(aEvent) {
+ var item = gListView.getListItemForID(this.guid);
+ if (!item)
+ return;
+
+ item.showPreferences = this.showPreferences;
+ },
+ showPreferences: function() {
+ var win = Services.wm.getMostRecentWindow("mozilla:preferences");
+ if (win) {
+ win.focus();
+ var doc = win.document;
+ var pane = doc.getElementById("paneLightning");
+ doc.querySelector("dialog").syncTreeWithPane(pane, true);
+ } else {
+ openDialog("chrome://communicator/content/pref/preferences.xhtml",
+ "PrefWindow",
+ "non-private,chrome,titlebar,dialog=no,resizable",
+ "paneLightning");
+ }
+ },
+ };
+
+ window.addEventListener("ViewChanged", lightningPrefs, false);
+ ]]></script>
+
+</overlay>
diff --git a/comm/suite/mailnews/components/calendar/content/suite-overlay-preferences.xhtml b/comm/suite/mailnews/components/calendar/content/suite-overlay-preferences.xhtml
new file mode 100644
index 0000000000..0e7aad33df
--- /dev/null
+++ b/comm/suite/mailnews/components/calendar/content/suite-overlay-preferences.xhtml
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://lightning/skin/lightning.css"?>
+
+<?xul-overlay href="chrome://calendar/content/preferences/general.xhtml"?>
+<?xul-overlay href="chrome://calendar/content/preferences/alarms.xhtml"?>
+<?xul-overlay href="chrome://calendar/content/preferences/categories.xhtml"?>
+<?xul-overlay href="chrome://calendar/content/preferences/views.xhtml"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd">
+ %lightningDTD;
+ <!ENTITY % preferencesDTD SYSTEM "chrome://calendar/locale/preferences/preferences.dtd">
+ %preferencesDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <treechildren id="prefsPanelChildren">
+ <treeitem container="true"
+ id="lightningItem"
+ insertafter="mailnewsItem,navigatorItem"
+ label="&lightning.preferencesLabel;"
+ prefpane="paneLightning">
+ <treechildren id="lightningChildren">
+ <treeitem id="lightningAlarms"
+ label="&paneAlarms.title;"
+ prefpane="paneLightningAlarms"/>
+ <treeitem id="lightningCategories"
+ label="&paneCategories.title;"
+ prefpane="paneLightningCategories"/>
+ <treeitem id="lightningViews"
+ label="&paneViews.title;"
+ prefpane="paneLightningViews"/>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+
+ <prefwindow id="prefDialog">
+ <prefpane id="paneLightning"
+ label="&lightning.preferencesLabel;"
+ onpaneload="gCalendarGeneralPane.init();">
+ <vbox id="calPreferencesBoxGeneral"/>
+ </prefpane>
+ <prefpane id="paneLightningAlarms"
+ label="&paneAlarms.title;"
+ onpaneload="gAlarmsPane.init();">
+ <vbox id="calPreferencesBoxAlarms"/>
+ </prefpane>
+ <prefpane id="paneLightningCategories"
+ label="&paneCategories.title;"
+ onpaneload="gCategoriesPane.init();">
+ <vbox id="calPreferencesBoxCategories"/>
+ </prefpane>
+ <prefpane id="paneLightningViews"
+ label="&paneViews.title;"
+ onpaneload="gViewsPane.init();">
+ <vbox id="calPreferencesBoxViews"/>
+ </prefpane>
+ </prefwindow>
+
+</overlay>
diff --git a/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.js b/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.js
new file mode 100644
index 0000000000..6c27c9c385
--- /dev/null
+++ b/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.js
@@ -0,0 +1,47 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/* import-globals-from ../../../suite/base/content/utilityOverlay.js */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+var ltnSuiteUtils = {
+ addStartupObserver: function() {
+ Services.obs.addObserver(this.startupObserver, "lightning-startup-done");
+ Services.obs.addObserver(this.startupObserver, "calendar-taskview-startup-done");
+ },
+
+ startupObserver: {
+ observe: function(subject, topic, state) {
+ if (topic != "lightning-startup-done" && topic != "calendar-taskview-startup-done") {
+ return;
+ }
+
+ const ids = [
+ ["CustomizeTaskActionsToolbar", "task-actions-toolbox"],
+ ["CustomizeCalendarToolbar", "calendar-toolbox"],
+ ["CustomizeTaskToolbar", "task-toolbox"],
+ ];
+
+ ids.forEach(([itemID, toolboxID]) => {
+ let item = document.getElementById(itemID);
+ let toolbox = document.getElementById(toolboxID);
+ toolbox.customizeInit = function() {
+ item.setAttribute("disabled", "true");
+ toolboxCustomizeInit("mail-menubar");
+ };
+ toolbox.customizeDone = function(aToolboxChanged) {
+ item.removeAttribute("disabled");
+ toolboxCustomizeDone("mail-menubar", toolbox, aToolboxChanged);
+ };
+ toolbox.customizeChange = function(aEvent) {
+ toolboxCustomizeChange(toolbox, aEvent);
+ };
+ });
+ },
+ },
+};
+
+ltnSuiteUtils.addStartupObserver();
diff --git a/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.xhtml b/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.xhtml
new file mode 100644
index 0000000000..79ae5d662a
--- /dev/null
+++ b/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.xhtml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<overlay id="suiteSidebarOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml">
+
+ <script src="chrome://lightning/content/suite-overlay-sidebar.js"/>
+
+ <key id="openLightningKey" removeelement="true"/>
+ <key id="openTasksKey" removeelement="true"/>
+ <key id="calendar-new-event-key" removeelement="true"/>
+ <key id="calendar-new-todo-key" removeelement="true"/>
+
+ <menuitem id="CustomizeTaskActionsToolbar"
+ oncommand="goCustomizeToolbar(document.getElementById('task-actions-toolbox'))"/>
+
+ <toolbox id="calendar-toolbox"
+ defaultlabelalign="end"
+ xpfe="false"/>
+ <toolbox id="task-toolbox"
+ defaultlabelalign="end"
+ xpfe="false"/>
+ <toolbox id="task-actions-toolbox"
+ defaultlabelalign="end"
+ xpfe="false"/>
+
+ <toolbar id="calendar-toolbar2"
+ defaultlabelalign="end"
+ context="toolbar-context-menu"/>
+ <toolbar id="task-toolbar2"
+ defaultlabelalign="end"
+ context="toolbar-context-menu"/>
+ <toolbar id="task-actions-toolbar"
+ context="toolbar-context-menu"/>
+
+</overlay>
diff --git a/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js b/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js
new file mode 100644
index 0000000000..48f7f31b1a
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js
@@ -0,0 +1,3936 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm");
+const {FolderUtils} = ChromeUtils.import("resource:///modules/FolderUtils.jsm");
+const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js");
+const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm");
+
+ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+/**
+ * interfaces
+ */
+var nsIMsgCompDeliverMode = Ci.nsIMsgCompDeliverMode;
+var nsIMsgCompSendFormat = Ci.nsIMsgCompSendFormat;
+var nsIMsgCompConvertible = Ci.nsIMsgCompConvertible;
+var nsIMsgCompType = Ci.nsIMsgCompType;
+var nsIMsgCompFormat = Ci.nsIMsgCompFormat;
+var nsIAbPreferMailFormat = Ci.nsIAbPreferMailFormat;
+var mozISpellCheckingEngine = Ci.mozISpellCheckingEngine;
+
+/**
+ * In order to distinguish clearly globals that are initialized once when js load (static globals) and those that need to be
+ * initialize every time a compose window open (globals), I (ducarroz) have decided to prefix by s... the static one and
+ * by g... the other one. Please try to continue and repect this rule in the future. Thanks.
+ */
+/**
+ * static globals, need to be initialized only once
+ */
+var sComposeMsgsBundle;
+var sBrandBundle;
+
+var sRDF = null;
+var sNameProperty = null;
+var sDictCount = 0;
+
+/**
+ * Global message window object. This is used by mail-offline.js and therefore
+ * should not be renamed. We need to avoid doing this kind of cross file global
+ * stuff in the future and instead pass this object as parameter when needed by
+ * functions in the other js file.
+ */
+var msgWindow;
+
+var gMessenger;
+
+/**
+ * Global variables, need to be re-initialized every time mostly because
+ * we need to release them when the window closes.
+ */
+var gHideMenus;
+var gMsgCompose;
+var gOriginalMsgURI;
+var gWindowLocked;
+var gSendLocked;
+var gContentChanged;
+var gAutoSaving;
+var gCurrentIdentity;
+var defaultSaveOperation;
+var gSendOrSaveOperationInProgress;
+var gCloseWindowAfterSave;
+var gSavedSendNowKey;
+var gSendFormat;
+var gLogComposePerformance;
+
+var gMsgIdentityElement;
+var gMsgAddressingWidgetElement;
+var gMsgSubjectElement;
+var gMsgAttachmentElement;
+var gMsgHeadersToolbarElement;
+var gComposeType;
+var gFormatToolbarHidden = false;
+var gBodyFromArgs;
+
+// i18n globals
+var gCharsetConvertManager;
+
+var gLastWindowToHaveFocus;
+var gReceiptOptionChanged;
+var gDSNOptionChanged;
+var gAttachVCardOptionChanged;
+
+var gAutoSaveInterval;
+var gAutoSaveTimeout;
+var gAutoSaveKickedIn;
+var gEditingDraft;
+
+var kComposeAttachDirPrefName = "mail.compose.attach.dir";
+
+function InitializeGlobalVariables()
+{
+ gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+ gMsgCompose = null;
+ gOriginalMsgURI = null;
+ gWindowLocked = false;
+ gContentChanged = false;
+ gCurrentIdentity = null;
+ defaultSaveOperation = "draft";
+ gSendOrSaveOperationInProgress = false;
+ gAutoSaving = false;
+ gCloseWindowAfterSave = false;
+ gSavedSendNowKey = null;
+ gSendFormat = nsIMsgCompSendFormat.AskUser;
+ gCharsetConvertManager = Cc['@mozilla.org/charset-converter-manager;1'].getService(Ci.nsICharsetConverterManager);
+ gHideMenus = false;
+ // We are storing the value of the bool logComposePerformance inorder to
+ // avoid logging unnecessarily.
+ gLogComposePerformance = MailServices.compose.logComposePerformance;
+
+ gLastWindowToHaveFocus = null;
+ gReceiptOptionChanged = false;
+ gDSNOptionChanged = false;
+ gAttachVCardOptionChanged = false;
+ msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"]
+ .createInstance(Ci.nsIMsgWindow);
+ MailServices.mailSession.AddMsgWindow(msgWindow);
+}
+InitializeGlobalVariables();
+
+function ReleaseGlobalVariables()
+{
+ gCurrentIdentity = null;
+ gCharsetConvertManager = null;
+ gMsgCompose = null;
+ gOriginalMsgURI = null;
+ gMessenger = null;
+ sComposeMsgsBundle = null;
+ sBrandBundle = null;
+ MailServices.mailSession.RemoveMsgWindow(msgWindow);
+ msgWindow = null;
+}
+
+function disableEditableFields()
+{
+ gMsgCompose.editor.flags |= Ci.nsIEditor.eEditorReadonlyMask;
+ var disableElements = document.getElementsByAttribute("disableonsend", "true");
+ for (let i = 0; i < disableElements.length; i++)
+ disableElements[i].setAttribute('disabled', 'true');
+
+}
+
+function enableEditableFields()
+{
+ gMsgCompose.editor.flags &= ~Ci.nsIEditor.eEditorReadonlyMask;
+ var enableElements = document.getElementsByAttribute("disableonsend", "true");
+ for (let i = 0; i < enableElements.length; i++)
+ enableElements[i].removeAttribute('disabled');
+
+}
+
+/**
+ * Small helper function to check whether the node passed in is a signature.
+ * Note that a text node is not a DOM element, hence .localName can't be used.
+ */
+function isSignature(aNode) {
+ return ["DIV","PRE"].includes(aNode.nodeName) &&
+ aNode.classList.contains("moz-signature");
+}
+
+var stateListener = {
+ NotifyComposeFieldsReady: function() {
+ ComposeFieldsReady();
+ updateSendCommands(true);
+ },
+
+ NotifyComposeBodyReady: function() {
+ this.useParagraph = gMsgCompose.composeHTML &&
+ Services.prefs.getBoolPref("mail.compose.default_to_paragraph");
+ this.editor = GetCurrentEditor();
+ this.paragraphState = document.getElementById("cmd_paragraphState");
+
+ // Look at the compose types which require action (nsIMsgComposeParams.idl):
+ switch (gComposeType) {
+
+ case Ci.nsIMsgCompType.MailToUrl:
+ gBodyFromArgs = true;
+ case Ci.nsIMsgCompType.New:
+ case Ci.nsIMsgCompType.NewsPost:
+ case Ci.nsIMsgCompType.ForwardAsAttachment:
+ this.NotifyComposeBodyReadyNew();
+ break;
+
+ case Ci.nsIMsgCompType.Reply:
+ case Ci.nsIMsgCompType.ReplyAll:
+ case Ci.nsIMsgCompType.ReplyToSender:
+ case Ci.nsIMsgCompType.ReplyToGroup:
+ case Ci.nsIMsgCompType.ReplyToSenderAndGroup:
+ case Ci.nsIMsgCompType.ReplyWithTemplate:
+ case Ci.nsIMsgCompType.ReplyToList:
+ this.NotifyComposeBodyReadyReply();
+ break;
+
+ case Ci.nsIMsgCompType.ForwardInline:
+ this.NotifyComposeBodyReadyForwardInline();
+ break;
+
+ case Ci.nsIMsgCompType.EditTemplate:
+ defaultSaveOperation = "template";
+ case Ci.nsIMsgCompType.Draft:
+ case Ci.nsIMsgCompType.Template:
+ case Ci.nsIMsgCompType.Redirect:
+ case Ci.nsIMsgCompType.EditAsNew:
+ break;
+
+ default:
+ dump("Unexpected nsIMsgCompType in NotifyComposeBodyReady (" +
+ gComposeType + ")\n");
+ }
+
+ // Set the selected item in the identity list as needed, which will cause
+ // an identity/signature switch. This can only be done once the message
+ // body has already been assembled with the signature we need to switch.
+ if (gMsgCompose.identity != gCurrentIdentity) {
+ // Since switching the signature loses the caret position, we record it
+ // and restore it later.
+ let selection = this.editor.selection;
+ let range = selection.getRangeAt(0);
+ let start = range.startOffset;
+ let startNode = range.startContainer;
+
+ this.editor.enableUndo(false);
+ let identityList = GetMsgIdentityElement();
+ identityList.selectedItem = identityList.getElementsByAttribute(
+ "identitykey", gMsgCompose.identity.key)[0];
+ LoadIdentity(false);
+
+ this.editor.enableUndo(true);
+ this.editor.resetModificationCount();
+ selection.collapse(startNode, start);
+ }
+
+ if (gMsgCompose.composeHTML)
+ loadHTMLMsgPrefs();
+ AdjustFocus();
+ },
+
+ NotifyComposeBodyReadyNew: function() {
+ let insertParagraph = this.useParagraph;
+
+ let mailDoc = document.getElementById("content-frame").contentDocument;
+ let mailBody = mailDoc.querySelector("body");
+ if (insertParagraph && gBodyFromArgs) {
+ // Check for "empty" body before allowing paragraph to be inserted.
+ // Non-empty bodies in a new message can occur when clicking on a
+ // mailto link or when using the command line option -compose.
+ // An "empty" body can be one of these two cases:
+ // 1) <br> and nothing follows (no next sibling)
+ // 2) <div/pre class="moz-signature">
+ // Note that <br><div/pre class="moz-signature"> doesn't happen in
+ // paragraph mode.
+ let firstChild = mailBody.firstChild;
+ if ((firstChild.nodeName != "BR" || firstChild.nextSibling) &&
+ !isSignature(firstChild))
+ insertParagraph = false;
+ }
+
+ // Control insertion of line breaks.
+ if (insertParagraph) {
+ this.editor.enableUndo(false);
+
+ this.editor.selection.collapse(mailBody, 0);
+ let pElement = this.editor.createElementWithDefaults("p");
+ let brElement = this.editor.createElementWithDefaults("br");
+ pElement.appendChild(brElement);
+ this.editor.insertElementAtSelection(pElement, false);
+
+ this.paragraphState.setAttribute("state", "p");
+
+ this.editor.beginningOfDocument();
+ this.editor.enableUndo(true);
+ this.editor.resetModificationCount();
+ } else {
+ this.paragraphState.setAttribute("state", "");
+ }
+ },
+
+ NotifyComposeBodyReadyReply: function() {
+ // Control insertion of line breaks.
+ if (this.useParagraph) {
+ let mailDoc = document.getElementById("content-frame").contentDocument;
+ let mailBody = mailDoc.querySelector("body");
+ let selection = this.editor.selection;
+
+ // Make sure the selection isn't inside the signature.
+ if (isSignature(mailBody.firstChild))
+ selection.collapse(mailBody, 0);
+
+ let range = selection.getRangeAt(0);
+ let start = range.startOffset;
+
+ if (start != range.endOffset) {
+ // The selection is not collapsed, most likely due to the
+ // "select the quote" option. In this case we do nothing.
+ return;
+ }
+
+ if (range.startContainer != mailBody) {
+ dump("Unexpected selection in NotifyComposeBodyReadyReply\n");
+ return;
+ }
+
+ this.editor.enableUndo(false);
+
+ let pElement = this.editor.createElementWithDefaults("p");
+ let brElement = this.editor.createElementWithDefaults("br");
+ pElement.appendChild(brElement);
+ this.editor.insertElementAtSelection(pElement, false);
+
+ // Position into the paragraph.
+ selection.collapse(pElement, 0);
+
+ this.paragraphState.setAttribute("state", "p");
+
+ this.editor.enableUndo(true);
+ this.editor.resetModificationCount();
+ } else {
+ this.paragraphState.setAttribute("state", "");
+ }
+ },
+
+ NotifyComposeBodyReadyForwardInline: function() {
+ let mailDoc = document.getElementById("content-frame").contentDocument;
+ let mailBody = mailDoc.querySelector("body");
+ let selection = this.editor.selection;
+
+ this.editor.enableUndo(false);
+
+ // Control insertion of line breaks.
+ selection.collapse(mailBody, 0);
+ if (this.useParagraph) {
+ let pElement = this.editor.createElementWithDefaults("p");
+ let brElement = this.editor.createElementWithDefaults("br");
+ pElement.appendChild(brElement);
+ this.editor.insertElementAtSelection(pElement, false);
+ this.paragraphState.setAttribute("state", "p");
+ } else {
+ // insertLineBreak() has been observed to insert two <br> elements
+ // instead of one before a <div>, so we'll do it ourselves here.
+ let brElement = this.editor.createElementWithDefaults("br");
+ this.editor.insertElementAtSelection(brElement, false);
+ this.paragraphState.setAttribute("state", "");
+ }
+
+ this.editor.beginningOfDocument();
+ this.editor.enableUndo(true);
+ this.editor.resetModificationCount();
+ },
+
+ ComposeProcessDone: function(aResult) {
+ gWindowLocked = false;
+ enableEditableFields();
+ updateComposeItems();
+
+ if (aResult== Cr.NS_OK)
+ {
+ if (!gAutoSaving)
+ SetContentAndBodyAsUnmodified();
+
+ if (gCloseWindowAfterSave)
+ {
+ // Notify the SendListener that Send has been aborted and Stopped
+ if (gMsgCompose)
+ gMsgCompose.onSendNotPerformed(null, Cr.NS_ERROR_ABORT);
+
+ MsgComposeCloseWindow();
+ }
+ }
+ // else if we failed to save, and we're autosaving, need to re-mark the editor
+ // as changed, so that we won't lose the changes.
+ else if (gAutoSaving)
+ {
+ gMsgCompose.bodyModified = true;
+ gContentChanged = true;
+ }
+
+ gAutoSaving = false;
+ gCloseWindowAfterSave = false;
+ },
+
+ SaveInFolderDone: function(folderURI) {
+ DisplaySaveFolderDlg(folderURI);
+ }
+};
+
+// all progress notifications are done through the nsIWebProgressListener implementation...
+var progressListener = {
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus)
+ {
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_START)
+ {
+ document.getElementById('navigator-throbber').setAttribute("busy", "true");
+ document.getElementById('compose-progressmeter').setAttribute( "mode", "undetermined" );
+ document.getElementById("statusbar-progresspanel").collapsed = false;
+ }
+
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
+ {
+ gSendOrSaveOperationInProgress = false;
+ document.getElementById('navigator-throbber').removeAttribute("busy");
+ document.getElementById('compose-progressmeter').setAttribute( "mode", "normal" );
+ document.getElementById('compose-progressmeter').setAttribute( "value", 0 );
+ document.getElementById("statusbar-progresspanel").collapsed = true;
+ document.getElementById('statusText').setAttribute('label', '');
+ }
+ },
+
+ onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress)
+ {
+ // Calculate percentage.
+ var percent;
+ if ( aMaxTotalProgress > 0 )
+ {
+ percent = Math.round( (aCurTotalProgress*100)/aMaxTotalProgress );
+ if ( percent > 100 )
+ percent = 100;
+
+ document.getElementById('compose-progressmeter').removeAttribute("mode");
+
+ // Advance progress meter.
+ document.getElementById('compose-progressmeter').setAttribute( "value", percent );
+ }
+ else
+ {
+ // Progress meter should be barber-pole in this case.
+ document.getElementById('compose-progressmeter').setAttribute( "mode", "undetermined" );
+ }
+ },
+
+ onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags)
+ {
+ // we can ignore this notification
+ },
+
+ onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage)
+ {
+ // Looks like it's possible that we get call while the document has been already delete!
+ // therefore we need to protect ourself by using try/catch
+ try {
+ let statusText = document.getElementById("statusText");
+ if (statusText)
+ statusText.setAttribute("label", aMessage);
+ } catch (ex) {}
+ },
+
+ onSecurityChange: function(aWebProgress, aRequest, state)
+ {
+ // we can ignore this notification
+ },
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Ci.nsIWebProgressListener) ||
+ iid.equals(Ci.nsISupportsWeakReference) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_NOINTERFACE;
+ }
+};
+
+var defaultController =
+{
+ supportsCommand: function(command)
+ {
+ switch (command)
+ {
+ //File Menu
+ case "cmd_attachFile":
+ case "cmd_attachPage":
+ case "cmd_close":
+ case "cmd_save":
+ case "cmd_saveAsFile":
+ case "cmd_saveAsDraft":
+ case "cmd_saveAsTemplate":
+ case "cmd_sendButton":
+ case "cmd_sendNow":
+ case "cmd_sendWithCheck":
+ case "cmd_sendLater":
+ case "cmd_printSetup":
+ case "cmd_printpreview":
+ case "cmd_print":
+
+ //Edit Menu
+ case "cmd_account":
+ case "cmd_preferences":
+
+ //Options Menu
+ case "cmd_selectAddress":
+ case "cmd_outputFormat":
+ case "cmd_quoteMessage":
+ return true;
+
+ default:
+ return false;
+ }
+ },
+ isCommandEnabled: function(command)
+ {
+ var composeHTML = gMsgCompose && gMsgCompose.composeHTML;
+
+ switch (command)
+ {
+ //File Menu
+ case "cmd_attachFile":
+ case "cmd_attachPage":
+ case "cmd_close":
+ case "cmd_save":
+ case "cmd_saveAsFile":
+ case "cmd_saveAsDraft":
+ case "cmd_saveAsTemplate":
+ case "cmd_printSetup":
+ case "cmd_printpreview":
+ case "cmd_print":
+ return !gWindowLocked;
+ case "cmd_sendButton":
+ case "cmd_sendLater":
+ case "cmd_sendWithCheck":
+ case "cmd_sendButton":
+ return !gWindowLocked && !gSendLocked;
+ case "cmd_sendNow":
+ return !gWindowLocked && !Services.io.offline && !gSendLocked;
+
+ //Edit Menu
+ case "cmd_account":
+ case "cmd_preferences":
+ return true;
+
+ //Options Menu
+ case "cmd_selectAddress":
+ return !gWindowLocked;
+ case "cmd_outputFormat":
+ return composeHTML;
+ case "cmd_quoteMessage":
+ var selectedURIs = GetSelectedMessages();
+ if (selectedURIs && selectedURIs.length > 0)
+ return true;
+ return false;
+
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function(command)
+ {
+ switch (command)
+ {
+ //File Menu
+ case "cmd_attachFile" : if (defaultController.isCommandEnabled(command)) AttachFile(); break;
+ case "cmd_attachPage" : AttachPage(); break;
+ case "cmd_close" : DoCommandClose(); break;
+ case "cmd_save" : Save(); break;
+ case "cmd_saveAsFile" : SaveAsFile(true); break;
+ case "cmd_saveAsDraft" : SaveAsDraft(); break;
+ case "cmd_saveAsTemplate" : SaveAsTemplate(); break;
+ case "cmd_sendButton" :
+ if (defaultController.isCommandEnabled(command))
+ {
+ if (Services.io.offline)
+ SendMessageLater();
+ else
+ SendMessage();
+ }
+ break;
+ case "cmd_sendNow" : if (defaultController.isCommandEnabled(command)) SendMessage(); break;
+ case "cmd_sendWithCheck" : if (defaultController.isCommandEnabled(command)) SendMessageWithCheck(); break;
+ case "cmd_sendLater" : if (defaultController.isCommandEnabled(command)) SendMessageLater(); break;
+ case "cmd_printSetup" : PrintUtils.showPageSetup(); break;
+ case "cmd_printpreview" : PrintUtils.printPreview(PrintPreviewListener); break;
+ case "cmd_print" :
+ let browser = GetCurrentEditorElement();
+ PrintUtils.printWindow(browser.outerWindowID, browser);
+ break;
+
+ //Edit Menu
+ case "cmd_account" :
+ let currentAccountKey = getCurrentAccountKey();
+ let account = MailServices.accounts.getAccount(currentAccountKey);
+ MsgAccountManager(null, account.incomingServer);
+ break;
+ case "cmd_preferences" : DoCommandPreferences(); break;
+
+ //Options Menu
+ case "cmd_selectAddress" : if (defaultController.isCommandEnabled(command)) SelectAddress(); break;
+ case "cmd_quoteMessage" : if (defaultController.isCommandEnabled(command)) QuoteSelectedMessage(); break;
+ default:
+ return;
+ }
+ },
+
+ onEvent: function(event)
+ {
+ }
+};
+
+var gAttachmentBucketController =
+{
+ supportsCommand: function(aCommand)
+ {
+ switch (aCommand)
+ {
+ case "cmd_delete":
+ case "cmd_renameAttachment":
+ case "cmd_selectAll":
+ case "cmd_openAttachment":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled: function(aCommand)
+ {
+ switch (aCommand)
+ {
+ case "cmd_delete":
+ return MessageGetNumSelectedAttachments() > 0;
+ case "cmd_renameAttachment":
+ return MessageGetNumSelectedAttachments() == 1;
+ case "cmd_selectAll":
+ return MessageHasAttachments();
+ case "cmd_openAttachment":
+ return MessageGetNumSelectedAttachments() == 1;
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function(aCommand)
+ {
+ switch (aCommand)
+ {
+ case "cmd_delete":
+ if (MessageGetNumSelectedAttachments() > 0)
+ RemoveSelectedAttachment();
+ break;
+ case "cmd_renameAttachment":
+ if (MessageGetNumSelectedAttachments() == 1)
+ RenameSelectedAttachment();
+ break;
+ case "cmd_selectAll":
+ if (MessageHasAttachments())
+ SelectAllAttachments();
+ break;
+ case "cmd_openAttachment":
+ if (MessageGetNumSelectedAttachments() == 1)
+ OpenSelectedAttachment();
+ break;
+ default:
+ return;
+ }
+ },
+
+ onEvent: function(event)
+ {
+ }
+};
+
+function QuoteSelectedMessage()
+{
+ var selectedURIs = GetSelectedMessages();
+ if (selectedURIs)
+ for (let i = 0; i < selectedURIs.length; i++)
+ gMsgCompose.quoteMessage(selectedURIs[i]);
+}
+
+function GetSelectedMessages()
+{
+ var mailWindow = gMsgCompose && Services.wm.getMostRecentWindow("mail:3pane");
+ return mailWindow && mailWindow.gFolderDisplay.selectedMessageUris;
+}
+
+function SetupCommandUpdateHandlers()
+{
+ top.controllers.appendController(defaultController);
+
+ let attachmentBucket = document.getElementById("attachmentBucket");
+ attachmentBucket.controllers.appendController(gAttachmentBucketController);
+
+ document.getElementById("optionsMenuPopup")
+ .addEventListener("popupshowing", updateOptionItems, true);
+}
+
+function UnloadCommandUpdateHandlers()
+{
+ document.getElementById("optionsMenuPopup")
+ .removeEventListener("popupshowing", updateOptionItems, true);
+
+ top.controllers.removeController(defaultController);
+
+ let attachmentBucket = document.getElementById("attachmentBucket");
+ attachmentBucket.controllers.removeController(gAttachmentBucketController);
+}
+
+function CommandUpdate_MsgCompose()
+{
+ var focusedWindow = top.document.commandDispatcher.focusedWindow;
+
+ // we're just setting focus to where it was before
+ if (focusedWindow == gLastWindowToHaveFocus) {
+ return;
+ }
+
+ gLastWindowToHaveFocus = focusedWindow;
+
+ updateComposeItems();
+}
+
+function updateComposeItems()
+{
+ try {
+ // Edit Menu
+ goUpdateCommand("cmd_rewrap");
+
+ // Insert Menu
+ if (gMsgCompose && gMsgCompose.composeHTML)
+ {
+ goUpdateCommand("cmd_renderedHTMLEnabler");
+ goUpdateCommand("cmd_decreaseFontStep");
+ goUpdateCommand("cmd_increaseFontStep");
+ goUpdateCommand("cmd_bold");
+ goUpdateCommand("cmd_italic");
+ goUpdateCommand("cmd_underline");
+ goUpdateCommand("cmd_ul");
+ goUpdateCommand("cmd_ol");
+ goUpdateCommand("cmd_indent");
+ goUpdateCommand("cmd_outdent");
+ goUpdateCommand("cmd_align");
+ goUpdateCommand("cmd_smiley");
+ }
+
+ // Options Menu
+ goUpdateCommand("cmd_spelling");
+ } catch(e) {}
+}
+
+function openEditorContextMenu(popup)
+{
+ gContextMenu = new nsContextMenu(popup);
+ if (gContextMenu.shouldDisplay)
+ {
+ // If message body context menu then focused element should be content.
+ var showPasteExtra =
+ top.document.commandDispatcher.focusedWindow == content;
+ gContextMenu.showItem("context-pasteNoFormatting", showPasteExtra);
+ gContextMenu.showItem("context-pasteQuote", showPasteExtra);
+ if (showPasteExtra)
+ {
+ goUpdateCommand("cmd_pasteNoFormatting");
+ goUpdateCommand("cmd_pasteQuote");
+ }
+ return true;
+ }
+ return false;
+}
+
+function updateEditItems()
+{
+ goUpdateCommand("cmd_pasteNoFormatting");
+ goUpdateCommand("cmd_pasteQuote");
+ goUpdateCommand("cmd_delete");
+ goUpdateCommand("cmd_renameAttachment");
+ goUpdateCommand("cmd_selectAll");
+ goUpdateCommand("cmd_openAttachment");
+ goUpdateCommand("cmd_findReplace");
+ goUpdateCommand("cmd_find");
+ goUpdateCommand("cmd_findNext");
+ goUpdateCommand("cmd_findPrev");
+}
+
+function updateOptionItems()
+{
+ goUpdateCommand("cmd_quoteMessage");
+}
+
+/**
+ * Update all the commands for sending a message to reflect their current state.
+ */
+function updateSendCommands(aHaveController) {
+ updateSendLock();
+ if (aHaveController) {
+ goUpdateCommand("cmd_sendButton");
+ goUpdateCommand("cmd_sendNow");
+ goUpdateCommand("cmd_sendLater");
+ goUpdateCommand("cmd_sendWithCheck");
+ } else {
+ goSetCommandEnabled("cmd_sendButton",
+ defaultController.isCommandEnabled("cmd_sendButton"));
+ goSetCommandEnabled("cmd_sendNow",
+ defaultController.isCommandEnabled("cmd_sendNow"));
+ goSetCommandEnabled("cmd_sendLater",
+ defaultController.isCommandEnabled("cmd_sendLater"));
+ goSetCommandEnabled("cmd_sendWithCheck",
+ defaultController.isCommandEnabled("cmd_sendWithCheck"));
+ }
+}
+
+var messageComposeOfflineQuitObserver = {
+ observe: function(aSubject, aTopic, aState) {
+ // sanity checks
+ if (aTopic == "network:offline-status-changed")
+ {
+ MessageComposeOfflineStateChanged(aState == "offline");
+ }
+ // check whether to veto the quit request (unless another observer already
+ // did)
+ else if (aTopic == "quit-application-requested" &&
+ aSubject instanceof Ci.nsISupportsPRBool &&
+ !aSubject.data)
+ aSubject.data = !ComposeCanClose();
+ }
+}
+
+function AddMessageComposeOfflineQuitObserver()
+{
+ Services.obs.addObserver(messageComposeOfflineQuitObserver,
+ "network:offline-status-changed");
+ Services.obs.addObserver(messageComposeOfflineQuitObserver,
+ "quit-application-requested");
+
+ // set the initial state of the send button
+ MessageComposeOfflineStateChanged(Services.io.offline);
+}
+
+function RemoveMessageComposeOfflineQuitObserver()
+{
+ Services.obs.removeObserver(messageComposeOfflineQuitObserver,
+ "network:offline-status-changed");
+ Services.obs.removeObserver(messageComposeOfflineQuitObserver,
+ "quit-application-requested");
+}
+
+function MessageComposeOfflineStateChanged(goingOffline)
+{
+ try {
+ var sendButton = document.getElementById("button-send");
+ var sendNowMenuItem = document.getElementById("menu_sendNow");
+
+ if (!gSavedSendNowKey) {
+ gSavedSendNowKey = sendNowMenuItem.getAttribute('key');
+ }
+
+ // don't use goUpdateCommand here ... the defaultController might not be installed yet
+ updateSendCommands(false);
+
+ if (goingOffline)
+ {
+ sendButton.label = sendButton.getAttribute('later_label');
+ sendButton.setAttribute('tooltiptext', sendButton.getAttribute('later_tooltiptext'));
+ sendNowMenuItem.removeAttribute('key');
+ }
+ else
+ {
+ sendButton.label = sendButton.getAttribute('now_label');
+ sendButton.setAttribute('tooltiptext', sendButton.getAttribute('now_tooltiptext'));
+ if (gSavedSendNowKey) {
+ sendNowMenuItem.setAttribute('key', gSavedSendNowKey);
+ }
+ }
+
+ } catch(e) {}
+}
+
+function DoCommandClose()
+{
+ if (ComposeCanClose()) {
+ // Notify the SendListener that Send has been aborted and Stopped
+ if (gMsgCompose)
+ gMsgCompose.onSendNotPerformed(null, Cr.NS_ERROR_ABORT);
+
+ // note: if we're not caching this window, this destroys it for us
+ MsgComposeCloseWindow();
+ }
+
+ return false;
+}
+
+function DoCommandPreferences()
+{
+ goPreferences('composing_messages_pane');
+}
+
+function toggleAffectedChrome(aHide)
+{
+ // chrome to toggle includes:
+ // (*) menubar
+ // (*) toolbox
+ // (*) sidebar
+ // (*) statusbar
+
+ if (!gChromeState)
+ gChromeState = {};
+
+ var statusbar = document.getElementById("status-bar");
+
+ // sidebar states map as follows:
+ // hidden => hide/show nothing
+ // collapsed => hide/show only the splitter
+ // shown => hide/show the splitter and the box
+ if (aHide)
+ {
+ // going into print preview mode
+ gChromeState.sidebar = SidebarGetState();
+ SidebarSetState("hidden");
+
+ // deal with the Status Bar
+ gChromeState.statusbarWasHidden = statusbar.hidden;
+ statusbar.hidden = true;
+ }
+ else
+ {
+ // restoring normal mode (i.e., leaving print preview mode)
+ SidebarSetState(gChromeState.sidebar);
+
+ // restore the Status Bar
+ statusbar.hidden = gChromeState.statusbarWasHidden;
+ }
+
+ // if we are unhiding and sidebar used to be there rebuild it
+ if (!aHide && gChromeState.sidebar == "visible")
+ SidebarRebuild();
+
+ getMailToolbox().hidden = aHide;
+ document.getElementById("appcontent").collapsed = aHide;
+}
+
+var PrintPreviewListener = {
+ getPrintPreviewBrowser()
+ {
+ var browser = document.getElementById("ppBrowser");
+ if (!browser)
+ {
+ browser = document.createElement("browser");
+ browser.setAttribute("id", "ppBrowser");
+ browser.setAttribute("flex", "1");
+ browser.setAttribute("disablehistory", "true");
+ browser.setAttribute("disablesecurity", "true");
+ browser.setAttribute("type", "content");
+ document.getElementById("sidebar-parent")
+ .insertBefore(browser, document.getElementById("appcontent"));
+ }
+ return browser;
+ },
+ getSourceBrowser()
+ {
+ return GetCurrentEditorElement();
+ },
+ getNavToolbox()
+ {
+ return getMailToolbox();
+ },
+ onEnter()
+ {
+ toggleAffectedChrome(true);
+ },
+ onExit()
+ {
+ document.getElementById("ppBrowser").collapsed = true;
+ toggleAffectedChrome(false);
+ }
+}
+
+function ToggleWindowLock()
+{
+ gWindowLocked = !gWindowLocked;
+ updateComposeItems();
+}
+
+/* This function will go away soon as now arguments are passed to the window using a object of type nsMsgComposeParams instead of a string */
+function GetArgs(originalData)
+{
+ var args = new Object();
+
+ if (originalData == "")
+ return null;
+
+ var data = "";
+ var separator = String.fromCharCode(1);
+
+ var quoteChar = "";
+ var prevChar = "";
+ var nextChar = "";
+ for (let i = 0; i < originalData.length; i++, prevChar = aChar)
+ {
+ var aChar = originalData.charAt(i)
+ var aCharCode = originalData.charCodeAt(i)
+ if ( i < originalData.length - 1)
+ nextChar = originalData.charAt(i + 1);
+ else
+ nextChar = "";
+
+ if (aChar == quoteChar && (nextChar == "," || nextChar == ""))
+ {
+ quoteChar = "";
+ data += aChar;
+ }
+ else if ((aCharCode == 39 || aCharCode == 34) && prevChar == "=") //quote or double quote
+ {
+ if (quoteChar == "")
+ quoteChar = aChar;
+ data += aChar;
+ }
+ else if (aChar == ",")
+ {
+ if (quoteChar == "")
+ data += separator;
+ else
+ data += aChar
+ }
+ else
+ data += aChar
+ }
+
+ var pairs = data.split(separator);
+
+ for (let i = pairs.length - 1; i >= 0; i--)
+ {
+ var pos = pairs[i].indexOf('=');
+ if (pos == -1)
+ continue;
+ var argname = pairs[i].substring(0, pos);
+ var argvalue = pairs[i].substring(pos + 1);
+ if (argvalue.charAt(0) == "'" && argvalue.charAt(argvalue.length - 1) == "'")
+ args[argname] = argvalue.substring(1, argvalue.length - 1);
+ else
+ try {
+ args[argname] = decodeURIComponent(argvalue);
+ } catch (e) {args[argname] = argvalue;}
+ // dump("[" + argname + "=" + args[argname] + "]\n");
+ }
+ return args;
+}
+
+function ComposeFieldsReady()
+{
+ //If we are in plain text, we need to set the wrap column
+ if (! gMsgCompose.composeHTML) {
+ try {
+ gMsgCompose.editor.wrapWidth = gMsgCompose.wrapLength;
+ }
+ catch (e) {
+ dump("### textEditor.wrapWidth exception text: " + e + " - failed\n");
+ }
+ }
+ CompFields2Recipients(gMsgCompose.compFields);
+ SetComposeWindowTitle();
+ enableEditableFields();
+}
+
+// checks if the passed in string is a mailto url, if it is, generates nsIMsgComposeParams
+// for the url and returns them.
+function handleMailtoArgs(mailtoUrl)
+{
+ // see if the string is a mailto url....do this by checking the first 7 characters of the string
+ if (/^mailto:/i.test(mailtoUrl))
+ {
+ // if it is a mailto url, turn the mailto url into a MsgComposeParams object....
+ var uri = Services.io.newURI(mailtoUrl);
+
+ if (uri)
+ return MailServices.compose.getParamsForMailto(uri);
+ }
+
+ return null;
+}
+/**
+ * Handle ESC keypress from composition window for
+ * notifications with close button in the
+ * attachmentNotificationBox.
+ */
+function handleEsc()
+{
+ let activeElement = document.activeElement;
+
+ // If findbar is visible and the focus is in the message body,
+ // hide it. (Focus on the findbar is handled by findbar itself).
+ let findbar = document.getElementById("FindToolbar");
+ if (findbar && !findbar.hidden && activeElement.id == "content-frame") {
+ findbar.close();
+ return;
+ }
+
+ // If there is a notification in the attachmentNotificationBox
+ // AND focus is in message body, subject field or on the notification,
+ // hide it.
+ let notification = document.getElementById("attachmentNotificationBox")
+ .currentNotification;
+ if (notification && (activeElement.id == "content-frame" ||
+ activeElement.parentNode.parentNode.id == "msgSubject" ||
+ notification.contains(activeElement) ||
+ activeElement.classList.contains("messageCloseButton"))) {
+ notification.close();
+ }
+}
+
+/**
+ * On paste or drop, we may want to modify the content before inserting it into
+ * the editor, replacing file URLs with data URLs when appropriate.
+ */
+function onPasteOrDrop(e) {
+ // For paste use e.clipboardData, for drop use e.dataTransfer.
+ let dataTransfer = ("clipboardData" in e) ? e.clipboardData : e.dataTransfer;
+
+ if (!dataTransfer.types.includes("text/html")) {
+ return;
+ }
+
+ if (!gMsgCompose.composeHTML) {
+ // We're in the plain text editor. Nothing to do here.
+ return;
+ }
+
+ let html = dataTransfer.getData("text/html");
+ let doc = (new DOMParser()).parseFromString(html, "text/html");
+ let tmpD = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ let pendingConversions = 0;
+ let needToPreventDefault = true;
+ for (let img of doc.images) {
+ if (!/^file:/i.test(img.src)) {
+ // Doesn't start with file:. Nothing to do here.
+ continue;
+ }
+
+ // This may throw if the URL is invalid for the OS.
+ let nsFile;
+ try {
+ nsFile = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getFileFromURLSpec(img.src);
+ } catch (ex) {
+ continue;
+ }
+
+ if (!nsFile.exists()) {
+ continue;
+ }
+
+ if (!tmpD.contains(nsFile)) {
+ // Not anywhere under the temp dir.
+ continue;
+ }
+
+ let contentType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromFile(nsFile);
+ if (!contentType.startsWith("image/")) {
+ continue;
+ }
+
+ // If we ever get here, we need to prevent the default paste or drop since
+ // the code below will do its own insertion.
+ if (needToPreventDefault) {
+ e.preventDefault();
+ needToPreventDefault = false;
+ }
+
+ File.createFromNsIFile(nsFile).then(function(file) {
+ if (file.lastModified < (Date.now() - 60000)) {
+ // Not put in temp in the last minute. May be something other than
+ // a copy-paste. Let's not allow that.
+ return;
+ }
+
+ let doTheInsert = function() {
+ // Now run it through sanitation to make sure there wasn't any
+ // unwanted things in the content.
+ let ParserUtils = Cc["@mozilla.org/parserutils;1"]
+ .getService(Ci.nsIParserUtils);
+ let html2 = ParserUtils.sanitize(doc.documentElement.innerHTML,
+ ParserUtils.SanitizerAllowStyle);
+ getBrowser().contentDocument.execCommand("insertHTML", false, html2);
+ }
+
+ // Everything checks out. Convert file to data URL.
+ let reader = new FileReader();
+ reader.addEventListener("load", function() {
+ let dataURL = reader.result;
+ pendingConversions--;
+ img.src = dataURL;
+ if (pendingConversions == 0) {
+ doTheInsert();
+ }
+ });
+
+ reader.addEventListener("error", function() {
+ pendingConversions--;
+ if (pendingConversions == 0) {
+ doTheInsert();
+ }
+ });
+
+ pendingConversions++;
+ reader.readAsDataURL(file);
+ });
+ }
+}
+
+function ComposeStartup(aParams)
+{
+ var params = null; // New way to pass parameters to the compose window as a nsIMsgComposeParameters object
+ var args = null; // old way, parameters are passed as a string
+ gBodyFromArgs = false;
+
+ if (aParams)
+ params = aParams;
+ else if (window.arguments && window.arguments[0]) {
+ try {
+ if (window.arguments[0] instanceof Ci.nsIMsgComposeParams)
+ params = window.arguments[0];
+ else
+ params = handleMailtoArgs(window.arguments[0]);
+ }
+ catch(ex) { dump("ERROR with parameters: " + ex + "\n"); }
+
+ // if still no dice, try and see if the params is an old fashioned list of string attributes
+ // XXX can we get rid of this yet?
+ if (!params)
+ {
+ args = GetArgs(window.arguments[0]);
+ }
+ }
+
+ // Set the document language to the preference as early as possible.
+ document.documentElement
+ .setAttribute("lang", Services.prefs.getCharPref("spellchecker.dictionary"));
+
+ var identityList = GetMsgIdentityElement();
+
+ document.addEventListener("paste", onPasteOrDrop);
+ document.addEventListener("drop", onPasteOrDrop);
+
+ if (identityList)
+ FillIdentityList(identityList);
+
+ if (!params) {
+ // This code will go away soon as now arguments are passed to the window
+ // using a object of type nsMsgComposeParams instead of a string.
+ params = Cc["@mozilla.org/messengercompose/composeparams;1"]
+ .createInstance(Ci.nsIMsgComposeParams);
+ params.composeFields = Cc["@mozilla.org/messengercompose/composefields;1"]
+ .createInstance(Ci.nsIMsgCompFields);
+
+ if (args) { //Convert old fashion arguments into params
+ var composeFields = params.composeFields;
+ if (args.bodyislink && args.bodyislink == "true")
+ params.bodyIsLink = true;
+ if (args.type)
+ params.type = args.type;
+ if (args.format) {
+ // Only use valid values.
+ if (args.format == Ci.nsIMsgCompFormat.PlainText ||
+ args.format == Ci.nsIMsgCompFormat.HTML ||
+ args.format == Ci.nsIMsgCompFormat.OppositeOfDefault)
+ params.format = args.format;
+ else if (args.format.toLowerCase().trim() == "html")
+ params.format = Ci.nsIMsgCompFormat.HTML;
+ else if (args.format.toLowerCase().trim() == "text")
+ params.format = Ci.nsIMsgCompFormat.PlainText;
+ }
+ if (args.originalMsgURI)
+ params.originalMsgURI = args.originalMsgURI;
+ if (args.preselectid)
+ params.identity = getIdentityForKey(args.preselectid);
+ if (args.from)
+ composeFields.from = args.from;
+ if (args.to)
+ composeFields.to = args.to;
+ if (args.cc)
+ composeFields.cc = args.cc;
+ if (args.bcc)
+ composeFields.bcc = args.bcc;
+ if (args.newsgroups)
+ composeFields.newsgroups = args.newsgroups;
+ if (args.subject)
+ composeFields.subject = args.subject;
+ if (args.attachment)
+ {
+ var attachmentList = args.attachment.split(",");
+ var commandLine = Cc["@mozilla.org/toolkit/command-line;1"]
+ .createInstance();
+ for (let i = 0; i < attachmentList.length; i++)
+ {
+ let attachmentStr = attachmentList[i];
+ let uri = commandLine.resolveURI(attachmentStr);
+ let attachment = Cc["@mozilla.org/messengercompose/attachment;1"]
+ .createInstance(Ci.nsIMsgAttachment);
+
+ if (uri instanceof Ci.nsIFileURL)
+ {
+ if (uri.file.exists())
+ attachment.size = uri.file.fileSize;
+ else
+ attachment = null;
+ }
+
+ // Only want to attach if a file that exists or it is not a file.
+ if (attachment)
+ {
+ attachment.url = uri.spec;
+ composeFields.addAttachment(attachment);
+ }
+ else
+ {
+ let title = sComposeMsgsBundle.getString("errorFileAttachTitle");
+ let msg = sComposeMsgsBundle.getFormattedString("errorFileAttachMessage",
+ [attachmentStr]);
+ Services.prompt.alert(null, title, msg);
+ }
+ }
+ }
+ if (args.newshost)
+ composeFields.newshost = args.newshost;
+ if (args.message) {
+ let msgFile = Cc["@mozilla.org/file/local;1"]
+ .createInstance(Ci.nsIFile);
+ if (OS.Path.dirname(args.message) == ".") {
+ let workingDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ args.message = OS.Path.join(workingDir.path, OS.Path.basename(args.message));
+ }
+ msgFile.initWithPath(args.message);
+
+ if (!msgFile.exists()) {
+ let title = sComposeMsgsBundle.getString("errorFileMessageTitle");
+ let msg = sComposeMsgsBundle.getFormattedString("errorFileMessageMessage",
+ [args.message]);
+ Services.prompt.alert(null, title, msg);
+ } else {
+ let data = "";
+ let fstream = null;
+ let cstream = null;
+
+ try {
+ fstream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Ci.nsIConverterInputStream);
+ fstream.init(msgFile, -1, 0, 0); // Open file in default/read-only mode.
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let str = {};
+ let read = 0;
+
+ do {
+ // Read as much as we can and put it in str.value.
+ read = cstream.readString(0xffffffff, str);
+ data += str.value;
+ } while (read != 0);
+ } catch (e) {
+ let title = sComposeMsgsBundle.getString("errorFileMessageTitle");
+ let msg = sComposeMsgsBundle.getFormattedString("errorLoadFileMessageMessage",
+ [args.message]);
+ Services.prompt.alert(null, title, msg);
+
+ } finally {
+ if (cstream)
+ cstream.close();
+ if (fstream)
+ fstream.close();
+ }
+
+ if (data) {
+ let pos = data.search(/\S/); // Find first non-whitespace character.
+
+ if (params.format != Ci.nsIMsgCompFormat.PlainText &&
+ (args.message.endsWith(".htm") ||
+ args.message.endsWith(".html") ||
+ data.substr(pos, 14).toLowerCase() == "<!doctype html" ||
+ data.substr(pos, 5).toLowerCase() == "<html")) {
+ // We replace line breaks because otherwise they'll be converted
+ // to <br> in nsMsgCompose::BuildBodyMessageAndSignature().
+ // Don't do the conversion if the user asked explicitly for plain
+ // text.
+ data = data.replace(/\r?\n/g, " ");
+ }
+ gBodyFromArgs = true;
+ composeFields.body = data;
+ }
+ }
+ } else if (args.body) {
+ gBodyFromArgs = true;
+ composeFields.body = args.body;
+ }
+ }
+ }
+
+ gComposeType = params.type;
+
+ // Detect correct identity when missing or mismatched.
+ // An identity with no email is likely not valid.
+ // When editing a draft, 'params.identity' is pre-populated with the identity
+ // that created the draft or the identity owning the draft folder for a
+ // "foreign", draft, see ComposeMessage() in mailCommands.js. We don't want
+ // the latter, so use the creator identity which could be null.
+ if (gComposeType == Ci.nsIMsgCompType.Draft) {
+ let creatorKey = params.composeFields.creatorIdentityKey;
+ params.identity = creatorKey ? getIdentityForKey(creatorKey) : null;
+ }
+ let from = [];
+ if (params.composeFields.from)
+ from = MailServices.headerParser
+ .parseEncodedHeader(params.composeFields.from, null);
+ from = (from.length && from[0] && from[0].email) ?
+ from[0].email.toLowerCase().trim() : null;
+ if (!params.identity || !params.identity.email ||
+ (from && !emailSimilar(from, params.identity.email))) {
+ let identities = MailServices.accounts.allIdentities;
+ let suitableCount = 0;
+
+ // Search for a matching identity.
+ if (from) {
+ for (let ident of identities) {
+ if (ident.email && from == ident.email.toLowerCase()) {
+ if (suitableCount == 0)
+ params.identity = ident;
+ suitableCount++;
+ if (suitableCount > 1)
+ break; // No need to find more, it's already not unique.
+ }
+ }
+ }
+
+ if (!params.identity || !params.identity.email) {
+ let identity = null;
+ // No preset identity and no match, so use the default account.
+ let defaultAccount = MailServices.accounts.defaultAccount;
+ if (defaultAccount) {
+ identity = defaultAccount.defaultIdentity;
+ }
+ if (!identity) {
+ // Get the first identity we have in the list.
+ let identitykey = identityList.getItemAtIndex(0).getAttribute("identitykey");
+ identity = MailServices.accounts.getIdentity(identitykey);
+ }
+ params.identity = identity;
+ }
+
+ // Warn if no or more than one match was found.
+ // But don't warn for +suffix additions (a+b@c.com).
+ if (from && (suitableCount > 1 ||
+ (suitableCount == 0 && !emailSimilar(from, params.identity.email))))
+ gComposeNotificationBar.setIdentityWarning(params.identity.identityName);
+ }
+
+ identityList.selectedItem =
+ identityList.getElementsByAttribute("identitykey", params.identity.key)[0];
+ if (params.composeFields.from)
+ identityList.value = MailServices.headerParser.parseDecodedHeader(params.composeFields.from)[0].toString();
+ LoadIdentity(true);
+
+ // Get the <editor> element to startup an editor
+ var editorElement = GetCurrentEditorElement();
+
+ // Remember the original message URI. When editing a draft which is a reply
+ // or forwarded message, this gets overwritten by the ancestor's message URI
+ // so the disposition flags ("replied" or "forwarded") can be set on the
+ // ancestor.
+ // For our purposes we need the URI of the message being processed, not its
+ // original ancestor.
+ gOriginalMsgURI = params.originalMsgURI;
+ gMsgCompose = MailServices.compose.initCompose(params, window,
+ editorElement.docShell);
+
+ document.getElementById("returnReceiptMenu")
+ .setAttribute("checked", gMsgCompose.compFields.returnReceipt);
+ document.getElementById("dsnMenu")
+ .setAttribute('checked', gMsgCompose.compFields.DSN);
+ document.getElementById("cmd_attachVCard")
+ .setAttribute("checked", gMsgCompose.compFields.attachVCard);
+ document.getElementById("menu_inlineSpellCheck")
+ .setAttribute("checked",
+ Services.prefs.getBoolPref("mail.spellcheck.inline"));
+
+ let editortype = gMsgCompose.composeHTML ? "htmlmail" : "textmail";
+ editorElement.makeEditable(editortype, true);
+
+ // setEditorType MUST be call before setContentWindow
+ if (gMsgCompose.composeHTML) {
+ initLocalFontFaceMenu(document.getElementById("FontFacePopup"));
+ } else {
+ //Remove HTML toolbar, format and insert menus as we are editing in plain
+ //text mode.
+ let toolbar = document.getElementById("FormatToolbar");
+ toolbar.hidden = true;
+ toolbar.setAttribute("hideinmenu", "true");
+ document.getElementById("outputFormatMenu").setAttribute("hidden", true);
+ document.getElementById("formatMenu").setAttribute("hidden", true);
+ document.getElementById("insertMenu").setAttribute("hidden", true);
+ }
+
+ // Do setup common to Message Composer and Web Composer.
+ EditorSharedStartup();
+
+ if (params.bodyIsLink) {
+ let body = gMsgCompose.compFields.body;
+ if (gMsgCompose.composeHTML) {
+ let cleanBody;
+ try {
+ cleanBody = decodeURI(body);
+ } catch(e) {
+ cleanBody = body;
+ }
+
+ body = body.replace(/&/g, "&amp;");
+ gMsgCompose.compFields.body =
+ "<br /><a href=\"" + body + "\">" + cleanBody + "</a><br />";
+ } else {
+ gMsgCompose.compFields.body = "\n<" + body + ">\n";
+ }
+ }
+
+ GetMsgSubjectElement().value = gMsgCompose.compFields.subject;
+
+ var attachments = gMsgCompose.compFields.attachments;
+ while (attachments.hasMoreElements()) {
+ AddAttachment(attachments.getNext().QueryInterface(Ci.nsIMsgAttachment));
+ }
+
+ var event = document.createEvent('Events');
+ event.initEvent('compose-window-init', false, true);
+ document.getElementById("msgcomposeWindow").dispatchEvent(event);
+
+ gMsgCompose.RegisterStateListener(stateListener);
+
+ // Add an observer to be called when document is done loading,
+ // which creates the editor.
+ try {
+ GetCurrentCommandManager().addCommandObserver(gMsgEditorCreationObserver,
+ "obs_documentCreated");
+
+ // Load empty page to create the editor
+ editorElement.webNavigation.loadURI("about:blank",
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
+ null, // referrer
+ null, // post-data stream
+ null, // HTTP headers
+ Services.scriptSecurityManager.getSystemPrincipal());
+ } catch (e) {
+ dump(" Failed to startup editor: "+e+"\n");
+ }
+
+ // create URI of the folder from draftId
+ var draftId = gMsgCompose.compFields.draftId;
+ var folderURI = draftId.substring(0, draftId.indexOf("#")).replace("-message", "");
+
+ try {
+ var folder = sRDF.GetResource(folderURI);
+
+ gEditingDraft = (folder instanceof Ci.nsIMsgFolder) &&
+ (folder.flags & Ci.nsMsgFolderFlags.Drafts);
+ }
+ catch (ex) {
+ gEditingDraft = false;
+ }
+
+ gAutoSaveKickedIn = false;
+
+ gAutoSaveInterval = Services.prefs.getBoolPref("mail.compose.autosave")
+ ? Services.prefs.getIntPref("mail.compose.autosaveinterval") * 60000
+ : 0;
+
+ if (gAutoSaveInterval)
+ gAutoSaveTimeout = setTimeout(AutoSave, gAutoSaveInterval);
+}
+
+function splitEmailAddress(aEmail) {
+ let at = aEmail.lastIndexOf("@");
+ return (at != -1) ? [aEmail.slice(0, at), aEmail.slice(at + 1)]
+ : [aEmail, ""];
+}
+
+// Emails are equal ignoring +suffixes (email+suffix@example.com).
+function emailSimilar(a, b) {
+ if (!a || !b)
+ return a == b;
+ a = splitEmailAddress(a.toLowerCase());
+ b = splitEmailAddress(b.toLowerCase());
+ return a[1] == b[1] && a[0].split("+", 1)[0] == b[0].split("+", 1)[0];
+}
+
+// The new, nice, simple way of getting notified when a new editor has been created
+var gMsgEditorCreationObserver =
+{
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (aTopic == "obs_documentCreated")
+ {
+ var editor = GetCurrentEditor();
+ var commandManager = GetCurrentCommandManager();
+ if (editor && commandManager == aSubject) {
+ let editorStyle = editor.QueryInterface(Ci.nsIEditorStyleSheets);
+ // We use addOverrideStyleSheet rather than addStyleSheet so that we get
+ // a synchronous load, rather than having a late-finishing async load
+ // mark our editor as modified when the user hasn't typed anything yet,
+ // but that means the sheet must not @import slow things, especially
+ // not over the network.
+ editorStyle.addOverrideStyleSheet("chrome://messenger/skin/messageQuotes.css");
+ InitEditor(editor);
+ }
+ // Now that we know this document is an editor, update commands now if
+ // the document has focus, or next time it receives focus via
+ // CommandUpdate_MsgCompose()
+ if (gLastWindowToHaveFocus == document.commandDispatcher.focusedWindow)
+ updateComposeItems();
+ else
+ gLastWindowToHaveFocus = null;
+ }
+ }
+}
+
+function WizCallback(state)
+{
+ if (state){
+ ComposeStartup(null);
+ }
+ else
+ {
+ // The account wizard is still closing so we can't close just yet
+ setTimeout(MsgComposeCloseWindow, 0);
+ }
+}
+
+function ComposeLoad()
+{
+ sComposeMsgsBundle = document.getElementById("bundle_composeMsgs");
+ sBrandBundle = document.getElementById("brandBundle");
+
+ var otherHeaders = Services.prefs.getCharPref("mail.compose.other.header");
+
+ sRDF = Cc['@mozilla.org/rdf/rdf-service;1']
+ .getService(Ci.nsIRDFService);
+ sNameProperty = sRDF.GetResource("http://home.netscape.com/NC-rdf#Name?sort=true");
+
+ AddMessageComposeOfflineQuitObserver();
+
+ if (gLogComposePerformance)
+ MailServices.compose.TimeStamp("Start initializing the compose window (ComposeLoad)", false);
+
+ msgWindow.notificationCallbacks = new nsMsgBadCertHandler();
+
+ try {
+ SetupCommandUpdateHandlers();
+ // This will do migration, or create a new account if we need to.
+ // We also want to open the account wizard if no identities are found
+ var state = verifyAccounts(WizCallback, true);
+
+ if (otherHeaders) {
+ var selectNode = document.getElementById('addressCol1#1');
+ var otherHeaders_Array = otherHeaders.split(",");
+ for (let i = 0; i < otherHeaders_Array.length; i++)
+ selectNode.appendItem(otherHeaders_Array[i] + ":", "addr_other");
+ }
+ if (state)
+ ComposeStartup(null);
+ }
+ catch (ex) {
+ Cu.reportError(ex);
+ var errorTitle = sComposeMsgsBundle.getString("initErrorDlogTitle");
+ var errorMsg = sComposeMsgsBundle.getString("initErrorDlgMessage");
+ Services.prompt.alert(window, errorTitle, errorMsg);
+
+ MsgComposeCloseWindow();
+ return;
+ }
+ if (gLogComposePerformance)
+ MailServices.compose.TimeStamp("Done with the initialization (ComposeLoad). Waiting on editor to load about:blank", false);
+
+ // Before and after callbacks for the customizeToolbar code
+ var mailToolbox = getMailToolbox();
+ mailToolbox.customizeInit = MailToolboxCustomizeInit;
+ mailToolbox.customizeDone = MailToolboxCustomizeDone;
+ mailToolbox.customizeChange = MailToolboxCustomizeChange;
+}
+
+function ComposeUnload()
+{
+ // Send notification that the window is going away completely.
+ document.getElementById("msgcomposeWindow").dispatchEvent(
+ new Event("compose-window-unload", { bubbles: false, cancelable: false }));
+
+ GetCurrentCommandManager().removeCommandObserver(gMsgEditorCreationObserver,
+ "obs_documentCreated");
+ UnloadCommandUpdateHandlers();
+
+ // Stop InlineSpellCheckerUI so personal dictionary is saved
+ EnableInlineSpellCheck(false);
+
+ EditorCleanup();
+
+ RemoveMessageComposeOfflineQuitObserver();
+
+ if (gMsgCompose)
+ gMsgCompose.UnregisterStateListener(stateListener);
+ if (gAutoSaveTimeout)
+ clearTimeout(gAutoSaveTimeout);
+ if (msgWindow) {
+ msgWindow.closeWindow();
+ msgWindow.notificationCallbacks = null;
+ }
+
+ ReleaseGlobalVariables();
+}
+
+function ComposeSetCharacterSet(aEvent)
+{
+ if (gMsgCompose)
+ SetDocumentCharacterSet(aEvent.target.getAttribute("charset"));
+ else
+ dump("Compose has not been created!\n");
+}
+
+function SetDocumentCharacterSet(aCharset)
+{
+ // Replace generic Japanese with ISO-2022-JP.
+ if (aCharset == "Japanese") {
+ aCharset = "ISO-2022-JP";
+ }
+ gMsgCompose.SetDocumentCharset(aCharset);
+ SetComposeWindowTitle();
+}
+
+function GetCharsetUIString()
+{
+ // The charset here is already the canonical charset (not an alias).
+ let charset = gMsgCompose.compFields.characterSet;
+ if (!charset)
+ return "";
+
+ if (charset.toLowerCase() != gMsgCompose.compFields.defaultCharacterSet.toLowerCase()) {
+ try {
+ return " - " + gCharsetConvertManager.getCharsetTitle(charset);
+ }
+ catch(e) { // Not a canonical charset after all...
+ Cu.reportError("Not charset title for charset=" + charset);
+ return " - " + charset;
+ }
+ }
+ return "";
+}
+
+// Add-ons can override this to customize the behavior.
+function DoSpellCheckBeforeSend()
+{
+ return Services.prefs.getBoolPref("mail.SpellCheckBeforeSend");
+}
+
+/**
+ * Handles message sending operations.
+ * @param msgType nsIMsgCompDeliverMode of the operation.
+ */
+function GenericSendMessage(msgType) {
+ var msgCompFields = gMsgCompose.compFields;
+
+ Recipients2CompFields(msgCompFields);
+ var address = GetMsgIdentityElement().value;
+ address = MailServices.headerParser.makeFromDisplayAddress(address);
+ msgCompFields.from = MailServices.headerParser.makeMimeHeader([address[0]]);
+ var subject = GetMsgSubjectElement().value;
+ msgCompFields.subject = subject;
+ Attachments2CompFields(msgCompFields);
+
+ if (msgType == Ci.nsIMsgCompDeliverMode.Now ||
+ msgType == Ci.nsIMsgCompDeliverMode.Later ||
+ msgType == Ci.nsIMsgCompDeliverMode.Background) {
+ //Do we need to check the spelling?
+ if (DoSpellCheckBeforeSend()) {
+ // We disable spellcheck for the following -subject line, attachment
+ // pane, identity and addressing widget therefore we need to explicitly
+ // focus on the mail body when we have to do a spellcheck.
+ SetMsgBodyFrameFocus();
+ window.cancelSendMessage = false;
+ window.openDialog("chrome://editor/content/EdSpellCheck.xul", "_blank",
+ "dialog,close,titlebar,modal,resizable",
+ true, true, false);
+ if (window.cancelSendMessage)
+ return;
+ }
+
+ // Strip trailing spaces and long consecutive WSP sequences from the
+ // subject line to prevent getting only WSP chars on a folded line.
+ var fixedSubject = subject.replace(/\s{74,}/g, " ")
+ .replace(/\s*$/, "");
+ if (fixedSubject != subject) {
+ subject = fixedSubject;
+ msgCompFields.subject = fixedSubject;
+ GetMsgSubjectElement().value = fixedSubject;
+ }
+
+ // Remind the person if there isn't a subject.
+ if (subject == "") {
+ if (Services.prompt.confirmEx(
+ window,
+ sComposeMsgsBundle.getString("subjectEmptyTitle"),
+ sComposeMsgsBundle.getString("subjectEmptyMessage"),
+ (Services.prompt.BUTTON_TITLE_IS_STRING *
+ Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_IS_STRING *
+ Services.prompt.BUTTON_POS_1),
+ sComposeMsgsBundle.getString("sendWithEmptySubjectButton"),
+ sComposeMsgsBundle.getString("cancelSendingButton"),
+ null, null, {value:0}) == 1) {
+ GetMsgSubjectElement().focus();
+ return;
+ }
+ }
+
+ // Check if the user tries to send a message to a newsgroup through a mail
+ // account.
+ var currentAccountKey = getCurrentAccountKey();
+ var account = MailServices.accounts.getAccount(currentAccountKey);
+ if (!account) {
+ throw "UNEXPECTED: currentAccountKey '" + currentAccountKey +
+ "' has no matching account!";
+ }
+
+ if (account.incomingServer.type != "nntp" &&
+ msgCompFields.newsgroups != "") {
+ const kDontAskAgainPref = "mail.compose.dontWarnMail2Newsgroup";
+ // Default to ask user if the pref is not set.
+ var dontAskAgain = Services.prefs.getBoolPref(kDontAskAgainPref);
+ if (!dontAskAgain) {
+ var checkbox = {value:false};
+ var okToProceed = Services.prompt.confirmCheck(
+ window,
+ sComposeMsgsBundle.getString("noNewsgroupSupportTitle"),
+ sComposeMsgsBundle.getString("recipientDlogMessage"),
+ sComposeMsgsBundle.getString("CheckMsg"),
+ checkbox);
+
+ if (!okToProceed)
+ return;
+ }
+ if (checkbox.value)
+ Services.prefs.setBoolPref(kDontAskAgainPref, true);
+
+ // Remove newsgroups to prevent news_p to be set
+ // in nsMsgComposeAndSend::DeliverMessage()
+ msgCompFields.newsgroups = "";
+ }
+
+ // Before sending the message, check what to do with HTML message,
+ // eventually abort.
+ var convert = DetermineConvertibility();
+ var action = DetermineHTMLAction(convert);
+ // Check if e-mail addresses are complete, in case user has turned off
+ // autocomplete to local domain.
+ if (!CheckValidEmailAddress(msgCompFields.to, msgCompFields.cc, msgCompFields.bcc))
+ return;
+
+ if (action == Ci.nsIMsgCompSendFormat.AskUser) {
+ var recommAction = (convert == Ci.nsIMsgCompConvertible.No)
+ ? Ci.nsIMsgCompSendFormat.AskUser
+ : Ci.nsIMsgCompSendFormat.PlainText;
+ var result2 = {action:recommAction, convertible:convert, abort:false};
+ window.openDialog("chrome://messenger/content/messengercompose/askSendFormat.xul",
+ "askSendFormatDialog", "chrome,modal,titlebar,centerscreen",
+ result2);
+ if (result2.abort)
+ return;
+ action = result2.action;
+ }
+
+ // We will remember the users "send format" decision in the address
+ // collector code (see nsAbAddressCollector::CollectAddress())
+ // by using msgCompFields.forcePlainText and
+ // msgCompFields.useMultipartAlternative to determine the
+ // nsIAbPreferMailFormat (unknown, plaintext, or html).
+ // If the user sends both, we remember html.
+ switch (action) {
+ case Ci.nsIMsgCompSendFormat.PlainText:
+ msgCompFields.forcePlainText = true;
+ msgCompFields.useMultipartAlternative = false;
+ break;
+ case Ci.nsIMsgCompSendFormat.HTML:
+ msgCompFields.forcePlainText = false;
+ msgCompFields.useMultipartAlternative = false;
+ break;
+ case Ci.nsIMsgCompSendFormat.Both:
+ msgCompFields.forcePlainText = false;
+ msgCompFields.useMultipartAlternative = true;
+ break;
+ default:
+ throw new Error("Invalid nsIMsgCompSendFormat action; action=" + action);
+ }
+ }
+
+ // Hook for extra compose pre-processing.
+ Services.obs.notifyObservers(window, "mail:composeOnSend");
+
+ var originalCharset = gMsgCompose.compFields.characterSet;
+ // Check if the headers of composing mail can be converted to a mail charset.
+ if (msgType == Ci.nsIMsgCompDeliverMode.Now ||
+ msgType == Ci.nsIMsgCompDeliverMode.Later ||
+ msgType == Ci.nsIMsgCompDeliverMode.Background ||
+ msgType == Ci.nsIMsgCompDeliverMode.Save ||
+ msgType == Ci.nsIMsgCompDeliverMode.SaveAsDraft ||
+ msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft ||
+ msgType == Ci.nsIMsgCompDeliverMode.SaveAsTemplate) {
+ var fallbackCharset = new Object;
+ // Check encoding, switch to UTF-8 if the default encoding doesn't fit
+ // and disable_fallback_to_utf8 isn't set for this encoding.
+ if (!gMsgCompose.checkCharsetConversion(getCurrentIdentity(),
+ fallbackCharset)) {
+ let disableFallback = Services.prefs
+ .getBoolPref("mailnews.disable_fallback_to_utf8." + originalCharset, false);
+ if (disableFallback)
+ msgCompFields.needToCheckCharset = false;
+ else
+ fallbackCharset.value = "UTF-8";
+ }
+
+ if (fallbackCharset &&
+ fallbackCharset.value && fallbackCharset.value != "")
+ gMsgCompose.SetDocumentCharset(fallbackCharset.value);
+ }
+ try {
+ // Just before we try to send the message, fire off the
+ // compose-send-message event for listeners such as smime so they can do
+ // any pre-security work such as fetching certificates before sending.
+ var event = document.createEvent('UIEvents');
+ event.initEvent('compose-send-message', false, true);
+ var msgcomposeWindow = document.getElementById("msgcomposeWindow");
+ msgcomposeWindow.setAttribute("msgtype", msgType);
+ msgcomposeWindow.dispatchEvent(event);
+ if (event.defaultPrevented)
+ throw Cr.NS_ERROR_ABORT;
+
+ gAutoSaving = (msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft);
+ if (!gAutoSaving) {
+ // Disable the ui if we're not auto-saving.
+ gWindowLocked = true;
+ disableEditableFields();
+ updateComposeItems();
+ } else {
+ // If we're auto saving, mark the body as not changed here, and not
+ // when the save is done, because the user might change it between now
+ // and when the save is done.
+ SetContentAndBodyAsUnmodified();
+ }
+
+ var progress = Cc["@mozilla.org/messenger/progress;1"]
+ .createInstance(Ci.nsIMsgProgress);
+ if (progress) {
+ progress.registerListener(progressListener);
+ gSendOrSaveOperationInProgress = true;
+ }
+ msgWindow.domWindow = window;
+ msgWindow.rootDocShell.allowAuth = true;
+ gMsgCompose.SendMsg(msgType, getCurrentIdentity(), getCurrentAccountKey(),
+ msgWindow, progress);
+ }
+ catch (ex) {
+ Cu.reportError("GenericSendMessage FAILED: " + ex);
+ gWindowLocked = false;
+ enableEditableFields();
+ updateComposeItems();
+ }
+ if (gMsgCompose && originalCharset != gMsgCompose.compFields.characterSet)
+ SetDocumentCharacterSet(gMsgCompose.compFields.characterSet);
+}
+
+/**
+ * Check if the given address is valid (contains a @).
+ *
+ * @param aAddress The address string to check.
+ */
+function isValidAddress(aAddress) {
+ return (aAddress.includes("@", 1) && !aAddress.endsWith("@"));
+}
+
+/**
+ * Keep the Send buttons disabled until any recipient is entered.
+ */
+function updateSendLock() {
+ gSendLocked = true;
+ if (!gMsgCompose)
+ return;
+
+ // Helper function to check for a valid list name.
+ function isValidListName(aInput) {
+ let listNames = MimeParser.parseHeaderField(aInput,
+ MimeParser.HEADER_ADDRESS);
+ return listNames.length > 0 &&
+ MailServices.ab.mailListNameExists(listNames[0].name);
+ }
+
+ const mailTypes = [ "addr_to", "addr_cc", "addr_bcc" ];
+
+ // Enable the send buttons if anything usable was entered into at least one
+ // recipient field.
+ for (let row = 1; row <= top.MAX_RECIPIENTS; row ++) {
+ let popupValue = awGetPopupElement(row).value;
+ let inputValue = awGetInputElement(row).value.trim();
+ // Check for a valid looking email address or a valid mailing list name
+ // from one of our addressbooks.
+ if ((mailTypes.includes(popupValue) &&
+ (isValidAddress(inputValue) || isValidListName(inputValue))) ||
+ ((popupValue == "addr_newsgroups") && (inputValue != ""))) {
+ gSendLocked = false;
+ break;
+ }
+ }
+}
+
+function CheckValidEmailAddress(aTo, aCC, aBCC)
+{
+ var invalidStr = null;
+ // crude check that the to, cc, and bcc fields contain at least one '@'.
+ // We could parse each address, but that might be overkill.
+ if (aTo.length > 0 && (aTo.indexOf("@") <= 0 && aTo.toLowerCase() != "postmaster" || aTo.indexOf("@") == aTo.length - 1))
+ invalidStr = aTo;
+ else if (aCC.length > 0 && (aCC.indexOf("@") <= 0 && aCC.toLowerCase() != "postmaster" || aCC.indexOf("@") == aCC.length - 1))
+ invalidStr = aCC;
+ else if (aBCC.length > 0 && (aBCC.indexOf("@") <= 0 && aBCC.toLowerCase() != "postmaster" || aBCC.indexOf("@") == aBCC.length - 1))
+ invalidStr = aBCC;
+ if (invalidStr)
+ {
+ var errorTitle = sComposeMsgsBundle.getString("addressInvalidTitle");
+ var errorMsg = sComposeMsgsBundle.getFormattedString("addressInvalid", [invalidStr], 1);
+ Services.prompt.alert(window, errorTitle, errorMsg);
+ return false;
+ }
+ return true;
+}
+
+function SendMessage()
+{
+ let sendInBackground = Services.prefs.getBoolPref("mailnews.sendInBackground");
+ if (sendInBackground && AppConstants.platform != "macosx") {
+ let enumerator = Services.wm.getEnumerator(null);
+ let count = 0;
+ while (enumerator.hasMoreElements() && count < 2)
+ {
+ enumerator.getNext();
+ count++;
+ }
+ if (count == 1)
+ sendInBackground = false;
+ }
+ GenericSendMessage(sendInBackground ? nsIMsgCompDeliverMode.Background
+ : nsIMsgCompDeliverMode.Now);
+}
+
+function SendMessageWithCheck()
+{
+ var warn = Services.prefs.getBoolPref("mail.warn_on_send_accel_key");
+
+ if (warn) {
+ var checkValue = {value:false};
+ var buttonPressed = Services.prompt.confirmEx(window,
+ sComposeMsgsBundle.getString('sendMessageCheckWindowTitle'),
+ sComposeMsgsBundle.getString('sendMessageCheckLabel'),
+ (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1),
+ sComposeMsgsBundle.getString('sendMessageCheckSendButtonLabel'),
+ null, null,
+ sComposeMsgsBundle.getString('CheckMsg'),
+ checkValue);
+ if (buttonPressed != 0) {
+ return;
+ }
+ if (checkValue.value) {
+ Services.prefs.setBoolPref("mail.warn_on_send_accel_key", false);
+ }
+ }
+
+ if (Services.io.offline)
+ SendMessageLater();
+ else
+ SendMessage();
+}
+
+function SendMessageLater()
+{
+ GenericSendMessage(nsIMsgCompDeliverMode.Later);
+}
+
+function Save()
+{
+ switch (defaultSaveOperation)
+ {
+ case "file" : SaveAsFile(false); break;
+ case "template" : SaveAsTemplate(false); break;
+ default : SaveAsDraft(false); break;
+ }
+}
+
+function SaveAsFile(saveAs)
+{
+ var subject = GetMsgSubjectElement().value;
+ GetCurrentEditorElement().contentDocument.title = subject;
+
+ if (gMsgCompose.bodyConvertible() == nsIMsgCompConvertible.Plain)
+ SaveDocument(saveAs, false, "text/plain");
+ else
+ SaveDocument(saveAs, false, "text/html");
+ defaultSaveOperation = "file";
+}
+
+function SaveAsDraft()
+{
+ GenericSendMessage(nsIMsgCompDeliverMode.SaveAsDraft);
+ defaultSaveOperation = "draft";
+
+ gAutoSaveKickedIn = false;
+ gEditingDraft = true;
+}
+
+function SaveAsTemplate()
+{
+ let savedReferences = null;
+ if (gMsgCompose && gMsgCompose.compFields) {
+ // Clear References header. When we use the template, we don't want that
+ // header, yet, "edit as new message" maintains it. So we need to clear
+ // it when saving the template.
+ // Note: The In-Reply-To header is the last entry in the references header,
+ // so it will get cleared as well.
+ savedReferences = gMsgCompose.compFields.references;
+ gMsgCompose.compFields.references = null;
+ }
+
+ GenericSendMessage(nsIMsgCompDeliverMode.SaveAsTemplate);
+ defaultSaveOperation = "template";
+
+ if (savedReferences)
+ gMsgCompose.compFields.references = savedReferences;
+
+ gAutoSaveKickedIn = false;
+ gEditingDraft = false;
+}
+
+// Sets the additional FCC, in addition to the default FCC.
+function MessageFcc(aFolder) {
+ if (!gMsgCompose)
+ return;
+
+ var msgCompFields = gMsgCompose.compFields;
+ if (!msgCompFields)
+ return;
+
+ // Get the uri for the folder to FCC into.
+ var fccURI = aFolder.URI;
+ msgCompFields.fcc2 = (msgCompFields.fcc2 == fccURI) ? "nocopy://" : fccURI;
+}
+
+function updatePriorityMenu(priorityMenu)
+{
+ var priority = (gMsgCompose && gMsgCompose.compFields && gMsgCompose.compFields.priority) || "Normal";
+ priorityMenu.getElementsByAttribute("value", priority)[0].setAttribute("checked", "true");
+}
+
+function PriorityMenuSelect(target)
+{
+ if (gMsgCompose)
+ {
+ var msgCompFields = gMsgCompose.compFields;
+ if (msgCompFields)
+ msgCompFields.priority = target.getAttribute("value");
+ }
+}
+
+function OutputFormatMenuSelect(target)
+{
+ if (gMsgCompose)
+ {
+ var msgCompFields = gMsgCompose.compFields;
+ var toolbar = document.getElementById("FormatToolbar");
+ var format_menubar = document.getElementById("formatMenu");
+ var insert_menubar = document.getElementById("insertMenu");
+
+ if (msgCompFields)
+ switch (target.getAttribute('id'))
+ {
+ case "format_auto": gSendFormat = nsIMsgCompSendFormat.AskUser; break;
+ case "format_plain": gSendFormat = nsIMsgCompSendFormat.PlainText; break;
+ case "format_html": gSendFormat = nsIMsgCompSendFormat.HTML; break;
+ case "format_both": gSendFormat = nsIMsgCompSendFormat.Both; break;
+ }
+ gHideMenus = (gSendFormat == nsIMsgCompSendFormat.PlainText);
+ format_menubar.hidden = gHideMenus;
+ insert_menubar.hidden = gHideMenus;
+ if (gHideMenus) {
+ gFormatToolbarHidden = toolbar.hidden;
+ toolbar.hidden = true;
+ toolbar.setAttribute("hideinmenu", "true");
+ } else {
+ toolbar.hidden = gFormatToolbarHidden;
+ toolbar.removeAttribute("hideinmenu");
+ }
+ }
+}
+
+function SelectAddress()
+{
+ var msgCompFields = gMsgCompose.compFields;
+
+ Recipients2CompFields(msgCompFields);
+
+ var toAddress = msgCompFields.to;
+ var ccAddress = msgCompFields.cc;
+ var bccAddress = msgCompFields.bcc;
+
+ dump("toAddress: " + toAddress + "\n");
+ window.openDialog("chrome://messenger/content/addressbook/abSelectAddressesDialog.xul",
+ "",
+ "chrome,resizable,titlebar,modal",
+ {composeWindow:top.window,
+ msgCompFields:msgCompFields,
+ toAddress:toAddress,
+ ccAddress:ccAddress,
+ bccAddress:bccAddress});
+ // We have to set focus to the addressingwidget because we seem to loose focus often
+ // after opening the SelectAddresses Dialog- bug # 89950
+ AdjustFocus();
+}
+
+// walk through the recipients list and add them to the inline spell checker ignore list
+function addRecipientsToIgnoreList(aAddressesToAdd)
+{
+ if (InlineSpellCheckerUI.enabled)
+ {
+ // break the list of potentially many recipients back into individual names
+ var emailAddresses = {};
+ var names = {};
+ var fullNames = {};
+ var numAddresses =
+ MailServices.headerParser.parseHeadersWithArray(aAddressesToAdd,
+ emailAddresses, names,
+ fullNames);
+ var tokenizedNames = [];
+
+ // each name could consist of multiple words delimited by commas and/or spaces.
+ // i.e. Green Lantern or Lantern,Green.
+ for (let i = 0; i < names.value.length; i++)
+ {
+ if (!names.value[i])
+ continue;
+ var splitNames = names.value[i].match(/[^\s,]+/g);
+ if (splitNames)
+ tokenizedNames = tokenizedNames.concat(splitNames);
+ }
+
+ if (InlineSpellCheckerUI.mInlineSpellChecker.spellCheckPending)
+ {
+ // spellchecker is enabled, but we must wait for its init to complete
+ Services.obs.addObserver(function observe(subject, topic, data) {
+ if (subject == gMsgCompose.editor)
+ {
+ Services.obs.removeObserver(observe, topic);
+ InlineSpellCheckerUI.mInlineSpellChecker.ignoreWords(tokenizedNames);
+ }
+ }, "inlineSpellChecker-spellCheck-ended");
+ }
+ else
+ {
+ InlineSpellCheckerUI.mInlineSpellChecker.ignoreWords(tokenizedNames);
+ }
+ }
+}
+
+function onAddressColCommand(aWidgetId) {
+ gContentChanged = true;
+ awSetAutoComplete(aWidgetId.slice(aWidgetId.lastIndexOf('#') + 1));
+ updateSendCommands(true);
+}
+
+/**
+ * Called if the list of recipients changed in any way.
+ *
+ * @param aAutomatic Set to true if the change of recipients was invoked
+ * programatically and should not be considered a change
+ * of message content.
+ */
+function onRecipientsChanged(aAutomatic) {
+ if (!aAutomatic) {
+ gContentChanged = true;
+ setupAutocomplete();
+ }
+ updateSendCommands(true);
+}
+
+function InitLanguageMenu()
+{
+ var languageMenuList = document.getElementById("languageMenuList");
+ if (!languageMenuList)
+ return;
+
+ var spellChecker = Cc["@mozilla.org/spellchecker/engine;1"]
+ .getService(mozISpellCheckingEngine);
+ // Get the list of dictionaries from the spellchecker.
+ var dictList = spellChecker.getDictionaryList();
+ var count = dictList.length;
+
+ // If dictionary count hasn't changed then no need to update the menu.
+ if (sDictCount == count)
+ return;
+
+ // Store current dictionary count.
+ sDictCount = count;
+
+ // Load the language string bundle that will help us map
+ // RFC 1766 strings to UI strings.
+ var languageBundle = document.getElementById("languageBundle");
+ var isoStrArray;
+ var langId;
+ var langLabel;
+
+ for (let i = 0; i < count; i++)
+ {
+ try
+ {
+ langId = dictList[i];
+ isoStrArray = dictList[i].split(/[-_]/);
+
+ if (languageBundle && isoStrArray[0])
+ langLabel = languageBundle.getString(isoStrArray[0].toLowerCase());
+
+ // the user needs to be able to distinguish between the UK English dictionary
+ // and say the United States English Dictionary. If we have a isoStr value then
+ // wrap it in parentheses and append it to the menu item string. i.e.
+ // English (US) and English (UK)
+ if (!langLabel)
+ langLabel = langId;
+ // if we have a language ID like US or UK, append it to the menu item, and any sub-variety
+ else if (isoStrArray.length > 1 && isoStrArray[1]) {
+ langLabel += ' (' + isoStrArray[1];
+ if (isoStrArray.length > 2 && isoStrArray[2])
+ langLabel += '-' + isoStrArray[2];
+ langLabel += ')';
+ }
+ }
+ catch (ex)
+ {
+ // getString throws an exception when a key is not found in the
+ // bundle. In that case, just use the original dictList string.
+ langLabel = langId;
+ }
+ dictList[i] = [langLabel, langId];
+ }
+
+ // sort by locale-aware collation
+ dictList.sort(
+ function compareFn(a, b)
+ {
+ return a[0].localeCompare(b[0]);
+ }
+ );
+
+ // Remove any languages from the list.
+ while (languageMenuList.hasChildNodes())
+ languageMenuList.lastChild.remove();
+
+ for (let i = 0; i < count; i++)
+ {
+ var item = document.createElement("menuitem");
+ item.setAttribute("label", dictList[i][0]);
+ item.setAttribute("value", dictList[i][1]);
+ item.setAttribute("type", "radio");
+ languageMenuList.appendChild(item);
+ }
+}
+
+function OnShowDictionaryMenu(aTarget)
+{
+ InitLanguageMenu();
+ var spellChecker = InlineSpellCheckerUI.mInlineSpellChecker.spellChecker;
+ var curLang = spellChecker.GetCurrentDictionary();
+ var languages = aTarget.getElementsByAttribute("value", curLang);
+ if (languages.length > 0)
+ languages[0].setAttribute("checked", true);
+}
+
+function ChangeLanguage(event)
+{
+ // We need to change the dictionary language and if we are using inline spell check,
+ // recheck the message
+ var spellChecker = InlineSpellCheckerUI.mInlineSpellChecker.spellChecker;
+ if (spellChecker.GetCurrentDictionary() != event.target.value)
+ {
+ spellChecker.SetCurrentDictionary(event.target.value);
+
+ ComposeChangeLanguage(event.target.value)
+ }
+ event.stopPropagation();
+}
+
+function ComposeChangeLanguage(aLang)
+{
+ if (document.documentElement.getAttribute("lang") != aLang) {
+
+ // Update the document language as well.
+ // This is needed to synchronize the subject.
+ document.documentElement.setAttribute("lang", aLang);
+
+ // Update spellchecker pref
+ Services.prefs.setCharPref("spellchecker.dictionary", aLang);
+
+ // Now check the document and the subject over again with the new
+ // dictionary.
+ if (InlineSpellCheckerUI.enabled) {
+ InlineSpellCheckerUI.mInlineSpellChecker.spellCheckRange(null);
+
+ // Also force a recheck of the subject. The spell checker for the subject
+ // isn't always ready yet. Usually throws unless the subject was selected
+ // at least once. So don't auto-create it, hence pass 'false'.
+ let inlineSpellChecker =
+ GetMsgSubjectElement().editor.getInlineSpellChecker(false);
+ if (inlineSpellChecker) {
+ inlineSpellChecker.spellCheckRange(null);
+ }
+ }
+ }
+}
+
+function ToggleReturnReceipt(target)
+{
+ var msgCompFields = gMsgCompose.compFields;
+ if (msgCompFields)
+ {
+ msgCompFields.returnReceipt = ! msgCompFields.returnReceipt;
+ target.setAttribute('checked', msgCompFields.returnReceipt);
+ gReceiptOptionChanged = true;
+ }
+}
+
+function ToggleDSN(target)
+{
+ var msgCompFields = gMsgCompose.compFields;
+
+ if (msgCompFields)
+ {
+ msgCompFields.DSN = !msgCompFields.DSN;
+ target.setAttribute('checked', msgCompFields.DSN);
+ gDSNOptionChanged = true;
+ }
+}
+
+function ToggleAttachVCard(target)
+{
+ var msgCompFields = gMsgCompose.compFields;
+ if (msgCompFields)
+ {
+ msgCompFields.attachVCard = ! msgCompFields.attachVCard;
+ target.setAttribute('checked', msgCompFields.attachVCard);
+ gAttachVCardOptionChanged = true;
+ }
+}
+
+function FillIdentityList(menulist)
+{
+ var accounts = FolderUtils.allAccountsSorted(true);
+
+ for (let acc = 0; acc < accounts.length; acc++)
+ {
+ let account = accounts[acc];
+ let identities = account.identities;
+
+ if (identities.length == 0)
+ continue;
+
+ for (let i = 0; i < identities.length; i++)
+ {
+ let identity = identities[i];
+ let item = menulist.appendItem(identity.identityName,
+ identity.fullAddress,
+ account.incomingServer.prettyName);
+ item.setAttribute("identitykey", identity.key);
+ item.setAttribute("accountkey", account.key);
+ if (i == 0)
+ {
+ // Mark the first identity as default.
+ item.setAttribute("default", "true");
+ }
+ }
+ }
+}
+
+function getCurrentAccountKey()
+{
+ // get the accounts key
+ var identityList = GetMsgIdentityElement();
+ return identityList.selectedItem.getAttribute("accountkey");
+}
+
+function getCurrentIdentityKey()
+{
+ // get the identity key
+ var identityList = GetMsgIdentityElement();
+ return identityList.selectedItem.getAttribute("identitykey");
+}
+
+function getIdentityForKey(key)
+{
+ return MailServices.accounts.getIdentity(key);
+}
+
+function getCurrentIdentity()
+{
+ return getIdentityForKey(getCurrentIdentityKey());
+}
+
+function AdjustFocus()
+{
+ let element = awGetInputElement(awGetNumberOfRecipients());
+ if (element.value == "") {
+ awSetFocusTo(element);
+ }
+ else
+ {
+ element = GetMsgSubjectElement();
+ if (element.value == "") {
+ element.focus();
+ }
+ else {
+ SetMsgBodyFrameFocus();
+ }
+ }
+}
+
+function SetComposeWindowTitle()
+{
+ var newTitle = GetMsgSubjectElement().value;
+
+ if (newTitle == "" )
+ newTitle = sComposeMsgsBundle.getString("defaultSubject");
+
+ newTitle += GetCharsetUIString();
+ document.title = sComposeMsgsBundle.getString("windowTitlePrefix") + " " + newTitle;
+}
+
+// Check for changes to document and allow saving before closing
+// This is hooked up to the OS's window close widget (e.g., "X" for Windows)
+function ComposeCanClose()
+{
+ if (gSendOrSaveOperationInProgress)
+ {
+ var brandShortName = sBrandBundle.getString("brandShortName");
+
+ var promptTitle = sComposeMsgsBundle.getString("quitComposeWindowTitle");
+ var promptMsg = sComposeMsgsBundle.getFormattedString("quitComposeWindowMessage2",
+ [brandShortName], 1);
+ var quitButtonLabel = sComposeMsgsBundle.getString("quitComposeWindowQuitButtonLabel2");
+ var waitButtonLabel = sComposeMsgsBundle.getString("quitComposeWindowWaitButtonLabel2");
+
+ if (Services.prompt.confirmEx(window, promptTitle, promptMsg,
+ (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1),
+ waitButtonLabel, quitButtonLabel, null, null, {value:0}) == 1)
+ {
+ gMsgCompose.abort();
+ return true;
+ }
+ return false;
+ }
+
+ // Returns FALSE only if user cancels save action
+ if (gContentChanged || gMsgCompose.bodyModified || (gAutoSaveKickedIn && !gEditingDraft))
+ {
+ // call window.focus, since we need to pop up a dialog
+ // and therefore need to be visible (to prevent user confusion)
+ window.focus();
+ let draftFolderURI = gCurrentIdentity.draftFolder;
+ let draftFolderName = MailUtils.getFolderForURI(draftFolderURI).prettyName;
+ switch (Services.prompt.confirmEx(window,
+ sComposeMsgsBundle.getString("saveDlogTitle"),
+ sComposeMsgsBundle.getFormattedString("saveDlogMessages3", [draftFolderName]),
+ (Services.prompt.BUTTON_TITLE_SAVE * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) +
+ (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_2),
+ null,
+ null,
+ sComposeMsgsBundle.getString("discardButtonLabel"),
+ null, {value:0}))
+ {
+ case 0: //Save
+ // we can close immediately if we already autosaved the draft
+ if (!gContentChanged && !gMsgCompose.bodyModified)
+ break;
+ gCloseWindowAfterSave = true;
+ GenericSendMessage(nsIMsgCompDeliverMode.AutoSaveAsDraft);
+ return false;
+ case 1: //Cancel
+ return false;
+ case 2: //Don't Save
+ // only delete the draft if we didn't start off editing a draft
+ if (!gEditingDraft && gAutoSaveKickedIn)
+ RemoveDraft();
+ break;
+ }
+ }
+
+ return true;
+}
+
+function RemoveDraft()
+{
+ try
+ {
+ var draftId = gMsgCompose.compFields.draftId;
+ var msgKey = draftId.substr(draftId.indexOf('#') + 1);
+ var folder = sRDF.GetResource(gMsgCompose.savedFolderURI);
+ try {
+ if (folder instanceof Ci.nsIMsgFolder)
+ {
+ let msg = folder.GetMessageHeader(msgKey);
+ folder.deleteMessages([msg], null, true, false, null, false);
+ }
+ }
+ catch (ex) // couldn't find header - perhaps an imap folder.
+ {
+ if (folder instanceof Ci.nsIMsgImapMailFolder)
+ {
+ const kImapMsgDeletedFlag = 0x0008;
+ folder.storeImapFlags(kImapMsgDeletedFlag, true, [msgKey], null);
+ }
+ }
+ } catch (ex) {}
+}
+
+function SetContentAndBodyAsUnmodified()
+{
+ gMsgCompose.bodyModified = false;
+ gContentChanged = false;
+}
+
+function MsgComposeCloseWindow()
+{
+ if (gMsgCompose)
+ gMsgCompose.CloseWindow();
+ else
+ window.close();
+}
+
+// attachedLocalFile must be a nsIFile
+function SetLastAttachDirectory(attachedLocalFile)
+{
+ try {
+ var file = attachedLocalFile.QueryInterface(Ci.nsIFile);
+ var parent = file.parent.QueryInterface(Ci.nsIFile);
+
+ Services.prefs.setComplexValue(kComposeAttachDirPrefName,
+ Ci.nsIFile, parent);
+ }
+ catch (ex) {
+ dump("error: SetLastAttachDirectory failed: " + ex + "\n");
+ }
+}
+
+function AttachFile()
+{
+ //Get file using nsIFilePicker and convert to URL
+ const nsIFilePicker = Ci.nsIFilePicker;
+ let fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ fp.init(window, sComposeMsgsBundle.getString("chooseFileToAttach"),
+ nsIFilePicker.modeOpenMultiple);
+ let lastDirectory = GetLocalFilePref(kComposeAttachDirPrefName);
+ if (lastDirectory)
+ fp.displayDirectory = lastDirectory;
+
+ fp.appendFilters(nsIFilePicker.filterAll);
+ fp.open(rv => {
+ if (rv != nsIFilePicker.returnOK || !fp.files) {
+ return;
+ }
+ try {
+ let firstAttachedFile = AttachFiles(fp.files);
+ if (firstAttachedFile) {
+ SetLastAttachDirectory(firstAttachedFile);
+ }
+ }
+ catch (ex) {
+ dump("failed to get attachments: " + ex + "\n");
+ }
+ });
+}
+
+function AttachFiles(attachments)
+{
+ if (!attachments || !attachments.hasMoreElements())
+ return null;
+
+ var firstAttachedFile = null;
+
+ while (attachments.hasMoreElements()) {
+ var currentFile = attachments.getNext().QueryInterface(Ci.nsIFile);
+
+ if (!firstAttachedFile) {
+ firstAttachedFile = currentFile;
+ }
+
+ var fileHandler = Services.io.getProtocolHandler("file").QueryInterface(Ci.nsIFileProtocolHandler);
+ var currentAttachment = fileHandler.getURLSpecFromFile(currentFile);
+
+ if (!DuplicateFileCheck(currentAttachment)) {
+ var attachment = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment);
+ attachment.url = currentAttachment;
+ attachment.size = currentFile.fileSize;
+ AddAttachment(attachment);
+ gContentChanged = true;
+ }
+ }
+ return firstAttachedFile;
+}
+
+function AddAttachment(attachment)
+{
+ if (attachment && attachment.url)
+ {
+ var bucket = GetMsgAttachmentElement();
+ var item = document.createElement("listitem");
+
+ if (!attachment.name)
+ attachment.name = gMsgCompose.AttachmentPrettyName(attachment.url, attachment.urlCharset);
+
+ // for security reasons, don't allow *-message:// uris to leak out
+ // we don't want to reveal the .slt path (for mailbox://), or the username or hostname
+ var messagePrefix = /^mailbox-message:|^imap-message:|^news-message:/i;
+ if (messagePrefix.test(attachment.name))
+ attachment.name = sComposeMsgsBundle.getString("messageAttachmentSafeName");
+ else {
+ // for security reasons, don't allow mail protocol uris to leak out
+ // we don't want to reveal the .slt path (for mailbox://), or the username or hostname
+ var mailProtocol = /^file:|^mailbox:|^imap:|^s?news:/i;
+ if (mailProtocol.test(attachment.name))
+ attachment.name = sComposeMsgsBundle.getString("partAttachmentSafeName");
+ }
+
+ var nameAndSize = attachment.name;
+ if (attachment.size != -1)
+ nameAndSize += " (" + gMessenger.formatFileSize(attachment.size) + ")";
+ item.setAttribute("label", nameAndSize); //use for display only
+ item.attachment = attachment; //full attachment object stored here
+ try {
+ item.setAttribute("tooltiptext", decodeURI(attachment.url));
+ } catch(e) {
+ item.setAttribute("tooltiptext", attachment.url);
+ }
+ item.setAttribute("class", "listitem-iconic");
+ item.setAttribute("image", "moz-icon:" + attachment.url);
+ item.setAttribute("crop", "center");
+ bucket.appendChild(item);
+ }
+}
+
+function SelectAllAttachments()
+{
+ var bucketList = GetMsgAttachmentElement();
+ if (bucketList)
+ bucketList.selectAll();
+}
+
+function MessageHasAttachments()
+{
+ var bucketList = GetMsgAttachmentElement();
+ if (bucketList) {
+ return (bucketList && bucketList.hasChildNodes() && (bucketList == top.document.commandDispatcher.focusedElement));
+ }
+ return false;
+}
+
+function MessageGetNumSelectedAttachments()
+{
+ var bucketList = GetMsgAttachmentElement();
+ return (bucketList) ? bucketList.selectedItems.length : 0;
+}
+
+function AttachPage()
+{
+ var params = { action: "5", url: null };
+ window.openDialog("chrome://communicator/content/openLocation.xul",
+ "_blank", "chrome,close,titlebar,modal", params);
+ if (params.url)
+ {
+ var attachment =
+ Cc["@mozilla.org/messengercompose/attachment;1"]
+ .createInstance(Ci.nsIMsgAttachment);
+ attachment.url = params.url;
+ AddAttachment(attachment);
+ }
+}
+
+function DuplicateFileCheck(FileUrl)
+{
+ var bucket = GetMsgAttachmentElement();
+ for (let i = 0; i < bucket.childNodes.length; i++)
+ {
+ let attachment = bucket.childNodes[i].attachment;
+ if (attachment)
+ {
+ if (FileUrl == attachment.url)
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function Attachments2CompFields(compFields)
+{
+ var bucket = GetMsgAttachmentElement();
+
+ //First, we need to clear all attachment in the compose fields
+ compFields.removeAttachments();
+
+ for (let i = 0; i < bucket.childNodes.length; i++)
+ {
+ let attachment = bucket.childNodes[i].attachment;
+ if (attachment)
+ compFields.addAttachment(attachment);
+ }
+}
+
+function RemoveAllAttachments()
+{
+ var child;
+ var bucket = GetMsgAttachmentElement();
+ while (bucket.hasChildNodes())
+ {
+ child = bucket.removeChild(bucket.lastChild);
+ // Let's release the attachment object hold by the node else it won't go away until the window is destroyed
+ child.attachment = null;
+ }
+}
+
+function RemoveSelectedAttachment()
+{
+ var child;
+ var bucket = GetMsgAttachmentElement();
+ if (bucket.selectedItems.length > 0) {
+ for (let i = bucket.selectedItems.length - 1; i >= 0; i--)
+ {
+ child = bucket.removeChild(bucket.selectedItems[i]);
+ // Let's release the attachment object hold by the node else it won't go away until the window is destroyed
+ child.attachment = null;
+ }
+ gContentChanged = true;
+ }
+}
+
+function RenameSelectedAttachment()
+{
+ var bucket = GetMsgAttachmentElement();
+ if (bucket.selectedItems.length != 1)
+ return; // not one attachment selected
+
+ var item = bucket.getSelectedItem(0);
+ var attachmentName = {value: item.attachment.name};
+ if (Services.prompt.prompt(
+ window,
+ sComposeMsgsBundle.getString("renameAttachmentTitle"),
+ sComposeMsgsBundle.getString("renameAttachmentMessage"),
+ attachmentName,
+ null,
+ {value: 0}))
+ {
+ var modifiedAttachmentName = attachmentName.value;
+ if (modifiedAttachmentName == "")
+ return; // name was not filled, bail out
+
+ var nameAndSize = modifiedAttachmentName;
+ if (item.attachment.size != -1)
+ nameAndSize += " (" + gMessenger.formatFileSize(item.attachment.size) + ")";
+ item.label = nameAndSize;
+ item.attachment.name = modifiedAttachmentName;
+ gContentChanged = true;
+ }
+}
+
+function FocusOnFirstAttachment()
+{
+ var bucketList = GetMsgAttachmentElement();
+
+ if (bucketList && bucketList.hasChildNodes())
+ bucketList.selectItem(bucketList.firstChild);
+}
+
+function AttachmentElementHasItems()
+{
+ var element = GetMsgAttachmentElement();
+ return element ? element.childNodes.length : 0;
+}
+
+function OpenSelectedAttachment()
+{
+ let bucket = document.getElementById("attachmentBucket");
+ if (bucket.selectedItems.length == 1) {
+ let attachmentUrl = bucket.getSelectedItem(0).attachment.url;
+
+ let messagePrefix = /^mailbox-message:|^imap-message:|^news-message:/i;
+ if (messagePrefix.test(attachmentUrl)) {
+ // We must be dealing with a forwarded attachment, treat this special.
+ let msgHdr = gMessenger.msgHdrFromURI(attachmentUrl);
+ if (msgHdr) {
+ MailUtils.openMessageInNewWindow(msgHdr);
+ }
+ } else {
+ // Turn the URL into a nsIURI object then open it.
+ let uri = Services.io.newURI(attachmentUrl);
+ if (uri) {
+ let channel = Services.io.newChannelFromURI(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+ if (channel) {
+ let uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
+ uriLoader.openURI(channel, true, new nsAttachmentOpener());
+ }
+ }
+ }
+ } // if one attachment selected
+}
+
+function nsAttachmentOpener()
+{
+}
+
+nsAttachmentOpener.prototype =
+{
+ QueryInterface: function(iid)
+ {
+ if (iid.equals(Ci.nsIURIContentListener) ||
+ iid.equals(Ci.nsIInterfaceRequestor) ||
+ iid.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ doContent: function(contentType, isContentPreferred, request, contentHandler)
+ {
+ return false;
+ },
+
+ isPreferred: function(contentType, desiredContentType)
+ {
+ return false;
+ },
+
+ canHandleContent: function(contentType, isContentPreferred, desiredContentType)
+ {
+ return false;
+ },
+
+ getInterface: function(iid)
+ {
+ if (iid.equals(Ci.nsIDOMWindow)) {
+ return window;
+ }
+
+ if (iid.equals(Ci.nsIDocShell)) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ }
+
+ return this.QueryInterface(iid);
+ },
+
+ loadCookie: null,
+ parentContentListener: null
+}
+
+function DetermineHTMLAction(convertible)
+{
+ try {
+ gMsgCompose.expandMailingLists();
+ } catch(ex) {
+ dump("gMsgCompose.expandMailingLists failed: " + ex + "\n");
+ }
+
+ if (!gMsgCompose.composeHTML)
+ {
+ return nsIMsgCompSendFormat.PlainText;
+ }
+
+ if (gSendFormat == nsIMsgCompSendFormat.AskUser)
+ {
+ return gMsgCompose.determineHTMLAction(convertible);
+ }
+
+ return gSendFormat;
+}
+
+function DetermineConvertibility()
+{
+ if (!gMsgCompose.composeHTML)
+ return nsIMsgCompConvertible.Plain;
+
+ try {
+ return gMsgCompose.bodyConvertible();
+ } catch(ex) {}
+ return nsIMsgCompConvertible.No;
+}
+
+function LoadIdentity(startup)
+{
+ var identityElement = GetMsgIdentityElement();
+ var prevIdentity = gCurrentIdentity;
+
+ if (identityElement) {
+ identityElement.value = identityElement.selectedItem.value;
+
+ var idKey = identityElement.selectedItem.getAttribute("identitykey");
+ gCurrentIdentity = MailServices.accounts.getIdentity(idKey);
+
+ let accountKey = null;
+ if (identityElement.selectedItem)
+ accountKey = identityElement.selectedItem.getAttribute("accountkey");
+
+ let maxRecipients = awGetMaxRecipients();
+ for (let i = 1; i <= maxRecipients; i++)
+ {
+ let params = JSON.parse(awGetInputElement(i).searchParam);
+ params.idKey = idKey;
+ params.accountKey = accountKey;
+ awGetInputElement(i).searchParam = JSON.stringify(params);
+ }
+
+ if (!startup && prevIdentity && idKey != prevIdentity.key)
+ {
+ var prevReplyTo = prevIdentity.replyTo;
+ var prevCc = "";
+ var prevBcc = "";
+ var prevReceipt = prevIdentity.requestReturnReceipt;
+ var prevDSN = prevIdentity.requestDSN;
+ var prevAttachVCard = prevIdentity.attachVCard;
+
+ if (prevIdentity.doCc)
+ prevCc += prevIdentity.doCcList;
+
+ if (prevIdentity.doBcc)
+ prevBcc += prevIdentity.doBccList;
+
+ var newReplyTo = gCurrentIdentity.replyTo;
+ var newCc = "";
+ var newBcc = "";
+ var newReceipt = gCurrentIdentity.requestReturnReceipt;
+ var newDSN = gCurrentIdentity.requestDSN;
+ var newAttachVCard = gCurrentIdentity.attachVCard;
+
+ if (gCurrentIdentity.doCc)
+ newCc += gCurrentIdentity.doCcList;
+
+ if (gCurrentIdentity.doBcc)
+ newBcc += gCurrentIdentity.doBccList;
+
+ var needToCleanUp = false;
+ var msgCompFields = gMsgCompose.compFields;
+
+ if (!gReceiptOptionChanged &&
+ prevReceipt == msgCompFields.returnReceipt &&
+ prevReceipt != newReceipt)
+ {
+ msgCompFields.returnReceipt = newReceipt;
+ document.getElementById("returnReceiptMenu").setAttribute('checked',msgCompFields.returnReceipt);
+ }
+
+ if (!gDSNOptionChanged &&
+ prevDSN == msgCompFields.DSN &&
+ prevDSN != newDSN)
+ {
+ msgCompFields.DSN = newDSN;
+ document.getElementById("dsnMenu").setAttribute('checked',msgCompFields.DSN);
+ }
+
+ if (!gAttachVCardOptionChanged &&
+ prevAttachVCard == msgCompFields.attachVCard &&
+ prevAttachVCard != newAttachVCard)
+ {
+ msgCompFields.attachVCard = newAttachVCard;
+ document.getElementById("cmd_attachVCard").setAttribute('checked',msgCompFields.attachVCard);
+ }
+
+ if (newReplyTo != prevReplyTo)
+ {
+ needToCleanUp = true;
+ if (prevReplyTo != "")
+ awRemoveRecipients(msgCompFields, "addr_reply", prevReplyTo);
+ if (newReplyTo != "")
+ awAddRecipients(msgCompFields, "addr_reply", newReplyTo);
+ }
+
+ let toAddrs = new Set(msgCompFields.splitRecipients(msgCompFields.to, true));
+ let ccAddrs = new Set(msgCompFields.splitRecipients(msgCompFields.cc, true));
+
+ if (newCc != prevCc)
+ {
+ needToCleanUp = true;
+ if (prevCc)
+ awRemoveRecipients(msgCompFields, "addr_cc", prevCc);
+ if (newCc) {
+ // Ensure none of the Ccs are already in To.
+ let cc2 = msgCompFields.splitRecipients(newCc, true);
+ newCc = cc2.filter(x => !toAddrs.has(x)).join(", ");
+ awAddRecipients(msgCompFields, "addr_cc", newCc);
+ }
+ }
+
+ if (newBcc != prevBcc)
+ {
+ needToCleanUp = true;
+ if (prevBcc)
+ awRemoveRecipients(msgCompFields, "addr_bcc", prevBcc);
+ if (newBcc) {
+ // Ensure none of the Bccs are already in To or Cc.
+ let bcc2 = msgCompFields.splitRecipients(newBcc, true);
+ let toCcAddrs = new Set([...toAddrs, ...ccAddrs]);
+ newBcc = bcc2.filter(x => !toCcAddrs.has(x)).join(", ");
+ awAddRecipients(msgCompFields, "addr_bcc", newBcc);
+ }
+ }
+
+ if (needToCleanUp)
+ awCleanupRows();
+
+ try {
+ gMsgCompose.identity = gCurrentIdentity;
+ } catch (ex) { dump("### Cannot change the identity: " + ex + "\n");}
+
+ var event = document.createEvent('Events');
+ event.initEvent('compose-from-changed', false, true);
+ document.getElementById("msgcomposeWindow").dispatchEvent(event);
+
+ gComposeNotificationBar.clearIdentityWarning();
+ }
+
+ if (!startup) {
+ if (Services.prefs.getBoolPref("mail.autoComplete.highlightNonMatches"))
+ document.getElementById('addressCol2#1').highlightNonMatches = true;
+
+ // Only do this if we aren't starting up...
+ // It gets done as part of startup already.
+ addRecipientsToIgnoreList(gCurrentIdentity.fullAddress);
+ }
+ }
+}
+
+function setupAutocomplete()
+{
+ var autoCompleteWidget = document.getElementById("addressCol2#1");
+
+ // if the pref is set to turn on the comment column, honor it here.
+ // this element then gets cloned for subsequent rows, so they should
+ // honor it as well
+ //
+ if (Services.prefs.getBoolPref("mail.autoComplete.highlightNonMatches"))
+ autoCompleteWidget.highlightNonMatches = true;
+
+ if (Services.prefs.getIntPref("mail.autoComplete.commentColumn", 0) != 0)
+ autoCompleteWidget.showCommentColumn = true;
+}
+
+function subjectKeyPress(event)
+{
+ switch(event.keyCode) {
+ case KeyEvent.DOM_VK_TAB:
+ if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
+ SetMsgBodyFrameFocus();
+ event.preventDefault();
+ }
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ SetMsgBodyFrameFocus();
+ break;
+ }
+}
+
+function AttachmentBucketClicked(event)
+{
+ if (event.button != 0)
+ return;
+
+ if (event.originalTarget.localName == "listboxbody")
+ goDoCommand('cmd_attachFile');
+ else if (event.originalTarget.localName == "listitem" && event.detail == 2)
+ OpenSelectedAttachment();
+}
+
+// Content types supported in the attachmentBucketObserver.
+let flavours = [ "text/x-moz-message", "application/x-moz-file",
+ "text/x-moz-url", ];
+
+var attachmentBucketObserver = {
+ onDrop(aEvent) {
+ let dt = aEvent.dataTransfer;
+ let dataList = [];
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ let types = Array.from(dt.mozTypesAt(i));
+ for (let flavour of flavours) {
+ if (types.includes(flavour)) {
+ let data = dt.mozGetDataAt(flavour, i);
+ if (data) {
+ dataList.push({ data, flavour });
+ }
+ break;
+ }
+ }
+ }
+
+ for (let { data, flavour } of dataList) {
+ let isValidAttachment = false;
+ let prettyName;
+ let size;
+
+ // We could be dropping an attachment of various flavours;
+ // check and do the right thing.
+ switch (flavour) {
+ case "application/x-moz-file": {
+ if (data instanceof Ci.nsIFile) {
+ size = data.fileSize;
+ }
+
+ try {
+ data = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler)
+ .getURLSpecFromFile(data);
+ isValidAttachment = true;
+ } catch (e) {
+ Cu.reportError("Couldn't process the dragged file " +
+ data.leafName + ":" + e);
+ }
+ break;
+ }
+
+ case "text/x-moz-message": {
+ isValidAttachment = true;
+ let msgHdr = gMessenger.messageServiceFromURI(data)
+ .messageURIToMsgHdr(data);
+ prettyName = msgHdr.mime2DecodedSubject + ".eml";
+ size = msgHdr.messageSize;
+ break;
+ }
+
+ case "text/x-moz-url": {
+ let pieces = data.split("\n");
+ data = pieces[0];
+ if (pieces.length > 1) {
+ prettyName = pieces[1];
+ }
+ if (pieces.length > 2) {
+ size = parseInt(pieces[2]);
+ }
+
+ // If this is a URL (or selected text), check if it's a valid URL
+ // by checking if we can extract a scheme using Services.io.
+ // Don't attach invalid or mailto: URLs.
+ try {
+ let scheme = Services.io.extractScheme(data);
+ if (scheme != "mailto") {
+ isValidAttachment = true;
+ }
+ } catch (ex) {}
+ break;
+ }
+ }
+
+ if (isValidAttachment && !DuplicateFileCheck(data)) {
+ let attachment = Cc["@mozilla.org/messengercompose/attachment;1"]
+ .createInstance(Ci.nsIMsgAttachment);
+ attachment.url = data;
+ attachment.name = prettyName;
+
+ if (size !== undefined) {
+ attachment.size = size;
+ }
+
+ AddAttachment(attachment);
+ }
+ }
+
+ aEvent.stopPropagation();
+ },
+
+ onDragOver(aEvent) {
+ let dragSession = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService).getCurrentSession();
+ for (let flavour of flavours) {
+ if (dragSession.isDataFlavorSupported(flavour)) {
+ let attachmentBucket = GetMsgAttachmentElement();
+ attachmentBucket.setAttribute("dragover", "true");
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ }
+ }
+ },
+
+ onDragExit(aEvent) {
+ let attachmentBucket = GetMsgAttachmentElement();
+ attachmentBucket.removeAttribute("dragover");
+ },
+};
+
+function DisplaySaveFolderDlg(folderURI)
+{
+ try
+ {
+ var showDialog = gCurrentIdentity.showSaveMsgDlg;
+ }
+ catch (e)
+ {
+ return;
+ }
+
+ if (showDialog){
+ let msgfolder = MailUtils.getFolderForURI(folderURI, true);
+ if (!msgfolder)
+ return;
+ var checkbox = {value:0};
+ var SaveDlgTitle = sComposeMsgsBundle.getString("SaveDialogTitle");
+ var dlgMsg = sComposeMsgsBundle.getFormattedString("SaveDialogMsg",
+ [msgfolder.name,
+ msgfolder.server.prettyName]);
+
+ var CheckMsg = sComposeMsgsBundle.getString("CheckMsg");
+ Services.prompt.alertCheck(window, SaveDlgTitle, dlgMsg, CheckMsg, checkbox);
+ try {
+ gCurrentIdentity.showSaveMsgDlg = !checkbox.value;
+ }//try
+ catch (e) {
+ return;
+ }//catch
+
+ }//if
+ return;
+}
+
+function SetMsgAddressingWidgetElementFocus()
+{
+ awSetFocusTo(awGetInputElement(awGetNumberOfRecipients()));
+}
+
+function SetMsgIdentityElementFocus()
+{
+ GetMsgIdentityElement().focus();
+}
+
+function SetMsgSubjectElementFocus()
+{
+ GetMsgSubjectElement().focus();
+}
+
+function SetMsgAttachmentElementFocus()
+{
+ GetMsgAttachmentElement().focus();
+ FocusOnFirstAttachment();
+}
+
+function SetMsgBodyFrameFocus()
+{
+ //window.content.focus(); fails to blur the currently focused element
+ document.commandDispatcher
+ .advanceFocusIntoSubtree(document.getElementById("appcontent"));
+}
+
+function GetMsgAddressingWidgetElement()
+{
+ if (!gMsgAddressingWidgetElement)
+ gMsgAddressingWidgetElement = document.getElementById("addressingWidget");
+
+ return gMsgAddressingWidgetElement;
+}
+
+function GetMsgIdentityElement()
+{
+ if (!gMsgIdentityElement)
+ gMsgIdentityElement = document.getElementById("msgIdentity");
+
+ return gMsgIdentityElement;
+}
+
+function GetMsgSubjectElement()
+{
+ if (!gMsgSubjectElement)
+ gMsgSubjectElement = document.getElementById("msgSubject");
+
+ return gMsgSubjectElement;
+}
+
+function GetMsgAttachmentElement()
+{
+ if (!gMsgAttachmentElement)
+ gMsgAttachmentElement = document.getElementById("attachmentBucket");
+
+ return gMsgAttachmentElement;
+}
+
+function GetMsgHeadersToolbarElement()
+{
+ if (!gMsgHeadersToolbarElement)
+ gMsgHeadersToolbarElement = document.getElementById("MsgHeadersToolbar");
+
+ return gMsgHeadersToolbarElement;
+}
+
+function IsMsgHeadersToolbarCollapsed()
+{
+ var element = GetMsgHeadersToolbarElement();
+ return element && element.collapsed;
+}
+
+function WhichElementHasFocus()
+{
+ var msgIdentityElement = GetMsgIdentityElement();
+ var msgAddressingWidgetElement = GetMsgAddressingWidgetElement();
+ var msgSubjectElement = GetMsgSubjectElement();
+ var msgAttachmentElement = GetMsgAttachmentElement();
+
+ if (top.document.commandDispatcher.focusedWindow == content)
+ return content;
+
+ var currentNode = top.document.commandDispatcher.focusedElement;
+ while (currentNode)
+ {
+ if (currentNode == msgIdentityElement ||
+ currentNode == msgAddressingWidgetElement ||
+ currentNode == msgSubjectElement ||
+ currentNode == msgAttachmentElement)
+ return currentNode;
+
+ currentNode = currentNode.parentNode;
+ }
+
+ return null;
+}
+
+// Function that performs the logic of switching focus from
+// one element to another in the mail compose window.
+// The default element to switch to when going in either
+// direction (shift or no shift key pressed), is the
+// AddressingWidgetElement.
+//
+// The only exception is when the MsgHeadersToolbar is
+// collapsed, then the focus will always be on the body of
+// the message.
+function SwitchElementFocus(event)
+{
+ var focusedElement = WhichElementHasFocus();
+
+ if (event && event.shiftKey)
+ {
+ if (IsMsgHeadersToolbarCollapsed())
+ SetMsgBodyFrameFocus();
+ else if (focusedElement == gMsgAddressingWidgetElement)
+ SetMsgIdentityElementFocus();
+ else if (focusedElement == gMsgIdentityElement)
+ SetMsgBodyFrameFocus();
+ else if (focusedElement == content)
+ {
+ // only set focus to the attachment element if there
+ // are any attachments.
+ if (AttachmentElementHasItems())
+ SetMsgAttachmentElementFocus();
+ else
+ SetMsgSubjectElementFocus();
+ }
+ else if (focusedElement == gMsgAttachmentElement)
+ SetMsgSubjectElementFocus();
+ else
+ SetMsgAddressingWidgetElementFocus();
+ }
+ else
+ {
+ if (IsMsgHeadersToolbarCollapsed())
+ SetMsgBodyFrameFocus();
+ else if (focusedElement == gMsgAddressingWidgetElement)
+ SetMsgSubjectElementFocus();
+ else if (focusedElement == gMsgSubjectElement)
+ {
+ // only set focus to the attachment element if there
+ // are any attachments.
+ if (AttachmentElementHasItems())
+ SetMsgAttachmentElementFocus();
+ else
+ SetMsgBodyFrameFocus();
+ }
+ else if (focusedElement == gMsgAttachmentElement)
+ SetMsgBodyFrameFocus();
+ else if (focusedElement == content)
+ SetMsgIdentityElementFocus();
+ else
+ SetMsgAddressingWidgetElementFocus();
+ }
+}
+
+function loadHTMLMsgPrefs()
+{
+ var fontFace = Services.prefs.getStringPref("msgcompose.font_face", "");
+ doStatefulCommand("cmd_fontFace", fontFace);
+
+ var fontSize = Services.prefs.getCharPref("msgcompose.font_size", "");
+ if (fontSize)
+ EditorSetFontSize(fontSize);
+
+ var bodyElement = GetBodyElement();
+
+ var textColor = Services.prefs.getCharPref("msgcompose.text_color", "");
+ if (!bodyElement.hasAttribute("text") && textColor)
+ {
+ bodyElement.setAttribute("text", textColor);
+ gDefaultTextColor = textColor;
+ document.getElementById("cmd_fontColor").setAttribute("state", textColor);
+ onFontColorChange();
+ }
+
+ var bgColor = Services.prefs.getCharPref("msgcompose.background_color", "");
+ if (!bodyElement.hasAttribute("bgcolor") && bgColor)
+ {
+ bodyElement.setAttribute("bgcolor", bgColor);
+ gDefaultBackgroundColor = bgColor;
+ document.getElementById("cmd_backgroundColor").setAttribute("state", bgColor);
+ onBackgroundColorChange();
+ }
+}
+
+function AutoSave()
+{
+ if (gMsgCompose.editor && (gContentChanged || gMsgCompose.bodyModified) &&
+ !gSendOrSaveOperationInProgress)
+ {
+ GenericSendMessage(nsIMsgCompDeliverMode.AutoSaveAsDraft);
+ gAutoSaveKickedIn = true;
+ }
+ gAutoSaveTimeout = setTimeout(AutoSave, gAutoSaveInterval);
+}
+
+/**
+ * Helper function to remove a query part from a URL, so for example:
+ * ...?remove=xx&other=yy becomes ...?other=yy.
+ *
+ * @param aURL the URL from which to remove the query part
+ * @param aQuery the query part to remove
+ * @return the URL with the query part removed
+ */
+function removeQueryPart(aURL, aQuery)
+{
+ // Quick pre-check.
+ if (!aURL.includes(aQuery))
+ return aURL;
+
+ let indexQM = aURL.indexOf("?");
+ if (indexQM < 0)
+ return aURL;
+
+ let queryParts = aURL.substr(indexQM + 1).split("&");
+ let indexPart = queryParts.indexOf(aQuery);
+ if (indexPart < 0)
+ return aURL;
+ queryParts.splice(indexPart, 1);
+ return aURL.substr(0, indexQM + 1) + queryParts.join("&");
+}
+
+function InitEditor(editor)
+{
+ // Set the eEditorMailMask flag to avoid using content prefs for the spell
+ // checker, otherwise the dictionary setting in preferences is ignored and
+ // the dictionary is inconsistent between the subject and message body.
+ var eEditorMailMask = Ci.nsIEditor.eEditorMailMask;
+ editor.flags |= eEditorMailMask;
+ GetMsgSubjectElement().editor.flags |= eEditorMailMask;
+
+ // Control insertion of line breaks.
+ editor.returnInParagraphCreatesNewParagraph =
+ Services.prefs.getBoolPref("mail.compose.default_to_paragraph") ||
+ Services.prefs.getBoolPref("editor.CR_creates_new_p");
+ editor.document.execCommand("defaultparagraphseparator", false,
+ gMsgCompose.composeHTML &&
+ Services.prefs.getBoolPref("mail.compose.default_to_paragraph") ?
+ "p" : "br");
+
+ gMsgCompose.initEditor(editor, window.content);
+ InlineSpellCheckerUI.init(editor);
+ EnableInlineSpellCheck(Services.prefs.getBoolPref("mail.spellcheck.inline"));
+ document.getElementById("menu_inlineSpellCheck").setAttribute("disabled", !InlineSpellCheckerUI.canSpellCheck);
+
+ // Listen for spellchecker changes, set the document language to the
+ // dictionary picked by the user via the right-click menu in the editor.
+ document.addEventListener("spellcheck-changed", updateDocumentLanguage);
+
+ // XXX: the error event fires twice for each load. Why??
+ editor.document.body.addEventListener("error", function(event) {
+ if (event.target.localName != "img") {
+ return;
+ }
+
+ if (event.target.getAttribute("moz-do-not-send") == "true") {
+ return;
+ }
+
+ let src = event.target.src;
+
+ if (!src) {
+ return;
+ }
+
+ if (!/^file:/i.test(src)) {
+ // Check if this is a protocol that can fetch parts.
+ let protocol = src.substr(0, src.indexOf(":")).toLowerCase();
+ if (!(Services.io.getProtocolHandler(protocol) instanceof
+ Ci.nsIMsgMessageFetchPartService)) {
+ // Can't fetch parts, don't try to load.
+ return;
+ }
+ }
+
+ if (event.target.classList.contains("loading-internal")) {
+ // We're already loading this, or tried so unsuccesfully.
+ return;
+ }
+
+ if (gOriginalMsgURI) {
+ let msgSvc = Cc["@mozilla.org/messenger;1"]
+ .createInstance(Ci.nsIMessenger)
+ .messageServiceFromURI(gOriginalMsgURI);
+ let originalMsgNeckoURI = msgSvc.getUrlForUri(gOriginalMsgURI);
+
+ if (src.startsWith(removeQueryPart(originalMsgNeckoURI.spec,
+ "type=application/x-message-display"))) {
+ // Reply/Forward/Edit Draft/Edit as New can contain references to
+ // images in the original message. Load those and make them data: URLs
+ // now.
+ event.target.classList.add("loading-internal");
+ try {
+ loadBlockedImage(src);
+ } catch (e) {
+ // Couldn't load the referenced image.
+ Cu.reportError(e);
+ }
+ }
+ else {
+ // Appears to reference a random message. Notify and keep blocking.
+ gComposeNotificationBar.setBlockedContent(src);
+ }
+ }
+ else {
+ // For file:, and references to parts of random messages, show the
+ // blocked content notification.
+ gComposeNotificationBar.setBlockedContent(src);
+ }
+ }, true);
+
+ // Convert mailnews URL back to data: URL.
+ let background = editor.document.body.background;
+ if (background && gOriginalMsgURI) {
+ // Check that background has the same URL as the message itself.
+ let msgSvc = Cc["@mozilla.org/messenger;1"]
+ .createInstance(Ci.nsIMessenger)
+ .messageServiceFromURI(gOriginalMsgURI);
+ let originalMsgNeckoURI = msgSvc.getUrlForUri(gOriginalMsgURI);
+
+ if (background.startsWith(
+ removeQueryPart(originalMsgNeckoURI.spec,
+ "type=application/x-message-display"))) {
+ try {
+ editor.document.body.background = loadBlockedImage(background, true);
+ } catch (e) {
+ // Couldn't load the referenced image.
+ Cu.reportError(e);
+ }
+ }
+ }
+}
+
+/**
+ * The event listener for the "spellcheck-changed" event updates
+ * the document language.
+ */
+function updateDocumentLanguage(event)
+{
+ document.documentElement.setAttribute("lang", event.detail.dictionary);
+}
+
+function EnableInlineSpellCheck(aEnableInlineSpellCheck)
+{
+ InlineSpellCheckerUI.enabled = aEnableInlineSpellCheck;
+ GetMsgSubjectElement().setAttribute("spellcheck", aEnableInlineSpellCheck);
+}
+
+function getMailToolbox()
+{
+ return document.getElementById("compose-toolbox");
+}
+
+function MailToolboxCustomizeInit()
+{
+ if (document.commandDispatcher.focusedWindow == content)
+ window.focus();
+ disableEditableFields();
+ GetMsgHeadersToolbarElement().setAttribute("moz-collapsed", true);
+ document.getElementById("compose-toolbar-sizer").setAttribute("moz-collapsed", true);
+ document.getElementById("content-frame").setAttribute("moz-collapsed", true);
+ toolboxCustomizeInit("mail-menubar");
+}
+
+function MailToolboxCustomizeDone(aToolboxChanged)
+{
+ toolboxCustomizeDone("mail-menubar", getMailToolbox(), aToolboxChanged);
+ GetMsgHeadersToolbarElement().removeAttribute("moz-collapsed");
+ document.getElementById("compose-toolbar-sizer").removeAttribute("moz-collapsed");
+ document.getElementById("content-frame").removeAttribute("moz-collapsed");
+ enableEditableFields();
+ SetMsgBodyFrameFocus();
+}
+
+function MailToolboxCustomizeChange(aEvent)
+{
+ toolboxCustomizeChange(getMailToolbox(), aEvent);
+}
+
+/**
+ * Object to handle message related notifications that are showing in a
+ * notificationbox below the composed message content.
+ */
+var gComposeNotificationBar = {
+
+ get notificationBar() {
+ delete this.notificationBar;
+ return this.notificationBar = document.getElementById("attachmentNotificationBox");
+ },
+
+ setBlockedContent: function(aBlockedURI) {
+ let brandName = sBrandBundle.getString("brandShortName");
+ let buttonLabel = sComposeMsgsBundle.getString("blockedContentPrefLabel");
+ let buttonAccesskey = sComposeMsgsBundle.getString("blockedContentPrefAccesskey");
+
+ let buttons = [{
+ label: buttonLabel,
+ accessKey: buttonAccesskey,
+ popup: "blockedContentOptions",
+ callback: function(aNotification, aButton) {
+ return true; // keep notification open
+ }
+ }];
+
+ // The popup value is a space separated list of all the blocked urls.
+ let popup = document.getElementById("blockedContentOptions");
+ let urls = popup.value ? popup.value.split(" ") : [];
+ if (!urls.includes(aBlockedURI)) {
+ urls.push(aBlockedURI);
+ }
+ popup.value = urls.join(" ");
+
+ let msg = sComposeMsgsBundle.getFormattedString("blockedContentMessage",
+ [brandName, brandName]);
+ msg = PluralForm.get(urls.length, msg);
+
+ if (!this.isShowingBlockedContentNotification()) {
+ this.notificationBar
+ .appendNotification(msg, "blockedContent", null,
+ this.notificationBar.PRIORITY_WARNING_MEDIUM,
+ buttons);
+ }
+ else {
+ this.notificationBar.getNotificationWithValue("blockedContent")
+ .setAttribute("label", msg);
+ }
+ },
+
+ isShowingBlockedContentNotification: function() {
+ return !!this.notificationBar.getNotificationWithValue("blockedContent");
+ },
+
+ clearBlockedContentNotification: function() {
+ this.notificationBar.removeNotification(
+ this.notificationBar.getNotificationWithValue("blockedContent"));
+ },
+
+ clearNotifications: function(aValue) {
+ this.notificationBar.removeAllNotifications(true);
+ },
+
+ setIdentityWarning: function(aIdentityName) {
+ if (!this.notificationBar.getNotificationWithValue("identityWarning")) {
+ let text = sComposeMsgsBundle.getString("identityWarning").split("%S");
+ let label = new DocumentFragment();
+ label.appendChild(document.createTextNode(text[0]));
+ label.appendChild(document.createElement("b"));
+ label.lastChild.appendChild(document.createTextNode(aIdentityName));
+ label.appendChild(document.createTextNode(text[1]));
+ this.notificationBar.appendNotification(label, "identityWarning", null,
+ this.notificationBar.PRIORITY_WARNING_HIGH, null);
+ }
+ },
+
+ clearIdentityWarning: function() {
+ let idWarning = this.notificationBar.getNotificationWithValue("identityWarning");
+ if (idWarning)
+ this.notificationBar.removeNotification(idWarning);
+ }
+};
+
+/**
+ * Populate the menuitems of what blocked content to unblock.
+ */
+function onBlockedContentOptionsShowing(aEvent) {
+ let urls = aEvent.target.value ? aEvent.target.value.split(" ") : [];
+
+ // Out with the old...
+ let childNodes = aEvent.target.childNodes;
+ for (let i = childNodes.length - 1; i >= 0; i--) {
+ childNodes[i].remove();
+ }
+
+ // ... and in with the new.
+ for (let url of urls) {
+ let menuitem = document.createElement("menuitem");
+ let fString = sComposeMsgsBundle.getFormattedString("blockedAllowResource",
+ [url]);
+ menuitem.setAttribute("label", fString);
+ menuitem.setAttribute("crop", "center");
+ menuitem.setAttribute("value", url);
+ menuitem.setAttribute("oncommand",
+ "onUnblockResource(this.value, this.parentNode);");
+ aEvent.target.appendChild(menuitem);
+ }
+}
+
+/**
+ * Handle clicking the "Load <url>" in the blocked content notification bar.
+ * @param {String} aURL - the URL that was unblocked
+ * @param {Node} aNode - the node holding as value the URLs of the blocked
+ * resources in the message (space separated).
+ */
+function onUnblockResource(aURL, aNode) {
+ try {
+ loadBlockedImage(aURL);
+ } catch (e) {
+ // Couldn't load the referenced image.
+ Cu.reportError(e);
+ } finally {
+ // Remove it from the list on success and failure.
+ let urls = aNode.value.split(" ");
+ for (let i = 0; i < urls.length; i++) {
+ if (urls[i] == aURL) {
+ urls.splice(i, 1);
+ aNode.value = urls.join(" ");
+ if (urls.length == 0) {
+ gComposeNotificationBar.clearBlockedContentNotification();
+ }
+ break;
+ }
+ }
+ }
+}
+
+/**
+ * Convert the blocked content to a data URL and swap the src to that for the
+ * elements that were using it.
+ *
+ * @param {String} aURL - (necko) URL to unblock
+ * @param {Bool} aReturnDataURL - return data: URL instead of processing image
+ * @return {String} the image as data: URL.
+ * @throw Error() if reading the data failed
+ */
+function loadBlockedImage(aURL, aReturnDataURL = false) {
+ let filename;
+ if (/^(file|chrome):/i.test(aURL)) {
+ filename = aURL.substr(aURL.lastIndexOf("/") + 1);
+ }
+ else {
+ let fnMatch = /[?&;]filename=([^?&]+)/.exec(aURL);
+ filename = (fnMatch && fnMatch[1]) || "";
+ }
+
+ filename = decodeURIComponent(filename);
+ let uri = Services.io.newURI(aURL);
+ let contentType;
+ if (filename) {
+ try {
+ contentType = Cc["@mozilla.org/mime;1"]
+ .getService(Ci.nsIMIMEService)
+ .getTypeFromURI(uri);
+ } catch (ex) {
+ contentType = "image/png";
+ }
+
+ if (!contentType.startsWith("image/")) {
+ // Unsafe to unblock this. It would just be garbage either way.
+ throw new Error("Won't unblock; URL=" + aURL +
+ ", contentType=" + contentType);
+ }
+ }
+ else {
+ // Assuming image/png is the best we can do.
+ contentType = "image/png";
+ }
+
+ let channel =
+ Services.io.newChannelFromURI(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER);
+
+ let inputStream = channel.open();
+ let stream = Cc["@mozilla.org/binaryinputstream;1"]
+ .createInstance(Ci.nsIBinaryInputStream);
+ stream.setInputStream(inputStream);
+ let streamData = "";
+ try {
+ while (stream.available() > 0) {
+ streamData += stream.readBytes(stream.available());
+ }
+ } catch(e) {
+ stream.close();
+ throw new Error("Couln't read all data from URL=" + aURL + " (" + e +")");
+ }
+ stream.close();
+
+ let encoded = btoa(streamData);
+ let dataURL = "data:" + contentType +
+ (filename ? ";filename=" + encodeURIComponent(filename) : "") +
+ ";base64," + encoded;
+
+ if (aReturnDataURL) {
+ return dataURL;
+ }
+
+ let editor = GetCurrentEditor();
+ for (let img of editor.document.images) {
+ if (img.src == aURL) {
+ img.src = dataURL; // Swap to data URL.
+ img.classList.remove("loading-internal");
+ }
+ }
+}
diff --git a/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js b/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js
new file mode 100644
index 0000000000..38cd1f5ecc
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js
@@ -0,0 +1,1167 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+top.MAX_RECIPIENTS = 1; /* for the initial listitem created in the XUL */
+
+var inputElementType = "";
+var selectElementType = "";
+var selectElementIndexTable = null;
+
+var gNumberOfCols = 0;
+
+var gDragService = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService);
+
+/**
+ * global variable inherited from MsgComposeCommands.js
+ *
+ var gMsgCompose;
+ */
+
+function awGetMaxRecipients()
+{
+ return top.MAX_RECIPIENTS;
+}
+
+function awGetNumberOfCols()
+{
+ if (gNumberOfCols == 0)
+ {
+ var listbox = document.getElementById('addressingWidget');
+ var listCols = listbox.getElementsByTagName('listcol');
+ gNumberOfCols = listCols.length;
+ if (!gNumberOfCols)
+ gNumberOfCols = 1; /* if no cols defined, that means we have only one! */
+ }
+
+ return gNumberOfCols;
+}
+
+function awInputElementName()
+{
+ if (inputElementType == "")
+ inputElementType = document.getElementById("addressCol2#1").localName;
+ return inputElementType;
+}
+
+function awSelectElementName()
+{
+ if (selectElementType == "")
+ selectElementType = document.getElementById("addressCol1#1").localName;
+ return selectElementType;
+}
+
+// TODO: replace awGetSelectItemIndex with recipient type index constants
+
+function awGetSelectItemIndex(itemData)
+{
+ if (selectElementIndexTable == null)
+ {
+ selectElementIndexTable = new Object();
+ var selectElem = document.getElementById("addressCol1#1");
+ for (var i = 0; i < selectElem.childNodes[0].childNodes.length; i ++)
+ {
+ var aData = selectElem.childNodes[0].childNodes[i].getAttribute("value");
+ selectElementIndexTable[aData] = i;
+ }
+ }
+ return selectElementIndexTable[itemData];
+}
+
+function Recipients2CompFields(msgCompFields)
+{
+ if (!msgCompFields) {
+ throw new Error("Message Compose Error: msgCompFields is null (ExtractRecipients)");
+ return;
+ }
+
+ var i = 1;
+ var addrTo = "";
+ var addrCc = "";
+ var addrBcc = "";
+ var addrReply = "";
+ var addrNg = "";
+ var addrFollow = "";
+ var to_Sep = "";
+ var cc_Sep = "";
+ var bcc_Sep = "";
+ var reply_Sep = "";
+ var ng_Sep = "";
+ var follow_Sep = "";
+
+ var recipientType;
+ var inputField;
+ var fieldValue;
+ var recipient;
+ while ((inputField = awGetInputElement(i)))
+ {
+ fieldValue = inputField.value;
+
+ if (fieldValue != "")
+ {
+ recipientType = awGetPopupElement(i).value;
+ recipient = null;
+
+ switch (recipientType)
+ {
+ case "addr_to" :
+ case "addr_cc" :
+ case "addr_bcc" :
+ case "addr_reply" :
+ try {
+ let headerParser = MailServices.headerParser;
+ recipient =
+ headerParser.makeFromDisplayAddress(fieldValue)
+ .map(fullValue => headerParser.makeMimeAddress(
+ fullValue.name,
+ fullValue.email))
+ .join(", ");
+ } catch (ex) {
+ recipient = fieldValue;
+ }
+ break;
+ }
+
+ switch (recipientType)
+ {
+ case "addr_to" : addrTo += to_Sep + recipient; to_Sep = ","; break;
+ case "addr_cc" : addrCc += cc_Sep + recipient; cc_Sep = ","; break;
+ case "addr_bcc" : addrBcc += bcc_Sep + recipient; bcc_Sep = ","; break;
+ case "addr_reply" : addrReply += reply_Sep + recipient; reply_Sep = ","; break;
+ case "addr_newsgroups" : addrNg += ng_Sep + fieldValue; ng_Sep = ","; break;
+ case "addr_followup" : addrFollow += follow_Sep + fieldValue; follow_Sep = ","; break;
+ case "addr_other":
+ let headerName = awGetPopupElement(i).label;
+ headerName = headerName.substring(0, headerName.indexOf(':'));
+ msgCompFields.setRawHeader(headerName, fieldValue, null);
+ break;
+ }
+ }
+ i ++;
+ }
+
+ msgCompFields.to = addrTo;
+ msgCompFields.cc = addrCc;
+ msgCompFields.bcc = addrBcc;
+ msgCompFields.replyTo = addrReply;
+ msgCompFields.newsgroups = addrNg;
+ msgCompFields.followupTo = addrFollow;
+}
+
+function CompFields2Recipients(msgCompFields)
+{
+ if (msgCompFields) {
+ var listbox = document.getElementById('addressingWidget');
+ var newListBoxNode = listbox.cloneNode(false);
+ var listBoxColsClone = listbox.firstChild.cloneNode(true);
+ newListBoxNode.appendChild(listBoxColsClone);
+ let templateNode = listbox.querySelector("listitem");
+ // dump("replacing child in comp fields 2 recips \n");
+ listbox.parentNode.replaceChild(newListBoxNode, listbox);
+
+ top.MAX_RECIPIENTS = 0;
+ var msgReplyTo = msgCompFields.replyTo;
+ var msgTo = msgCompFields.to;
+ var msgCC = msgCompFields.cc;
+ var msgBCC = msgCompFields.bcc;
+ var msgNewsgroups = msgCompFields.newsgroups;
+ var msgFollowupTo = msgCompFields.followupTo;
+ var havePrimaryRecipient = false;
+ if (msgReplyTo)
+ awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgReplyTo, false),
+ "addr_reply", newListBoxNode, templateNode);
+ if (msgTo) {
+ var rcp = msgCompFields.splitRecipients(msgTo, false);
+ if (rcp.length)
+ {
+ awSetInputAndPopupFromArray(rcp, "addr_to", newListBoxNode, templateNode);
+ havePrimaryRecipient = true;
+ }
+ }
+ if (msgCC)
+ awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgCC, false),
+ "addr_cc", newListBoxNode, templateNode);
+ if (msgBCC)
+ awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgBCC, false),
+ "addr_bcc", newListBoxNode, templateNode);
+ if (msgNewsgroups) {
+ awSetInputAndPopup(msgNewsgroups, "addr_newsgroups", newListBoxNode, templateNode);
+ havePrimaryRecipient = true;
+ }
+ if(msgFollowupTo)
+ awSetInputAndPopup(msgFollowupTo, "addr_followup", newListBoxNode, templateNode);
+
+ // If it's a new message, we need to add an extra empty recipient.
+ if (!havePrimaryRecipient)
+ _awSetInputAndPopup("", "addr_to", newListBoxNode, templateNode);
+ awFitDummyRows(2);
+
+ // CompFields2Recipients is called whenever a user replies or edits an existing message.
+ // We want to add all of the recipients for this message to the ignore list for spell check
+ let currentAddress = gCurrentIdentity ? gCurrentIdentity.fullAddress : "";
+ addRecipientsToIgnoreList([currentAddress,msgTo,msgCC,msgBCC].filter(adr => adr).join(", "));
+ }
+}
+
+function awSetInputAndPopupId(inputElem, popupElem, rowNumber)
+{
+ popupElem.id = "addressCol1#" + rowNumber;
+ inputElem.id = "addressCol2#" + rowNumber;
+ inputElem.setAttribute("aria-labelledby", popupElem.id);
+}
+
+function awSetInputAndPopupValue(inputElem, inputValue, popupElem, popupValue, rowNumber)
+{
+ inputElem.value = inputValue.trimLeft();
+
+ popupElem.selectedItem = popupElem.childNodes[0].childNodes[awGetSelectItemIndex(popupValue)];
+
+ if (rowNumber >= 0)
+ awSetInputAndPopupId(inputElem, popupElem, rowNumber);
+
+ _awSetAutoComplete(popupElem, inputElem);
+
+ onRecipientsChanged(true);
+}
+
+function _awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode)
+{
+ top.MAX_RECIPIENTS++;
+
+ var newNode = templateNode.cloneNode(true);
+ parentNode.appendChild(newNode); // we need to insert the new node before we set the value of the select element!
+
+ var input = newNode.getElementsByTagName(awInputElementName());
+ var select = newNode.getElementsByTagName(awSelectElementName());
+
+ if (input && input.length == 1 && select && select.length == 1)
+ awSetInputAndPopupValue(input[0], inputValue, select[0], popupValue, top.MAX_RECIPIENTS)
+}
+
+function awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode)
+{
+ if ( inputValue && popupValue )
+ {
+ var addressArray = inputValue.split(",");
+
+ for ( var index = 0; index < addressArray.length; index++ )
+ _awSetInputAndPopup(addressArray[index], popupValue, parentNode, templateNode);
+ }
+}
+
+function awSetInputAndPopupFromArray(inputArray, popupValue, parentNode, templateNode)
+{
+ if (popupValue)
+ {
+ for (let recipient of inputArray)
+ _awSetInputAndPopup(recipient, popupValue, parentNode, templateNode);
+ }
+}
+
+function awRemoveRecipients(msgCompFields, recipientType, recipientsList)
+{
+ if (!msgCompFields)
+ return;
+
+ var recipientArray = msgCompFields.splitRecipients(recipientsList, false);
+
+ for (var index = 0; index < recipientArray.length; index++)
+ for (var row = 1; row <= top.MAX_RECIPIENTS; row ++)
+ {
+ var popup = awGetPopupElement(row);
+ if (popup.value == recipientType) {
+ var input = awGetInputElement(row);
+ if (input.value == recipientArray[index])
+ {
+ awSetInputAndPopupValue(input, "", popup, "addr_to", -1);
+ break;
+ }
+ }
+ }
+}
+
+function awAddRecipients(msgCompFields, recipientType, recipientsList)
+{
+ if (!msgCompFields)
+ return;
+
+ var recipientArray = msgCompFields.splitRecipients(recipientsList, false);
+
+ for (var index = 0; index < recipientArray.length; index++)
+ awAddRecipient(recipientType, recipientArray[index]);
+}
+
+// this was broken out of awAddRecipients so it can be re-used...adds a new row matching recipientType and
+// drops in the single address.
+function awAddRecipient(recipientType, address)
+{
+ for (var row = 1; row <= top.MAX_RECIPIENTS; row ++)
+ {
+ if (awGetInputElement(row).value == "")
+ break;
+ }
+
+ if (row > top.MAX_RECIPIENTS)
+ awAppendNewRow(false);
+
+ awSetInputAndPopupValue(awGetInputElement(row), address, awGetPopupElement(row), recipientType, row);
+
+ /* be sure we still have an empty row left at the end */
+ if (row == top.MAX_RECIPIENTS)
+ {
+ awAppendNewRow(true);
+ awSetInputAndPopupValue(awGetInputElement(top.MAX_RECIPIENTS), "", awGetPopupElement(top.MAX_RECIPIENTS), recipientType, top.MAX_RECIPIENTS);
+ }
+
+ // add the recipient to our spell check ignore list
+ addRecipientsToIgnoreList(address);
+}
+
+function awTestRowSequence()
+{
+ /*
+ This function is for debug and testing purpose only, normal users should not run it!
+
+ Everytime we insert or delete a row, we must be sure we didn't break the ID sequence of
+ the addressing widget rows. This function will run a quick test to see if the sequence still ok
+
+ You need to define the pref mail.debug.test_addresses_sequence to true in order to activate it
+ */
+
+ var test_sequence;
+ if (Services.prefs.getPrefType("mail.debug.test_addresses_sequence") == Ci.nsIPrefBranch.PREF_BOOL)
+ test_sequence = Services.prefs.getBoolPref("mail.debug.test_addresses_sequence");
+ if (!test_sequence)
+ return true;
+
+ /* debug code to verify the sequence still good */
+
+ var listbox = document.getElementById('addressingWidget');
+ var listitems = listbox.getElementsByTagName('listitem');
+ if (listitems.length >= top.MAX_RECIPIENTS )
+ {
+ for (var i = 1; i <= listitems.length; i ++)
+ {
+ var item = listitems [i - 1];
+ let inputID = item.querySelector(awInputElementName()).id.split("#")[1];
+ let popupID = item.querySelector(awSelectElementName()).id.split("#")[1];
+ if (inputID != i || popupID != i)
+ {
+ dump("#ERROR: sequence broken at row " + i + ", inputID=" + inputID + ", popupID=" + popupID + "\n");
+ return false;
+ }
+ dump("---SEQUENCE OK---\n");
+ return true;
+ }
+ }
+ else
+ dump("#ERROR: listitems.length(" + listitems.length + ") < top.MAX_RECIPIENTS(" + top.MAX_RECIPIENTS + ")\n");
+
+ return false;
+}
+
+function awCleanupRows()
+{
+ var maxRecipients = top.MAX_RECIPIENTS;
+ var rowID = 1;
+
+ for (var row = 1; row <= maxRecipients; row ++)
+ {
+ var inputElem = awGetInputElement(row);
+ if (inputElem.value == "" && row < maxRecipients)
+ awRemoveRow(awGetRowByInputElement(inputElem));
+ else
+ {
+ awSetInputAndPopupId(inputElem, awGetPopupElement(row), rowID);
+ rowID ++;
+ }
+ }
+
+ awTestRowSequence();
+}
+
+function awDeleteRow(rowToDelete)
+{
+ /* When we delete a row, we must reset the id of others row in order to not break the sequence */
+ var maxRecipients = top.MAX_RECIPIENTS;
+ awRemoveRow(rowToDelete);
+
+ // assume 2 column update (input and popup)
+ for (var row = rowToDelete + 1; row <= maxRecipients; row ++)
+ awSetInputAndPopupId(awGetInputElement(row), awGetPopupElement(row), (row-1));
+
+ awTestRowSequence();
+}
+
+function awClickEmptySpace(target, setFocus)
+{
+ if (target == null ||
+ (target.localName != "listboxbody" &&
+ target.localName != "listcell" &&
+ target.localName != "listitem"))
+ return;
+
+ let lastInput = awGetInputElement(top.MAX_RECIPIENTS);
+
+ if ( lastInput && lastInput.value )
+ awAppendNewRow(setFocus);
+ else if (setFocus)
+ awSetFocusTo(lastInput);
+}
+
+function awReturnHit(inputElement)
+{
+ let row = awGetRowByInputElement(inputElement);
+ let nextInput = awGetInputElement(row+1);
+
+ if ( !nextInput )
+ {
+ if ( inputElement.value )
+ awAppendNewRow(true);
+ else // No address entered, switch to Subject field
+ {
+ var subjectField = document.getElementById( 'msgSubject' );
+ subjectField.select();
+ subjectField.focus();
+ }
+ }
+ else
+ {
+ nextInput.select();
+ awSetFocusTo(nextInput);
+ }
+
+ // be sure to add the recipient to our ignore list
+ // when the user hits enter in an autocomplete widget...
+ addRecipientsToIgnoreList(inputElement.value);
+}
+
+function awDeleteHit(inputElement)
+{
+ let row = awGetRowByInputElement(inputElement);
+
+ /* 1. don't delete the row if it's the last one remaining, just reset it! */
+ if (top.MAX_RECIPIENTS <= 1)
+ {
+ inputElement.value = "";
+ return;
+ }
+
+ /* 2. Set the focus to the previous field if possible */
+ // Note: awSetFocusTo() is asynchronous, i.e. we'll focus after row removal.
+ if (row > 1)
+ awSetFocusTo(awGetInputElement(row - 1))
+ else
+ awSetFocusTo(awGetInputElement(2))
+
+ /* 3. Delete the row */
+ awDeleteRow(row);
+}
+
+function awAppendNewRow(setFocus)
+{
+ var listbox = document.getElementById('addressingWidget');
+ var listitem1 = awGetListItem(1);
+
+ if ( listbox && listitem1 )
+ {
+ var lastRecipientType = awGetPopupElement(top.MAX_RECIPIENTS).value;
+
+ var nextDummy = awGetNextDummyRow();
+ var newNode = listitem1.cloneNode(true);
+ if (nextDummy)
+ listbox.replaceChild(newNode, nextDummy);
+ else
+ listbox.appendChild(newNode);
+
+ top.MAX_RECIPIENTS++;
+
+ var input = newNode.getElementsByTagName(awInputElementName());
+ if ( input && input.length == 1 )
+ {
+ input[0].value = "";
+
+ // We always clone the first row. The problem is that the first row
+ // could be focused. When we clone that row, we end up with a cloned
+ // XUL textbox that has a focused attribute set. Therefore we think
+ // we're focused and don't properly refocus. The best solution to this
+ // would be to clone a template row that didn't really have any presentation,
+ // rather than using the real visible first row of the listbox.
+ //
+ // For now we'll just put in a hack that ensures the focused attribute
+ // is never copied when the node is cloned.
+ if (input[0].getAttribute('focused') != '')
+ input[0].removeAttribute('focused');
+ }
+ var select = newNode.getElementsByTagName(awSelectElementName());
+ if ( select && select.length == 1 )
+ {
+ // It only makes sense to clone some field types; others
+ // should not be cloned, since it just makes the user have
+ // to go to the trouble of selecting something else. In such
+ // cases let's default to 'To' (a reasonable default since
+ // we already default to 'To' on the first dummy field of
+ // a new message).
+ switch (lastRecipientType)
+ {
+ case "addr_reply":
+ case "addr_other":
+ select[0].selectedIndex = awGetSelectItemIndex("addr_to");
+ break;
+ case "addr_followup":
+ select[0].selectedIndex = awGetSelectItemIndex("addr_newsgroups");
+ break;
+ default:
+ // e.g. "addr_to","addr_cc","addr_bcc","addr_newsgroups":
+ select[0].selectedIndex = awGetSelectItemIndex(lastRecipientType);
+ }
+
+ awSetInputAndPopupId(input[0], select[0], top.MAX_RECIPIENTS);
+
+ if (input)
+ _awSetAutoComplete(select[0], input[0]);
+ }
+
+ // Focus the new input widget.
+ if (setFocus && input[0] )
+ awSetFocusTo(input[0]);
+ }
+}
+
+// functions for accessing the elements in the addressing widget
+
+/**
+ * Returns the recipient type popup for a row.
+ *
+ * @param row Index of the recipient row to return. Starts at 1.
+ * @return This returns the menulist (not its child menupopup), despite the
+ * function name.
+ */
+function awGetPopupElement(row)
+{
+ return document.getElementById("addressCol1#" + row);
+}
+
+/**
+ * Returns the recipient inputbox for a row.
+ *
+ * @param row Index of the recipient row to return. Starts at 1.
+ * @return This returns the textbox element.
+ */
+function awGetInputElement(row)
+{
+ return document.getElementById("addressCol2#" + row);
+}
+
+function awGetElementByCol(row, col)
+{
+ var colID = "addressCol" + col + "#" + row;
+ return document.getElementById(colID);
+}
+
+function awGetListItem(row)
+{
+ var listbox = document.getElementById('addressingWidget');
+
+ if ( listbox && row > 0)
+ {
+ var listitems = listbox.getElementsByTagName('listitem');
+ if ( listitems && listitems.length >= row )
+ return listitems[row-1];
+ }
+ return 0;
+}
+
+function awGetRowByInputElement(inputElement)
+{
+ var row = 0;
+ if (inputElement) {
+ var listitem = inputElement.parentNode.parentNode;
+ while (listitem) {
+ if (listitem.localName == "listitem")
+ ++row;
+ listitem = listitem.previousSibling;
+ }
+ }
+ return row;
+}
+
+
+// Copy Node - copy this node and insert ahead of the (before) node. Append to end if before=0
+function awCopyNode(node, parentNode, beforeNode)
+{
+ var newNode = node.cloneNode(true);
+
+ if ( beforeNode )
+ parentNode.insertBefore(newNode, beforeNode);
+ else
+ parentNode.appendChild(newNode);
+
+ return newNode;
+}
+
+// remove row
+
+function awRemoveRow(row)
+{
+ awGetListItem(row).remove();
+ awFitDummyRows();
+
+ top.MAX_RECIPIENTS --;
+}
+
+/**
+ * Set focus to the specified element, typically a recipient input element.
+ * We do this asynchronusly to allow other processes like adding or removing rows
+ * to complete before shifting focus.
+ *
+ * @param element the element to receive focus asynchronously
+ */
+function awSetFocusTo(element) {
+ // Remember the (input) element to focus for asynchronous focusing, so that we
+ // play safe if this gets called again and the original element gets removed
+ // before we can focus it.
+ top.awInputToFocus = element;
+ setTimeout(_awSetFocusTo, 0);
+}
+
+function _awSetFocusTo() {
+ top.awInputToFocus.focus();
+}
+
+// Deprecated - use awSetFocusTo() instead.
+// ### TODO: This function should be removed if we're sure addons aren't using it.
+function awSetFocus(row, inputElement) {
+ awSetFocusTo(inputElement);
+}
+
+function awTabFromRecipient(element, event) {
+ var row = awGetRowByInputElement(element);
+ if (!event.shiftKey && row < top.MAX_RECIPIENTS) {
+ var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based.
+ var listBox = document.getElementById("addressingWidget");
+ listBox.listBoxObject.ensureIndexIsVisible(listBoxRow + 1);
+ }
+
+ // be sure to add the recipient to our ignore list
+ // when the user tabs out of an autocomplete line...
+ addRecipientsToIgnoreList(element.value);
+}
+
+function awTabFromMenulist(element, event)
+{
+ var row = awGetRowByInputElement(element);
+ if (event.shiftKey && row > 1) {
+ var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based.
+ var listBox = document.getElementById("addressingWidget");
+ listBox.listBoxObject.ensureIndexIsVisible(listBoxRow - 1);
+ }
+}
+
+function awGetNumberOfRecipients()
+{
+ return top.MAX_RECIPIENTS;
+}
+
+function DropOnAddressingTarget(event, onWidget) {
+ let dragSession = gDragService.getCurrentSession();
+
+ let trans = Cc["@mozilla.org/widget/transferable;1"]
+ .createInstance(Ci.nsITransferable);
+ trans.init(getLoadContext());
+ trans.addDataFlavor("text/x-moz-address");
+
+ let added = false;
+ for (let i = 0; i < dragSession.numDropItems; ++i) {
+ dragSession.getData(trans, i);
+ let dataObj = {};
+ let bestFlavor = {};
+ let len = {};
+
+ // Ensure we catch any empty data that may have slipped through.
+ try {
+ trans.getAnyTransferData(bestFlavor, dataObj, len);
+ } catch(ex) {
+ continue;
+ }
+ if (dataObj) {
+ dataObj = dataObj.value.QueryInterface(Ci.nsISupportsString);
+ }
+ if (!dataObj) {
+ continue;
+ }
+
+ // Pull the address out of the data object.
+ let address = dataObj.data.substring(0, len.value);
+ if (!address) {
+ continue;
+ }
+
+ if (onWidget) {
+ // Break down and add each address.
+ parseAndAddAddresses(address,
+ awGetPopupElement(top.MAX_RECIPIENTS).value);
+ } else {
+ // Add address into the bucket.
+ DropRecipient(address);
+ }
+ added = true;
+ }
+
+ // We added at least one address during the drop.
+ // Disable the default handler and stop propagating the event
+ // to avoid data being dropped twice.
+ if (added) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+}
+
+function _awSetAutoComplete(selectElem, inputElem)
+{
+ let params = JSON.parse(inputElem.getAttribute('autocompletesearchparam'));
+ params.type = selectElem.value;
+ inputElem.setAttribute('autocompletesearchparam', JSON.stringify(params));
+}
+
+function awSetAutoComplete(rowNumber)
+{
+ var inputElem = awGetInputElement(rowNumber);
+ var selectElem = awGetPopupElement(rowNumber);
+ _awSetAutoComplete(selectElem, inputElem)
+}
+
+function awRecipientTextCommand(userAction, element)
+{
+ if (userAction == "typing" || userAction == "scrolling")
+ awReturnHit(element);
+}
+
+// Called when an autocomplete session item is selected and the status of
+// the session it was selected from is nsIAutoCompleteStatus::failureItems.
+//
+// As of this writing, the only way that can happen is when an LDAP
+// autocomplete session returns an error to be displayed to the user.
+//
+// There are hardcoded messages in here, but these are just fallbacks for
+// when string bundles have already failed us.
+//
+function awRecipientErrorCommand(errItem, element)
+{
+ // remove the angle brackets from the general error message to construct
+ // the title for the alert. someday we'll pass this info using a real
+ // exception object, and then this code can go away.
+ //
+ var generalErrString;
+ if (errItem.value != "") {
+ generalErrString = errItem.value.slice(1, errItem.value.length-1);
+ } else {
+ generalErrString = "Unknown LDAP server problem encountered";
+ }
+
+ // try and get the string of the specific error to contruct the complete
+ // err msg, otherwise fall back to something generic. This message is
+ // handed to us as an nsISupportsString in the param slot of the
+ // autocomplete error item, by agreement documented in
+ // nsILDAPAutoCompFormatter.idl
+ //
+ var specificErrString = "";
+ try {
+ var specificError = errItem.param.QueryInterface(Ci.nsISupportsString);
+ specificErrString = specificError.data;
+ } catch (ex) {
+ }
+ if (specificErrString == "") {
+ specificErrString = "Internal error";
+ }
+
+ Services.prompt.alert(window, generalErrString, specificErrString);
+}
+
+function awRecipientKeyPress(event, element)
+{
+ switch(event.key) {
+ case "ArrowUp":
+ awArrowHit(element, -1);
+ break;
+ case "ArrowDown":
+ awArrowHit(element, 1);
+ break;
+ case "Enter":
+ case "Tab":
+ // if the user text contains a comma or a line return, ignore
+ if (element.value.includes(',')) {
+ var addresses = element.value;
+ element.value = ""; // clear out the current line so we don't try to autocomplete it..
+ parseAndAddAddresses(addresses, awGetPopupElement(awGetRowByInputElement(element)).value);
+ }
+ else if (event.key == "Tab")
+ awTabFromRecipient(element, event);
+
+ break;
+ }
+}
+
+function awArrowHit(inputElement, direction)
+{
+ var row = awGetRowByInputElement(inputElement) + direction;
+ if (row) {
+ var nextInput = awGetInputElement(row);
+
+ if (nextInput)
+ awSetFocusTo(nextInput);
+ else if (inputElement.value)
+ awAppendNewRow(true);
+ }
+}
+
+function awRecipientKeyDown(event, element)
+{
+ switch(event.key) {
+ case "Delete":
+ case "Backspace":
+ /* do not query directly the value of the text field else the autocomplete widget could potentially
+ alter it value while doing some internal cleanup, instead, query the value through the first child
+ */
+ if (!element.value)
+ awDeleteHit(element);
+
+ //We need to stop the event else the listbox will receive it and the function
+ //awKeyDown will be executed!
+ event.stopPropagation();
+ break;
+ }
+}
+
+function awKeyDown(event, listboxElement)
+{
+ switch(event.key) {
+ case "Delete":
+ case "Backspace":
+ /* Warning, the listboxElement.selectedItems will change everytime we delete a row */
+ var length = listboxElement.selectedCount;
+ for (var i = 1; i <= length; i++) {
+ var inputs = listboxElement.selectedItem.getElementsByTagName(awInputElementName());
+ if (inputs && inputs.length == 1)
+ awDeleteHit(inputs[0]);
+ }
+ break;
+ }
+}
+
+function awMenulistKeyPress(event, element)
+{
+ switch(event.key) {
+ case "Tab":
+ awTabFromMenulist(element, event);
+ break;
+ }
+}
+
+/* ::::::::::: addressing widget dummy rows ::::::::::::::::: */
+
+var gAWContentHeight = 0;
+var gAWRowHeight = 0;
+
+function awFitDummyRows()
+{
+ awCalcContentHeight();
+ awCreateOrRemoveDummyRows();
+}
+
+function awCreateOrRemoveDummyRows()
+{
+ var listbox = document.getElementById("addressingWidget");
+ var listboxHeight = listbox.boxObject.height;
+
+ // remove rows to remove scrollbar
+ let kids = listbox.querySelectorAll('[_isDummyRow]');
+ for (let i = kids.length - 1; gAWContentHeight > listboxHeight && i >= 0; --i) {
+ gAWContentHeight -= gAWRowHeight;
+ kids[i].remove();
+ }
+
+ // add rows to fill space
+ if (gAWRowHeight) {
+ while (gAWContentHeight + gAWRowHeight < listboxHeight) {
+ awCreateDummyItem(listbox);
+ gAWContentHeight += gAWRowHeight;
+ }
+ }
+}
+
+function awCalcContentHeight()
+{
+ var listbox = document.getElementById("addressingWidget");
+ var items = listbox.getElementsByTagName("listitem");
+
+ gAWContentHeight = 0;
+ if (items.length > 0) {
+ // all rows are forced to a uniform height in xul listboxes, so
+ // find the first listitem with a boxObject and use it as precedent
+ var i = 0;
+ do {
+ gAWRowHeight = items[i].boxObject.height;
+ ++i;
+ } while (i < items.length && !gAWRowHeight);
+ gAWContentHeight = gAWRowHeight*items.length;
+ }
+}
+
+function awCreateDummyItem(aParent)
+{
+ var titem = document.createElement("listitem");
+ titem.setAttribute("_isDummyRow", "true");
+ titem.setAttribute("class", "dummy-row");
+
+ for (var i = awGetNumberOfCols(); i > 0; i--)
+ awCreateDummyCell(titem);
+
+ if (aParent)
+ aParent.appendChild(titem);
+
+ return titem;
+}
+
+function awCreateDummyCell(aParent)
+{
+ var cell = document.createElement("listcell");
+ cell.setAttribute("class", "addressingWidgetCell dummy-row-cell");
+ if (aParent)
+ aParent.appendChild(cell);
+
+ return cell;
+}
+
+function awGetNextDummyRow()
+{
+ // gets the next row from the top down
+ return document.querySelector('#addressingWidget > [_isDummyRow]');
+}
+
+function awSizerListen()
+{
+ // when splitter is clicked, fill in necessary dummy rows each time the mouse is moved
+ awCalcContentHeight(); // precalculate
+ document.addEventListener("mousemove", awSizerMouseMove, true);
+ document.addEventListener("mouseup", awSizerMouseUp);
+}
+
+function awSizerMouseMove()
+{
+ awCreateOrRemoveDummyRows(2);
+}
+
+function awSizerMouseUp()
+{
+ document.removeEventListener("mousemove", awSizerMouseMove);
+ document.removeEventListener("mouseup", awSizerMouseUp);
+}
+
+function awSizerResized(aSplitter)
+{
+ // set the height on the listbox rather than on the toolbox
+ var listbox = document.getElementById("addressingWidget");
+ listbox.height = listbox.boxObject.height;
+ // remove all the heights set on the splitter's previous siblings
+ for (let sib = aSplitter.previousSibling; sib; sib = sib.previousSibling)
+ sib.removeAttribute("height");
+}
+
+function awDocumentKeyPress(event)
+{
+ try {
+ var id = event.target.id;
+ if (id.startsWith('addressCol1'))
+ awRecipientKeyPress(event, event.target);
+ } catch (e) { }
+}
+
+function awRecipientInputCommand(event, inputElement)
+{
+ gContentChanged=true;
+ setupAutocomplete();
+}
+
+// Given an arbitrary block of text like a comma delimited list of names or a names separated by spaces,
+// we will try to autocomplete each of the names and then take the FIRST match for each name, adding it the
+// addressing widget on the compose window.
+
+var gAutomatedAutoCompleteListener = null;
+
+function parseAndAddAddresses(addressText, recipientType)
+{
+ // strip any leading >> characters inserted by the autocomplete widget
+ var strippedAddresses = addressText.replace(/.* >> /, "");
+
+ var addresses = MailServices.headerParser
+ .makeFromDisplayAddress(strippedAddresses);
+
+ if (addresses.length)
+ {
+ // we need to set up our own autocomplete session and search for results
+
+ setupAutocomplete(); // be safe, make sure we are setup
+ if (!gAutomatedAutoCompleteListener)
+ gAutomatedAutoCompleteListener = new AutomatedAutoCompleteHandler();
+
+ gAutomatedAutoCompleteListener.init(addresses.map(addr => addr.toString()),
+ addresses.length, recipientType);
+ }
+}
+
+function AutomatedAutoCompleteHandler()
+{
+}
+
+// state driven self contained object which will autocomplete a block of addresses without any UI.
+// force picks the first match and adds it to the addressing widget, then goes on to the next
+// name to complete.
+
+AutomatedAutoCompleteHandler.prototype =
+{
+ param: this,
+ sessionName: null,
+ namesToComplete: {},
+ numNamesToComplete: 0,
+ indexIntoNames: 0,
+
+ numSessionsToSearch: 0,
+ numSessionsSearched: 0,
+ recipientType: null,
+ searchResults: null,
+
+ init:function(namesToComplete, numNamesToComplete, recipientType)
+ {
+ this.indexIntoNames = 0;
+ this.numNamesToComplete = numNamesToComplete;
+ this.namesToComplete = namesToComplete;
+
+ this.recipientType = recipientType;
+
+ // set up the auto complete sessions to use
+ setupAutocomplete();
+ this.autoCompleteNextAddress();
+ },
+
+ autoCompleteNextAddress:function()
+ {
+ this.numSessionsToSearch = 0;
+ this.numSessionsSearched = 0;
+ this.searchResults = new Array;
+
+ if (this.indexIntoNames < this.numNamesToComplete && this.namesToComplete[this.indexIntoNames])
+ {
+ /* XXX This is used to work, until switching to the new toolkit broke it
+ We should fix it see bug 456550.
+ if (!this.namesToComplete[this.indexIntoNames].includes('@')) // don't autocomplete if address has an @ sign in it
+ {
+ // make sure total session count is updated before we kick off ANY actual searches
+ if (gAutocompleteSession)
+ this.numSessionsToSearch++;
+
+ if (gLDAPSession && gCurrentAutocompleteDirectory)
+ this.numSessionsToSearch++;
+
+ if (gAutocompleteSession)
+ {
+ gAutocompleteSession.onAutoComplete(this.namesToComplete[this.indexIntoNames], null, this);
+ // AB searches are actually synchronous. So by the time we get here we have already looked up results.
+
+ // if we WERE going to also do an LDAP lookup, then check to see if we have a valid match in the AB, if we do
+ // don't bother with the LDAP search too just return
+
+ if (gLDAPSession && gCurrentAutocompleteDirectory && this.searchResults[0] && this.searchResults[0].defaultItemIndex != -1)
+ {
+ this.processAllResults();
+ return;
+ }
+ }
+
+ if (gLDAPSession && gCurrentAutocompleteDirectory)
+ gLDAPSession.onStartLookup(this.namesToComplete[this.indexIntoNames], null, this);
+ }
+ */
+
+ if (!this.numSessionsToSearch)
+ this.processAllResults(); // ldap and ab are turned off, so leave text alone
+ }
+ },
+
+ onStatus:function(aStatus)
+ {
+ return;
+ },
+
+ onAutoComplete: function(aResults, aStatus)
+ {
+ // store the results until all sessions are done and have reported in
+ if (aResults)
+ this.searchResults[this.numSessionsSearched] = aResults;
+
+ this.numSessionsSearched++; // bump our counter
+
+ if (this.numSessionsToSearch <= this.numSessionsSearched)
+ setTimeout('gAutomatedAutoCompleteListener.processAllResults()', 0); // we are all done
+ },
+
+ processAllResults: function()
+ {
+ // Take the first result and add it to the compose window
+ var addressToAdd;
+
+ // loop through the results looking for the non default case (default case is the address book with only one match, the default domain)
+ var sessionIndex;
+
+ var searchResultsForSession;
+
+ for (sessionIndex in this.searchResults)
+ {
+ searchResultsForSession = this.searchResults[sessionIndex];
+ if (searchResultsForSession && searchResultsForSession.defaultItemIndex > -1)
+ {
+ addressToAdd = searchResultsForSession.items
+ .queryElementAt(searchResultsForSession.defaultItemIndex,
+ Ci.nsIAutoCompleteItem).value;
+ break;
+ }
+ }
+
+ // still no match? loop through looking for the -1 default index
+ if (!addressToAdd)
+ {
+ for (sessionIndex in this.searchResults)
+ {
+ searchResultsForSession = this.searchResults[sessionIndex];
+ if (searchResultsForSession && searchResultsForSession.defaultItemIndex == -1)
+ {
+ addressToAdd = searchResultsForSession.items
+ .queryElementAt(0, Ci.nsIAutoCompleteItem).value;
+ break;
+ }
+ }
+ }
+
+ // no matches anywhere...just use what we were given
+ if (!addressToAdd)
+ addressToAdd = this.namesToComplete[this.indexIntoNames];
+
+ // that will automatically set the focus on a new available row, and make sure it is visible
+ awAddRecipient(this.recipientType ? this.recipientType : "addr_to", addressToAdd);
+
+ this.indexIntoNames++;
+ this.autoCompleteNextAddress();
+ },
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Ci.nsIAutoCompleteListener) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ }
+}
diff --git a/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul b/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul
new file mode 100644
index 0000000000..efd1c6007a
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<overlay id="mailComposeOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <menupopup id="menu_EditPopup" onpopupshowing="updateEditItems();">
+ <menuitem id="menu_inlineSpellCheck"
+ oncommand="EnableInlineSpellCheck(!InlineSpellCheckerUI.enabled);"/>
+ <menuitem id="menu_accountmgr"
+ insertafter="sep_preferences"
+ command="cmd_account"/>
+ </menupopup>
+</overlay>
diff --git a/comm/suite/mailnews/components/compose/content/messengercompose.xul b/comm/suite/mailnews/components/compose/content/messengercompose.xul
new file mode 100644
index 0000000000..89126d814d
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/messengercompose.xul
@@ -0,0 +1,720 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messengercompose/messengercompose.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://editor/skin/editorFormatToolbar.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/addressingWidget.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/smime/msgCompSMIMEOverlay.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/content/bindings.css" type="text/css"?>
+
+<?xul-overlay href="chrome://communicator/content/charsetOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/sidebar/sidebarOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/contentAreaContextOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/messengercompose/msgComposeContextOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+<?xul-overlay href="chrome://editor/content/editorOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/messengercompose/mailComposeOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/mailOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % messengercomposeDTD SYSTEM "chrome://messenger/locale/messengercompose/messengercompose.dtd" >
+%messengercomposeDTD;
+<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
+%messengerDTD;
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % utilityDTD SYSTEM "chrome://communicator/locale/utilityOverlay.dtd">
+%utilityDTD;
+<!ENTITY % msgCompSMIMEDTD SYSTEM "chrome://messenger-smime/locale/msgCompSMIMEOverlay.dtd">
+%msgCompSMIMEDTD;
+]>
+
+<window id="msgcomposeWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ onunload="ComposeUnload()"
+ onload="ComposeLoad()"
+ onclose="return DoCommandClose()"
+ onfocus="EditorOnFocus()"
+ title="&msgComposeWindow.title;"
+ toggletoolbar="true"
+ lightweightthemes="true"
+ lightweightthemesfooter="status-bar"
+ windowtype="msgcompose"
+ macanimationtype="document"
+ drawtitle="true"
+ width="640" height="480"
+ persist="screenX screenY width height sizemode">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_composeMsgs" src="chrome://messenger/locale/messengercompose/composeMsgs.properties"/>
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_offlinePrompts" src="chrome://messenger/locale/offline.properties"/>
+ <stringbundle id="languageBundle" src="chrome://global/locale/languageNames.properties"/>
+ <stringbundle id="brandBundle" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="bundle_comp_smime" src="chrome://messenger-smime/locale/msgCompSMIMEOverlay.properties"/>
+ </stringbundleset>
+
+ <script src="chrome://communicator/content/contentAreaClick.js"/>
+ <script src="chrome://global/content/printUtils.js"/>
+ <script src="chrome://messenger/content/accountUtils.js"/>
+ <script src="chrome://messenger/content/mail-offline.js"/>
+ <script src="chrome://editor/content/editor.js"/>
+ <script src="chrome://messenger/content/messengercompose/MsgComposeCommands.js"/>
+ <script src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"/>
+ <script src="chrome://messenger/content/addressbook/abDragDrop.js"/>
+ <script src="chrome://messenger-smime/content/msgCompSMIMEOverlay.js"/>
+
+ <commandset id="composeCommands">
+ <commandset id="msgComposeCommandUpdate"
+ commandupdater="true"
+ events="focus"
+ oncommandupdate="CommandUpdate_MsgCompose()"/>
+
+ <commandset id="editorCommands"/>
+ <commandset id="commonEditorMenuItems"/>
+ <commandset id="composerMenuItems"/>
+ <commandset id="composerEditMenuItems"/>
+ <commandset id="composerStyleMenuItems"/>
+ <commandset id="composerTableMenuItems"/>
+ <commandset id="composerListMenuItems"/>
+ <commandset id="tasksCommands"/>
+ <!-- File Menu -->
+ <command id="cmd_attachFile" oncommand="goDoCommand('cmd_attachFile')"/>
+ <command id="cmd_attachPage" oncommand="goDoCommand('cmd_attachPage')"/>
+ <command id="cmd_attachVCard" checked="false" oncommand="ToggleAttachVCard(event.target)"/>
+ <command id="cmd_save" oncommand="goDoCommand('cmd_save')"/>
+ <command id="cmd_saveAsFile" oncommand="goDoCommand('cmd_saveAsFile')"/>
+ <command id="cmd_saveAsDraft" oncommand="goDoCommand('cmd_saveAsDraft')"/>
+ <command id="cmd_saveAsTemplate" oncommand="goDoCommand('cmd_saveAsTemplate')"/>
+ <command id="cmd_sendButton" oncommand="goDoCommand('cmd_sendButton')"/>
+ <command id="cmd_sendNow" oncommand="goDoCommand('cmd_sendNow')"/>
+ <command id="cmd_sendWithCheck" oncommand="goDoCommand('cmd_sendWithCheck')"/>
+ <command id="cmd_sendLater" oncommand="goDoCommand('cmd_sendLater')"/>
+
+ <!-- Edit Menu -->
+ <!--command id="cmd_pasteQuote"/ DO NOT INCLUDE THOSE COMMANDS ELSE THE EDIT MENU WILL BE BROKEN! -->
+ <!--command id="cmd_find"/-->
+ <!--command id="cmd_findNext"/-->
+ <!--command id="cmd_findReplace"/-->
+ <command id="cmd_renameAttachment" oncommand="goDoCommand('cmd_renameAttachment')" disabled="true"/>
+ <command id="cmd_openAttachment" oncommand="goDoCommand('cmd_openAttachment')"/>
+ <command id="cmd_account"
+ label="&accountManagerCmd.label;"
+ accesskey="&accountManagerCmd.accesskey;"
+ oncommand="goDoCommand('cmd_account');"/>
+
+ <!-- Options Menu -->
+ <command id="cmd_selectAddress" oncommand="goDoCommand('cmd_selectAddress')"/>
+ <command id="cmd_outputFormat" oncommand="OutputFormatMenuSelect(event.target)"/>
+ <command id="cmd_quoteMessage" oncommand="goDoCommand('cmd_quoteMessage')"/>
+ <command id="cmd_viewSecurityStatus" oncommand="showMessageComposeSecurityStatus();"/>
+ </commandset>
+
+ <broadcasterset id="composeBroadcasters">
+ <broadcaster id="Communicator:WorkMode"/>
+ <broadcaster id="securityStatus" crypto="" signing=""/>
+ </broadcasterset>
+
+ <observes element="securityStatus" attribute="crypto"/>
+ <observes element="securityStatus" attribute="signing"/>
+
+ <broadcasterset id="mainBroadcasterSet"/>
+
+ <keyset id="tasksKeys">
+ <!-- File Menu -->
+ <key id="key_send" keycode="&sendCmd.keycode;" observes="cmd_sendWithCheck" modifiers="accel"/>
+ <key id="key_sendLater" keycode="&sendLaterCmd.keycode;" observes="cmd_sendLater" modifiers="accel, shift"/>
+
+ <!-- Options Menu -->
+ <!-- key id="key_selectAddresses" key="&selectAddressCmd.key;" command="cmd_selectAddress"/ -->
+
+ <key id="showHideSidebar"/>
+ <!-- Tab/F6 Keys -->
+ <key keycode="VK_TAB" oncommand="SwitchElementFocus(event);" modifiers="control"/>
+ <key keycode="VK_TAB" oncommand="SwitchElementFocus(event);" modifiers="control,shift"/>
+ <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="control"/>
+ <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="control,shift"/>
+ <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="shift"/>
+ <key keycode="VK_F6" oncommand="SwitchElementFocus(event);"/>
+ <key keycode="VK_ESCAPE" oncommand="handleEsc();"/>
+ </keyset>
+ <keyset id="editorKeys"/>
+ <keyset id="composeKeys">
+#ifndef XP_MACOSX
+ <key id="key_renameAttachment" keycode="VK_F2"
+ oncommand="goDoCommand('cmd_renameAttachment');"/>
+#endif
+ </keyset>
+
+ <popupset id="contentAreaContextSet"/>
+
+ <popupset id="editorPopupSet">
+ <menupopup id="sidebarPopup"/>
+
+ <menupopup id="msgComposeAttachmentContext"
+ onpopupshowing="updateEditItems();">
+ <menuitem label="&openAttachment.label;"
+ accesskey="&openAttachment.accesskey;"
+ command="cmd_openAttachment"/>
+ <menuitem accesskey="&deleteAttachment.accesskey;"
+ command="cmd_delete"/>
+ <menuitem label="&renameAttachment.label;"
+ accesskey="&renameAttachment.accesskey;"
+ command="cmd_renameAttachment"/>
+ <menuitem label="&selectAllCmd.label;"
+ accesskey="&selectAllAttachments.accesskey;"
+ command="cmd_selectAll"/>
+ <menuseparator/>
+ <menuitem label="&attachFile.label;"
+ accesskey="&attachFile.accesskey;"
+ command="cmd_attachFile"/>
+ <menuitem label="&attachPage.label;"
+ accesskey="&attachPage.accesskey;"
+ command="cmd_attachPage"/>
+ </menupopup>
+ </popupset>
+
+ <menupopup id="blockedContentOptions" value=""
+ onpopupshowing="onBlockedContentOptionsShowing(event);">
+ </menupopup>
+
+ <vbox id="titlebar"/>
+
+ <toolbox id="compose-toolbox"
+ class="toolbox-top"
+ mode="full"
+ defaultmode="full">
+ <toolbar id="compose-toolbar-menubar2"
+ type="menubar"
+ class="chromeclass-menubar"
+ persist="collapsed"
+ grippytooltiptext="&menuBar.tooltip;"
+ customizable="true"
+ defaultset="menubar-items"
+ mode="icons"
+ iconsize="small"
+ defaultmode="icons"
+ defaulticonsize="small"
+ context="toolbar-context-menu">
+ <toolbaritem id="menubar-items"
+ class="menubar-items"
+ align="center">
+ <menubar id="mail-menubar">
+ <menu id="menu_File">
+ <menupopup id="menu_FilePopup">
+ <menu id="menu_New">
+ <menupopup id="menu_NewPopup">
+ <menuitem id="menu_newMessage"/>
+ <menuseparator id="menuNewPopupSeparator"/>
+ <menuitem id="menu_newCard"/>
+ <menuitem id="menu_newNavigator"/>
+ <menuitem id="menu_newPrivateWindow"/>
+ <menuitem id="menu_newEditor"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_Attach"
+ label="&attachMenu.label;"
+ accesskey="&attachMenu.accesskey;">
+ <menupopup id="menu_AttachPopup">
+ <menuitem id="menu_AttachFile"
+ label="&attachFileCmd.label;"
+ accesskey="&attachFileCmd.accesskey;"
+ command="cmd_attachFile"/>
+ <menuitem id="menu_AttachPage"
+ label="&attachPageCmd.label;"
+ accesskey="&attachPageCmd.accesskey;"
+ command="cmd_attachPage"/>
+ <menuseparator id="menuAttachPageSeparator"/>
+ <menuitem id="menu_AttachPageVCard"
+ type="checkbox"
+ label="&attachVCardCmd.label;"
+ accesskey="&attachVCardCmd.accesskey;"
+ command="cmd_attachVCard"/>
+ </menupopup>
+ </menu>
+ <menuitem id="menu_close"/>
+ <menuseparator id="menuFileAfterCloseSeparator"/>
+ <menuitem id="menu_SaveCmd"
+ label="&saveCmd.label;"
+ accesskey="&saveCmd.accesskey;"
+ key="key_save"
+ command="cmd_save"/>
+ <menu id="menu_SaveAsCmd"
+ label="&saveAsCmd.label;"
+ accesskey="&saveAsCmd.accesskey;">
+ <menupopup id="menu_SaveAsCmdPopup">
+ <menuitem id="menu_SaveAsFileCmd"
+ label="&saveAsFileCmd.label;"
+ accesskey="&saveAsFileCmd.accesskey;"
+ command="cmd_saveAsFile"/>
+ <menuseparator id="menuSaveAfterSaveAsSeparator"/>
+ <menuitem id="menu_SaveAsDraftCmd"
+ label="&saveAsDraftCmd.label;"
+ accesskey="&saveAsDraftCmd.accesskey;"
+ command="cmd_saveAsDraft"/>
+ <menuitem id="menu_SaveAsTemplateCmd"
+ label="&saveAsTemplateCmd.label;"
+ accesskey="&saveAsTemplateCmd.accesskey;"
+ command="cmd_saveAsTemplate"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="menuFileAfterSaveAsSeparator"/>
+ <menuitem id="menu_sendNow"
+ label="&sendNowCmd.label;"
+ accesskey="&sendNowCmd.accesskey;"
+ key="key_send" command="cmd_sendNow"/>
+ <menuitem id="menu_sendLater"
+ label="&sendLaterCmd.label;"
+ accesskey="&sendLaterCmd.accesskey;"
+ key="key_sendLater"
+ command="cmd_sendLater"/>
+ <menuseparator id="menuFileAfterSendLaterSeparator"/>
+ <menuitem id="menu_printSetup"/>
+ <menuitem id="menu_printPreview"/>
+ <menuitem id="menu_print"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_Edit"/>
+ <menu id="menu_View">
+ <menupopup id="menu_View_Popup">
+ <menu id="menu_Toolbars">
+ <menupopup id="view_toolbars_popup"
+ onpopupshowing="onViewToolbarsPopupShowing(event)"
+ oncommand="onViewToolbarCommand(event);">
+ <menuitem id="menu_showTaskbar"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="viewMenuBeforeSecurityStatusSeparator"/>
+ <menuitem id="menu_viewSecurityStatus"
+ label="&menu_viewSecurityStatus.label;"
+ accesskey="&menu_viewSecurityStatus.accesskey;"
+ command="cmd_viewSecurityStatus"/>
+ </menupopup>
+ </menu>
+
+ <menu id="insertMenu"
+ command="cmd_renderedHTMLEnabler"/>
+
+ <menu id="formatMenu"
+ label="&formatMenu.label;"
+ accesskey="&formatMenu.accesskey;"
+ command="cmd_renderedHTMLEnabler">
+ <menupopup id="formatMenuPopup">
+ <menu id="tableMenu"/>
+ <menuseparator id="menuFormatAfterTableSeparator"/>
+ <menuitem id="objectProperties"/>
+ <menuitem id="colorsAndBackground"/>
+ </menupopup>
+ </menu>
+
+ <menu id="optionsMenu"
+ label="&optionsMenu.label;"
+ accesskey="&optionsMenu.accesskey;">
+ <menupopup id="optionsMenuPopup"
+ onpopupshowing="setSecuritySettings(1);">
+ <menuitem id="menu_selectAddress"
+ label="&selectAddressCmd.label;"
+ accesskey="&selectAddressCmd.accesskey;"
+ command="cmd_selectAddress"/>
+ <menuitem id="menu_quoteMessage"
+ label="&quoteCmd.label;"
+ accesskey="&quoteCmd.accesskey;"
+ command="cmd_quoteMessage"/>
+ <menuseparator id="menuOptionsAfterQuoteSeparator"/>
+ <menuitem id="returnReceiptMenu"
+ type="checkbox"
+ label="&returnReceiptMenu.label;"
+ accesskey="&returnReceiptMenu.accesskey;"
+ checked="false"
+ oncommand="ToggleReturnReceipt(event.target)"/>
+ <menuitem id="dsnMenu"
+ type="checkbox"
+ label="&dsnMenu.label;"
+ accesskey="&dsnMenu.accesskey;"
+ checked="false"
+ oncommand="ToggleDSN(event.target);"/>
+ <menu id="outputFormatMenu"
+ label="&outputFormatMenu.label;"
+ accesskey="&outputFormatMenu.accesskey;"
+ command="cmd_outputFormat">
+ <menupopup id="outputFormatMenuPopup">
+ <menuitem id="format_auto" type="radio" name="output_format" label="&autoFormatCmd.label;" accesskey="&autoFormatCmd.accesskey;" checked="true"/>
+ <menuitem id="format_plain" type="radio" name="output_format" label="&plainTextFormatCmd.label;" accesskey="&plainTextFormatCmd.accesskey;"/>
+ <menuitem id="format_html" type="radio" name="output_format" label="&htmlFormatCmd.label;" accesskey="&htmlFormatCmd.accesskey;"/>
+ <menuitem id="format_both" type="radio" name="output_format" label="&bothFormatCmd.label;" accesskey="&bothFormatCmd.accesskey;"/>
+ </menupopup>
+ </menu>
+ <menu id="priorityMenu"
+ label="&priorityMenu.label;"
+ accesskey="&priorityMenu.accesskey;"
+ oncommand="PriorityMenuSelect(event.target);">
+ <menupopup id="priorityMenuPopup"
+ onpopupshowing="updatePriorityMenu(this);">
+ <menuitem id="priority_highest" type="radio" name="priority" label="&highestPriorityCmd.label;" accesskey="&highestPriorityCmd.accesskey;" value="Highest"/>
+ <menuitem id="priority_high" type="radio" name="priority" label="&highPriorityCmd.label;" accesskey="&highPriorityCmd.accesskey;" value="High"/>
+ <menuitem id="priority_normal" type="radio" name="priority" label="&normalPriorityCmd.label;" accesskey="&normalPriorityCmd.accesskey;" value="Normal"/>
+ <menuitem id="priority_low" type="radio" name="priority" label="&lowPriorityCmd.label;" accesskey="&lowPriorityCmd.accesskey;" value="Low"/>
+ <menuitem id="priority_lowest" type="radio" name="priority" label="&lowestPriorityCmd.label;" accesskey="&lowestPriorityCmd.accesskey;" value="Lowest"/>
+ </menupopup>
+ </menu>
+ <menu id="charsetMenu"
+ onpopupshowing="UpdateCharsetMenu(gMsgCompose.compFields.characterSet, this);"
+ oncommand="ComposeSetCharacterSet(event);">
+ <menupopup id="charsetPopup" detectors="false"/>
+ </menu>
+ <menu id="fccMenu"
+ label="&fileCarbonCopyCmd.label;"
+ accesskey="&fileCarbonCopyCmd.accesskey;"
+ oncommand="MessageFcc(event.target._folder);">
+ <menupopup id="fccMenuPopup"
+ type="folder"
+ mode="filing"
+ showFileHereLabel="true"
+ fileHereLabel="&fileHereMenu.label;"/>
+ </menu>
+ <menuseparator id="smimeOptionsSeparator"/>
+ <menuitem id="menu_securityEncryptRequire1"
+ type="checkbox"
+ label="&menu_securityEncryptRequire.label;"
+ accesskey="&menu_securityEncryptRequire.accesskey;"
+ oncommand="toggleEncryptMessage();"/>
+ <menuitem id="menu_securitySign1"
+ type="checkbox"
+ label="&menu_securitySign.label;"
+ accesskey="&menu_securitySign.accesskey;"
+ oncommand="toggleSignMessage();"/>
+ </menupopup>
+ </menu>
+ <menu id="tasksMenu"/>
+ <menu id="windowMenu"/>
+ <menu id="menu_Help"/>
+ </menubar>
+ </toolbaritem>
+ </toolbar>
+
+ <toolbar id="composeToolbar"
+ class="toolbar-primary chromeclass-toolbar"
+ persist="collapsed"
+ grippytooltiptext="&mailToolbar.tooltip;"
+ toolbarname="&showComposeToolbarCmd.label;"
+ accesskey="&showComposeToolbarCmd.accesskey;"
+ customizable="true"
+ defaultset="button-send,separator,button-address,button-attach,spellingButton,button-security,separator,button-save,spring,throbber-box"
+ context="toolbar-context-menu">
+ <toolbarbutton id="button-send"
+ class="toolbarbutton-1"
+ label="&sendButton.label;"
+ tooltiptext="&sendButton.tooltip;"
+ now_label="&sendButton.label;"
+ now_tooltiptext="&sendButton.tooltip;"
+ later_label="&sendLaterCmd.label;"
+ later_tooltiptext="&sendlaterButton.tooltip;"
+ removable="true"
+ command="cmd_sendButton">
+ <observes element="Communicator:WorkMode"
+ attribute="offline"/>
+ </toolbarbutton>
+
+ <toolbarbutton id="button-address"
+ class="toolbarbutton-1"
+ label="&addressButton.label;"
+ tooltiptext="&addressButton.tooltip;"
+ removable="true"
+ command="cmd_selectAddress"/>
+
+ <toolbarbutton id="button-attach"
+ type="menu-button"
+ class="toolbarbutton-1"
+ label="&attachButton.label;"
+ tooltiptext="&attachButton.tooltip;"
+ removable="true"
+ command="cmd_attachFile">
+ <menupopup id="button-attachPopup">
+ <menuitem id="button-attachFile"
+ label="&attachFileCmd.label;"
+ accesskey="&attachFileCmd.accesskey;"
+ command="cmd_attachFile"/>
+ <menuitem id="button-attachPage"
+ label="&attachPageCmd.label;"
+ accesskey="&attachPageCmd.accesskey;"
+ command="cmd_attachPage"/>
+ <menuseparator id="buttonAttachAfterPageSeparator"/>
+ <menuitem id="button-attachVCard"
+ type="checkbox"
+ label="&attachVCardCmd.label;"
+ accesskey="&attachVCardCmd.accesskey;"
+ command="cmd_attachVCard"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbarbutton id="spellingButton"
+ type="menu-button"
+ class="toolbarbutton-1"
+ label="&spellingButton.label;"
+ removable="true"
+ command="cmd_spelling">
+ <!-- this popup gets dynamically generated -->
+ <menupopup id="languageMenuList"
+ oncommand="ChangeLanguage(event);"
+ onpopupshowing="OnShowDictionaryMenu(event.target);"/>
+ </toolbarbutton>
+
+ <toolbarbutton id="button-save"
+ type="menu-button"
+ class="toolbarbutton-1"
+ label="&saveButton.label;"
+ tooltiptext="&saveButton.tooltip;"
+ removable="true"
+ command="cmd_save">
+ <menupopup id="button-savePopup">
+ <menuitem id="button-saveAsFile"
+ label="&saveAsFileCmd.label;"
+ accesskey="&saveAsFileCmd.accesskey;"
+ command="cmd_saveAsFile"/>
+ <menuseparator id="buttonSaveAfterFileSeparator"/>
+ <menuitem id="button-saveAsDraft"
+ label="&saveAsDraftCmd.label;"
+ accesskey="&saveAsDraftCmd.accesskey;"
+ command="cmd_saveAsDraft"/>
+ <menuitem id="button-saveAsTemplate"
+ label="&saveAsTemplateCmd.label;"
+ accesskey="&saveAsTemplateCmd.accesskey;"
+ command="cmd_saveAsTemplate"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbaritem id="throbber-box"/>
+ </toolbar>
+
+ <toolbarset id="customToolbars" context="toolbar-context-menu"/>
+
+ <toolbar id="MsgHeadersToolbar"
+ persist="collapsed"
+ flex="1"
+ grippytooltiptext="&addressBar.tooltip;"
+ nowindowdrag="true">
+ <hbox id="msgheaderstoolbar-box" flex="1">
+ <vbox id="addresses-box" flex="1">
+ <hbox align="center">
+ <label value="&fromAddr.label;" accesskey="&fromAddr.accesskey;" control="msgIdentity"/>
+ <menulist id="msgIdentity"
+ editable="true"
+ disableautoselect="true"
+ flex="1"
+ oncommand="LoadIdentity(false);">
+ <menupopup id="msgIdentityPopup"/>
+ </menulist>
+ </hbox>
+ <!-- Addressing Widget -->
+ <listbox id="addressingWidget" flex="1"
+ seltype="multiple" rows="4"
+ onkeydown="awKeyDown(event, this);"
+ onclick="awClickEmptySpace(event.originalTarget, true);"
+ ondragover="DragAddressOverTargetControl(event);"
+ ondrop="DropOnAddressingTarget(event, true);">
+
+ <listcols>
+ <listcol id="typecol-addressingWidget"/>
+ <listcol id="textcol-addressingWidget" flex="1"/>
+ </listcols>
+
+ <listitem class="addressingWidgetItem" allowevents="true">
+ <listcell class="addressingWidgetCell" align="stretch">
+ <menulist id="addressCol1#1" disableonsend="true"
+ class="aw-menulist menulist-compact" flex="1"
+ onkeypress="awMenulistKeyPress(event, this);"
+ oncommand="onAddressColCommand(this.id);">
+ <menupopup>
+ <menuitem value="addr_to" label="&toAddr.label;"/>
+ <menuitem value="addr_cc" label="&ccAddr.label;"/>
+ <menuitem value="addr_bcc" label="&bccAddr.label;"/>
+ <menuitem value="addr_reply" label="&replyAddr.label;"/>
+ <menuitem value="addr_newsgroups"
+ label="&newsgroupsAddr.label;"/>
+ <menuitem value="addr_followup"
+ label="&followupAddr.label;"/>
+ </menupopup>
+ </menulist>
+ </listcell>
+
+ <listcell class="addressingWidgetCell">
+ <textbox id="addressCol2#1"
+ class="plain textbox-addressingWidget uri-element"
+ aria-labelledby="addressCol1#1"
+ type="autocomplete" flex="1" maxrows="4"
+ newlines="replacewithcommas"
+ autocompletesearch="mydomain addrbook ldap news"
+ timeout="300" autocompletesearchparam="{}"
+ completedefaultindex="true" forcecomplete="true"
+ minresultsforpopup="2" ignoreblurwhilesearching="true"
+ ontextentered="awRecipientTextCommand(eventParam, this);"
+ onerrorcommand="awRecipientErrorCommand(eventParam, this);"
+ onchange="onRecipientsChanged();"
+ oninput="onRecipientsChanged();"
+ onkeypress="awRecipientKeyPress(event, this);"
+ onkeydown="awRecipientKeyDown(event, this);"
+ disableonsend="true">
+ <image class="person-icon"
+ onclick="this.parentNode.select();"/>
+ </textbox>
+ </listcell>
+ </listitem>
+ </listbox>
+ <hbox align="center">
+ <label value="&subject.label;" accesskey="&subject.accesskey;" control="msgSubject"/>
+ <textbox id="msgSubject" flex="1" class="toolbar" disableonsend="true" spellcheck="true"
+ oninput="gContentChanged=true;SetComposeWindowTitle();"
+ onkeypress="subjectKeyPress(event);" />
+ </hbox>
+ </vbox>
+ <splitter id="attachmentbucket-sizer" collapse="after"/>
+ <vbox id="attachments-box">
+ <label id="attachmentBucketText" value="&attachments.label;" crop="right"
+ accesskey="&attachments.accesskey;" control="attachmentBucket"/>
+ <listbox id="attachmentBucket"
+ seltype="multiple"
+ flex="1"
+ rows="4"
+ tabindex="-1"
+ context="msgComposeAttachmentContext"
+ disableoncustomize="true"
+ onkeypress="if (event.keyCode == 8 || event.keyCode == 46) RemoveSelectedAttachment();"
+ onclick="AttachmentBucketClicked(event);"
+ ondragover="attachmentBucketObserver.onDragOver(event);"
+ ondrop="attachmentBucketObserver.onDrop(event);"
+ ondragexit="attachmentBucketObserver.onDragExit(event);"/>
+ </vbox>
+ </hbox>
+ </toolbar>
+
+ <!-- These toolbar items get filled out from the editorOverlay -->
+ <toolbar id="FormatToolbar"
+ class="chromeclass-toolbar"
+ persist="collapsed"
+ grippytooltiptext="&formatToolbar.tooltip;"
+ toolbarname="&showFormatToolbarCmd.label;"
+ accesskey="&showFormatToolbarCmd.accesskey;"
+ customizable="true"
+ defaultset="paragraph-select-container,font-face-select-container,color-buttons-container,DecreaseFontSizeButton,IncreaseFontSizeButton,separator,boldButton,italicButton,underlineButton,separator,ulButton,olButton,outdentButton,indentButton,separator,AlignPopupButton,InsertPopupButton,smileButtonMenu"
+ mode="icons"
+ iconsize="small"
+ defaultmode="icons"
+ defaulticonsize="small"
+ context="toolbar-context-menu"
+ nowindowdrag="true">
+ <toolbaritem id="paragraph-select-container"/>
+ <toolbaritem id="font-face-select-container"/>
+ <toolbaritem id="color-buttons-container"
+ disableoncustomize="true"/>
+ <toolbarbutton id="DecreaseFontSizeButton"/>
+ <toolbarbutton id="IncreaseFontSizeButton"/>
+ <toolbarbutton id="boldButton"/>
+ <toolbarbutton id="italicButton"/>
+ <toolbarbutton id="underlineButton"/>
+ <toolbarbutton id="ulButton"/>
+ <toolbarbutton id="olButton"/>
+ <toolbarbutton id="outdentButton"/>
+ <toolbarbutton id="indentButton"/>
+ <toolbarbutton id="AlignPopupButton"/>
+ <toolbarbutton id="InsertPopupButton"/>
+ <toolbarbutton id="smileButtonMenu"/>
+ </toolbar>
+
+ <toolbarpalette id="MsgComposeToolbarPalette">
+ <toolbarbutton id="print-button"
+ label="&printButton.label;"
+ tooltiptext="&printButton.tooltip;"/>
+ <toolbarbutton id="button-security"
+ type="menu-button"
+ class="toolbarbutton-1"
+ label="&securityButton.label;"
+ tooltiptext="&securityButton.tooltip;"
+ oncommand="doSecurityButton();">
+ <menupopup onpopupshowing="setSecuritySettings(2);">
+ <menuitem id="menu_securityEncryptRequire2"
+ type="checkbox"
+ label="&menu_securityEncryptRequire.label;"
+ accesskey="&menu_securityEncryptRequire.accesskey;"
+ oncommand="setNextCommand('encryptMessage');"/>
+ <menuitem id="menu_securitySign2"
+ type="checkbox"
+ label="&menu_securitySign.label;"
+ accesskey="&menu_securitySign.accesskey;"
+ oncommand="setNextCommand('signMessage');"/>
+ <menuseparator id="smimeToolbarButtonSeparator"/>
+ <menuitem id="menu_securityStatus2"
+ label="&menu_securityStatus.label;"
+ accesskey="&menu_securityStatus.accesskey;"
+ oncommand="setNextCommand('show');"/>
+ </menupopup>
+ </toolbarbutton>
+ </toolbarpalette>
+
+ </toolbox>
+
+ <splitter id="compose-toolbar-sizer"
+ resizeafter="grow"
+ onmousedown="awSizerListen();"
+ oncommand="awSizerResized(this);">
+ <observes element="MsgHeadersToolbar" attribute="collapsed"/>
+ </splitter>
+
+ <!-- sidebar/toolbar/content/status -->
+ <hbox id="sidebar-parent" flex="1">
+ <!-- From sidebarOverlay.xul -->
+ <vbox id="sidebar-box" class="chromeclass-extrachrome" hidden="true"/>
+ <splitter id="sidebar-splitter" class="chromeclass-extrachrome" hidden="true"/>
+
+ <!-- The mail message body frame -->
+ <vbox id="appcontent" flex="1">
+ <findbar id="FindToolbar" browserid="content-frame"/>
+ <editor id="content-frame"
+ type="content"
+ primary="true"
+ src="about:blank"
+ name="browser.message.body"
+ minheight="100"
+ flex="1"
+ ondblclick="EditorDblClick(event);"
+ context="contentAreaContextMenu"/>
+ </vbox>
+ </hbox>
+
+ <hbox>
+ <notificationbox id="attachmentNotificationBox"
+ flex="1"
+ notificationside="bottom"/>
+ </hbox>
+
+ <statusbar id="status-bar"
+ class="chromeclass-status">
+ <statusbarpanel id="component-bar"/>
+ <statusbarpanel id="statusText"
+ flex="1"/>
+ <statusbarpanel id="statusbar-progresspanel"
+ class="statusbarpanel-progress"
+ collapsed="true">
+ <progressmeter id="compose-progressmeter"
+ class="progressmeter-statusbar"
+ mode="normal"
+ value="0"/>
+ </statusbarpanel>
+ <statusbarpanel id="signing-status"
+ class="statusbarpanel-iconic"
+ collapsed="true"
+ oncommand="showMessageComposeSecurityStatus();"/>
+ <statusbarpanel id="encryption-status"
+ class="statusbarpanel-iconic"
+ collapsed="true"
+ oncommand="showMessageComposeSecurityStatus();"/>
+ <statusbarpanel id="offline-status"
+ class="statusbarpanel-iconic"
+ checkfunc="MailCheckBeforeOfflineChange();"/>
+ </statusbar>
+
+</window>
diff --git a/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul b/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul
new file mode 100644
index 0000000000..f3558873d2
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<overlay id="msgComposeContextOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <menupopup id="contentAreaContextMenu"
+ onpopupshowing="return event.target != this ||
+ openEditorContextMenu(this);">
+ <!-- Hide the menuitems by default so they do not to show up
+ in the sidebar context menu. -->
+ <menuitem id="context-pasteNoFormatting"
+ insertafter="context-paste"
+ hidden="true"
+ command="cmd_pasteNoFormatting"/>
+ <menuitem id="context-pasteQuote"
+ insertafter="context-pasteNoFormatting"
+ hidden="true"
+ command="cmd_pasteQuote"/>
+ </menupopup>
+</overlay>
diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js
new file mode 100644
index 0000000000..f67d919f63
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js
@@ -0,0 +1,30 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+function Startup() {
+ let value = document.getElementById("mail.compose.autosave").value;
+ EnableElementById("autoSaveInterval", value, false);
+}
+
+function EnableMailComposeAutosaveInterval(aValue) {
+ let focus = (document.getElementById("autoSave") == document.commandDispatcher.focusedElement);
+ EnableElementById("autoSaveInterval", aValue, focus);
+}
+
+function PopulateFonts() {
+ var fontsList = document.getElementById("fontSelect");
+ try {
+ var enumerator = Cc["@mozilla.org/gfx/fontenumerator;1"]
+ .getService(Ci.nsIFontEnumerator);
+ var localFonts = enumerator.EnumerateAllFonts();
+ for (let font of localFonts)
+ if (font != "serif" && font != "sans-serif" && font != "monospace")
+ fontsList.appendItem(font, font);
+ } catch (ex) { }
+
+ // Select the item after the list is completely generated.
+ document.getElementById(fontsList.getAttribute("preference"))
+ .setElementValue(fontsList);
+}
diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul
new file mode 100644
index 0000000000..c6f3b4fac8
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul
@@ -0,0 +1,212 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % pref-composing_messagesDTD SYSTEM "chrome://messenger/locale/messengercompose/pref-composing_messages.dtd">
+%pref-composing_messagesDTD;
+<!ENTITY % editorOverlayDTD SYSTEM "chrome://editor/locale/editorOverlay.dtd">
+%editorOverlayDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="composing_messages_pane"
+ label="&pref.composing.messages.title;"
+ script="chrome://messenger/content/messengercompose/pref-composing_messages.js"
+ onpaneload="this.PopulateFonts();">
+
+ <preferences id="composing_messages_preferences">
+ <preference id="mail.forward_message_mode"
+ name="mail.forward_message_mode"
+ type="int"/>
+ <preference id="mail.reply_quote_inline"
+ name="mail.reply_quote_inline"
+ type="bool"/>
+ <preference id="mail.compose.autosave"
+ name="mail.compose.autosave"
+ type="bool"
+ onchange="EnableMailComposeAutosaveInterval(this.value);"/>
+ <preference id="mail.compose.autosaveinterval"
+ name="mail.compose.autosaveinterval"
+ type="int"/>
+ <preference id="mail.warn_on_send_accel_key"
+ name="mail.warn_on_send_accel_key"
+ type="bool"/>
+ <preference id="mailnews.wraplength"
+ name="mailnews.wraplength"
+ type="int"/>
+ <preference id="msgcompose.font_face"
+ name="msgcompose.font_face"
+ type="string"/>
+ <preference id="msgcompose.font_size"
+ name="msgcompose.font_size"
+ type="string"/>
+ <preference id="msgcompose.text_color"
+ name="msgcompose.text_color"
+ type="string"/>
+ <preference id="msgcompose.background_color"
+ name="msgcompose.background_color"
+ type="string"/>
+ <preference id="mailnews.reply_header_type"
+ name="mailnews.reply_header_type"
+ type="int"/>
+ <preference id="mail.compose.default_to_paragraph"
+ name="mail.compose.default_to_paragraph"
+ type="bool"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&generalComposing.label;"/>
+
+ <radiogroup id="forwardMessageMode"
+ orient="horizontal"
+ align="center"
+ preference="mail.forward_message_mode">
+ <label value="&forwardMsg.label;" control="forwardMessageMode"/>
+ <radio value="2"
+ label="&inline.label;"
+ accesskey="&inline.accesskey;"/>
+ <radio value="0"
+ label="&asAttachment.label;"
+ accesskey="&asAttachment.accesskey;"/>
+ </radiogroup>
+
+ <checkbox id="replyQuoteInline" label="&replyQuoteInline.label;"
+ preference="mail.reply_quote_inline"
+ accesskey="&replyQuoteInline.accesskey;"/>
+
+ <hbox align="center">
+ <checkbox id="autoSave" label="&autoSave.label;"
+ preference="mail.compose.autosave"
+ accesskey="&autoSave.accesskey;"
+ aria-labelledby="autoSave autoSaveInterval autoSaveEnd"/>
+ <textbox id="autoSaveInterval"
+ type="number"
+ min="1"
+ max="99"
+ size="2"
+ preference="mail.compose.autosaveinterval"
+ aria-labelledby="autoSave autoSaveInterval autoSaveEnd"/>
+ <label id="autoSaveEnd" value="&autoSaveEnd.label;"/>
+ </hbox>
+
+ <checkbox id="mailWarnOnSendAccelKey"
+ label="&warnOnSendAccelKey.label;"
+ accesskey="&warnOnSendAccelKey.accesskey;"
+ preference="mail.warn_on_send_accel_key"/>
+
+ <hbox align="center">
+ <label id="wrapOutLabel"
+ value="&wrapOutMsg.label;"
+ accesskey="&wrapOutMsg.accesskey;"
+ control="wrapLength"/>
+ <textbox id="wrapLength"
+ type="number"
+ min="0"
+ max="999"
+ size="3"
+ preference="mailnews.wraplength"
+ aria-labelledby="wrapOutLabel wrapLength wrapOutEnd"/>
+ <label id="wrapOutEnd" value="&char.label;"/>
+ </hbox>
+ <hbox align="center">
+ <label id="selectHeaderType"
+ value="&selectHeaderType.label;"
+ accesskey="&selectHeaderType.accesskey;"
+ control="mailNewsReplyList"/>
+ <menulist id="mailNewsReplyList"
+ preference="mailnews.reply_header_type">
+ <menupopup>
+ <menuitem value="0"
+ label="&noReplyOption.label;"/>
+ <menuitem value="1"
+ label="&authorWroteOption.label;"/>
+ <menuitem value="2"
+ label="&onDateAuthorWroteOption.label;"/>
+ <menuitem value="3"
+ label="&authorWroteOnDateOption.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ </groupbox>
+
+ <!-- Composing Mail -->
+
+ <groupbox align="start">
+ <caption label="&defaultMessagesHeader.label;"/>
+ <grid>
+ <columns>
+ <column/>
+ <column/>
+ </columns>
+
+ <rows>
+ <row align="center">
+ <label value="&font.label;"
+ accesskey="&font.accesskey;"
+ control="fontSelect"/>
+ <menulist id="fontSelect" preference="msgcompose.font_face">
+ <menupopup>
+ <menuitem value=""
+ label="&fontVarWidth.label;"/>
+ <menuitem value="tt"
+ label="&fontFixedWidth.label;"/>
+ <menuseparator/>
+ <menuitem value="Helvetica, Arial, sans-serif"
+ label="&fontHelvetica.label;"/>
+ <menuitem value="Times New Roman, Times, serif"
+ label="&fontTimes.label;"/>
+ <menuitem value="Courier New, Courier, monospace"
+ label="&fontCourier.label;"/>
+ <menuseparator/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row align="center">
+ <label value="&size.label;"
+ accesskey="&size.accesskey;"
+ control="fontSizeSelect"/>
+ <hbox align="center">
+ <menulist id="fontSizeSelect" preference="msgcompose.font_size">
+ <menupopup>
+ <menuitem value="x-small" label="&size-tinyCmd.label;"/>
+ <menuitem value="small" label="&size-smallCmd.label;"/>
+ <menuitem value="medium" label="&size-mediumCmd.label;"/>
+ <menuitem value="large" label="&size-largeCmd.label;"/>
+ <menuitem value="x-large" label="&size-extraLargeCmd.label;"/>
+ <menuitem value="xx-large" label="&size-hugeCmd.label;"/>
+ </menupopup>
+ </menulist>
+ <label value="&fontColor.label;"
+ accesskey="&fontColor.accesskey;"
+ control="msgComposeTextColor"/>
+ <colorpicker id="msgComposeTextColor"
+ type="button"
+ preference="msgcompose.text_color"/>
+ <label value="&bgColor.label;"
+ accesskey="&bgColor.accesskey;"
+ control="msgComposeBackgroundColor"/>
+ <colorpicker id="msgComposeBackgroundColor"
+ type="button"
+ preference="msgcompose.background_color"/>
+ </hbox>
+ </row>
+ </rows>
+ </grid>
+ <separator class="thin"/>
+ <description>&defaultCompose.label;</description>
+ <radiogroup id="defaultCompose"
+ class="indent"
+ preference="mail.compose.default_to_paragraph">
+ <radio value="false"
+ label="&defaultBodyText.label;"
+ accesskey="&defaultBodyText.accesskey;"/>
+ <radio value="true"
+ label="&defaultParagraph.label;"
+ accesskey="&defaultParagraph.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js
new file mode 100644
index 0000000000..b5c31d424d
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js
@@ -0,0 +1,151 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var gListbox;
+var gPref;
+var gError;
+
+function Startup()
+{
+ // Store some useful elements in globals.
+ gListbox =
+ {
+ html: document.getElementById("html_domains"),
+ plaintext: document.getElementById("plaintext_domains")
+ };
+ gPref =
+ {
+ html_domains: document.getElementById("mailnews.html_domains"),
+ plaintext_domains: document.getElementById("mailnews.plaintext_domains")
+ };
+ gError = document.getElementById("formatting_error_msg");
+
+ // Make it easier to access the pref pane from onsync.
+ gListbox.html.pane = this;
+ gListbox.plaintext.pane = this;
+}
+
+function AddDomain(aType)
+{
+ var domains = null;
+ var result = {value: null};
+ if (Services.prompt.prompt(window, gListbox[aType].getAttribute("title"),
+ gListbox[aType].getAttribute("msg"), result,
+ null, {value: 0}))
+ domains = result.value.replace(/ /g, "").split(",");
+
+ if (domains)
+ {
+ var added = false;
+ var removed = false;
+ var listbox = gListbox[aType];
+ var other = aType == "html" ? gListbox.plaintext : gListbox.html;
+ for (var i = 0; i < domains.length; i++)
+ {
+ var domainName = TidyDomainName(domains[i], true);
+ if (domainName)
+ {
+ if (!DomainFirstMatch(listbox, domainName))
+ {
+ var match = DomainFirstMatch(other, domainName);
+ if (match)
+ {
+ match.remove();
+ removed = true;
+ }
+ listbox.appendItem(domainName);
+ added = true;
+ }
+ }
+ }
+ if (added)
+ listbox.doCommand();
+ if (removed)
+ other.doCommand();
+ }
+}
+
+function TidyDomainName(aDomain, aWarn)
+{
+ // See if it is an email address and if so take just the domain part.
+ aDomain = aDomain.replace(/.*@/, "");
+
+ // See if it is a valid domain otherwise return null.
+ if (!/.\../.test(aDomain))
+ {
+ if (aWarn)
+ {
+ var errorMsg = gError.getAttribute("inverr").replace(/@string@/, aDomain);
+ Services.prompt.alert(window, gError.getAttribute("title"), errorMsg);
+ }
+ return null;
+ }
+
+ // Finally make sure the domain is in lowercase.
+ return aDomain.toLowerCase();
+}
+
+function DomainFirstMatch(aListbox, aDomain)
+{
+ return aListbox.getElementsByAttribute("label", aDomain).item(0);
+}
+
+function RemoveDomains(aType, aEvent)
+{
+ if (aEvent && aEvent.keyCode != KeyEvent.DOM_VK_DELETE &&
+ aEvent.keyCode != KeyEvent.DOM_VK_BACK_SPACE)
+ return;
+
+ var nextNode = null;
+ var listbox = gListbox[aType];
+
+ while (listbox.selectedItem)
+ {
+ var selectedNode = listbox.selectedItem;
+ nextNode = selectedNode.nextSibling || selectedNode.previousSibling;
+ selectedNode.remove();
+ }
+
+ if (nextNode)
+ listbox.selectItem(nextNode);
+
+ listbox.doCommand();
+}
+
+function ReadDomains(aListbox)
+{
+ var arrayOfPrefs = gPref[aListbox.id].value.replace(/ /g, "").split(",");
+ if (arrayOfPrefs)
+ {
+ var i;
+ // Check all the existing items, remove any that are not needed and
+ // make sure we do not duplicate any by removing from pref array.
+ var domains = aListbox.getElementsByAttribute("label", "*");
+ if (domains)
+ {
+ for (i = domains.length; --i >= 0; )
+ {
+ var domain = domains[i];
+ var index = arrayOfPrefs.indexOf(domain.label);
+ if (index > -1)
+ arrayOfPrefs.splice(index, 1);
+ else
+ domain.remove();
+ }
+ }
+ for (i = 0; i < arrayOfPrefs.length; i++)
+ {
+ var str = TidyDomainName(arrayOfPrefs[i], false);
+ if (str)
+ aListbox.appendItem(str);
+ }
+ }
+}
+
+function WriteDomains(aListbox)
+{
+ var domains = aListbox.getElementsByAttribute("label", "*");
+ return Array.from(domains, e => e.label).join(",");
+}
diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul
new file mode 100644
index 0000000000..0167a0990f
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/messengercompose/pref-formatting.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="formatting_pane"
+ label="&pref.formatting.title;"
+ script="chrome://messenger/content/messengercompose/pref-formatting.js">
+ <preferences id="formatting_preferences">
+ <preference id="mail.default_html_action"
+ name="mail.default_html_action"
+ type="int"/>
+ <preference id="mailnews.html_domains"
+ name="mailnews.html_domains"
+ type="string"/>
+ <preference id="mailnews.plaintext_domains"
+ name="mailnews.plaintext_domains"
+ type="string"/>
+ <preference id="mailnews.sendformat.auto_downgrade"
+ name="mailnews.sendformat.auto_downgrade"
+ type="bool"/>
+ </preferences>
+
+ <data id="formatting_error_msg"
+ title="&domainnameError.title;"
+ inverr="&invalidEntryError.label;"/>
+
+ <description>&sendMaildesc.label;</description>
+
+ <radiogroup id="mailDefaultHTMLAction"
+ preference="mail.default_html_action">
+ <radio value="0"
+ label="&askMe.label;"
+ accesskey="&askMe.accesskey;"/>
+ <radio value="1"
+ label="&convertPlain2.label;"
+ accesskey="&convertPlain2.accesskey;"/>
+ <radio value="2"
+ label="&sendHTML2.label;"
+ accesskey="&sendHTML2.accesskey;"/>
+ <radio value="3"
+ label="&sendBoth2.label;"
+ accesskey="&sendBoth2.accesskey;"/>
+ </radiogroup>
+
+ <groupbox flex="1">
+ <caption label="&domain.title;"/>
+
+ <description>&domaindesc.label;</description>
+
+ <hbox flex="1">
+ <vbox flex="1">
+ <label value="&HTMLdomaintitle.label;"
+ accesskey="&HTMLdomaintitle.accesskey;"
+ control="html_domains"/>
+ <hbox flex="1">
+ <listbox id="html_domains"
+ title="&add.htmltitle;"
+ msg="&add.htmldomain;"
+ flex="1"
+ seltype="multiple"
+ preference="mailnews.html_domains"
+ onsyncfrompreference="return this.pane.ReadDomains(this);"
+ onsynctopreference="return this.pane.WriteDomains(this);"
+ onkeypress="RemoveDomains('html', event);"/>
+ <vbox>
+ <button label="&AddButton.label;"
+ accesskey="&AddHtmlDomain.accesskey;"
+ oncommand="AddDomain('html');">
+ <observes element="html_domains" attribute="disabled"/>
+ </button>
+ <button label="&DeleteButton.label;"
+ accesskey="&DeleteHtmlDomain.accesskey;"
+ oncommand="RemoveDomains('html', null);">
+ <observes element="html_domains" attribute="disabled"/>
+ </button>
+ </vbox>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <label value="&PlainTexttitle.label;"
+ accesskey="&PlainTexttitle.accesskey;"
+ control="plaintext_domains"/>
+ <hbox flex="1">
+ <listbox id="plaintext_domains"
+ title="&add.plaintexttitle;"
+ msg="&add.plaintextdomain;"
+ flex="1"
+ seltype="multiple"
+ preference="mailnews.plaintext_domains"
+ onsyncfrompreference="return this.pane.ReadDomains(this);"
+ onsynctopreference="return this.pane.WriteDomains(this);"
+ onkeypress="RemoveDomains('plaintext', event);"/>
+ <vbox>
+ <button label="&AddButton.label;"
+ accesskey="&AddPlainText.accesskey;"
+ oncommand="AddDomain('plaintext');">
+ <observes element="plaintext_domains" attribute="disabled"/>
+ </button>
+ <button label="&DeleteButton.label;"
+ accesskey="&DeletePlainText.accesskey;"
+ oncommand="RemoveDomains('plaintext', null);">
+ <observes element="plaintext_domains" attribute="disabled"/>
+ </button>
+ </vbox>
+ </hbox>
+ </vbox>
+ </hbox>
+ </groupbox>
+
+ <checkbox id="autoDowngrade"
+ label="&autoDowngrade.label;"
+ accesskey="&autoDowngrade.accesskey;"
+ preference="mailnews.sendformat.auto_downgrade"/>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/compose/jar.mn b/comm/suite/mailnews/components/compose/jar.mn
new file mode 100644
index 0000000000..c9465fa8d7
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/jar.mn
@@ -0,0 +1,14 @@
+# 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/.
+
+messenger.jar:
+ content/messenger/messengercompose/pref-composing_messages.xul (content/prefs/pref-composing_messages.xul)
+ content/messenger/messengercompose/pref-composing_messages.js (content/prefs/pref-composing_messages.js)
+ content/messenger/messengercompose/pref-formatting.xul (content/prefs/pref-formatting.xul)
+ content/messenger/messengercompose/pref-formatting.js (content/prefs/pref-formatting.js)
+* content/messenger/messengercompose/messengercompose.xul (content/messengercompose.xul)
+ content/messenger/messengercompose/mailComposeOverlay.xul (content/mailComposeOverlay.xul)
+ content/messenger/messengercompose/msgComposeContextOverlay.xul (content/msgComposeContextOverlay.xul)
+ content/messenger/messengercompose/MsgComposeCommands.js (content/MsgComposeCommands.js)
+ content/messenger/messengercompose/addressingWidgetOverlay.js (content/addressingWidgetOverlay.js)
diff --git a/comm/suite/mailnews/components/compose/moz.build b/comm/suite/mailnews/components/compose/moz.build
new file mode 100644
index 0000000000..de5cd1bf81
--- /dev/null
+++ b/comm/suite/mailnews/components/compose/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/mailnews/components/moz.build b/comm/suite/mailnews/components/moz.build
new file mode 100644
index 0000000000..9d5b9f36ad
--- /dev/null
+++ b/comm/suite/mailnews/components/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ "compose",
+ "prefs",
+ "addrbook",
+ "smime",
+]
diff --git a/comm/suite/mailnews/components/prefs/content/mailPrefsOverlay.xul b/comm/suite/mailnews/components/prefs/content/mailPrefsOverlay.xul
new file mode 100644
index 0000000000..2a4acef93b
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/mailPrefsOverlay.xul
@@ -0,0 +1,102 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+
+<!DOCTYPE overlay [
+<!ENTITY % mailPrefsOverlayDTD SYSTEM "chrome://messenger/locale/mailPrefsOverlay.dtd">
+%mailPrefsOverlayDTD;
+]>
+
+<overlay id="mailPrefsOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <preferences id="appearance_preferences">
+ <preference id="general.startup.mail"
+ name="general.startup.mail"
+ type="bool"/>
+ <preference id="general.startup.addressbook"
+ name="general.startup.addressbook"
+ type="bool"/>
+ </preferences>
+
+ <!-- mail startup toggle -->
+ <groupbox id="generalStartupPreferences">
+ <checkbox id="generalStartupMail"
+ insertafter="generalStartupBrowser"
+ label="&mail.label;"
+ accesskey="&mail.accesskey;"
+ preference="general.startup.mail"/>
+ <checkbox id="generalStartupAddressBook"
+ insertafter="generalStartupEditor,generalStartupMail"
+ label="&addressbook.label;"
+ accesskey="&addressbook.accesskey;"
+ preference="general.startup.addressbook"/>
+ </groupbox>
+
+ <!-- category tree entries for mail/news -->
+ <treechildren id="prefsPanelChildren">
+ <treeitem container="true"
+ id="mailnewsItem"
+ insertafter="navigatorItem"
+ label="&mail.label;"
+ prefpane="mailnews_pane"
+ url="chrome://messenger/content/pref-mailnews.xul"
+ helpTopic="mail_prefs_general">
+ <treechildren id="messengerChildren">
+ <treeitem id="viewingMessagesItem"
+ label="&viewingMessages.label;"
+ prefpane="viewing_messages_pane"
+ url="chrome://messenger/content/pref-viewing_messages.xul"
+ helpTopic="mail_prefs_display"/>
+ <treeitem id="notificationsItem"
+ label="&notifications.label;"
+ prefpane="notifications_pane"
+ url="chrome://messenger/content/pref-notifications.xul"
+ helpTopic="mail_prefs_notifications"/>
+ <treeitem id="composingItem"
+ label="&composingMessages.label;"
+ prefpane="composing_messages_pane"
+ url="chrome://messenger/content/messengercompose/pref-composing_messages.xul"
+ helpTopic="mail_prefs_messages"/>
+ <treeitem id="formattingItem"
+ label="&format.label;"
+ prefpane="formatting_pane"
+ url="chrome://messenger/content/messengercompose/pref-formatting.xul"
+ helpTopic="mail_prefs_formatting"/>
+ <treeitem id="addressItem"
+ label="&address.label;"
+ prefpane="addressing_pane"
+ url="chrome://messenger/content/addressbook/pref-addressing.xul"
+ helpTopic="mail_prefs_addressing"/>
+ <treeitem id="junkItem"
+ label="&junk.label;"
+ prefpane="junk_pane"
+ url="chrome://messenger/content/pref-junk.xul"
+ helpTopic="mail-prefs-junk"/>
+ <treeitem id="tagsItem"
+ label="&tags.label;"
+ prefpane="tags_pane"
+ url="chrome://messenger/content/pref-tags.xul"
+ helpTopic="mail-prefs-tags"/>
+ <treeitem id="receiptsItem"
+ label="&return.label;"
+ prefpane="receipts_pane"
+ url="chrome://messenger/content/pref-receipts.xul"
+ helpTopic="mail-prefs-receipts"/>
+ <treeitem id="characterEncodingItem"
+ label="&characterEncoding2.label;"
+ prefpane="character_encoding_pane"
+ url="chrome://messenger/content/pref-character_encoding.xul"
+ helpTopic="mail_prefs_text_encoding"/>
+ <treeitem id="offlineItem"
+ label="&networkStorage.label;"
+ prefpane="offline_pane"
+ url="chrome://messenger/content/pref-offline.xul"
+ helpTopic="mail_prefs_offline"/>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/content/pref-character_encoding.js b/comm/suite/mailnews/components/prefs/content/pref-character_encoding.js
new file mode 100644
index 0000000000..0ae30e9b1f
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-character_encoding.js
@@ -0,0 +1,41 @@
+/* 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/. */
+
+// The contents of this file will be loaded into the scope of the object
+// <prefpane id="character_encoding_pane">!
+
+var updatingPref = false;
+
+function Startup ()
+{
+ PrefChanged(document.getElementById('mailnews.view_default_charset'));
+ PrefChanged(document.getElementById('mailnews.send_default_charset'));
+}
+
+function PrefChanged(aPref)
+{
+ if (updatingPref)
+ return;
+
+ var id = aPref.id.substr(9, 4) + "DefaultCharsetList";
+ var menulist = document.getElementById(id);
+ if (!aPref.hasUserValue)
+ menulist.selectedIndex = 0;
+ else {
+ var bundle = document.getElementById("charsetBundle");
+ menulist.value = bundle.getString(aPref.value.toLowerCase());
+ }
+}
+
+function UpdatePref(aMenulist)
+{
+ updatingPref = true;
+ var id = "mailnews." + aMenulist.id.substr(0, 4) + "_default_charset";
+ var pref = document.getElementById(id);
+ if (aMenulist.selectedIndex)
+ pref.value = aMenulist.value;
+ else
+ pref.value = undefined; // reset to default
+ updatingPref = false;
+}
diff --git a/comm/suite/mailnews/components/prefs/content/pref-character_encoding.xul b/comm/suite/mailnews/components/prefs/content/pref-character_encoding.xul
new file mode 100755
index 0000000000..009f5f49de
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-character_encoding.xul
@@ -0,0 +1,111 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % prefCharacterEncodingDTD SYSTEM "chrome://messenger/locale/pref-character_encoding.dtd"> %prefCharacterEncodingDTD;
+ <!ENTITY % prefUtilitiesDTD SYSTEM "chrome://communicator/locale/pref/prefutilities.dtd"> %prefUtilitiesDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="character_encoding_pane"
+ label="&pref.character.encoding2.title;"
+ script="chrome://messenger/content/pref-character_encoding.js">
+ <preferences id="character_encoding_preferences">
+ <preference id="mailnews.view_default_charset"
+ name="mailnews.view_default_charset"
+ type="wstring"
+ onchange="PrefChanged(this);"/>
+ <preference id="mail.strictly_mime"
+ name="mail.strictly_mime"
+ type="bool"/>
+ <preference id="mailnews.send_default_charset"
+ name="mailnews.send_default_charset"
+ type="wstring"
+ onchange="PrefChanged(this);"/>
+ <preference id="mailnews.reply_in_default_charset"
+ name="mailnews.reply_in_default_charset"
+ type="bool"/>
+ </preferences>
+
+ <groupbox align="start">
+ <caption label="&messageDisplay.caption;"/>
+ <hbox align="center">
+ <label control="viewDefaultCharsetList"
+ value="&viewFallbackCharset2.label;"
+ accesskey="&viewFallbackCharset2.accesskey;"/>
+ <menulist id="viewDefaultCharsetList"
+ oncommand="UpdatePref(this);">
+ <menupopup>
+ <menuitem label="&FallbackCharset.auto;" value=""/>
+ <menuitem label="&FallbackCharset.unicode;" value="UTF-8"/>
+ <menuitem label="&FallbackCharset.other;" value="windows-1252"/>
+ <menuseparator/>
+ <menuitem label="&FallbackCharset.arabic;" value="windows-1256"/>
+ <menuitem label="&FallbackCharset.baltic;" value="windows-1257"/>
+ <menuitem label="&FallbackCharset.ceiso;" value="ISO-8859-2"/>
+ <menuitem label="&FallbackCharset.cewindows;" value="windows-1250"/>
+ <menuitem label="&FallbackCharset.simplified;" value="gbk"/>
+ <menuitem label="&FallbackCharset.traditional;" value="Big5"/>
+ <menuitem label="&FallbackCharset.cyrillic;" value="windows-1251"/>
+ <menuitem label="&FallbackCharset.greek;" value="ISO-8859-7"/>
+ <menuitem label="&FallbackCharset.hebrew;" value="windows-1255"/>
+ <menuitem label="&FallbackCharset.japanese;" value="Shift_JIS"/>
+ <menuitem label="&FallbackCharset.korean;" value="EUC-KR"/>
+ <menuitem label="&FallbackCharset.thai;" value="windows-874"/>
+ <menuitem label="&FallbackCharset.turkish;" value="windows-1254"/>
+ <menuitem label="&FallbackCharset.vietnamese;" value="windows-1258"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <description>&viewFallbackCharset.desc;</description>
+ </groupbox>
+
+ <!-- Composing Mail -->
+ <groupbox align="start">
+ <caption label="&composingMessages.caption;"/>
+
+ <checkbox id="strictlyMime"
+ label="&useMIME.label;"
+ accesskey="&useMIME.accesskey;"
+ preference="mail.strictly_mime"/>
+
+ <hbox align="center">
+ <label value="&sendDefaultCharset2.label;"
+ accesskey="&sendDefaultCharset2.accesskey;"
+ control="sendDefaultCharsetList"/>
+ <menulist id="sendDefaultCharsetList"
+ oncommand="UpdatePref(this);">
+ <menupopup>
+ <menuitem label="&FallbackCharset.auto;" value=""/>
+ <menuitem label="&FallbackCharset.unicode;" value="UTF-8"/>
+ <menuitem label="&FallbackCharset.other;" value="windows-1252"/>
+ <menuseparator/>
+ <menuitem label="&FallbackCharset.arabic;" value="windows-1256"/>
+ <menuitem label="&FallbackCharset.baltic;" value="windows-1257"/>
+ <menuitem label="&FallbackCharset.ceiso;" value="ISO-8859-2"/>
+ <menuitem label="&FallbackCharset.cewindows;" value="windows-1250"/>
+ <menuitem label="&FallbackCharset.simplified;" value="gbk"/>
+ <menuitem label="&FallbackCharset.traditional;" value="Big5"/>
+ <menuitem label="&FallbackCharset.cyrillic;" value="windows-1251"/>
+ <menuitem label="&FallbackCharset.greek;" value="ISO-8859-7"/>
+ <menuitem label="&FallbackCharset.hebrew;" value="windows-1255"/>
+ <menuitem label="&FallbackCharset.japanese;" value="Shift_JIS"/>
+ <menuitem label="&FallbackCharset.korean;" value="EUC-KR"/>
+ <menuitem label="&FallbackCharset.thai;" value="windows-874"/>
+ <menuitem label="&FallbackCharset.turkish;" value="windows-1254"/>
+ <menuitem label="&FallbackCharset.vietnamese;" value="windows-1258"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <checkbox id="replyInDefaultCharset"
+ label="&replyInDefaultCharset3.label;"
+ accesskey="&replyInDefaultCharset3.accesskey;"
+ preference="mailnews.reply_in_default_charset"/>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/content/pref-junk.js b/comm/suite/mailnews/components/prefs/content/pref-junk.js
new file mode 100644
index 0000000000..9f31050c46
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-junk.js
@@ -0,0 +1,45 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function Startup()
+{
+ UpdateDependentElement("manualMark", "manualMarkMode");
+ UpdateDependentElement("enableJunkLogging", "openJunkLog");
+}
+
+function UpdateDependentElement(aBaseId, aDependentId)
+{
+ var pref = document.getElementById(aBaseId).getAttribute("preference");
+ EnableElementById(aDependentId, document.getElementById(pref).value, false);
+}
+
+function OpenJunkLog()
+{
+ window.openDialog("chrome://messenger/content/junkLog.xul",
+ "junkLog",
+ "chrome,modal,titlebar,resizable,centerscreen");
+}
+
+function ResetTrainingData()
+{
+ // make sure the user really wants to do this
+ var bundle = document.getElementById("bundleJunkPreferences");
+ var title = bundle.getString("confirmResetJunkTrainingTitle");
+ var text = bundle.getString("confirmResetJunkTrainingText");
+
+ // if the user says no, then just fall out
+ if (Services.prompt.confirmEx(window, title, text,
+ Services.prompt.STD_YES_NO_BUTTONS |
+ Services.prompt.BUTTON_POS_1_DEFAULT,
+ "", "", "", null, {}))
+ return;
+
+ // otherwise go ahead and remove the training data
+ MailServices.junk.resetTrainingData();
+}
diff --git a/comm/suite/mailnews/components/prefs/content/pref-junk.xul b/comm/suite/mailnews/components/prefs/content/pref-junk.xul
new file mode 100644
index 0000000000..b6d1f4507d
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-junk.xul
@@ -0,0 +1,134 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?>
+
+<!DOCTYPE overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % prefJunkDTD SYSTEM "chrome://messenger/locale/pref-junk.dtd">
+%prefJunkDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="junk_pane"
+ label="&pref.junk.title;"
+ script="chrome://messenger/content/pref-junk.js">
+ <preferences id="junk_preferences">
+ <preference id="mail.spam.manualMark"
+ name="mail.spam.manualMark"
+ type="bool"
+ onchange="EnableElementById('manualMarkMode', this.value, false);"/>
+ <preference id="mail.spam.manualMarkMode"
+ name="mail.spam.manualMarkMode"
+ type="int"/>
+ <preference id="mail.spam.markAsReadOnSpam"
+ name="mail.spam.markAsReadOnSpam"
+ type="bool"/>
+ <preference id="mailnews.ui.junk.manualMarkAsJunkMarksRead"
+ name="mailnews.ui.junk.manualMarkAsJunkMarksRead"
+ type="bool"/>
+ <preference id="mail.spam.logging.enabled"
+ name="mail.spam.logging.enabled"
+ type="bool"
+ onchange="EnableElementById('openJunkLog', this.value, false);"/>
+ <preference id="pref.junk.disable_button.openJunkLog"
+ name="pref.junk.disable_button.openJunkLog"
+ type="string"/>
+ <preference id="pref.junk.disable_button.resetTrainingData"
+ name="pref.junk.disable_button.resetTrainingData"
+ type="string"/>
+ <preference id="mail.phishing.detection.enabled"
+ name="mail.phishing.detection.enabled"
+ type="bool"/>
+ <preference id="mailnews.downloadToTempFile"
+ name="mailnews.downloadToTempFile"
+ type="bool"/>
+ </preferences>
+
+ <stringbundleset id="junkBundleset">
+ <stringbundle id="bundleJunkPreferences"
+ src="chrome://messenger/locale/messenger.properties"/>
+ </stringbundleset>
+
+ <groupbox>
+ <caption label="&junkSettings.caption;"/>
+ <description>&junkMail.intro;</description>
+ <class separator="thin"/>
+
+ <checkbox id="manualMark"
+ label="&manualMark.label;"
+ accesskey="&manualMark.accesskey;"
+ preference="mail.spam.manualMark"/>
+ <radiogroup id="manualMarkMode"
+ class="indent"
+ aria-labelledby="manualMark"
+ preference="mail.spam.manualMarkMode">
+ <radio id="manualMarkMode0"
+ label="&manualMarkModeMove.label;"
+ accesskey="&manualMarkModeMove.accesskey;"
+ value="0"/>
+ <radio id="manualMarkMode1"
+ label="&manualMarkModeDelete.label;"
+ accesskey="&manualMarkModeDelete.accesskey;"
+ value="1"/>
+ </radiogroup>
+
+ <separator class="thin"/>
+
+ <description>&markAsRead.intro;</description>
+ <vbox class="indent">
+ <checkbox id="autoMarkAsRead"
+ label="&autoMarkAsRead.label;"
+ accesskey="&autoMarkAsRead.accesskey;"
+ preference="mail.spam.markAsReadOnSpam"/>
+ <checkbox id="manualMarkAsRead"
+ label="&manualMarkAsRead.label;"
+ accesskey="&manualMarkAsRead.accesskey;"
+ preference="mailnews.ui.junk.manualMarkAsJunkMarksRead"/>
+ </vbox>
+
+ <separator class="thin"/>
+
+ <hbox align="start">
+ <checkbox id="enableJunkLogging"
+ label="&enableJunkLogging.label;"
+ accesskey="&enableJunkLogging.accesskey;"
+ preference="mail.spam.logging.enabled"/>
+ <spacer flex="1"/>
+ <button id="openJunkLog"
+ label="&openJunkLog.label;"
+ accesskey="&openJunkLog.accesskey;"
+ preference="pref.junk.disable_button.openJunkLog"
+ oncommand="OpenJunkLog();"/>
+ </hbox>
+ <hbox align="start">
+ <spacer flex="1"/>
+ <button id="resetTrainingData"
+ label="&resetTrainingData.label;"
+ accesskey="&resetTrainingData.accesskey;"
+ preference="pref.junk.disable_button.resetTrainingData"
+ oncommand="ResetTrainingData();"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&pref.suspectMail.caption;"/>
+
+ <checkbox id="enablePhishingDetector"
+ label="&enablePhishingDetector.label;"
+ accesskey="&enablePhishingDetector.accesskey;"
+ preference="mail.phishing.detection.enabled"/>
+
+ <separator class="thin"/>
+
+ <checkbox id="enableAntiVirusQuarantine"
+ label="&antiVirus.label;"
+ accesskey="&antiVirus.accesskey;"
+ preference="mailnews.downloadToTempFile"/>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/content/pref-mailnews.js b/comm/suite/mailnews/components/prefs/content/pref-mailnews.js
new file mode 100644
index 0000000000..f057fb46ae
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-mailnews.js
@@ -0,0 +1,25 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+function Startup()
+{
+ startPageCheck();
+}
+
+function startPageCheck()
+{
+ var checked = document.getElementById("mailnews.start_page.enabled").value;
+ var urlElement = document.getElementById("mailnewsStartPageUrl");
+ var prefLocked = document.getElementById("mailnews.start_page.url").locked;
+
+ urlElement.disabled = !checked || prefLocked;
+}
+
+function setHomePageToDefaultPage()
+{
+ var startPagePref = document.getElementById("mailnews.start_page.url");
+
+ startPagePref.value = startPagePref.defaultValue;
+}
diff --git a/comm/suite/mailnews/components/prefs/content/pref-mailnews.xul b/comm/suite/mailnews/components/prefs/content/pref-mailnews.xul
new file mode 100644
index 0000000000..e3fbeb12e8
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-mailnews.xul
@@ -0,0 +1,141 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?>
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE overlay [
+<!ENTITY % prefMailnewsDTD SYSTEM "chrome://messenger/locale/pref-mailnews.dtd">
+%prefMailnewsDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="mailnews_pane"
+ label="&pref.mailnews.title;"
+ script="chrome://messenger/content/pref-mailnews.js">
+
+ <preferences id="mailnews_preferences">
+ <preference id="mailnews.confirm.moveFoldersToTrash"
+ name="mailnews.confirm.moveFoldersToTrash" type="bool"/>
+ <preference id="mailnews.remember_selected_message"
+ name="mailnews.remember_selected_message" type="bool"/>
+ <preference id="mailnews.thread_pane_column_unthreads"
+ name="mailnews.thread_pane_column_unthreads"
+ inverted="true" type="bool"/>
+ <preference id="mail.tabs.autoHide"
+ name="mail.tabs.autoHide"
+ type="bool"/>
+ <preference id="mail.tabs.loadInBackground"
+ name="mail.tabs.loadInBackground"
+ inverted="true" type="bool"/>
+ <preference id="mail.biff.on_new_window"
+ name="mail.biff.on_new_window"
+ type="bool"
+ inverted="true"/>
+ <preference id="mail.tabs.opentabfor.middleclick"
+ name="mail.tabs.opentabfor.middleclick"
+ type="bool"/>
+ <preference id="mail.tabs.opentabfor.doubleclick"
+ name="mail.tabs.opentabfor.doubleclick"
+ type="bool"/>
+ <preference id="mailnews.start_page.enabled"
+ onchange="this.parentNode.parentNode.startPageCheck();"
+ name="mailnews.start_page.enabled" type="bool"/>
+ <preference id="mailnews.start_page.url"
+ name="mailnews.start_page.url" type="wstring"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&generalSettings.caption;"/>
+
+ <hbox align="center">
+ <checkbox id="mailnewsConfirmMoveFoldersToTrash" label="&confirmMove.label;"
+ preference="mailnews.confirm.moveFoldersToTrash"
+ accesskey="&confirmMove.accesskey;"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="mailRememberLastMsg" label="&rememberLastMsg.label;"
+ preference="mailnews.remember_selected_message"
+ accesskey="&rememberLastMsg.accesskey;" />
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="mailPreserveThreading"
+ label="&preserveThreading.label;"
+ accesskey="&preserveThreading.accesskey;"
+ preference="mailnews.thread_pane_column_unthreads"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="mailAutoHide"
+ label="&mailAutoHide.label;"
+ accesskey="&mailAutoHide.accesskey;"
+ preference="mail.tabs.autoHide"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="loadInBackground"
+ label="&loadInBackground.label;"
+ accesskey="&loadInBackground.accesskey;"
+ preference="mail.tabs.loadInBackground"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="mailBiffOnNewWindow"
+ label="&mailBiffOnNewWindow.label;"
+ accesskey="&mailBiffOnNewWindow.accesskey;"
+ preference="mail.biff.on_new_window"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox id="mailOpenTabFor" align="start">
+ <caption label="&mailOpenTabsFor.label;"/>
+ <hbox align="center">
+ <checkbox id="mailMiddleClick"
+#ifndef XP_MACOSX
+ label="&mailMiddleClick.label;"
+ accesskey="&mailMiddleClick.accesskey;"
+#else
+ label="&mailMiddleClickMac.label;"
+ accesskey="&mailMiddleClickMac.accesskey;"
+#endif
+ preference="mail.tabs.opentabfor.middleclick"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="mailDoubleClick"
+ label="&mailDoubleClick.label;"
+ accesskey="&mailDoubleClick.accesskey;"
+ preference="mail.tabs.opentabfor.doubleclick"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&messengerStartPage.caption;"/>
+ <hbox align="center">
+ <checkbox id="mailnewsStartPageEnabled" label="&enableStartPage.label;"
+ preference="mailnews.start_page.enabled"
+ accesskey="&enableStartPage.accesskey;"/>
+ </hbox>
+
+ <hbox align="center">
+ <label value="&location.label;" accesskey="&location.accesskey;"
+ control="mailnewsStartPageUrl"/>
+ <textbox id="mailnewsStartPageUrl" flex="1" type="autocomplete"
+ preference="mailnews.start_page.url" timeout="50"
+ autocompletesearch="history" maxrows="6" class="uri-element"/>
+ </hbox>
+ <hbox pack="end">
+ <button label="&useDefault.label;" accesskey="&useDefault.accesskey;"
+ oncommand="setHomePageToDefaultPage();">
+ <observes element="mailnewsStartPageUrl" attribute="disabled"/>
+ </button>
+ </hbox>
+
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/content/pref-notifications.js b/comm/suite/mailnews/components/prefs/content/pref-notifications.js
new file mode 100644
index 0000000000..89191e7cd6
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-notifications.js
@@ -0,0 +1,91 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// The contents of this file will be loaded into the scope of the object
+// <prefpane id="notifications_pane">!
+
+var {AppConstants} = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+var gSoundUrlPref = null;
+
+function Startup()
+{
+ // if we don't have the alert service, hide the pref UI for using alerts to notify on new mail
+ // see bug #158711
+ var newMailNotificationAlertUI = document.getElementById("newMailNotificationAlertBox");
+ newMailNotificationAlertUI.hidden = !("@mozilla.org/alerts-service;1" in Cc);
+
+ // as long as the old notification code is still around, the new options
+ // won't apply if mail.biff.show_new_alert is false and should be hidden
+ document.getElementById("showAlertPreviewText").hidden =
+ document.getElementById("showAlertSubject").hidden =
+ document.getElementById("showAlertSender").hidden =
+ !Services.prefs.getBoolPref("mail.biff.show_new_alert");
+
+ // animate dock icon option currently available for macOS only
+ var newMailNotificationBouncePref = document.getElementById("newMailNotificationBounceBox");
+ newMailNotificationBouncePref.hidden = AppConstants.platform != "macosx";
+
+ // show tray icon option currently available for Windows only
+ var newMailNotificationTrayIconPref = document.getElementById("newMailNotificationTrayIconBox");
+ newMailNotificationTrayIconPref.hidden = AppConstants.platform != "win";
+
+ // use system alert option currently available for Linux only
+ var useSystemAlertPref = document.getElementById("useSystemAlertBox");
+ useSystemAlertPref.hidden = AppConstants.platform != "linux";
+
+ EnableAlert(document.getElementById("mail.biff.show_alert").value, false);
+ EnableTrayIcon(document.getElementById("mail.biff.show_tray_icon").value);
+
+ gSoundUrlPref = document.getElementById("mail.biff.play_sound.url");
+
+ PlaySoundCheck(document.getElementById("mail.biff.play_sound").value);
+}
+
+function EnableAlert(aEnable, aFocus)
+{
+ // switch off the balloon on Windows if the user wants regular alerts
+ if (aEnable && AppConstants.platform == "win") {
+ let balloonAlert = document.getElementById("mail.biff.show_balloon");
+ if (!balloonAlert.locked)
+ balloonAlert.value = false;
+ }
+
+ EnableElementById("showAlertTime", aEnable, aFocus);
+ EnableElementById("showAlertPreviewText", aEnable, false);
+ EnableElementById("showAlertSubject", aEnable, false);
+ EnableElementById("showAlertSender", aEnable, false);
+ EnableElementById("useSystemAlertRadio", aEnable, false);
+}
+
+function EnableTrayIcon(aEnable)
+{
+ EnableElementById("newMailNotificationBalloon", aEnable, false);
+}
+
+function ClearAlert(aEnable)
+{
+ // switch off the regular alerts if the user wants the balloon
+ if (aEnable && AppConstants.platform == "win") {
+ let showAlert = document.getElementById("mail.biff.show_alert");
+ if (!showAlert.locked)
+ showAlert.value = false;
+ }
+}
+
+function PlaySoundCheck(aPlaySound)
+{
+ let playSoundType = document.getElementById("mail.biff.play_sound.type").value;
+
+ EnableElementById("newMailNotificationType", aPlaySound, false);
+ EnableSoundURL(aPlaySound && (playSoundType == 1));
+}
+
+function EnableSoundURL(aEnable)
+{
+ EnableElementById("mailnewsSoundFileUrl", aEnable, false);
+}
diff --git a/comm/suite/mailnews/components/prefs/content/pref-notifications.xul b/comm/suite/mailnews/components/prefs/content/pref-notifications.xul
new file mode 100644
index 0000000000..20ac974050
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-notifications.xul
@@ -0,0 +1,187 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?>
+
+<!DOCTYPE overlay [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % prefNotificationsDTD SYSTEM "chrome://messenger/locale/pref-notifications.dtd">
+%prefNotificationsDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="notifications_pane"
+ label="&pref.notifications.title;"
+ script="chrome://messenger/content/pref-notifications.js">
+
+ <preferences id="notifications_preferences">
+ <preference id="mail.biff.show_alert"
+ name="mail.biff.show_alert"
+ type="bool"
+ onchange="EnableAlert(this.value, this.value);"/>
+ <preference id="alerts.totalOpenTime"
+ name="alerts.totalOpenTime"
+ type="int"/>
+ <preference id="mail.biff.alert.show_preview"
+ name="mail.biff.alert.show_preview"
+ type="bool"/>
+ <preference id="mail.biff.alert.show_subject"
+ name="mail.biff.alert.show_subject"
+ type="bool"/>
+ <preference id="mail.biff.alert.show_sender"
+ name="mail.biff.alert.show_sender"
+ type="bool"/>
+ <preference id="mail.biff.use_system_alert"
+ name="mail.biff.use_system_alert"
+ type="bool"/>
+ <preference id="mail.biff.show_tray_icon"
+ name="mail.biff.show_tray_icon"
+ type="bool"
+ onchange="EnableTrayIcon(this.value);"/>
+ <preference id="mail.biff.show_balloon"
+ name="mail.biff.show_balloon"
+ type="bool"
+ onchange="ClearAlert(this.value);"/>
+ <preference id="mail.biff.animate_dock_icon"
+ name="mail.biff.animate_dock_icon"
+ type="bool"/>
+ <preference id="mail.biff.play_sound"
+ name="mail.biff.play_sound"
+ type="bool"
+ onchange="PlaySoundCheck(this.value);"/>
+ <preference id="mail.biff.play_sound.type"
+ name="mail.biff.play_sound.type"
+ type="int"
+ onchange="EnableSoundURL(this.value == 1);"/>
+ <preference id="mail.biff.play_sound.url"
+ name="mail.biff.play_sound.url"
+ type="string"/>
+ </preferences>
+
+ <groupbox id="newMessagesArrivePrefs">
+ <caption label="&notifications.caption;"/>
+
+ <label value="&newMessagesArrive.label;"/>
+ <vbox id="newMailNotificationAlertBox">
+ <hbox align="center">
+ <checkbox id="newMailNotificationAlert"
+ label="&showAlertFor.label;"
+ accesskey="&showAlertFor.accesskey;"
+ preference="mail.biff.show_alert"/>
+ <textbox id="showAlertTime"
+ type="number"
+ size="3"
+ min="1"
+ max="3600"
+ preference="alerts.totalOpenTime"
+ onsyncfrompreference="return document.getElementById(this.getAttribute('preference')).value / 1000;"
+ onsynctopreference="return this.value * 1000;"
+ aria-labelledby="newMailNotificationAlert showAlertTime showAlertTimeEnd"/>
+ <label id="showAlertTimeEnd"
+ value="&showAlertTimeEnd.label;">
+ <observes element="newMailNotificationAlert"
+ attribute="disabled"/>
+ </label>
+ </hbox>
+ <vbox id="showAlertOptionsBox"
+ class="indent">
+ <checkbox id="showAlertPreviewText"
+ label="&showAlertPreviewText.label;"
+ accesskey="&showAlertPreviewText.accesskey;"
+ preference="mail.biff.alert.show_preview"/>
+ <checkbox id="showAlertSubject"
+ label="&showAlertSubject.label;"
+ accesskey="&showAlertSubject.accesskey;"
+ preference="mail.biff.alert.show_subject"/>
+ <checkbox id="showAlertSender"
+ label="&showAlertSender.label;"
+ accesskey="&showAlertSender.accesskey;"
+ preference="mail.biff.alert.show_sender"/>
+ <separator id="newMailNotificationAlertSeparator"
+ class="thin"/>
+ <vbox id="useSystemAlertBox">
+ <radiogroup id="useSystemAlertRadio"
+ preference="mail.biff.use_system_alert">
+ <radio id="useSystemAlert"
+ value="true"
+ label="&useSystemAlert.label;"
+ accesskey="&useSystemAlert.accesskey;"/>
+ <radio id="useBuiltInAlert"
+ value="false"
+ label="&useBuiltInAlert.label;"
+ accesskey="&useBuiltInAlert.accesskey;"/>
+ </radiogroup>
+ <separator id="useSystemAlertSeparator"
+ class="thin"/>
+ </vbox>
+ </vbox>
+ </vbox>
+ <vbox id="newMailNotificationTrayIconBox">
+ <checkbox id="newMailNotificationTrayIcon"
+ label="&showTrayIcon.label;"
+ accesskey="&showTrayIcon.accesskey;"
+ preference="mail.biff.show_tray_icon"/>
+ <checkbox id="newMailNotificationBalloon"
+ class="indent"
+ label="&showBalloon.label;"
+ accesskey="&showBalloon.accesskey;"
+ preference="mail.biff.show_balloon"/>
+ <separator id="newMailNotificationTrayIconSeparator"
+ class="thin"/>
+ </vbox>
+ <vbox id="newMailNotificationBounceBox">
+ <checkbox id="newMailNotificationBounce"
+ label="&bounceSystemDockIcon.label;"
+ accesskey="&bounceSystemDockIcon.accesskey;"
+ preference="mail.biff.animate_dock_icon"/>
+ <separator id="newMailNotificationBounceSeparator"
+ class="thin"/>
+ </vbox>
+ <checkbox id="newMailNotification"
+ label="&playSound.label;"
+ accesskey="&playSound.accesskey;"
+ preference="mail.biff.play_sound"/>
+ <radiogroup id="newMailNotificationType"
+ preference="mail.biff.play_sound.type"
+ class="indent"
+ aria-labelledby="newMailNotification">
+ <radio id="system"
+ value="0"
+ label="&systemsound.label;"
+ accesskey="&systemsound.accesskey;"/>
+ <radio id="custom"
+ value="1"
+ label="&customsound.label;"
+ accesskey="&customsound.accesskey;"/>
+ </radiogroup>
+
+ <hbox align="center" class="indent">
+ <filefield id="mailnewsSoundFileUrl"
+ flex="1"
+ preference="mail.biff.play_sound.url"
+ preference-editable="true"
+ onsyncfrompreference="return WriteSoundField(this, document.getElementById('notifications_pane').gSoundUrlPref.value);"
+ aria-labelledby="custom"/>
+ <hbox align="center">
+ <button id="browse"
+ label="&browse.label;"
+ filepickertitle="&browse.title;"
+ accesskey="&browse.accesskey;"
+ oncommand="SelectSound(gSoundUrlPref);">
+ <observes element="mailnewsSoundFileUrl" attribute="disabled"/>
+ </button>
+ <button id="playButton"
+ label="&playButton.label;"
+ accesskey="&playButton.accesskey;"
+ oncommand="PlaySound(gSoundUrlPref.value, true);">
+ <observes element="mailnewsSoundFileUrl" attribute="disabled"/>
+ </button>
+ </hbox>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/content/pref-offline.js b/comm/suite/mailnews/components/prefs/content/pref-offline.js
new file mode 100644
index 0000000000..d453c8f97a
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-offline.js
@@ -0,0 +1,19 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// The contents of this file will be loaded into the scope of the object
+// <prefpane id="offline_pane">!
+
+function Startup()
+{
+ var value = document.getElementById("mail.prompt_purge_threshhold").value;
+ EnableElementById("offlineCompactFolderMin", value, false);
+}
+
+function EnableMailPurgeThreshold(aValue)
+{
+ var focus = (document.getElementById("offlineCompactFolder") == document.commandDispatcher.focusedElement);
+ EnableElementById("offlineCompactFolderMin", aValue, focus);
+}
diff --git a/comm/suite/mailnews/components/prefs/content/pref-offline.xul b/comm/suite/mailnews/components/prefs/content/pref-offline.xul
new file mode 100644
index 0000000000..49e4288ab0
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-offline.xul
@@ -0,0 +1,121 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/pref-offline.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="offline_pane"
+ label="&pref.network.title;"
+ script="chrome://messenger/content/pref-offline.js">
+
+ <preferences id="offline_preferences">
+ <preference id="offline.startup_state"
+ name="offline.startup_state"
+ type="int"/>
+ <preference id="offline.send.unsent_messages"
+ name="offline.send.unsent_messages"
+ type="int"/>
+ <preference id="offline.download.download_messages"
+ name="offline.download.download_messages"
+ type="int"/>
+ <preference id="mailnews.tcptimeout"
+ name="mailnews.tcptimeout"
+ type="int"/>
+ <preference id="mail.prompt_purge_threshhold"
+ name="mail.prompt_purge_threshhold"
+ type="bool"
+ onchange="EnableMailPurgeThreshold(this.value);"/>
+ <preference id="mail.purge_threshhold_mb"
+ name="mail.purge_threshhold_mb"
+ type="int"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&pref.offline.caption;"/>
+
+ <hbox align="center">
+ <label value="&textStartUp;" control="whenStartingUp"
+ accesskey="&textStartUp.accesskey;"/>
+ <menulist id="whenStartingUp" preference="offline.startup_state">
+ <menupopup>
+ <menuitem value="0" label="&menuitemRememberPrevState;"/>
+ <menuitem value="1" label="&menuitemAskMe;"/>
+ <menuitem value="2" label="&menuitemAlwaysOnline;"/>
+ <menuitem value="3" label="&menuitemAlwaysOffline;"/>
+ <menuitem value="4" label="&menuitemAutomatic;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <separator/>
+
+ <label value="&textGoingOnline;" control="whengoingOnlinestate"/>
+ <radiogroup id="whengoingOnlinestate"
+ orient="horizontal" class="indent"
+ preference="offline.send.unsent_messages">
+ <radio value="1" label="&radioAutoSend;"
+ accesskey="&radioAutoSend.accesskey;"/>
+ <radio value="2" label="&radioNotSend;"
+ accesskey="&radioNotSend.accesskey;"/>
+ <radio value="0" label="&radioAskUnsent;"
+ accesskey="&radioAskUnsent.accesskey;"/>
+ </radiogroup>
+
+ <separator/>
+
+ <label value="&textGoingOffline;" control="whengoingOfflinestate"/>
+ <radiogroup id="whengoingOfflinestate"
+ orient="horizontal" class="indent"
+ preference="offline.download.download_messages">
+ <radio value="1" label="&radioAutoDownload;"
+ accesskey="&radioAutoDownload.accesskey;"/>
+ <radio value="2" label="&radioNotDownload;"
+ accesskey="&radioNotDownload.accesskey;"/>
+ <radio value="0" label="&radioAskDownload;"
+ accesskey="&radioAskDownload.accesskey;"/>
+ </radiogroup>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&mailConnections.caption;"/>
+ <hbox align="center">
+ <label id="timeoutLabel"
+ value="&mailnewsTimeout.label;"
+ accesskey="&mailnewsTimeout.accesskey;"
+ control="mailnewsTimeoutSeconds"/>
+ <textbox id="mailnewsTimeoutSeconds"
+ type="number"
+ size="4"
+ preference="mailnews.tcptimeout"
+ aria-labelledby="timeoutLabel mailnewsTimeoutSeconds timeoutSeconds"/>
+ <label id="timeoutSeconds" value="&mailnewsTimeoutSeconds.label;"/>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&Diskspace;"/>
+ <hbox align="center">
+ <checkbox id="offlineCompactFolder"
+ label="&offlineCompactFolders.label;"
+ accesskey="&offlineCompactFolders.accesskey;"
+ preference="mail.prompt_purge_threshhold"
+ aria-labelledby="offlineCompactFolder offlineCompactFolderMin offlineCompactFolderMB"/>
+ <textbox id="offlineCompactFolderMin"
+ type="number"
+ size="4"
+ min="1"
+ max="2048"
+ increment="10"
+ value="20"
+ preference="mail.purge_threshhold_mb"
+ aria-labelledby="offlineCompactFolder offlineCompactFolderMin offlineCompactFolderMB"/>
+ <label id="offlineCompactFolderMB" value="&offlineCompactFoldersMB.label;"/>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/content/pref-receipts.js b/comm/suite/mailnews/components/prefs/content/pref-receipts.js
new file mode 100644
index 0000000000..7a85ac1a1f
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-receipts.js
@@ -0,0 +1,28 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var gNotInToCcLabel;
+var gOutsideDomainLabel;
+var gOtherCasesLabel;
+
+function Startup()
+{
+ gNotInToCcLabel = document.getElementById("notInToCcLabel");
+ gOutsideDomainLabel = document.getElementById("outsideDomainLabel");
+ gOtherCasesLabel = document.getElementById("otherCasesLabel");
+
+ var value = document.getElementById("mail.mdn.report.enabled").value;
+ EnableDisableAllowedReceipts(value);
+}
+
+function EnableDisableAllowedReceipts(aEnable)
+{
+ EnableElementById("notInToCcPref", aEnable, false);
+ EnableElementById("outsideDomainPref", aEnable, false);
+ EnableElementById("otherCasesPref", aEnable, false);
+ gNotInToCcLabel.disabled = !aEnable;
+ gOutsideDomainLabel.disabled = !aEnable;
+ gOtherCasesLabel.disabled = !aEnable;
+}
diff --git a/comm/suite/mailnews/components/prefs/content/pref-receipts.xul b/comm/suite/mailnews/components/prefs/content/pref-receipts.xul
new file mode 100644
index 0000000000..0ca17a02ed
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-receipts.xul
@@ -0,0 +1,146 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+
+<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/pref-receipts.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="receipts_pane"
+ label="&pref.receipts.title;"
+ script="chrome://messenger/content/pref-receipts.js">
+ <preferences id="receipts_preferences">
+ <preference id="mail.receipt.request_return_receipt_on"
+ name="mail.receipt.request_return_receipt_on"
+ type="bool"/>
+ <preference id="mail.incorporate.return_receipt"
+ name="mail.incorporate.return_receipt"
+ type="int"/>
+ <preference id="mail.mdn.report.enabled"
+ name="mail.mdn.report.enabled"
+ type="bool"
+ onchange="EnableDisableAllowedReceipts(this.value);"/>
+ <preference id="mail.mdn.report.not_in_to_cc"
+ name="mail.mdn.report.not_in_to_cc"
+ type="int"/>
+ <preference id="mail.mdn.report.outside_domain"
+ name="mail.mdn.report.outside_domain"
+ type="int"/>
+ <preference id="mail.mdn.report.other"
+ name="mail.mdn.report.other"
+ type="int"/>
+ </preferences>
+
+ <groupbox>
+ <caption label="&prefReceipts.caption;"/>
+
+ <vbox id="returnReceiptSettings" align="start">
+ <checkbox id="alwaysRequest"
+ label="&requestReceipt.label;"
+ accesskey="&requestReceipt.accesskey;"
+ preference="mail.receipt.request_return_receipt_on"/>
+
+ <separator/>
+
+ <vbox id="receiptArrive">
+ <label control="receiptFolder">&receiptArrive.label;</label>
+ <radiogroup id="receiptFolder"
+ class="indent"
+ preference="mail.incorporate.return_receipt">
+ <radio value="0"
+ label="&leaveIt.label;"
+ accesskey="&leaveIt.accesskey;"/>
+ <radio value="1"
+ label="&moveToSent.label;"
+ accesskey="&moveToSent.accesskey;"/>
+ </radiogroup>
+ </vbox>
+
+ <separator/>
+
+ <vbox id="receiptRequest">
+ <label control="receiptSend">&requestMDN.label;</label>
+ <radiogroup id="receiptSend"
+ class="indent"
+ preference="mail.mdn.report.enabled">
+ <radio value="false"
+ label="&never.label;"
+ accesskey="&never.accesskey;"/>
+ <radio value="true"
+ label="&returnSome.label;"
+ accesskey="&returnSome.accesskey;"/>
+
+ <hbox id="receiptSendIf" class="indent">
+ <grid>
+ <columns>
+ <column/>
+ <column/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label id="notInToCcLabel"
+ accesskey="&notInToCc.accesskey;"
+ control="notInToCcPref"
+ value="&notInToCc.label;"/>
+ <menulist id="notInToCcPref"
+ preference="mail.mdn.report.not_in_to_cc">
+ <menupopup>
+ <menuitem value="0"
+ label="&neverSend.label;"/>
+ <menuitem value="1"
+ label="&alwaysSend.label;"/>
+ <menuitem value="2"
+ label="&askMe.label;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row align="center">
+ <label id="outsideDomainLabel"
+ accesskey="&outsideDomain.accesskey;"
+ control="outsideDomainPref"
+ value="&outsideDomain.label;"/>
+ <menulist id="outsideDomainPref"
+ preference="mail.mdn.report.outside_domain">
+ <menupopup>
+ <menuitem value="0"
+ label="&neverSend.label;"/>
+ <menuitem value="1"
+ label="&alwaysSend.label;"/>
+ <menuitem value="2"
+ label="&askMe.label;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row align="center">
+ <label id="otherCasesLabel"
+ accesskey="&otherCases.accesskey;"
+ control="otherCasesPref"
+ value="&otherCases.label;"/>
+ <menulist id="otherCasesPref"
+ preference="mail.mdn.report.other">
+ <menupopup>
+ <menuitem value="0"
+ label="&neverSend.label;"/>
+ <menuitem value="1"
+ label="&alwaysSend.label;"/>
+ <menuitem value="2"
+ label="&askMe.label;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ </rows>
+ </grid>
+ </hbox>
+ </radiogroup>
+
+ </vbox>
+
+ </vbox>
+
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/content/pref-tags.js b/comm/suite/mailnews/components/prefs/content/pref-tags.js
new file mode 100644
index 0000000000..8182fe7237
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-tags.js
@@ -0,0 +1,478 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// Each tag entry in our list looks like this:
+// <listitem>
+// <listcell>
+// <textbox/>
+// </listcell>
+// <listcell>
+// <colorpicker type='button'/>
+// </listcell>
+// </listitem>
+// For ease of handling, all tag data is stored in <listitem>.tagInfo also.
+
+const kOrdinalCharLow = "a";
+const kOrdinalCharHigh = "z";
+const kOrdinalPadding = String.fromCharCode(kOrdinalCharLow.charCodeAt(0) - 1);
+
+var gInstantApply = document.documentElement.instantApply; // read only once
+var gTagList = null; // tagList root element
+var gAddButton = null;
+var gDeleteButton = null;
+var gRaiseButton = null;
+var gLowerButton = null;
+
+var gDeletedTags = {}; // tags marked for deletion in non-instant apply mode
+
+
+function Startup()
+{
+ gTagList = document.getElementById('tagList');
+ gAddButton = document.getElementById('addTagButton');
+ gDeleteButton = document.getElementById('deleteTagButton');
+ gRaiseButton = document.getElementById('raiseTagButton');
+ gLowerButton = document.getElementById('lowerTagButton');
+ InitTagList();
+ if (!gInstantApply)
+ window.addEventListener("dialogaccept", this.OnOK, true);
+ UpdateButtonStates();
+}
+
+function InitTagList()
+{
+ // Read the tags from preferences via the tag service.
+ var tagArray = MailServices.tags.getAllTags();
+ for (var i = 0; i < tagArray.length; ++i)
+ {
+ var t = tagArray[i];
+ var tagInfo = {tag: t.tag,
+ key: t.key,
+ color: t.color,
+ ordinal: t.ordinal,
+ new: false, // not added in this run
+ changed: false}; // not changed (yet)
+ AppendTagEntry(tagInfo, null);
+ }
+}
+
+// read text and color from the listitem
+function UpdateTagInfo(aTagInfo, aEntry)
+{
+ var tag = aEntry.firstChild.firstChild.value;
+ var color = aEntry.lastChild.lastChild.color;
+ if (tag != aTagInfo.tag || color != aTagInfo.color)
+ {
+ aTagInfo.changed = true; // never unset changed flag here!
+ aTagInfo.tag = tag;
+ aTagInfo.color = color;
+ }
+}
+
+// set text and color of the listitem
+function UpdateTagEntry(aTagInfo, aEntry)
+{
+ aEntry.firstChild.firstChild.value = aTagInfo.tag;
+ aEntry.lastChild.lastChild.color = aTagInfo.color || 'inherit';
+}
+
+function AppendTagEntry(aTagInfo, aRefChild)
+{
+ // Creating a colorpicker dynamically in an onload handler is really sucky.
+ // You MUST first set its type attribute (to select the correct binding), then
+ // add the element to the DOM (to bind the binding) and finally set the color
+ // property(!) afterwards. Try in any other order and fail... :-(
+ var tagCell = document.createElement('listcell');
+ var textbox = document.createElement('textbox');
+ textbox.setAttribute('flex', 1);
+ textbox.setAttribute('value', aTagInfo.tag);
+ tagCell.appendChild(textbox);
+
+ var colorCell = document.createElement('listcell');
+ var colorpicker = document.createElement('colorpicker');
+ colorpicker.setAttribute('type', 'button');
+ colorpicker.setAttribute('color', aTagInfo.color || 'inherit')
+ colorCell.appendChild(colorpicker);
+
+ var entry = document.createElement('listitem');
+ entry.addEventListener('focus', OnFocus, true);
+ entry.addEventListener('change', OnChange);
+ entry.setAttribute('allowevents', 'true'); // activate textbox and colorpicker
+ entry.tagInfo = aTagInfo;
+ entry.appendChild(tagCell);
+ entry.appendChild(colorCell);
+
+ gTagList.insertBefore(entry, aRefChild);
+ return entry;
+}
+
+function OnFocus(aEvent)
+{
+ gTagList.selectedItem = this;
+ UpdateButtonStates();
+}
+
+function FocusTagEntry(aEntry)
+{
+ // focus the entry's textbox
+ gTagList.ensureElementIsVisible(aEntry);
+ aEntry.firstChild.firstChild.focus();
+}
+
+function GetTagOrdinal(aTagInfo)
+{
+ if (aTagInfo.ordinal)
+ return aTagInfo.ordinal;
+ return aTagInfo.key;
+}
+
+function SetTagOrdinal(aTagInfo, aOrdinal)
+{
+ var ordinal = aTagInfo.ordinal;
+ aTagInfo.ordinal = (aTagInfo.key != aOrdinal) ? aOrdinal : '';
+ if (aTagInfo.ordinal != ordinal)
+ aTagInfo.changed = true;
+}
+
+function BisectString(aPrev, aNext)
+{
+ // find a string which is lexically greater than aPrev and lesser than aNext:
+ // - copy leading parts common to aPrev and aNext into the result
+ // - find the first position where aPrev and aNext differ:
+ // - if we can squeeze a character in between there: fine, done!
+ // - if not:
+ // - if the rest of aNext is longer than one character, we can squeeze
+ // in just the first aNext rest-character and be done!
+ // - else we try to "increment" aPrev a bit to fit in
+ if ((aPrev >= aNext) || (aPrev + kOrdinalCharLow >= aNext))
+ return ''; // no such string exists
+
+ // pad the shorter string
+ var lenPrev = aPrev.length;
+ var lenNext = aNext.length;
+ var lenMax = Math.max(lenPrev, lenNext);
+
+ // loop over both strings at once, padding if necessary
+ var constructing = false;
+ var result = '';
+ for (var i = 0; i < lenMax; ++i)
+ {
+ var prevChar = (i < lenPrev) ? aPrev[i] : kOrdinalPadding;
+ var nextChar = constructing ? kOrdinalCharHigh
+ : (i < lenNext) ? aNext[i]
+ : kOrdinalPadding;
+ var prevCode = prevChar.charCodeAt(0);
+ var nextCode = nextChar.charCodeAt(0);
+ if (prevCode == nextCode)
+ {
+ // copy common characters
+ result += prevChar;
+ }
+ else if (prevCode + 1 < nextCode)
+ {
+ // found a real bisecting string
+ result += String.fromCharCode((prevCode + nextCode) / 2);
+ return result;
+ }
+ else
+ {
+ // nextCode is greater than prevCode, but there's no place in between.
+ // But if aNext[i+1] exists, then nextChar will suffice and we're done!
+ // ("x" < "xsomething")
+ if (i + 1 < lenNext)
+ {
+ // found a real bisecting string
+ return result + nextChar;
+ }
+ // just copy over prevChar and enter construction mode
+ result += prevChar;
+ constructing = true;
+ }
+ }
+ return ''; // nothing found
+}
+
+function RecalculateOrdinal(aEntry)
+{
+ // Calculate a new ordinal for the given entry, assuming that both its
+ // predecessor's and successor's are correct, i.e. ord(p) < ord(s)!
+ var tagInfo = aEntry.tagInfo;
+ var ordinal = tagInfo.key;
+ // get neighbouring ordinals
+ var prevOrdinal = '', nextOrdinal = '';
+ var prev = aEntry.previousSibling;
+ if (prev && prev.nodeName == 'listitem') // first.prev == listhead
+ prevOrdinal = GetTagOrdinal(prev.tagInfo);
+ var next = aEntry.nextSibling;
+ if (next)
+ {
+ nextOrdinal = GetTagOrdinal(next.tagInfo);
+ }
+ else
+ {
+ // ensure key < nextOrdinal if entry is the last/only entry
+ nextOrdinal = prevOrdinal || ordinal;
+ nextOrdinal = String.fromCharCode(nextOrdinal.charCodeAt(0) + 2);
+ }
+
+ if (prevOrdinal < ordinal && ordinal < nextOrdinal)
+ {
+ // no ordinal needed, just clear it
+ SetTagOrdinal(tagInfo, '')
+ return;
+ }
+
+ // so we need a new ordinal, because key <= prevOrdinal or key >= nextOrdinal
+ ordinal = BisectString(prevOrdinal, nextOrdinal);
+ if (ordinal)
+ {
+ // found a new ordinal
+ SetTagOrdinal(tagInfo, ordinal)
+ return;
+ }
+
+ // couldn't find an ordinal before the nextOrdinal, so take that instead
+ // and recalculate a new one for the next entry
+ SetTagOrdinal(tagInfo, nextOrdinal);
+ if (next)
+ ApplyChange(next);
+}
+
+function OnChange(aEvent)
+{
+ ApplyChange(aEvent.currentTarget);
+}
+
+function ApplyChange(aEntry)
+{
+ if (!aEntry)
+ {
+ dump('ApplyChange: aEntry is null! (called by ' + ApplyChange.caller.name + ')\n');
+ return;
+ }
+
+ // the tag data got changed, so write it back to the system
+ var tagInfo = aEntry.tagInfo;
+ UpdateTagInfo(tagInfo, aEntry);
+ // ensure unique tag name
+ var dupeList = ReadTagListFromUI(aEntry);
+ var uniqueTag = DisambiguateTag(tagInfo.tag, dupeList);
+ if (tagInfo.tag != uniqueTag)
+ {
+ tagInfo.tag = uniqueTag;
+ tagInfo.changed = true;
+ UpdateTagEntry(tagInfo, aEntry);
+ }
+
+ if (gInstantApply)
+ {
+ // If the item was newly added, we still can rename the key,
+ // so that it's in sync with the actual tag.
+ if (tagInfo.new && tagInfo.key)
+ {
+ // Do not clear the "new" flag!
+ // The key will only stick after closing the dialog.
+ MailServices.tags.deleteKey(tagInfo.key);
+ tagInfo.key = '';
+ }
+ if (!tagInfo.key)
+ {
+ // create a new key, based upon the new tag
+ MailServices.tags.addTag(tagInfo.tag, '', '');
+ tagInfo.key = MailServices.tags.getKeyForTag(tagInfo.tag);
+ }
+
+ // Recalculate the sort ordinal, if necessary.
+ // We assume that the neighbour's ordinals are correct,
+ // i.e. that ordinal(pos - 1) < ordinal(pos + 1)!
+ RecalculateOrdinal(aEntry);
+ WriteTag(tagInfo);
+ }
+}
+
+function WriteTag(aTagInfo)
+{
+//dump('********** WriteTag: ' + aTagInfo.toSource() + '\n');
+ try
+ {
+ MailServices.tags.addTagForKey(aTagInfo.key, aTagInfo.tag, aTagInfo.color,
+ aTagInfo.ordinal);
+ aTagInfo.changed = false;
+ }
+ catch (e)
+ {
+ dump('WriteTag: update exception:\n' + e);
+ }
+}
+
+function UpdateButtonStates()
+{
+ var entry = gTagList.selectedItem;
+ // disable Delete if no selection
+ gDeleteButton.disabled = !entry;
+ // disable Raise if no selection or first entry
+ gRaiseButton.disabled = !entry || !gTagList.getPreviousItem(entry, 1);
+ // disable Lower if no selection or last entry
+ gLowerButton.disabled = !entry || !gTagList.getNextItem(entry, 1);
+}
+
+function ReadTagListFromUI(aIgnoreEntry)
+{
+ // reads distinct tag names from the UI
+ var dupeList = {}; // indexed by tag
+ for (var entry = gTagList.firstChild; entry; entry = entry.nextSibling)
+ if ((entry != aIgnoreEntry) && (entry.localName == 'listitem'))
+ dupeList[entry.firstChild.firstChild.value] = true;
+ return dupeList;
+}
+
+function DisambiguateTag(aTag, aTagList)
+{
+ if (aTag in aTagList)
+ {
+ var suffix = 2;
+ while (aTag + ' ' + suffix in aTagList)
+ ++suffix;
+ aTag += ' ' + suffix;
+ }
+ return aTag;
+}
+
+function AddTag()
+{
+ // Add a new tag to the UI here.
+ // It will be be written to the preference system
+ // (a) directly on each change for instant apply, or
+ // (b) only if the dialogaccept handler is executed.
+
+ // create new unique tag name
+ var dupeList = ReadTagListFromUI();
+ var tag = DisambiguateTag(gAddButton.getAttribute('defaulttagname'), dupeList);
+
+ // create new tag list entry
+ var tagInfo = {tag: tag,
+ key: '',
+ color: 'inherit',
+ ordinal: '',
+ new: true,
+ changed: true};
+ var refChild = gTagList.getNextItem(gTagList.selectedItem, 1);
+ var newEntry = AppendTagEntry(tagInfo, refChild);
+ ApplyChange(newEntry);
+ FocusTagEntry(newEntry);
+}
+
+function DeleteTag()
+{
+ // Delete the selected tag from the UI here. If it was added during this
+ // preference dialog session, we can drop it at once; if it was read from
+ // the preferences system, we may need to remember killing it in OnOK.
+ var entry = gTagList.selectedItem;
+ var key = entry.tagInfo.key;
+ if (key)
+ {
+ if (gInstantApply)
+ MailServices.tags.deleteKey(key);
+ else
+ gDeletedTags[key] = true; // dummy value
+ }
+ // after removing, move focus to next entry, if it exist, else try previous
+ var newFocusItem = gTagList.getNextItem(entry, 1) ||
+ gTagList.getPreviousItem(entry, 1);
+ gTagList.removeItemAt(gTagList.getIndexOfItem(entry));
+ if (newFocusItem)
+ FocusTagEntry(newFocusItem);
+ else
+ UpdateButtonStates();
+}
+
+function MoveTag(aMoveUp)
+{
+ // Move the selected tag one position up or down in the tagList's child order.
+ // This reordering may require changing ordinal strings.
+ var entry = gTagList.selectedItem;
+ var tagInfo = entry.tagInfo;
+ UpdateTagInfo(tagInfo, entry); // remember changed values
+ var successor = aMoveUp ? gTagList.getPreviousItem(entry, 1)
+ : gTagList.getNextItem(entry, 2);
+ entry.parentNode.insertBefore(entry, successor);
+ FocusTagEntry(entry);
+ tagInfo.changed = true;
+ UpdateTagEntry(tagInfo, entry); // needs to be visible
+ ApplyChange(entry);
+}
+
+function Restore()
+{
+ // clear pref panel tag list
+ // Remember any known keys for deletion in the OKHandler.
+ while (gTagList.getRowCount())
+ {
+ var key = gTagList.removeItemAt(0).tagInfo.key;
+ if (key)
+ {
+ if (gInstantApply)
+ MailServices.tags.deleteKey(key);
+ else
+ gDeletedTags[key] = true; // dummy value
+ }
+ }
+ // add default items (no ordinal strings for those)
+ for (var i = 1; i <= 5; ++i)
+ {
+ // create default tags from the former label defaults
+ var key = "$label" + i;
+ var tag = GetLocalizedStringPref("mailnews.labels.description." + i);
+ var color = Services.prefs.getDefaultBranch("mailnews.labels.color.").getCharPref(i);
+ var tagInfo = {tag: tag,
+ key: key,
+ color: color,
+ ordinal: '',
+ new: false,
+ changed: true};
+ var newEntry = AppendTagEntry(tagInfo, null);
+ ApplyChange(newEntry);
+ }
+ FocusTagEntry(gTagList.getItemAtIndex(0));
+}
+
+function OnOK()
+{
+ // remove all deleted tags from the preferences system
+ for (var key in gDeletedTags)
+ MailServices.tags.deleteKey(key);
+
+ // Write tags to the preferences system, creating keys and ordinal strings.
+ for (var entry = gTagList.firstChild; entry; entry = entry.nextSibling)
+ {
+ if (entry.localName == 'listitem')
+ {
+ // only write listitems which have changed (this includes new ones)
+ var tagInfo = entry.tagInfo;
+ if (tagInfo.changed)
+ {
+ if (!tagInfo.key)
+ {
+ // newly added tag, need to create a key and read it
+ MailServices.tags.addTag(tagInfo.tag, '', '');
+ tagInfo.key = MailServices.tags.getKeyForTag(tagInfo.tag);
+ }
+ if (tagInfo.key)
+ {
+ // Recalculate the sort ordinal, if necessary.
+ // We assume that the neighbour's ordinals are correct,
+ // i.e. that ordinal(pos - 1) < ordinal(pos + 1)!
+ RecalculateOrdinal(entry);
+ // update the tag definition
+ WriteTag(tagInfo);
+ }
+ }
+ }
+ }
+}
diff --git a/comm/suite/mailnews/components/prefs/content/pref-tags.xul b/comm/suite/mailnews/components/prefs/content/pref-tags.xul
new file mode 100644
index 0000000000..cd99824a25
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-tags.xul
@@ -0,0 +1,83 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?>
+
+<!DOCTYPE page SYSTEM "chrome://messenger/locale/pref-tags.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="tags_pane"
+ label="&pref.tags.title;"
+ script="chrome://messenger/content/pref-tags.js">
+
+ <preferences id="tags_preferences">
+ <preference id="pref.tags.disable_button.add"
+ name="pref.tags.disable_button.add"
+ type="bool"/>
+ <preference id="pref.tags.disable_button.delete"
+ name="pref.tags.disable_button.delete"
+ type="bool"/>
+ <preference id="pref.tags.disable_button.lower"
+ name="pref.tags.disable_button.lower"
+ type="bool"/>
+ <preference id="pref.tags.disable_button.raise"
+ name="pref.tags.disable_button.raise"
+ type="bool"/>
+ <preference id="pref.tags.disable_button.restore"
+ name="pref.tags.disable_button.restore"
+ type="bool"/>
+ </preferences>
+
+ <groupbox flex="1">
+ <caption label="&pref.tags.caption;"/>
+ <label control="tagList">&pref.tags.description;</label>
+ <hbox flex="1">
+ <listbox id="tagList" flex="1" onselect="UpdateButtonStates();">
+ <listcols>
+ <listcol flex="1"/>
+ <listcol/>
+ </listcols>
+ <listhead>
+ <listheader label="&tagColumn.label;"/>
+ <listheader label="&colorColumn.label;"/>
+ </listhead>
+ </listbox>
+
+ <vbox>
+ <button id="addTagButton"
+ label="&addTagButton.label;"
+ accesskey="&addTagButton.accesskey;"
+ defaulttagname="&defaultTagName.label;"
+ preference="pref.tags.disable_button.add"
+ oncommand="AddTag();"/>
+ <button id="deleteTagButton"
+ label="&deleteTagButton.label;"
+ accesskey="&deleteTagButton.accesskey;"
+ preference="pref.tags.disable_button.delete"
+ oncommand="DeleteTag();"/>
+ <spacer flex="1"/>
+ <button id="raiseTagButton"
+ label="&raiseTagButton.label;"
+ accesskey="&raiseTagButton.accesskey;"
+ preference="pref.tags.disable_button.raise"
+ oncommand="MoveTag(true);"/>
+ <button id="lowerTagButton"
+ label="&lowerTagButton.label;"
+ accesskey="&lowerTagButton.accesskey;"
+ preference="pref.tags.disable_button.lower"
+ oncommand="MoveTag(false);"/>
+ <spacer flex="1"/>
+ <button id="restoreButton"
+ label="&restoreButton.label;"
+ accesskey="&restoreButton.accesskey;"
+ preference="pref.tags.disable_button.restore"
+ oncommand="Restore();"/>
+ </vbox>
+ </hbox>
+ </groupbox>
+
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.js b/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.js
new file mode 100644
index 0000000000..75b5da1b3d
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.js
@@ -0,0 +1,26 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// The contents of this file will be loaded into the scope of the object
+// <prefpane id="viewing_messages_pane">!
+
+function Startup()
+{
+ var autoPref = document.getElementById("mailnews.mark_message_read.auto");
+ UpdateMarkAsReadOptions(autoPref.value);
+}
+
+function UpdateMarkAsReadOptions(aEnableReadDelay)
+{
+ EnableElementById("markAsReadAfterPreferences", aEnableReadDelay, false);
+ // ... and the extras!
+ var delayPref = document.getElementById("mailnews.mark_message_read.delay");
+ UpdateMarkAsReadTextbox(aEnableReadDelay && delayPref.value, false);
+}
+
+function UpdateMarkAsReadTextbox(aEnable, aFocus)
+{
+ EnableElementById("markAsReadDelay", aEnable, aFocus);
+}
diff --git a/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.xul b/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.xul
new file mode 100644
index 0000000000..117761c86b
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.xul
@@ -0,0 +1,174 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % pref-viewing_messagesDTD SYSTEM "chrome://messenger/locale/pref-viewing_messages.dtd">
+%pref-viewing_messagesDTD;
+<!ENTITY % editorOverlayDTD SYSTEM "chrome://editor/locale/editorOverlay.dtd">
+%editorOverlayDTD;
+]>
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <prefpane id="viewing_messages_pane"
+ label="&pref.viewing.messages.title;"
+ script="chrome://messenger/content/pref-viewing_messages.js">
+
+ <preferences id="viewing_messages_preferences">
+ <preference id="mailnews.reuse_message_window"
+ name="mailnews.reuse_message_window"
+ type="bool"/>
+ <preference id="mail.close_message_window.on_delete"
+ name="mail.close_message_window.on_delete"
+ type="bool"/>
+ <preference id="mailnews.message_display.disable_remote_image"
+ name="mailnews.message_display.disable_remote_image"
+ type="bool"/>
+ <preference id="mailnews.mark_message_read.auto"
+ name="mailnews.mark_message_read.auto"
+ type="bool"
+ onchange="UpdateMarkAsReadOptions(this.value);"/>
+ <preference id="mailnews.mark_message_read.delay"
+ name="mailnews.mark_message_read.delay"
+ type="bool"
+ onchange="UpdateMarkAsReadTextbox(this.value, this.value);"/>
+ <preference id="mailnews.mark_message_read.delay.interval"
+ name="mailnews.mark_message_read.delay.interval"
+ type="int"/>
+ <preference id="mail.fixed_width_messages"
+ name="mail.fixed_width_messages"
+ type="bool"/>
+ <preference id="mail.wrap_long_lines"
+ name="mail.wrap_long_lines"
+ type="bool"/>
+ <preference id="mail.display_glyph"
+ name="mail.display_glyph"
+ type="bool"/>
+ <preference id="mail.quoted_style"
+ name="mail.quoted_style"
+ type="int"/>
+ <preference id="mail.quoted_size"
+ name="mail.quoted_size"
+ type="int"/>
+ <preference id="mail.citation_color"
+ name="mail.citation_color"
+ type="string"/>
+ <preference id="mail.showCondensedAddresses"
+ name="mail.showCondensedAddresses"
+ type="bool"/>
+ </preferences>
+
+ <groupbox align="start">
+ <caption label="&generalMessageDisplay.caption;"/>
+ <label value="&openingMessages.label;" control="reuseMessageWindow"/>
+ <vbox class="indent">
+ <radiogroup id="reuseMessageWindow"
+ orient="horizontal"
+ preference="mailnews.reuse_message_window">
+ <radio id="new"
+ label="&newWindowRadio.label;"
+ accesskey="&newWindowRadio.accesskey;"
+ value="false"/>
+ <radio id="existing"
+ label="&existingWindowRadio.label;"
+ accesskey="&existingWindowRadio.accesskey;"
+ value="true"/>
+ </radiogroup>
+ <checkbox id="closeMsgWindowOnDelete"
+ label="&closeMsgWindowOnDelete.label;"
+ accesskey="&closeMsgWindowOnDelete.accesskey;"
+ preference="mail.close_message_window.on_delete"/>
+ </vbox>
+
+ <checkbox id="disableContent" label="&disableContent.label;"
+ accesskey="&disableContent.accesskey;"
+ preference="mailnews.message_display.disable_remote_image"/>
+ <checkbox id="showCondensedAddresses"
+ label="&showCondensedAddresses.label;"
+ accesskey="&showCondensedAddresses.accesskey;"
+ preference="mail.showCondensedAddresses"/>
+
+ <separator class="thin"/>
+
+ <checkbox id="automaticallyMarkAsRead"
+ preference="mailnews.mark_message_read.auto"
+ label="&autoMarkAsRead.label;"
+ accesskey="&autoMarkAsRead.accesskey;"
+ oncommand="UpdateMarkAsReadOptions(this.checked);"/>
+
+ <hbox align="center" class="indent">
+ <checkbox id="markAsReadAfterPreferences"
+ label="&markAsReadAfter.label;"
+ accesskey="&markAsReadAfter.accesskey;"
+ preference="mailnews.mark_message_read.delay"/>
+ <textbox id="markAsReadDelay"
+ type="number"
+ size="2"
+ maximum="99"
+ preference="mailnews.mark_message_read.delay.interval"
+ aria-labelledby="markAsReadAfterPreferences markAsReadDelay secondsLabel"/>
+ <label id="secondsLabel"
+ value="&secondsLabel.label;">
+ <observes element="markAsReadAfterPreferences"
+ attribute="disabled"/>
+ </label>
+ </hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&displayPlainText.caption;"/>
+ <hbox align="center">
+ <label value="&fontPlainText.label;"
+ accesskey="&fontPlainText.accesskey;"
+ control="mailFixedWidthMessages"/>
+ <radiogroup id="mailFixedWidthMessages"
+ orient="horizontal"
+ preference="mail.fixed_width_messages">
+ <radio label="&fontFixedWidth.label;"
+ accesskey="&fontFixedWidth.accesskey;"
+ value="true"/>
+ <radio label="&fontVarWidth.label;"
+ accesskey="&fontVarWidth.accesskey;"
+ value="false"/>
+ </radiogroup>
+ </hbox>
+
+ <checkbox id="wrapLongLines" label="&wrapInMsg.label;"
+ accesskey="&wrapInMsg.accesskey;"
+ preference="mail.wrap_long_lines"/>
+ <checkbox id="displayGlyph" label="&convertEmoticons.label;"
+ accesskey="&convertEmoticons.accesskey;"
+ preference="mail.display_glyph"/>
+
+ <separator class="thin"/>
+
+ <description>&displayQuoted.label;</description>
+ <hbox class="indent" align="center">
+ <label value="&style.label;" accesskey="&style.accesskey;" control="mailQuotedStyle"/>
+ <menulist id="mailQuotedStyle" preference="mail.quoted_style">
+ <menupopup>
+ <menuitem value="0" label="&regular.label;"/>
+ <menuitem value="1" label="&bold.label;"/>
+ <menuitem value="2" label="&italic.label;"/>
+ <menuitem value="3" label="&boldItalic.label;"/>
+ </menupopup>
+ </menulist>
+
+ <label value="&size.label;" accesskey="&size.accesskey;" control="mailQuotedSize"/>
+ <menulist id="mailQuotedSize" preference="mail.quoted_size">
+ <menupopup>
+ <menuitem value="0" label="&regular.label;"/>
+ <menuitem value="1" label="&bigger.label;"/>
+ <menuitem value="2" label="&smaller.label;"/>
+ </menupopup>
+ </menulist>
+
+ <label value="&color.label;" accesskey="&color.accesskey;" control="mailCitationColor"/>
+ <colorpicker type="button" id="mailCitationColor" palettename="standard"
+ preference="mail.citation_color"/>
+ </hbox>
+ </groupbox>
+ </prefpane>
+</overlay>
diff --git a/comm/suite/mailnews/components/prefs/jar.mn b/comm/suite/mailnews/components/prefs/jar.mn
new file mode 100644
index 0000000000..9baa230079
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/jar.mn
@@ -0,0 +1,23 @@
+# 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/.
+
+messenger.jar:
+ content/messenger/mailPrefsOverlay.xul (content/mailPrefsOverlay.xul)
+* content/messenger/pref-mailnews.xul (content/pref-mailnews.xul)
+ content/messenger/pref-mailnews.js (content/pref-mailnews.js)
+ content/messenger/pref-notifications.xul (content/pref-notifications.xul)
+ content/messenger/pref-notifications.js (content/pref-notifications.js)
+ content/messenger/pref-junk.xul (content/pref-junk.xul)
+ content/messenger/pref-junk.js (content/pref-junk.js)
+ content/messenger/pref-tags.xul (content/pref-tags.xul)
+ content/messenger/pref-tags.js (content/pref-tags.js)
+ content/messenger/pref-viewing_messages.xul (content/pref-viewing_messages.xul)
+ content/messenger/pref-viewing_messages.js (content/pref-viewing_messages.js)
+ content/messenger/pref-receipts.xul (content/pref-receipts.xul)
+ content/messenger/pref-receipts.js (content/pref-receipts.js)
+ content/messenger/pref-character_encoding.xul (content/pref-character_encoding.xul)
+ content/messenger/pref-character_encoding.js (content/pref-character_encoding.js)
+ content/messenger/pref-offline.xul (content/pref-offline.xul)
+ content/messenger/pref-offline.js (content/pref-offline.js)
+
diff --git a/comm/suite/mailnews/components/prefs/moz.build b/comm/suite/mailnews/components/prefs/moz.build
new file mode 100644
index 0000000000..de5cd1bf81
--- /dev/null
+++ b/comm/suite/mailnews/components/prefs/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/mailnews/components/smime/content/msgCompSMIMEOverlay.js b/comm/suite/mailnews/components/smime/content/msgCompSMIMEOverlay.js
new file mode 100644
index 0000000000..e802130008
--- /dev/null
+++ b/comm/suite/mailnews/components/smime/content/msgCompSMIMEOverlay.js
@@ -0,0 +1,354 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// Account encryption policy values:
+// const kEncryptionPolicy_Never = 0;
+// 'IfPossible' was used by ns4.
+// const kEncryptionPolicy_IfPossible = 1;
+var kEncryptionPolicy_Always = 2;
+
+var gEncryptedURIService =
+ Cc["@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"]
+ .getService(Ci.nsIEncryptedSMIMEURIsService);
+
+var gNextSecurityButtonCommand = "";
+var gSMFields = null;
+var gEncryptOptionChanged;
+var gSignOptionChanged;
+
+function onComposerLoad()
+{
+ // Are we already set up ? Or are the required fields missing ?
+ if (gSMFields || !gMsgCompose || !gMsgCompose.compFields)
+ return;
+
+ gMsgCompose.compFields.composeSecure = null;
+
+ gSMFields = Cc["@mozilla.org/messengercompose/composesecure;1"]
+ .createInstance(Ci.nsIMsgComposeSecure);
+ if (!gSMFields)
+ return;
+
+ gMsgCompose.compFields.composeSecure = gSMFields;
+
+ // Set up the initial security state.
+ gSMFields.requireEncryptMessage =
+ gCurrentIdentity.getIntAttribute("encryptionpolicy") == kEncryptionPolicy_Always;
+ if (!gSMFields.requireEncryptMessage &&
+ gEncryptedURIService &&
+ gEncryptedURIService.isEncrypted(gMsgCompose.originalMsgURI))
+ {
+ // Override encryption setting if original is known as encrypted.
+ gSMFields.requireEncryptMessage = true;
+ }
+ if (gSMFields.requireEncryptMessage)
+ setEncryptionUI();
+ else
+ setNoEncryptionUI();
+
+ gSMFields.signMessage = gCurrentIdentity.getBoolAttribute("sign_mail");
+ if (gSMFields.signMessage)
+ setSignatureUI();
+ else
+ setNoSignatureUI();
+}
+
+addEventListener("load", smimeComposeOnLoad, {capture: false, once: true});
+
+// this function gets called multiple times
+function smimeComposeOnLoad()
+{
+ onComposerLoad();
+
+ top.controllers.appendController(SecurityController);
+
+ addEventListener("compose-from-changed", onComposerFromChanged, true);
+ addEventListener("compose-send-message", onComposerSendMessage, true);
+
+ addEventListener("unload", smimeComposeOnUnload, {capture: false, once: true});
+}
+
+function smimeComposeOnUnload()
+{
+ removeEventListener("compose-from-changed", onComposerFromChanged, true);
+ removeEventListener("compose-send-message", onComposerSendMessage, true);
+
+ top.controllers.removeController(SecurityController);
+}
+
+function showNeedSetupInfo()
+{
+ let compSmimeBundle = document.getElementById("bundle_comp_smime");
+ let brandBundle = document.getElementById("brandBundle");
+ if (!compSmimeBundle || !brandBundle)
+ return;
+
+ let buttonPressed = Services.prompt.confirmEx(window,
+ brandBundle.getString("brandShortName"),
+ compSmimeBundle.getString("NeedSetup"),
+ Services.prompt.STD_YES_NO_BUTTONS, 0, 0, 0, null, {});
+ if (buttonPressed == 0)
+ openHelp("sign-encrypt", "chrome://communicator/locale/help/suitehelp.rdf");
+}
+
+function toggleEncryptMessage()
+{
+ if (!gSMFields)
+ return;
+
+ gSMFields.requireEncryptMessage = !gSMFields.requireEncryptMessage;
+
+ if (gSMFields.requireEncryptMessage)
+ {
+ // Make sure we have a cert.
+ if (!gCurrentIdentity.getUnicharAttribute("encryption_cert_name"))
+ {
+ gSMFields.requireEncryptMessage = false;
+ showNeedSetupInfo();
+ return;
+ }
+
+ setEncryptionUI();
+ }
+ else
+ {
+ setNoEncryptionUI();
+ }
+
+ gEncryptOptionChanged = true;
+}
+
+function toggleSignMessage()
+{
+ if (!gSMFields)
+ return;
+
+ gSMFields.signMessage = !gSMFields.signMessage;
+
+ if (gSMFields.signMessage) // make sure we have a cert name...
+ {
+ if (!gCurrentIdentity.getUnicharAttribute("signing_cert_name"))
+ {
+ gSMFields.signMessage = false;
+ showNeedSetupInfo();
+ return;
+ }
+
+ setSignatureUI();
+ }
+ else
+ {
+ setNoSignatureUI();
+ }
+
+ gSignOptionChanged = true;
+}
+
+function setSecuritySettings(menu_id)
+{
+ if (!gSMFields)
+ return;
+
+ document.getElementById("menu_securityEncryptRequire" + menu_id)
+ .setAttribute("checked", gSMFields.requireEncryptMessage);
+ document.getElementById("menu_securitySign" + menu_id)
+ .setAttribute("checked", gSMFields.signMessage);
+}
+
+function setNextCommand(what)
+{
+ gNextSecurityButtonCommand = what;
+}
+
+function doSecurityButton()
+{
+ var what = gNextSecurityButtonCommand;
+ gNextSecurityButtonCommand = "";
+
+ switch (what)
+ {
+ case "encryptMessage":
+ toggleEncryptMessage();
+ break;
+
+ case "signMessage":
+ toggleSignMessage();
+ break;
+
+ case "show":
+ default:
+ showMessageComposeSecurityStatus();
+ }
+}
+
+function setNoSignatureUI()
+{
+ top.document.getElementById("securityStatus").removeAttribute("signing");
+ top.document.getElementById("signing-status").collapsed = true;
+}
+
+function setSignatureUI()
+{
+ top.document.getElementById("securityStatus").setAttribute("signing", "ok");
+ top.document.getElementById("signing-status").collapsed = false;
+}
+
+function setNoEncryptionUI()
+{
+ top.document.getElementById("securityStatus").removeAttribute("crypto");
+ top.document.getElementById("encryption-status").collapsed = true;
+}
+
+function setEncryptionUI()
+{
+ top.document.getElementById("securityStatus").setAttribute("crypto", "ok");
+ top.document.getElementById("encryption-status").collapsed = false;
+}
+
+function showMessageComposeSecurityStatus()
+{
+ Recipients2CompFields(gMsgCompose.compFields);
+
+ window.openDialog(
+ "chrome://messenger-smime/content/msgCompSecurityInfo.xul",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ {
+ compFields : gMsgCompose.compFields,
+ subject : GetMsgSubjectElement().value,
+ smFields : gSMFields,
+ isSigningCertAvailable :
+ gCurrentIdentity.getUnicharAttribute("signing_cert_name") != "",
+ isEncryptionCertAvailable :
+ gCurrentIdentity.getUnicharAttribute("encryption_cert_name") != "",
+ currentIdentity : gCurrentIdentity
+ }
+ );
+}
+
+var SecurityController =
+{
+ supportsCommand: function(command)
+ {
+ switch (command)
+ {
+ case "cmd_viewSecurityStatus":
+ return true;
+
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled: function(command)
+ {
+ switch (command)
+ {
+ case "cmd_viewSecurityStatus":
+ return true;
+
+ default:
+ return false;
+ }
+ }
+};
+
+function onComposerSendMessage()
+{
+ let emailAddresses = [];
+
+ try
+ {
+ if (!gMsgCompose.compFields.composeSecure.requireEncryptMessage)
+ return;
+
+ emailAddresses = Cc["@mozilla.org/messenger-smime/smimejshelper;1"]
+ .createInstance(Ci.nsISMimeJSHelper)
+ .getNoCertAddresses(gMsgCompose.compFields);
+ }
+ catch (e)
+ {
+ return;
+ }
+
+ if (emailAddresses.length > 0)
+ {
+ // The rules here: If the current identity has a directoryServer set, then
+ // use that, otherwise, try the global preference instead.
+
+ let autocompleteDirectory;
+
+ // Does the current identity override the global preference?
+ if (gCurrentIdentity.overrideGlobalPref)
+ {
+ autocompleteDirectory = gCurrentIdentity.directoryServer;
+ }
+ else
+ {
+ // Try the global one
+ if (Services.prefs.getBoolPref("ldap_2.autoComplete.useDirectory"))
+ autocompleteDirectory =
+ Services.prefs.getCharPref("ldap_2.autoComplete.directoryServer");
+ }
+
+ if (autocompleteDirectory)
+ window.openDialog("chrome://messenger-smime/content/certFetchingStatus.xul",
+ "",
+ "chrome,modal,resizable,centerscreen",
+ autocompleteDirectory,
+ emailAddresses);
+ }
+}
+
+function onComposerFromChanged()
+{
+ if (!gSMFields)
+ return;
+
+ var encryptionPolicy = gCurrentIdentity.getIntAttribute("encryptionpolicy");
+ var useEncryption = false;
+
+ if (!gEncryptOptionChanged)
+ {
+ // Encryption wasn't manually checked.
+ // Set up the encryption policy from the setting of the new identity.
+
+ // 0 == never, 1 == if possible (ns4), 2 == always encrypt.
+ useEncryption = (encryptionPolicy == kEncryptionPolicy_Always);
+ }
+ else
+ {
+ useEncryption = !!gCurrentIdentity.getUnicharAttribute("encryption_cert_name");
+ }
+
+ gSMFields.requireEncryptMessage = useEncryption;
+ if (useEncryption)
+ setEncryptionUI();
+ else
+ setNoEncryptionUI();
+
+ // - If signing is disabled, we will not turn it on automatically.
+ // - If signing is enabled, but the new account defaults to not sign, we will turn signing off.
+ var signMessage = gCurrentIdentity.getBoolAttribute("sign_mail");
+ var useSigning = false;
+
+ if (!gSignOptionChanged)
+ {
+ // Signing wasn't manually checked.
+ // Set up the signing policy from the setting of the new identity.
+ useSigning = signMessage;
+ }
+ else
+ {
+ useSigning = !!gCurrentIdentity.getUnicharAttribute("signing_cert_name");
+ }
+ gSMFields.signMessage = useSigning;
+ if (useSigning)
+ setSignatureUI();
+ else
+ setNoSignatureUI();
+}
diff --git a/comm/suite/mailnews/components/smime/content/msgHdrViewSMIMEOverlay.js b/comm/suite/mailnews/components/smime/content/msgHdrViewSMIMEOverlay.js
new file mode 100644
index 0000000000..09f665b4d0
--- /dev/null
+++ b/comm/suite/mailnews/components/smime/content/msgHdrViewSMIMEOverlay.js
@@ -0,0 +1,258 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var gSignedUINode = null;
+var gEncryptedUINode = null;
+var gSMIMEContainer = null;
+var gStatusBar = null;
+var gSignedStatusPanel = null;
+var gEncryptedStatusPanel = null;
+
+var gEncryptedURIService = null;
+var gMyLastEncryptedURI = null;
+
+var gSMIMEBundle = null;
+// var gBrandBundle; -- defined in mailWindow.js
+
+// manipulates some globals from msgReadSMIMEOverlay.js
+
+var nsICMSMessageErrors = Ci.nsICMSMessageErrors;
+
+/// Get the necko URL for the message URI.
+function neckoURLForMessageURI(aMessageURI)
+{
+ let msgSvc = Cc["@mozilla.org/messenger;1"]
+ .createInstance(Ci.nsIMessenger)
+ .messageServiceFromURI(aMessageURI);
+ let neckoURI = msgSvc.getUrlForUri(aMessageURI);
+ return neckoURI.spec;
+}
+
+var smimeHeaderSink =
+{
+ maxWantedNesting: function()
+ {
+ return 1;
+ },
+
+ signedStatus: function(aNestingLevel, aSignatureStatus, aSignerCert)
+ {
+ if (aNestingLevel > 1) {
+ // we are not interested
+ return;
+ }
+
+ gSignatureStatus = aSignatureStatus;
+ gSignerCert = aSignerCert;
+
+ gSMIMEContainer.collapsed = false;
+ gSignedUINode.collapsed = false;
+ gSignedStatusPanel.collapsed = false;
+
+ switch (aSignatureStatus) {
+ case nsICMSMessageErrors.SUCCESS:
+ gSignedUINode.setAttribute("signed", "ok");
+ gStatusBar.setAttribute("signed", "ok");
+ break;
+
+ case nsICMSMessageErrors.VERIFY_NOT_YET_ATTEMPTED:
+ gSignedUINode.setAttribute("signed", "unknown");
+ gStatusBar.setAttribute("signed", "unknown");
+ break;
+
+ case nsICMSMessageErrors.VERIFY_CERT_WITHOUT_ADDRESS:
+ case nsICMSMessageErrors.VERIFY_HEADER_MISMATCH:
+ gSignedUINode.setAttribute("signed", "mismatch");
+ gStatusBar.setAttribute("signed", "mismatch");
+ break;
+
+ default:
+ gSignedUINode.setAttribute("signed", "notok");
+ gStatusBar.setAttribute("signed", "notok");
+ break;
+ }
+ },
+
+ encryptionStatus: function(aNestingLevel, aEncryptionStatus, aRecipientCert)
+ {
+ if (aNestingLevel > 1) {
+ // we are not interested
+ return;
+ }
+
+ gEncryptionStatus = aEncryptionStatus;
+ gEncryptionCert = aRecipientCert;
+
+ gSMIMEContainer.collapsed = false;
+ gEncryptedUINode.collapsed = false;
+ gEncryptedStatusPanel.collapsed = false;
+
+ if (nsICMSMessageErrors.SUCCESS == aEncryptionStatus)
+ {
+ gEncryptedUINode.setAttribute("encrypted", "ok");
+ gStatusBar.setAttribute("encrypted", "ok");
+ }
+ else
+ {
+ gEncryptedUINode.setAttribute("encrypted", "notok");
+ gStatusBar.setAttribute("encrypted", "notok");
+ }
+
+ if (gEncryptedURIService)
+ {
+ // Remember the message URI and the corresponding necko URI.
+ gMyLastEncryptedURI = GetLoadedMessage();
+ gEncryptedURIService.rememberEncrypted(gMyLastEncryptedURI);
+ gEncryptedURIService.rememberEncrypted(
+ neckoURLForMessageURI(gMyLastEncryptedURI));
+ }
+
+ switch (aEncryptionStatus)
+ {
+ case nsICMSMessageErrors.SUCCESS:
+ case nsICMSMessageErrors.ENCRYPT_INCOMPLETE:
+ break;
+ default:
+ var brand = gBrandBundle.getString("brandShortName");
+ var title = gSMIMEBundle.getString("CantDecryptTitle").replace(/%brand%/g, brand);
+ var body = gSMIMEBundle.getString("CantDecryptBody").replace(/%brand%/g, brand);
+
+ // insert our message
+ msgWindow.displayHTMLInMessagePane(title,
+ "<html>\n" +
+ "<body bgcolor=\"#fafaee\">\n" +
+ "<center><br><br><br>\n" +
+ "<table>\n" +
+ "<tr><td>\n" +
+ "<center><strong><font size=\"+3\">\n" +
+ title+"</font></center><br>\n" +
+ body+"\n" +
+ "</td></tr></table></center></body></html>", false);
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]),
+};
+
+function forgetEncryptedURI()
+{
+ if (gMyLastEncryptedURI && gEncryptedURIService)
+ {
+ gEncryptedURIService.forgetEncrypted(gMyLastEncryptedURI);
+ gEncryptedURIService.forgetEncrypted(
+ neckoURLForMessageURI(gMyLastEncryptedURI));
+ gMyLastEncryptedURI = null;
+ }
+}
+
+function onSMIMEStartHeaders()
+{
+ gEncryptionStatus = -1;
+ gSignatureStatus = -1;
+
+ gSignerCert = null;
+ gEncryptionCert = null;
+
+ gSMIMEContainer.collapsed = true;
+
+ gSignedUINode.collapsed = true;
+ gSignedUINode.removeAttribute("signed");
+ gSignedStatusPanel.collapsed = true;
+ gStatusBar.removeAttribute("signed");
+
+ gEncryptedUINode.collapsed = true;
+ gEncryptedUINode.removeAttribute("encrypted");
+ gEncryptedStatusPanel.collapsed = true;
+ gStatusBar.removeAttribute("encrypted");
+
+ forgetEncryptedURI();
+}
+
+function onSMIMEEndHeaders()
+{}
+
+function onSmartCardChange()
+{
+ // only reload encrypted windows
+ if (gMyLastEncryptedURI && gEncryptionStatus != -1)
+ ReloadMessage();
+}
+
+function msgHdrViewSMIMEOnLoad(event)
+{
+ window.crypto.enableSmartCardEvents = true;
+ document.addEventListener("smartcard-insert", onSmartCardChange);
+ document.addEventListener("smartcard-remove", onSmartCardChange);
+ if (!gSMIMEBundle)
+ gSMIMEBundle = document.getElementById("bundle_read_smime");
+
+ // we want to register our security header sink as an opaque nsISupports
+ // on the msgHdrSink used by mail.....
+ msgWindow.msgHeaderSink.securityInfo = smimeHeaderSink;
+
+ gSignedUINode = document.getElementById('signedHdrIcon');
+ gEncryptedUINode = document.getElementById('encryptedHdrIcon');
+ gSMIMEContainer = document.getElementById('smimeBox');
+ gStatusBar = document.getElementById('status-bar');
+ gSignedStatusPanel = document.getElementById('signed-status');
+ gEncryptedStatusPanel = document.getElementById('encrypted-status');
+
+ // add ourself to the list of message display listeners so we get notified when we are about to display a
+ // message.
+ var listener = {};
+ listener.onStartHeaders = onSMIMEStartHeaders;
+ listener.onEndHeaders = onSMIMEEndHeaders;
+ gMessageListeners.push(listener);
+
+ gEncryptedURIService =
+ Cc["@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"]
+ .getService(Ci.nsIEncryptedSMIMEURIsService);
+}
+
+function msgHdrViewSMIMEOnUnload(event)
+{
+ window.crypto.enableSmartCardEvents = false;
+ document.removeEventListener("smartcard-insert", onSmartCardChange);
+ document.removeEventListener("smartcard-remove", onSmartCardChange);
+ forgetEncryptedURI();
+ removeEventListener("messagepane-loaded", msgHdrViewSMIMEOnLoad, true);
+ removeEventListener("messagepane-unloaded", msgHdrViewSMIMEOnUnload, true);
+ removeEventListener("messagepane-hide", msgHdrViewSMIMEOnMessagePaneHide, true);
+ removeEventListener("messagepane-unhide", msgHdrViewSMIMEOnMessagePaneUnhide, true);
+}
+
+function msgHdrViewSMIMEOnMessagePaneHide()
+{
+ gSMIMEContainer.collapsed = true;
+ gSignedUINode.collapsed = true;
+ gSignedStatusPanel.collapsed = true;
+ gEncryptedUINode.collapsed = true;
+ gEncryptedStatusPanel.collapsed = true;
+}
+
+function msgHdrViewSMIMEOnMessagePaneUnhide()
+{
+ if (gEncryptionStatus != -1 || gSignatureStatus != -1)
+ {
+ gSMIMEContainer.collapsed = false;
+
+ if (gSignatureStatus != -1)
+ {
+ gSignedUINode.collapsed = false;
+ gSignedStatusPanel.collapsed = false;
+ }
+
+ if (gEncryptionStatus != -1)
+ {
+ gEncryptedUINode.collapsed = false;
+ gEncryptedStatusPanel.collapsed = false;
+ }
+ }
+}
+
+addEventListener('messagepane-loaded', msgHdrViewSMIMEOnLoad, true);
+addEventListener('messagepane-unloaded', msgHdrViewSMIMEOnUnload, true);
+addEventListener('messagepane-hide', msgHdrViewSMIMEOnMessagePaneHide, true);
+addEventListener('messagepane-unhide', msgHdrViewSMIMEOnMessagePaneUnhide, true);
diff --git a/comm/suite/mailnews/components/smime/jar.mn b/comm/suite/mailnews/components/smime/jar.mn
new file mode 100644
index 0000000000..2f1f63cf32
--- /dev/null
+++ b/comm/suite/mailnews/components/smime/jar.mn
@@ -0,0 +1,15 @@
+# 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/.
+
+messenger.jar:
+% content messenger-smime %content/messenger-smime/
+ content/messenger/certpicker.js (../../../../mailnews/extensions/smime/certpicker.js)
+ content/messenger/certpicker.xhtml (../../../../mailnews/extensions/smime/certpicker.xhtml)
+ content/messenger-smime/msgHdrViewSMIMEOverlay.js (content/msgHdrViewSMIMEOverlay.js)
+ content/messenger-smime/msgCompSMIMEOverlay.js (content/msgCompSMIMEOverlay.js)
+ content/messenger-smime/msgReadSMIMEOverlay.js (../../../../mailnews/extensions/smime/msgReadSMIMEOverlay.js)
+ content/messenger-smime/msgCompSecurityInfo.js (../../../../mailnews/extensions/smime/msgCompSecurityInfo.js)
+ content/messenger-smime/msgCompSecurityInfo.xhtml (../../../../mailnews/extensions/smime/msgCompSecurityInfo.xhtml)
+ content/messenger-smime/certFetchingStatus.js (../../../../mailnews/extensions/smime/certFetchingStatus.js)
+ content/messenger-smime/certFetchingStatus.xhtml (../../../../mailnews/extensions/smime/certFetchingStatus.xhtml)
diff --git a/comm/suite/mailnews/components/smime/moz.build b/comm/suite/mailnews/components/smime/moz.build
new file mode 100644
index 0000000000..de5cd1bf81
--- /dev/null
+++ b/comm/suite/mailnews/components/smime/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/comm/suite/mailnews/content/ABSearchDialog.js b/comm/suite/mailnews/content/ABSearchDialog.js
new file mode 100644
index 0000000000..15d85b6234
--- /dev/null
+++ b/comm/suite/mailnews/content/ABSearchDialog.js
@@ -0,0 +1,327 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const {encodeABTermValue} = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm");
+const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+
+var searchSessionContractID = "@mozilla.org/messenger/searchSession;1";
+var gSearchSession;
+
+var nsMsgSearchScope = Ci.nsMsgSearchScope;
+var nsMsgSearchOp = Ci.nsMsgSearchOp;
+var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+var nsIAbDirectory = Ci.nsIAbDirectory;
+
+var gStatusText;
+var gSearchBundle;
+var gAddressBookBundle;
+
+var gSearchStopButton;
+var gPropertiesButton;
+var gComposeButton;
+var gSearchPhoneticName = "false";
+
+var gSearchAbViewListener = {
+ onSelectionChanged: function() {
+ UpdateCardView();
+ },
+ onCountChanged: function(aTotal) {
+ let statusText;
+ if (aTotal == 0) {
+ statusText = gAddressBookBundle.getString("noMatchFound");
+ } else {
+ statusText = PluralForm
+ .get(aTotal, gAddressBookBundle.getString("matchesFound1"))
+ .replace("#1", aTotal);
+ }
+
+ gStatusText.setAttribute("label", statusText);
+ }
+};
+
+function searchOnLoad()
+{
+ setHelpFileURI("chrome://communicator/locale/help/suitehelp.rdf");
+ UpgradeAddressBookResultsPaneUI("mailnews.ui.advanced_directory_search_results.version");
+
+ initializeSearchWidgets();
+ initializeSearchWindowWidgets();
+
+ gSearchBundle = document.getElementById("bundle_search");
+ gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForSearchButton"));
+ gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
+ gAddressBookBundle = document.getElementById("bundle_addressBook");
+ gSearchSession = Cc[searchSessionContractID].createInstance(Ci.nsIMsgSearchSession);
+
+ // initialize a flag for phonetic name search
+ gSearchPhoneticName =
+ GetLocalizedStringPref("mail.addr_book.show_phonetic_fields");
+
+ if (window.arguments && window.arguments[0])
+ SelectDirectory(window.arguments[0].directory);
+ else
+ SelectDirectory(document.getElementById("abPopup-menupopup")
+ .firstChild.value);
+
+ // initialize globals, see abCommon.js, InitCommonJS()
+ abList = document.getElementById("abPopup");
+
+ onMore(null);
+}
+
+function searchOnUnload()
+{
+ CloseAbView();
+}
+
+function initializeSearchWindowWidgets()
+{
+ gSearchStopButton = document.getElementById("search-button");
+ gPropertiesButton = document.getElementById("propertiesButton");
+ gComposeButton = document.getElementById("composeButton");
+ gStatusText = document.getElementById('statusText');
+ // matchAll doesn't make sense for address book search
+ hideMatchAllItem();
+}
+
+function onSearchStop()
+{
+}
+
+function onAbSearchReset(event)
+{
+ gPropertiesButton.setAttribute("disabled","true");
+ gComposeButton.setAttribute("disabled","true");
+
+ CloseAbView();
+
+ onReset(event);
+ gStatusText.setAttribute("label", "");
+}
+
+function SelectDirectory(aURI)
+{
+ var selectedAB = aURI;
+
+ if (!selectedAB)
+ selectedAB = kPersonalAddressbookURI;
+
+ // set popup with address book names
+ var abPopup = document.getElementById('abPopup');
+ if ( abPopup )
+ abPopup.value = selectedAB;
+
+ setSearchScope(GetScopeForDirectoryURI(selectedAB));
+}
+
+function GetScopeForDirectoryURI(aURI)
+{
+ var directory = MailServices.ab.getDirectory(aURI);
+ var booleanAnd = gSearchBooleanRadiogroup.selectedItem.value == "and";
+
+ if (directory.isRemote) {
+ if (booleanAnd)
+ return nsMsgSearchScope.LDAPAnd;
+ else
+ return nsMsgSearchScope.LDAP;
+ }
+ else {
+ if (booleanAnd)
+ return nsMsgSearchScope.LocalABAnd;
+ else
+ return nsMsgSearchScope.LocalAB;
+ }
+}
+
+function onEnterInSearchTerm()
+{
+ // on enter
+ // if not searching, start the search
+ // if searching, stop and then start again
+ if (gSearchStopButton.getAttribute("label") == gSearchBundle.getString("labelForSearchButton")) {
+ onSearch();
+ }
+ else {
+ onSearchStop();
+ onSearch();
+ }
+}
+
+function onSearch()
+{
+ gStatusText.setAttribute("label", "");
+ gPropertiesButton.setAttribute("disabled","true");
+ gComposeButton.setAttribute("disabled","true");
+
+ gSearchSession.clearScopes();
+
+ var currentAbURI = document.getElementById('abPopup').getAttribute('value');
+
+ gSearchSession.addDirectoryScopeTerm(GetScopeForDirectoryURI(currentAbURI));
+ gSearchSession.searchTerms = saveSearchTerms(gSearchSession.searchTerms, gSearchSession);
+
+ var searchUri = currentAbURI + "?(";
+
+ for (let i = 0; i < gSearchSession.searchTerms.length; i++) {
+ let searchTerm = gSearchSession.searchTerms[i];
+
+ // get the "and" / "or" value from the first term
+ if (i == 0) {
+ if (searchTerm.booleanAnd)
+ searchUri += "and";
+ else
+ searchUri += "or";
+ }
+
+ var attrs;
+
+ switch (searchTerm.attrib) {
+ case nsMsgSearchAttrib.Name:
+ if (gSearchPhoneticName != "true")
+ attrs = ["DisplayName","FirstName","LastName","NickName"];
+ else
+ attrs = ["DisplayName","FirstName","LastName","NickName","PhoneticFirstName","PhoneticLastName"];
+ break;
+ case nsMsgSearchAttrib.DisplayName:
+ attrs = ["DisplayName"];
+ break;
+ case nsMsgSearchAttrib.Email:
+ attrs = ["PrimaryEmail"];
+ break;
+ case nsMsgSearchAttrib.PhoneNumber:
+ attrs = ["HomePhone","WorkPhone","FaxNumber","PagerNumber","CellularNumber"];
+ break;
+ case nsMsgSearchAttrib.Organization:
+ attrs = ["Company"];
+ break;
+ case nsMsgSearchAttrib.Department:
+ attrs = ["Department"];
+ break;
+ case nsMsgSearchAttrib.City:
+ attrs = ["WorkCity"];
+ break;
+ case nsMsgSearchAttrib.Street:
+ attrs = ["WorkAddress"];
+ break;
+ case nsMsgSearchAttrib.Nickname:
+ attrs = ["NickName"];
+ break;
+ case nsMsgSearchAttrib.WorkPhone:
+ attrs = ["WorkPhone"];
+ break;
+ case nsMsgSearchAttrib.HomePhone:
+ attrs = ["HomePhone"];
+ break;
+ case nsMsgSearchAttrib.Fax:
+ attrs = ["FaxNumber"];
+ break;
+ case nsMsgSearchAttrib.Pager:
+ attrs = ["PagerNumber"];
+ break;
+ case nsMsgSearchAttrib.Mobile:
+ attrs = ["CellularNumber"];
+ break;
+ case nsMsgSearchAttrib.Title:
+ attrs = ["JobTitle"];
+ break;
+ case nsMsgSearchAttrib.AdditionalEmail:
+ attrs = ["SecondEmail"];
+ break;
+ default:
+ dump("XXX " + searchTerm.attrib + " not a supported search attr!\n");
+ attrs = ["DisplayName"];
+ break;
+ }
+
+ var opStr;
+
+ switch (searchTerm.op) {
+ case nsMsgSearchOp.Contains:
+ opStr = "c";
+ break;
+ case nsMsgSearchOp.DoesntContain:
+ opStr = "!c";
+ break;
+ case nsMsgSearchOp.Is:
+ opStr = "=";
+ break;
+ case nsMsgSearchOp.Isnt:
+ opStr = "!=";
+ break;
+ case nsMsgSearchOp.BeginsWith:
+ opStr = "bw";
+ break;
+ case nsMsgSearchOp.EndsWith:
+ opStr = "ew";
+ break;
+ case nsMsgSearchOp.SoundsLike:
+ opStr = "~=";
+ break;
+ default:
+ opStr = "c";
+ break;
+ }
+
+ // currently, we can't do "and" and "or" searches at the same time
+ // (it's either all "and"s or all "or"s)
+ var max_attrs = attrs.length;
+
+ for (var j=0;j<max_attrs;j++) {
+ // append the term(s) to the searchUri
+ searchUri += "(" + attrs[j] + "," + opStr + "," + encodeABTermValue(searchTerm.value.str) + ")";
+ }
+ }
+
+ searchUri += ")";
+ SetAbView(searchUri);
+}
+
+// used to toggle functionality for Search/Stop button.
+function onSearchButton(event)
+{
+ if (event.target.label == gSearchBundle.getString("labelForSearchButton"))
+ onSearch();
+ else
+ onSearchStop();
+}
+
+function GetAbViewListener()
+{
+ return gSearchAbViewListener;
+}
+
+function onProperties()
+{
+ AbEditSelectedCard();
+}
+
+function onCompose()
+{
+ AbNewMessage();
+}
+
+function AbResultsPaneDoubleClick(card)
+{
+ AbEditCard(card);
+}
+
+function UpdateCardView()
+{
+ var numSelected = GetNumSelectedCards();
+
+ if (!numSelected) {
+ gPropertiesButton.setAttribute("disabled","true");
+ gComposeButton.setAttribute("disabled","true");
+ return;
+ }
+
+ gComposeButton.removeAttribute("disabled");
+
+ if (numSelected == 1)
+ gPropertiesButton.removeAttribute("disabled");
+ else
+ gPropertiesButton.setAttribute("disabled","true");
+}
diff --git a/comm/suite/mailnews/content/ABSearchDialog.xul b/comm/suite/mailnews/content/ABSearchDialog.xul
new file mode 100644
index 0000000000..a32c7dbb8b
--- /dev/null
+++ b/comm/suite/mailnews/content/ABSearchDialog.xul
@@ -0,0 +1,99 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://messenger/skin/searchDialog.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/addressbook/abResultsPaneOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/searchTermOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % abResultsPaneOverlayDTD SYSTEM "chrome://messenger/locale/addressbook
+/abResultsPaneOverlay.dtd">
+%abResultsPaneOverlayDTD;
+<!ENTITY % SearchDialogDTD SYSTEM "chrome://messenger/locale/SearchDialog.dtd">
+%SearchDialogDTD;
+]>
+
+<dialog id="searchAddressBookWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ windowtype="mailnews:absearch"
+ title="&abSearchDialogTitle.label;"
+ style="width: 52em; height: 34em;"
+ persist="screenX screenY width height sizemode"
+ buttons="help"
+ ondialoghelp="return openHelp('mail_advanced_ab_search');"
+ onload="searchOnLoad();"
+ onunload="onSearchStop(); searchOnUnload();">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+ <stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/>
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ </stringbundleset>
+
+ <script src="chrome://messenger/content/mailWindow.js"/>
+ <script src="chrome://messenger/content/msgMail3PaneWindow.js"/>
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://messenger/content/commandglue.js"/>
+ <script src="chrome://messenger/content/ABSearchDialog.js"/>
+ <script src="chrome://messenger/content/addressbook/abCommon.js"/>
+
+ <broadcaster id="Communicator:WorkMode"/>
+
+ <dummy class="usesMailWidgets"/>
+
+ <vbox id="searchTerms" flex="3" persist="height">
+ <vbox>
+ <hbox align="center">
+ <label value="&abSearchHeading.label;" accesskey="&abSearchHeading.accesskey;" control="abPopup"/>
+ <menulist id="abPopup" oncommand="SelectDirectory(this.value);">
+ <menupopup id="abPopup-menupopup" class="addrbooksPopup"/>
+ </menulist>
+ <spacer flex="10"/>
+ <button id="search-button" oncommand="onSearchButton(event);" default="true"/>
+ </hbox>
+ <hbox align="center">
+ <spacer flex="1"/>
+ <button label="&resetButton.label;" oncommand="onAbSearchReset(event);" accesskey="&resetButton.accesskey;"/>
+ </hbox>
+ </vbox>
+
+ <hbox flex="1">
+ <vbox id="searchTermListBox" flex="1"/>
+ </hbox>
+ </vbox>
+
+ <splitter id="gray_horizontal_splitter" collapse="after" persist="state">
+ <grippy/>
+ </splitter>
+
+ <vbox id="searchResults" flex="4" persist="height">
+ <vbox id="searchResultListBox" flex="1">
+ <tree id="abResultsTree" flex="1" context="threadPaneContext"/>
+ </vbox>
+ <hbox align="center">
+ <button id="propertiesButton"
+ label="&propertiesButton.label;"
+ accesskey="&propertiesButton.accesskey;"
+ disabled="true"
+ oncommand="onProperties();"/>
+ <button id="composeButton"
+ label="&composeButton.label;"
+ accesskey="&composeButton.accesskey;"
+ disabled="true"
+ oncommand="onCompose();"/>
+ <spacer flex="1"/>
+ <button dlgtype="help" class="dialog-button"/>
+ </hbox>
+ </vbox>
+
+ <statusbar class="chromeclass-status" id="status-bar">
+ <statusbarpanel id="statusText" crop="right" flex="1"/>
+ <statusbarpanel id="offline-status" class="statusbarpanel-iconic"/>
+ </statusbar>
+
+</dialog>
diff --git a/comm/suite/mailnews/content/FilterListDialog.js b/comm/suite/mailnews/content/FilterListDialog.js
new file mode 100644
index 0000000000..555796fc5e
--- /dev/null
+++ b/comm/suite/mailnews/content/FilterListDialog.js
@@ -0,0 +1,1037 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const { PluralForm } = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+var gEditButton;
+var gDeleteButton;
+var gNewButton;
+var gCopyToNewButton;
+var gTopButton;
+var gUpButton;
+var gDownButton;
+var gBottomButton;
+var gSearchBox;
+var gRunFiltersFolderPrefix;
+var gRunFiltersFolder;
+var gRunFiltersButton;
+var gFilterBundle;
+var gFilterListMsgWindow = null;
+var gFilterListbox;
+var gCurrentFilterList;
+var gStatusBar;
+var gStatusText;
+var gServerMenu;
+
+var msgMoveMotion = {
+ Up : 0,
+ Down : 1,
+ Top : 2,
+ Bottom : 3,
+}
+
+var gStatusFeedback = {
+ showStatusString: function(status)
+ {
+ gStatusText.setAttribute("value", status);
+ },
+ startMeteors: function()
+ {
+ // change run button to be a stop button
+ gRunFiltersButton.setAttribute("label", gRunFiltersButton.getAttribute("stoplabel"));
+ gRunFiltersButton.setAttribute("accesskey", gRunFiltersButton.getAttribute("stopaccesskey"));
+ gStatusBar.setAttribute("mode", "undetermined");
+ },
+ stopMeteors: function()
+ {
+ try {
+ // change run button to be a stop button
+ gRunFiltersButton.setAttribute("label", gRunFiltersButton.getAttribute("runlabel"));
+ gRunFiltersButton.setAttribute("accesskey", gRunFiltersButton.getAttribute("runaccesskey"));
+ gStatusBar.setAttribute("mode", "normal");
+ }
+ catch (ex) {
+ // can get here if closing window when running filters
+ }
+ },
+ showProgress: function(percentage)
+ {
+ },
+ closeWindow: function()
+ {
+ }
+};
+
+function onLoad()
+{
+ setHelpFileURI("chrome://communicator/locale/help/suitehelp.rdf");
+ gFilterListMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(Ci.nsIMsgWindow);
+ gFilterListMsgWindow.domWindow = window;
+ gFilterListMsgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL;
+ gFilterListMsgWindow.statusFeedback = gStatusFeedback;
+
+ gFilterBundle = document.getElementById("bundle_filter");
+
+ gServerMenu = document.getElementById("serverMenu");
+ gFilterListbox = document.getElementById("filterList");
+
+ gEditButton = document.getElementById("editButton");
+ gDeleteButton = document.getElementById("deleteButton");
+ gNewButton = document.getElementById("newButton");
+ gCopyToNewButton = document.getElementById("copyToNewButton");
+ gTopButton = document.getElementById("reorderTopButton");
+ gUpButton = document.getElementById("reorderUpButton");
+ gDownButton = document.getElementById("reorderDownButton");
+ gBottomButton = document.getElementById("reorderBottomButton");
+ gSearchBox = document.getElementById("searchBox");
+ gRunFiltersFolderPrefix = document.getElementById("folderPickerPrefix");
+ gRunFiltersFolder = document.getElementById("runFiltersFolder");
+ gRunFiltersButton = document.getElementById("runFiltersButton");
+ gStatusBar = document.getElementById("statusbar-icon");
+ gStatusText = document.getElementById("statusText");
+
+ updateButtons();
+
+ processWindowArguments(window.arguments[0]);
+
+ Services.obs.addObserver(onFilterClose,
+ "quit-application-requested");
+
+ top.controllers.insertControllerAt(0, gFilterController);
+}
+
+/**
+ * Processes arguments sent to this dialog when opened or refreshed.
+ *
+ * @param aArguments An object having members representing the arguments.
+ * { arg1: value1, arg2: value2, ... }
+ */
+function processWindowArguments(aArguments) {
+ let wantedFolder;
+ if ("folder" in aArguments)
+ wantedFolder = aArguments.folder;
+
+ // If a specific folder was requested, try to select it
+ // if we don't already show its server.
+ if (!gServerMenu._folder ||
+ (wantedFolder && (wantedFolder != gServerMenu._folder) &&
+ (wantedFolder.rootFolder != gServerMenu._folder))) {
+
+ // Get the folder where filters should be defined, if that server
+ // can accept filters.
+ let firstItem = getFilterFolderForSelection(wantedFolder);
+
+ // if the selected server cannot have filters, get the default server
+ // if the default server cannot have filters, check all accounts
+ // and get a server that can have filters.
+ if (!firstItem) {
+ var server = getServerThatCanHaveFilters();
+ if (server)
+ firstItem = server.rootFolder;
+ }
+
+ if (firstItem)
+ setFilterFolder(firstItem);
+
+ if (wantedFolder)
+ setRunFolder(wantedFolder);
+ } else {
+ // If we didn't change folder still redraw the list
+ // to show potential new filters if we were called for refresh.
+ rebuildFilterList();
+ }
+
+ // If a specific filter was requested, try to select it.
+ if ("filter" in aArguments)
+ selectFilter(aArguments.filter);
+}
+
+/**
+ * This is called from OpenOrFocusWindow() if the dialog is already open.
+ * New filters could have been created by operations outside the dialog.
+ *
+ * @param aArguments An object of arguments having the same format
+ * as window.arguments[0].
+ */
+function refresh(aArguments) {
+ // As we really don't know what has changed, clear the search box
+ // unconditionally so that the changed/added filters are surely visible.
+ resetSearchBox();
+
+ processWindowArguments(aArguments);
+}
+
+function CanRunFiltersAfterTheFact(aServer)
+{
+ // filter after the fact is implement using search
+ // so if you can't search, you can't filter after the fact
+ return aServer.canSearchMessages;
+}
+
+/**
+ * Change the root server for which we are managing filters.
+ *
+ * @param msgFolder The nsIMsgFolder server containing filters
+ * (or a folder for NNTP server).
+ */
+function setFilterFolder(msgFolder) {
+ if (!msgFolder || msgFolder == gServerMenu._folder)
+ return;
+
+ // Save the current filters to disk before switching because
+ // the dialog may be closed and we'll lose current filters.
+ if (gCurrentFilterList)
+ gCurrentFilterList.saveToDefaultFile();
+
+ // Setting this attribute should go away in bug 473009.
+ gServerMenu._folder = msgFolder;
+ // Calling this should go away in bug 802609.
+ gServerMenu.menupopup.selectFolder(msgFolder);
+
+ // Calling getEditableFilterList will detect any errors in
+ // msgFilterRules.dat, backup the file, and alert the user.
+ gCurrentFilterList = msgFolder.getEditableFilterList(gFilterListMsgWindow);
+ rebuildFilterList();
+
+ // Select the first item in the list, if there is one.
+ if (gFilterListbox.itemCount > 0)
+ gFilterListbox.selectItem(gFilterListbox.getItemAtIndex(0));
+
+ // This will get the deferred to account root folder, if server is deferred.
+ // We intentionally do this after setting the current server, as we want
+ // that to refer to the rootFolder for the actual server, not the
+ // deferred-to server, as current server is really a proxy for the
+ // server whose filters we are editing. But below here we are managing
+ // where the filters will get applied, which is on the deferred-to server.
+ msgFolder = msgFolder.server.rootMsgFolder;
+
+ // root the folder picker to this server
+ let runMenu = gRunFiltersFolder.menupopup;
+ runMenu._teardown();
+ runMenu._parentFolder = msgFolder;
+ runMenu._ensureInitialized();
+
+ var canFilterAfterTheFact = CanRunFiltersAfterTheFact(msgFolder.server);
+ gRunFiltersButton.hidden = !canFilterAfterTheFact;
+ gRunFiltersFolder.hidden = !canFilterAfterTheFact;
+ gRunFiltersFolderPrefix.hidden = !canFilterAfterTheFact;
+
+ if (canFilterAfterTheFact) {
+ let wantedFolder = null;
+ // For a given server folder, get the default run target folder or show
+ // "Choose Folder".
+ if (!msgFolder.isServer) {
+ wantedFolder = msgFolder;
+ } else {
+ try {
+ switch (msgFolder.server.type) {
+ case "nntp":
+ // For NNTP select the subscribed newsgroup.
+ wantedFolder = gServerMenu._folder;
+ break;
+ case "rss":
+ // Show "Choose Folder" for feeds.
+ wantedFolder = null;
+ break;
+ case "imap":
+ case "pop3":
+ case "none":
+ // Find Inbox for IMAP and POP or Local Folders,
+ // show "Choose Folder" if not found.
+ wantedFolder = msgFolder.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+ break;
+ default:
+ // For other account types we don't know what's good to select,
+ // so show "Choose Folder".
+ wantedFolder = null;
+ }
+ } catch (e) {
+ Cu.reportError("Failed to select a suitable folder to run filters on: " + e);
+ wantedFolder = null;
+ }
+ }
+ // Select a useful first folder for the server.
+ setRunFolder(wantedFolder);
+ }
+}
+
+/**
+ * Select a folder on which filters are to be run.
+ *
+ * @param aFolder nsIMsgFolder folder to select.
+ */
+function setRunFolder(aFolder) {
+ // Setting this attribute should go away in bug 473009.
+ gRunFiltersFolder._folder = aFolder;
+ // Calling this should go away in bug 802609.
+ gRunFiltersFolder.menupopup.selectFolder(gRunFiltersFolder._folder);
+ updateButtons();
+}
+
+/**
+ * Toggle enabled state of a filter, in both the filter properties and the UI.
+ *
+ * @param aFilterItem an item (row) of the filter list to be toggled
+ */
+function toggleFilter(aFilterItem)
+{
+ let filter = aFilterItem._filter;
+ if (filter.unparseable && !filter.enabled)
+ {
+ Services.prompt.alert(window, null,
+ gFilterBundle.getFormattedString("cannotEnableIncompatFilter",
+ [document.getElementById("bundle_brand").getString("brandShortName")]));
+ return;
+ }
+ filter.enabled = !filter.enabled;
+
+ // Now update the checkbox
+ aFilterItem.childNodes[1].setAttribute("enabled", filter.enabled);
+ // For accessibility set the checked state on listitem
+ aFilterItem.setAttribute("aria-checked", filter.enabled);
+}
+
+/**
+ * Selects a specific filter in the filter list.
+ * The listbox view is scrolled to the corresponding item.
+ *
+ * @param aFilter The nsIMsgFilter to select.
+ *
+ * @return true/false indicating whether the filter was found and selected.
+ */
+function selectFilter(aFilter) {
+ if (currentFilter() == aFilter)
+ return true;
+
+ resetSearchBox(aFilter);
+
+ let filterCount = gCurrentFilterList.filterCount;
+ for (let i = 0; i < filterCount; i++) {
+ if (gCurrentFilterList.getFilterAt(i) == aFilter) {
+ gFilterListbox.ensureIndexIsVisible(i);
+ gFilterListbox.selectedIndex = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Returns the currently selected filter. If multiple filters are selected,
+ * returns the first one. If none are selected, returns null.
+ */
+function currentFilter()
+{
+ let currentItem = gFilterListbox.selectedItem;
+ return currentItem ? currentItem._filter : null;
+}
+
+function onEditFilter()
+{
+ if (gEditButton.disabled)
+ return;
+
+ var selectedFilter = currentFilter();
+ if (!selectedFilter)
+ return;
+
+ let args = {filter: selectedFilter, filterList: gCurrentFilterList};
+
+ window.openDialog("chrome://messenger/content/FilterEditor.xul", "FilterEditor", "chrome,modal,titlebar,resizable,centerscreen", args);
+
+ if ("refresh" in args && args.refresh) {
+ // Reset search if edit was okay (name change might lead to hidden entry).
+ resetSearchBox(selectedFilter);
+ rebuildFilterList();
+ }
+}
+
+/**
+ * Handler function for the 'New...' buttons.
+ * Opens the filter dialog for creating a new filter.
+ */
+function onNewFilter() {
+ calculatePositionAndShowCreateFilterDialog({});
+}
+
+/**
+ * Handler function for the 'Copy...' button.
+ * Opens the filter dialog for copying the selected filter.
+ */
+function onCopyToNewFilter() {
+ if (gCopyToNewButton.disabled)
+ return;
+
+ let selectedFilter = currentFilter();
+ if (!selectedFilter)
+ return;
+
+ calculatePositionAndShowCreateFilterDialog({copiedFilter: selectedFilter});
+}
+
+/**
+ * Calculates the position for inserting the new filter,
+ * and then displays the create dialog.
+ *
+ * @param args The object containing the arguments for the dialog,
+ * passed to the filterEditorOnLoad() function.
+ * It will be augmented with the insertion position
+ * and global filters list properties by this function.
+ */
+function calculatePositionAndShowCreateFilterDialog(args) {
+ let selectedFilter = currentFilter();
+ // If no filter is selected use the first position.
+ let position = 0;
+ if (selectedFilter) {
+ // Get the position in the unfiltered list.
+ // - this is where the new filter should be inserted!
+ let filterCount = gCurrentFilterList.filterCount;
+ for (let i = 0; i < filterCount; i++) {
+ if (gCurrentFilterList.getFilterAt(i) == selectedFilter) {
+ position = i;
+ break;
+ }
+ }
+ }
+ args.filterPosition = position;
+ args.filterList = gCurrentFilterList;
+ args.refresh = false;
+
+ window.openDialog("chrome://messenger/content/FilterEditor.xul",
+ "FilterEditor",
+ "chrome,modal,titlebar,resizable,centerscreen", args);
+
+ if (args.refresh)
+ {
+ // On success: reset the search box if necessary!
+ resetSearchBox(args.newFilter);
+ rebuildFilterList();
+
+ // Select the new filter, it is at the position of previous selection.
+ gFilterListbox.selectItem(gFilterListbox.getItemAtIndex(position));
+ if (currentFilter() != args.newFilter)
+ Cu.reportError("Filter created at an unexpected position!");
+ }
+}
+
+function onDeleteFilter()
+{
+ if (gDeleteButton.disabled)
+ return;
+
+ let items = gFilterListbox.selectedItems;
+ if (!items.length)
+ return;
+
+ let checkValue = {value: false};
+ if (Services.prefs.getBoolPref("mailnews.filters.confirm_delete") &&
+ Services.prompt.confirmEx(window, null,
+ gFilterBundle.getString("deleteFilterConfirmation"),
+ Services.prompt.STD_YES_NO_BUTTONS,
+ '', '', '',
+ gFilterBundle.getString('dontWarnAboutDeleteCheckbox'),
+ checkValue))
+ return;
+
+ if (checkValue.value)
+ Services.prefs.setBoolPref("mailnews.filters.confirm_delete", false);
+
+ // Save filter position before the first selected one.
+ let newSelectionIndex = gFilterListbox.selectedIndex - 1;
+
+ // Must reverse the loop, as the items list shrinks when we delete.
+ for (let index = items.length - 1; index >= 0; --index) {
+ let item = items[index];
+ gCurrentFilterList.removeFilter(item._filter);
+ item.remove();
+ }
+ updateCountBox();
+
+ // Select filter above previously selected if one existed,
+ // otherwise the first one.
+ if (newSelectionIndex == -1 && gFilterListbox.itemCount > 0)
+ newSelectionIndex = 0;
+ if (newSelectionIndex > -1) {
+ gFilterListbox.selectedIndex = newSelectionIndex;
+ updateViewPosition(-1);
+ }
+}
+
+/**
+ * Move filter one step up in visible list.
+ */
+function onUp(event) {
+ moveFilter(msgMoveMotion.Up);
+}
+
+/**
+ * Move filter one step down in visible list.
+ */
+function onDown(event) {
+ moveFilter(msgMoveMotion.Down);
+}
+
+/**
+ * Move filter to bottom for long filter lists.
+ */
+function onTop(event) {
+ moveFilter(msgMoveMotion.Top);
+}
+
+/**
+ * Move filter to top for long filter lists.
+ */
+function onBottom(event) {
+ moveFilter(msgMoveMotion.Bottom);
+}
+
+/**
+ * Moves a singular selected filter up or down either 1 increment or to the
+ * top/bottom.
+ *
+ * @param motion
+ * msgMoveMotion.Up, msgMoveMotion.Down, msgMoveMotion.Top, msgMoveMotion.Bottom
+ */
+function moveFilter(motion) {
+ // At the moment, do not allow moving groups of filters.
+ let selectedFilter = currentFilter();
+ if (!selectedFilter)
+ return;
+
+ let relativeStep = 0;
+ let moveFilterNative;
+
+ switch (motion) {
+ case msgMoveMotion.Top:
+ if (selectedFilter) {
+ gCurrentFilterList.removeFilter(selectedFilter);
+ gCurrentFilterList.insertFilterAt(0, selectedFilter);
+ rebuildFilterList();
+ }
+ return;
+ case msgMoveMotion.Bottom:
+ if (selectedFilter) {
+ gCurrentFilterList.removeFilter(selectedFilter);
+ gCurrentFilterList.insertFilterAt(gCurrentFilterList.filterCount,
+ selectedFilter);
+ rebuildFilterList();
+ }
+ return;
+ case msgMoveMotion.Up:
+ relativeStep = -1;
+ moveFilterNative = Ci.nsMsgFilterMotion.up;
+ break;
+ case msgMoveMotion.Down:
+ relativeStep = +1;
+ moveFilterNative = Ci.nsMsgFilterMotion.down;
+ break;
+ }
+
+ if (!gSearchBox.value) {
+ // Use legacy move filter code: up, down; only if searchBox is empty.
+ moveCurrentFilter(moveFilterNative);
+ return;
+ }
+
+ let nextIndex = gFilterListbox.selectedIndex + relativeStep;
+ let nextFilter = gFilterListbox.getItemAtIndex(nextIndex)._filter;
+
+ gCurrentFilterList.removeFilter(selectedFilter);
+
+ // Find the index of the filter we want to insert at.
+ let newIndex = -1;
+ let filterCount = gCurrentFilterList.filterCount;
+ for (let i = 0; i < filterCount; i++) {
+ if (gCurrentFilterList.getFilterAt(i) == nextFilter) {
+ newIndex = i;
+ break;
+ }
+ }
+
+ if (motion == msgMoveMotion.Down)
+ newIndex += relativeStep;
+
+ gCurrentFilterList.insertFilterAt(newIndex, selectedFilter);
+
+ rebuildFilterList();
+}
+
+function viewLog()
+{
+ let args = {filterList: gCurrentFilterList};
+
+ window.openDialog("chrome://messenger/content/viewLog.xul", "FilterLog", "chrome,modal,titlebar,resizable,centerscreen", args);
+}
+
+function onFilterUnload()
+{
+ // make sure to save the filter to disk
+ if (gCurrentFilterList)
+ gCurrentFilterList.saveToDefaultFile();
+
+ Services.obs.removeObserver(onFilterClose, "quit-application-requested");
+ top.controllers.removeController(gFilterController);
+}
+
+function onFilterClose(aCancelQuit, aTopic, aData)
+{
+ if (aTopic == "quit-application-requested" &&
+ aCancelQuit instanceof Ci.nsISupportsPRBool &&
+ aCancelQuit.data)
+ return false;
+
+ if (gRunFiltersButton.getAttribute("label") == gRunFiltersButton.getAttribute("stoplabel")) {
+ var promptTitle = gFilterBundle.getString("promptTitle");
+ var promptMsg = gFilterBundle.getString("promptMsg");;
+ var stopButtonLabel = gFilterBundle.getString("stopButtonLabel");
+ var continueButtonLabel = gFilterBundle.getString("continueButtonLabel");
+
+ if (Services.prompt.confirmEx(window, promptTitle, promptMsg,
+ (Services.prompt.BUTTON_TITLE_IS_STRING *
+ Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1),
+ continueButtonLabel, stopButtonLabel, null, null, {value:0}) == 0) {
+ if (aTopic == "quit-application-requested")
+ aCancelQuit.data = true;
+ return false;
+ }
+ gFilterListMsgWindow.StopUrls();
+ }
+
+ return true;
+}
+
+function runSelectedFilters()
+{
+ // if run button has "stop" label, do stop.
+ if (gRunFiltersButton.getAttribute("label") == gRunFiltersButton.getAttribute("stoplabel")) {
+ gFilterListMsgWindow.StopUrls();
+ return;
+ }
+
+ let folder = gRunFiltersFolder._folder ||
+ gRunFiltersFolder.selectedItem._folder;
+ if (!folder)
+ return;
+
+ let filterList = MailServices.filters.getTempFilterList(folder);
+
+ // make sure the tmp filter list uses the real filter list log stream
+ filterList.logStream = gCurrentFilterList.logStream;
+ filterList.loggingEnabled = gCurrentFilterList.loggingEnabled;
+
+ let index = 0;
+ for (let item of gFilterListbox.selectedItems) {
+ filterList.insertFilterAt(index++, item._filter);
+ }
+
+ MailServices.filters.applyFiltersToFolders(filterList, [folder], gFilterListMsgWindow);
+}
+
+function moveCurrentFilter(motion)
+{
+ let filter = currentFilter();
+ if (!filter)
+ return;
+
+ gCurrentFilterList.moveFilter(filter, motion);
+ rebuildFilterList();
+}
+
+/**
+ * Redraws the list of filters. Takes the search box value into account.
+ *
+ * This function should perform very fast even in case of high number of filters.
+ * Therefore there are some optimizations (e.g. listelement.children[] instead of
+ * list.getItemAtIndex()), that favour speed vs. semantical perfection.
+ */
+function rebuildFilterList()
+{
+ // Get filters that match the search box.
+ let aTempFilterList = onFindFilter();
+
+ let searchBoxFocus = false;
+ let activeElement = document.activeElement;
+
+ // Find if the currently focused element is a child inside the search box
+ // (probably html:input). Traverse up the parents until the first element
+ // with an ID is found. If it is not searchBox, return false.
+ while (activeElement != null) {
+ if (activeElement == gSearchBox) {
+ searchBoxFocus = true;
+ break;
+ }
+ else if (activeElement.id) {
+ searchBoxFocus = false;
+ break;
+ }
+ activeElement = activeElement.parentNode;
+ }
+
+ // Make a note of which filters were previously selected
+ let selectedNames = [];
+ for (let i = 0; i < gFilterListbox.selectedItems.length; i++)
+ selectedNames.push(gFilterListbox.selectedItems[i]._filter.filterName);
+
+ // Save scroll position so we can try to restore it later.
+ // Doesn't work when the list is rebuilt after search box condition changed.
+ let firstVisibleRowIndex = gFilterListbox.getIndexOfFirstVisibleRow();
+
+ // listbox.xml seems to cache the value of the first selected item in a
+ // range at _selectionStart. The old value though is now obsolete,
+ // since we will recreate all of the elements. We need to clear this,
+ // and one way to do this is with a call to clearSelection. This might be
+ // ugly from an accessibility perspective, since it fires an onSelect event.
+ gFilterListbox.clearSelection();
+
+ let listitem, nameCell, enabledCell, filter;
+ let filterCount = gCurrentFilterList.filterCount;
+ let listitemCount = gFilterListbox.itemCount;
+ let listitemIndex = 0;
+ let tempFilterListLength = aTempFilterList ? aTempFilterList.length - 1 : 0;
+ for (let i = 0; i < filterCount; i++) {
+ if (aTempFilterList && listitemIndex > tempFilterListLength)
+ break;
+
+ filter = gCurrentFilterList.getFilterAt(i);
+ if (aTempFilterList && aTempFilterList[listitemIndex] != i)
+ continue;
+
+ if (listitemCount > listitemIndex) {
+ // If there is a free existing listitem, reuse it.
+ // Use .children[] instead of .getItemAtIndex() as it is much faster.
+ listitem = gFilterListbox.children[listitemIndex + 1];
+ nameCell = listitem.childNodes[0];
+ enabledCell = listitem.childNodes[1];
+ }
+ else
+ {
+ // If there are not enough listitems in the list, create a new one.
+ listitem = document.createElement("listitem");
+ listitem.setAttribute("role", "checkbox");
+ nameCell = document.createElement("listcell");
+ enabledCell = document.createElement("listcell");
+ enabledCell.setAttribute("class", "listcell-iconic");
+ listitem.appendChild(nameCell);
+ listitem.appendChild(enabledCell);
+ gFilterListbox.appendChild(listitem);
+ let size = (enabledCell.clientWidth - 28) / 2;
+ enabledCell.style.paddingLeft = size.toString() + "px";
+ // We have to attach this listener to the listitem, even though we only
+ // care about clicks on the enabledCell. However, attaching to that item
+ // doesn't result in any events actually getting received.
+ listitem.addEventListener("click", onFilterClick, true);
+ listitem.addEventListener("dblclick", onFilterDoubleClick, true);
+ }
+ // For accessibility set the label on listitem.
+ listitem.setAttribute("label", filter.filterName);
+ // Set the listitem values to represent the current filter.
+ nameCell.setAttribute("label", filter.filterName);
+ enabledCell.setAttribute("enabled", filter.enabled);
+ listitem.setAttribute("aria-checked", filter.enabled);
+ listitem._filter = filter;
+
+ if (selectedNames.includes(filter.filterName))
+ gFilterListbox.addItemToSelection(listitem);
+
+ listitemIndex++;
+ }
+ // Remove any superfluous listitems, if the number of filters shrunk.
+ for (let i = listitemCount - 1; i >= listitemIndex; i--) {
+ gFilterListbox.lastChild.remove();
+ }
+
+ updateViewPosition(firstVisibleRowIndex);
+ updateCountBox();
+
+ // If before rebuilding the list the searchbox was focused, focus it again.
+ // In any other case, focus the list.
+ if (searchBoxFocus)
+ gSearchBox.focus();
+ else
+ gFilterListbox.focus();
+}
+
+function updateViewPosition(firstVisibleRowIndex)
+{
+ if (firstVisibleRowIndex == -1)
+ firstVisibleRowIndex = gFilterListbox.getIndexOfFirstVisibleRow();
+
+ // Restore to the extent possible the scroll position.
+ if (firstVisibleRowIndex && gFilterListbox.itemCount)
+ gFilterListbox.scrollToIndex(Math.min(firstVisibleRowIndex,
+ gFilterListbox.itemCount - 1));
+
+ if (gFilterListbox.selectedCount) {
+ // Make sure that at least the first selected item is visible.
+ gFilterListbox.ensureElementIsVisible(gFilterListbox.selectedItems[0]);
+
+ // The current item should be the first selected item, so that keyboard
+ // selection extension can work.
+ gFilterListbox.currentItem = gFilterListbox.selectedItems[0];
+ }
+
+ updateButtons();
+}
+
+/**
+ * Try to only enable buttons that make sense
+ * - moving filters is currently only enabled for single selection
+ * also movement is restricted by searchBox and current selection position
+ * - edit only for single filters
+ * - delete / run only for one or more selected filters
+ */
+function updateButtons()
+{
+ var numFiltersSelected = gFilterListbox.selectedItems.length;
+ var oneFilterSelected = (numFiltersSelected == 1);
+
+ // "edit" only enabled when one filter selected
+ // or if we couldn't parse the filter.
+ let disabled = !oneFilterSelected || currentFilter().unparseable;
+ gEditButton.disabled = disabled;
+
+ // "copy" is the same as "edit".
+ gCopyToNewButton.disabled = disabled;
+
+ // "delete" only disabled when no filters are selected
+ gDeleteButton.disabled = !numFiltersSelected;
+
+ // we can run multiple filters on a folder
+ // so only disable this UI if no filters are selected
+ gRunFiltersFolderPrefix.disabled = !numFiltersSelected;
+ gRunFiltersFolder.disabled = !numFiltersSelected;
+ gRunFiltersButton.disabled = !numFiltersSelected ||
+ !gRunFiltersFolder._folder;
+
+ // "up" and "top" enabled only if one filter is selected,
+ // and it's not the first.
+ // Don't use gFilterListbox.currentIndex here, it's buggy when we've just
+ // changed the children in the list (via rebuildFilterList)
+ disabled = !(oneFilterSelected &&
+ gFilterListbox.getSelectedItem(0) != gFilterListbox.getItemAtIndex(0));
+ gUpButton.disabled = disabled;
+ gTopButton.disabled = disabled;
+
+ // "down" and "bottom" enabled only if one filter selected,
+ // and it's not the last.
+ disabled = !(oneFilterSelected &&
+ gFilterListbox.selectedIndex < gFilterListbox.itemCount - 1);
+ gDownButton.disabled = disabled;
+ gBottomButton.disabled = disabled;
+}
+
+/**
+ * Given a selected folder, returns the folder where filters should
+ * be defined (the root folder except for news) if the server can
+ * accept filters.
+ *
+ * @param nsIMsgFolder aFolder - selected folder, from window args
+ * @returns an nsIMsgFolder where the filter is defined
+ */
+function getFilterFolderForSelection(aFolder) {
+ if (!aFolder || !aFolder.server)
+ return null;
+
+ let rootFolder = aFolder.server.rootFolder;
+ if (rootFolder && rootFolder.isServer && rootFolder.server.canHaveFilters)
+ return (aFolder.server.type == "nntp") ? aFolder : rootFolder;
+
+ return null;
+}
+
+/**
+ * If the selected server cannot have filters, get the default server.
+ * If the default server cannot have filters, check all accounts
+ * and get a server that can have filters.
+ *
+ * @returns an nsIMsgIncomingServer
+ */
+function getServerThatCanHaveFilters()
+{
+ let defaultAccount = MailServices.accounts.defaultAccount;
+ if (defaultAccount) {
+ let defaultIncomingServer = defaultAccount.incomingServer;
+ // Check to see if default server can have filters.
+ if (defaultIncomingServer.canHaveFilters)
+ return defaultIncomingServer;
+ }
+
+ // if it cannot, check all accounts to find a server
+ // that can have filters
+ for (let currentServer of MailServices.accounts.allServers)
+ {
+ if (currentServer.canHaveFilters)
+ return currentServer;
+ }
+
+ return null;
+}
+
+function onFilterClick(event)
+{
+ // We only care about button 0 (left click) events.
+ if (event.button != 0)
+ return;
+
+ // Remember, we had to attach the click-listener to the whole listitem, so
+ // now we need to see if the clicked the enable-column
+ let toggle = event.target.childNodes[1];
+ if ((event.clientX < toggle.boxObject.x + toggle.boxObject.width) &&
+ (event.clientX > toggle.boxObject.x)) {
+ toggleFilter(event.target);
+ event.stopPropagation();
+ }
+}
+
+function onFilterDoubleClick(event)
+{
+ // We only care about button 0 (left click) events.
+ if (event.button != 0)
+ return;
+
+ onEditFilter();
+}
+
+function onFilterListKeyPress(aEvent) {
+ if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey || aEvent.shiftKey)
+ return;
+
+ if (aEvent.keyCode) {
+ switch (aEvent.keyCode) {
+ case KeyEvent.DOM_VK_INSERT:
+ if (!gNewButton.disabled)
+ onNewFilter();
+ break;
+ case KeyEvent.DOM_VK_DELETE:
+ if (!gDeleteButton.disabled)
+ onDeleteFilter();
+ break;
+ case KeyEvent.DOM_VK_RETURN:
+ if (!gEditButton.disabled)
+ onEditFilter();
+ break;
+ }
+ return;
+ }
+
+ switch (aEvent.charCode) {
+ case KeyEvent.DOM_VK_SPACE:
+ for (let item of gFilterListbox.selectedItems) {
+ toggleFilter(item);
+ }
+ break;
+ default:
+ gSearchBox.focus();
+ gSearchBox.value = String.fromCharCode(aEvent.charCode);
+ }
+}
+
+/**
+ * Decides if the given filter matches the given keyword.
+ *
+ * @param aFilter nsIMsgFilter to check
+ * @param aKeyword the string to find in the filter name
+ *
+ * @return True if the filter name contains the searched keyword.
+ Otherwise false. In the future this may be extended to match
+ other filter attributes.
+ */
+function filterSearchMatch(aFilter, aKeyword) {
+ return (aFilter.filterName.toLocaleLowerCase().includes(aKeyword))
+}
+
+/**
+ * Called from rebuildFilterList when the list needs to be redrawn.
+ * @return Uses the search term in search box, to produce an array of
+ * row (filter) numbers (indexes) that match the search term.
+ */
+function onFindFilter() {
+ let keyWord = gSearchBox.value.toLocaleLowerCase();
+
+ // If searchbox is empty, just return and let rebuildFilterList
+ // create an unfiltered list.
+ if (!keyWord)
+ return null;
+
+ // Rematch everything in the list, remove what doesn't match the search box.
+ let rows = gCurrentFilterList.filterCount;
+ let matchingFilterList = [];
+ // Use the full gCurrentFilterList, not the filterList listbox,
+ // which may already be filtered.
+ for (let i = 0; i < rows; i++) {
+ if (filterSearchMatch(gCurrentFilterList.getFilterAt(i), keyWord))
+ matchingFilterList.push(i);
+ }
+
+ return matchingFilterList;
+}
+
+/**
+ * Clear the search term in the search box if needed.
+ *
+ * @param aFilter If this nsIMsgFilter matches the search term,
+ * do not reset the box. If this is null,
+ * reset unconditionally.
+ */
+function resetSearchBox(aFilter) {
+ let keyword = gSearchBox.value.toLocaleLowerCase();
+ if (keyword && (!aFilter || !filterSearchMatch(aFilter, keyword)))
+ gSearchBox.reset();
+}
+
+/**
+ * Display "1 item", "11 items" or "4 of 10" if list is filtered via search box.
+ */
+function updateCountBox() {
+ let countBox = document.getElementById("countBox");
+ let sum = gCurrentFilterList.filterCount;
+ let len = gFilterListbox.itemCount;
+
+ if (len == sum) {
+ // "N items"
+ countBox.value = PluralForm.get(len, gFilterBundle.getString("filterCountItems"))
+ .replace("#1", len);
+ countBox.removeAttribute("filterActive");
+ } else {
+ // "N of M"
+ countBox.value = gFilterBundle.getFormattedString("filterCountVisibleOfTotal",
+ [len, sum]);
+ if (len == 0 && sum > 0)
+ countBox.setAttribute("filterActive", "nomatches");
+ else
+ countBox.setAttribute("filterActive", "matches");
+ }
+}
+
+function doHelpButton()
+{
+ openHelp("mail-filters");
+}
+
+var gFilterController =
+{
+ supportsCommand: function(aCommand)
+ {
+ return aCommand == "cmd_selectAll";
+ },
+
+ isCommandEnabled: function(aCommand)
+ {
+ return aCommand == "cmd_selectAll";
+ },
+
+ doCommand: function(aCommand)
+ {
+ if (aCommand == "cmd_selectAll")
+ gFilterListbox.selectAll();
+ },
+
+ onEvent: function(aEvent)
+ {
+ }
+};
diff --git a/comm/suite/mailnews/content/FilterListDialog.xul b/comm/suite/mailnews/content/FilterListDialog.xul
new file mode 100644
index 0000000000..95f6d473ae
--- /dev/null
+++ b/comm/suite/mailnews/content/FilterListDialog.xul
@@ -0,0 +1,197 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/filterDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/FilterListDialog.dtd">
+
+<dialog id="filterListDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="mailnews:filterlist"
+ title="&filterListDialog.title;"
+ style="width: 45em; height: 31em;"
+ persist="width height screenX screenY"
+ buttons="help"
+ ondialoghelp="return openHelp('mail-filters');"
+ onload="onLoad();"
+ onunload="onFilterUnload();"
+ onclose="return onFilterClose();">
+
+ <script src="chrome://messenger/content/FilterListDialog.js"/>
+
+ <stringbundle id="bundle_filter"
+ src="chrome://messenger/locale/filter.properties"/>
+ <stringbundle id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"/>
+
+ <keyset id="filterKeys">
+ <key id="key_selectAll"/>
+ </keyset>
+
+ <hbox align="center">
+ <label value="&filtersForPrefix.label;"
+ accesskey="&filtersForPrefix.accesskey;"
+ control="serverMenu"/>
+
+ <menulist id="serverMenu"
+ class="folderMenuItem"
+ IsServer="true"
+ IsSecure="false"
+ ServerType="none">
+ <menupopup id="serverMenuPopup"
+ class="menulist-menupopup"
+ type="folder"
+ mode="filters"
+ expandFolders="nntp"
+ showFileHereLabel="true"
+ showAccountsFileHere="true"
+ oncommand="setFilterFolder(event.target._folder)"/>
+ </menulist>
+ <textbox id="searchBox"
+ class="searchBox"
+ flex="1"
+ type="search"
+ oncommand="rebuildFilterList();"
+ emptytext="&searchBox.emptyText;"
+ isempty="true"/>
+ </hbox>
+
+ <grid flex="1">
+ <columns>
+ <column flex="1"/>
+ <column/>
+ </columns>
+ <rows>
+ <row>
+ <separator class="thin"/>
+ </row>
+
+ <row>
+ <hbox>
+ <label id="filterHeader"
+ flex="1"
+ control="filterTree">&filterHeader.label;</label>
+ <label id="countBox"/>
+ </hbox>
+ </row>
+
+ <row flex="1">
+ <vbox>
+ <listbox id="filterList"
+ flex="1"
+ seltype="multiple"
+ onselect="updateButtons();"
+ onkeypress="onFilterListKeyPress(event);">
+ <listhead>
+ <listheader id="nameColumn"
+ label="&nameColumn.label;"
+ flex="1"/>
+ <listheader id="activeColumn"
+ label="&activeColumn.label;"
+ minwidth="40px"/>
+ </listhead>
+ </listbox>
+ </vbox>
+
+ <vbox>
+ <button id="newButton"
+ label="&newButton.label;"
+ accesskey="&newButton.accesskey;"
+ oncommand="onNewFilter();"/>
+ <button id="copyToNewButton"
+ label="&copyButton.label;"
+ accesskey="&copyButton.accesskey;"
+ oncommand="onCopyToNewFilter();"/>
+ <button id="editButton"
+ label="&editButton.label;"
+ accesskey="&editButton.accesskey;"
+ oncommand="onEditFilter();"/>
+ <button id="deleteButton"
+ label="&deleteButton.label;"
+ accesskey="&deleteButton.accesskey;"
+ oncommand="onDeleteFilter();"/>
+ <spacer flex="1"/>
+ <button id="reorderTopButton"
+ label="&reorderTopButton;"
+ accesskey="&reorderTopButton.accessKey;"
+ tooltiptext="&reorderTopButton.toolTip;"
+ oncommand="onTop(event);"/>
+ <button id="reorderUpButton"
+ label="&reorderUpButton.label;"
+ accesskey="&reorderUpButton.accesskey;"
+ class="up"
+ oncommand="onUp(event);"/>
+ <button id="reorderDownButton"
+ label="&reorderDownButton.label;"
+ accesskey="&reorderDownButton.accesskey;"
+ class="down"
+ oncommand="onDown(event);"/>
+ <button id="reorderBottomButton"
+ label="&reorderBottomButton;"
+ accesskey="&reorderBottomButton.accessKey;"
+ tooltiptext="&reorderBottomButton.toolTip;"
+ oncommand="onBottom(event);"/>
+ <spacer flex="1"/>
+ <button dlgtype="help" class="dialog-button"/>
+ </vbox>
+ </row>
+
+ <row>
+ <separator class="thin"/>
+ </row>
+
+ <row align="center">
+ <hbox align="center">
+ <label id="folderPickerPrefix"
+ value="&folderPickerPrefix.label;"
+ accesskey="&folderPickerPrefix.accesskey;"
+ disabled="true"
+ control="runFiltersFolder"/>
+
+ <menulist id="runFiltersFolder"
+ flex="1"
+ disabled="true"
+ class="folderMenuItem"
+ displayformat="verbose">
+ <menupopup id="runFiltersPopup"
+ class="menulist-menupopup"
+ type="folder"
+ showFileHereLabel="true"
+ showAccountsFileHere="false"
+ oncommand="setRunFolder(event.target._folder);"/>
+ </menulist>
+ <spacer flex="1"/>
+ <button id="runFiltersButton"
+ label="&runFilters.label;"
+ accesskey="&runFilters.accesskey;"
+ runlabel="&runFilters.label;"
+ runaccesskey="&runFilters.accesskey;"
+ stoplabel="&stopFilters.label;"
+ stopaccesskey="&stopFilters.accesskey;"
+ disabled="true"
+ oncommand="runSelectedFilters();"/>
+ </hbox>
+ <vbox>
+ <button label="&viewLogButton.label;"
+ accesskey="&viewLogButton.accesskey;"
+ oncommand="viewLog();"/>
+ </vbox>
+ </row>
+ </rows>
+ </grid>
+
+ <statusbar class="chromeclass-status" id="status-bar">
+ <statusbarpanel class="statusbarpanel-progress">
+ <progressmeter id="statusbar-icon"
+ class="progressmeter-statusbar"
+ mode="normal"
+ value="0"/>
+ </statusbarpanel>
+ <statusbarpanel id="statusText" crop="right" flex="1"/>
+ </statusbar>
+</dialog>
diff --git a/comm/suite/mailnews/content/SearchDialog.js b/comm/suite/mailnews/content/SearchDialog.js
new file mode 100644
index 0000000000..665b12e16b
--- /dev/null
+++ b/comm/suite/mailnews/content/SearchDialog.js
@@ -0,0 +1,729 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js");
+const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+
+var searchSessionContractID = "@mozilla.org/messenger/searchSession;1";
+var gDBView;
+var gSearchSession;
+var gMsgFolderSelected;
+
+var nsIMsgFolder = Ci.nsIMsgFolder;
+var nsIMsgWindow = Ci.nsIMsgWindow;
+var nsMsgSearchScope = Ci.nsMsgSearchScope;
+
+var gFolderPicker;
+var gStatusBar = null;
+var gStatusFeedback = new nsMsgStatusFeedback();
+var gMessengerBundle = null;
+var RDF;
+var gSearchBundle;
+var gNextMessageViewIndexAfterDelete = -2;
+
+// Datasource search listener -- made global as it has to be registered
+// and unregistered in different functions.
+var gDataSourceSearchListener;
+var gViewSearchListener;
+
+var gSearchStopButton;
+
+// Controller object for search results thread pane
+var nsSearchResultsController =
+{
+ supportsCommand: function(command)
+ {
+ switch(command) {
+ case "cmd_openMessage":
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ case "button_delete":
+ case "file_message_button":
+ case "goto_folder_button":
+ case "saveas_vf_button":
+ case "cmd_selectAll":
+ case "cmd_markAsRead":
+ case "cmd_markAsUnread":
+ case "cmd_markAsFlagged":
+ return true;
+ default:
+ return false;
+ }
+ },
+
+ // this controller only handles commands
+ // that rely on items being selected in
+ // the search results pane.
+ isCommandEnabled: function(command)
+ {
+ var enabled = true;
+
+ switch (command) {
+ case "goto_folder_button":
+ if (GetNumSelectedMessages() != 1)
+ enabled = false;
+ break;
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ case "button_delete":
+ // this assumes that advanced searches don't cross accounts
+ if (GetNumSelectedMessages() <= 0)
+ enabled = false;
+ break;
+ case "saveas_vf_button":
+ // need someway to see if there are any search criteria...
+ return true;
+ case "cmd_selectAll":
+ return GetDBView() != null;
+ default:
+ if (GetNumSelectedMessages() <= 0)
+ enabled = false;
+ break;
+ }
+
+ return enabled;
+ },
+
+ doCommand: function(command)
+ {
+ switch(command) {
+ case "cmd_openMessage":
+ MsgOpenSelectedMessages();
+ return true;
+
+ case "cmd_delete":
+ case "button_delete":
+ MsgDeleteSelectedMessages(nsMsgViewCommandType.deleteMsg);
+ return true;
+ case "cmd_shiftDelete":
+ MsgDeleteSelectedMessages(nsMsgViewCommandType.deleteNoTrash);
+ return true;
+
+ case "goto_folder_button":
+ GoToFolder();
+ return true;
+
+ case "saveas_vf_button":
+ saveAsVirtualFolder();
+ return true;
+
+ case "cmd_selectAll":
+ // move the focus to the search results pane
+ GetThreadTree().focus();
+ GetDBView().doCommand(nsMsgViewCommandType.selectAll)
+ return true;
+
+ case "cmd_markAsRead":
+ MsgMarkMsgAsRead(true);
+ return true;
+
+ case "cmd_markAsUnread":
+ MsgMarkMsgAsRead(false);
+ return true;
+
+ case "cmd_markAsFlagged":
+ MsgMarkAsFlagged();
+ return true;
+
+ default:
+ return false;
+ }
+
+ },
+
+ onEvent: function(event)
+ {
+ }
+}
+
+function UpdateMailSearch(caller)
+{
+ //dump("XXX update mail-search " + caller + "\n");
+ document.commandDispatcher.updateCommands('mail-search');
+}
+
+function SetAdvancedSearchStatusText(aNumHits)
+{
+ var statusMsg;
+ // if there are no hits, it means no matches were found in the search.
+ if (aNumHits == 0)
+ {
+ statusMsg = gSearchBundle.getString("noMatchesFound");
+ }
+ else
+ {
+ statusMsg = PluralForm.get(aNumHits,
+ gSearchBundle.getString("matchesFound"));
+ statusMsg = statusMsg.replace("#1", aNumHits);
+ }
+ gStatusFeedback.showStatusString(statusMsg);
+}
+
+// nsIMsgSearchNotify object
+var gSearchNotificationListener =
+{
+ onSearchHit: function(header, folder)
+ {
+ // XXX TODO
+ // update status text?
+ },
+
+ onSearchDone: function(status)
+ {
+ gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForSearchButton"));
+ gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
+ gStatusFeedback._stopMeteors();
+ SetAdvancedSearchStatusText(gDBView.QueryInterface(Ci.nsITreeView).rowCount);
+ },
+
+ onNewSearch: function()
+ {
+ gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForStopButton"));
+ gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForStopButton.accesskey"));
+ UpdateMailSearch("new-search");
+ gStatusFeedback._startMeteors();
+ gStatusFeedback.showStatusString(gSearchBundle.getString("searchingMessage"));
+ }
+}
+
+// the folderListener object
+var gFolderListener = {
+ onFolderAdded: function(parentFolder, child) {},
+ onMessageAdded: function(parentFolder, msg) {},
+ onFolderRemoved: function(parentFolder, child) {},
+ onMessageRemoved: function(parentFolder, msg) {},
+
+ onFolderPropertyChanged: function(item, property, oldValue, newValue) {},
+
+ onFolderIntPropertyChanged: function(item, property, oldValue, newValue) {},
+
+ onFolderBoolPropertyChanged: function(item, property, oldValue, newValue) {},
+
+ onFolderUnicharPropertyChanged: function(item, property, oldValue, newValue){},
+ onFolderPropertyFlagChanged: function(item, property, oldFlag, newFlag) {},
+
+ onFolderEvent: function(folder, event) {
+ if (event == "DeleteOrMoveMsgCompleted") {
+ HandleDeleteOrMoveMessageCompleted(folder);
+ }
+ else if (event == "DeleteOrMoveMsgFailed") {
+ HandleDeleteOrMoveMessageFailed(folder);
+ }
+ }
+}
+
+function HideSearchColumn(id)
+{
+ var col = document.getElementById(id);
+ if (col) {
+ col.setAttribute("hidden","true");
+ col.setAttribute("ignoreincolumnpicker","true");
+ }
+}
+
+function ShowSearchColumn(id)
+{
+ var col = document.getElementById(id);
+ if (col) {
+ col.removeAttribute("hidden");
+ col.removeAttribute("ignoreincolumnpicker");
+ }
+}
+
+function searchOnLoad()
+{
+ setHelpFileURI("chrome://communicator/locale/help/suitehelp.rdf");
+ initializeSearchWidgets();
+ initializeSearchWindowWidgets();
+ messenger = Cc["@mozilla.org/messenger;1"]
+ .createInstance(Ci.nsIMessenger);
+
+ gSearchBundle = document.getElementById("bundle_search");
+ gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForSearchButton"));
+ gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForSearchButton.accesskey"));
+ gMessengerBundle = document.getElementById("bundle_messenger");
+ setupDatasource();
+ setupSearchListener();
+
+ if (window.arguments && window.arguments[0])
+ selectFolder(window.arguments[0].folder);
+
+ onMore(null);
+ UpdateMailSearch("onload");
+
+ // hide and remove these columns from the column picker. you can't thread search results
+ HideSearchColumn("threadCol"); // since you can't thread search results
+ HideSearchColumn("totalCol"); // since you can't thread search results
+ HideSearchColumn("unreadCol"); // since you can't thread search results
+ HideSearchColumn("unreadButtonColHeader");
+ HideSearchColumn("idCol");
+ HideSearchColumn("junkStatusCol");
+ HideSearchColumn("accountCol");
+
+ // we want to show the location column for search
+ ShowSearchColumn("locationCol");
+}
+
+function searchOnUnload()
+{
+ // unregister listeners
+ gSearchSession.unregisterListener(gViewSearchListener);
+ gSearchSession.unregisterListener(gSearchNotificationListener);
+
+ MailServices.mailSession.RemoveFolderListener(gFolderListener);
+
+ if (gDBView)
+ {
+ gDBView.close();
+ gDBView = null;
+ }
+
+ top.controllers.removeController(nsSearchResultsController);
+
+ // release this early because msgWindow holds a weak reference
+ msgWindow.rootDocShell = null;
+}
+
+function initializeSearchWindowWidgets()
+{
+ gFolderPicker = document.getElementById("searchableFolders");
+ gSearchStopButton = document.getElementById("search-button");
+ gStatusBar = document.getElementById('statusbar-icon');
+ hideMatchAllItem();
+
+ msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"]
+ .createInstance(nsIMsgWindow);
+ msgWindow.domWindow = window;
+ msgWindow.rootDocShell.allowAuth = true;
+ msgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL;
+ msgWindow.statusFeedback = gStatusFeedback;
+
+ // functionality to enable/disable buttons using nsSearchResultsController
+ // depending of whether items are selected in the search results thread pane.
+ top.controllers.insertControllerAt(0, nsSearchResultsController);
+}
+
+
+function onSearchStop() {
+ gSearchSession.interruptSearch();
+}
+
+function onResetSearch(event) {
+ onReset(event);
+
+ var tree = GetThreadTree();
+ tree.treeBoxObject.view = null;
+ gStatusFeedback.showStatusString("");
+}
+
+function selectFolder(folder)
+{
+ var folderURI;
+
+ // if we can't search messages on this folder, just select the first one
+ if (!folder || !folder.server.canSearchMessages ||
+ (folder.flags & Ci.nsMsgFolderFlags.Virtual)) {
+ // find first item in our folder picker menu list
+ folderURI = gFolderPicker.firstChild.tree.builderView.getResourceAtIndex(0).Value;
+ } else {
+ folderURI = folder.URI;
+ }
+ updateSearchFolderPicker(folderURI);
+}
+
+function updateSearchFolderPicker(folderURI)
+{
+ SetFolderPicker(folderURI, gFolderPicker.id);
+
+ // use the URI to get the real folder
+ gMsgFolderSelected = MailUtils.getFolderForURI(folderURI);
+
+ var searchSubFolders = document.getElementById("checkSearchSubFolders");
+ if (searchSubFolders)
+ searchSubFolders.disabled = !gMsgFolderSelected.hasSubFolders;
+ var searchLocalSystem = document.getElementById("menuSearchLocalSystem");
+ if (searchLocalSystem)
+ searchLocalSystem.disabled = gMsgFolderSelected.server.searchScope == nsMsgSearchScope.offlineMail;
+ setSearchScope(GetScopeForFolder(gMsgFolderSelected));
+}
+
+function updateSearchLocalSystem()
+{
+ setSearchScope(GetScopeForFolder(gMsgFolderSelected));
+}
+
+function UpdateAfterCustomHeaderChange()
+{
+ updateSearchAttributes();
+}
+
+function onChooseFolder(event) {
+ var folderURI = event.id;
+ if (folderURI) {
+ updateSearchFolderPicker(folderURI);
+ }
+}
+
+function onEnterInSearchTerm()
+{
+ // on enter
+ // if not searching, start the search
+ // if searching, stop and then start again
+ if (gSearchStopButton.getAttribute("label") == gSearchBundle.getString("labelForSearchButton")) {
+ onSearch();
+ }
+ else {
+ onSearchStop();
+ onSearch();
+ }
+}
+
+function onSearch()
+{
+ // set the view. do this on every search, to
+ // allow the tree to reset itself
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ if (treeView)
+ {
+ var tree = GetThreadTree();
+ tree.treeBoxObject.view = treeView;
+ }
+
+ gSearchSession.clearScopes();
+ // tell the search session what the new scope is
+ if (!gMsgFolderSelected.isServer && !gMsgFolderSelected.noSelect)
+ gSearchSession.addScopeTerm(GetScopeForFolder(gMsgFolderSelected),
+ gMsgFolderSelected);
+
+ var searchSubfolders = document.getElementById("checkSearchSubFolders").checked;
+ if (gMsgFolderSelected && (searchSubfolders || gMsgFolderSelected.isServer || gMsgFolderSelected.noSelect))
+ {
+ AddSubFolders(gMsgFolderSelected);
+ }
+ // reflect the search widgets back into the search session
+ gSearchSession.searchTerms = saveSearchTerms(gSearchSession.searchTerms, gSearchSession);
+
+ try
+ {
+ gSearchSession.search(msgWindow);
+ }
+ catch(ex)
+ {
+ dump("Search Exception\n");
+ }
+ // refresh the tree after the search starts, because initiating the
+ // search will cause the datasource to clear itself
+}
+
+function AddSubFolders(folder) {
+ for (let nextFolder of folder.subFolders) {
+ if (!(nextFolder.flags & Ci.nsMsgFolderFlags.Virtual))
+ {
+ if (!nextFolder.noSelect)
+ gSearchSession.addScopeTerm(GetScopeForFolder(nextFolder), nextFolder);
+
+ AddSubFolders(nextFolder);
+ }
+ }
+}
+
+function AddSubFoldersToURI(folder)
+{
+ var returnString = "";
+
+ for (let nextFolder of folder.subFolders) {
+ {
+ if (!(nextFolder.flags & Ci.nsMsgFolderFlags.Virtual))
+ {
+ if (!nextFolder.noSelect && !nextFolder.isServer)
+ {
+ if (returnString.length > 0)
+ returnString += '|';
+ returnString += nextFolder.URI;
+ }
+ var subFoldersString = AddSubFoldersToURI(nextFolder);
+ if (subFoldersString.length > 0)
+ {
+ if (returnString.length > 0)
+ returnString += '|';
+ returnString += subFoldersString;
+ }
+ }
+ }
+ return returnString;
+}
+
+
+function GetScopeForFolder(folder)
+{
+ var searchLocalSystem = document.getElementById("menuSearchLocalSystem");
+ return searchLocalSystem && searchLocalSystem.value == "local" ?
+ nsMsgSearchScope.offlineMail :
+ folder.server.searchScope;
+}
+
+var nsMsgViewSortType = Ci.nsMsgViewSortType;
+var nsMsgViewSortOrder = Ci.nsMsgViewSortOrder;
+var nsMsgViewFlagsType = Ci.nsMsgViewFlagsType;
+var nsMsgViewCommandType = Ci.nsMsgViewCommandType;
+
+function goUpdateSearchItems(commandset)
+{
+ for (var i = 0; i < commandset.childNodes.length; i++)
+ {
+ var commandID = commandset.childNodes[i].getAttribute("id");
+ if (commandID)
+ {
+ goUpdateCommand(commandID);
+ }
+ }
+}
+
+function nsMsgSearchCommandUpdater()
+{}
+
+nsMsgSearchCommandUpdater.prototype =
+{
+ updateCommandStatus : function()
+ {
+ // the back end is smart and is only telling us to update command status
+ // when the # of items in the selection has actually changed.
+ document.commandDispatcher.updateCommands('mail-search');
+ },
+ displayMessageChanged : function(aFolder, aSubject, aKeywords)
+ {
+ },
+
+ updateNextMessageAfterDelete : function()
+ {
+ SetNextMessageAfterDelete();
+ },
+
+ summarizeSelection: function() {return false},
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Ci.nsIMsgDBViewCommandUpdater) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_NOINTERFACE;
+ }
+}
+
+function setupDatasource() {
+ gDBView = Cc["@mozilla.org/messenger/msgdbview;1?type=search"]
+ .createInstance(Ci.nsIMsgDBView);
+ var count = new Object;
+ var cmdupdator = new nsMsgSearchCommandUpdater();
+
+ gDBView.init(messenger, msgWindow, cmdupdator);
+ gDBView.open(null, nsMsgViewSortType.byId, nsMsgViewSortOrder.ascending, nsMsgViewFlagsType.kNone, count);
+
+ // the thread pane needs to use the search datasource (to get the
+ // actual list of messages) and the message datasource (to get any
+ // attributes about each message)
+ gSearchSession = Cc[searchSessionContractID].createInstance(Ci.nsIMsgSearchSession);
+
+ var nsIFolderListener = Ci.nsIFolderListener;
+ var notifyFlags = nsIFolderListener.event;
+ MailServices.mailSession.AddFolderListener(gFolderListener, notifyFlags);
+
+ // the datasource is a listener on the search results
+ gViewSearchListener = gDBView.QueryInterface(Ci.nsIMsgSearchNotify);
+ gSearchSession.registerListener(gViewSearchListener);
+}
+
+
+function setupSearchListener()
+{
+ // Setup the javascript object as a listener on the search results
+ gSearchSession.registerListener(gSearchNotificationListener);
+}
+
+// used to toggle functionality for Search/Stop button.
+function onSearchButton(event)
+{
+ if (event.target.label == gSearchBundle.getString("labelForSearchButton"))
+ onSearch();
+ else
+ onSearchStop();
+}
+
+// Stuff after this is implemented to make the thread pane work.
+function GetNumSelectedMessages()
+{
+ try {
+ return gDBView.numSelected;
+ }
+ catch (ex) {
+ return 0;
+ }
+}
+
+function GetDBView()
+{
+ return gDBView;
+}
+
+function MsgDeleteSelectedMessages(aCommandType)
+{
+ SetNextMessageAfterDelete();
+ gDBView.doCommand(aCommandType);
+}
+
+function SetNextMessageAfterDelete()
+{
+ gNextMessageViewIndexAfterDelete = gDBView.msgToSelectAfterDelete;
+}
+
+function HandleDeleteOrMoveMessageFailed(folder)
+{
+ gDBView.onDeleteCompleted(false);
+ gNextMessageViewIndexAfterDelete = -2;
+}
+
+function HandleDeleteOrMoveMessageCompleted(folder)
+{
+ gDBView.onDeleteCompleted(true);
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var treeSelection = treeView.selection;
+ var viewSize = treeView.rowCount;
+
+ if (gNextMessageViewIndexAfterDelete == -2) {
+ // a move or delete can cause our selection can change underneath us.
+ // this can happen when the user
+ // deletes message from the stand alone msg window
+ // or the three pane
+ if (!treeSelection) {
+ // this can happen if you open the search window
+ // and before you do any searches
+ // and you do delete from another mail window
+ return;
+ }
+ else if (treeSelection.count == 0) {
+ // this can happen if you double clicked a message
+ // in the thread pane, and deleted it from the stand alone msg window
+ // see bug #185147
+ treeSelection.clearSelection();
+
+ UpdateMailSearch("delete from another view, 0 rows now selected");
+ }
+ else if (treeSelection.count == 1) {
+ // this can happen if you had two messages selected
+ // in the search results pane, and you deleted one of them from another view
+ // (like the view in the stand alone msg window or the three pane)
+ // since one item is selected, we should load it.
+ var startIndex = {};
+ var endIndex = {};
+ treeSelection.getRangeAt(0, startIndex, endIndex);
+
+ // select the selected item, so we'll load it
+ treeSelection.select(startIndex.value);
+ treeView.selectionChanged();
+
+ EnsureRowInThreadTreeIsVisible(startIndex.value);
+ UpdateMailSearch("delete from another view, 1 row now selected");
+ }
+ else {
+ // this can happen if you have more than 2 messages selected
+ // in the search results pane, and you deleted one of them from another view
+ // (like the view in the stand alone msg window or the three pane)
+ // since multiple messages are still selected, do nothing.
+ }
+ }
+ else {
+ if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None && gNextMessageViewIndexAfterDelete >= viewSize)
+ {
+ if (viewSize > 0)
+ gNextMessageViewIndexAfterDelete = viewSize - 1;
+ else
+ {
+ gNextMessageViewIndexAfterDelete = nsMsgViewIndex_None;
+
+ // there is nothing to select since viewSize is 0
+ treeSelection.clearSelection();
+
+ UpdateMailSearch("delete from current view, 0 rows left");
+ }
+ }
+
+ // if we are about to set the selection with a new element then DON'T clear
+ // the selection then add the next message to select. This just generates
+ // an extra round of command updating notifications that we are trying to
+ // optimize away.
+ if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None)
+ {
+ treeSelection.select(gNextMessageViewIndexAfterDelete);
+ // since gNextMessageViewIndexAfterDelete probably has the same value
+ // as the last index we had selected, the tree isn't generating a new
+ // selectionChanged notification for the tree view. So we aren't loading the
+ // next message. to fix this, force the selection changed update.
+ if (treeView)
+ treeView.selectionChanged();
+
+ EnsureRowInThreadTreeIsVisible(gNextMessageViewIndexAfterDelete);
+
+ // XXX TODO
+ // I think there is a bug in the suppression code above.
+ // what if I have two rows selected, and I hit delete,
+ // and so we load the next row.
+ // what if I have commands that only enable where
+ // exactly one row is selected?
+ UpdateMailSearch("delete from current view, at least one row selected");
+ }
+ }
+
+ // default value after delete/move/copy is over
+ gNextMessageViewIndexAfterDelete = -2;
+
+ // something might have been deleted, so update the status text
+ SetAdvancedSearchStatusText(viewSize);
+}
+
+function MoveMessageInSearch(destFolder)
+{
+ if (destFolder._folder)
+ {
+ try {
+ SetNextMessageAfterDelete();
+ gDBView.doCommandWithFolder(nsMsgViewCommandType.moveMessages,
+ destFolder._folder);
+ }
+ catch (ex) {
+ dump("MoveMessageInSearch failed: " + ex + "\n");
+ }
+ }
+}
+
+function GoToFolder()
+{
+ var hdr = gDBView.hdrForFirstSelectedMessage;
+ MsgOpenNewWindowForFolder(hdr.folder.URI, hdr.messageKey);
+}
+
+function saveAsVirtualFolder()
+{
+ let searchFolderURIs = window.arguments[0].folder.URI;
+
+ var searchSubfolders = document.getElementById("checkSearchSubFolders").checked;
+ if (gMsgFolderSelected && (searchSubfolders || gMsgFolderSelected.isServer || gMsgFolderSelected.noSelect))
+ {
+ var subFolderURIs = AddSubFoldersToURI(gMsgFolderSelected);
+ if (subFolderURIs.length > 0)
+ searchFolderURIs += '|' + subFolderURIs;
+ }
+
+ var dialog = window.openDialog("chrome://messenger/content/virtualFolderProperties.xul", "",
+ "chrome,titlebar,modal,centerscreen",
+ {folder:window.arguments[0].folder,
+ searchTerms:gSearchSession.searchTerms,
+ searchFolderURIs: searchFolderURIs});
+}
+
+function OnTagsChange()
+{
+ // Dummy, called by RemoveAllMessageTags and ToggleMessageTag
+}
diff --git a/comm/suite/mailnews/content/SearchDialog.xul b/comm/suite/mailnews/content/SearchDialog.xul
new file mode 100644
index 0000000000..65fabb731b
--- /dev/null
+++ b/comm/suite/mailnews/content/SearchDialog.xul
@@ -0,0 +1,178 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://messenger/skin/searchDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/threadPane.xul"?>
+<?xul-overlay href="chrome://messenger/content/searchTermOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/mailKeysOverlay.xul"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/SearchDialog.dtd">
+
+<dialog id="searchMailWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ windowtype="mailnews:search"
+ title="&searchDialogTitle.label;"
+ style="width: 52em; height: 34em;"
+ persist="screenX screenY width height sizemode"
+ buttons="help"
+ ondialoghelp="return openHelp('search_messages');"
+ ondialogaccept="return false; /* allow Search on Enter */"
+ onload="searchOnLoad();"
+ onunload="onSearchStop(); searchOnUnload();">
+
+ <stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/>
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+
+ <script src="chrome://messenger/content/mailWindow.js"/>
+ <script src="chrome://messenger/content/msgMail3PaneWindow.js"/>
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://messenger/content/mailCommands.js"/>
+ <script src="chrome://messenger/content/mailWindowOverlay.js"/>
+ <script src="chrome://messenger/content/commandglue.js"/>
+ <script src="chrome://messenger/content/SearchDialog.js"/>
+ <script src="chrome://messenger/content/msgFolderPickerOverlay.js"/>
+ <script src="chrome://messenger/content/tabmail.js"/>
+ <script src="chrome://messenger/content/folderDisplay.js"/>
+ <script src="chrome://global/content/contentAreaUtils.js"/>
+ <script src="chrome://messenger-newsblog/content/newsblogOverlay.js"/>
+
+ <commands id="commands">
+ <commandset id="mailSearchItems"
+ commandupdater="true"
+ events="mail-search"
+ oncommandupdate="goUpdateSearchItems(this)">
+ <command id="cmd_openMessage" oncommand="goDoCommand('cmd_openMessage');" disabled="true"/>
+ <command id="button_delete" oncommand="goDoCommand('button_delete')" disabled="true"/>
+ <command id="goto_folder_button" oncommand="goDoCommand('goto_folder_button')" disabled="true"/>
+ <command id="saveas_vf_button" oncommand="goDoCommand('saveas_vf_button')" disabled="false"/>
+ <command id="file_message_button"/>
+ <command id="cmd_delete"/>
+ <command id="cmd_shiftDelete" oncommand="goDoCommand('cmd_shiftDelete');"/>
+ </commandset>
+ </commands>
+
+ <keyset id="mailKeys"/>
+
+ <broadcasterset id="mailBroadcasters">
+ <broadcaster id="Communicator:WorkMode"/>
+ </broadcasterset>
+
+ <dummy class="usesMailWidgets"/>
+
+ <vbox id="searchTerms" flex="3" persist="height">
+ <vbox>
+ <hbox align="center">
+ <label value="&searchHeading.label;" accesskey="&searchHeading.accesskey;"
+ control="searchableFolders"/>
+ <menulist id="searchableFolders" flex="2"
+ class="folderMenuItem"
+ displayformat="verbose">
+ <menupopup class="menulist-menupopup"
+ type="folder"
+ mode="search"
+ showAccountsFileHere="true"
+ showFileHereLabel="true"
+ oncommand="updateSearchFolderPicker(event.target.id);"/>
+ </menulist>
+ <checkbox id="checkSearchSubFolders"
+ label="&searchSubfolders.label;"
+ checked="true"
+ accesskey="&searchSubfolders.accesskey;"/>
+ <spacer flex="3"/>
+ <button id="search-button" oncommand="onSearchButton(event);" default="true"/>
+ </hbox>
+ <hbox align="center">
+ <label id="searchOnHeading"
+ value="&searchOnHeading.label;"
+ accesskey="&searchOnHeading.accesskey;"
+ control="menuSearchLocalSystem">
+ <observes element="menuSearchLocalSystem"
+ attribute="disabled"/>
+ </label>
+ <menulist id="menuSearchLocalSystem"
+ persist="value"
+ oncommand="updateSearchLocalSystem();">
+ <menupopup>
+ <menuitem id="menuOnRemote"
+ value="remote"
+ label="&searchOnRemote.label;"/>
+ <menuitem id="menuOnLocal"
+ value="local"
+ label="&searchOnLocal.label;"/>
+ </menupopup>
+ </menulist>
+ <spacer flex="1"/>
+ <button label="&resetButton.label;" oncommand="onResetSearch(event);" accesskey="&resetButton.accesskey;"/>
+ </hbox>
+ </vbox>
+
+ <hbox flex="1">
+ <vbox id="searchTermListBox" flex="1"/>
+ </hbox>
+ </vbox>
+
+ <splitter id="gray_horizontal_splitter" persist="state">
+ <grippy/>
+ </splitter>
+
+ <vbox id="searchResults" flex="4" persist="height">
+ <vbox id="searchResultListBox" flex="1">
+ <tree id="threadTree"/>
+ </vbox>
+ <hbox align="center">
+
+ <button id="openButton"
+ label="&openButton.label;"
+ command="cmd_openMessage"
+ accesskey="&openButton.accesskey;"/>
+ <button id="fileMessageButton"
+ type="menu"
+ label="&moveButton.label;"
+ accesskey="&moveButton.accesskey;"
+ observes="file_message_button"
+ oncommand="MoveMessageInSearch(event.target);">
+ <menupopup type="folder"
+ showFileHereLabel="true"
+ mode="filing"
+ fileHereLabel="&moveHereMenu.label;"
+ fileHereAccessKey="&moveHereMenu.accesskey;"/>
+ </button>
+
+ <button id="deleteButton"
+ label="&deleteButton.label;"
+ accesskey="&deleteButton.accesskey;"
+ command="button_delete"/>
+ <button id="goToFolderButton"
+ label="&goToFolderButton.label;"
+ accesskey="&goToFolderButton.accesskey;"
+ command="goto_folder_button"/>
+ <button id="saveAsVFButton"
+ label="&saveAsVFButton.label;"
+ accesskey="&saveAsVFButton.accesskey;"
+ command="saveas_vf_button"/>
+ <spacer flex="1"/>
+ <button dlgtype="help" class="dialog-button"/>
+ </hbox>
+ </vbox>
+
+ <statusbar id="status-bar" class="chromeclass-status">
+ <statusbarpanel id="statusbar-progresspanel"
+ class="statusbarpanel-progress"
+ collapsed="true">
+ <progressmeter id="statusbar-icon"
+ class="progressmeter-statusbar"
+ mode="normal"
+ value="0"/>
+ </statusbarpanel>
+ <statusbarpanel id="statusText" crop="right" flex="1"/>
+ <statusbarpanel id="offline-status" class="statusbarpanel-iconic"/>
+ </statusbar>
+
+</dialog>
diff --git a/comm/suite/mailnews/content/browserRequest.js b/comm/suite/mailnews/content/browserRequest.js
new file mode 100644
index 0000000000..56ff0f8b9e
--- /dev/null
+++ b/comm/suite/mailnews/content/browserRequest.js
@@ -0,0 +1,107 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const wpl = Ci.nsIWebProgressListener;
+
+var reporterListener = {
+ _isBusy: false,
+ get securityButton() {
+ delete this.securityButton;
+ return this.securityButton = document.getElementById("security-button");
+ },
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsIWebProgressListener) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ onStateChange: function(/*in nsIWebProgress*/ aWebProgress,
+ /*in nsIRequest*/ aRequest,
+ /*in unsigned long*/ aStateFlags,
+ /*in nsresult*/ aStatus) {
+ },
+
+ onProgressChange: function(/*in nsIWebProgress*/ aWebProgress,
+ /*in nsIRequest*/ aRequest,
+ /*in long*/ aCurSelfProgress,
+ /*in long */aMaxSelfProgress,
+ /*in long */aCurTotalProgress,
+ /*in long */aMaxTotalProgress) {
+ },
+
+ onLocationChange: function(/*in nsIWebProgress*/ aWebProgress,
+ /*in nsIRequest*/ aRequest,
+ /*in nsIURI*/ aLocation) {
+ document.getElementById("headerMessage").textContent = aLocation.spec;
+ },
+
+ onStatusChange: function(/*in nsIWebProgress*/ aWebProgress,
+ /*in nsIRequest*/ aRequest,
+ /*in nsresult*/ aStatus,
+ /*in wstring*/ aMessage) {
+ },
+
+ onSecurityChange: function(/*in nsIWebProgress*/ aWebProgress,
+ /*in nsIRequest*/ aRequest,
+ /*in unsigned long*/ aState) {
+ const wpl_security_bits = wpl.STATE_IS_SECURE |
+ wpl.STATE_IS_BROKEN |
+ wpl.STATE_IS_INSECURE;
+ var browser = document.getElementById("requestFrame");
+ var level;
+
+ switch (aState & wpl_security_bits) {
+ case wpl.STATE_IS_SECURE:
+ level = "high";
+ break;
+ case wpl.STATE_IS_BROKEN:
+ level = "broken";
+ break;
+ }
+ if (level) {
+ this.securityButton.setAttribute("level", level);
+ this.securityButton.hidden = false;
+ } else {
+ this.securityButton.hidden = true;
+ this.securityButton.removeAttribute("level");
+ }
+ this.securityButton.setAttribute("tooltiptext",
+ browser.securityUI.tooltipText);
+ }
+}
+
+function cancelRequest()
+{
+ reportUserClosed();
+ window.close();
+}
+
+function reportUserClosed()
+{
+ let request = window.arguments[0].wrappedJSObject;
+ request.cancelled();
+}
+
+function loadRequestedUrl()
+{
+ let request = window.arguments[0].wrappedJSObject;
+ document.getElementById("headerMessage").textContent = request.promptText;
+ let account = request.account;
+ if (request.iconURI != "")
+ document.getElementById("headerImage").src = request.iconURI;
+
+ var browser = document.getElementById("requestFrame");
+ browser.addProgressListener(reporterListener,
+ Ci.nsIWebProgress.NOTIFY_ALL);
+ var url = request.url;
+ if (url != "") {
+ browser.setAttribute("src", url);
+ document.getElementById("headerMessage").textContent = url;
+ }
+ request.loaded(window, browser.webProgress);
+}
diff --git a/comm/suite/mailnews/content/browserRequest.xul b/comm/suite/mailnews/content/browserRequest.xul
new file mode 100644
index 0000000000..9911601f86
--- /dev/null
+++ b/comm/suite/mailnews/content/browserRequest.xul
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--# 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/.
+ -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/browserRequest.css" type="text/css"?>
+
+<!DOCTYPE window>
+<window id="browserRequest"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ buttons=","
+ onload="loadRequestedUrl()"
+ onclose="reportUserClosed()"
+ title=""
+ width="800"
+ height="500"
+ orient="vertical">
+
+ <script src="chrome://messenger/content/browserRequest.js"/>
+
+ <keyset id="mainKeyset">
+ <key id="key_close" key="w" modifiers="accel" oncommand="cancelRequest()"/>
+ <key id="key_close2" keycode="VK_ESCAPE" oncommand="cancelRequest()"/>
+ </keyset>
+ <hbox id="header">
+ <hbox id="addressbox" flex="1" disabled="true">
+ <image id="security-button"/>
+ <description id="headerMessage"/>
+ </hbox>
+ </hbox>
+ <browser type="content" src="about:blank" id="requestFrame" flex="1"/>
+</window>
diff --git a/comm/suite/mailnews/content/commandglue.js b/comm/suite/mailnews/content/commandglue.js
new file mode 100644
index 0000000000..a9a9332c64
--- /dev/null
+++ b/comm/suite/mailnews/content/commandglue.js
@@ -0,0 +1,989 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/*
+ * Command-specific code. This stuff should be called by the widgets
+ */
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+//NOTE: gMessengerBundle and gBrandBundle must be defined and set
+// for this Overlay to work properly
+
+var gFolderJustSwitched = false;
+var gBeforeFolderLoadTime;
+var gVirtualFolderTerms;
+var gXFVirtualFolderTerms;
+var gCurrentVirtualFolderUri;
+var gPrevFolderFlags;
+var gPrevSelectedFolder;
+var gMsgFolderSelected;
+
+function setTitleFromFolder(msgfolder, subject)
+{
+ var title = subject || "";
+
+ if (msgfolder)
+ {
+ if (title)
+ title += " - ";
+
+ title += msgfolder.prettyName;
+
+ if (!msgfolder.isServer)
+ {
+ var server = msgfolder.server;
+ var middle;
+ var end;
+ if (server.type == "nntp") {
+ // <folder> on <hostname>
+ middle = gMessengerBundle.getString("titleNewsPreHost");
+ end = server.hostName;
+ } else {
+ // <folder> for <accountname>
+ middle = gMessengerBundle.getString("titleMailPreHost");
+ end = server.prettyName;
+ }
+ if (middle) title += " " + middle;
+ if (end) title += " " + end;
+ }
+ }
+
+ if (AppConstants.platform != "macosx") {
+ title += " - " + gBrandBundle.getString("brandShortName");
+ }
+
+ document.title = title;
+
+ // Notify the current tab, it might want to update also.
+ var tabmail = GetTabMail();
+ if (tabmail)
+ {
+ tabmail.saveCurrentTabState(); // gDBView may have changed!
+ tabmail.setTabTitle();
+ }
+}
+
+function UpdateMailToolbar(caller)
+{
+ //dump("XXX update mail-toolbar " + caller + "\n");
+ document.commandDispatcher.updateCommands('mail-toolbar');
+
+ // hook for extra toolbar items
+ Services.obs.notifyObservers(window, "mail:updateToolbarItems");
+}
+
+/**
+ * @param folder - If viewFolder is a single folder saved
+ - search, this folder is the scope of the
+ - saved search, the real, underlying folder.
+ - Otherwise, it's the same as the viewFolder.
+ * @param viewFolder - nsIMsgFolder selected in the folder pane.
+ - Will be the same as folder, except if
+ - it's a single folder saved search.
+ * @param viewType - nsMsgViewType (see nsIMsgDBView.idl)
+ * @param viewFlags - nsMsgViewFlagsType (see nsIMsgDBView.idl)
+ * @param sortType - nsMsgViewSortType (see nsIMsgDBView.idl)
+ * @param sortOrder - nsMsgViewSortOrder (see nsIMsgDBView.idl)
+ **/
+function ChangeFolder(folder, viewFolder, viewType, viewFlags, sortType, sortOrder)
+{
+ if (folder.URI == gCurrentLoadingFolderURI)
+ return;
+
+ SetUpToolbarButtons(folder.URI);
+
+ // hook for extra toolbar items
+ Services.obs.notifyObservers(window, "mail:setupToolbarItems", folder.URI);
+
+ try {
+ setTitleFromFolder(viewFolder, null);
+ } catch (ex) {
+ dump("error setting title: " + ex + "\n");
+ }
+
+ //if it's a server, clear the threadpane and don't bother trying to load.
+ if (folder.isServer) {
+ msgWindow.openFolder = null;
+
+ ClearThreadPane();
+
+ // Load AccountCentral page here.
+ ShowAccountCentral(folder);
+
+ return;
+ }
+ else
+ {
+ if (folder.server.displayStartupPage)
+ {
+ gDisplayStartupPage = true;
+ folder.server.displayStartupPage = false;
+ }
+ }
+
+ // If the user clicks on folder, time to display thread pane and message pane.
+ ShowThreadPane();
+
+ gCurrentLoadingFolderURI = folder.URI;
+ gNextMessageAfterDelete = null; // forget what message to select, if any
+
+ gCurrentFolderToReroot = folder.URI;
+ gCurrentLoadingFolderViewFlags = viewFlags;
+ gCurrentLoadingFolderViewType = viewType;
+ gCurrentLoadingFolderSortType = sortType;
+ gCurrentLoadingFolderSortOrder = sortOrder;
+
+ var showMessagesAfterLoading;
+ try {
+ let server = folder.server;
+ if (Services.prefs.getBoolPref("mail.password_protect_local_cache"))
+ {
+ showMessagesAfterLoading = server.passwordPromptRequired;
+ // servers w/o passwords (like local mail) will always be non-authenticated.
+ // So we need to use the account manager for that case.
+ }
+ else
+ showMessagesAfterLoading = false;
+ }
+ catch (ex) {
+ showMessagesAfterLoading = false;
+ }
+
+ if (viewType != nsMsgViewType.eShowVirtualFolderResults &&
+ (folder.manyHeadersToDownload || showMessagesAfterLoading))
+ {
+ gRerootOnFolderLoad = true;
+ try
+ {
+ ClearThreadPane();
+ SetBusyCursor(window, true);
+ folder.startFolderLoading();
+ folder.updateFolder(msgWindow);
+ }
+ catch(ex)
+ {
+ SetBusyCursor(window, false);
+ dump("Error loading with many headers to download: " + ex + "\n");
+ }
+ }
+ else
+ {
+ if (viewType != nsMsgViewType.eShowVirtualFolderResults)
+ SetBusyCursor(window, true);
+ RerootFolder(folder.URI, folder, viewType, viewFlags, sortType, sortOrder);
+ gRerootOnFolderLoad = false;
+ folder.startFolderLoading();
+
+ //Need to do this after rerooting folder. Otherwise possibility of receiving folder loaded
+ //notification before folder has actually changed.
+ if (viewType != nsMsgViewType.eShowVirtualFolderResults)
+ folder.updateFolder(msgWindow);
+ }
+}
+
+function isNewsURI(uri)
+{
+ return ((/^news-message:/.test(uri)) || (/^news:/.test(uri)));
+}
+
+function RerootFolder(uri, newFolder, viewType, viewFlags, sortType, sortOrder)
+{
+ viewDebug("In reroot folder, sortType = " + sortType + "viewType = " + viewType + "\n");
+ if (sortType == 0)
+ {
+ try
+ {
+ var dbFolderInfo = newFolder.msgDatabase.dBFolderInfo;
+ sortType = dbFolderInfo.sortType;
+ sortOrder = dbFolderInfo.sortOrder;
+ viewFlags = dbFolderInfo.viewFlags;
+ viewType = dbFolderInfo.viewType;
+ dbFolderInfo = null;
+ }
+ catch(ex)
+ {
+ dump("invalid db in RerootFolder: " + ex + "\n");
+ }
+ }
+
+ // workaround for #39655
+ gFolderJustSwitched = true;
+
+ ClearThreadPaneSelection();
+
+ //Clear the new messages of the old folder
+ var oldFolder = gPrevSelectedFolder;
+ if (oldFolder) {
+ oldFolder.clearNewMessages();
+ oldFolder.hasNewMessages = false;
+ }
+
+ //Set the window's new open folder.
+ msgWindow.openFolder = newFolder;
+
+ //the new folder being selected should have its biff state get cleared.
+ if(newFolder)
+ {
+ newFolder.biffState =
+ Ci.nsIMsgFolder.nsMsgBiffState_NoMail;
+ }
+
+ //Clear out the thread pane so that we can sort it with the new sort id without taking any time.
+ // folder.setAttribute('ref', "");
+
+ // null this out, so we don't try sort.
+ if (gDBView) {
+ gDBView.close();
+ gDBView = null;
+ }
+
+ // cancel the pending mark as read timer
+ ClearPendingReadTimer();
+
+ // If this is the sent, drafts, templates, or send later folder,
+ // we show "Recipient" instead of "Author".
+ let outgoingFlags = Ci.nsMsgFolderFlags.SentMail |
+ Ci.nsMsgFolderFlags.Drafts |
+ Ci.nsMsgFolderFlags.Templates |
+ Ci.nsMsgFolderFlags.Queue;
+ SetSentFolderColumns(newFolder.isSpecialFolder(outgoingFlags, true));
+ ShowLocationColumn(viewType == nsMsgViewType.eShowVirtualFolderResults);
+ // Only show 'Received' column for e-mails. For newsgroup messages, the 'Date' header is as reliable as an e-mail's
+ // 'Received' header, as it is replaced with the news server's (more reliable) date.
+ UpdateReceivedColumn(newFolder);
+
+ // now create the db view, which will sort it.
+ CreateDBView(newFolder, viewType, viewFlags, sortType, sortOrder);
+ if (oldFolder)
+ {
+ /*disable quick search clear button if we were in the search view on folder switching*/
+ disableQuickSearchClearButton();
+
+ /*we don't null out the db reference for inbox because inbox is like the "main" folder
+ and performance outweighs footprint */
+ if (!oldFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Inbox, false))
+ if (oldFolder.URI != newFolder.URI)
+ oldFolder.msgDatabase = null;
+ }
+ // that should have initialized gDBView, now re-root the thread pane
+ RerootThreadPane();
+
+ UpdateStatusMessageCounts(gMsgFolderSelected);
+
+ UpdateMailToolbar("reroot folder in 3 pane");
+ // hook for extra toolbar items
+ Services.obs.notifyObservers(window, "mail:updateToolbarItems");
+ // this is to kick off cross-folder searches for virtual folders.
+ if (gSearchSession && !gVirtualFolderTerms) // another var might be better...
+ {
+ viewDebug("doing a xf folder search in rerootFolder\n");
+ gCurrentLoadingFolderURI = "";
+ ViewChangeByFolder(newFolder);
+ gPreQuickSearchView = null; // don't remember the cross folder search
+ ScrollToMessageAfterFolderLoad(newFolder);
+ }
+}
+
+function SwitchView(command)
+{
+ // when switching thread views, we might be coming out of quick search
+ // or a message view.
+ // first set view picker to all
+ ViewChangeByValue(kViewItemAll);
+
+ // clear the QS text, if we need to
+ ClearQSIfNecessary();
+
+ // now switch views
+ var oldSortType = gDBView ? gDBView.sortType : nsMsgViewSortType.byThread;
+ var oldSortOrder = gDBView ? gDBView.sortOrder : nsMsgViewSortOrder.ascending;
+ var viewFlags = gDBView ? gDBView.viewFlags : gCurViewFlags;
+
+ // close existing view.
+ if (gDBView) {
+ gDBView.close();
+ gDBView = null;
+ }
+
+ switch(command)
+ {
+ // "All" threads and "Unread" threads don't change threading state
+ case "cmd_viewAllMsgs":
+ viewFlags = viewFlags & ~nsMsgViewFlagsType.kUnreadOnly;
+ CreateDBView(msgWindow.openFolder, nsMsgViewType.eShowAllThreads, viewFlags,
+ oldSortType, oldSortOrder);
+ break;
+ case "cmd_viewUnreadMsgs":
+ viewFlags = viewFlags | nsMsgViewFlagsType.kUnreadOnly;
+ CreateDBView(msgWindow.openFolder, nsMsgViewType.eShowAllThreads, viewFlags,
+ oldSortType, oldSortOrder );
+ break;
+ // "Threads with Unread" and "Watched Threads with Unread" force threading
+ case "cmd_viewThreadsWithUnread":
+ CreateDBView(msgWindow.openFolder, nsMsgViewType.eShowThreadsWithUnread, nsMsgViewFlagsType.kThreadedDisplay,
+ oldSortType, oldSortOrder);
+ break;
+ case "cmd_viewWatchedThreadsWithUnread":
+ CreateDBView(msgWindow.openFolder, nsMsgViewType.eShowWatchedThreadsWithUnread, nsMsgViewFlagsType.kThreadedDisplay,
+ oldSortType, oldSortOrder);
+ break;
+ // "Ignored Threads" toggles 'ignored' inclusion --
+ // but it also resets 'With Unread' views to 'All'
+ case "cmd_viewIgnoredThreads":
+ if (viewFlags & nsMsgViewFlagsType.kShowIgnored)
+ viewFlags = viewFlags & ~nsMsgViewFlagsType.kShowIgnored;
+ else
+ viewFlags = viewFlags | nsMsgViewFlagsType.kShowIgnored;
+ CreateDBView(msgWindow.openFolder, nsMsgViewType.eShowAllThreads, viewFlags,
+ oldSortType, oldSortOrder);
+ break;
+ }
+
+ RerootThreadPane();
+
+ // this is to kick off cross-folder searches for virtual folders.
+ if (gSearchSession && !gVirtualFolderTerms) // another var might be better...
+ {
+ gDBView.searchSession = gSearchSession;
+ gSearchSession.search(msgWindow);
+ }
+}
+
+function SetSentFolderColumns(isSentFolder)
+{
+ var tree = GetThreadTree();
+ var searchBox = document.getElementById("searchInput");
+
+ var lastFolderSent = tree.getAttribute("lastfoldersent") == "true";
+ if (isSentFolder != lastFolderSent)
+ {
+ var senderColumn = document.getElementById("senderCol");
+ var recipientColumn = document.getElementById("recipientCol");
+
+ var saveHidden = senderColumn.getAttribute("hidden");
+ senderColumn.setAttribute("hidden", senderColumn.getAttribute("swappedhidden"));
+ senderColumn.setAttribute("swappedhidden", saveHidden);
+
+ saveHidden = recipientColumn.getAttribute("hidden");
+ recipientColumn.setAttribute("hidden", recipientColumn.getAttribute("swappedhidden"));
+ recipientColumn.setAttribute("swappedhidden", saveHidden);
+ }
+
+ tree.setAttribute("lastfoldersent", isSentFolder ? "true" : "false");
+}
+
+function ShowLocationColumn(show)
+{
+ var col = document.getElementById("locationCol");
+ if (col) {
+ if (show) {
+ col.removeAttribute("hidden");
+ col.removeAttribute("ignoreincolumnpicker");
+ }
+ else {
+ col.setAttribute("hidden","true");
+ col.setAttribute("ignoreincolumnpicker","true");
+ }
+ }
+}
+
+function UpdateReceivedColumn(newFolder)
+{
+ // Only show 'Received' column for e-mails. For newsgroup messages, the 'Date' header is as reliable as an e-mail's
+ // 'Received' header, as it is replaced with the news server's (more reliable) date.
+ var receivedColumn = document.getElementById("receivedCol");
+
+ var newFolderShowsRcvd = (newFolder.flags & Ci.nsMsgFolderFlags.Mail) &&
+ !(newFolder.flags & (Ci.nsMsgFolderFlags.Queue |
+ Ci.nsMsgFolderFlags.Templates |
+ Ci.nsMsgFolderFlags.Drafts |
+ Ci.nsMsgFolderFlags.SentMail));
+
+ var tempHidden = receivedColumn.getAttribute("temphidden") == "true";
+ var isHidden = receivedColumn.getAttribute("hidden") == "true";
+
+ if (!newFolderShowsRcvd && !isHidden)
+ {
+ // Record state & hide
+ receivedColumn.setAttribute("temphidden", "true");
+ receivedColumn.setAttribute("hidden", "true");
+ }
+ else if (newFolderShowsRcvd && tempHidden && isHidden)
+ {
+ receivedColumn.setAttribute("hidden", "false");
+ }
+
+ if (newFolderShowsRcvd)
+ {
+ receivedColumn.removeAttribute("ignoreincolumnpicker");
+ receivedColumn.removeAttribute("temphidden");
+ }
+ else
+ receivedColumn.setAttribute("ignoreincolumnpicker", "true");
+}
+
+
+function SetNewsFolderColumns()
+{
+ var sizeColumn = document.getElementById("sizeCol");
+
+ if (gDBView.usingLines) {
+ sizeColumn.setAttribute("tooltiptext",gMessengerBundle.getString("linesColumnTooltip2"));
+ sizeColumn.setAttribute("label",gMessengerBundle.getString("linesColumnHeader"));
+ }
+ else {
+ sizeColumn.setAttribute("tooltiptext", gMessengerBundle.getString("sizeColumnTooltip2"));
+ sizeColumn.setAttribute("label", gMessengerBundle.getString("sizeColumnHeader"));
+ }
+}
+
+function UpdateStatusMessageCounts(folder)
+{
+ var unreadElement = GetUnreadCountElement();
+ var totalElement = GetTotalCountElement();
+ if(folder && unreadElement && totalElement)
+ {
+ var numSelected = GetNumSelectedMessages();
+
+ var numUnread = (numSelected > 1) ?
+ gMessengerBundle.getFormattedString("selectedMsgStatus",
+ [numSelected]) :
+ gMessengerBundle.getFormattedString("unreadMsgStatus",
+ [ folder.getNumUnread(false)]);
+ var numTotal =
+ gMessengerBundle.getFormattedString("totalMsgStatus",
+ [folder.getTotalMessages(false)]);
+
+ unreadElement.setAttribute("label", numUnread);
+ totalElement.setAttribute("label", numTotal);
+ unreadElement.hidden = false;
+ totalElement.hidden = false;
+
+ }
+
+}
+
+function ConvertSortTypeToColumnID(sortKey)
+{
+ var columnID;
+
+ // Hack to turn this into an integer, if it was a string.
+ // It would be a string if it came from xulstore.json
+ sortKey = sortKey - 0;
+
+ switch (sortKey) {
+ // In the case of None, we default to the date column
+ // This appears to be the case in such instances as
+ // Global search, so don't complain about it.
+ case nsMsgViewSortType.byNone:
+ case nsMsgViewSortType.byDate:
+ columnID = "dateCol";
+ break;
+ case nsMsgViewSortType.byReceived:
+ columnID = "receivedCol";
+ break;
+ case nsMsgViewSortType.byAuthor:
+ columnID = "senderCol";
+ break;
+ case nsMsgViewSortType.byRecipient:
+ columnID = "recipientCol";
+ break;
+ case nsMsgViewSortType.bySubject:
+ columnID = "subjectCol";
+ break;
+ case nsMsgViewSortType.byLocation:
+ columnID = "locationCol";
+ break;
+ case nsMsgViewSortType.byAccount:
+ columnID = "accountCol";
+ break;
+ case nsMsgViewSortType.byUnread:
+ columnID = "unreadButtonColHeader";
+ break;
+ case nsMsgViewSortType.byStatus:
+ columnID = "statusCol";
+ break;
+ case nsMsgViewSortType.byTags:
+ columnID = "tagsCol";
+ break;
+ case nsMsgViewSortType.bySize:
+ columnID = "sizeCol";
+ break;
+ case nsMsgViewSortType.byPriority:
+ columnID = "priorityCol";
+ break;
+ case nsMsgViewSortType.byFlagged:
+ columnID = "flaggedCol";
+ break;
+ case nsMsgViewSortType.byThread:
+ columnID = "threadCol";
+ break;
+ case nsMsgViewSortType.byId:
+ columnID = "idCol";
+ break;
+ case nsMsgViewSortType.byJunkStatus:
+ columnID = "junkStatusCol";
+ break;
+ case nsMsgViewSortType.byAttachments:
+ columnID = "attachmentCol";
+ break;
+ case nsMsgViewSortType.byCustom:
+ columnID = gDBView.db.dBFolderInfo.getProperty("customSortCol");
+ if (!columnID) {
+ dump("ConvertSortTypeToColumnID: custom sort key but columnID not found\n");
+ columnID = "dateCol";
+ }
+ break;
+ default:
+ dump("unsupported sort key: " + sortKey + "\n");
+ columnID = null;
+ break;
+ }
+ return columnID;
+}
+
+var nsMsgViewSortType = Ci.nsMsgViewSortType;
+var nsMsgViewSortOrder = Ci.nsMsgViewSortOrder;
+var nsMsgViewFlagsType = Ci.nsMsgViewFlagsType;
+var nsMsgViewCommandType = Ci.nsMsgViewCommandType;
+var nsMsgViewType = Ci.nsMsgViewType;
+var nsMsgNavigationType = Ci.nsMsgNavigationType;
+
+var gDBView = null;
+var gCurViewFlags;
+var gCurSortType;
+
+// CreateDBView is called when we have a thread pane. CreateBareDBView is called when there is no
+// tree associated with the view. CreateDBView will call into CreateBareDBView...
+
+function CreateBareDBView(originalView, msgFolder, viewType, viewFlags, sortType, sortOrder)
+{
+ var dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=";
+ // hack to turn this into an integer, if it was a string
+ // it would be a string if it came from xulstore.json
+ viewType = viewType - 0;
+
+ switch (viewType) {
+ case nsMsgViewType.eShowQuickSearchResults:
+ dbviewContractId += "quicksearch";
+ break;
+ case nsMsgViewType.eShowSearch:
+ dbviewContractId += "search";
+ break;
+ case nsMsgViewType.eShowThreadsWithUnread:
+ dbviewContractId += "threadswithunread";
+ break;
+ case nsMsgViewType.eShowWatchedThreadsWithUnread:
+ dbviewContractId += "watchedthreadswithunread";
+ break;
+ case nsMsgViewType.eShowVirtualFolderResults:
+ dbviewContractId += "xfvf";
+ break;
+ case nsMsgViewType.eShowAllThreads:
+ default:
+ if (viewFlags & nsMsgViewFlagsType.kGroupBySort)
+ dbviewContractId += "group";
+ else
+ dbviewContractId += "threaded";
+ break;
+ }
+
+// dump ("contract id = " + dbviewContractId + "original view = " + originalView + "\n");
+ if (!originalView)
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+
+ gCurViewFlags = viewFlags;
+ var count = new Object;
+ if (!gThreadPaneCommandUpdater)
+ gThreadPaneCommandUpdater = new nsMsgDBViewCommandUpdater();
+
+ gCurSortType = sortType;
+
+ if (!originalView) {
+ gDBView.init(messenger, msgWindow, gThreadPaneCommandUpdater);
+ gDBView.open(msgFolder, gCurSortType, sortOrder, viewFlags, count);
+ if (viewType == nsMsgViewType.eShowVirtualFolderResults)
+ {
+ // the view is a listener on the search results
+ gViewSearchListener = gDBView.QueryInterface(Ci.nsIMsgSearchNotify);
+ gSearchSession.registerListener(gViewSearchListener);
+ }
+ }
+ else {
+ gDBView = originalView.cloneDBView(messenger, msgWindow, gThreadPaneCommandUpdater);
+ }
+}
+
+function CreateDBView(msgFolder, viewType, viewFlags, sortType, sortOrder)
+{
+ // call the inner create method
+ CreateBareDBView(null, msgFolder, viewType, viewFlags, sortType, sortOrder);
+
+ // now do tree specific work
+
+ // based on the collapsed state of the thread pane/message pane splitter,
+ // suppress message display if appropriate.
+ gDBView.suppressMsgDisplay = IsMessagePaneCollapsed();
+
+ UpdateSortIndicators(gCurSortType, sortOrder);
+ Services.obs.notifyObservers(msgFolder, "MsgCreateDBView", viewType + ":" + viewFlags);
+}
+
+function FolderPaneSelectionChange()
+{
+ let folders = GetSelectedMsgFolders();
+ if (folders.length) {
+ let locationItem = document.getElementById("locationFolders");
+ if (locationItem &&
+ locationItem.parentNode.parentNode.localName != "toolbarpalette") {
+ let msgFolder = folders[0];
+ locationItem.setAttribute("label", msgFolder.prettyName);
+ document.getElementById("folderLocationPopup")
+ ._setCssSelectors(msgFolder, locationItem);
+ }
+ }
+
+ let folderSelection = gFolderTreeView.selection;
+
+ // This prevents a folder from being loaded in the case that the user
+ // has right-clicked on a folder different from the one that was
+ // originally highlighted. On a right-click, the highlight (selection)
+ // of a row will be different from the value of currentIndex, thus if
+ // the currentIndex is not selected, it means the user right-clicked
+ // and we don't want to load the contents of the folder.
+ if (!folderSelection.isSelected(folderSelection.currentIndex))
+ return;
+
+ gVirtualFolderTerms = null;
+ gXFVirtualFolderTerms = null;
+
+ if (folders.length == 1)
+ {
+ let msgFolder = folders[0];
+ let uriToLoad = msgFolder.URI;
+
+ if (msgFolder == gMsgFolderSelected)
+ return;
+ // If msgFolder turns out to be a single folder saved search, not a virtual folder,
+ // realFolder will get set to the underlying folder the saved search is based on.
+ let realFolder = msgFolder;
+ gPrevSelectedFolder = gMsgFolderSelected;
+ gMsgFolderSelected = msgFolder;
+ var folderFlags = msgFolder.flags;
+ const kVirtual = Ci.nsMsgFolderFlags.Virtual;
+ // if this is same folder, and we're not showing a virtual folder
+ // then do nothing.
+ if (msgFolder == msgWindow.openFolder &&
+ !(folderFlags & kVirtual) && !(gPrevFolderFlags & kVirtual))
+ return;
+
+ OnLeavingFolder(gPrevSelectedFolder); // mark all read in last folder
+ var sortType = 0;
+ var sortOrder = 0;
+ var viewFlags = 0;
+ var viewType = 0;
+ gDefaultSearchViewTerms = null;
+ gVirtualFolderTerms = null;
+ gXFVirtualFolderTerms = null;
+ gPrevFolderFlags = folderFlags;
+ gCurrentVirtualFolderUri = null;
+ // don't get the db if this folder is a server
+ // we're going to be display account central
+ if (!(msgFolder.isServer))
+ {
+ try
+ {
+ var msgDatabase = msgFolder.msgDatabase;
+ if (msgDatabase)
+ {
+ gSearchSession = null;
+ var dbFolderInfo = msgDatabase.dBFolderInfo;
+ sortType = dbFolderInfo.sortType;
+ sortOrder = dbFolderInfo.sortOrder;
+ viewType = dbFolderInfo.viewType;
+ viewFlags = dbFolderInfo.viewFlags;
+ if (folderFlags & kVirtual)
+ {
+ viewType = nsMsgViewType.eShowQuickSearchResults;
+ var searchTermString = dbFolderInfo.getCharProperty("searchStr");
+ // trick the view code into updating the real folder...
+ gCurrentVirtualFolderUri = uriToLoad;
+ var srchFolderUri = dbFolderInfo.getCharProperty("searchFolderUri");
+ var srchFolderUriArray = srchFolderUri.split('|');
+ var searchOnline = dbFolderInfo.getBooleanProperty("searchOnline", false);
+ // cross folder search
+ var filterList = MailServices.filters.getTempFilterList(msgFolder);
+ var tempFilter = filterList.createFilter("temp");
+ filterList.parseCondition(tempFilter, searchTermString);
+ if (srchFolderUriArray.length > 1)
+ {
+ viewType = nsMsgViewType.eShowVirtualFolderResults;
+ gXFVirtualFolderTerms = CreateGroupedSearchTerms(tempFilter.searchTerms);
+ setupXFVirtualFolderSearch(srchFolderUriArray, gXFVirtualFolderTerms, searchOnline);
+ // need to set things up so that reroot folder issues the search
+ }
+ else
+ {
+ uriToLoad = srchFolderUri;
+ // we need to load the db for the actual folder so that many hdrs to download
+ // will return false...
+ realFolder = MailUtils.getFolderForURI(uriToLoad);
+ msgDatabase = realFolder.msgDatabase;
+// dump("search term string = " + searchTermString + "\n");
+
+ gVirtualFolderTerms = CreateGroupedSearchTerms(tempFilter.searchTerms);
+ }
+ }
+ msgDatabase = null;
+ dbFolderInfo = null;
+ }
+ }
+ catch (ex)
+ {
+ dump("failed to get view & sort values. ex = " + ex +"\n");
+ }
+ }
+ // clear cached view if we have no db or a pending quick search
+ if (!gDBView || gDBView.viewType == nsMsgViewType.eShowQuickSearchResults)
+ {
+ if (gPreQuickSearchView) //close cached view before quick search
+ {
+ gPreQuickSearchView.close();
+ gPreQuickSearchView = null;
+ }
+ var searchInput = document.getElementById("searchInput"); //reset the search input on folder switch
+ if (searchInput)
+ searchInput.value = "";
+ }
+ ClearMessagePane();
+
+ if (gXFVirtualFolderTerms)
+ viewType = nsMsgViewType.eShowVirtualFolderResults;
+ else if (gSearchEmailAddress || gVirtualFolderTerms)
+ viewType = nsMsgViewType.eShowQuickSearchResults;
+ else if (viewType == nsMsgViewType.eShowQuickSearchResults)
+ viewType = nsMsgViewType.eShowAllThreads; //override viewType - we don't want to start w/ quick search
+ ChangeFolder(realFolder, msgFolder, viewType, viewFlags, sortType, sortOrder);
+ if (gVirtualFolderTerms)
+ gDBView.viewFolder = msgFolder;
+
+ let tabmail = GetTabMail();
+ if (tabmail)
+ {
+ tabmail.saveCurrentTabState(); // gDBView may have changed!
+ tabmail.setTabTitle();
+ }
+ }
+ else
+ {
+ msgWindow.openFolder = null;
+ ClearThreadPane();
+ }
+
+ if (gAccountCentralLoaded)
+ UpdateMailToolbar("gAccountCentralLoaded");
+
+ if (gDisplayStartupPage)
+ {
+ loadStartPage();
+ gDisplayStartupPage = false;
+ UpdateMailToolbar("gDisplayStartupPage");
+ }
+}
+
+function ClearThreadPane()
+{
+ if (gDBView) {
+ gDBView.close();
+ gDBView = null;
+ }
+}
+
+var mailOfflineObserver = {
+ observe: function(subject, topic, state) {
+ // sanity checks
+ if (topic != "network:offline-status-changed") return;
+ MailOfflineStateChanged(state == "offline");
+ }
+}
+
+function AddMailOfflineObserver()
+{
+ Services.obs.addObserver(mailOfflineObserver, "network:offline-status-changed");
+}
+
+function RemoveMailOfflineObserver()
+{
+ Services.obs.removeObserver(mailOfflineObserver, "network:offline-status-changed");
+}
+
+function getSearchTermString(searchTerms)
+{
+ var searchIndex;
+ var condition = "";
+ var count = searchTerms.length;
+ for (searchIndex = 0; searchIndex < count; )
+ {
+ var term = searchTerms[searchIndex++];
+
+ if (condition.length > 1)
+ condition += ' ';
+
+ if (term.matchAll)
+ {
+ condition = "ALL";
+ break;
+ }
+ condition += (term.booleanAnd) ? "AND (" : "OR (";
+ condition += term.termAsString + ')';
+ }
+ return condition;
+}
+
+function CreateVirtualFolder(newName, parentFolder, searchFolderURIs, searchTerms, searchOnline)
+{
+ // ### need to make sure view/folder doesn't exist.
+ if (searchFolderURIs && (searchFolderURIs != "") && newName && (newName != ""))
+ {
+ var newFolder;
+ try
+ {
+ if (parentFolder instanceof(Ci.nsIMsgLocalMailFolder))
+ newFolder = parentFolder.createLocalSubfolder(newName);
+ else
+ newFolder = parentFolder.addSubfolder(newName);
+ newFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);
+ var vfdb = newFolder.msgDatabase;
+ var searchTermString = getSearchTermString(searchTerms);
+ var dbFolderInfo = vfdb.dBFolderInfo;
+ // set the view string as a property of the db folder info
+ // set the original folder name as well.
+ dbFolderInfo.setCharProperty("searchStr", searchTermString);
+ dbFolderInfo.setCharProperty("searchFolderUri", searchFolderURIs);
+ dbFolderInfo.setBooleanProperty("searchOnline", searchOnline);
+ vfdb.summaryValid = true;
+ vfdb.Close(true);
+ parentFolder.notifyFolderAdded(newFolder);
+ MailServices.accounts.saveVirtualFolders();
+ }
+ catch(e)
+ {
+ throw(e); // so that the dialog does not automatically close
+ dump ("Exception : creating virtual folder \n");
+ }
+ }
+ else
+ {
+ dump("no name or nothing selected\n");
+ }
+}
+
+var searchSessionContractID = "@mozilla.org/messenger/searchSession;1";
+var gSearchSession;
+
+var nsMsgSearchScope = Ci.nsMsgSearchScope;
+
+var gMessengerBundle = null;
+
+var gViewSearchListener;
+
+function GetScopeForFolder(folder)
+{
+ return folder.server.searchScope;
+}
+
+function setupXFVirtualFolderSearch(folderUrisToSearch, searchTerms, searchOnline)
+{
+ var count = new Object;
+ var i;
+
+ gSearchSession = Cc[searchSessionContractID]
+ .createInstance(Ci.nsIMsgSearchSession);
+
+ for (i in folderUrisToSearch)
+ {
+ let realFolder = MailUtils.getFolderForURI(folderUrisToSearch[i]);
+ if (!realFolder.isServer)
+ gSearchSession.addScopeTerm(!searchOnline ? nsMsgSearchScope.offlineMail : GetScopeForFolder(realFolder), realFolder);
+ }
+
+ for (let term of searchTerms) {
+ gSearchSession.appendTerm(term);
+ }
+}
+
+/**
+ * Uses an array of search terms to produce a new list usable from quick search.
+ *
+ * @param searchTermsArray A nsIArray of terms to copy.
+ *
+ * @return nsIMutableArray of search terms
+ */
+function CreateGroupedSearchTerms(searchTermsArray)
+{
+
+ var searchSession = gSearchSession ||
+ Cc[searchSessionContractID].createInstance(Ci.nsIMsgSearchSession);
+
+ // Create a temporary nsIMutableArray to store our search terms
+ // since we will be modifying the terms so they work with quick search.
+ var searchTermsArrayForQS = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+
+ var numEntries = searchTermsArray.length;
+ for (let i = 0; i < numEntries; i++) {
+ let searchTerm = searchTermsArray[i];
+
+ // clone the term, since we might be modifying it
+ var searchTermForQS = searchSession.createTerm();
+ searchTermForQS.value = searchTerm.value;
+ searchTermForQS.attrib = searchTerm.attrib;
+ searchTermForQS.arbitraryHeader = searchTerm.arbitraryHeader
+ searchTermForQS.hdrProperty = searchTerm.hdrProperty;
+ searchTermForQS.customId = searchTerm.customId
+ searchTermForQS.op = searchTerm.op;
+
+ // mark the first node as a group
+ if (i == 0)
+ searchTermForQS.beginsGrouping = true;
+ else if (i == numEntries - 1)
+ searchTermForQS.endsGrouping = true;
+
+ // turn the first term to true to work with quick search...
+ searchTermForQS.booleanAnd = i ? searchTerm.booleanAnd : true;
+
+ searchTermsArrayForQS.appendElement(searchTermForQS);
+ }
+ return searchTermsArrayForQS;
+}
+
+function OnLeavingFolder(aFolder)
+{
+ try
+ {
+ // Mark all messages of aFolder as read:
+ // We can't use the command controller, because it is already tuned in to the
+ // new folder, so we just mimic its behaviour wrt goDoCommand('cmd_markAllRead').
+ if (gDBView && Services.prefs.getBoolPref("mailnews.mark_message_read." + aFolder.server.type))
+ {
+ gDBView.doCommand(nsMsgViewCommandType.markAllRead);
+ }
+ }
+ catch(e){/* ignore */}
+}
+
+var gViewDebug = false;
+
+function viewDebug(str)
+{
+ if (gViewDebug)
+ dump(str);
+}
+
diff --git a/comm/suite/mailnews/content/folderDisplay.js b/comm/suite/mailnews/content/folderDisplay.js
new file mode 100644
index 0000000000..0318fb2e66
--- /dev/null
+++ b/comm/suite/mailnews/content/folderDisplay.js
@@ -0,0 +1,142 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var gFolderDisplay =
+{
+ get selectedCount()
+ {
+ return gDBView ? gDBView.numSelected : 0;
+ },
+
+ get selectedMessage()
+ {
+ if (!this.selectedIndices.length)
+ return null;
+ return gDBView.hdrForFirstSelectedMessage;
+ },
+
+ get selectedMessageUri()
+ {
+ if (!this.selectedIndices.length)
+ return null;
+ return gDBView.URIForFirstSelectedMessage;
+ },
+
+ get selectedMessageIsFeed()
+ {
+ return FeedMessageHandler.isFeedMessage(this.selectedMessage);
+ },
+
+ get selectedMessageIsImap()
+ {
+ var message = this.selectedMessage;
+ return message && message.folder &&
+ (message.folder.flags & Ci.nsMsgFolderFlags.ImapBox) != 0;
+ },
+
+ get selectedMessageIsNews()
+ {
+ var message = this.selectedMessage;
+ return message && message.folder &&
+ (message.folder.flags & Ci.nsMsgFolderFlags.Newsgroup) != 0;
+ },
+
+ get selectedMessageIsExternal()
+ {
+ var message = this.selectedMessage;
+ return message && !message.folder;
+ },
+
+ get selectedIndices()
+ {
+ return gDBView ? gDBView.getIndicesForSelection() : [];
+ },
+
+ get selectedMessages()
+ {
+ return gDBView ? gDBView.getSelectedMsgHdrs() : [];
+ },
+
+ get selectedMessageUris()
+ {
+ if (!gDBView)
+ return null;
+ var messageArray = gDBView.getURIsForSelection();
+ return messageArray.length ? messageArray : null;
+ },
+
+ get canArchiveSelectedMessages()
+ {
+ if (!gDBView)
+ return false;
+ var selectedMessages = this.selectedMessages;
+ if (selectedMessages.length == 0)
+ return false;
+ return selectedMessages.every(function(aMsg) {
+ let identity = GetIdentityForHeader(aMsg);
+ return identity && identity.archiveEnabled;
+ });
+ },
+
+ get displayedFolder()
+ {
+ return gMsgFolderSelected;
+ },
+
+ /**
+ * Determine which pane currently has focus (one of the folder pane, thread
+ * pane, or message pane). When changing focus to the message pane, be sure
+ * to focus the appropriate content window in addition to the messagepanebox
+ * (doing both is required in order to blur the previously-focused chrome
+ * element).
+ *
+ * @return the focused pane
+ */
+ get focusedPane() {
+ let panes = ["threadTree", "folderTree", "messagepanebox"].map(id =>
+ document.getElementById(id));
+
+ let currentNode = top.document.activeElement;
+
+ while (currentNode) {
+ if (panes.includes(currentNode)) {
+ return currentNode;
+ }
+
+ currentNode = currentNode.parentNode;
+ }
+ return null;
+ },
+
+}
+
+var gMessageDisplay =
+{
+ get displayedMessage()
+ {
+ if (!gDBView)
+ return null;
+ var viewIndex = gDBView.currentlyDisplayedMessage;
+ return viewIndex == nsMsgViewIndex_None ? null :
+ gDBView.getMsgHdrAt(viewIndex);
+ },
+
+ get isDummy()
+ {
+ return gDBView && gDBView.keyForFirstSelectedMessage == nsMsgKey_None;
+ },
+
+ get visible()
+ {
+ return !GetMessagePane().collapsed;
+ },
+
+ set visible(aVisible)
+ {
+ return aVisible; // Fake setter for the time being.
+ }
+}
+
+gFolderDisplay.messageDisplay = gMessageDisplay;
diff --git a/comm/suite/mailnews/content/folderPane.js b/comm/suite/mailnews/content/folderPane.js
new file mode 100644
index 0000000000..35cdc8849a
--- /dev/null
+++ b/comm/suite/mailnews/content/folderPane.js
@@ -0,0 +1,2221 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// Implements a tree of folders. It shows icons depending on folder type
+// and other fancy styling.
+// This is used in the main folder pane, but also some dialogs that need
+// to show a nice list of folders.
+
+var { FeedUtils } =
+ ChromeUtils.import("resource:///modules/FeedUtils.jsm");
+var { FolderUtils } =
+ ChromeUtils.import("resource:///modules/FolderUtils.jsm");
+var { IOUtils } =
+ ChromeUtils.import("resource:///modules/IOUtils.js");
+var { IteratorUtils } =
+ ChromeUtils.import("resource:///modules/iteratorUtils.jsm");
+var { mailServices } =
+ ChromeUtils.import("resource:///modules/mailServices.js");
+var { MailUtils } =
+ ChromeUtils.import("resource:///modules/MailUtils.js");
+var { AppConstants } =
+ ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+var { Services } =
+ ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+if (typeof FeedMessageHandler != "object") {
+ Services.scriptloader.loadSubScript("chrome://messenger-newsblog/content/newsblogOverlay.js");
+}
+
+const kDefaultMode = "all";
+
+/**
+ * This file contains the controls and functions for the folder pane.
+ * The following definitions will be useful to know:
+ *
+ * gFolderTreeView - the controller for the folder tree.
+ * ftvItem - folder tree view item, representing a row in the tree
+ * mode - folder view type, e.g., all folders, favorite folders, MRU...
+ */
+
+ /**
+ * An interface that needs to be implemented in order to add a new view to the
+ * folder tree. For default behavior, it is recommended that implementers
+ * subclass this interface instead of relying on duck typing.
+ *
+ * For implementation examples, see |gFolderTreeView._modes|. For how to
+ * register this mode with |gFolderTreeView|, see
+ * |gFolderTreeView.registerFolderTreeMode|.
+ */
+let IFolderTreeMode = {
+ /**
+ * Generates the folder map for this mode.
+ *
+ * @param aFolderTreeView The gFolderTreeView for which this mode is being
+ * activated.
+ *
+ * @returns An array containing ftvItem instances representing the top-level
+ * folders in this view.
+ */
+ generateMap: function IFolderTreeMode_generateMap(aFolderTreeView) {
+ return null;
+ },
+
+ /**
+ * Given an nsIMsgFolder, returns its parent in the map. The default behaviour
+ * is to return the folder's actual parent (aFolder.parent). Folder tree modes
+ * may decide to override it.
+ *
+ * If the parent isn't easily computable given just the folder, you may
+ * consider generating the entire ftvItem tree at once and using a map from
+ * folders to ftvItems.
+ *
+ * @returns an nsIMsgFolder representing the parent of the folder in the view,
+ * or null if the folder is a top-level folder in the map. It is expected
+ * that the returned parent will have the given folder as one of its
+ * children.
+ * @note This function need not guarantee that either the folder or its parent
+ * is actually in the view.
+ */
+ getParentOfFolder: function IFolderTreeMode_getParentOfFolder(aFolder) {
+ return aFolder.parent;
+ },
+
+ /**
+ * Given an nsIMsgDBHdr, returns the folder it is considered to be contained
+ * in, in this mode. This is usually just the physical folder it is contained
+ * in (aMsgHdr.folder), but some modes may decide to override this. For
+ * example, combined views like Smart Folders return the smart inbox for any
+ * messages in any inbox.
+ *
+ * The folder returned doesn't need to be in the view.
+ *
+ * @returns The folder the message header is considered to be contained in, in
+ * this mode. The returned folder may or may not actually be in the view
+ * -- however, given a valid nsIMsgDBHdr, it is expected that a) a
+ * non-null folder is returned, and that b) the folder that is returned
+ * actually does contain the message header.
+ */
+ getFolderForMsgHdr: function IFolderTreeMode_getFolderForMsgHdr(aMsgHdr) {
+ return aMsgHdr.folder;
+ },
+
+ /**
+ * Notified when a folder is added. The default behavior is to add it as a
+ * child of the parent item, but some views may decide to override this. For
+ * example, combined views like Smart Folders add any new inbox as a child of
+ * the smart inbox.
+ *
+ * @param aParent The parent of the folder that was added.
+ * @param aFolder The folder that was added.
+ */
+ onFolderAdded: function IFolderTreeMode_onFolderAdded(aParent, aFolder) {
+ gFolderTreeView.addFolder(aParent, aFolder);
+ },
+
+ /**
+ * Notified when a folder int property is changed.
+ *
+ * Returns true if the event was processed inside the function and no further
+ * default handling should be done in the caller. Otherwise false.
+ *
+ * @param aItem The folder with a change.
+ * @param aProperty The changed property string.
+ * @param aOld The old value of the property.
+ * @param aNew The new value of the property.
+ */
+ handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
+ return false;
+ }
+};
+
+/**
+ * This is our controller for the folder-tree. It includes our nsITreeView
+ * implementation, as well as other control functions.
+ */
+let gFolderTreeView = {
+ messengerBundle: null,
+
+ /**
+ * Called when the window is initially loaded. This function initializes the
+ * folder-pane to the view last shown before the application was closed.
+ */
+ load: function ftv_load(aTree, aJSONFile) {
+ this._treeElement = aTree;
+ this.messengerBundle = document.getElementById("bundle_messenger");
+
+ // The folder pane can be used for other trees which may not have these
+ // elements.
+ if (document.getElementById("folderpane-splitter"))
+ document.getElementById("folderpane-splitter").collapsed = false;
+ if (document.getElementById("folderPaneBox"))
+ document.getElementById("folderPaneBox").collapsed = false;
+
+ if (aJSONFile) {
+ // Parse our persistent-open-state json file.
+ let data = IOUtils.loadFileToString(aJSONFile);
+ if (data) {
+ try {
+ this._persistOpenMap = JSON.parse(data);
+ } catch (x) {
+ Cu.reportError(gFolderTreeView.messengerBundle.getFormattedString("failedToReadFile", [aJSONFile, x]));
+ }
+ }
+ }
+
+ // Load our data.
+ this._rebuild();
+ // And actually draw the tree.
+ aTree.view = this;
+
+ gFolderStatsHelpers.init();
+
+ // Add this listener so that we can update the tree when things change.
+ MailServices.mailSession.AddFolderListener(this, Ci.nsIFolderListener.all);
+ },
+
+ /**
+ * Called when the window is being torn down. Here we undo everything we did
+ * onload. That means removing our listener and serializing our JSON.
+ */
+ unload: function ftv_unload(aJSONFile) {
+ // Remove our listener.
+ MailServices.mailSession.RemoveFolderListener(this);
+
+ if (aJSONFile) {
+ // Write out our json file...
+ let data = JSON.stringify(this._persistOpenMap);
+ IOUtils.saveStringToFile(aJSONFile, data);
+ }
+ },
+
+ /**
+ * Extensions can use this function to add a new mode to the folder pane.
+ *
+ * @param aCommonName an internal name to identify this mode. Must be unique
+ * @param aMode An implementation of |IFolderTreeMode| for this mode.
+ * @param aDisplayName a localized name for this mode
+ */
+ registerFolderTreeMode: function ftv_registerFolderTreeMode(aCommonName,
+ aMode,
+ aDisplayName) {
+ this._modeNames.push(aCommonName);
+ this._modes[aCommonName] = aMode;
+ this._modeDisplayNames[aCommonName] = aDisplayName;
+ },
+
+ /**
+ * Unregisters a previously registered mode. Since common-names must be unique
+ * this is all that need be provided to unregister.
+ * @param aCommonName the common-name with which the mode was previously
+ * registered
+ */
+ unregisterFolderTreeMode: function ftv_unregisterFolderTreeMode(aCommonName) {
+ this._modeNames.splice(this._modeNames.indexOf(aCommonName), 1);
+ delete this._modes[aCommonName];
+ delete this._modeDisplayNames[aCommonName];
+ if (this._mode == aCommonName)
+ this.mode = kDefaultMode;
+ },
+
+ /**
+ * Retrieves a specific mode object
+ * @param aCommonName the common-name with which the mode was previously
+ * registered
+ */
+ getFolderTreeMode: function ftv_getFolderTreeMode(aCommonName) {
+ return this._modes[aCommonName];
+ },
+
+ /**
+ * Called to move to the next/prev folder-mode in the list
+ *
+ * @param aForward whether or not we should move forward in the list
+ */
+ cycleMode: function ftv_cycleMode(aForward) {
+ let index = this._modeNames.indexOf(this.mode);
+ let offset = aForward ? 1 : this._modeNames.length - 1;
+ index = (index + offset) % this._modeNames.length;
+
+ this.mode = this._modeNames[index];
+ },
+
+ /**
+ * If the hidden pref is set, then double-clicking on a folder should open it
+ *
+ * @param event the double-click event
+ */
+ onDoubleClick: function ftv_onDoubleClick(aEvent) {
+ if (aEvent.button != 0 || aEvent.originalTarget.localName == "twisty" ||
+ aEvent.originalTarget.localName == "slider" ||
+ aEvent.originalTarget.localName == "scrollbarbutton")
+ return;
+
+ let row = gFolderTreeView._treeElement.treeBoxObject.getRowAt(aEvent.clientX,
+ aEvent.clientY);
+ let folderItem = gFolderTreeView._rowMap[row];
+ if (folderItem)
+ folderItem.command();
+
+ // Don't let the double-click toggle the open state of the folder here.
+ aEvent.stopPropagation();
+ },
+
+ onKeyPress(event) {
+ if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
+ if ((AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) &&
+ AllowOpenTabOnMiddleClick()) {
+ FolderPaneContextMenuNewTab(event);
+ let folderTree = document.getElementById("folderTree");
+ RestoreSelectionWithoutContentLoad(folderTree);
+ }
+ }
+ },
+
+ getFolderAtCoords: function ftv_getFolderAtCoords(aX, aY) {
+ let row = gFolderTreeView._treeElement.treeBoxObject.getRowAt(aX, aY);
+ if (row in gFolderTreeView._rowMap)
+ return gFolderTreeView._rowMap[row]._folder;
+ return null;
+ },
+
+ /**
+ * A string representation for the current display-mode. Each value here must
+ * correspond to an entry in _modes
+ */
+ _mode: null,
+ get mode() {
+ if (!this._mode) {
+ this._mode = this._treeElement.getAttribute("mode");
+ // This can happen when an extension is removed.
+ if (!(this._mode in this._modes))
+ this._mode = kDefaultMode;
+ }
+ return this._mode;
+ },
+
+ /**
+ * @param aMode The final name of the mode to switch to.
+ */
+ set mode(aMode) {
+ // Ignore unknown modes.
+ if (!(aMode in this._modes))
+ return;
+
+ this._mode = aMode;
+
+ // Store current mode and actually build the folder pane.
+ this._treeElement.setAttribute("mode", this._mode);
+ this._rebuild();
+ },
+
+ /**
+ * Selects a given nsIMsgFolder in the tree. This function will also ensure
+ * that the folder is actually being displayed (that is, that none of its
+ * ancestors are collapsed.
+ *
+ * @param aFolder the nsIMsgFolder to select
+ * @param [aForceSelect] Whether we should switch to the default mode to
+ * select the folder in case we didn't find the folder in the current
+ * view. Defaults to false.
+ * @returns true if the folder selection was successful, false if it failed
+ * (probably because the folder isn't in the view at all)
+ */
+ selectFolder: function ftv_selectFolder(aFolder, aForceSelect = false) {
+ // "this" inside the nested function refers to the function...
+ // Also note that openIfNot is recursive.
+ let tree = this;
+ let folderTreeMode = this._modes[this._mode];
+ function openIfNot(aFolderToOpen) {
+ let index = tree.getIndexOfFolder(aFolderToOpen);
+ if (index != null) {
+ if (!tree._rowMap[index].open)
+ tree._toggleRow(index, false);
+ return true;
+ }
+
+ // Not found, so open the parent.
+ let parent = folderTreeMode.getParentOfFolder(aFolderToOpen);
+ if (parent && openIfNot(parent)) {
+ // Now our parent is open, so we can open ourselves.
+ index = tree.getIndexOfFolder(aFolderToOpen);
+ if (index != null) {
+ tree._toggleRow(index, false);
+ return true;
+ }
+ }
+
+ // No way we can find the folder now.
+ return false;
+ }
+ let parent = folderTreeMode.getParentOfFolder(aFolder);
+ if (parent)
+ openIfNot(parent);
+
+ let folderIndex = tree.getIndexOfFolder(aFolder);
+ if (folderIndex == null) {
+ if (aForceSelect) {
+ // Switch to the default mode. The assumption here is that the default
+ // mode can display every folder.
+ this.mode = kDefaultMode;
+ // We don't want to get stuck in an infinite recursion,
+ // so pass in false.
+ return this.selectFolder(aFolder, false);
+ }
+
+ return false;
+ }
+
+ this.selection.select(folderIndex);
+ this._treeElement.treeBoxObject.ensureRowIsVisible(folderIndex);
+ return true;
+ },
+
+ /**
+ * Returns the index of a folder in the current display.
+ *
+ * @param aFolder the folder whose index should be returned.
+ * @returns The index of the folder in the view (a number).
+ * @note If the folder is not in the display (perhaps because one of its
+ * anscetors is collapsed), this function returns null.
+ */
+ getIndexOfFolder: function ftv_getIndexOfFolder(aFolder) {
+ for (let [iRow, row] of this._rowMap.entries()) {
+ if (row.id == aFolder.URI)
+ return iRow;
+ }
+ return null;
+ },
+
+ /**
+ * Returns the folder for an index in the current display.
+ *
+ * @param aIndex the index for which the folder should be returned.
+ * @note If the index is out of bounds, this function returns null.
+ */
+ getFolderForIndex: function ftv_getFolderForIndex(aIndex) {
+ if (aIndex < 0 || aIndex >= this._rowMap.length)
+ return null;
+ return this._rowMap[aIndex]._folder;
+ },
+
+ /**
+ * Returns the parent of a folder in the current view. This may be, but is not
+ * necessarily, the actual parent of the folder (aFolder.parent). In
+ * particular, in the smart view, special folders are usually children of the
+ * smart folder of that kind.
+ *
+ * @param aFolder The folder to get the parent of.
+ * @returns The parent of the folder, or null if the parent wasn't found.
+ * @note This function does not guarantee that either the folder or its parent
+ * is actually in the view.
+ */
+ getParentOfFolder: function ftv_getParentOfFolder(aFolder) {
+ return this._modes[this._mode].getParentOfFolder(aFolder);
+ },
+
+ /**
+ * Given an nsIMsgDBHdr, returns the folder it is considered to be contained
+ * in, in the current mode. This is usually, but not necessarily, the actual
+ * folder the message is in (aMsgHdr.folder). For more details, see
+ * |IFolderTreeMode.getFolderForMsgHdr|.
+ */
+ getFolderForMsgHdr: function ftv_getFolderForMsgHdr(aMsgHdr) {
+ return this._modes[this._mode].getFolderForMsgHdr(aMsgHdr);
+ },
+
+ /**
+ * Returns the |ftvItem| for an index in the current display. Intended for use
+ * by folder tree mode implementers.
+ *
+ * @param aIndex The index for which the ftvItem should be returned.
+ * @note If the index is out of bounds, this function returns null.
+ */
+ getFTVItemForIndex: function ftv_getFTVItemForIndex(aIndex) {
+ return this._rowMap[aIndex];
+ },
+
+ /**
+ * Returns an array of nsIMsgFolders corresponding to the current selection
+ * in the tree
+ */
+ getSelectedFolders: function ftv_getSelectedFolders() {
+ let selection = this.selection;
+ if (!selection)
+ return [];
+
+ let folderArray = [];
+ let rangeCount = selection.getRangeCount();
+ for (let i = 0; i < rangeCount; i++) {
+ let startIndex = {};
+ let endIndex = {};
+ selection.getRangeAt(i, startIndex, endIndex);
+ for (let j = startIndex.value; j <= endIndex.value; j++) {
+ if (j < this._rowMap.length)
+ folderArray.push(this._rowMap[j]._folder);
+ }
+ }
+ return folderArray;
+ },
+
+ /**
+ * Adds a new child |ftvItem| to the given parent |ftvItem|. Intended for use
+ * by folder tree mode implementers.
+ *
+ * @param aParentItem The parent ftvItem. It is assumed that this is visible
+ * in the view.
+ * @param aParentIndex The index of the parent ftvItem in the view.
+ * @param aItem The item to add.
+ */
+ addChildItem: function ftv_addChildItem(aParentItem, aParentIndex, aItem) {
+ this._addChildToView(aParentItem, aParentIndex, aItem);
+ },
+
+ // ****************** Start of nsITreeView implementation **************** //
+
+ get rowCount() {
+ return this._rowMap.length;
+ },
+
+ /**
+ * drag drop interfaces
+ */
+ canDrop: function ftv_canDrop(aRow, aOrientation) {
+ let targetFolder = gFolderTreeView._rowMap[aRow]._folder;
+ if (!targetFolder)
+ return false;
+ let dt = this._currentTransfer;
+ let types = Array.from(dt.mozTypesAt(0));
+ if (types.includes("text/x-moz-message")) {
+ if (aOrientation != Ci.nsITreeView.DROP_ON)
+ return false;
+ // Don't allow drop onto server itself.
+ if (targetFolder.isServer)
+ return false;
+ // Don't allow drop into a folder that cannot take messages.
+ if (!targetFolder.canFileMessages)
+ return false;
+ let messenger = Cc["@mozilla.org/messenger;1"]
+ .createInstance(Ci.nsIMessenger);
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ let msgHdr = messenger.msgHdrFromURI(dt.mozGetDataAt("text/x-moz-message", i));
+ // Don't allow drop onto original folder.
+ if (msgHdr.folder == targetFolder)
+ return false;
+ }
+ return true;
+ }
+ else if (types.includes("text/x-moz-folder")) {
+ if (aOrientation != Ci.nsITreeView.DROP_ON)
+ return false;
+ // If cannot create subfolders then don't allow drop here.
+ if (!targetFolder.canCreateSubfolders)
+ return false;
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ let folder = dt.mozGetDataAt("text/x-moz-folder", i)
+ .QueryInterface(Ci.nsIMsgFolder);
+ // Don't allow to drop on itself.
+ if (targetFolder == folder)
+ return false;
+ // Don't copy within same server.
+ if ((folder.server == targetFolder.server) &&
+ (dt.dropEffect == 'copy'))
+ return false;
+ // Don't allow immediate child to be dropped onto its parent.
+ if (targetFolder == folder.parent)
+ return false;
+ // Don't allow dragging of virtual folders across accounts.
+ if ((folder.flags & Ci.nsMsgFolderFlags.Virtual) &&
+ folder.server != targetFolder.server)
+ return false;
+ // Don't allow parent to be dropped on its ancestors.
+ if (folder.isAncestorOf(targetFolder))
+ return false;
+ // If there is a folder that can't be renamed, don't allow it to be
+ // dropped if it is not to "Local Folders" or is to the same account.
+ if (!folder.canRename && (targetFolder.server.type != "none" ||
+ folder.server == targetFolder.server))
+ return false;
+ }
+ return true;
+ }
+ else if (types.includes("text/x-moz-newsfolder")) {
+ // Don't allow dragging onto element.
+ if (aOrientation == Ci.nsITreeView.DROP_ON)
+ return false;
+ // Don't allow drop onto server itself.
+ if (targetFolder.isServer)
+ return false;
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ let folder = dt.mozGetDataAt("text/x-moz-newsfolder", i)
+ .QueryInterface(Ci.nsIMsgFolder);
+ // Don't allow dragging newsgroup to other account.
+ if (targetFolder.rootFolder != folder.rootFolder)
+ return false;
+ // Don't allow dragging newsgroup to before/after itself.
+ if (targetFolder == folder)
+ return false;
+ // Don't allow dragging newsgroup to before item after or
+ // after item before.
+ let row = aRow + aOrientation;
+ if (row in gFolderTreeView._rowMap &&
+ (gFolderTreeView._rowMap[row]._folder == folder))
+ return false;
+ }
+ return true;
+ }
+ // Allow subscribing to feeds by dragging an url to a feed account.
+ else if (targetFolder.server.type == "rss" && dt.mozItemCount == 1)
+ return FeedUtils.getFeedUriFromDataTransfer(dt) ? true : false;
+ else if (types.includes("application/x-moz-file")) {
+ if (aOrientation != Ci.nsITreeView.DROP_ON)
+ return false;
+ // Don't allow drop onto server itself.
+ if (targetFolder.isServer)
+ return false;
+ // Don't allow drop into a folder that cannot take messages.
+ if (!targetFolder.canFileMessages)
+ return false;
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ let extFile = dt.mozGetDataAt("application/x-moz-file", i);
+ if (!extFile) {
+ continue;
+ }
+ return extFile.QueryInterface(Ci.nsIFile).isFile();
+ }
+ }
+ return false;
+ },
+ drop: function ftv_drop(aRow, aOrientation) {
+ let targetFolder = gFolderTreeView._rowMap[aRow]._folder;
+
+ let dt = this._currentTransfer;
+ let count = dt.mozItemCount;
+ let cs = MailServices.copy;
+
+ // This is a potential rss feed. A link image as well as link text url
+ // should be handled; try to extract a url from non moz apps as well.
+ let feedUri = targetFolder.server.type == "rss" && count == 1 ?
+ FeedUtils.getFeedUriFromDataTransfer(dt) : null;
+
+ // We only support drag of a single flavor at a time.
+ let types = Array.from(dt.mozTypesAt(0));
+ if (types.includes("text/x-moz-folder")) {
+ for (let i = 0; i < count; i++) {
+ let folder = dt.mozGetDataAt("text/x-moz-folder", i)
+ .QueryInterface(Ci.nsIMsgFolder);
+ cs.copyFolders(folder, targetFolder,
+ (folder.server == targetFolder.server), null,
+ msgWindow);
+ }
+ }
+ else if (types.includes("text/x-moz-newsfolder")) {
+ // Start by getting folders into order.
+ let folders = new Array;
+ for (let i = 0; i < count; i++) {
+ let folder = dt.mozGetDataAt("text/x-moz-newsfolder", i)
+ .QueryInterface(Ci.nsIMsgFolder);
+ folders[this.getIndexOfFolder(folder)] = folder;
+ }
+ let newsFolder = targetFolder.rootFolder
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ // When moving down, want to insert first one last.
+ // When moving up, want to insert first one first.
+ let i = (aOrientation == 1) ? folders.length - 1 : 0;
+ while (i >= 0 && i < folders.length) {
+ let folder = folders[i];
+ if (folder) {
+ newsFolder.moveFolder(folder, targetFolder, aOrientation);
+ this.selection.toggleSelect(this.getIndexOfFolder(folder));
+ }
+ i -= aOrientation;
+ }
+ }
+ else if (types.includes("text/x-moz-message")) {
+ let array = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ let sourceFolder;
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+ for (let i = 0; i < count; i++) {
+ let msgHdr = messenger.msgHdrFromURI(dt.mozGetDataAt("text/x-moz-message", i));
+ if (!i)
+ sourceFolder = msgHdr.folder;
+ array.appendElement(msgHdr);
+ }
+ let isMove = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService).getCurrentSession()
+ .dragAction == Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
+ let isNews = sourceFolder.flags & Ci.nsMsgFolderFlags.Newsgroup;
+ if (!sourceFolder.canDeleteMessages || isNews)
+ isMove = false;
+
+ Services.prefs.setCharPref("mail.last_msg_movecopy_target_uri",
+ targetFolder.URI);
+ Services.prefs.setBoolPref("mail.last_msg_movecopy_was_move", isMove);
+ // ### ugh, so this won't work with cross-folder views. We would
+ // really need to partition the messages by folder.
+ cs.copyMessages(sourceFolder, array, targetFolder, isMove, null,
+ msgWindow, true);
+ }
+ else if (feedUri) {
+ Cc["@mozilla.org/newsblog-feed-downloader;1"]
+ .getService(Ci.nsINewsBlogFeedDownloader)
+ .subscribeToFeed(feedUri.spec, targetFolder, msgWindow);
+ }
+ else if (types.includes("application/x-moz-file")) {
+ for (let i = 0; i < count; i++) {
+ let extFile = dt.mozGetDataAt("application/x-moz-file", i)
+ .QueryInterface(Ci.nsIFile);
+ if (extFile.isFile()) {
+ let len = extFile.leafName.length;
+ if (len > 4 && extFile.leafName.toLowerCase().endsWith(".eml"))
+ cs.copyFileMessage(extFile, targetFolder, null, false, 1, "", null,
+ msgWindow);
+ }
+ }
+ }
+ },
+
+ _onDragStart: function ftv_dragStart(aEvent) {
+ // Ugh, this is ugly but necessary.
+ let view = gFolderTreeView;
+
+ if (aEvent.originalTarget.localName != "treechildren")
+ return;
+
+ let folders = view.getSelectedFolders();
+ folders = folders.filter(function(f) { return !f.isServer; });
+ for (let i in folders) {
+ let flavor = folders[i].server.type == "nntp" ? "text/x-moz-newsfolder" :
+ "text/x-moz-folder";
+ aEvent.dataTransfer.mozSetDataAt(flavor, folders[i], i);
+ }
+ aEvent.dataTransfer.effectAllowed = "copyMove";
+ aEvent.dataTransfer.addElement(aEvent.originalTarget);
+ return;
+ },
+
+ _onDragOver: function ftv_onDragOver(aEvent) {
+ this._currentTransfer = aEvent.dataTransfer;
+ },
+
+ _onDragDrop: function ftv_onDragDrop(aEvent) {
+ this._currentTransfer = aEvent.dataTransfer;
+ },
+
+ /**
+ * CSS files will cue off of these. Note that we reach into the rowMap's
+ * items so that custom data-displays can define their own properties
+ */
+ getCellProperties: function ftv_getCellProperties(aRow, aCol) {
+ return this._rowMap[aRow].getProperties(aCol);
+ },
+
+ /**
+ * The actual text to display in the tree
+ */
+ getCellText: function ftv_getCellText(aRow, aCol) {
+ if ((aCol.id == "folderNameCol") ||
+ (aCol.id == "folderUnreadCol") ||
+ (aCol.id == "folderTotalCol") ||
+ (aCol.id == "folderSizeCol"))
+ return this._rowMap[aRow].getText(aCol.id);
+ return "";
+ },
+
+ /**
+ * The ftvItems take care of assigning this when created.
+ */
+ getLevel: function ftv_getLevel(aIndex) {
+ return this._rowMap[aIndex].level;
+ },
+
+ /**
+ * The ftvItems take care of assigning this when building children lists
+ */
+ getServerNameAdded: function ftv_getServerNameAdded(aIndex) {
+ return this._rowMap[aIndex].addServerName;
+ },
+
+ /**
+ * This is easy since the ftv items assigned the _parent property when making
+ * the child lists
+ */
+ getParentIndex: function ftv_getParentIndex(aIndex) {
+ return this._rowMap.indexOf(this._rowMap[aIndex]._parent);
+ },
+
+ /**
+ * This is duplicative for our normal ftv views, but custom data-displays may
+ * want to do something special here
+ */
+ getRowProperties: function ftv_getRowProperties(aRow) {
+ return this._rowMap[aRow].getProperties();
+ },
+
+ /**
+ * Check whether there are any more rows with our level before the next row
+ * at our parent's level
+ */
+ hasNextSibling: function ftv_hasNextSibling(aIndex, aNextIndex) {
+ var currentLevel = this._rowMap[aIndex].level;
+ for (var i = aNextIndex + 1; i < this._rowMap.length; i++) {
+ if (this._rowMap[i].level == currentLevel)
+ return true;
+ if (this._rowMap[i].level < currentLevel)
+ return false;
+ }
+ return false;
+ },
+
+ /**
+ * All folders are containers, so we can drag drop messages to them.
+ */
+ isContainer: function ftv_isContainer(aIndex) {
+ return true;
+ },
+
+ isContainerEmpty: function ftv_isContainerEmpty(aIndex) {
+ // If the folder has no children, the container is empty.
+ return !this._rowMap[aIndex].children.length;
+ },
+
+ /**
+ * Just look at the ftvItem here
+ */
+ isContainerOpen: function ftv_isContainerOpen(aIndex) {
+ return this._rowMap[aIndex].open;
+ },
+ getSummarizedCounts: function(aIndex, aColName) {
+ return this._rowMap[aIndex]._summarizedCounts.get(aColName);
+ },
+ isEditable: function ftv_isEditable(aRow, aCol) {
+ // We don't support editing rows in the tree yet. We may want to later as
+ // an easier way to rename folders.
+ return false;
+ },
+ isSeparator: function ftv_isSeparator(aIndex) {
+ // There are no separators in our trees.
+ return false;
+ },
+ isSorted: function ftv_isSorted() {
+ // We do our own customized sorting.
+ return false;
+ },
+ setTree: function ftv_setTree(aTree) {
+ this._tree = aTree;
+ },
+
+ /**
+ * Opens or closes a folder with children. The logic here is a bit hairy, so
+ * be very careful about changing anything.
+ */
+ toggleOpenState: function ftv_toggleOpenState(aIndex) {
+ this._toggleRow(aIndex, true);
+ },
+
+ recursivelyAddToMap: function ftv_recursivelyAddToMap(aChild, aNewIndex) {
+ // When we add sub-children, we're going to need to increase our index
+ // for the next add item at our own level.
+ let count = 0;
+ if (aChild.children.length && aChild.open) {
+ for (let [i, child] of Array.from(this._rowMap[aNewIndex].children).entries()) {
+ count++;
+ let index = Number(aNewIndex) + Number(i) + 1;
+ this._rowMap.splice(index, 0, child);
+
+ let kidsAdded = this.recursivelyAddToMap(child, index);
+ count += kidsAdded;
+ // Somehow the aNewIndex turns into a string without this.
+ aNewIndex = Number(aNewIndex) + kidsAdded;
+ }
+ }
+ return count;
+ },
+
+ _toggleRow: function toggleRow(aIndex, aExpandServer)
+ {
+ // Ok, this is a bit tricky.
+ this._rowMap[aIndex].open = !this._rowMap[aIndex].open;
+ if (!this._rowMap[aIndex].open) {
+ // We're closing the current container. Remove the children.
+
+ // Note that we can't simply splice out children.length, because some of
+ // them might have children too. Find out how many items we're actually
+ // going to splice.
+ let count = 0;
+ let i = aIndex + 1;
+ let row = this._rowMap[i];
+ while (row && row.level > this._rowMap[aIndex].level) {
+ count++;
+ row = this._rowMap[++i];
+ }
+ this._rowMap.splice(aIndex + 1, count);
+
+ // Remove us from the persist map.
+ this._persistItemClosed(this._rowMap[aIndex].id);
+
+ // Notify the tree of changes.
+ if (this._tree) {
+ this._tree.rowCountChanged(aIndex + 1, (-1) * count);
+ this._tree.invalidateRow(aIndex);
+ }
+ } else {
+ // We're opening the container. Add the children to our map.
+
+ // Note that these children may have been open when we were last closed,
+ // and if they are, we also have to add those grandchildren to the map.
+ let oldCount = this._rowMap.length;
+ this.recursivelyAddToMap(this._rowMap[aIndex], aIndex);
+
+ // Add this folder to the persist map.
+ this._persistItemOpen(this._rowMap[aIndex].id);
+
+ // Notify the tree of changes.
+ if (this._tree) {
+ this._tree.rowCountChanged(aIndex + 1, this._rowMap.length - oldCount);
+ this._tree.invalidateRow(aIndex);
+ }
+
+ if (this._treeElement.getAttribute("simplelist") == "true")
+ return;
+
+ // If this was a server that was expanded, let it update its counts.
+ let folder = this._rowMap[aIndex]._folder;
+ if (aExpandServer) {
+ if (folder.isServer)
+ folder.server.performExpand(msgWindow);
+ else if (folder instanceof Ci.nsIMsgImapMailFolder)
+ folder.performExpand(msgWindow);
+ }
+ }
+ },
+
+ // We don't implement any of these at the moment.
+ performAction: function ftv_performAction(aAction) {},
+ performActionOnCell: function ftv_performActionOnCell(aAction, aRow, aCol) {},
+ performActionOnRow: function ftv_performActionOnRow(aAction, aRow) {},
+ selectionChanged: function ftv_selectionChanged() {},
+ setCellText: function ftv_setCellText(aRow, aCol, aValue) {},
+ setCellValue: function ftv_setCellValue(aRow, aCol, aValue) {},
+ getCellValue: function ftv_getCellValue(aRow, aCol) {},
+ getColumnProperties: function ftv_getColumnProperties(aCol) { return ""; },
+ getImageSrc: function ftv_getImageSrc(aRow, aCol) {},
+ getProgressMode: function ftv_getProgressMode(aRow, aCol) {},
+ cycleCell: function ftv_cycleCell(aRow, aCol) {},
+ cycleHeader: function ftv_cycleHeader(aCol) {},
+
+ // ****************** End of nsITreeView implementation **************** //
+
+ //
+ // WARNING: Everything below this point is considered private. Touch at your
+ // own risk.
+
+ /**
+ * This is an array of all possible modes for the folder tree. You should not
+ * modify this directly, but rather use registerFolderTreeMode.
+ *
+ * Internally each mode is defined separately. But in the UI we currently
+ * expose only the "base" name (see baseMode()) of the mode plus a
+ * "Compact view" option. The internal name of the mode to use is then
+ * constructed from the base name and "_compact" suffix if compact view is
+ * selected. See bug 978592.
+ */
+ _modeNames: ["all", "unread", "unread_compact", "favorite", "favorite_compact", "recent_compact"],
+ _modeDisplayNames: {},
+
+ /**
+ * This is a javascript map of which folders we had open, so that we can
+ * persist their state over-time. It is designed to be used as a JSON object.
+ */
+ _persistOpenMap: {},
+ _notPersistedModes: ["unread", "unread_compact", "favorite", "favorite_compact", "recent_compact"],
+
+ /**
+ * Iterate over the persistent list and open the items (folders) stored in it.
+ */
+ _restoreOpenStates: function ftv__persistOpenStates() {
+ let mode = this.mode;
+ // Remove any saved state of modes where open state should not be persisted.
+ // This is mostly for migration from older profiles that may have the info
+ // stored.
+ if (this._notPersistedModes.includes(mode)) {
+ delete this._persistOpenMap[mode];
+ }
+
+ let curLevel = 0;
+ let tree = this;
+ let map = tree._persistOpenMap[mode]; // may be undefined
+ function openLevel() {
+ let goOn = false;
+ // We can't use a js iterator because we're changing the array as we go.
+ // So fallback on old trick of going backwards from the end, which
+ // doesn't care when you add things at the end.
+ for (let i = tree._rowMap.length - 1; i >= 0; i--) {
+ let row = tree._rowMap[i];
+ if (row.level != curLevel)
+ continue;
+
+ // The initial state of all rows is closed,
+ // so toggle those we want open.
+ if (!map || map.includes(row.id)) {
+ tree._toggleRow(i, false);
+ goOn = true;
+ }
+ }
+
+ // If we opened up any new kids, we need to check their level as well.
+ curLevel++;
+ if (goOn)
+ openLevel();
+ }
+ openLevel();
+ },
+
+ /**
+ * Remove the item from the persistent list, meaning the item should
+ * be persisted as closed in the tree.
+ *
+ * @param aItemId The URI of the folder item.
+ */
+ _persistItemClosed: function ftv_unpersistItem(aItemId) {
+ let mode = this.mode;
+ if (this._notPersistedModes.includes(mode))
+ return;
+
+ // If the whole mode is not in the map yet,
+ // we can silently ignore the folder removal.
+ if (!this._persistOpenMap[mode])
+ return;
+
+ let persistMapIndex = this._persistOpenMap[mode].indexOf(aItemId);
+ if (persistMapIndex != -1)
+ this._persistOpenMap[mode].splice(persistMapIndex, 1);
+ },
+
+ /**
+ * Add the item from the persistent list, meaning the item should
+ * be persisted as open (expanded) in the tree.
+ *
+ * @param aItemId The URI of the folder item.
+ */
+ _persistItemOpen: function ftv_persistItem(aItemId) {
+ let mode = this.mode;
+ if (this._notPersistedModes.includes(mode))
+ return;
+
+ if (!this._persistOpenMap[mode])
+ this._persistOpenMap[mode] = [];
+
+ if (!this._persistOpenMap[mode].includes(aItemId))
+ this._persistOpenMap[mode].push(aItemId);
+ },
+
+ _tree: null,
+ selection: null,
+ /**
+ * An array of ftvItems, where each item corresponds to a row in the tree
+ */
+ _rowMap: null,
+
+ /**
+ * Completely discards the current tree and rebuilds it based on current
+ * settings
+ */
+ _rebuild: function ftv__rebuild() {
+ let newRowMap;
+ try {
+ newRowMap = this._modes[this.mode].generateMap(this);
+ } catch(ex) {
+ Services.console.logStringMessage("generator " + this.mode +
+ " failed with exception: " + ex);
+ this.mode = kDefaultMode;
+ newRowMap = this._modes[this.mode].generateMap(this);
+ }
+ let selectedFolders = this.getSelectedFolders();
+ if (this.selection)
+ this.selection.clearSelection();
+ // There's a chance the call to the map generator altered this._rowMap, so
+ // evaluate oldCount after calling it rather than before.
+ let oldCount = this._rowMap ? this._rowMap.length : null;
+ this._rowMap = newRowMap;
+
+ this._treeElement.dispatchEvent(new Event("mapRebuild",
+ { bubbles: true, cancelable: false }));
+
+ if (this._tree) {
+ if (oldCount !== null)
+ this._tree.rowCountChanged(0, this._rowMap.length - oldCount);
+ this._tree.invalidate();
+ }
+
+ this._restoreOpenStates();
+ // Restore selection.
+ for (let folder of selectedFolders) {
+ if (folder) {
+ let index = this.getIndexOfFolder(folder);
+ if (index != null)
+ this.selection.toggleSelect(index);
+ }
+ }
+ },
+
+ _sortedAccounts: function ftv_getSortedAccounts() {
+ let accounts = FolderUtils.allAccountsSorted(true);
+
+ // Don't show deferred pop accounts.
+ accounts = accounts.filter(function isNotDeferred(a) {
+ let server = a.incomingServer;
+ return !(server instanceof Ci.nsIPop3IncomingServer &&
+ server.deferredToAccount);
+ });
+
+ return accounts;
+ },
+ /**
+ * Contains the set of modes registered with the folder tree, initially those
+ * included by default. This is a map from names of modes to their
+ * implementations of |IFolderTreeMode|.
+ */
+ _modes: {
+ /**
+ * The all mode returns all folders, arranged in a hierarchy
+ */
+ all: {
+ __proto__: IFolderTreeMode,
+
+ generateMap: function(ftv) {
+ let accounts = gFolderTreeView._sortedAccounts();
+ // Force each root folder to do its local subfolder discovery.
+ MailUtils.discoverFolders();
+
+ return accounts.map(acct => new ftvItem(acct.incomingServer.rootFolder));
+ }
+ },
+
+ /**
+ * The unread mode returns all folders that are not root-folders and that
+ * have unread items. Also always keep the currently selected folder
+ * so it doesn't disappear under the user.
+ * It also includes parent folders of the Unread folders so the hierarchy
+ * shown.
+ */
+ unread: {
+ __proto__: IFolderTreeMode,
+
+ generateMap: function(ftv) {
+ let filterUnread = function filterUnread(aFolder) {
+ let currentFolder = gFolderTreeView.getSelectedFolders()[0];
+ return ((aFolder.getNumUnread(true) > 0) ||
+ (aFolder == currentFolder));
+ }
+
+ let accounts = gFolderTreeView._sortedAccounts();
+ // Force each root folder to do its local subfolder discovery.
+ MailUtils.discoverFolders();
+
+ let unreadRootFolders = [];
+ for (let acct of accounts) {
+ let rootFolder = acct.incomingServer.rootFolder;
+ // Add rootFolders of accounts that contain at least one Favorite
+ // folder.
+ if (rootFolder.getNumUnread(true) > 0)
+ unreadRootFolders.push(new ftvItem(rootFolder, filterUnread));
+ }
+
+ return unreadRootFolders;
+ },
+
+ handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
+ // We want to rebuild only if we have a newly unread folder
+ // and we didn't already have the folder.
+ if (aProperty == "TotalUnreadMessages" && aOld == 0 && aNew > 0 &&
+ gFolderTreeView.getIndexOfFolder(aItem) == null) {
+ gFolderTreeView._rebuild();
+ return true;
+ }
+ return false;
+ }
+ },
+
+ /**
+ * A variant of the 'unread' mode above. This does not include the parent
+ * folders and the unread folders are shown in a flat list with no
+ * hierarchy.
+ */
+ unread_compact: {
+ __proto__: IFolderTreeMode,
+
+ generateMap: function(ftv) {
+ let map = [];
+ let currentFolder = gFolderTreeView.getSelectedFolders()[0];
+ for (let folder of ftv._enumerateFolders) {
+ if ((!folder.isServer && folder.getNumUnread(false) > 0) ||
+ (folder == currentFolder))
+ map.push(new ftvItem(folder));
+ }
+
+ // There are no children in this view!
+ for (let folder of map) {
+ folder.__defineGetter__("children", () => []);
+ folder.addServerName = true;
+ }
+ sortFolderItems(map);
+ return map;
+ },
+
+ getParentOfFolder: function(aFolder) {
+ // This is a flat view, so no folders have parents.
+ return null;
+ },
+
+ handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
+ // We want to rebuild only if we have a newly unread folder
+ // and we didn't already have the folder.
+ if (aProperty == "TotalUnreadMessages" && aOld == 0 && aNew > 0 &&
+ gFolderTreeView.getIndexOfFolder(aItem) == null) {
+ gFolderTreeView._rebuild();
+ return true;
+ }
+ return false;
+ }
+ },
+
+ /**
+ * The favorites mode returns all folders whose flags are set to include
+ * the favorite flag.
+ * It also includes parent folders of the Unread folders so the hierarchy
+ * shown.
+ */
+ favorite: {
+ __proto__: IFolderTreeMode,
+
+ generateMap: function(ftv) {
+ let accounts = gFolderTreeView._sortedAccounts();
+ // Force each root folder to do its local subfolder discovery.
+ MailUtils.discoverFolders();
+
+ let favRootFolders = [];
+ let filterFavorite = function filterFavorite(aFolder) {
+ return aFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Favorite) != null;
+ }
+ for (let acct of accounts) {
+ let rootFolder = acct.incomingServer.rootFolder;
+ // Add rootFolders of accounts that contain at least one Favorite folder.
+ if (filterFavorite(rootFolder))
+ favRootFolders.push(new ftvItem(rootFolder, filterFavorite));
+ }
+
+ return favRootFolders;
+ },
+
+ handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
+ // We want to rebuild if the favorite status of a folder changed.
+ if (aProperty == "FolderFlag" &&
+ ((aOld & Ci.nsMsgFolderFlags.Favorite) !=
+ (aNew & Ci.nsMsgFolderFlags.Favorite))) {
+ gFolderTreeView._rebuild();
+ return true;
+ }
+ return false;
+ }
+ },
+
+ /**
+ * A variant of the 'favorite' mode above. This does not include the parent
+ * folders and the unread folders are shown in a compact list with no
+ * hierarchy.
+ */
+ favorite_compact: {
+ __proto__: IFolderTreeMode,
+
+ generateMap: function(ftv) {
+ let faves = [];
+ for (let folder of ftv._enumerateFolders) {
+ if (folder.flags & Ci.nsMsgFolderFlags.Favorite)
+ faves.push(new ftvItem(folder));
+ }
+
+ // We want to display the account name alongside folders that have
+ // duplicated folder names.
+ let uniqueNames = new Set(); // set of folder names seen at least once
+ let dupeNames = new Set(); // set of folders seen at least twice
+ for (let item of faves) {
+ let name = item._folder.abbreviatedName.toLocaleLowerCase();
+ if (uniqueNames.has(name)) {
+ if (!dupeNames.has(name))
+ dupeNames.add(name);
+ } else {
+ uniqueNames.add(name);
+ }
+ }
+
+ // There are no children in this view!
+ for (let item of faves) {
+ let name = item._folder.abbreviatedName.toLocaleLowerCase();
+ item.__defineGetter__("children", () => []);
+ item.addServerName = dupeNames.has(name);
+ }
+ sortFolderItems(faves);
+ return faves;
+ },
+
+ getParentOfFolder: function(aFolder) {
+ // This is a flat view, so no folders have parents.
+ return null;
+ },
+
+ handleChangedIntProperty: function(aItem, aProperty, aOld, aNew) {
+ // We want to rebuild if the favorite status of a folder changed.
+ if (aProperty == "FolderFlag" &&
+ ((aOld & Ci.nsMsgFolderFlags.Favorite) !=
+ (aNew & Ci.nsMsgFolderFlags.Favorite))) {
+ gFolderTreeView._rebuild();
+ return true;
+ }
+ return false;
+ }
+ },
+
+ /**
+ * The recent mode is a flat view of the 15 most recently used folders
+ */
+ recent_compact: {
+ __proto__: IFolderTreeMode,
+
+ generateMap: function(ftv) {
+ const MAXRECENT = 15;
+
+ // Get 15 (MAXRECENT) most recently accessed folders.
+ let recentFolders = FolderUtils.getMostRecentFolders(
+ ftv._enumerateFolders,
+ MAXRECENT,
+ "MRUTime",
+ null
+ );
+
+ // Sort the folder names alphabetically.
+ recentFolders.sort(function rf_sort(a, b){
+ let aLabel = a.prettyName;
+ let bLabel = b.prettyName;
+ if (aLabel == bLabel) {
+ aLabel = a.server.prettyName;
+ bLabel = b.server.prettyName;
+ }
+ return FolderUtils.folderNameCompare(aLabel, bLabel);
+ });
+
+ let items = recentFolders.map(f => new ftvItem(f));
+
+ // There are no children in this view!
+ // And we want to display the account name to distinguish folders w/
+ // the same name.
+ for (let folder of items) {
+ folder.__defineGetter__("children", () => []);
+ folder.addServerName = true;
+ }
+
+ return items;
+ },
+
+ getParentOfFolder: function(aFolder) {
+ // This is a flat view, so no folders have parents.
+ return null;
+ }
+ }
+ },
+
+ /**
+ * This is a helper attribute that simply returns a flat list of all folders
+ */
+ get _enumerateFolders() {
+ let folders = [];
+
+ for (let server of fixIterator(MailServices.accounts.allServers, Ci.nsIMsgIncomingServer)) {
+ // Skip deferred accounts.
+ if (server instanceof Ci.nsIPop3IncomingServer &&
+ server.deferredToAccount)
+ continue;
+
+ let rootFolder = server.rootFolder;
+ folders.push(rootFolder);
+ this.addSubFolders(rootFolder, folders);
+ }
+ return folders;
+ },
+
+ /**
+ * This is a recursive function to add all subfolders to the array. It
+ * assumes that the passed in folder itself has already been added.
+ *
+ * @param aFolder the folder whose subfolders should be added
+ * @param folders the array to add the folders to.
+ */
+ addSubFolders : function ftv_addSubFolders (folder, folders) {
+ for (let f of fixIterator(folder.subFolders, Ci.nsIMsgFolder)) {
+ folders.push(f);
+ this.addSubFolders(f, folders);
+ }
+ },
+
+ /**
+ * This updates the rowmap and invalidates the right row(s) in the tree
+ */
+ _addChildToView: function ftl_addChildToView(aParent, aParentIndex, aNewChild) {
+ if (aParent.open) {
+ let newChildIndex;
+ let newChildNum = aParent._children.indexOf(aNewChild);
+ // Only child - go right after our parent.
+ if (newChildNum == 0) {
+ newChildIndex = Number(aParentIndex) + 1
+ }
+ // If we're not the last child, insert ourselves before the next child.
+ else if (newChildNum < aParent._children.length - 1) {
+ newChildIndex = this.getIndexOfFolder(aParent._children[Number(newChildNum) + 1]._folder);
+ }
+ // Otherwise, go after the last child.
+ else {
+ let lastChild = aParent._children[newChildNum - 1];
+ let lastChildIndex = this.getIndexOfFolder(lastChild._folder);
+ newChildIndex = Number(lastChildIndex) + 1;
+ while (newChildIndex < this.rowCount &&
+ this._rowMap[newChildIndex].level > this._rowMap[lastChildIndex].level)
+ newChildIndex++;
+ }
+ this._rowMap.splice(newChildIndex, 0, aNewChild);
+ this._tree.rowCountChanged(newChildIndex, 1);
+ } else {
+ this._tree.invalidateRow(aParentIndex);
+ }
+ },
+
+ /**
+ * This is our implementation of nsIMsgFolderListener to watch for changes
+ */
+ onFolderAdded: function ftl_add(aParentItem, aItem) {
+ // Ignore this item if it's not a folder, or we knew about it.
+ if (this.getIndexOfFolder(aItem) != null)
+ return;
+
+ // If no parent, this is an account, so let's rebuild.
+ if (!aParentItem) {
+ if (!aItem.server.hidden) // Ignore hidden server items.
+ this._rebuild();
+ return;
+ }
+ this._modes[this._mode].onFolderAdded(
+ aParentItem.QueryInterface(Ci.nsIMsgFolder), aItem);
+ },
+ onMessageAdded: function(parentFolder, msg) {},
+
+ addFolder: function ftl_add_folder(aParentItem, aItem) {
+ // This intentionally adds any new folder even if it would not pass the
+ // _filterFunction. The idea is that the user can add new folders even
+ // in modes like "unread" or "favorite" and could wonder why they
+ // are not appearing (forgetting they do not meet the criteria of the view).
+ // The folders will be hidden properly next time the view is rebuilt.
+ let parentIndex = this.getIndexOfFolder(aParentItem);
+ let parent = this._rowMap[parentIndex];
+ if (!parent)
+ return;
+
+ // Getting these children might have triggered our parent to build its
+ // array just now, in which case the added item will already exist.
+ let children = parent.children;
+ var newChild;
+ for (let child of children) {
+ if (child._folder == aItem) {
+ newChild = child;
+ break;
+ }
+ }
+ if (!newChild) {
+ newChild = new ftvItem(aItem);
+ parent.children.push(newChild);
+ newChild._level = parent._level + 1;
+ newChild._parent = parent;
+ sortFolderItems(parent._children);
+ }
+ // If the parent is open, add the new child into the folder pane.
+ // Otherwise, just invalidate the parent row. Note that this code doesn't
+ // get called for the smart folder case.
+ if (!parent.open) {
+ // Special case adding a special folder when the parent is collapsed.
+ // Expand the parent so the user can see the special child.
+ // Expanding the parent is sufficient to add the folder to the view,
+ // because either we knew about it, or we will have added a child item
+ // for it above.
+ if (newChild._folder.flags & Ci.nsMsgFolderFlags.SpecialUse) {
+ this._toggleRow(parentIndex, false);
+ return;
+ }
+ }
+ this._addChildToView(parent, parentIndex, newChild);
+ },
+
+ onFolderRemoved: function ftl_remove(aRDFParentItem, aItem) {
+ this._persistItemClosed(aItem.URI);
+
+ let index = this.getIndexOfFolder(aItem);
+ if (index == null)
+ return;
+ // Forget our parent's children; they'll get rebuilt.
+ if (aRDFParentItem && this._rowMap[index]._parent)
+ this._rowMap[index]._parent._children = null;
+ let kidCount = 1;
+ let walker = Number(index) + 1;
+ while (walker < this.rowCount &&
+ this._rowMap[walker].level > this._rowMap[index].level) {
+ walker++;
+ kidCount++;
+ }
+ this._rowMap.splice(index, kidCount);
+ this._tree.rowCountChanged(index, -1 * kidCount);
+ this._tree.invalidateRow(index);
+ },
+
+ onMessageRemoved: function(parentFolder, msg) {},
+
+ onFolderPropertyChanged: function(aItem, aProperty, aOld, aNew) {},
+ onFolderIntPropertyChanged: function(aItem, aProperty, aOld, aNew) {
+ // First try mode specific handling of the changed property.
+ if (this._modes[this.mode].handleChangedIntProperty(aItem, aProperty, aOld,
+ aNew))
+ return;
+
+ if (aItem instanceof Ci.nsIMsgFolder) {
+ let index = this.getIndexOfFolder(aItem);
+ let folder = aItem;
+ let folderTreeMode = this._modes[this._mode];
+ // Look for first visible ancestor.
+ while (index == null) {
+ folder = folderTreeMode.getParentOfFolder(folder);
+ if (!folder)
+ break;
+ index = this.getIndexOfFolder(folder);
+ }
+ if (index != null)
+ this._tree.invalidateRow(index);
+ }
+ },
+
+ onFolderBoolPropertyChanged: function(aItem, aProperty, aOld, aNew) {
+ let index = this.getIndexOfFolder(aItem);
+ if (index != null)
+ this._tree.invalidateRow(index);
+ },
+ onFolderUnicharPropertyChanged: function(aItem, aProperty, aOld, aNew) {},
+ onFolderPropertyFlagChanged: function(aItem, aProperty, aOld, aNew) {},
+ onFolderEvent: function(aFolder, aEvent) {
+ let index = this.getIndexOfFolder(aFolder);
+ if (index != null)
+ this._tree.invalidateRow(index);
+ }
+};
+
+/**
+ * The ftvItem object represents a single row in the tree view. Because I'm lazy
+ * I'm just going to define the expected interface here. You are free to return
+ * an alternative object, provided that it matches this interface:
+ *
+ * id (attribute) - a unique string for this object. Must persist over sessions
+ * text (attribute) - the text to display in the tree
+ * level (attribute) - the level in the tree to display the item at
+ * open (rw, attribute) - whether or not this container is open
+ * children (attribute) - an array of child items also conforming to this spec
+ * getProperties (function) - a call from getRowProperties or getCellProperties
+ * for this item will be passed into this function
+ * command (function) - this function will be called when the item is double-
+ * clicked
+ */
+
+/**
+ * The ftvItem constructor takes these arguments:
+ *
+ * @param aFolder The folder attached to this row in the tree.
+ * @param aFolderFilter When showing children folders of this one,
+ * only show those that pass this filter function.
+ * If unset, show all subfolders.
+ */
+function ftvItem(aFolder, aFolderFilter) {
+ this._folder = aFolder;
+ this._level = 0;
+ this._parent = null;
+ this._folderFilter = aFolderFilter;
+ // The map contains message counts for each folder column.
+ // Each key is a column name (ID) from the folder tree.
+ // Value is an array of the format
+ // "[value_for_folder, value_for_all_its_subfolders]".
+ this._summarizedCounts = new Map();
+}
+
+ftvItem.prototype = {
+ open: false,
+ addServerName: false,
+ useServerNameOnly: false,
+
+ get id() {
+ return this._folder.URI;
+ },
+ get text() {
+ return this.getText("folderNameCol");
+ },
+
+ getText(aColName) {
+ // Only show counts / total size of subtree if the pref is set,
+ // we are in "All folders" mode and this folder row is not expanded.
+ gFolderStatsHelpers.sumSubfolders =
+ gFolderStatsHelpers.sumSubfoldersPref &&
+ (gFolderTreeView.mode == kDefaultMode) &&
+ this._folder.hasSubFolders && !this.open;
+
+ this._summarizedCounts.delete(aColName);
+ switch (aColName) {
+ case "folderNameCol":
+ let text;
+ if (this.useServerNameOnly)
+ text = this._folder.server.prettyName;
+ else {
+ text = this._folder.abbreviatedName;
+ if (this.addServerName) {
+ text = gFolderTreeView.messengerBundle.getFormattedString(
+ "folderWithAccount", [text, this._folder.server.prettyName]);
+ }
+ }
+
+ // In a simple list tree we don't care for attributes other than folder
+ // name.
+ if (gFolderTreeView._treeElement.getAttribute("simplelist") == "true")
+ return text;
+
+ // If the unread column is shown, we don't need to add the count
+ // to the name.
+ if (!document.getElementById("folderUnreadCol").hidden)
+ return text;
+
+ let unread = this._folder.getNumUnread(false);
+ let totalUnread = gFolderStatsHelpers.sumSubfolders ?
+ this._folder.getNumUnread(true) : unread;
+ this._summarizedCounts.set(aColName, [unread, totalUnread - unread]);
+ if (totalUnread > 0) {
+ text = gFolderTreeView.messengerBundle.getFormattedString(
+ "folderWithUnreadMsgs",
+ [text,
+ gFolderStatsHelpers.addSummarizedPrefix(totalUnread,
+ unread != totalUnread)]);
+ }
+ return text;
+
+ case "folderUnreadCol":
+ let folderUnread = this._folder.getNumUnread(false);
+ let subfoldersUnread = gFolderStatsHelpers.sumSubfolders ?
+ this._folder.getNumUnread(true) : folderUnread;
+ this._summarizedCounts.set(aColName, [folderUnread,
+ subfoldersUnread - folderUnread]);
+ return gFolderStatsHelpers
+ .fixNum(subfoldersUnread, folderUnread != subfoldersUnread);
+
+ case "folderTotalCol":
+ let folderTotal = this._folder.getTotalMessages(false);
+ let subfoldersTotal = gFolderStatsHelpers.sumSubfolders ?
+ this._folder.getTotalMessages(true) : folderTotal;
+ this._summarizedCounts.set(aColName, [folderTotal,
+ subfoldersTotal - folderTotal]);
+ return gFolderStatsHelpers
+ .fixNum(subfoldersTotal, folderTotal != subfoldersTotal);
+
+ case "folderSizeCol":
+ let thisFolderSize = gFolderStatsHelpers.getFolderSize(this._folder);
+ let subfoldersSize = gFolderStatsHelpers.sumSubfolders ?
+ gFolderStatsHelpers.getSubfoldersSize(this._folder) : 0;
+
+ if (subfoldersSize == gFolderStatsHelpers.kUnknownSize ||
+ thisFolderSize == gFolderStatsHelpers.kUnknownSize)
+ return gFolderStatsHelpers.kUnknownSize;
+
+ let totalSize = thisFolderSize + subfoldersSize;
+ if (totalSize == 0)
+ return "";
+
+ let [totalText, folderUnit] = gFolderStatsHelpers.formatFolderSize(totalSize);
+ let folderText = (subfoldersSize == 0) ? totalText :
+ gFolderStatsHelpers.formatFolderSize(thisFolderSize, folderUnit)[0];
+ let subfoldersText = (subfoldersSize == 0) ? "" :
+ gFolderStatsHelpers.formatFolderSize(subfoldersSize, folderUnit)[0];
+ this._summarizedCounts.set(aColName, [folderText, subfoldersText]);
+ return gFolderStatsHelpers
+ .addSummarizedPrefix(totalText, totalSize != thisFolderSize);
+
+ default:
+ return "";
+ }
+ },
+
+ get level() {
+ return this._level;
+ },
+
+ getProperties: function (aColumn) {
+ if (aColumn && aColumn.id != "folderNameCol")
+ return "";
+
+ let properties = FolderUtils.getFolderProperties(this._folder, this.open);
+
+ return properties;
+ },
+
+ command: function fti_command() {
+ if (!Services.prefs.getBoolPref("mailnews.reuse_thread_window2")) {
+ MsgOpenNewWindowForFolder(this._folder.URI, -1 /* key */);
+ }
+ },
+
+ _children: null,
+ get children() {
+ // We're caching our child list to save perf.
+ if (!this._children) {
+ let iter;
+ try {
+ iter = fixIterator(this._folder.subFolders, Ci.nsIMsgFolder);
+ } catch (ex) {
+ Services.console.logStringMessage("Discovering children for " +
+ this._folder.URI + " failed with " +
+ "exception: " + ex);
+ iter = [];
+ }
+ this._children = [];
+ // Out of all children, only keep those that match the _folderFilter
+ // and those that contain such children.
+ for (let folder of iter) {
+ if (!this._folderFilter || this._folderFilter(folder)) {
+ this._children.push(new ftvItem(folder, this._folderFilter));
+ }
+ }
+ sortFolderItems(this._children);
+ // Each child is a level one below us.
+ for (let child of this._children) {
+ child._level = this._level + 1;
+ child._parent = this;
+ }
+ }
+ return this._children;
+ }
+};
+
+/**
+ * This handles the invocation of most commands dealing with folders, based off
+ * of the current selection, or a passed in folder.
+ */
+var gFolderTreeController = {
+ /**
+ * Opens the dialog to create a new sub-folder, and creates it if the user
+ * accepts
+ *
+ * @param aParent (optional) the parent for the new subfolder
+ */
+ newFolder(aParent) {
+ let folder = aParent || GetSelectedMsgFolders()[0];
+
+ // Make sure we actually can create subfolders.
+ if (!folder.canCreateSubfolders) {
+ // Check if we can create them at the root.
+ let rootMsgFolder = folder.server.rootMsgFolder;
+ if (rootMsgFolder.canCreateSubfolders)
+ folder = rootMsgFolder;
+ else // just use the default account
+ folder = GetDefaultAccountRootFolder();
+ }
+
+ let dualUseFolders = true;
+ if (folder.server instanceof Ci.nsIImapIncomingServer)
+ dualUseFolders = folder.server.dualUseFolders;
+
+ function newFolderCallback(aName, aFolder) {
+ // createSubfolder can throw an exception, causing the newFolder dialog
+ // to not close and wait for another input.
+ // TODO: Rewrite this logic and move the opening of alert dialogs from
+ // nsMsgLocalMailFolder::CreateSubfolderInternal to here (bug 831190#c16).
+ if (aName)
+ aFolder.createSubfolder(aName, msgWindow);
+ }
+
+ window.openDialog("chrome://messenger/content/newFolderDialog.xul",
+ "",
+ "chrome,modal,centerscreen",
+ {folder: folder,
+ dualUseFolders: dualUseFolders,
+ okCallback: newFolderCallback});
+ },
+
+ /**
+ * Opens the dialog to edit the properties for a folder
+ *
+ * @param aTabID (optional) the tab to show in the dialog
+ * @param aFolder (optional) the folder to edit, if not the selected one
+ */
+ editFolder(aTabID, aFolder) {
+ let folder = aFolder || GetSelectedMsgFolders()[0];
+
+ // If a server is selected, view settings for that account.
+ if (folder.isServer) {
+ MsgAccountManager(null, folder.server);
+ return;
+ }
+
+ if (folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
+ // virtual folders get their own property dialog that contains all of the
+ // search information related to the virtual folder.
+ this.editVirtualFolder(folder);
+ return;
+ }
+
+ let title = gFolderTreeView.messengerBundle.getString("folderProperties");
+
+ function editFolderCallback(aNewName, aOldName, aUri) {
+ if (aNewName != aOldName)
+ folder.rename(aNewName, msgWindow);
+ }
+
+ function rebuildSummary(msgFolder) {
+ if (msgFolder.locked) {
+ msgFolder.throwAlertMsg("operationFailedFolderBusy", msgWindow);
+ return;
+ }
+ if (msgFolder.supportsOffline) {
+ // Remove the offline store, if any.
+ let offlineStore = msgFolder.filePath;
+ // XXX todo: figure out how to delete a maildir directory async. This
+ // delete causes main thread lockup for large maildir folders.
+ if (offlineStore.exists())
+ offlineStore.remove(true);
+ }
+
+ // Send a notification that we are triggering a database rebuild.
+ MailServices.mfn.notifyItemEvent(folder, "FolderReindexTriggered", null,
+ null);
+
+ msgFolder.msgDatabase.summaryValid = false;
+
+ try {
+ msgFolder.closeAndBackupFolderDB("");
+ }
+ catch(e) {
+ // In a failure, proceed anyway since we're dealing with problems
+ msgFolder.ForceDBClosed();
+ }
+ // these two lines will cause the thread pane to get reloaded
+ // when the download/reparse is finished. Only do this
+ // if the selected folder is loaded (i.e., not thru the
+ // context menu on a non-loaded folder).
+ if (msgFolder == GetLoadedMsgFolder()) {
+ gRerootOnFolderLoad = true;
+ gCurrentFolderToReroot = msgFolder.URI;
+ }
+ msgFolder.updateFolder(msgWindow);
+ }
+
+ window.openDialog("chrome://messenger/content/folderProps.xul",
+ "", "chrome,modal,centerscreen",
+ {folder: folder, serverType: folder.server.type,
+ msgWindow: msgWindow, title: title,
+ okCallback: editFolderCallback, tabID: aTabID,
+ name: folder.prettyName,
+ rebuildSummaryCallback: rebuildSummary});
+ },
+
+ /**
+ * Opens the dialog to rename a particular folder, and does the renaming if
+ * the user clicks OK in that dialog
+ *
+ * @param aFolder (optional) the folder to rename, if different than the
+ * currently selected one
+ */
+ renameFolder(aFolder) {
+ let folder = aFolder || GetSelectedMsgFolders()[0];
+
+ let controller = this;
+ function renameCallback(aName, aUri) {
+ if (aUri != folder.URI)
+ Cu.reportError("got back a different folder to rename!");
+
+ controller._resetThreadPane();
+ let folderTree = document.getElementById("folderTree");
+ folderTree.view.selection.clearSelection();
+
+ folder.rename(aName, msgWindow);
+ }
+
+ window.openDialog("chrome://messenger/content/renameFolderDialog.xul",
+ "", "chrome,modal,centerscreen",
+ {preselectedURI: folder.URI,
+ okCallback: renameCallback, name: folder.prettyName});
+ },
+
+ /**
+ * Deletes a folder from its parent. Also handles unsubscribe from newsgroups
+ * if the selected folder/s happen to be nntp.
+ *
+ * @param aFolder (optional) the folder to delete, if not the selected one
+ */
+ deleteFolder(aFolder) {
+ let folders = aFolder ? [aFolder] : GetSelectedMsgFolders();
+ let prompt = Services.prompt;
+ for (let folder of folders) {
+ // For newsgroups, "delete" means "unsubscribe".
+ if (folder.server.type == "nntp" &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
+ MsgUnsubscribe([folder]);
+ continue;
+ }
+
+ let canDelete = folder.isSpecialFolder(Ci.nsMsgFolderFlags.Junk, false) ?
+ CanRenameDeleteJunkMail(folder.URI) : folder.deletable;
+ if (!canDelete)
+ continue;
+
+ if (folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
+ let confirmation = gMessengerBundle.getString("confirmSavedSearchDeleteMessage");
+ let title = gMessengerBundle.getString("confirmSavedSearchDeleteTitle");
+ let buttonTitle = gMessengerBundle.getString("confirmSavedSearchDeleteButton");
+ let buttonFlags = prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_0 +
+ prompt.BUTTON_TITLE_CANCEL * prompt.BUTTON_POS_1;
+ if (prompt.confirmEx(window, title, confirmation, buttonFlags, buttonTitle,
+ "", "", "", {}) != 0) /* the yes button is in position 0 */
+ continue;
+ if (gCurrentVirtualFolderUri == folder.URI)
+ gCurrentVirtualFolderUri = null;
+ }
+
+ // We can delete this folder.
+ try {
+ folder.deleteSelf(msgWindow);
+ }
+ // Ignore known errors from canceled warning dialogs.
+ catch (ex) {
+ const NS_MSG_ERROR_COPY_FOLDER_ABORTED = 0x8055001a;
+ if (ex.result != NS_MSG_ERROR_COPY_FOLDER_ABORTED) {
+ throw ex;
+ }
+ }
+ }
+ },
+
+ /**
+ * Prompts the user to confirm and empties the trash for the selected folder.
+ * The folder and its children are only emptied if it has the proper Trash
+ * flag.
+ *
+ * @param aFolder (optional) The trash folder to empty. If unspecified or not
+ * a trash folder, the currently selected server's
+ * trash folder is used.
+ */
+ emptyTrash(aFolder) {
+ let folder = aFolder || GetSelectedMsgFolders()[0];
+ if (!folder.getFlag(Ci.nsMsgFolderFlags.Trash))
+ folder = folder.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash);
+ if (!folder)
+ return;
+
+ if (this._checkConfirmationPrompt("emptyTrash"))
+ folder.emptyTrash(null);
+ },
+
+ /**
+ * Deletes everything (folders and messages) in the selected folder.
+ * The folder is only emptied if it has the proper Junk flag.
+ *
+ * @param aFolder (optional) The folder to empty. If unspecified, the
+ * currently selected folder is used, if it
+ * is junk.
+ */
+ emptyJunk(aFolder) {
+ let folder = aFolder || GetSelectedMsgFolders()[0];
+
+ if (!folder || !folder.getFlag(Ci.nsMsgFolderFlags.Junk))
+ return;
+
+ if (!this._checkConfirmationPrompt("emptyJunk"))
+ return;
+
+ // Delete any sub-folders this folder might have.
+ for (let f of folder.subFolders) {
+ folder.propagateDelete(f, true);
+ }
+
+ // Now delete the messages.
+ folder.deleteMessages([...folder.messages], msgWindow, true, false, null, false);
+ },
+
+ /**
+ * Compacts either particular folder/s, or selected folders.
+ *
+ * @param aFolders (optional) the folders to compact, if different than the
+ * currently selected ones
+ */
+ compactFolders(aFolders) {
+ let folders = aFolders || GetSelectedMsgFolders();
+ for (let folder of folders) {
+ let isImapFolder = folder.server.type == "imap";
+ // Can't compact folders that have just been compacted
+ if (!isImapFolder && !folder.expungedBytes)
+ return;
+
+ // Reset thread pane for non-imap folders.
+ if (!isImapFolder && gDBView && gDBView.msgFolder == folder) {
+ this._resetThreadPane();
+ }
+
+ folder.compact(null, msgWindow);
+ }
+ },
+ /**
+ * Compacts all folders for accounts that the given folders belong
+ * to, or all folders for accounts of the currently selected folders.
+ *
+ * @param aFolders (optional) the folders for whose accounts we should compact
+ * all folders, if different than the currently
+ * selected ones
+ */
+ compactAllFoldersForAccount(aFolders) {
+ let folders = aFolders || GetSelectedMsgFolders();
+ for (let folder of folders) {
+ folder.compactAll(null, msgWindow);
+ // Reset thread pane for non-imap folders.
+ if (gDBView && folder.server.type != "imap")
+ this._resetThreadPane();
+ }
+ },
+
+ /**
+ * Opens the dialog to create a new virtual folder
+ *
+ * @param aName - the default name for the new folder
+ * @param aSearchTerms - the search terms associated with the folder
+ * @param aParent - the folder to run the search terms on
+ */
+ newVirtualFolder(aName, aSearchTerms, aParent) {
+ let folder = aParent || GetSelectedMsgFolders()[0];
+ if (!folder)
+ folder = GetDefaultAccountRootFolder();
+
+ let name = folder.prettyName;
+ if (aName)
+ name += "-" + aName;
+
+ window.openDialog("chrome://messenger/content/virtualFolderProperties.xul",
+ "", "chrome,modal,centerscreen",
+ {folder: folder, searchTerms: aSearchTerms,
+ newFolderName: name});
+ },
+
+ /**
+ * Opens the dialog to edit the properties for a virtual folder
+ *
+ * @param aFolder (optional) the folder to edit, if not the selected one
+ */
+ editVirtualFolder(aFolder) {
+ let folder = aFolder || GetSelectedMsgFolders()[0];
+
+ function editVirtualCallback(aURI) {
+ // we need to reload the folder if it is the currently loaded folder...
+ if (gMsgFolderSelected && aURI == gMsgFolderSelected.URI) {
+ // force the folder pane to reload the virtual folder
+ gMsgFolderSelected = null;
+ FolderPaneSelectionChange();
+ }
+ }
+ window.openDialog("chrome://messenger/content/virtualFolderProperties.xul",
+ "", "chrome,modal,centerscreen",
+ {folder: folder, editExistingFolder: true,
+ onOKCallback: editVirtualCallback,
+ msgWindow:msgWindow});
+ },
+
+ /**
+ * Opens a search window with the given folder, or the selected one if none
+ * is given.
+ *
+ * @param [aFolder] the folder to open the search window for, if different
+ * from the selected one
+ */
+ searchMessages(aFolder) {
+ MsgSearchMessages(aFolder || GetSelectedMsgFolders()[0]);
+ },
+
+ /**
+ * For certain folder commands, the thread pane needs to be invalidated, this
+ * takes care of doing so.
+ */
+ _resetThreadPane() {
+ if (gDBView)
+ gCurrentlyDisplayedMessage = gDBView.currentlyDisplayedMessage;
+
+ ClearThreadPaneSelection();
+ ClearThreadPane();
+ ClearMessagePane();
+ },
+
+ /**
+ * Prompts for confirmation, if the user hasn't already chosen the "don't ask
+ * again" option.
+ *
+ * @param aCommand - the command to prompt for
+ */
+ _checkConfirmationPrompt(aCommand) {
+ const kDontAskAgainPref = "mailnews." + aCommand + ".dontAskAgain";
+ // default to ask user if the pref is not set
+ if (!Services.prefs.getBoolPref(kDontAskAgainPref, false)) {
+ let checkbox = {value: false};
+ let choice = Services.prompt.confirmEx(
+ window,
+ gMessengerBundle.getString(aCommand + "Title"),
+ gMessengerBundle.getString(aCommand + "Message"),
+ Services.prompt.STD_YES_NO_BUTTONS,
+ null, null, null,
+ gMessengerBundle.getString(aCommand + "DontAsk"),
+ checkbox);
+ if (checkbox.value)
+ Services.prefs.setBoolPref(kDontAskAgainPref, true);
+
+ if (choice != 0)
+ return false;
+ }
+ return true;
+ },
+}
+
+/**
+ * Sorts the passed in array of folder items using the folder sort key
+ *
+ * @param aFolders - the array of ftvItems to sort.
+ */
+function sortFolderItems (aFtvItems) {
+ function sorter(a, b) {
+ return a._folder.compareSortKeys(b._folder);
+ }
+ aFtvItems.sort(sorter);
+}
+
+var gFolderStatsHelpers = {
+ kUnknownSize: "-",
+ sumSubfoldersPref: false,
+ sumSubfolders: false,
+ sizeUnits: "",
+ kiloUnit: "KB",
+ megaUnit: "MB",
+
+ init: function() {
+ // We cache these values because the cells in the folder pane columns
+ // using these helpers can be redrawn often.
+ this.sumSubfoldersPref = Services.prefs.getBoolPref("mail.folderpane.sumSubfolders");
+ this.sizeUnits = Services.prefs.getCharPref("mail.folderpane.sizeUnits");
+ this.kiloUnit = gFolderTreeView.messengerBundle.getString("kiloByteAbbreviation2");
+ this.megaUnit = gFolderTreeView.messengerBundle.getString("megaByteAbbreviation2");
+ },
+
+ /**
+ * Add a prefix to denote the value is actually a sum of all the subfolders.
+ * The prefix is useful as this sum may not always be the exact sum of
+ * individual folders when they are shown expanded (due to rounding to a
+ * unit).
+ * E.g. folder1 600bytes -> 1KB, folder2 700bytes -> 1KB
+ * summarized at parent folder: 1300bytes -> 1KB
+ *
+ * @param aValue The value to be displayed.
+ * @param aSubfoldersContributed Boolean indicating whether subfolders
+ * contributed to the accumulated total value.
+ */
+ addSummarizedPrefix: function(aValue, aSubfoldersContributed) {
+ if (!this.sumSubfolders)
+ return aValue;
+
+ if (!aSubfoldersContributed)
+ return aValue;
+
+ return gFolderTreeView.messengerBundle.getFormattedString("folderSummarizedSymbolValue", [aValue]);
+ },
+
+ /**
+ * nsIMsgFolder uses -1 as a magic number to mean "I don't know". In those
+ * cases we indicate it to the user. The user has to open the folder
+ * so that the property is initialized from the DB.
+ *
+ * @param aNumber The number to translate for the user.
+ * @param aSubfoldersContributed Boolean indicating whether subfolders
+ * contributed to the accumulated total value.
+ */
+ fixNum: function(aNumber, aSubfoldersContributed) {
+ if (aNumber < 0)
+ return this.kUnknownSize;
+
+ return (aNumber == 0 ? ""
+ : this.addSummarizedPrefix(aNumber,
+ aSubfoldersContributed));
+ },
+
+ /**
+ * Get the size of the specified folder.
+ *
+ * @param aFolder The nsIMsgFolder to analyze.
+ */
+ getFolderSize: function(aFolder) {
+ let folderSize = 0;
+ try {
+ folderSize = aFolder.sizeOnDisk;
+ if (folderSize < 0)
+ return this.kUnknownSize;
+ } catch(ex) {
+ return this.kUnknownSize;
+ }
+ return folderSize;
+ },
+
+ /**
+ * Get the total size of all subfolders of the specified folder.
+ *
+ * @param aFolder The nsIMsgFolder to analyze.
+ */
+ getSubfoldersSize: function(aFolder) {
+ let folderSize = 0;
+ if (aFolder.hasSubFolders) {
+ let subFolders = aFolder.subFolders;
+ while (subFolders.hasMoreElements()) {
+ let subFolder = subFolders.getNext().QueryInterface(Ci.nsIMsgFolder);
+ let subSize = this.getFolderSize(subFolder);
+ let subSubSize = this.getSubfoldersSize(subFolder);
+ if (subSize == this.kUnknownSize || subSubSize == this.kUnknownSize)
+ return subSize;
+
+ folderSize += subSize + subSubSize;
+ }
+ }
+ return folderSize;
+ },
+
+ /**
+ * Format the given folder size into a string with an appropriate unit.
+ *
+ * @param aSize The size in bytes to format.
+ * @param aUnit Optional unit to use for the format.
+ * Possible values are "KB" or "MB".
+ * @return An array with 2 values.
+ * First is the resulting formatted strings.
+ * The second one is the final unit used to format the string.
+ */
+ formatFolderSize: function(aSize, aUnit = gFolderStatsHelpers.sizeUnits) {
+ let size = Math.round(aSize / 1024);
+ let unit = gFolderStatsHelpers.kiloUnit;
+ // If size is non-zero try to show it in a unit that fits in 3 digits,
+ // but if user specified a fixed unit, use that.
+ if (aUnit != "KB" && (size > 999 || aUnit == "MB")) {
+ size = Math.round(size / 1024);
+ unit = gFolderStatsHelpers.megaUnit;
+ aUnit = "MB";
+ }
+ // This needs to be updated if the "%.*f" placeholder string
+ // in "*ByteAbbreviation2" in messenger.properties changes.
+ return [unit.replace("%.*f", size).replace(" ",""), aUnit];
+ }
+};
diff --git a/comm/suite/mailnews/content/folderPane.xul b/comm/suite/mailnews/content/folderPane.xul
new file mode 100644
index 0000000000..c56c799071
--- /dev/null
+++ b/comm/suite/mailnews/content/folderPane.xul
@@ -0,0 +1,169 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPaneExtras.css" type="text/css"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % folderpaneDTD SYSTEM "chrome://messenger/locale/folderpane.dtd">
+ %folderpaneDTD;
+ <!ENTITY % msgViewPickerDTD SYSTEM "chrome://messenger/locale/msgViewPickerOverlay.dtd">
+ %msgViewPickerDTD;
+]>
+
+<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <tree id="folderTree"
+ class="plain focusring window-focusborder"
+ flex="1"
+ treelines="true"
+ persist="mode"
+ mode="all"
+ keepcurrentinview="true"
+ context="folderPaneContext"
+ focusring="false"
+ disableKeyNavigation="true"
+ ondragstart="gFolderTreeView._onDragStart(event);"
+ ondragover="gFolderTreeView._onDragOver(event);"
+ ondrop="gFolderTreeView._onDragDrop(event);"
+ ondblclick="gFolderTreeView.onDoubleClick(event);"
+ onkeypress="gFolderTreeView.onKeyPress(event);"
+ onselect="FolderPaneSelectionChange();">
+ <treecols id="folderPaneCols">
+ <treecol id="folderNameCol"
+ flex="5"
+ label="&nameColumn.label;"
+ crop="center"
+ persist="width"
+ ignoreincolumnpicker="true"
+ primary="true"
+ sortActive="true"
+ sortDirection="ascending"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="folderUnreadCol"
+ hidden="true"
+ persist="hidden width"
+ flex="1"
+ label="&unreadColumn.label;"
+ selectable="false"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="folderTotalCol"
+ hidden="true"
+ persist="hidden width"
+ flex="1"
+ label="&totalColumn.label;"
+ selectable="false"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="folderSizeCol"
+ hidden="true"
+ persist="hidden width"
+ flex="1"
+ label="&folderSizeColumn.label;"
+ selectable="false"/>
+ </treecols>
+ </tree>
+
+ <toolbarpalette id="MailToolbarPalette">
+ <toolbaritem id="folder-location-container"
+ title="&folderLocationToolbarItem.title;"
+ align="center"
+ context="folderPaneContext"
+ class="toolbaritem-noline chromeclass-toolbar-additional">
+ <image id="locationIcon" class="folderMenuItem"/>
+ <menulist id="locationFolders"
+ class="folderMenuItem"
+ label=" "
+ crop="center">
+ <menupopup id="folderLocationPopup"
+ class="menulist-menupopup"
+ type="folder"
+ flex="1"
+ mode="notDeferred"
+ showFileHereLabel="true"
+ oncommand="gFolderTreeView.selectFolder(event.target._folder, true);"/>
+ </menulist>
+ </toolbaritem>
+ <toolbaritem id="mailviews-container"
+ title="&mailViewsToolbarItem.title;"
+ observes="mailDisableViewsSearch"
+ align="center"
+ class="toolbaritem-noline chromeclass-toolbar-additional">
+ <label id="viewPickerLabel"
+ value="&viewPicker.label;"
+ accesskey="&viewPicker.accesskey;"
+ control="viewPicker">
+ <observes element="mailviews-container" attribute="disabled"/>
+ </label>
+ <menulist id="viewPicker"
+ oncommand="ViewChangeByMenuitem(event.target);">
+ <menupopup id="viewPickerPopup"
+ onpopupshowing="RefreshViewPopup(this);">
+ <menuitem id="viewPickerAll"
+ class="menuitem-iconic"
+ label="&viewAll.label;"
+ type="radio"
+ name="viewmessages"
+ value="0"/>
+ <menuitem id="viewPickerUnread"
+ class="menuitem-iconic"
+ label="&viewUnread.label;"
+ type="radio"
+ name="viewmessages"
+ value="1"/>
+ <menuitem id="viewPickerNotDeleted"
+ class="menuitem-iconic"
+ label="&viewNotDeleted.label;"
+ type="radio"
+ name="viewmessages"
+ value="3"/>
+ <menuseparator id="afterViewPickerUnreadSeparator"/>
+ <menu id="viewPickerTags"
+ class="menu-iconic"
+ label="&viewTags.label;">
+ <menupopup id="viewPickerTagsPopup"
+ class="menulist-menupopup"
+ onpopupshowing="RefreshTagsPopup(this);"/>
+ </menu>
+ <menu id="viewPickerCustomViews"
+ class="menu-iconic"
+ label="&viewCustomViews.label;">
+ <menupopup id="viewPickerCustomViewsPopup"
+ class="menulist-menupopup"
+ onpopupshowing="RefreshCustomViewsPopup(this);"/>
+ </menu>
+ <menuseparator id="afterViewPickerCustomViewsSeparator"/>
+ <menuitem id="viewPickerVirtualFolder"
+ class="menuitem-iconic"
+ label="&viewVirtualFolder.label;"
+ value="7"/>
+ <menuitem id="viewPickerCustomize"
+ class="menuitem-iconic"
+ label="&viewCustomizeView.label;"
+ value="8"/>
+ </menupopup>
+ <observes element="mailviews-container" attribute="disabled"/>
+ </menulist>
+ </toolbaritem>
+ <toolbaritem id="search-container"
+ title="&searchToolbarItem.title;"
+ observes="mailDisableViewsSearch"
+ align="center"
+ flex="1"
+ class="toolbaritem-noline chromeclass-toolbar-additional">
+ <textbox id="searchInput"
+ flex="1"
+ type="search"
+ aria-controls="threadTree"
+ placeholder="&searchSubjectOrAddress.placeholder;"
+ clickSelectsAll="true"
+ onkeypress="if (event.keyCode == KeyEvent.DOM_VK_RETURN) this.select();"
+ oncommand="onEnterInSearchBar();">
+ <observes element="search-container" attribute="disabled"/>
+ </textbox>
+ </toolbaritem>
+ </toolbarpalette>
+</overlay>
diff --git a/comm/suite/mailnews/content/mail-offline.js b/comm/suite/mailnews/content/mail-offline.js
new file mode 100644
index 0000000000..506c198c9a
--- /dev/null
+++ b/comm/suite/mailnews/content/mail-offline.js
@@ -0,0 +1,164 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var gOfflinePromptsBundle;
+var gOfflineManager;
+
+function MailOfflineStateChanged(goingOffline)
+{
+ // tweak any mail UI here that needs to change when we go offline or come back online
+ gFolderJustSwitched = true;
+}
+
+function MsgSettingsOffline()
+{
+ window.parent.MsgAccountManager('am-offline.xul');
+}
+
+// Check for unsent messages
+function CheckForUnsentMessages()
+{
+ return Cc["@mozilla.org/messengercompose/sendlater;1"]
+ .getService(Ci.nsIMsgSendLater)
+ .hasUnsentMessages();
+}
+
+// Init strings.
+function InitPrompts()
+{
+ if (!gOfflinePromptsBundle)
+ gOfflinePromptsBundle = document.getElementById("bundle_offlinePrompts");
+}
+
+// prompt for sending messages while going online, and go online.
+function PromptSendMessages()
+{
+ InitPrompts();
+ InitServices();
+
+ var checkValue = {value:true};
+ var buttonPressed = Services.prompt.confirmEx(
+ window,
+ gOfflinePromptsBundle.getString('sendMessagesWindowTitle'),
+ gOfflinePromptsBundle.getString('sendMessagesLabel2'),
+ Services.prompt.BUTTON_TITLE_IS_STRING * (Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_POS_1 + Services.prompt.BUTTON_POS_2),
+ gOfflinePromptsBundle.getString('sendMessagesSendButtonLabel'),
+ gOfflinePromptsBundle.getString('sendMessagesCancelButtonLabel'),
+ gOfflinePromptsBundle.getString('sendMessagesNoSendButtonLabel'),
+ gOfflinePromptsBundle.getString('sendMessagesCheckboxLabel'),
+ checkValue);
+ switch (buttonPressed) {
+ case 0:
+ Services.prefs.setIntPref("offline.send.unsent_messages", !checkValue.value);
+ gOfflineManager.goOnline(true, true, msgWindow);
+ return true;
+
+ case 2:
+ Services.prefs.setIntPref("offline.send.unsent_messages", 2*!checkValue.value);
+ gOfflineManager.goOnline(false, true, msgWindow);
+ return true;
+ }
+ return false;
+}
+
+// prompt for downlading messages while going offline, and synchronise
+function PromptDownloadMessages()
+{
+ InitPrompts();
+ InitServices();
+
+ var checkValue = {value:true};
+ var buttonPressed = Services.prompt.confirmEx(
+ window,
+ gOfflinePromptsBundle.getString('downloadMessagesWindowTitle'),
+ gOfflinePromptsBundle.getString('downloadMessagesLabel'),
+ Services.prompt.BUTTON_TITLE_IS_STRING * (Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_POS_1 + Services.prompt.BUTTON_POS_2),
+ gOfflinePromptsBundle.getString('downloadMessagesDownloadButtonLabel'),
+ gOfflinePromptsBundle.getString('downloadMessagesCancelButtonLabel'),
+ gOfflinePromptsBundle.getString('downloadMessagesNoDownloadButtonLabel'),
+ gOfflinePromptsBundle.getString('downloadMessagesCheckboxLabel'),
+ checkValue);
+ switch (buttonPressed) {
+ case 0:
+ Services.prefs.setIntPref("offline.download.download_messages", !checkValue.value);
+ gOfflineManager.synchronizeForOffline(true, true, false, true, msgWindow);
+ return true;
+
+ case 2:
+ Services.prefs.setIntPref("offline.download.download_messages", 2*!checkValue.value);
+ gOfflineManager.synchronizeForOffline(false, false, false, true, msgWindow);
+ return true;
+ }
+ return false;
+}
+
+// Init Pref Service & Offline Manager
+function InitServices()
+{
+ if (!gOfflineManager)
+ GetOfflineMgrService();
+}
+
+// Init Offline Manager
+function GetOfflineMgrService()
+{
+ if (!gOfflineManager) {
+ gOfflineManager = Cc["@mozilla.org/messenger/offline-manager;1"]
+ .getService(Ci.nsIMsgOfflineManager);
+ }
+}
+
+// This function must always return false to prevent toggling of offline state because
+// we change the offline state ourselves
+function MailCheckBeforeOfflineChange()
+{
+ InitServices();
+
+
+ if (Services.io.offline) {
+ switch(Services.prefs.getIntPref("offline.send.unsent_messages")) {
+ case 0:
+ if(CheckForUnsentMessages()) {
+ if(! PromptSendMessages())
+ return false;
+ }
+ else
+ gOfflineManager.goOnline(false /* sendUnsentMessages */,
+ true /* playbackOfflineImapOperations */,
+ msgWindow);
+ break;
+ case 1:
+ gOfflineManager.goOnline(CheckForUnsentMessages() /* sendUnsentMessages */,
+ true /* playbackOfflineImapOperations */,
+ msgWindow);
+ break;
+ case 2:
+ gOfflineManager.goOnline(false /* sendUnsentMessages */,
+ true /* playbackOfflineImapOperations */,
+ msgWindow);
+ break;
+ }
+ }
+ else {
+ // going offline
+ switch(Services.prefs.getIntPref("offline.download.download_messages")) {
+ case 0:
+ if(! PromptDownloadMessages()) return false;
+ break;
+ case 1:
+ // download news, download mail, send unsent messages, go offline when done, msg window
+ gOfflineManager.synchronizeForOffline(true, true, false, true, msgWindow);
+ break;
+ case 2:
+ // download news, download mail, send unsent messages, go offline when done, msg window
+ gOfflineManager.synchronizeForOffline(false, false, false, true, msgWindow);
+ break;
+ }
+ }
+ return false;
+}
+
diff --git a/comm/suite/mailnews/content/mail3PaneWindowCommands.js b/comm/suite/mailnews/content/mail3PaneWindowCommands.js
new file mode 100644
index 0000000000..6bd9f762d4
--- /dev/null
+++ b/comm/suite/mailnews/content/mail3PaneWindowCommands.js
@@ -0,0 +1,1057 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/**
+ * Functionality for the main application window (aka the 3pane) usually
+ * consisting of folder pane, thread pane and message pane.
+ */
+
+const { MailServices } =
+ ChromeUtils.import("resource:///modules/MailServices.jsm");
+
+// Controller object for folder pane
+var FolderPaneController =
+{
+ supportsCommand: function(command)
+ {
+ switch ( command )
+ {
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ case "button_delete":
+ case "button_shiftDelete":
+ // Even if the folder pane has focus, don't do a folder delete if
+ // we have a selected message, but do a message delete instead.
+ // Return false here supportsCommand and let the command fall back
+ // to the DefaultController.
+ if (Services.prefs.getBoolPref("mailnews.ui.deleteAlwaysSelectedMessages") && (gFolderDisplay.selectedCount != 0))
+ return false;
+ // else fall through
+ //case "cmd_selectAll": the folder pane currently only handles single selection
+ case "cmd_cut":
+ case "cmd_copy":
+ case "cmd_paste":
+ return true;
+
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled: function(command)
+ {
+ switch ( command )
+ {
+ case "cmd_cut":
+ case "cmd_copy":
+ case "cmd_paste":
+ return false;
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ case "button_delete":
+ case "button_shiftDelete":
+ {
+ // Make sure the button doesn't show "Undelete" for folders.
+ UpdateDeleteToolbarButton(true);
+ let folders = GetSelectedMsgFolders();
+ if (folders.length) {
+ let folder = folders[0];
+ // XXX Figure out some better way/place to update the folder labels.
+ UpdateDeleteLabelsFromFolderCommand(folder, command);
+ return CanDeleteFolder(folder) && folder.isCommandEnabled(command);
+ }
+ return false;
+ }
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function(command)
+ {
+ // if the user invoked a key short cut then it is possible that we got here for a command which is
+ // really disabled. kick out if the command should be disabled.
+ if (!this.isCommandEnabled(command)) return;
+
+ switch ( command )
+ {
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ case "button_delete":
+ case "button_shiftDelete":
+ gFolderTreeController.deleteFolder();
+ break;
+ }
+ },
+
+ onEvent: function(event)
+ {
+ }
+};
+
+function UpdateDeleteLabelsFromFolderCommand(folder, command) {
+ if (command != "cmd_delete")
+ return;
+
+ if (folder.server.type == "nntp" &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
+ goSetMenuValue(command, "valueNewsgroup");
+ goSetAccessKey(command, "valueNewsgroupAccessKey");
+ }
+ else {
+ goSetMenuValue(command, "valueFolder");
+ }
+}
+
+// DefaultController object (handles commands when one of the trees does not have focus)
+var DefaultController =
+{
+ supportsCommand: function(command)
+ {
+
+ switch ( command )
+ {
+ case "cmd_createFilterFromPopup":
+ case "cmd_archive":
+ case "cmd_reply":
+ case "button_reply":
+ case "cmd_replyList":
+ case "cmd_replyGroup":
+ case "cmd_replySender":
+ case "cmd_replyall":
+ case "button_replyall":
+ case "cmd_replySenderAndGroup":
+ case "cmd_replyAllRecipients":
+ case "cmd_forward":
+ case "button_forward":
+ case "cmd_forwardInline":
+ case "cmd_forwardAttachment":
+ case "cmd_editAsNew":
+ case "cmd_editDraftMsg":
+ case "cmd_newMsgFromTemplate":
+ case "cmd_editTemplateMsg":
+ case "cmd_createFilterFromMenu":
+ case "cmd_delete":
+ case "cmd_shiftDelete":
+ case "button_delete":
+ case "button_shiftDelete":
+ case "button_junk":
+ case "cmd_nextMsg":
+ case "button_next":
+ case "cmd_nextUnreadMsg":
+ case "cmd_nextFlaggedMsg":
+ case "cmd_nextUnreadThread":
+ case "cmd_previousMsg":
+ case "cmd_previousUnreadMsg":
+ case "cmd_previousFlaggedMsg":
+ case "button_goBack":
+ case "cmd_goBack":
+ case "button_goForward":
+ case "cmd_goForward":
+ case "cmd_goStartPage":
+ case "cmd_viewAllMsgs":
+ case "cmd_viewUnreadMsgs":
+ case "cmd_viewThreadsWithUnread":
+ case "cmd_viewWatchedThreadsWithUnread":
+ case "cmd_viewIgnoredThreads":
+ case "cmd_stop":
+ case "cmd_undo":
+ case "cmd_redo":
+ case "cmd_expandAllThreads":
+ case "cmd_collapseAllThreads":
+ case "cmd_renameFolder":
+ case "cmd_sendUnsentMsgs":
+ case "cmd_subscribe":
+ case "cmd_openMessage":
+ case "button_print":
+ case "cmd_print":
+ case "cmd_printpreview":
+ case "cmd_printSetup":
+ case "cmd_saveAsFile":
+ case "cmd_saveAsTemplate":
+ case "cmd_properties":
+ case "cmd_viewPageSource":
+ case "cmd_setFolderCharset":
+ case "cmd_reload":
+ case "button_getNewMessages":
+ case "cmd_getNewMessages":
+ case "cmd_getMsgsForAuthAccounts":
+ case "cmd_getNextNMessages":
+ case "cmd_find":
+ case "cmd_findNext":
+ case "cmd_findPrev":
+ case "button_search":
+ case "cmd_search":
+ case "button_mark":
+ case "cmd_markAsRead":
+ case "cmd_markAsUnread":
+ case "cmd_markAllRead":
+ case "cmd_markThreadAsRead":
+ case "cmd_markReadByDate":
+ case "cmd_markAsFlagged":
+ case "cmd_markAsJunk":
+ case "cmd_markAsNotJunk":
+ case "cmd_recalculateJunkScore":
+ case "cmd_markAsShowRemote":
+ case "cmd_markAsNotPhish":
+ case "cmd_displayMsgFilters":
+ case "cmd_applyFiltersToSelection":
+ case "cmd_applyFilters":
+ case "cmd_runJunkControls":
+ case "cmd_deleteJunk":
+ case "button_file":
+ case "cmd_emptyTrash":
+ case "cmd_compactFolder":
+ case "cmd_settingsOffline":
+ case "cmd_selectAll":
+ case "cmd_selectThread":
+ case "cmd_selectFlagged":
+ case "cmd_viewAllHeader":
+ case "cmd_viewNormalHeader":
+ return true;
+ case "cmd_downloadFlagged":
+ case "cmd_downloadSelected":
+ case "cmd_synchronizeOffline":
+ return !Services.io.offline;
+ case "cmd_watchThread":
+ case "cmd_killThread":
+ case "cmd_killSubthread":
+ case "cmd_cancel":
+ return gFolderDisplay.selectedMessageIsNews;
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled: function(command)
+ {
+ var enabled = new Object();
+ enabled.value = false;
+ var checkStatus = new Object();
+
+ switch ( command )
+ {
+ case "cmd_delete":
+ UpdateDeleteCommand();
+ // fall through
+ case "button_delete":
+ if (command == "button_delete")
+ UpdateDeleteToolbarButton(false);
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.deleteMsg, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_shiftDelete":
+ case "button_shiftDelete":
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.deleteNoTrash, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_cancel":
+ return GetNumSelectedMessages() == 1 &&
+ gFolderDisplay.selectedMessageIsNews;
+ case "button_junk":
+ UpdateJunkToolbarButton();
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.junk, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_killThread":
+ case "cmd_killSubthread":
+ return GetNumSelectedMessages() > 0;
+ case "cmd_watchThread":
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.toggleThreadWatched, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_createFilterFromPopup":
+ case "cmd_createFilterFromMenu":
+ var loadedFolder = GetLoadedMsgFolder();
+ if (!(loadedFolder && loadedFolder.server.canHaveFilters))
+ return false; // else fall thru
+ case "cmd_saveAsFile":
+ return GetNumSelectedMessages() > 0;
+ case "cmd_saveAsTemplate":
+ var msgFolder = GetSelectedMsgFolders();
+ var target = msgFolder[0].server.localStoreType;
+ if (GetNumSelectedMessages() == 0 || target == "news")
+ return false; // else fall thru
+ case "cmd_reply":
+ case "button_reply":
+ case "cmd_replyList":
+ case "cmd_replyGroup":
+ case "cmd_replySender":
+ case "cmd_replyall":
+ case "button_replyall":
+ case "cmd_replySenderAndGroup":
+ case "cmd_replyAllRecipients":
+ case "cmd_forward":
+ case "button_forward":
+ case "cmd_forwardInline":
+ case "cmd_forwardAttachment":
+ case "cmd_editAsNew":
+ case "cmd_editDraftMsg":
+ case "cmd_newMsgFromTemplate":
+ case "cmd_editTemplateMsg":
+ case "cmd_openMessage":
+ case "button_print":
+ case "cmd_print":
+ case "cmd_viewPageSource":
+ case "cmd_reload":
+ case "cmd_applyFiltersToSelection":
+ if (command == "cmd_applyFiltersToSelection")
+ {
+ var whichText = "valueMessage";
+ if (GetNumSelectedMessages() > 1)
+ whichText = "valueSelection";
+ goSetMenuValue(command, whichText);
+ goSetAccessKey(command, whichText + "AccessKey");
+ }
+ if (GetNumSelectedMessages() > 0)
+ {
+ if (gDBView)
+ {
+ gDBView.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody, enabled, checkStatus);
+ return enabled.value;
+ }
+ }
+ return false;
+ case "cmd_printpreview":
+ if ( GetNumSelectedMessages() == 1 && gDBView)
+ {
+ gDBView.getCommandStatus(nsMsgViewCommandType.cmdRequiringMsgBody, enabled, checkStatus);
+ return enabled.value;
+ }
+ return false;
+ case "cmd_printSetup":
+ case "cmd_viewAllHeader":
+ case "cmd_viewNormalHeader":
+ return true;
+ case "cmd_markAsFlagged":
+ case "button_file":
+ return GetNumSelectedMessages() > 0;
+ case "cmd_archive":
+ return gFolderDisplay.canArchiveSelectedMessages;
+ case "cmd_markAsJunk":
+ case "cmd_markAsNotJunk":
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.junk, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_recalculateJunkScore":
+ // We're going to take a conservative position here, because we really
+ // don't want people running junk controls on folders that are not
+ // enabled for junk. The junk type picks up possible dummy message headers,
+ // while the runJunkControls will prevent running on XF virtual folders.
+ if (gDBView)
+ {
+ gDBView.getCommandStatus(nsMsgViewCommandType.runJunkControls, enabled, checkStatus);
+ if (enabled.value)
+ gDBView.getCommandStatus(nsMsgViewCommandType.junk, enabled, checkStatus);
+ }
+ return enabled.value;
+ case "cmd_markAsShowRemote":
+ return (GetNumSelectedMessages() > 0 && checkMsgHdrPropertyIsNot("remoteContentPolicy", kAllowRemoteContent));
+ case "cmd_markAsNotPhish":
+ return (GetNumSelectedMessages() > 0 && checkMsgHdrPropertyIsNot("notAPhishMessage", kNotAPhishMessage));
+ case "cmd_displayMsgFilters":
+ return MailServices.accounts.accounts.length > 0;
+ case "cmd_applyFilters":
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.applyFilters, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_runJunkControls":
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.runJunkControls, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_deleteJunk":
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.deleteJunk, enabled, checkStatus);
+ return enabled.value;
+ case "button_mark":
+ case "cmd_markThreadAsRead":
+ return GetNumSelectedMessages() > 0;
+ case "cmd_markAsRead":
+ return CanMarkMsgAsRead(true);
+ case "cmd_markAsUnread":
+ return CanMarkMsgAsRead(false);
+ case "button_next":
+ return IsViewNavigationItemEnabled();
+ case "cmd_nextMsg":
+ case "cmd_nextUnreadMsg":
+ case "cmd_nextUnreadThread":
+ case "cmd_previousMsg":
+ case "cmd_previousUnreadMsg":
+ return IsViewNavigationItemEnabled();
+ case "button_goBack":
+ case "cmd_goBack":
+ return gDBView && gDBView.navigateStatus(nsMsgNavigationType.back);
+ case "button_goForward":
+ case "cmd_goForward":
+ return gDBView && gDBView.navigateStatus(nsMsgNavigationType.forward);
+ case "cmd_goStartPage":
+ return Services.prefs.getBoolPref("mailnews.start_page.enabled") && !IsMessagePaneCollapsed();
+ case "cmd_markAllRead":
+ return IsFolderSelected() && gDBView && gDBView.msgFolder.getNumUnread(false) > 0;
+ case "cmd_markReadByDate":
+ return IsFolderSelected();
+ case "cmd_find":
+ case "cmd_findNext":
+ case "cmd_findPrev":
+ return IsMessageDisplayedInMessagePane();
+ break;
+ case "button_search":
+ case "cmd_search":
+ return MailServices.accounts.accounts.length > 0;
+ case "cmd_selectAll":
+ case "cmd_selectFlagged":
+ return !!gDBView;
+ // these are enabled on when we are in threaded mode
+ case "cmd_selectThread":
+ if (GetNumSelectedMessages() <= 0) return false;
+ case "cmd_expandAllThreads":
+ case "cmd_collapseAllThreads":
+ return gDBView && (gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay);
+ break;
+ case "cmd_nextFlaggedMsg":
+ case "cmd_previousFlaggedMsg":
+ return IsViewNavigationItemEnabled();
+ case "cmd_viewAllMsgs":
+ case "cmd_viewUnreadMsgs":
+ case "cmd_viewIgnoredThreads":
+ return gDBView;
+ case "cmd_viewThreadsWithUnread":
+ case "cmd_viewWatchedThreadsWithUnread":
+ return gDBView && !(GetSelectedMsgFolders()[0].flags &
+ Ci.nsMsgFolderFlags.Virtual);
+ case "cmd_stop":
+ return true;
+ case "cmd_undo":
+ case "cmd_redo":
+ return SetupUndoRedoCommand(command);
+ case "cmd_renameFolder":
+ {
+ let folders = GetSelectedMsgFolders();
+ return folders.length == 1 && folders[0].canRename &&
+ folders[0].isCommandEnabled("cmd_renameFolder");
+ }
+ case "cmd_sendUnsentMsgs":
+ return IsSendUnsentMsgsEnabled(null);
+ case "cmd_subscribe":
+ return IsSubscribeEnabled();
+ case "cmd_properties":
+ return IsPropertiesEnabled(command);
+ case "button_getNewMessages":
+ case "cmd_getNewMessages":
+ case "cmd_getMsgsForAuthAccounts":
+ return IsGetNewMessagesEnabled();
+ case "cmd_getNextNMessages":
+ return IsGetNextNMessagesEnabled();
+ case "cmd_emptyTrash":
+ {
+ let folder = GetSelectedMsgFolders()[0];
+ return folder && folder.server.canEmptyTrashOnExit ?
+ IsMailFolderSelected() : false;
+ }
+ case "cmd_compactFolder":
+ {
+ let folders = GetSelectedMsgFolders();
+ let canCompactAll = function canCompactAll(folder) {
+ return folder.server.canCompactFoldersOnServer &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Virtual) &&
+ folder.isCommandEnabled("cmd_compactFolder");
+ }
+ return folders && folders.every(canCompactAll);
+ }
+ case "cmd_setFolderCharset":
+ return IsFolderCharsetEnabled();
+ case "cmd_downloadFlagged":
+ return !Services.io.offline;
+ case "cmd_downloadSelected":
+ return IsFolderSelected() && !Services.io.offline &&
+ GetNumSelectedMessages() > 0;
+ case "cmd_synchronizeOffline":
+ return !Services.io.offline;
+ case "cmd_settingsOffline":
+ return IsAccountOfflineEnabled();
+ default:
+ return false;
+ }
+ return false;
+ },
+
+ doCommand: function(command)
+ {
+ // if the user invoked a key short cut then it is possible that we got here for a command which is
+ // really disabled. kick out if the command should be disabled.
+ if (!this.isCommandEnabled(command))
+ return;
+
+ switch (command)
+ {
+ case "button_getNewMessages":
+ case "cmd_getNewMessages":
+ MsgGetMessage();
+ break;
+ case "cmd_getMsgsForAuthAccounts":
+ MsgGetMessagesForAllAuthenticatedAccounts();
+ break;
+ case "cmd_getNextNMessages":
+ MsgGetNextNMessages();
+ break;
+ case "cmd_archive":
+ MsgArchiveSelectedMessages(null);
+ break;
+ case "cmd_reply":
+ MsgReplyMessage(null);
+ break;
+ case "cmd_replyList":
+ MsgReplyList(null);
+ break;
+ case "cmd_replyGroup":
+ MsgReplyGroup(null);
+ break;
+ case "cmd_replySender":
+ MsgReplySender(null);
+ break;
+ case "cmd_replyall":
+ MsgReplyToAllMessage(null);
+ break;
+ case "cmd_replySenderAndGroup":
+ MsgReplyToSenderAndGroup(null);
+ break;
+ case "cmd_replyAllRecipients":
+ MsgReplyToAllRecipients(null);
+ break;
+ case "cmd_forward":
+ MsgForwardMessage(null);
+ break;
+ case "cmd_forwardInline":
+ MsgForwardAsInline(null);
+ break;
+ case "cmd_forwardAttachment":
+ MsgForwardAsAttachment(null);
+ break;
+ case "cmd_editAsNew":
+ MsgEditMessageAsNew(null);
+ break;
+ case "cmd_editDraftMsg":
+ MsgEditDraftMessage(null);
+ break;
+ case "cmd_newMsgFromTemplate":
+ MsgNewMessageFromTemplate(null);
+ break;
+ case "cmd_editTemplateMsg":
+ MsgEditTemplateMessage(null);
+ break;
+ case "cmd_createFilterFromMenu":
+ MsgCreateFilter();
+ break;
+ case "cmd_createFilterFromPopup":
+ CreateFilter(document.popupNode);
+ break;
+ case "cmd_delete":
+ case "button_delete":
+ MsgDeleteMessage(false);
+ UpdateDeleteToolbarButton(false);
+ break;
+ case "cmd_shiftDelete":
+ case "button_shiftDelete":
+ MsgDeleteMessage(true);
+ UpdateDeleteToolbarButton(false);
+ break;
+ case "cmd_cancel":
+ let message = gFolderDisplay.selectedMessage;
+ message.folder.QueryInterface(Ci.nsIMsgNewsFolder)
+ .cancelMessage(message, msgWindow);
+ break;
+ case "cmd_killThread":
+ /* kill thread kills the thread and then does a next unread */
+ GoNextMessage(nsMsgNavigationType.toggleThreadKilled, true);
+ break;
+ case "cmd_killSubthread":
+ GoNextMessage(nsMsgNavigationType.toggleSubthreadKilled, true);
+ break;
+ case "cmd_watchThread":
+ gDBView.doCommand(nsMsgViewCommandType.toggleThreadWatched);
+ break;
+ case "button_next":
+ case "cmd_nextUnreadMsg":
+ GoNextMessage(nsMsgNavigationType.nextUnreadMessage, true);
+ break;
+ case "cmd_nextUnreadThread":
+ GoNextMessage(nsMsgNavigationType.nextUnreadThread, true);
+ break;
+ case "cmd_nextMsg":
+ GoNextMessage(nsMsgNavigationType.nextMessage, false);
+ break;
+ case "cmd_nextFlaggedMsg":
+ GoNextMessage(nsMsgNavigationType.nextFlagged, true);
+ break;
+ case "cmd_previousMsg":
+ GoNextMessage(nsMsgNavigationType.previousMessage, false);
+ break;
+ case "cmd_previousUnreadMsg":
+ GoNextMessage(nsMsgNavigationType.previousUnreadMessage, true);
+ break;
+ case "cmd_previousFlaggedMsg":
+ GoNextMessage(nsMsgNavigationType.previousFlagged, true);
+ break;
+ case "button_goForward":
+ case "cmd_goForward":
+ GoNextMessage(nsMsgNavigationType.forward, true);
+ break;
+ case "button_goBack":
+ case "cmd_goBack":
+ GoNextMessage(nsMsgNavigationType.back, true);
+ break;
+ case "cmd_goStartPage":
+ HideMessageHeaderPane();
+ loadStartPage();
+ break;
+ case "cmd_viewAllMsgs":
+ case "cmd_viewThreadsWithUnread":
+ case "cmd_viewWatchedThreadsWithUnread":
+ case "cmd_viewUnreadMsgs":
+ case "cmd_viewIgnoredThreads":
+ SwitchView(command);
+ break;
+ case "cmd_undo":
+ messenger.undo(msgWindow);
+ break;
+ case "cmd_redo":
+ messenger.redo(msgWindow);
+ break;
+ case "cmd_expandAllThreads":
+ gDBView.doCommand(nsMsgViewCommandType.expandAll);
+ break;
+ case "cmd_collapseAllThreads":
+ gDBView.doCommand(nsMsgViewCommandType.collapseAll);
+ break;
+ case "cmd_renameFolder":
+ gFolderTreeController.renameFolder();
+ return;
+ case "cmd_sendUnsentMsgs":
+ MsgSendUnsentMsgs();
+ return;
+ case "cmd_subscribe":
+ MsgSubscribe();
+ return;
+ case "cmd_openMessage":
+ MsgOpenSelectedMessages();
+ return;
+ case "cmd_printSetup":
+ PrintUtils.showPageSetup();
+ return;
+ case "cmd_print":
+ PrintEnginePrint();
+ return;
+ case "cmd_printpreview":
+ PrintEnginePrintPreview();
+ return;
+ case "cmd_saveAsFile":
+ MsgSaveAsFile();
+ return;
+ case "cmd_saveAsTemplate":
+ MsgSaveAsTemplate();
+ return;
+ case "cmd_viewPageSource":
+ MsgViewPageSource();
+ return;
+ case "cmd_setFolderCharset":
+ gFolderTreeController.editFolder();
+ return;
+ case "cmd_reload":
+ ReloadMessage();
+ return;
+ case "cmd_find":
+ MsgFind();
+ return;
+ case "cmd_findNext":
+ MsgFindAgain(false);
+ return;
+ case "cmd_findPrev":
+ MsgFindAgain(true);
+ return;
+ case "cmd_properties":
+ gFolderTreeController.editFolder();
+ return;
+ case "button_search":
+ case "cmd_search":
+ MsgSearchMessages();
+ return;
+ case "button_mark":
+ MsgMarkMsgAsRead();
+ return;
+ case "cmd_markAsRead":
+ MsgMarkMsgAsRead(true);
+ return;
+ case "cmd_markAsUnread":
+ MsgMarkMsgAsRead(false);
+ return;
+ case "cmd_markThreadAsRead":
+ MsgMarkThreadAsRead();
+ return;
+ case "cmd_markAllRead":
+ gDBView.doCommand(nsMsgViewCommandType.markAllRead);
+ return;
+ case "cmd_markReadByDate":
+ MsgMarkReadByDate();
+ return;
+ case "button_junk":
+ MsgJunk();
+ return;
+ case "cmd_stop":
+ msgWindow.StopUrls();
+ return;
+ case "cmd_markAsFlagged":
+ MsgMarkAsFlagged();
+ return;
+ case "cmd_viewAllHeader":
+ MsgViewAllHeaders();
+ return;
+ case "cmd_viewNormalHeader":
+ MsgViewNormalHeaders();
+ return;
+ case "cmd_markAsJunk":
+ JunkSelectedMessages(true);
+ return;
+ case "cmd_markAsNotJunk":
+ JunkSelectedMessages(false);
+ return;
+ case "cmd_recalculateJunkScore":
+ analyzeMessagesForJunk();
+ return;
+ case "cmd_markAsShowRemote":
+ LoadMsgWithRemoteContent();
+ return;
+ case "cmd_markAsNotPhish":
+ MsgIsNotAScam();
+ return;
+ case "cmd_displayMsgFilters":
+ MsgFilters(null, null);
+ return;
+ case "cmd_applyFiltersToSelection":
+ MsgApplyFiltersToSelection();
+ return;
+ case "cmd_applyFilters":
+ MsgApplyFilters(null);
+ return;
+ case "cmd_runJunkControls":
+ filterFolderForJunk();
+ return;
+ case "cmd_deleteJunk":
+ deleteJunkInFolder();
+ return;
+ case "cmd_emptyTrash":
+ gFolderTreeController.emptyTrash();
+ return;
+ case "cmd_compactFolder":
+ gFolderTreeController.compactAllFoldersForAccount();
+ return;
+ case "cmd_downloadFlagged":
+ MsgDownloadFlagged();
+ break;
+ case "cmd_downloadSelected":
+ MsgDownloadSelected();
+ break;
+ case "cmd_synchronizeOffline":
+ MsgSynchronizeOffline();
+ break;
+ case "cmd_settingsOffline":
+ MsgSettingsOffline();
+ break;
+ case "cmd_selectAll":
+ // move the focus so the user can delete the newly selected messages, not the folder
+ SetFocusThreadPane();
+ // if in threaded mode, the view will expand all before selecting all
+ gDBView.doCommand(nsMsgViewCommandType.selectAll)
+ if (gDBView.numSelected != 1) {
+ setTitleFromFolder(gDBView.msgFolder,null);
+ ClearMessagePane();
+ }
+ break;
+ case "cmd_selectThread":
+ gDBView.doCommand(nsMsgViewCommandType.selectThread);
+ break;
+ case "cmd_selectFlagged":
+ gDBView.doCommand(nsMsgViewCommandType.selectFlagged);
+ break;
+ }
+ },
+
+ onEvent: function(event)
+ {
+ // on blur events set the menu item texts back to the normal values
+ if ( event == 'blur' )
+ {
+ goSetMenuValue('cmd_undo', 'valueDefault');
+ goSetMenuValue('cmd_redo', 'valueDefault');
+ }
+ }
+};
+
+function MsgCloseTabOrWindow()
+{
+ var tabmail = GetTabMail();
+ if (tabmail.tabInfo.length > 1)
+ tabmail.removeCurrentTab();
+ else
+ window.close();
+}
+
+function GetNumSelectedMessages()
+{
+ return gDBView ? gDBView.numSelected : 0;
+}
+
+var gLastFocusedElement=null;
+
+function FocusRingUpdate_Mail()
+{
+ // If the focusedElement is null, we're here on a blur.
+ // nsFocusController::Blur() calls nsFocusController::SetFocusedElement(null),
+ // which will update any commands listening for "focus".
+ // we really only care about nsFocusController::Focus() happens,
+ // which calls nsFocusController::SetFocusedElement(element)
+ var currentFocusedElement = gFolderDisplay.focusedPane;
+
+ if (currentFocusedElement != gLastFocusedElement) {
+ if (currentFocusedElement)
+ currentFocusedElement.setAttribute("focusring", "true");
+
+ if (gLastFocusedElement)
+ gLastFocusedElement.removeAttribute("focusring");
+
+ gLastFocusedElement = currentFocusedElement;
+
+ // since we just changed the pane with focus we need to update the toolbar to reflect this
+ // XXX TODO
+ // can we optimize
+ // and just update cmd_delete and button_delete?
+ UpdateMailToolbar("focus");
+ }
+}
+
+function SetupCommandUpdateHandlers()
+{
+ // folder pane
+ var widget = document.getElementById("folderTree");
+ if (widget)
+ widget.controllers.appendController(FolderPaneController);
+}
+
+// Called from <msgMail3PaneWindow.js>.
+function UnloadCommandUpdateHandlers()
+{
+ var widget = document.getElementById("folderTree");
+ if (widget)
+ widget.controllers.removeController(FolderPaneController);
+}
+
+function IsSendUnsentMsgsEnabled(folderResource)
+{
+ var msgSendLater =
+ Cc["@mozilla.org/messengercompose/sendlater;1"]
+ .getService(Ci.nsIMsgSendLater);
+
+ // If we're currently sending unsent msgs, disable this cmd.
+ if (msgSendLater.sendingMessages)
+ return false;
+
+ if (folderResource &&
+ folderResource instanceof Ci.nsIMsgFolder) {
+ // If unsentMsgsFolder is non-null, it is the "Outbox" folder.
+ // We're here because we've done a right click on the "Outbox"
+ // folder (context menu), so we can use the folder and return true/false
+ // straight away.
+ return folderResource.getTotalMessages(false) > 0;
+ }
+
+ // Otherwise, we don't know where we are, so use the current identity and
+ // find out if we have messages or not via that.
+ let identity = null;
+ let folders = GetSelectedMsgFolders();
+ if (folders.length > 0)
+ identity = getIdentityForServer(folders[0].server);
+
+ if (!identity) {
+ let defaultAccount = MailServices.accounts.defaultAccount;
+ if (defaultAccount)
+ identity = defaultAccount.defaultIdentity;
+
+ if (!identity)
+ return false;
+ }
+
+ return msgSendLater.hasUnsentMessages(identity);
+}
+
+/**
+ * Determine whether there exists any server for which to show the Subscribe dialog.
+ */
+function IsSubscribeEnabled()
+{
+ // If there are any IMAP or News servers, we can show the dialog any time and
+ // it will properly show those.
+ for (let server of accountManager.allServers) {
+ if (server.type == "imap" || server.type == "nntp")
+ return true;
+ }
+
+ // RSS accounts use a separate Subscribe dialog that we can only show when
+ // such an account is selected.
+ let preselectedFolder = GetFirstSelectedMsgFolder();
+ if (preselectedFolder && preselectedFolder.server.type == "rss")
+ return true;
+
+ return false;
+}
+
+function IsFolderCharsetEnabled()
+{
+ return IsFolderSelected();
+}
+
+function IsPropertiesEnabled(command)
+{
+ let folders = GetSelectedMsgFolders();
+ if (!folders.length)
+ return false;
+
+ let folder = folders[0];
+ // When servers are selected, it should be "Edit | Properties...".
+ if (folder.isServer) {
+ goSetMenuValue(command, "valueGeneric");
+ } else if (folder.server.type == "nntp" &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
+ goSetMenuValue(command, "valueNewsgroup");
+ } else {
+ goSetMenuValue(command, "valueFolder");
+ }
+
+ return folders.length == 1;
+}
+
+function IsViewNavigationItemEnabled()
+{
+ return IsFolderSelected();
+}
+
+function IsFolderSelected()
+{
+ let folders = GetSelectedMsgFolders();
+ return folders.length == 1 && !folders[0].isServer;
+}
+
+function IsMessageDisplayedInMessagePane()
+{
+ return (!IsMessagePaneCollapsed() && (GetNumSelectedMessages() > 0));
+}
+
+function SetFocusThreadPaneIfNotOnMessagePane()
+{
+ var focusedElement = gFolderDisplay.focusedPane;
+
+ if((focusedElement != GetThreadTree()) &&
+ (focusedElement != GetMessagePane()))
+ SetFocusThreadPane();
+}
+
+function SwitchPaneFocus(event)
+{
+ var folderTree = document.getElementById("folderTree");
+ var threadTree = GetThreadTree();
+ var messagePane = GetMessagePane();
+
+ var folderPaneCollapsed = document.getElementById("folderPaneBox").collapsed;
+
+ // Although internally this is actually a four-pane window, it is presented as
+ // a three-pane -- the search pane is more of a toolbar. So, shift among the
+ // three main panes.
+
+ var focusedElement = gFolderDisplay.focusedPane;
+ if (focusedElement == null) // focus not on one of the main three panes?
+ focusedElement = threadTree; // treat as if on thread tree
+
+ if (event && event.shiftKey)
+ {
+ // Reverse traversal: Message -> Thread -> Folder -> Message
+ if (focusedElement == threadTree && !folderPaneCollapsed)
+ folderTree.focus();
+ else if (focusedElement != messagePane && !IsMessagePaneCollapsed())
+ SetFocusMessagePane();
+ else
+ threadTree.focus();
+ }
+ else
+ {
+ // Forward traversal: Folder -> Thread -> Message -> Folder
+ if (focusedElement == threadTree && !IsMessagePaneCollapsed())
+ SetFocusMessagePane();
+ else if (focusedElement != folderTree && !folderPaneCollapsed)
+ folderTree.focus();
+ else
+ threadTree.focus();
+ }
+}
+
+function SetFocusThreadPane()
+{
+ var threadTree = GetThreadTree();
+ threadTree.focus();
+}
+
+function SetFocusMessagePane()
+{
+ // XXX hack: to clear the focus on the previous element first focus
+ // on the message pane element then focus on the main content window
+ GetMessagePane().focus();
+ GetMessagePaneFrame().focus();
+}
+
+//
+// This function checks if the configured junk mail can be renamed or deleted.
+//
+function CanRenameDeleteJunkMail(aFolderUri)
+{
+ if (!aFolderUri)
+ return false;
+
+ // Go through junk mail settings for all servers and see if the folder is set/used by anyone.
+ try
+ {
+ var allServers = accountManager.allServers;
+
+ for (var i = 0; i < allServers.length; i++)
+ {
+ var currentServer =
+ allServers.queryElementAt(i, Ci.nsIMsgIncomingServer);
+ var settings = currentServer.spamSettings;
+ // If junk mail control or move junk mail to folder option is disabled then
+ // allow the folder to be removed/renamed since the folder is not used in this case.
+ if (!settings.level || !settings.moveOnSpam)
+ continue;
+ if (settings.spamFolderURI == aFolderUri)
+ return false;
+ }
+ }
+ catch(ex)
+ {
+ dump("Can't get all servers\n");
+ }
+ return true;
+}
+
+/** Check if this is a folder the user is allowed to delete. */
+function CanDeleteFolder(folder) {
+ if (folder.isServer)
+ return false;
+
+ var specialFolder = FolderUtils.getSpecialFolderString(folder);
+
+ if (specialFolder == "Inbox" || specialFolder == "Trash" ||
+ specialFolder == "Drafts" || specialFolder == "Sent" ||
+ specialFolder == "Templates" || specialFolder == "Outbox" ||
+ (specialFolder == "Junk" && !CanRenameDeleteJunkMail(folder.URI)))
+ return false;
+
+ return true;
+}
diff --git a/comm/suite/mailnews/content/mailCommands.js b/comm/suite/mailnews/content/mailCommands.js
new file mode 100644
index 0000000000..ae7e91a6cb
--- /dev/null
+++ b/comm/suite/mailnews/content/mailCommands.js
@@ -0,0 +1,415 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js");
+
+/**
+ * Get the identity that most likely is the best one to use, given the hint.
+ * @param {Array<nsIMsgIdentity> identities The candidates to pick from.
+ * @param {String} optionalHint String containing comma separated mailboxes
+ */
+function getBestIdentity(identities, optionalHint)
+{
+ let identityCount = identities.length;
+ if (identityCount < 1)
+ return null;
+
+ // If we have more than one identity and a hint to help us pick one.
+ if (identityCount > 1 && optionalHint) {
+ // Normalize case on the optional hint to improve our chances of
+ // finding a match.
+ let hints = optionalHint.toLowerCase().split(",");
+
+ for (let i = 0 ; i < hints.length; i++) {
+ for (let identity of identities) {
+ if (!identity.email)
+ continue;
+ if (hints[i].trim() == identity.email.toLowerCase() ||
+ hints[i].includes("<" + identity.email.toLowerCase() + ">"))
+ return identity;
+ }
+ }
+ }
+ // Return only found identity or pick the first one from list if no matches found.
+ return identities[0];
+}
+
+function getIdentityForServer(server, optionalHint)
+{
+ let identities = accountManager.getIdentitiesForServer(server);
+ return getBestIdentity(identities, optionalHint);
+}
+
+/**
+ * Get the identity for the given header.
+ * @param hdr nsIMsgHdr message header
+ * @param type nsIMsgCompType compose type the identity ise used for.
+ */
+
+function GetIdentityForHeader(aMsgHdr, aType)
+{
+ function findDeliveredToIdentityEmail() {
+ // Get the delivered-to headers.
+ let key = "delivered-to";
+ let deliveredTos = new Array();
+ let index = 0;
+ let header = "";
+ while (currentHeaderData[key]) {
+ deliveredTos.push(currentHeaderData[key].headerValue.toLowerCase().trim());
+ key = "delivered-to" + index++;
+ }
+
+ // Reverse the array so that the last delivered-to header will show at front.
+ deliveredTos.reverse();
+ for (let i = 0; i < deliveredTos.length; i++) {
+ for (let identity of accountManager.allIdentities) {
+ if (!identity.email)
+ continue;
+ // If the deliver-to header contains the defined identity, that's it.
+ if (deliveredTos[i] == identity.email.toLowerCase() ||
+ deliveredTos[i].includes("<" + identity.email.toLowerCase() + ">"))
+ return identity.email;
+ }
+ }
+ return "";
+ }
+
+ let hintForIdentity = "";
+ if (aType == Ci.nsIMsgCompType.ReplyToList)
+ hintForIdentity = findDeliveredToIdentityEmail();
+ else if (aType == Ci.nsIMsgCompType.Template ||
+ aType == Ci.nsIMsgCompType.EditTemplate ||
+ aType == Ci.nsIMsgCompType.EditAsNew)
+ hintForIdentity = aMsgHdr.author;
+ else
+ hintForIdentity = aMsgHdr.recipients + "," + aMsgHdr.ccList + "," +
+ findDeliveredToIdentityEmail();
+
+ let server = null;
+ let identity = null;
+ let folder = aMsgHdr.folder;
+ if (folder)
+ {
+ server = folder.server;
+ identity = folder.customIdentity;
+ }
+
+ if (!identity)
+ {
+ let accountKey = aMsgHdr.accountKey;
+ if (accountKey.length > 0)
+ {
+ let account = accountManager.getAccount(accountKey);
+ if (account)
+ server = account.incomingServer;
+ }
+
+ if (server)
+ identity = getIdentityForServer(server, hintForIdentity);
+
+ if (!identity)
+ identity = getBestIdentity(accountManager.allIdentities, hintForIdentity);
+ }
+ return identity;
+}
+
+function GetNextNMessages(folder)
+{
+ if (folder) {
+ var newsFolder = folder.QueryInterface(Ci.nsIMsgNewsFolder);
+ if (newsFolder) {
+ newsFolder.getNextNMessages(msgWindow);
+ }
+ }
+}
+
+// type is a nsIMsgCompType and format is a nsIMsgCompFormat
+function ComposeMessage(type, format, folder, messageArray)
+{
+ var msgComposeType = Ci.nsIMsgCompType;
+ var identity = null;
+ var newsgroup = null;
+ var hdr;
+
+ // dump("ComposeMessage folder=" + folder + "\n");
+ try
+ {
+ if (folder)
+ {
+ // Get the incoming server associated with this uri.
+ var server = folder.server;
+
+ // If they hit new or reply and they are reading a newsgroup,
+ // turn this into a new post or a reply to group.
+ if (!folder.isServer && server.type == "nntp" && type == msgComposeType.New)
+ {
+ type = msgComposeType.NewsPost;
+ newsgroup = folder.folderURL;
+ }
+
+ identity = getIdentityForServer(server);
+ // dump("identity = " + identity + "\n");
+ }
+ }
+ catch (ex)
+ {
+ dump("failed to get an identity to pre-select: " + ex + "\n");
+ }
+
+ // dump("\nComposeMessage from XUL: " + identity + "\n");
+
+ if (!msgComposeService)
+ {
+ dump("### msgComposeService is invalid\n");
+ return;
+ }
+
+ switch (type)
+ {
+ case msgComposeType.New: //new message
+ // dump("OpenComposeWindow with " + identity + "\n");
+ // If the addressbook sidebar panel is open and has focus, get
+ // the selected addresses from it.
+ if (document.commandDispatcher.focusedWindow &&
+ document.commandDispatcher.focusedWindow
+ .document.documentElement.hasAttribute("selectedaddresses"))
+ NewMessageToSelectedAddresses(type, format, identity);
+ else
+ msgComposeService.OpenComposeWindow(null, null, null, type,
+ format, identity, null, msgWindow);
+ return;
+ case msgComposeType.NewsPost:
+ // dump("OpenComposeWindow with " + identity + " and " + newsgroup + "\n");
+ msgComposeService.OpenComposeWindow(null, null, newsgroup, type,
+ format, identity, null, msgWindow);
+ return;
+ case msgComposeType.ForwardAsAttachment:
+ if (messageArray && messageArray.length)
+ {
+ // If we have more than one ForwardAsAttachment then pass null instead
+ // of the header to tell the compose service to work out the attachment
+ // subjects from the URIs.
+ hdr = messageArray.length > 1 ? null : messenger.msgHdrFromURI(messageArray[0]);
+ msgComposeService.OpenComposeWindow(null, hdr, messageArray.join(','),
+ type, format, identity, null, msgWindow);
+ return;
+ }
+ default:
+ if (!messageArray)
+ return;
+
+ // Limit the number of new compose windows to 8. Why 8 ?
+ // I like that number :-)
+ if (messageArray.length > 8)
+ messageArray.length = 8;
+
+ for (var i = 0; i < messageArray.length; ++i)
+ {
+ var messageUri = messageArray[i];
+ hdr = messenger.msgHdrFromURI(messageUri);
+ identity = GetIdentityForHeader(hdr, type);
+ if (FeedMessageHandler.isFeedMessage(hdr))
+ openComposeWindowForRSSArticle(null, hdr, messageUri, type,
+ format, identity, msgWindow);
+ else
+ msgComposeService.OpenComposeWindow(null, hdr, messageUri, type,
+ format, identity, null, msgWindow);
+ }
+ }
+}
+
+function NewMessageToSelectedAddresses(type, format, identity) {
+ var abSidebarPanel = document.commandDispatcher.focusedWindow;
+ var abResultsTree = abSidebarPanel.document.getElementById("abResultsTree");
+ var abResultsBoxObject = abResultsTree.treeBoxObject;
+ var abView = abResultsBoxObject.view;
+ abView = abView.QueryInterface(Ci.nsIAbView);
+ var addresses = abView.selectedAddresses;
+ var params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(Ci.nsIMsgComposeParams);
+ if (params) {
+ params.type = type;
+ params.format = format;
+ params.identity = identity;
+ var composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(Ci.nsIMsgCompFields);
+ if (composeFields) {
+ let addressList = [];
+ const nsISupportsString = Ci.nsISupportsString;
+ for (let i = 0; i < addresses.length; i++) {
+ addressList.push(addresses.queryElementAt(i, nsISupportsString).data);
+ }
+ composeFields.to = addressList.join(",");
+ params.composeFields = composeFields;
+ msgComposeService.OpenComposeWindowWithParams(null, params);
+ }
+ }
+}
+
+function Subscribe(preselectedMsgFolder)
+{
+ window.openDialog("chrome://messenger/content/subscribe.xul",
+ "subscribe", "chrome,modal,titlebar,resizable=yes",
+ {folder:preselectedMsgFolder,
+ okCallback:SubscribeOKCallback});
+}
+
+function SubscribeOKCallback(changeTable)
+{
+ for (var serverURI in changeTable) {
+ var folder = MailUtils.getFolderForURI(serverURI, true);
+ var server = folder.server;
+ var subscribableServer =
+ server.QueryInterface(Ci.nsISubscribableServer);
+
+ for (var name in changeTable[serverURI]) {
+ if (changeTable[serverURI][name] == true) {
+ try {
+ subscribableServer.subscribe(name);
+ }
+ catch (ex) {
+ dump("failed to subscribe to " + name + ": " + ex + "\n");
+ }
+ }
+ else if (changeTable[serverURI][name] == false) {
+ try {
+ subscribableServer.unsubscribe(name);
+ }
+ catch (ex) {
+ dump("failed to unsubscribe to " + name + ": " + ex + "\n");
+ }
+ }
+ else {
+ // no change
+ }
+ }
+
+ try {
+ subscribableServer.commitSubscribeChanges();
+ }
+ catch (ex) {
+ dump("failed to commit the changes: " + ex + "\n");
+ }
+ }
+}
+
+function SaveAsFile(aUris)
+{
+ if (/type=application\/x-message-display/.test(aUris[0]))
+ {
+ saveURL(aUris[0], null, "", true, false, null, document);
+ return;
+ }
+
+ var num = aUris.length;
+ var fileNames = [];
+ for (let i = 0; i < num; i++)
+ {
+ let subject = messenger.messageServiceFromURI(aUris[i])
+ .messageURIToMsgHdr(aUris[i])
+ .mime2DecodedSubject;
+ fileNames[i] = suggestUniqueFileName(subject.substr(0, 120), ".eml",
+ fileNames);
+ }
+ if (num == 1)
+ messenger.saveAs(aUris[0], true, null, fileNames[0]);
+ else
+ messenger.saveMessages(fileNames, aUris);
+}
+
+function saveAsUrlListener(aUri, aIdentity)
+{
+ this.uri = aUri;
+ this.identity = aIdentity;
+}
+
+saveAsUrlListener.prototype = {
+ OnStartRunningUrl: function(aUrl)
+ {
+ },
+ OnStopRunningUrl: function(aUrl, aExitCode)
+ {
+ messenger.saveAs(this.uri, false, this.identity, null);
+ }
+};
+
+function SaveAsTemplate(aUris)
+{
+ // For backwards compatibility check if the argument is a string and,
+ // if so, convert to an array.
+ if (typeof aUris == "string")
+ aUris = [aUris];
+
+ var num = aUris.length;
+ if (!num)
+ return;
+
+ for (let i = 0; i < num; i++)
+ {
+ let uri = aUris[i];
+ var hdr = messenger.msgHdrFromURI(uri);
+ var identity = GetIdentityForHeader(hdr, Ci.nsIMsgCompType.Template);
+ var templates = MailUtils.getFolderForURI(identity.stationeryFolder, false);
+ if (!templates.parent)
+ {
+ templates.setFlag(Ci.nsMsgFolderFlags.Templates);
+ let isAsync = templates.server.protocolInfo.foldersCreatedAsync;
+ templates.createStorageIfMissing(new saveAsUrlListener(uri, identity));
+ if (isAsync)
+ continue;
+ }
+ messenger.saveAs(uri, false, identity, null);
+ }
+}
+
+function MarkSelectedMessagesRead(markRead)
+{
+ ClearPendingReadTimer();
+ gDBView.doCommand(markRead ? nsMsgViewCommandType.markMessagesRead : nsMsgViewCommandType.markMessagesUnread);
+}
+
+function MarkSelectedMessagesFlagged(markFlagged)
+{
+ gDBView.doCommand(markFlagged ? nsMsgViewCommandType.flagMessages : nsMsgViewCommandType.unflagMessages);
+}
+
+function ViewPageSource(messages)
+{
+ var numMessages = messages.length;
+
+ if (numMessages == 0)
+ {
+ dump("MsgViewPageSource(): No messages selected.\n");
+ return false;
+ }
+
+ var browser = getBrowser();
+
+ try {
+ // First, get the mail session.
+ for (let i = 0; i < numMessages; i++) {
+ // Now, we need to get a URL from a URI.
+ var url = MailServices.mailSession.ConvertMsgURIToMsgURL(messages[i],
+ msgWindow);
+
+ // Strip out the message-display parameter to ensure that attached
+ // emails display the message source, not the processed HTML.
+ url = url.replace(/(\?|&)type=application\/x-message-display(&|$)/, "$1")
+ .replace(/\?$/, "");
+ window.openDialog("chrome://global/content/viewSource.xul", "_blank",
+ "all,dialog=no",
+ {URL: url, browser: browser,
+ outerWindowID: browser.outerWindowID});
+ }
+ return true;
+ } catch (e) {
+ // Couldn't get mail session.
+ return false;
+ }
+}
+
+function doHelpButton()
+{
+ openHelp("mail-offline-items");
+}
diff --git a/comm/suite/mailnews/content/mailContextMenus.js b/comm/suite/mailnews/content/mailContextMenus.js
new file mode 100644
index 0000000000..d70d7a31fc
--- /dev/null
+++ b/comm/suite/mailnews/content/mailContextMenus.js
@@ -0,0 +1,828 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+
+//NOTE: gMessengerBundle must be defined and set or this Overlay won't work
+
+/**
+ * Function to change the highlighted row back to the row that is currently
+ * outline/dotted without loading the contents of either rows. This is
+ * triggered when the context menu for a given row is hidden/closed
+ * (onpopuphiding).
+ * @param tree the tree element to restore selection for
+ */
+function RestoreSelectionWithoutContentLoad(tree)
+{
+ // If a delete or move command had been issued, then we should
+ // reset gRightMouseButtonDown and gThreadPaneDeleteOrMoveOccurred
+ // and return (see bug 142065).
+ if(gThreadPaneDeleteOrMoveOccurred)
+ {
+ gRightMouseButtonDown = false;
+ gThreadPaneDeleteOrMoveOccurred = false;
+ return;
+ }
+
+ var treeSelection = tree.view.selection;
+
+ // make sure that currentIndex is valid so that we don't try to restore
+ // a selection of an invalid row.
+ if((!treeSelection.isSelected(treeSelection.currentIndex)) &&
+ (treeSelection.currentIndex >= 0))
+ {
+ treeSelection.selectEventsSuppressed = true;
+ treeSelection.select(treeSelection.currentIndex);
+ treeSelection.selectEventsSuppressed = false;
+
+ // Keep track of which row in the thread pane is currently selected.
+ // This is currently only needed when deleting messages. See
+ // declaration of var in msgMail3PaneWindow.js.
+ if(tree.id == "threadTree")
+ gThreadPaneCurrentSelectedIndex = treeSelection.currentIndex;
+ }
+ else if(treeSelection.currentIndex < 0)
+ // Clear the selection in the case of when a folder has just been
+ // loaded where the message pane does not have a message loaded yet.
+ // When right-clicking a message in this case and dismissing the
+ // popup menu (by either executing a menu command or clicking
+ // somewhere else), the selection needs to be cleared.
+ // However, if the 'Delete Message' or 'Move To' menu item has been
+ // selected, DO NOT clear the selection, else it will prevent the
+ // tree view from refreshing.
+ treeSelection.clearSelection();
+
+ // Need to reset gRightMouseButtonDown to false here because
+ // TreeOnMouseDown() is only called on a mousedown, not on a key down.
+ // So resetting it here allows the loading of messages in the messagepane
+ // when navigating via the keyboard or the toolbar buttons *after*
+ // the context menu has been dismissed.
+ gRightMouseButtonDown = false;
+}
+
+/**
+ * Function to clear out the global nsContextMenu, and in the case when we
+ * are a threadpane context menu, restore the selection so that a right-click
+ * on a non-selected row doesn't move the selection.
+ * @param aTarget the target of the popup event
+ */
+function MailContextOnPopupHiding(aTarget, aEvent) {
+ // Don't do anything if it's a submenu's onpopuphiding that's just bubbling
+ // up to the top.
+ if (aEvent.target != aTarget)
+ return;
+
+ gContextMenu.hiding();
+ gContextMenu = null;
+ if (InThreadPane(aTarget))
+ RestoreSelectionWithoutContentLoad(GetThreadTree());
+}
+
+/**
+ * Determines whether the context menu was triggered by a node that's a child
+ * of the threadpane by looking for an ancestor node with id="threadTree".
+ * @param aTarget the target of the popup event
+ * @return true if the popupNode is a child of the threadpane, otherwise false
+ */
+function InThreadPane(aTarget)
+{
+ var node = aTarget.triggerNode;
+ while (node)
+ {
+ if (node.id == "threadTree")
+ return true;
+ node = node.parentNode;
+ }
+ return false;
+}
+
+/**
+ * Function to set up the global nsContextMenu, and the mailnews overlay.
+ * @param aTarget the target of the popup event
+ * @return true always
+ */
+function FillMailContextMenu(aTarget, aEvent) {
+ // If the popupshowing was for a submenu, we don't need to do anything.
+ if (aEvent.target != aTarget)
+ return true;
+
+ var inThreadPane = InThreadPane(aTarget);
+ gContextMenu = new nsContextMenu(aTarget);
+
+ // Initialize gContextMenuContentData.
+ if (aEvent)
+ gContextMenu.initContentData(aEvent);
+
+ // Need to call nsContextMenu's initItems to hide what is not used.
+ gContextMenu.initItems();
+
+ var numSelected = GetNumSelectedMessages();
+ var oneOrMore = (numSelected > 0);
+ var single = (numSelected == 1);
+
+ var isNewsgroup = gFolderDisplay.selectedMessageIsNews;
+
+ // Clear the global var used to keep track if a 'Delete Message' or 'Move
+ // To' command has been triggered via the thread pane context menu.
+ gThreadPaneDeleteOrMoveOccurred = false;
+
+ // Don't show mail items for links/images, just show related items.
+ var showMailItems = inThreadPane ||
+ (!gContextMenu.onImage && !gContextMenu.onLink);
+
+ // Select-all and copy are only available in the message-pane
+ ShowMenuItem("context-selectall", single && !inThreadPane);
+ ShowMenuItem("context-copy", !inThreadPane);
+
+ ShowMenuItem("mailContext-openNewWindow", inThreadPane && single);
+ ShowMenuItem("mailContext-openNewTab", inThreadPane && single);
+ ShowMenuItem("mailContext-downloadflagged",
+ inThreadPane || (numSelected > 1));
+ ShowMenuItem("mailContext-downloadselected",
+ inThreadPane || (numSelected > 1));
+
+ ShowMenuItem("mailContext-editAsNew", showMailItems && oneOrMore);
+ // Show "Edit Draft Message" menus only in a drafts folder;
+ // otherwise hide them.
+ let showEditDraft = showCommandInSpecialFolder("cmd_editDraftMsg",
+ Ci.nsMsgFolderFlags.Drafts);
+ ShowMenuItem("mailContext-editDraftMsg",
+ showMailItems && oneOrMore && showEditDraft);
+ // Show "New Message from Template" and "Edit Template" menus only in a
+ // templates folder; otherwise hide them.
+ let showTemplates = showCommandInSpecialFolder("cmd_newMsgFromTemplate",
+ Ci.nsMsgFolderFlags.Templates);
+ ShowMenuItem("mailContext-newMsgFromTemplate",
+ showMailItems && oneOrMore && showTemplates);
+ showTemplates = showCommandInSpecialFolder("cmd_editTemplateMsg",
+ Ci.nsMsgFolderFlags.Templates);
+ ShowMenuItem("mailContext-editTemplateMsg",
+ showMailItems && oneOrMore && showTemplates);
+
+ ShowMenuItem("mailContext-replySender", showMailItems && single);
+ ShowMenuItem("mailContext-replyList",
+ showMailItems && single && !isNewsgroup && IsListPost());
+ ShowMenuItem("mailContext-replyNewsgroup",
+ showMailItems && single && isNewsgroup);
+ ShowMenuItem("mailContext-replySenderAndNewsgroup",
+ showMailItems && single && isNewsgroup);
+ ShowMenuItem("mailContext-replyAll", showMailItems && single);
+ ShowMenuItem("mailContext-forward", showMailItems && single);
+ ShowMenuItem("mailContext-forwardAsAttachment",
+ showMailItems && (numSelected > 1));
+ ShowMenuItem("mailContext-copyMessageUrl",
+ showMailItems && single && isNewsgroup);
+ ShowMenuItem("mailContext-archive", showMailItems && oneOrMore &&
+ gFolderDisplay.canArchiveSelectedMessages);
+
+ // Set up the move menu. We can't move from newsgroups.
+ // Disable move if we can't delete message(s) from this folder.
+ var msgFolder = GetLoadedMsgFolder();
+ ShowMenuItem("mailContext-moveMenu",
+ showMailItems && oneOrMore && !isNewsgroup);
+ EnableMenuItem("mailContext-moveMenu",
+ oneOrMore && msgFolder && msgFolder.canDeleteMessages);
+
+ // Copy is available as long as something is selected.
+ var canCopy = showMailItems && oneOrMore && (!gMessageDisplay.isDummy ||
+ window.arguments[0].scheme == "file");
+ ShowMenuItem("mailContext-copyMenu", canCopy);
+ ShowMenuItem("mailContext-tags", showMailItems && oneOrMore);
+ ShowMenuItem("mailContext-mark", showMailItems && oneOrMore);
+ ShowMenuItem("mailContext-saveAs", showMailItems && oneOrMore);
+ ShowMenuItem("mailContext-printpreview", showMailItems && single);
+
+ ShowMenuItem("mailContext-print", showMailItems);
+ EnableMenuItem("mailContext-print", oneOrMore);
+ ShowMenuItem("mailContext-delete", showMailItems);
+ EnableMenuItem("mailContext-delete", oneOrMore);
+ // This function is needed for the case where a folder is just loaded
+ // (while there isn't a message loaded in the message pane), a right-click
+ // is done in the thread pane. This function will disable enable the
+ // 'Delete Message' menu item.
+ goUpdateCommand('cmd_delete');
+
+ ShowMenuItem("context-addemail", gContextMenu.onMailtoLink);
+ ShowMenuItem("context-composeemailto", gContextMenu.onMailtoLink);
+ ShowMenuItem("context-createfilterfrom", gContextMenu.onMailtoLink);
+
+ // Figure out separators.
+ initSeparators();
+
+ return true;
+}
+
+/**
+ * Hide separators with no active menu items.
+ *
+ */
+function initSeparators() {
+ const mailContextSeparators = [
+ "mailContext-sep-link", "mailContext-sep-open",
+ "mailContext-sep-tags", "mailContext-sep-mark",
+ "mailContext-sep-move", "mailContext-sep-print",
+ "mailContext-sep-edit", "mailContext-sep-image",
+ "mailContext-sep-blockimage", "mailContext-sep-copy",
+ ];
+
+ mailContextSeparators.forEach(hideIfAppropriate);
+}
+
+/**
+ * Hide a separator based on whether there are any non-hidden items between
+ * it and the previous separator.
+ *
+ * @param aID The id of the separator element.
+ */
+function hideIfAppropriate(aID) {
+ let separator = document.getElementById(aID);
+
+ function hasAVisibleNextSibling(aNode) {
+ let sibling = aNode.nextSibling;
+ while (sibling) {
+ if (sibling.getAttribute("hidden") != "true" &&
+ sibling.localName != "menuseparator")
+ return true;
+ sibling = sibling.nextSibling;
+ }
+ return false;
+ }
+
+ let sibling = separator.previousSibling;
+ while (sibling) {
+ if (sibling.getAttribute("hidden") != "true") {
+ ShowMenuItem(aID, sibling.localName != "menuseparator" &&
+ hasAVisibleNextSibling(separator));
+ return;
+ }
+ sibling = sibling.previousSibling;
+ }
+ ShowMenuItem(aID, false);
+}
+
+function FolderPaneOnPopupHiding()
+{
+ RestoreSelectionWithoutContentLoad(document.getElementById("folderTree"));
+}
+
+function FillFolderPaneContextMenu()
+{
+ // Do not show menu if rows are selected.
+ let folders = gFolderTreeView.getSelectedFolders();
+ let numSelected = folders.length;
+ if (!numSelected)
+ return false;
+
+ function checkIsVirtualFolder(folder) {
+ return folder.getFlag(Ci.nsMsgFolderFlags.Virtual);
+ }
+ let haveAnyVirtualFolders = folders.some(checkIsVirtualFolder);
+
+ function checkIsServer(folder) {
+ return folder.isServer;
+ }
+ let selectedServers = folders.filter(checkIsServer);
+
+ let folder = folders[0];
+ let isServer = folder.isServer;
+ let serverType = folder.server.type;
+ let specialFolder = haveAnyVirtualFolders ? "Virtual" :
+ FolderUtils.getSpecialFolderString(folder);
+
+ function checkCanSubscribeToFolder(folder) {
+ if (checkIsVirtualFolder(folder))
+ return false;
+
+ // All feed account folders, besides Trash, are subscribable.
+ if (folder.server.type == "rss" &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Trash))
+ return true;
+
+ // We only want the subscribe item on the account nodes.
+ if (!folder.isServer)
+ return false;
+
+ return folder.server.type == "nntp" ||
+ folder.server.type == "imap";
+ }
+ let haveOnlySubscribableFolders = folders.every(checkCanSubscribeToFolder);
+
+ function checkIsNewsgroup(folder) {
+ return !folder.isServer && folder.server.type == "nntp" &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Virtual);
+ }
+ let haveOnlyNewsgroups = folders.every(checkIsNewsgroup);
+
+ function checkIsMailFolder(folder) {
+ return !folder.isServer && folder.server.type != "nntp";
+ }
+ let haveOnlyMailFolders = folders.every(checkIsMailFolder);
+
+ function checkCanGetMessages(folder) {
+ return (folder.isServer && (folder.server.type != "none")) ||
+ checkIsNewsgroup(folder) ||
+ ((folder.server.type == "rss") &&
+ !folder.isSpecialFolder(Ci.nsMsgFolderFlags.Trash, true) &&
+ !checkIsVirtualFolder(folder));
+ }
+ let selectedFoldersThatCanGetMessages = folders.filter(checkCanGetMessages);
+
+ // --- Set up folder properties / account settings menu item.
+ if (numSelected != 1) {
+ ShowMenuItem("folderPaneContext-settings", false);
+ ShowMenuItem("folderPaneContext-properties", false);
+ }
+ else if (selectedServers.length != 1) {
+ ShowMenuItem("folderPaneContext-settings", false);
+ ShowMenuItem("folderPaneContext-properties", true);
+ }
+ else {
+ ShowMenuItem("folderPaneContext-properties", false);
+ ShowMenuItem("folderPaneContext-settings", true);
+ }
+
+ // --- Set up the get messages menu item.
+ // Show if only servers, or it's only newsgroups/feeds. We could mix,
+ // but it gets messy for situations where both server and a folder
+ // on the server are selected.
+ let showGet = selectedFoldersThatCanGetMessages.length == numSelected;
+ ShowMenuItem("folderPaneContext-getMessages", showGet);
+ if (showGet) {
+ if (selectedServers.length > 0 &&
+ selectedServers.length == selectedFoldersThatCanGetMessages.length) {
+ SetMenuItemLabel("folderPaneContext-getMessages",
+ gMessengerBundle.getString("getMessagesFor"));
+ }
+ else {
+ SetMenuItemLabel("folderPaneContext-getMessages",
+ gMessengerBundle.getString("getMessages"));
+ }
+ }
+
+ // --- Setup the Mark All Folders Read menu item.
+ // Show only in case the server item is selected.
+ ShowMenuItem("folderPaneContext-markAllFoldersRead",
+ selectedServers.length > 0);
+
+ // --- Set up new sub/folder menu item.
+ let isInbox = specialFolder == "Inbox";
+ let showNew =
+ (numSelected == 1) &&
+ ((serverType != "nntp" && folder.canCreateSubfolders) || isInbox);
+ ShowMenuItem("folderPaneContext-new", showNew);
+ if (showNew) {
+ EnableMenuItem("folderPaneContext-new",
+ serverType != "imap" || !Services.io.offline);
+ let label = (isServer || isInbox) ? "newFolder" : "newSubfolder";
+ SetMenuItemLabel("folderPaneContext-new",
+ gMessengerBundle.getString(label));
+ }
+
+ // --- Set up rename menu item.
+ let canRename = (numSelected == 1) && !isServer && folder.canRename &&
+ (specialFolder == "none" || specialFolder == "Virtual" ||
+ (specialFolder == "Junk" &&
+ CanRenameDeleteJunkMail(folder.URI)));
+ ShowMenuItem("folderPaneContext-rename", canRename);
+ if (canRename) {
+ EnableMenuItem("folderPaneContext-rename",
+ !isServer && folder.isCommandEnabled("cmd_renameFolder"));
+ SetMenuItemLabel("folderPaneContext-rename",
+ gMessengerBundle.getString("renameFolder"));
+ }
+
+ // --- Set up the delete folder menu item.
+ function checkCanDeleteFolder(folder) {
+ if (folder.isSpecialFolder(Ci.nsMsgFolderFlags.Junk, false))
+ return CanRenameDeleteJunkMail(folder.URI);
+ return folder.deletable;
+ }
+ let haveOnlyDeletableFolders = folders.every(checkCanDeleteFolder);
+ ShowMenuItem("folderPaneContext-remove",
+ haveOnlyDeletableFolders && numSelected == 1);
+ if (haveOnlyDeletableFolders && numSelected == 1)
+ SetMenuItemLabel("folderPaneContext-remove",
+ gMessengerBundle.getString("removeFolder"));
+
+ function checkIsDeleteEnabled(folder) {
+ return folder.isCommandEnabled("cmd_delete");
+ }
+ let haveOnlyDeleteEnabledFolders = folders.every(checkIsDeleteEnabled);
+ EnableMenuItem("folderPaneContext-remove", haveOnlyDeleteEnabledFolders);
+
+ // --- Set up the compact folder menu item.
+ function checkCanCompactFolder(folder) {
+ return folder.canCompact &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Virtual) &&
+ folder.isCommandEnabled("cmd_compactFolder");
+ }
+ let haveOnlyCompactableFolders = folders.every(checkCanCompactFolder);
+ ShowMenuItem("folderPaneContext-compact", haveOnlyCompactableFolders);
+ if (haveOnlyCompactableFolders)
+ SetMenuItemLabel("folderPaneContext-compact",
+ PluralForm.get(numSelected, gMessengerBundle.getString("compactFolders")));
+
+ function checkIsCompactEnabled(folder) {
+ return folder.isCommandEnabled("cmd_compactFolder");
+ }
+ let haveOnlyCompactEnabledFolders = folders.every(checkIsCompactEnabled);
+ EnableMenuItem("folderPaneContext-compact", haveOnlyCompactEnabledFolders);
+
+ // --- Set up favorite folder menu item.
+ let showFavorite = (numSelected == 1) && !isServer;
+ ShowMenuItem("folderPaneContext-favoriteFolder", showFavorite);
+ if (showFavorite) {
+ // Adjust the checked state on the menu item.
+ document.getElementById("folderPaneContext-favoriteFolder")
+ .setAttribute("checked",
+ folder.getFlag(Ci.nsMsgFolderFlags.Favorite));
+ }
+
+ // --- Set up the empty trash menu item.
+ ShowMenuItem("folderPaneContext-emptyTrash",
+ numSelected == 1 && specialFolder == "Trash");
+
+ // --- Set up the empty junk menu item.
+ ShowMenuItem("folderPaneContext-emptyJunk",
+ numSelected == 1 && specialFolder == "Junk");
+
+ // --- Set up the send unsent messages menu item.
+ let showSendUnsentMessages = numSelected == 1 && specialFolder == "Outbox";
+ ShowMenuItem("folderPaneContext-sendUnsentMessages", showSendUnsentMessages);
+ if (showSendUnsentMessages)
+ EnableMenuItem("folderPaneContext-sendUnsentMessages",
+ IsSendUnsentMsgsEnabled(folder));
+
+ // --- Set up the subscribe menu item.
+ ShowMenuItem("folderPaneContext-subscribe",
+ numSelected == 1 && haveOnlySubscribableFolders);
+
+ // --- Set up the unsubscribe menu item.
+ ShowMenuItem("folderPaneContext-newsUnsubscribe", haveOnlyNewsgroups);
+
+ // --- Set up the mark newsgroup/s read menu item.
+ ShowMenuItem("folderPaneContext-markNewsgroupAllRead", haveOnlyNewsgroups);
+ SetMenuItemLabel("folderPaneContext-markNewsgroupAllRead",
+ PluralForm.get(numSelected, gMessengerBundle.getString("markNewsgroupRead")));
+
+ // --- Set up the mark folder/s read menu item.
+ ShowMenuItem("folderPaneContext-markMailFolderAllRead",
+ haveOnlyMailFolders && !haveAnyVirtualFolders);
+ SetMenuItemLabel("folderPaneContext-markMailFolderAllRead",
+ PluralForm.get(numSelected, gMessengerBundle.getString("markFolderRead")));
+
+ // Set up the search menu item.
+ ShowMenuItem("folderPaneContext-searchMessages",
+ numSelected == 1 && !haveAnyVirtualFolders);
+ goUpdateCommand('cmd_search');
+
+ ShowMenuItem("folderPaneContext-openNewWindow", numSelected == 1);
+ ShowMenuItem("folderPaneContext-openNewTab", numSelected == 1);
+
+ // Hide / Show our menu separators based on the menu items we are showing.
+ hideIfAppropriate("folderPaneContext-sep1");
+ hideIfAppropriate("folderPaneContext-sep-edit");
+ hideIfAppropriate("folderPaneContext-sep4");
+
+ return true;
+}
+
+function ShowMenuItem(id, showItem)
+{
+ var item = document.getElementById(id);
+ if(item && item.hidden != "true")
+ item.hidden = !showItem;
+}
+
+function EnableMenuItem(id, enableItem)
+{
+ var item = document.getElementById(id);
+ if(item)
+ {
+ var enabled = (item.getAttribute('disabled') !='true');
+ if(enableItem != enabled)
+ {
+ item.setAttribute('disabled', enableItem ? '' : 'true');
+ }
+ }
+}
+
+function SetMenuItemLabel(id, label)
+{
+ var item = document.getElementById(id);
+ if(item)
+ item.setAttribute('label', label);
+}
+
+function SetMenuItemAccessKey(id, accessKey)
+{
+ var item = document.getElementById(id);
+ if(item)
+ item.setAttribute('accesskey', accessKey);
+}
+
+// message pane context menu helper methods
+function AddContact(aEmailAddressNode)
+{
+ if (aEmailAddressNode)
+ AddEmailToAddressBook(aEmailAddressNode.getAttribute("emailAddress"),
+ aEmailAddressNode.getAttribute("displayName"));
+}
+
+function AddEmailToAddressBook(primaryEmail, displayName)
+{
+ window.openDialog("chrome://messenger/content/addressbook/abNewCardDialog.xul",
+ "", "chrome,resizable=no,titlebar,modal,centerscreen",
+ {primaryEmail:primaryEmail, displayName:displayName});
+}
+
+function EditContact(aEmailAddressNode)
+{
+ if (aEmailAddressNode.cardDetails.card)
+ {
+ window.openDialog("chrome://messenger/content/addressbook/abEditCardDialog.xul",
+ "", "chrome,resizable=no,modal,titlebar,centerscreen",
+ { abURI: aEmailAddressNode.cardDetails.book.URI,
+ card: aEmailAddressNode.cardDetails.card });
+ }
+}
+
+/**
+ * SendMailToNode takes the email address title button, extracts the email address
+ * we stored in there and opens a compose window with that address.
+ *
+ * @param addressNode a node which has a "fullAddress" attribute
+ * @param aEvent the event object when user triggers the menuitem
+ */
+function SendMailToNode(emailAddressNode, aEvent)
+{
+ if (emailAddressNode)
+ SendMailTo(emailAddressNode.getAttribute("fullAddress"), aEvent);
+}
+
+function SendMailTo(fullAddress, aEvent)
+{
+ var fields = Cc["@mozilla.org/messengercompose/composefields;1"]
+ .createInstance(Ci.nsIMsgCompFields);
+ var params = Cc["@mozilla.org/messengercompose/composeparams;1"]
+ .createInstance(Ci.nsIMsgComposeParams);
+
+ var headerParser = MailServices.headerParser;
+ var addresses = headerParser.makeFromDisplayAddress(fullAddress);
+ fields.to = headerParser.makeMimeHeader([addresses[0]]);
+ params.type = Ci.nsIMsgCompType.New;
+
+ // If aEvent is passed, check if Shift key was pressed for composition in
+ // non-default format (HTML vs. plaintext).
+ params.format = (aEvent && aEvent.shiftKey) ?
+ Ci.nsIMsgCompFormat.OppositeOfDefault :
+ Ci.nsIMsgCompFormat.Default;
+
+ params.identity = accountManager.getFirstIdentityForServer(GetLoadedMsgFolder().server);
+ params.composeFields = fields;
+ MailServices.compose.OpenComposeWindowWithParams(null, params);
+}
+
+/**
+ * Takes the email address, extracts the address/name
+ * we stored in there and copies it to the clipboard.
+ *
+ * @param addressNode a node which has an "emailAddress"
+ * attribute
+ * @param aIncludeName when true, also copy the name onto the clipboard,
+ * otherwise only the email address
+ */
+function CopyEmailAddress(emailAddressNode, aIncludeName = false)
+{
+ if (emailAddressNode) {
+ let address = emailAddressNode.getAttribute(aIncludeName ? "fullAddress"
+ : "emailAddress");
+ CopyString(address);
+ }
+}
+
+// show the message id in the context menu
+function FillMessageIdContextMenu(messageIdNode)
+{
+ var msgId = messageIdNode.getAttribute("messageid");
+ document.getElementById("messageIdContext-messageIdTarget")
+ .setAttribute("label", msgId);
+
+ // We don't want to show "Open Message For ID" for the same message
+ // we're viewing.
+ var currentMsgId = "<" + gFolderDisplay.selectedMessage.messageId + ">";
+ document.getElementById("messageIdContext-openMessageForMsgId")
+ .hidden = (currentMsgId == msgId);
+
+ // We don't want to show "Open Browser With Message-ID" for non-nntp messages.
+ document.getElementById("messageIdContext-openBrowserWithMsgId")
+ .hidden = !gFolderDisplay.selectedMessageIsNews;
+}
+
+function GetMessageIdFromNode(messageIdNode, cleanMessageId)
+{
+ var messageId = messageIdNode.getAttribute("messageid");
+
+ // remove < and >
+ if (cleanMessageId)
+ messageId = messageId.substring(1, messageId.length - 1);
+
+ return messageId;
+}
+
+// take the message id from the messageIdNode and use the
+// url defined in the hidden pref "mailnews.messageid_browser.url"
+// to open it in a browser window (%mid is replaced by the message id)
+function OpenBrowserWithMessageId(messageId)
+{
+ var browserURL = GetLocalizedStringPref("mailnews.messageid_browser.url");
+ if (browserURL)
+ openAsExternal(browserURL.replace(/%mid/, messageId));
+}
+
+// take the message id from the messageIdNode, search for the
+// corresponding message in all folders starting with the current
+// selected folder, then the current account followed by the other
+// accounts and open corresponding message if found
+function OpenMessageForMessageId(messageId)
+{
+ var startServer = gDBView.msgFolder.server;
+ var messageHeader;
+
+ window.setCursor("wait");
+
+ // first search in current folder for message id
+ var messageHeader = CheckForMessageIdInFolder(gDBView.msgFolder, messageId);
+
+ // if message id not found in current folder search in all folders
+ if (!messageHeader)
+ {
+ messageHeader = SearchForMessageIdInSubFolder(startServer.rootFolder, messageId);
+
+ for (let currentServer of MailServices.accounts.allServers)
+ {
+ if (currentServer && startServer != currentServer &&
+ currentServer.canSearchMessages && !currentServer.isDeferredTo)
+ {
+ messageHeader = SearchForMessageIdInSubFolder(currentServer.rootFolder, messageId);
+ }
+ }
+ }
+ window.setCursor("auto");
+
+ // if message id was found open corresponding message
+ // else show error message
+ if (messageHeader)
+ OpenMessageByHeader(messageHeader, Services.prefs.getBoolPref("mailnews.messageid.openInNewWindow"));
+ else
+ {
+ var messageIdStr = "<" + messageId + ">";
+ var errorTitle = gMessengerBundle.getString("errorOpenMessageForMessageIdTitle");
+ var errorMessage = gMessengerBundle.getFormattedString("errorOpenMessageForMessageIdMessage",
+ [messageIdStr]);
+ Services.prompt.alert(window, errorTitle, errorMessage);
+ }
+}
+
+function OpenMessageByHeader(messageHeader, openInNewWindow)
+{
+ var folder = messageHeader.folder;
+ var folderURI = folder.URI;
+
+ if (openInNewWindow)
+ {
+ var messageURI = folder.getUriForMsg(messageHeader);
+
+ window.openDialog("chrome://messenger/content/messageWindow.xul",
+ "_blank", "all,chrome,dialog=no,status,toolbar",
+ messageURI, folderURI, null);
+ }
+ else
+ {
+ if (msgWindow.openFolder != folderURI)
+ gFolderTreeView.selectFolder(folder)
+
+ var tree = null;
+ var wintype = document.documentElement.getAttribute('windowtype');
+ if (wintype != "mail:messageWindow")
+ {
+ tree = GetThreadTree();
+ tree.view.selection.clearSelection();
+ }
+
+ try
+ {
+ gDBView.selectMsgByKey(messageHeader.messageKey);
+ }
+ catch(e)
+ { // message not in the thread pane
+ try
+ {
+ goDoCommand("cmd_viewAllMsgs");
+ gDBView.selectMsgByKey(messageHeader.messageKey);
+ }
+ catch(e)
+ {
+ dump("select messagekey " + messageHeader.messageKey +
+ " failed in folder " + folder.URI);
+ }
+ }
+
+ if (tree && tree.currentIndex != -1)
+ tree.treeBoxObject.ensureRowIsVisible(tree.currentIndex);
+ }
+}
+
+// search for message by message id in given folder and its subfolders
+// return message header if message was found
+function SearchForMessageIdInSubFolder(folder, messageId)
+{
+ var messageHeader;
+
+ // search in folder
+ if (!folder.isServer)
+ messageHeader = CheckForMessageIdInFolder(folder, messageId);
+
+ // search subfolders recursively
+ for (let currentFolder of folder.subFolders) {
+ // search in current folder
+ messageHeader = CheckForMessageIdInFolder(currentFolder, messageId);
+
+ // search in its subfolder
+ if (!messageHeader && currentFolder.hasSubFolders)
+ messageHeader = SearchForMessageIdInSubFolder(currentFolder, messageId);
+ }
+
+ return messageHeader;
+}
+
+// check folder for corresponding message to given message id
+// return message header if message was found
+function CheckForMessageIdInFolder(folder, messageId)
+{
+ var messageDatabase = folder.msgDatabase;
+ var messageHeader;
+
+ try
+ {
+ messageHeader = messageDatabase.getMsgHdrForMessageID(messageId);
+ }
+ catch (ex)
+ {
+ dump("Failed to find message-id in folder!");
+ }
+
+ if (!MailServices.mailSession.IsFolderOpenInWindow(folder) &&
+ !folder.getFlag(Ci.nsMsgFolderFlags.Trash | Ci.nsMsgFolderFlags.Inbox))
+ {
+ folder.msgDatabase = null;
+ }
+
+ return messageHeader;
+}
+
+// CreateFilter opens the Message Filters and Filter Rules dialogs.
+//The Filter Rules dialog has focus. The window is prefilled with filtername <email address>
+//Sender condition is selected and the value is prefilled <email address>
+function CreateFilter(emailAddressNode)
+{
+ if (emailAddressNode)
+ CreateFilterFromMail(emailAddressNode.getAttribute("emailAddress"));
+}
+
+function CreateFilterFromMail(emailAddress)
+{
+ if (emailAddress)
+ top.MsgFilters(emailAddress, GetFirstSelectedMsgFolder());
+}
+
+function CopyMessageUrl()
+{
+ try
+ {
+ var hdr = gDBView.hdrForFirstSelectedMessage;
+ var server = hdr.folder.server;
+
+ // TODO let backend construct URL and return as attribute
+ var url = (server.socketType == Ci.nsMsgSocketType.SSL) ?
+ "snews://" : "news://";
+ url += server.hostName + ":" + server.port + "/" + hdr.messageId;
+ CopyString(url);
+ }
+ catch (ex)
+ {
+ dump("ex="+ex+"\n");
+ }
+}
+
+function CopyString(aString)
+{
+ Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper)
+ .copyString(aString);
+}
diff --git a/comm/suite/mailnews/content/mailEditorOverlay.xul b/comm/suite/mailnews/content/mailEditorOverlay.xul
new file mode 100644
index 0000000000..7eb6d651ad
--- /dev/null
+++ b/comm/suite/mailnews/content/mailEditorOverlay.xul
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+
+<!-- retrieve generic commands -->
+<?xul-overlay href="chrome://messenger/content/mailOverlay.xul"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/mailEditorOverlay.dtd" >
+
+<overlay id="mailEditorOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script>
+ <![CDATA[
+
+ function openComposeWindow(pageUrl, pageTitle)
+ {
+ var params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(Ci.nsIMsgComposeParams);
+ if (params)
+ {
+ params.composeFields = Cc['@mozilla.org/messengercompose/composefields;1'].createInstance(Ci.nsIMsgCompFields);
+ if (params.composeFields)
+ {
+ params.composeFields.body = pageUrl;
+ params.composeFields.subject = pageTitle;
+ var attachmentData = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment);
+ if (attachmentData)
+ {
+ attachmentData.url = pageUrl;
+ params.composeFields.addAttachment(attachmentData);
+ }
+ params.bodyIsLink = true;
+
+ var composeService = Cc["@mozilla.org/messengercompose;1"].getService(Ci.nsIMsgComposeService);
+ if (composeService)
+ composeService.OpenComposeWindowWithParams(null, params);
+ }
+ }
+ }
+
+ ]]>
+ </script>
+
+ <!-- editor specific UI items -->
+ <menupopup id="menu_NewPopup">
+ <!-- Command nodes and implemention are in mailOverlay.xul -->
+ <menuitem id="menu_newMessage" insertafter="menu_newPrivateWindow"/>
+ <menuitem id="menu_newCard" insertafter="menu_newPrivateWindow"/>
+ </menupopup>
+
+ <menupopup id="menu_FilePopup">
+ <!-- The command node cmd_editSendPage is in editor.xul.
+ Implementation is in ComposerCommands.js
+ -->
+ <menuitem id="menu_sendPage" label="&sendPage.label;" accesskey="&sendPage.accesskey;" observes="cmd_editSendPage" insertafter="previewInBrowser"/>
+ </menupopup>
+
+</overlay>
+
diff --git a/comm/suite/mailnews/content/mailKeysOverlay.xul b/comm/suite/mailnews/content/mailKeysOverlay.xul
new file mode 100644
index 0000000000..e89cee3968
--- /dev/null
+++ b/comm/suite/mailnews/content/mailKeysOverlay.xul
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/mailKeysOverlay.dtd">
+
+<overlay id="mailKeysOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <keyset id="mailKeys">
+ <key id="key_delete"/>
+ <key id="key_delete2"/> <!-- secondary delete key -->
+#ifdef XP_MACOSX
+ <!-- not all Mac keyboards have a VK_DELETE key, so we use VK_BACK as
+ the primary and provide VK_DELETE as a secondary key definition -->
+ <key id="key_shiftDelete" keycode="VK_BACK"
+ modifiers="shift" command="cmd_shiftDelete"/>
+ <key id="key_shiftDelete2" keycode="VK_DELETE"
+ modifiers="shift" command="cmd_shiftDelete"/>
+#else
+ <key id="key_shiftDelete" keycode="VK_DELETE"
+ modifiers="shift" command="cmd_shiftDelete"/>
+#endif
+ <key id="key_selectAll"/>
+
+ <key id="key_markAsRead"
+ key="&markAsReadCmd.key;"
+ oncommand="goDoCommand('cmd_markAsRead');"/>
+ <key id="key_markAsUnread"
+ key="&markAsUnreadCmd2.key;"
+ oncommand="goDoCommand('cmd_markAsUnread');"/>
+ <key id="key_toggleFlagged" key="&markFlaggedCmd.key;"
+ oncommand="goDoCommand('cmd_markAsFlagged');"/>
+ <key id="key_openMessage" key="&openMessageWindowCmd.key;"
+ modifiers="accel" oncommand="goDoCommand('cmd_openMessage');"/>
+
+ <!-- Tag Keys -->
+ <!-- Includes both shifted and not, for Azerty and other layouts where the
+ numeric keys are shifted. -->
+ <key id="key_tag0" key="&tagCmd0.key;" modifiers="shift any"
+ oncommand="RemoveAllMessageTags();"/>
+ <key id="key_tag1" key="&tagCmd1.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(1);"/>
+ <key id="key_tag2" key="&tagCmd2.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(2);"/>
+ <key id="key_tag3" key="&tagCmd3.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(3);"/>
+ <key id="key_tag4" key="&tagCmd4.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(4);"/>
+ <key id="key_tag5" key="&tagCmd5.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(5);"/>
+ <key id="key_tag6" key="&tagCmd6.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(6);"/>
+ <key id="key_tag7" key="&tagCmd7.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(7);"/>
+ <key id="key_tag8" key="&tagCmd8.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(8);"/>
+ <key id="key_tag9" key="&tagCmd9.key;" modifiers="shift any"
+ oncommand="ToggleMessageTagKey(9);"/>
+ </keyset>
+
+</overlay>
+
diff --git a/comm/suite/mailnews/content/mailOverlay.js b/comm/suite/mailnews/content/mailOverlay.js
new file mode 100644
index 0000000000..fd812eaa54
--- /dev/null
+++ b/comm/suite/mailnews/content/mailOverlay.js
@@ -0,0 +1,30 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+function openNewCardDialog()
+{
+ window.openDialog("chrome://messenger/content/addressbook/abNewCardDialog.xul",
+ "", "chrome,modal,resizable=no,centerscreen");
+}
+
+function goOpenNewMessage()
+{
+ // if there is a MsgNewMessage function in scope
+ // and we should use it, so that we choose the proper
+ // identity, based on the selected message or folder
+ // if not, bring up the compose window to the default identity
+ if ("MsgNewMessage" in window)
+ {
+ MsgNewMessage(null);
+ return;
+ }
+
+ Cc["@mozilla.org/messengercompose;1"]
+ .getService(Ci.nsIMsgComposeService)
+ .OpenComposeWindow(null, null, null,
+ Ci.nsIMsgCompType.New,
+ Ci.nsIMsgCompFormat.Default,
+ null, null, null);
+}
diff --git a/comm/suite/mailnews/content/mailOverlay.xul b/comm/suite/mailnews/content/mailOverlay.xul
new file mode 100644
index 0000000000..b1379f6824
--- /dev/null
+++ b/comm/suite/mailnews/content/mailOverlay.xul
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/mailOverlay.dtd">
+<overlay id="mailOverlay.xul"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://messenger/content/mailOverlay.js"/>
+
+ <!-- generic commands -->
+ <commandset id="tasksCommands">
+ <command id="cmd_newMessage" oncommand="goOpenNewMessage();"/>
+ <command id="cmd_newCard" oncommand="openNewCardDialog()"/>
+ </commandset>
+ <menuitem id="menu_newCard" label="&newContactCmd.label;"
+ accesskey="&newContactCmd.accesskey;" command="cmd_newCard"/>
+ <menuitem id="menu_newMessage" label="&newMessageCmd.label;" accesskey="&newMessageCmd.accesskey;" key="key_newMessage" command="cmd_newMessage"/>
+ <keyset id="tasksKeys">
+#ifdef XP_MACOSX
+ <key id="key_newMessage" key="&newMessageCmd.key;"
+ modifiers="accel,shift" command="cmd_newMessage"/>
+#else
+ <key id="key_newMessage" key="&newMessageCmd.key;"
+ modifiers="accel" command="cmd_newMessage"/>
+#endif
+ </keyset>
+</overlay>
diff --git a/comm/suite/mailnews/content/mailTasksOverlay.js b/comm/suite/mailnews/content/mailTasksOverlay.js
new file mode 100644
index 0000000000..2033eb0651
--- /dev/null
+++ b/comm/suite/mailnews/content/mailTasksOverlay.js
@@ -0,0 +1,250 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// biff observer topic
+const BIFF_TOPIC = "mail:biff-state-changed";
+
+// biff state constants used by themes
+const BIFF_STATE_MESSAGES = "NewMail";
+const BIFF_STATE_NOMESSAGES = "NoMail";
+const BIFF_STATE_UNKNOWN = "UnknownMail";
+
+
+// uses "toOpenWindowByType" function provided by tasksOverlay.js
+// which is included by most clients.
+function toMessengerWindow()
+{
+ toOpenWindowByType("mail:3pane", "chrome://messenger/content/");
+}
+
+function toAddressBook()
+{
+ toOpenWindowByType("mail:addressbook",
+ "chrome://messenger/content/addressbook/addressbook.xul");
+}
+
+function toNewsgroups()
+{
+ dump("Sorry, command not implemented.\n");
+}
+
+function toImport()
+{
+ window.openDialog("chrome://messenger/content/importDialog.xul",
+ "importDialog",
+ "chrome, modal, titlebar, centerscreen");
+}
+
+function CoalesceGetMsgsForPop3ServersByDestFolder(aCurrentServer,
+ aPOP3DownloadServersArray,
+ aLocalFoldersToDownloadTo)
+{
+ // coalesce the servers that download into the same folder...
+ var inbox = aCurrentServer.rootMsgFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+ var index = aLocalFoldersToDownloadTo.indexOf(inbox);
+ if (index == -1)
+ {
+ inbox.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NoMail;
+ inbox.clearNewMessages();
+ aLocalFoldersToDownloadTo.push(inbox);
+ index = aPOP3DownloadServersArray.length;
+ aPOP3DownloadServersArray.push([]);
+ }
+ aPOP3DownloadServersArray[index].push(aCurrentServer);
+}
+
+function MailTasksGetMessagesForAllServers(aBiff, aMsgWindow, aDefaultServer)
+{
+ // now log into any server
+ try
+ {
+ // array of array of servers for a particular folder
+ var pop3DownloadServersArray = [];
+ // parallel array of folders to download to...
+ var localFoldersToDownloadTo = [];
+ var pop3Server = null;
+ for (let currentServer of MailServices.accounts.allServers)
+ {
+ if (currentServer)
+ {
+ if (aBiff)
+ {
+ if (currentServer.protocolInfo.canLoginAtStartUp &&
+ currentServer.loginAtStartUp)
+ {
+ if (aDefaultServer &&
+ aDefaultServer.equals(currentServer) &&
+ !aDefaultServer.isDeferredTo &&
+ aDefaultServer.rootFolder == aDefaultServer.rootMsgFolder)
+ {
+ dump(currentServer.serverURI + " ... skipping, already opened\n");
+ }
+ else if (currentServer.type == "pop3" && currentServer.downloadOnBiff)
+ {
+ CoalesceGetMsgsForPop3ServersByDestFolder(currentServer,
+ pop3DownloadServersArray,
+ localFoldersToDownloadTo);
+ pop3Server = currentServer;
+ }
+ else
+ {
+ // check to see if there are new messages on the server
+ currentServer.performBiff(aMsgWindow);
+ }
+ }
+ }
+ else
+ {
+ if (currentServer.protocolInfo.canGetMessages &&
+ !currentServer.passwordPromptRequired)
+ {
+ if (currentServer.type == "pop3")
+ {
+ CoalesceGetMsgsForPop3ServersByDestFolder(currentServer,
+ pop3DownloadServersArray,
+ localFoldersToDownloadTo);
+ pop3Server = currentServer;
+ }
+ else
+ {
+ // get new messages on the server for IMAP or RSS
+ GetMessagesForInboxOnServer(currentServer);
+ }
+ }
+ }
+ }
+ }
+
+ if (pop3Server instanceof Ci.nsIPop3IncomingServer)
+ {
+ for (let i = 0; i < pop3DownloadServersArray.length; ++i)
+ {
+ // any ol' pop3Server will do -
+ // the serversArray specifies which servers to download from
+ pop3Server.downloadMailFromServers(pop3DownloadServersArray[i],
+ aMsgWindow,
+ localFoldersToDownloadTo[i],
+ null);
+ }
+ }
+ }
+ catch (e)
+ {
+ Cu.reportError(e);
+ }
+}
+
+var biffObserver =
+{
+ observe: function observe(subject, topic, state)
+ {
+ // sanity check
+ if (topic == BIFF_TOPIC)
+ {
+ var biffManager = Cc["@mozilla.org/messenger/statusBarBiffManager;1"]
+ .getService(Ci.nsIStatusBarBiffManager);
+ document.getElementById("mini-mail")
+ .setAttribute("BiffState",
+ [BIFF_STATE_MESSAGES,
+ BIFF_STATE_NOMESSAGES,
+ BIFF_STATE_UNKNOWN][biffManager.biffState]);
+ }
+ }
+};
+
+function MailTasksOnLoad(aEvent)
+{
+ // Without the mini-mail icon to show the biff state, there's no need to
+ // initialize this here. We won't start with the hidden window alone,
+ // so this early return doesn't break anything.
+ var miniMail = document.getElementById("mini-mail");
+ if (!miniMail)
+ return;
+
+ // initialize biff state
+ Services.obs.addObserver(biffObserver, BIFF_TOPIC);
+ biffObserver.observe(null, BIFF_TOPIC, null); // init mini-mail icon
+ addEventListener("unload", MailTasksOnUnload, false);
+
+ // don't try to biff if offline, but do so silently
+ if (Services.io.offline)
+ return;
+
+ // Performing biff here will mean performing it for all new windows opened!
+ // This might make non-users of mailnews unhappy...
+ if (!Services.prefs.getBoolPref("mail.biff.on_new_window"))
+ return;
+
+ // The MailNews main window will perform biff later in its onload handler,
+ // so we don't need to do this here.
+ if (Services.wm.getMostRecentWindow("mail:3pane"))
+ return;
+
+ // If we already have a defined biff-state set on the mini-mail icon,
+ // we know that biff is already running.
+ const kBiffState = Cc["@mozilla.org/messenger/statusBarBiffManager;1"]
+ .getService(Ci.nsIStatusBarBiffManager)
+ .biffState;
+ if (kBiffState != Ci.nsIMsgFolder.nsMsgBiffState_Unknown)
+ return;
+
+ // still no excuse to refuse to use this ruse
+ MailTasksGetMessagesForAllServers(true, null, null);
+}
+
+function MailTasksOnUnload(aEvent)
+{
+ Services.obs.removeObserver(biffObserver, BIFF_TOPIC);
+}
+
+/**
+ * This class implements nsIBadCertListener2. Its job is to prevent "bad cert"
+ * security dialogs from being shown to the user. Currently it puts up the
+ * cert override dialog, though we'd like to give the user more detailed
+ * information in the future.
+ */
+function nsMsgBadCertHandler() {
+}
+
+nsMsgBadCertHandler.prototype = {
+ // Suppress any certificate errors
+ notifyCertProblem: function(socketInfo, status, targetSite) {
+ if (!status)
+ return true;
+
+ setTimeout(InformUserOfCertError, 0, status, targetSite);
+ return true;
+ },
+
+ // nsIInterfaceRequestor
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ // nsISupports
+ QueryInterface: function(iid) {
+ if (!iid.equals(Ci.nsIBadCertListener2) &&
+ !iid.equals(Ci.nsIInterfaceRequestor) &&
+ !iid.equals(Ci.nsISupports))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+};
+
+function InformUserOfCertError(status, targetSite)
+{
+ var params = { exceptionAdded : false,
+ sslStatus : status,
+ prefetchCert : true,
+ location : targetSite };
+ window.openDialog('chrome://pippki/content/exceptionDialog.xul',
+ '','chrome,centerscreen,modal', params);
+}
+
+addEventListener("load", MailTasksOnLoad, false);
diff --git a/comm/suite/mailnews/content/mailTasksOverlay.xul b/comm/suite/mailnews/content/mailTasksOverlay.xul
new file mode 100644
index 0000000000..9a208bb750
--- /dev/null
+++ b/comm/suite/mailnews/content/mailTasksOverlay.xul
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/mailTasksOverlay.dtd">
+
+<overlay id="mailTasksOverlay"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://messenger/content/mailTasksOverlay.js"/>
+
+ <keyset id="tasksKeys">
+ <key id="key_mail"
+ command="Tasks:Mail"
+ key="&messengerCmd.commandkey;"
+ modifiers="accel"/>
+ <key id="key_addressbook"
+ command="Tasks:AddressBook"
+ key="&addressBookCmd.commandkey;"
+ modifiers="accel"/>
+ </keyset>
+
+ <commandset id="tasksCommands">
+ <command id="Tasks:Mail" oncommand="toMessengerWindow();"/>
+ <command id="Tasks:AddressBook" oncommand="toAddressBook();"/>
+ </commandset>
+
+ <statusbarpanel id="component-bar">
+ <toolbarbutton id="mini-mail"
+ class="taskbutton"
+ oncommand="toMessengerWindow()"
+ position="2"
+ tooltiptext="&taskMessenger.tooltip;"/>
+ <toolbarbutton id="mini-comp"
+ insertafter="mini-mail"/>
+ <toolbarbutton id="mini-addr"
+ class="taskbutton"
+ oncommand="toAddressBook();"
+ insertafter="mini-comp"
+ tooltiptext="&taskAddressBook.tooltip;"/>
+ </statusbarpanel>
+
+ <menupopup id="windowPopup">
+ <menuitem id="tasksMenuMail"
+ class="menuitem-iconic icon-mail16 menu-iconic"
+ label="&messengerCmd.label;"
+ accesskey="&messengerCmd.accesskey;"
+ key="key_mail"
+ command="Tasks:Mail"
+ insertafter="tasksMenuNavigator"/>
+ <menuitem id="tasksMenuEditor"
+ insertafter="tasksMenuMail"/>
+ <menuitem id="tasksMenuAddressBook"
+ class="menuitem-iconic icon-addressbook16 menu-iconic"
+ label="&addressBookCmd.label;"
+ accesskey="&addressBookCmd.accesskey;"
+ key="key_addressbook"
+ command="Tasks:AddressBook"
+ insertafter="tasksMenuEditor"/>
+ </menupopup>
+
+</overlay>
diff --git a/comm/suite/mailnews/content/mailViewList.js b/comm/suite/mailnews/content/mailViewList.js
new file mode 100644
index 0000000000..ef9589ba74
--- /dev/null
+++ b/comm/suite/mailnews/content/mailViewList.js
@@ -0,0 +1,161 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var gMailListView;
+var gListBox;
+var gEditButton;
+var gDeleteButton;
+var gMailViewListController =
+{
+ supportsCommand: function(aCommand)
+ {
+ switch (aCommand)
+ {
+ case "cmd_new":
+ case "cmd_edit":
+ case "cmd_delete":
+ return true;
+ }
+ return false;
+ },
+
+ isCommandEnabled: function(aCommand)
+ {
+ switch (aCommand)
+ {
+ case "cmd_new":
+ return true;
+ case "cmd_edit":
+ case "cmd_delete":
+ return gListBox.selectedIndex >= 0;
+ }
+ return false;
+ },
+
+ doCommand: function(aCommand)
+ {
+ switch (aCommand)
+ {
+ case "cmd_new":
+ OnNewMailView();
+ break;
+ case "cmd_edit":
+ OnEditMailView();
+ break;
+ case "cmd_delete":
+ OnDeleteMailView();
+ break;
+ }
+ },
+
+ onEvent: function(aEvent) {},
+
+ onCommandUpdate: function()
+ {
+ for (let command of ["cmd_new", "cmd_edit", "cmd_delete"])
+ goUpdateCommand(command);
+ }
+};
+
+function MailViewListOnLoad()
+{
+ gMailListView = Cc["@mozilla.org/messenger/mailviewlist;1"]
+ .getService(Ci.nsIMsgMailViewList);
+ gListBox = document.getElementById('mailViewList');
+
+ window.controllers.insertControllerAt(0, gMailViewListController);
+
+ // Construct list view based on current mail view list data
+ RefreshListView(null);
+ gEditButton = document.getElementById('editButton');
+ gDeleteButton = document.getElementById('deleteButton');
+}
+
+function MailViewListOnUnload()
+{
+ window.controllers.removeController(gMailViewListController);
+}
+
+function RefreshListView(aSelectedMailView)
+{
+ // remove any existing items in the view...
+ for (let index = gListBox.getRowCount(); index > 0; index--)
+ gListBox.getItemAtIndex(index - 1).remove();
+
+ var numItems = gMailListView.mailViewCount;
+ for (let index = 0; index < numItems; index++)
+ {
+ let mailView = gMailListView.getMailViewAt(index);
+ gListBox.appendItem(mailView.prettyName, index);
+ if (aSelectedMailView && (mailView.prettyName == aSelectedMailView.prettyName))
+ gListBox.selectedIndex = index;
+ }
+}
+
+function OnNewMailView()
+{
+ window.openDialog('chrome://messenger/content/mailViewSetup.xul',
+ '',
+ 'centerscreen,resizable,modal,titlebar,chrome',
+ {onOkCallback: RefreshListView});
+}
+
+function OnDeleteMailView()
+{
+ let bundle = Services.strings.createBundle("chrome://messenger/locale/messenger.properties");
+
+ let ps = Services.prompt;
+ if (!ps.confirm(window, bundle.GetStringFromName("confirmViewDeleteTitle"),
+ bundle.GetStringFromName("confirmViewDeleteMessage")))
+ return;
+
+ // get the selected index
+ var selectedIndex = gListBox.selectedIndex;
+ if (selectedIndex >= 0)
+ {
+ var mailView = gMailListView.getMailViewAt(selectedIndex);
+ if (mailView)
+ {
+ gMailListView.removeMailView(mailView);
+ // now remove it from the view...
+ gListBox.selectedItem.remove();
+
+ // select the next item in the list..
+ if (selectedIndex < gListBox.getRowCount())
+ gListBox.selectedIndex = selectedIndex;
+ else
+ gListBox.selectedIndex = gListBox.getRowCount() - 1;
+
+ gMailListView.save();
+ }
+ }
+}
+
+function OnEditMailView()
+{
+ // get the selected index
+ var selectedIndex = gListBox.selectedIndex;
+ if (selectedIndex >= 0)
+ {
+ let selMailView = gMailListView.getMailViewAt(selectedIndex);
+ // open up the mail view setup dialog passing in the mail view as an argument
+ let args = {mailView: selMailView, onOkCallback: RefreshListView};
+ window.openDialog('chrome://messenger/content/mailViewSetup.xul',
+ '',
+ 'centerscreen,modal,resizable,titlebar,chrome',
+ args);
+ }
+}
+
+function OnMailViewSelect(aEvent)
+{
+ gMailViewListController.onCommandUpdate();
+}
+
+function OnMailViewDoubleClick(aEvent)
+{
+ if (aEvent.button == 0 && aEvent.target.selected)
+ OnEditMailView();
+}
diff --git a/comm/suite/mailnews/content/mailViewList.xul b/comm/suite/mailnews/content/mailViewList.xul
new file mode 100644
index 0000000000..c055bfd02a
--- /dev/null
+++ b/comm/suite/mailnews/content/mailViewList.xul
@@ -0,0 +1,79 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/" type="text/css"?>
+<!-- Mac needs dialog.css to correctly style the moved Help button -->
+<?xml-stylesheet href="chrome://global/skin/dialog.css" type="text/css"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % mailViewLisDTD SYSTEM "chrome://messenger/locale/mailViewList.dtd">
+%mailViewLisDTD;
+<!ENTITY % FilterListDialogDTD SYSTEM "chrome://messenger/locale/FilterListDialog.dtd">
+%FilterListDialogDTD;
+]>
+
+<dialog id="mailViewListDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="MailViewListOnLoad();"
+ onunload="MailViewListOnUnload();"
+ ondialogaccept="return false;"
+ windowtype="mailnews:mailviewlist"
+ title="&mailViewListTitle.label;"
+ width="400" height="340"
+ buttons=","
+ persist="screenX screenY width height">
+
+ <script src="chrome://messenger/content/mailViewList.js"/>
+ <script src="chrome://global/content/globalOverlay.js"/>
+
+ <commandset id="mailViewCommands">
+ <command id="cmd_new" oncommand="goDoCommand('cmd_new');"/>
+ <command id="cmd_edit" oncommand="goDoCommand('cmd_edit');" disabled="true"/>
+ <command id="cmd_delete" oncommand="goDoCommand('cmd_delete');" disabled="true"/>
+ </commandset>
+
+ <keyset id="mailViewListKeys">
+ <key id="key_delete"/>
+ <key id="key_delete2"/>
+ <key id="key_open" keycode="VK_RETURN" command="cmd_edit"/>
+ </keyset>
+
+ <vbox flex="1">
+ <hbox flex="1">
+ <listbox id="mailViewList"
+ flex="1"
+ onselect="OnMailViewSelect(event);"
+ ondblclick="OnMailViewDoubleClick(event);">
+ <listcols>
+ <listcol flex="1" width="0"/>
+ </listcols>
+ <listhead>
+ <listheader label="&viewName.label;"/>
+ </listhead>
+ </listbox>
+
+ <vbox id="buttonCol">
+ <button id="newButton"
+ label="&newButton.label;"
+ accesskey="&newButton.accesskey;"
+ command="cmd_new"/>
+ <button id="editButton"
+ label="&editButton.label;"
+ accesskey="&editButton.accesskey;"
+ command="cmd_edit"/>
+ <button id="deleteButton"
+ label="&deleteButton.label;"
+ accesskey="&deleteButton.accesskey;"
+ command="cmd_delete"/>
+ <spacer flex="1"/>
+ <button id="helpButton"
+ dlgtype="help"
+ class="dialog-button"/>
+ </vbox>
+ </hbox>
+ </vbox>
+</dialog>
diff --git a/comm/suite/mailnews/content/mailViewSetup.js b/comm/suite/mailnews/content/mailViewSetup.js
new file mode 100644
index 0000000000..4c9b47f070
--- /dev/null
+++ b/comm/suite/mailnews/content/mailViewSetup.js
@@ -0,0 +1,119 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var nsMsgSearchScope = Ci.nsMsgSearchScope;
+var gMailView = null;
+
+var dialog;
+
+function mailViewOnLoad()
+{
+ initializeSearchWidgets();
+ initializeMailViewOverrides();
+ dialog = {};
+
+ if ("arguments" in window && window.arguments[0])
+ {
+ var args = window.arguments[0];
+ if ("mailView" in args)
+ gMailView = window.arguments[0].mailView;
+ if ("onOkCallback" in args)
+ dialog.okCallback = window.arguments[0].onOkCallback;
+ }
+
+ dialog.OKButton = document.documentElement.getButton("accept");
+ dialog.nameField = document.getElementById("name");
+ dialog.nameField.focus();
+
+ setSearchScope(nsMsgSearchScope.offlineMail);
+
+ if (gMailView)
+ {
+ dialog.nameField.value = gMailView.prettyName;
+ initializeSearchRows(nsMsgSearchScope.offlineMail, gMailView.searchTerms);
+ }
+ else
+ onMore(null);
+
+ doEnabling();
+}
+
+function mailViewOnUnLoad()
+{
+
+}
+
+function onOK()
+{
+ var mailViewList = Cc["@mozilla.org/messenger/mailviewlist;1"].getService(Ci.nsIMsgMailViewList);
+
+ // reflect the search widgets back into the search session
+ var newMailView = null;
+ if (gMailView)
+ {
+ gMailView.searchTerms = saveSearchTerms(gMailView.searchTerms, gMailView);
+ // if the name of the view has been changed...
+ if (gMailView.prettyName != dialog.nameField.value)
+ gMailView.mailViewName = dialog.nameField.value;
+ }
+ else
+ {
+ // otherwise, create a new mail view
+ newMailView = mailViewList.createMailView();
+
+ newMailView.searchTerms = saveSearchTerms(newMailView.searchTerms, newMailView);
+ newMailView.mailViewName = dialog.nameField.value;
+ // now add the mail view to our mail view list
+ mailViewList.addMailView(newMailView);
+ }
+
+ mailViewList.save();
+
+ if (dialog.okCallback)
+ dialog.okCallback(gMailView ? gMailView : newMailView);
+
+ return true;
+}
+
+function initializeMailViewOverrides()
+{
+ // replace some text with something we want. Need to add some ids to searchOverlay.js
+ //var orButton = document.getElementById('or');
+ //orButton.setAttribute('label', 'Any of the following');
+ //var andButton = document.getElementById('and');
+ //andButton.setAttribute('label', 'All of the following');
+ // matchAll doesn't make sense for views, since views are a single folder
+ hideMatchAllItem();
+
+}
+
+function UpdateAfterCustomHeaderChange()
+{
+ updateSearchAttributes();
+}
+
+function doEnabling()
+{
+ if (dialog.nameField.value)
+ {
+ if (dialog.OKButton.disabled)
+ dialog.OKButton.disabled = false;
+ } else
+ {
+ if (!dialog.OKButton.disabled)
+ dialog.OKButton.disabled = true;
+ }
+}
+
+function onEnterInSearchTerm()
+{
+ // no-op for us...
+}
+
+function doHelpButton()
+{
+ openHelp("message-views-create-new");
+}
+
diff --git a/comm/suite/mailnews/content/mailViewSetup.xul b/comm/suite/mailnews/content/mailViewSetup.xul
new file mode 100644
index 0000000000..203a8b8991
--- /dev/null
+++ b/comm/suite/mailnews/content/mailViewSetup.xul
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/searchDialog.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/searchTermOverlay.xul"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/mailViewSetup.dtd" >
+
+<dialog id="mailViewSetupDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="mailViewOnLoad();"
+ onunload="mailViewOnUnLoad();"
+ ondialogaccept="return onOK();"
+ buttons="accept,cancel"
+ buttonalign="right"
+ windowtype="mailnews:mailview"
+ title="&mailViewSetupTitle.label;"
+ style="width: 52em; height: 22em;"
+ persist="screenX screenY width height">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/>
+ </stringbundleset>
+
+ <script src="chrome://global/content/globalOverlay.js"/>
+ <script src="chrome://messenger/content/mailViewSetup.js"/>
+
+ <dummy class="usesMailWidgets"/>
+
+ <vbox flex="1">
+ <separator class="thin"/>
+ <vbox>
+ <hbox align="center">
+ <label value="&mailViewHeading.label;" accesskey="&mailViewHeading.accesskey;" control="name"/>
+ <textbox tabindex="0" id="name" oninput="doEnabling();"/>
+ </hbox>
+ </vbox>
+
+ <separator/>
+ <label value="&searchTermCaption.label;"/>
+ <hbox flex="1">
+ <vbox id="searchTermListBox" flex="1"/>
+ </hbox>
+ </vbox>
+
+</dialog>
diff --git a/comm/suite/mailnews/content/mailWidgets.xml b/comm/suite/mailnews/content/mailWidgets.xml
new file mode 100644
index 0000000000..29288b3b70
--- /dev/null
+++ b/comm/suite/mailnews/content/mailWidgets.xml
@@ -0,0 +1,1946 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+
+<bindings id="mailBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <!-- dummy widget to force this file to load -->
+ <binding id="dummy" extends="xul:box"/>
+
+ <!-- temporary holding place for horizontal list -->
+
+ <binding id="extdescription" extends="chrome://global/content/bindings/listbox.xml#listbox-base">
+ <implementation>
+ <constructor><![CDATA[
+ this.children.filter(aChild => aChild.getAttribute("selected") == "true")
+ .forEach(this.selectedItems.append, this.selectedItems);
+ ]]></constructor>
+
+ <!-- ///////////////// public members ///////////////// -->
+
+ <property name="itemCount" readonly="true"
+ onget="return this.children.length;"/>
+
+ <method name="getIndexOfItem">
+ <parameter name="item"/>
+ <body><![CDATA[
+ return this.children.indexOf(item);
+ ]]></body>
+ </method>
+ <method name="getItemAtIndex">
+ <parameter name="index"/>
+ <body><![CDATA[
+ return this.children[index] || null;
+ ]]></body>
+ </method>
+ <method name="getRowCount">
+ <body><![CDATA[
+ return this.children.length;
+ ]]></body>
+ </method>
+ <method name="getNumberOfVisibleRows">
+ <body><![CDATA[
+ var firstItem = this.children[0] || null;
+ if (!firstItem)
+ return 0; // nothing to be visible
+ var itemsPerRow = Math.floor(this.boxObject.width / firstItem.boxObject.width);
+ var itemsPerCol = Math.floor(this.boxObject.height / firstItem.boxObject.height);
+ return Math.max(itemsPerRow, 1) * Math.max(itemsPerCol, 1);
+ ]]></body>
+ </method>
+ <method name="getIndexOfFirstVisibleRow">
+ <body><![CDATA[
+ //XXXzeniko unimplementable without a way to scroll
+ ]]></body>
+ </method>
+
+ <method name="ensureIndexIsVisible">
+ <parameter name="index"/>
+ <body><![CDATA[
+ this.ensureElementIsVisible(this.getItemAtIndex(index));
+ ]]></body>
+ </method>
+ <method name="ensureElementIsVisible">
+ <parameter name="item"/>
+ <body><![CDATA[
+ //XXXzeniko unimplementable without a way to scroll
+ ]]></body>
+ </method>
+ <method name="scrollToIndex">
+ <parameter name="index"/>
+ <body><![CDATA[
+ //XXXzeniko unimplementable without a way to scroll
+ ]]></body>
+ </method>
+
+ <method name="appendItem">
+ <parameter name="label"/>
+ <parameter name="value"/>
+ <body><![CDATA[
+ // -1 appends due to the way getItemAtIndex is implemented
+ return this.insertItemAt(-1, label, value);
+ ]]></body>
+ </method>
+ <method name="insertItemAt">
+ <parameter name="index"/>
+ <parameter name="label"/>
+ <parameter name="value"/>
+ <body><![CDATA[
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var item = document.createElementNS(XULNS, "descriptionitem");
+ item.setAttribute("label", label);
+ this.insertBefore(item, this.getItemAtIndex(index));
+ return item;
+ ]]></body>
+ </method>
+
+ <method name="scrollOnePage">
+ <parameter name="direction"/>
+ <body><![CDATA[
+ return direction * this.getNumberOfVisibleRows();
+ ]]></body>
+ </method>
+
+ <!-- ///////////////// private members ///////////////// -->
+
+ <property name="children" readonly="true"
+ onget="return Array.from(this.getElementsByTagName('descriptionitem'));"/>
+
+ <method name="_fireOnSelect">
+ <body><![CDATA[
+ if (!this._suppressOnSelect && !this.suppressOnSelect) {
+ this.dispatchEvent(new Event("select",
+ { bubbles: false, cancelable: true }));
+ }
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="keypress" keycode="VK_LEFT" modifiers="control shift any"
+ action="this.moveByOffset(-1, !event.ctrlKey, event.shiftKey);"
+ phase="target" preventdefault="true"/>
+ <handler event="keypress" keycode="VK_RIGHT" modifiers="control shift any"
+ action="this.moveByOffset(1, !event.ctrlKey, event.shiftKey);"
+ phase="target" preventdefault="true"/>
+ <handler event="click" button="0" phase="target"><![CDATA[
+ if (this.selType != "multiple" || (!event.ctrlKey && !event.shiftKey && !event.metaKey))
+ this.clearSelection();
+ ]]></handler>
+ <!-- make sure we keep the focus... -->
+ <handler event="mousedown" button="0"
+ action="if (document.commandDispatcher.focusedElement != this) this.focus();"/>
+ </handlers>
+ </binding>
+
+ <binding id="descriptionitem" extends="chrome://global/content/bindings/listbox.xml#listitem">
+ <content>
+ <xul:hbox class="attachmentBox" xbl:inherits="orient" align="start">
+ <xul:label class="descriptioncell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled,context" flex="1" dir="ltr" crop="center"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="descriptionitem-iconic" extends="chrome://global/content/bindings/listbox.xml#listitem">
+ <content>
+ <xul:hbox class="attachmentBox" xbl:inherits="orient" align="center">
+ <xul:image class="descriptioncell-icon" xbl:inherits="src=image"/>
+ <xul:label class="descriptioncell-label" xbl:inherits="value=label,flex=flexlabel,crop,disabled,context" flex="1" dir="ltr" crop="center"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <!-- Message Pane Widgets -->
+
+ <!-- mail-toggle-headerfield: Non-email addrs headers which have a toggle
+ associated with them (i.e. the subject).
+ Use label to set the header name.
+ Use headerValue to set the header value. -->
+ <binding id="mail-toggle-headerfield">
+ <content>
+ <xul:hbox class="headerNameBox" align="start">
+ <xul:image class="expandHeaderViewButton" xbl:inherits="onclick=ontwistyclick"/>
+ <xul:spacer flex="1"/>
+ <xul:label class="headerName" xbl:inherits="value=label" control="headerValue"/>
+ </xul:hbox>
+ <xul:hbox class="headerValueBox" flex="1" align="start">
+ <xul:textbox class="headerValue plain" anonid="headerValue" flex="1" readonly="true"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <property name="headerValue" onset="return document.getAnonymousElementByAttribute(this, 'anonid', 'headerValue').value = val;"/>
+ </implementation>
+ </binding>
+
+ <!-- mail-headerfield: presents standard text header name & value pairs. Don't use this for email addresses.
+ use label to set the header name.
+ use headerValue to set the header value. -->
+ <binding id="mail-headerfield">
+ <content>
+ <xul:hbox class="headerNameBox" align="start">
+ <xul:label class="headerName" xbl:inherits="value=label" control="headerValue" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="headerValueBox" flex="1" align="start">
+ <xul:textbox class="headerValue plain" anonid="headerValue" flex="1" readonly="true"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <property name="headerValue" onset="return document.getAnonymousElementByAttribute(this, 'anonid', 'headerValue').value = val;"/>
+ </implementation>
+ </binding>
+
+ <binding id="mail-urlfield" extends="chrome://messenger/content/mailWidgets.xml#mail-headerfield">
+ <content>
+ <xul:hbox class="headerNameBox" align="start">
+ <xul:label class="headerName" xbl:inherits="value=label" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="headerValueBox" flex="1" align="start">
+ <xul:label onclick="if (event.button != 2) openAsExternal(event.target.value);"
+ ondragstart="this.parentNode.setDataTransfer(event);"
+ class="headerValue plain text-link headerValueUrl"
+ anonid="headerValue" flex="1" readonly="true" context="copyUrlPopup"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <method name="setDataTransfer">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ var dt = aEvent.dataTransfer;
+ var val = aEvent.target.value;
+ dt.setData('text/x-moz-url', val + "\n" + val);
+ dt.setData('text/uri-list', val);
+ dt.setData('text/plain', val);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="mail-emailheaderfield">
+ <content>
+ <xul:hbox class="headerNameBox" align="start">
+ <xul:label class="headerName" xbl:inherits="value=label" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="headerValueBox" flex="1" align="start">
+ <xul:mail-emailaddress class="headerValue" anonid="emailAddressNode"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <property name="emailAddressNode" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'emailAddressNode');"
+ readonly="true"/>
+ </implementation>
+ </binding>
+
+ <!-- multi-emailHeaderField: presents multiple emailheaderfields with a toggle -->
+ <binding id="mail-multi-emailHeaderField">
+ <content>
+ <xul:hbox class="headerNameBox" align="start" pack="end">
+ <xul:image class="addresstwisty" anonid="toggleIcon"
+ collapsed="true" onclick="toggleWrap();"/>
+ <xul:label class="headerName" xbl:inherits="value=label"/>
+ </xul:hbox>
+
+ <xul:hbox class="headerValueBox" anonid="longEmailAddresses" flex="1" align="start"
+ onoverflow="if (event.detail != 1) this.parentNode.toggleIcon.collapsed = false;"
+ onunderflow="if (event.detail != 1) this.parentNode.toggleIcon.collapsed = true;">
+ <xul:description class="headerValue" anonid="emailAddresses" flex="1"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this.mAddresses = new Array;
+ ]]>
+ </constructor>
+
+ <field name="mAddresses"/>
+ <!-- as a perf optimization we are going to keep a cache of email address nodes which we've
+ created around for the lifetime of the widget. mSizeOfAddressCache controls how many of these
+ elements we keep around -->
+ <field name="mSizeOfAddressCache">3</field>
+
+ <!-- addAddressView: a public method used to add an address to this widget.
+ aAddresses is an object with 3 properties: displayName, emailAddress and fullAddress
+ -->
+ <method name="addAddressView">
+ <parameter name="aAddress"/>
+ <body>
+ <![CDATA[
+ this.mAddresses.push(aAddress);
+ ]]>
+ </body>
+ </method>
+
+ <!-- updateEmailAddressNode: private method used to set properties on an address node -->
+ <method name="updateEmailAddressNode">
+ <parameter name="aEmailNode"/>
+ <parameter name="aAddress"/>
+ <body>
+ <![CDATA[
+ if (aEmailNode.parentNode.useShortView && aAddress.displayName)
+ {
+ aEmailNode.setAttribute("label", aAddress.displayName);
+ aEmailNode.setAttribute("tooltiptext", aAddress.fullAddress);
+ }
+ else
+ {
+ aEmailNode.setAttribute("label", aAddress.fullAddress || aAddress.displayName);
+ aEmailNode.removeAttribute("tooltiptext");
+ }
+ aEmailNode.setAttribute("emailAddress", aAddress.emailAddress);
+ aEmailNode.setAttribute("fullAddress", aAddress.fullAddress);
+ aEmailNode.setAttribute("displayName", aAddress.displayName);
+
+ // Add aria-label with header field type and header field content
+ // for better accessibility.
+ // Note: No extra colon and space needed, since it is
+ // already provided by this object's label attribute.
+ var ariaLabel = this.getAttribute("label") +
+ aEmailNode.getAttribute("label");
+ aEmailNode.setAttribute("aria-label", ariaLabel);
+
+ try
+ {
+ if ("UpdateEmailNodeDetails" in top)
+ UpdateEmailNodeDetails(aAddress.emailAddress, aEmailNode);
+ }
+ catch(ex)
+ {
+ dump("UpdateEmailNodeDetails failed: " + ex + "\n");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- fillCachedAddresses: private method used to fill up any cached pre-existing
+ emailAddress fields without creating new email address fields. Returns a remainder
+ for the # of addresses which require new addresses being created.
+ Invariants: 1) aNumAddressesToShow >= 0 && it is <= mAddresses.length -->
+ <method name="fillCachedAddresses">
+ <parameter name="aAddressesNode"/>
+ <parameter name="aNumAddressesToShow"/>
+ <body>
+ <![CDATA[
+ var numExistingCachedAddresses = aAddressesNode.childNodes.length;
+ if (!numExistingCachedAddresses)
+ return this.mAddresses.length; // we couldn't pre fill anything
+ else if (numExistingCachedAddresses > 1)
+ numExistingCachedAddresses = (numExistingCachedAddresses + 1)/ 2;
+
+ var index = 0;
+ var numAddressesAdded = 0;
+ var emailAddressNode;
+ var commaNode;
+ while (numAddressesAdded < numExistingCachedAddresses && numAddressesAdded < aNumAddressesToShow)
+ {
+ if (index && numExistingCachedAddresses > 1)
+ {
+ commaNode = aAddressesNode.childNodes[index++];
+ if (commaNode)
+ commaNode.hidden = false;
+ }
+
+ // get the node pointed to by index
+ emailAddressNode = aAddressesNode.childNodes[index++];
+ this.updateEmailAddressNode(emailAddressNode, this.mAddresses[numAddressesAdded]);
+ emailAddressNode.hidden = false;
+ numAddressesAdded++;
+ }
+
+ // if we have added all of our elements but we still have more cached items in this address node
+ // then make sure the extra cached copies are hidden...
+ numExistingCachedAddresses = aAddressesNode.childNodes.length; // reset
+ while (index < numExistingCachedAddresses)
+ {
+ aAddressesNode.childNodes[index++].hidden = true;
+ }
+
+ return this.mAddresses.length - numAddressesAdded;
+ ]]>
+ </body>
+ </method>
+
+ <!-- fillAddressesNode: private method used to create email address nodes for either our short
+ or long view. aAddressesNode: the div we want to add addresses too.
+ aNumAddressesToShow: number of addresses to put into the list -->
+ <method name="fillAddressesNode">
+ <parameter name="aAddressesNode"/>
+ <parameter name="aNumAddressesToShow"/>
+ <body>
+ <![CDATA[
+ var numAddresses = this.mAddresses.length;
+ if (aNumAddressesToShow <= 0 || aNumAddressesToShow > numAddresses) // then show all
+ aNumAddressesToShow = numAddresses;
+
+ // before we try to create email address nodes, try to leverage any cached nodes...
+ var remainder = this.fillCachedAddresses(aAddressesNode, aNumAddressesToShow);
+ var index = numAddresses - remainder;
+ while (index < numAddresses && index < aNumAddressesToShow)
+ {
+ var newAddressNode = document.createElement("mail-emailaddress");
+
+ // Stash the headerName somewhere that UpdateEmailNodeDetails
+ // will be able to find it.
+ newAddressNode.setAttribute("headerName", this.headerName);
+
+ if (index)
+ {
+ var textNode = document.createElement("text");
+ textNode.setAttribute("value", ", ");
+ textNode.setAttribute("class", "emailSeparator");
+ aAddressesNode.appendChild(textNode);
+ }
+
+ var itemInDocument = aAddressesNode.appendChild(newAddressNode);
+ this.updateEmailAddressNode(itemInDocument, this.mAddresses[index]);
+ index++;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="emailAddresses" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'emailAddresses');"
+ readonly="true"/>
+ <property name="longEmailAddresses" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'longEmailAddresses');"
+ readonly="true"/>
+ <property name="toggleIcon" onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'toggleIcon');"
+ readonly="true"/>
+
+ <!-- buildView: public method used by callers when they are done adding all the email addresses to the widget
+ aNumAddressesToShow: total # of addresses to show in the short view -->
+ <method name="buildViews">
+ <body>
+ <![CDATA[
+ this.fillAddressesNode(this.emailAddresses, -1);
+ ]]>
+ </body>
+ </method>
+
+ <!-- Updates the nodes of this field with a call to
+ UpdateExtraAddressProcessing. The parameters are optional fields
+ that can contain extra information to be passed to
+ UpdateExtraAddressProcessing, the implementation of that function
+ should be checked to determine what it requires -->
+ <method name="updateExtraAddressProcessing">
+ <parameter name="aParam1"/>
+ <parameter name="aParam2"/>
+ <parameter name="aParam3"/>
+ <body>
+ <![CDATA[
+ if (UpdateExtraAddressProcessing) {
+ var childNodes = this.emailAddresses.childNodes;
+ for (let i = 0; i < this.mAddresses.length; i++) {
+ UpdateExtraAddressProcessing(this.mAddresses[i],
+ childNodes[i * 2],
+ aParam1, aParam2, aParam3);
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="toggleWrap">
+ <body>
+ <![CDATA[
+ if (this.toggleIcon.hasAttribute("open")) {
+ this.toggleIcon.removeAttribute("open");
+ this.longEmailAddresses.setAttribute("singleline", "true");
+ } else {
+ this.toggleIcon.setAttribute("open", "true");
+ this.longEmailAddresses.removeAttribute("singleline");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- internal method used to clear both our divs -->
+ <method name="clearChildNodes">
+ <parameter name="aParentNode"/>
+ <body>
+ <![CDATA[
+ // we want to keep around the first mSizeOfAddressCache email address nodes
+ // don't forget that we have comma text nodes in there too so really we want to keep
+ // around cache size * 2 - 1.
+ var numItemsToPreserve = this.mSizeOfAddressCache * 2 - 1;
+ var numItemsInNode = aParentNode.childNodes.length;
+
+ while (numItemsInNode && (numItemsInNode > numItemsToPreserve))
+ {
+ aParentNode.childNodes[numItemsInNode - 1].remove();
+ numItemsInNode = numItemsInNode - 1;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="clearHeaderValues">
+ <body>
+ <![CDATA[
+ // clear out our local state
+ this.mAddresses = new Array;
+ if (this.toggleIcon.hasAttribute("open"))
+ // no automatic overflow tracking in this case
+ this.toggleIcon.collapsed = true;
+ this.toggleIcon.removeAttribute("open");
+ this.longEmailAddresses.setAttribute("singleline", "true");
+ // remove anything inside of each of our labels....
+ this.clearChildNodes(this.emailAddresses);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="mail-emailaddress">
+ <content popup="emailAddressPopup" context="emailAddressPopup">
+ <xul:description anonid="emailValue" class="emailDisplayButton plain"
+ xbl:inherits="xbl:text=label,crop,aria-label" flex="1"/>
+ <xul:image class="emailDisplayImage" anonid="emailImage"
+ xbl:inherits="src=image"/>
+ </content>
+
+ <implementation>
+ <property name="label" onset="this.getPart('emailValue').setAttribute('label',val); return val;"
+ onget="return this.getPart('emailValue').getAttribute('label');"/>
+ <property name="crop" onset="this.getPart('emailValue').setAttribute('crop',val); return val;"
+ onget="return this.getPart('emailValue').getAttribute('crop');"/>
+ <property name="disabled" onset="this.getPart('emailValue').setAttribute('disabled',val); return val;"
+ onget="return this.getPart('emailValue').getAttribute('disabled');"/>
+ <property name="src" onset="this.getPart('emailImage').setAttribute('src',val); return val;"
+ onget="return this.getPart('emailImage').getAttribute('src');"/>
+ <property name="imgalign" onset="this.getPart('emailImage').setAttribute('imgalign',val); return val;"
+ onget="return this.getPart('emailImage').getAttribute('imgalign');"/>
+
+ <method name="getPart">
+ <parameter name="aPartId"/>
+ <body><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "anonid", aPartId);
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="mail-messageids-headerfield">
+ <content>
+ <xul:hbox class="headerNameBox" align="start" pack="end">
+ <xul:image class="addresstwisty" anonid="toggleIcon"
+ onclick="toggleWrap();"/>
+ <xul:label class="headerName" xbl:inherits="value=label"/>
+ </xul:hbox>
+ <xul:hbox class="headerValueBox" flex="1" align="start">
+ <xul:label class="headerValue" anonid="headerValue" flex="1"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this.mMessageIds = [];
+ this.showFullMessageIds = false;
+ ]]>
+ </constructor>
+
+ <property name="headerValue" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'headerValue');"/>
+ <property name="toggleIcon" readonly="true"
+ onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'toggleIcon');"/>
+
+ <field name="mMessageIds"/>
+
+ <!-- addMessageIdView: a public method used to add a message-id to this widget. -->
+ <method name="addMessageIdView">
+ <parameter name="aMessageId"/>
+ <body>
+ <![CDATA[
+ this.mMessageIds.push(aMessageId);
+ ]]>
+ </body>
+ </method>
+
+ <!-- updateMessageIdNode: private method used to set properties on an MessageId node -->
+ <method name="updateMessageIdNode">
+ <parameter name="aMessageIdNode"/>
+ <parameter name="aIndex"/>
+ <parameter name="aMessageId"/>
+ <parameter name="aLastId"/>
+ <body>
+ <![CDATA[
+ var showFullMessageIds = this.showFullMessageIds;
+
+ if (showFullMessageIds || aIndex == aLastId)
+ {
+ aMessageIdNode.setAttribute("label", aMessageId);
+ aMessageIdNode.removeAttribute("tooltiptext");
+ }
+ else
+ {
+ aMessageIdNode.setAttribute("label", aIndex);
+ aMessageIdNode.setAttribute("tooltiptext", aMessageId);
+ }
+
+ aMessageIdNode.setAttribute("index", aIndex);
+ aMessageIdNode.setAttribute("messageid", aMessageId);
+ ]]>
+ </body>
+ </method>
+
+ <method name="fillMessageIdNodes">
+ <body>
+ <![CDATA[
+ var headerValue = this.headerValue;
+ var messageIdNodes = headerValue.childNodes;
+ var numMessageIds = this.mMessageIds.length;
+ var index = 0;
+
+ while (messageIdNodes.length > numMessageIds * 2 - 1)
+ headerValue.lastChild.remove();
+
+ this.toggleIcon.hidden = numMessageIds <= 1;
+
+ for (var index = 0; index < numMessageIds; index++)
+ {
+ if (index * 2 <= messageIdNodes.length - 1)
+ {
+ this.updateMessageIdNode(messageIdNodes[index * 2], index + 1,
+ this.mMessageIds[index], numMessageIds);
+ }
+ else
+ {
+ var newMessageIdNode = document.createElement("mail-messageid");
+
+ if (index)
+ {
+ var textNode = document.createElement("text");
+ textNode.setAttribute("value", ", ");
+ textNode.setAttribute("class", "messageIdSeparator");
+ headerValue.appendChild(textNode);
+ }
+ var itemInDocument = headerValue.appendChild(newMessageIdNode);
+ this.updateMessageIdNode(itemInDocument, index + 1,
+ this.mMessageIds[index], numMessageIds);
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="toggleWrap">
+ <body>
+ <![CDATA[
+ var headerValue = this.headerValue;
+ var messageIdNodes = headerValue.childNodes;
+ var showFullMessageIds = !this.showFullMessageIds;
+ var messageIds = this.mMessageIds
+
+ for (var i = 0; i < messageIdNodes.length; i += 2)
+ {
+ if (showFullMessageIds)
+ {
+ this.toggleIcon.setAttribute("open", "true");
+ messageIdNodes[i].setAttribute("label", messageIds[i / 2]);
+ messageIdNodes[i].removeAttribute("tooltiptext");
+ headerValue.removeAttribute("singleline");
+ } else
+ {
+ this.toggleIcon.removeAttribute("open");
+ messageIdNodes[i].setAttribute("label", i / 2 + 1);
+ messageIdNodes[i].setAttribute("tooltiptext", messageIds[i / 2]);
+ }
+ }
+
+ this.showFullMessageIds = showFullMessageIds;
+ ]]>
+ </body>
+ </method>
+
+ <method name="clearHeaderValues">
+ <body>
+ <![CDATA[
+ // clear out our local state
+ this.mMessageIds = new Array;
+ if (this.showFullMessageIds)
+ {
+ this.showFullMessageIds = false;
+ this.toggleIcon.removeAttribute("open");
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="mail-messageid">
+ <content context="messageIdContext" onclick="MessageIdClick(this, event);">
+ <xul:label anonid="messageIdValue" class="messageIdDisplayButton plain"
+ xbl:inherits="value=label"/>
+ <xul:image class="messageIdDisplayImage" anonid="messageIdImage"/>
+ </content>
+
+ <implementation>
+ <property name="label" onset="this.getPart().setAttribute('label',val); return val;"
+ onget="return this.getPart('messageIdValue').getAttribute('label');"/>
+
+ <method name="getPart">
+ <parameter name="aPartId"/>
+ <body><![CDATA[
+ return document.getAnonymousElementByAttribute(this, "anonid", 'messageIdValue');
+ ]]></body>
+ </method>
+ </implementation>
+ </binding>
+
+ <!-- Header field for showing the tags associated with a message -->
+ <binding id="mail-headerfield-tags">
+ <content>
+ <xul:hbox class="headerNameBox" align="start">
+ <xul:label class="headerName" xbl:inherits="value=label" flex="1"/>
+ </xul:hbox>
+ <xul:hbox class="headerValueBox" flex="1" align="start">
+ <xul:label class="headerValue plain" anonid="headerValue" flex="1"/>
+ </xul:hbox>
+ </content>
+
+ <implementation>
+ <property name="headerValue" onset="return this.buildTags(val);"/>
+ <method name="buildTags">
+ <parameter name="aTags"/>
+ <body>
+ <![CDATA[
+ // aTags contains a list of actual tag names (not the keys), delimited by spaces
+ // each tag name is encoded.
+
+ // remove any existing tag items we've appended to the list
+ var headerValueNode = document.getAnonymousElementByAttribute(this, 'anonid', 'headerValue');
+ for (var i = headerValueNode.childNodes.length - 1; i >= 0; --i)
+ headerValueNode.childNodes[i].remove();
+
+ // tokenize the keywords based on ' '
+ var tagsArray = aTags.split(' ');
+ for (var index = 0; index < tagsArray.length; index++)
+ {
+ // for each tag, create a label, give it the font color that corresponds to the
+ // color of the tag and append it.
+ var tagName;
+ try {
+ // if we got a bad tag name, getTagForKey will throw an exception, skip it
+ // and go to the next one.
+ tagName = MailServices.tags.getTagForKey(tagsArray[index]);
+ } catch (ex) { continue; }
+
+ var color = MailServices.tags.getColorForKey(tagsArray[index]);
+
+ // now create a label for the tag name, and set the color
+ var label = document.createElement("label");
+ label.setAttribute('value', tagName);
+ label.style.color = color;
+ label.className = "tagvalue blc-" + color.substr(1);
+ headerValueNode.appendChild(label);
+ }
+ ]]>
+ </body>
+ </method>
+ <constructor>
+ <![CDATA[
+ var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="search-menulist-abstract" name="searchMenulistAbstract" extends="xul:box">
+ <content>
+ <xul:menulist class="search-menulist" xbl:inherits="flex,disabled" oncommand="this.parentNode.onSelect(event)">
+ <xul:menupopup class="search-menulist-popup"/>
+ </xul:menulist>
+ </content>
+
+ <implementation>
+ <field name="internalScope">null</field>
+ <field name="internalValue">-1</field>
+ <field readonly="true" name="validityManager">
+ <![CDATA[
+ Cc['@mozilla.org/mail/search/validityManager;1'].getService(Ci.nsIMsgSearchValidityManager);
+ ]]>
+ </field>
+ <property name="searchScope" onget="return this.internalScope;">
+ <!-- scope ID - retrieve the table -->
+ <setter>
+ <![CDATA[
+ // if scope isn't changing this is a noop
+ if (this.internalScope == val) return val;
+
+ this.internalScope = val;
+ this.refreshList();
+ var targets = this.targets;
+ if (targets) {
+ for (var i=0; i< targets.length; i++) {
+ targets[i].searchScope = val;
+ }
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="validityTable" readonly="true" onget="return this.validityManager.getTable(this.searchScope)"/>
+
+ <property name="targets" readonly="true">
+ <getter>
+ <![CDATA[
+ var forAttrs = this.getAttribute("for");
+ if (!forAttrs) return null;
+ var targetIds = forAttrs.split(",");
+ if (targetIds.length == 0) return null;
+
+ var targets = new Array;
+ for (let j = 0, i = 0; i < targetIds.length; i++) {
+ var target = document.getElementById(targetIds[i]);
+ if (target) targets[j++] = target;
+ }
+ return targets;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="optargets" readonly="true">
+ <getter>
+ <![CDATA[
+ var forAttrs = this.getAttribute("opfor");
+ if (!forAttrs) return null;
+ var optargetIds = forAttrs.split(",");
+ if (optargetIds.length == 0) return null;
+
+ var optargets = new Array;
+ var j=0;
+ for (var i=0; i<optargetIds.length;i++) {
+ var optarget = document.getElementById(optargetIds[i]);
+ if (optarget) optargets[j++] = optarget;
+ }
+ return optargets;
+ ]]>
+ </getter>
+ </property>
+
+ <property name="value" onget="return this.internalValue;">
+ <setter>
+ <![CDATA[
+ if (this.internalValue == val)
+ return val;
+ this.internalValue = val;
+ var menulist = document.getAnonymousNodes(this)[0];
+ menulist.selectedItem = this.validMenuitem;
+
+ // now notify targets of new parent's value
+ var targets = this.targets;
+ if (targets) {
+ for (var i=0; i < targets.length; i++) {
+ targets[i].parentValue = val;
+ }
+ }
+
+ // now notify optargets of new op parent's value
+ var optargets = this.optargets;
+ if (optargets) {
+ for (i=0; i < optargets.length; i++) {
+ optargets[i].opParentValue = val;
+ }
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <!-- label forwards to the internal menulist's "label" attribute -->
+ <property name="label" onget="return document.getAnonymousNodes(this)[0].selectedItem.getAttribute('label');">
+ </property>
+ <property name="validMenuitem" readonly="true">
+ <!-- Prepare menulist selection, adding a missing hidden menuitem if needed, and
+ updating the disabled state of the menulist label. -->
+ <getter>
+ <![CDATA[
+ if (this.value == -1) // -1 means not initialized
+ return null;
+
+ let menulist = document.getAnonymousNodes(this)[0];
+ let isCustom = isNaN(this.value);
+ let typedValue = isCustom ? this.value : parseInt(this.value);
+
+ // custom attribute to style the unavailable menulist item
+ menulist.setAttribute("unavailable",
+ !this.valueIds.includes(typedValue));
+
+ // add a hidden menulist item if value is missing
+ let menuitem = menulist.getElementsByAttribute("value", this.value).item(0);
+ if (!menuitem)
+ { // need to add a hidden menuitem
+ menuitem = menulist.appendItem(this.valueLabel, this.value);
+ menuitem.hidden = true;
+ }
+ return menuitem;
+ ]]>
+ </getter>
+ </property>
+ <method name="refreshList">
+ <parameter name="dontRestore"/> <!-- should we not restore old selection? -->
+ <body>
+ <![CDATA[
+ var menuItemIds = this.valueIds;
+ var menuItemStrings = this.valueStrings;
+
+ var menulist = document.getAnonymousNodes(this)[0];
+ var popup = menulist.firstChild;
+
+ // save our old "value" so we can restore it later
+ var oldData;
+ if (!dontRestore)
+ oldData = menulist.value;
+
+ // remove the old popup children
+ while (popup.hasChildNodes())
+ popup.lastChild.remove();
+
+ var newSelection;
+ var customizePos=-1;
+ for (var i = 0; i < menuItemIds.length; ++i)
+ {
+ // create the menuitem
+ if (Ci.nsMsgSearchAttrib.OtherHeader == menuItemIds[i].toString())
+ customizePos = i;
+ else
+ {
+ var menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("label", menuItemStrings[i]);
+ menuitem.setAttribute("value", menuItemIds[i]);
+ popup.appendChild(menuitem);
+ // try to restore the selection
+ if (!newSelection || oldData == menuItemIds[i].toString())
+ newSelection = menuitem;
+ }
+ }
+ if (customizePos != -1)
+ {
+ var separator = document.createElement("menuseparator");
+ popup.appendChild(separator);
+ menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("label", menuItemStrings[customizePos]);
+ menuitem.setAttribute("value", menuItemIds[customizePos]);
+ popup.appendChild(menuitem);
+ }
+ //
+ // If we are either uninitialized, or if we are called because
+ // of a change in our parent, update the value to the
+ // default stored in newSelection.
+ //
+ if ((this.value == -1 || dontRestore) && newSelection)
+ this.value = newSelection.getAttribute("value");
+ menulist.selectedItem = this.validMenuitem;
+ ]]>
+ </body>
+ </method>
+ <method name="onSelect">
+ <parameter name="event"/>
+ <body>
+ <![CDATA[
+ var menulist = document.getAnonymousNodes(this)[0];
+ if (menulist.value == Ci.nsMsgSearchAttrib.OtherHeader) {
+ // Customize menuitem selected.
+ let args = {};
+ window.openDialog("chrome://messenger/content/CustomHeaders.xul",
+ "",
+ "modal,centerscreen,resizable,titlebar,chrome",
+ args);
+ // User may have removed the custom header currently selected in
+ // the menulist so temporarily set the selection to a safe value.
+ this.value = Ci.nsMsgSearchAttrib.OtherHeader;
+ // rebuild the menulist
+ UpdateAfterCustomHeaderChange();
+ // Find the created or chosen custom header and select it.
+ if (args.selectedVal) {
+ let menuitem = menulist.querySelector('[label="' +
+ args.selectedVal + '"]');
+ this.value = menuitem.value;
+ } else {
+ // Nothing was picked in the custom headers editor so just pick
+ // something instead of the current "Customize" menuitem.
+ this.value = menulist.getItemAtIndex(0).value;
+ }
+ } else {
+ this.value = menulist.value;
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <!-- searchattribute - Subject, Sender, To, CC, etc. -->
+ <binding id="searchattribute" name="searchAttribute"
+ extends="chrome://messenger/content/mailWidgets.xml#search-menulist-abstract">
+ <implementation>
+ <field name="stringBundle">
+ <![CDATA[
+ this.Services.strings.createBundle(
+ "chrome://messenger/locale/search-attributes.properties");
+ ]]>
+ </field>
+ <property name="valueLabel" readonly="true">
+ <getter>
+ <![CDATA[
+ if (isNaN(this.value)) // is this a custom term?
+ {
+ let customTerm = MailServices.filters.getCustomTerm(this.value);
+ if (customTerm)
+ return customTerm.name;
+ // The custom term may be missing after the extension that added
+ // it was disabled or removed. We need to notify the user.
+ let scriptError = Cc["@mozilla.org/scripterror;1"]
+ .createInstance(Ci.nsIScriptError);
+ scriptError.init("Missing custom search term " + this.value,
+ null, null, 0, 0, Ci.nsIScriptError.errorFlag,
+ "component javascript");
+ this.Services.console.logMessage(scriptError);
+ return this.stringBundle.GetStringFromName("MissingCustomTerm");
+ }
+ return this.stringBundle.GetStringFromName(
+ this.validityManager.getAttributeProperty(parseInt(this.value)));
+ ]]>
+ </getter>
+ </property>
+ <property name="valueIds" readonly="true">
+ <getter>
+ <![CDATA[
+ let result = this.validityTable.getAvailableAttributes();
+ // add any available custom search terms
+ for (let customTerm of MailServices.filters.getCustomTerms()) {
+ // for custom terms, the array element is a string with the custom id
+ // instead of the integer attribute
+ if (customTerm.getAvailable(this.searchScope, null))
+ result.push(customTerm.id);
+ }
+ return result;
+ ]]>
+ </getter>
+ </property>
+ <property name="valueStrings" readonly="true">
+ <getter>
+ <![CDATA[
+ let strings = new Array;
+ let ids = this.valueIds;
+ let hdrsArray = null;
+ try
+ {
+ let hdrs =
+ this.Services.prefs.getCharPref("mailnews.customHeaders");
+ hdrs = hdrs.replace(/\s+/g, ""); //remove white spaces before splitting
+ hdrsArray = hdrs.match(/[^:]+/g);
+ }
+ catch(ex)
+ {
+ }
+
+ let j = 0;
+ for (let i = 0; i < ids.length; i++)
+ {
+ if (isNaN(ids[i])) // Is this a custom search term?
+ {
+ let customTerm = MailServices.filters.getCustomTerm(ids[i]);
+ if (customTerm)
+ strings[i] = customTerm.name;
+ else
+ strings[i] = "";
+ }
+ else if(ids[i] > Ci.nsMsgSearchAttrib.OtherHeader && hdrsArray)
+ strings[i] = hdrsArray[j++];
+ else
+ strings[i] = this.stringBundle.GetStringFromName(
+ this.validityManager.getAttributeProperty(ids[i]));
+ }
+ return strings;
+ ]]>
+ </getter>
+ </property>
+ <constructor>
+ <![CDATA[
+ var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ ChromeUtils.import("resource://gre/modules/Services.jsm", this);
+ initializeTermFromId(this.id);
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <!-- searchoperator - Contains, Is Less than, etc -->
+ <binding id="searchoperator" name="searchOperator"
+ extends="chrome://messenger/content/mailWidgets.xml#search-menulist-abstract">
+ <implementation>
+ <field name="searchAttribute">Ci.nsMsgSearchAttrib.Default</field>
+ <field name="stringBundle">
+ <![CDATA[
+ this.Services.strings.createBundle("chrome://messenger/locale/search-operators.properties")
+ ]]>
+ </field>
+ <property name="valueLabel" readonly="true">
+ <getter>
+ <![CDATA[
+ return this.stringBundle.GetStringFromName(this.value);
+ ]]>
+ </getter>
+ </property>
+ <property name="valueIds" readonly="true">
+ <getter>
+ <![CDATA[
+ let isCustom = isNaN(this.searchAttribute);
+ if (isCustom)
+ {
+ let customTerm = MailServices.filters.getCustomTerm(this.searchAttribute);
+ if (customTerm)
+ return customTerm.getAvailableOperators(this.searchScope);
+ return [Ci.nsMsgSearchOp.Contains];
+ }
+ return this.validityTable.getAvailableOperators(this.searchAttribute);
+ ]]>
+ </getter>
+ </property>
+ <property name="valueStrings" readonly="true">
+ <getter>
+ <![CDATA[
+ let strings = new Array;
+ let ids = this.valueIds;
+ for (let i = 0; i < ids.length; i++)
+ strings[i] = this.stringBundle.GetStringFromID(ids[i]);
+ return strings;
+ ]]>
+ </getter>
+ </property>
+ <property name="parentValue">
+ <setter>
+ <![CDATA[
+ if (this.searchAttribute == val && val != Ci.nsMsgSearchAttrib.OtherHeader) return val;
+ this.searchAttribute = val;
+ this.refreshList(true); // don't restore the selection, since searchvalue nulls it
+ if (val == Ci.nsMsgSearchAttrib.AgeInDays) {
+ // Bug 187741 We want "Age in Days" to default to "is less than".
+ this.value = Ci.nsMsgSearchOp.IsLessThan;
+ }
+ return val;
+ ]]>
+ </setter>
+ <getter>
+ <![CDATA[
+ return this.searchAttribute;
+ ]]>
+ </getter>
+ </property>
+ <constructor>
+ <![CDATA[
+ var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ ChromeUtils.import("resource://gre/modules/Services.jsm", this);
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <!-- searchvalue - a widget which dynamically changes its user interface
+ depending on what type of data it's supposed to be showing
+ currently handles arbitrary text entry, and menulists for
+ priority, status, junk status, tags, hasAttachment status,
+ and addressbook
+ -->
+ <binding id="searchvalue" name="searchValue">
+ <content>
+ <xul:textbox flex="1" class="search-value-textbox" xbl:inherits="disabled"/>
+ <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
+ <xul:menupopup class="search-value-popup">
+ <xul:menuitem value="6" stringTag="priorityHighest" class="search-value-menuitem"/>
+ <xul:menuitem value="5" stringTag="priorityHigh" class="search-value-menuitem"/>
+ <xul:menuitem value="4" stringTag="priorityNormal" class="search-value-menuitem"/>
+ <xul:menuitem value="3" stringTag="priorityLow" class="search-value-menuitem"/>
+ <xul:menuitem value="2" stringTag="priorityLowest" class="search-value-menuitem"/>
+ </xul:menupopup>
+ </xul:menulist>
+ <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
+ <xul:menupopup class="search-value-popup">
+ <xul:menuitem value="2" stringTag="replied" class="search-value-menuitem"/>
+ <xul:menuitem value="1" stringTag="read" class="search-value-menuitem"/>
+ <xul:menuitem value="65536" stringTag="new" class="search-value-menuitem"/>
+ <xul:menuitem value="4096" stringTag="forwarded" class="search-value-menuitem"/>
+ <xul:menuitem value="4" stringTag="flagged" class="search-value-menuitem"/>
+ </xul:menupopup>
+ </xul:menulist>
+ <xul:textbox flex="1" class="search-value-textbox" xbl:inherits="disabled"/>
+ <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
+ <xul:menupopup class="search-value-popup addrbooksPopup" localonly="true"/>
+ </xul:menulist>
+ <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
+ <xul:menupopup class="search-value-popup">
+ </xul:menupopup>
+ </xul:menulist>
+ <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
+ <xul:menupopup class="search-value-popup">
+ <xul:menuitem value="2" stringTag="junk" class="search-value-menuitem"/>
+ </xul:menupopup>
+ </xul:menulist>
+ <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
+ <xul:menupopup class="search-value-popup">
+ <xul:menuitem value="0" stringTag="hasAttachments" class="search-value-menuitem"/>
+ </xul:menupopup>
+ </xul:menulist>
+ <xul:menulist flex="1" class="search-value-menulist" xbl:inherits="disabled">
+ <xul:menupopup class="search-value-popup">
+ <xul:menuitem value="plugin" stringTag="junkScoreOriginPlugin"
+ class="search-value-menuitem"/>
+ <xul:menuitem value="user" stringTag="junkScoreOriginUser"
+ class="search-value-menuitem"/>
+ <xul:menuitem value="filter" stringTag="junkScoreOriginFilter"
+ class="search-value-menuitem"/>
+ <xul:menuitem value="whitelist" stringTag="junkScoreOriginWhitelist"
+ class="search-value-menuitem"/>
+ <xul:menuitem value="imapflag" stringTag="junkScoreOriginImapFlag"
+ class="search-value-menuitem"/>
+ </xul:menupopup>
+ </xul:menulist>
+ <xul:hbox flex="1" class="search-value-custom" xbl:inherits="disabled"/>
+ </content>
+ <implementation>
+ <field name="internalOperator">null</field>
+ <field name="internalAttribute">null</field>
+ <field name="internalValue">null</field>
+ <property name="opParentValue" onget="return this.internalOperator;">
+ <setter>
+ <![CDATA[
+ // noop if we're not changing it
+ if (this.internalOperator == val) return val;
+
+ // Keywords has the null field IsEmpty
+ if (this.searchAttribute == Ci.nsMsgSearchAttrib.Keywords) {
+ if (val == Ci.nsMsgSearchOp.IsEmpty ||
+ val == Ci.nsMsgSearchOp.IsntEmpty)
+ this.setAttribute("selectedIndex", "-1");
+ else
+ this.setAttribute("selectedIndex", "5");
+ }
+
+ // JunkStatus has the null field IsEmpty
+ if (this.searchAttribute == Ci.nsMsgSearchAttrib.JunkStatus) {
+ if (val == Ci.nsMsgSearchOp.IsEmpty ||
+ val == Ci.nsMsgSearchOp.IsntEmpty)
+ this.setAttribute("selectedIndex", "-1");
+ else
+ this.setAttribute("selectedIndex", "6");
+ }
+
+ // if it's not sender, to, cc, alladdresses, or toorcc, we don't care
+ if (this.searchAttribute != Ci.nsMsgSearchAttrib.Sender &&
+ this.searchAttribute != Ci.nsMsgSearchAttrib.To &&
+ this.searchAttribute != Ci.nsMsgSearchAttrib.ToOrCC &&
+ this.searchAttribute != Ci.nsMsgSearchAttrib.AllAddresses &&
+ this.searchAttribute != Ci.nsMsgSearchAttrib.CC ) {
+ this.internalOperator = val;
+ return val;
+ }
+
+ var children = document.getAnonymousNodes(this);
+ if (val == Ci.nsMsgSearchOp.IsntInAB ||
+ val == Ci.nsMsgSearchOp.IsInAB) {
+ // if the old internalOperator was
+ // IsntInAB or IsInAB, and the new internalOperator is
+ // IsntInAB or IsInAB, noop because the search value
+ // was an ab type, and it still is.
+ // otherwise, switch to the ab picker and select the PAB
+ if (this.internalOperator != Ci.nsMsgSearchOp.IsntInAB &&
+ this.internalOperator != Ci.nsMsgSearchOp.IsInAB) {
+ var abs = children[4].getElementsByAttribute("value", "moz-abmdbdirectory://abook.mab");
+ if (abs.item(0))
+ children[4].selectedItem = abs[0];
+ this.setAttribute("selectedIndex", "4");
+ }
+ }
+ else {
+ // if the old internalOperator wasn't
+ // IsntInAB or IsInAB, and the new internalOperator isn't
+ // IsntInAB or IsInAB, noop because the search value
+ // wasn't an ab type, and it still isn't.
+ // otherwise, switch to the textbox and clear it
+ if (this.internalOperator == Ci.nsMsgSearchOp.IsntInAB ||
+ this.internalOperator == Ci.nsMsgSearchOp.IsInAB) {
+ children[0].value = "";
+ this.setAttribute("selectedIndex", "0");
+ }
+ }
+
+ this.internalOperator = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <!-- parentValue forwards to the attribute -->
+ <property name="parentValue" onset="return this.searchAttribute=val;"
+ onget="return this.searchAttribute;"/>
+ <property name="searchAttribute" onget="return this.internalAttribute;">
+ <setter>
+ <![CDATA[
+ // noop if we're not changing it
+ if (this.internalAttribute == val) return val;
+ this.internalAttribute = val;
+
+ // if the searchAttribute changing, null out the internalOperator
+ this.internalOperator = null;
+
+ // we inherit from a deck, so just use it's index attribute
+ // to hide/show widgets
+ if (isNaN(val)) // Is this a custom attribute?
+ {
+ this.setAttribute("selectedIndex", "9");
+ let customHbox = document.getAnonymousNodes(this)[9];
+ if (this.internalValue)
+ customHbox.setAttribute("value", this.internalValue.str);
+ // the searchAttribute attribute is intended as a selector in
+ // CSS for custom search terms to bind a custom value
+ customHbox.setAttribute("searchAttribute", val);
+ }
+ else if (val == Ci.nsMsgSearchAttrib.Priority)
+ this.setAttribute("selectedIndex", "1");
+ else if (val == Ci.nsMsgSearchAttrib.MsgStatus)
+ this.setAttribute("selectedIndex", "2");
+ else if (val == Ci.nsMsgSearchAttrib.Date)
+ this.setAttribute("selectedIndex", "3");
+ else if (val == Ci.nsMsgSearchAttrib.Sender) {
+ // since the internalOperator is null
+ // this is the same as the initial state
+ // the initial state for Sender isn't an ab type search
+ // it's a text search, so show the textbox
+ this.setAttribute("selectedIndex", "0");
+ }
+ else if (val == Ci.nsMsgSearchAttrib.Keywords) {
+ this.setAttribute("selectedIndex", "5");
+ }
+ else if (val == Ci.nsMsgSearchAttrib.JunkStatus) {
+ this.setAttribute("selectedIndex", "6");
+ }
+ else if (val == Ci.nsMsgSearchAttrib.HasAttachmentStatus) {
+ this.setAttribute("selectedIndex", "7");
+ }
+ else if (val == Ci.nsMsgSearchAttrib.JunkScoreOrigin) {
+ this.setAttribute("selectedIndex", "8");
+ }
+ else {
+ // a normal text field
+ this.setAttribute("selectedIndex", "0");
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <property name="value" onget="return this.internalValue;">
+ <setter>
+ <![CDATA[
+ // val is a nsIMsgSearchValue object
+ this.internalValue = val;
+ var attrib = this.internalAttribute;
+ var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ var children = document.getAnonymousNodes(this);
+ this.searchAttribute = attrib;
+ if (isNaN(attrib)) // a custom term
+ {
+ let customHbox = document.getAnonymousNodes(this)[9];
+ customHbox.setAttribute("value", val.str);
+ return val;
+ }
+ if (attrib == nsMsgSearchAttrib.Priority) {
+ var matchingPriority =
+ children[1].getElementsByAttribute("value", val.priority);
+ if (matchingPriority.item(0))
+ children[1].selectedItem = matchingPriority[0];
+ }
+ else if (attrib == nsMsgSearchAttrib.MsgStatus) {
+ var matchingStatus =
+ children[2].getElementsByAttribute("value", val.status);
+ if (matchingStatus.item(0))
+ children[2].selectedItem = matchingStatus[0];
+ }
+ else if (attrib == nsMsgSearchAttrib.AgeInDays)
+ children[0].value = val.age;
+ else if (attrib == nsMsgSearchAttrib.Date)
+ children[3].value = convertPRTimeToString(val.date);
+ else if (attrib == nsMsgSearchAttrib.Sender ||
+ attrib == nsMsgSearchAttrib.To ||
+ attrib == nsMsgSearchAttrib.CC ||
+ attrib == nsMsgSearchAttrib.AllAddresses ||
+ attrib == nsMsgSearchAttrib.ToOrCC)
+ {
+ if (this.internalOperator == Ci.nsMsgSearchOp.IsntInAB ||
+ this.internalOperator == Ci.nsMsgSearchOp.IsInAB) {
+ var abs = children[4].getElementsByAttribute("value", val.str);
+ if (abs.item(0))
+ children[4].selectedItem = abs[0];
+ }
+ else
+ children[0].value = val.str;
+ }
+ else if (attrib == nsMsgSearchAttrib.Keywords)
+ {
+ var keywordVal = children[5].getElementsByAttribute("value", val.str);
+ if (keywordVal.item(0))
+ {
+ children[5].value = val.str;
+ children[5].selectedItem = keywordVal[0];
+ }
+ }
+ else if (attrib == nsMsgSearchAttrib.JunkStatus) {
+ var junkStatus =
+ children[6].getElementsByAttribute("value", val.junkStatus);
+ if (junkStatus.item(0))
+ children[6].selectedItem = junkStatus[0];
+ }
+ else if (attrib == nsMsgSearchAttrib.HasAttachmentStatus) {
+ var hasAttachmentStatus =
+ children[7].getElementsByAttribute("value", val.hasAttachmentStatus);
+ if (hasAttachmentStatus.item(0))
+ children[7].selectedItem = hasAttachmentStatus[0];
+ }
+ else if (attrib == nsMsgSearchAttrib.JunkScoreOrigin) {
+ var junkScoreOrigin =
+ children[8].getElementsByAttribute("value", val.str);
+ if (junkScoreOrigin.item(0))
+ children[8].selectedItem = junkScoreOrigin[0];
+ }
+ else if (attrib == nsMsgSearchAttrib.JunkPercent) {
+ children[0].value = val.junkPercent;
+ }
+ else if (attrib == nsMsgSearchAttrib.Size) {
+ children[0].value = val.size;
+ }
+ else
+ children[0].value = val.str;
+ return val;
+ ]]>
+ </setter>
+ </property>
+ <method name="save">
+ <body>
+ <![CDATA[
+ var searchValue = this.value;
+ var searchAttribute = this.searchAttribute;
+ var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ var children = document.getAnonymousNodes(this);
+
+ searchValue.attrib = searchAttribute;
+ if (searchAttribute == nsMsgSearchAttrib.Priority) {
+ searchValue.priority = children[1].selectedItem.value;
+ }
+ else if (searchAttribute == nsMsgSearchAttrib.MsgStatus)
+ searchValue.status = children[2].value;
+ else if (searchAttribute == nsMsgSearchAttrib.AgeInDays)
+ searchValue.age = children[0].value;
+ else if (searchAttribute == nsMsgSearchAttrib.Date)
+ searchValue.date = convertStringToPRTime(children[3].value);
+ else if (searchAttribute == nsMsgSearchAttrib.Sender ||
+ searchAttribute == nsMsgSearchAttrib.To ||
+ searchAttribute == nsMsgSearchAttrib.CC ||
+ searchAttribute == nsMsgSearchAttrib.AllAddresses ||
+ searchAttribute == nsMsgSearchAttrib.ToOrCC)
+ {
+ if (this.internalOperator == Ci.nsMsgSearchOp.IsntInAB ||
+ this.internalOperator == Ci.nsMsgSearchOp.IsInAB)
+ searchValue.str = children[4].selectedItem.value;
+ else
+ searchValue.str = children[0].value;
+ }
+ else if (searchAttribute == nsMsgSearchAttrib.Keywords)
+ {
+ searchValue.str = children[5].value;
+ }
+ else if (searchAttribute == nsMsgSearchAttrib.JunkStatus)
+ searchValue.junkStatus = children[6].value;
+ else if (searchAttribute == nsMsgSearchAttrib.JunkPercent)
+ searchValue.junkPercent = children[0].value;
+ else if (searchAttribute == nsMsgSearchAttrib.Size)
+ searchValue.size = children[0].value;
+ else if (searchAttribute == nsMsgSearchAttrib.HasAttachmentStatus)
+ searchValue.status = 0x10000000; // 0x10000000 is MSG_FLAG_ATTACHMENT;
+ else if (searchAttribute == nsMsgSearchAttrib.JunkScoreOrigin)
+ searchValue.str = children[8].value;
+ else if (isNaN(searchAttribute)) // a custom term
+ {
+ searchValue.attrib = nsMsgSearchAttrib.Custom;
+ searchValue.str = children[9].getAttribute("value");
+ }
+ else
+ searchValue.str = children[0].value;
+ ]]>
+ </body>
+ </method>
+ <method name="saveTo">
+ <parameter name="searchValue"/>
+ <body>
+ <![CDATA[
+ this.internalValue = searchValue;
+ this.save();
+ ]]>
+ </body>
+ </method>
+ <method name="fillInTags">
+ <body>
+ <![CDATA[
+ var children = document.getAnonymousNodes(this);
+ var popupMenu = children[5].firstChild;
+ var tagArray = MailServices.tags.getAllTags();
+ for (var i = 0; i < tagArray.length; ++i)
+ {
+ var taginfo = tagArray[i];
+ var newMenuItem = document.createElement('menuitem');
+ newMenuItem.setAttribute('label', taginfo.tag);
+ newMenuItem.setAttribute('value', taginfo.key);
+ popupMenu.appendChild(newMenuItem);
+ if (!i)
+ children[5].selectedItem = newMenuItem;
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="fillStringsForChildren">
+ <parameter name="parentNode"/>
+ <parameter name="bundle"/>
+ <body>
+ <![CDATA[
+ var children = parentNode.childNodes;
+ var len=children.length;
+ for (var i=0; i<len; i++) {
+ var node = children[i];
+ var stringTag = node.getAttribute("stringTag");
+ if (stringTag) {
+ var attr = (node.tagName == "label") ? "value" : "label";
+ node.setAttribute(attr, bundle.GetStringFromName(stringTag));
+ }
+ }
+ ]]>
+ </body>
+ </method>
+ <method name="initialize">
+ <parameter name="menulist"/>
+ <parameter name="bundle"/>
+ <body>
+ <![CDATA[
+ this.fillStringsForChildren(menulist.firstChild, bundle);
+ ]]>
+ </body>
+ </method>
+ <constructor>
+ <![CDATA[
+ var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ ChromeUtils.import("resource://gre/modules/Services.jsm", this);
+
+ // initialize strings
+ let bundle = Services.strings.createBundle("chrome://messenger/locale/messenger.properties");
+
+ // intialize the priority picker
+ this.initialize(document.getAnonymousNodes(this)[1], bundle);
+
+ // initialize the status picker
+ this.initialize(document.getAnonymousNodes(this)[2], bundle);
+
+ // initialize the date picker
+ var datePicker = document.getAnonymousNodes(this)[3];
+ var searchAttribute = this.searchAttribute;
+ var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ var time;
+ if (searchAttribute == nsMsgSearchAttrib.Date)
+ time = datePicker.value;
+ else
+ time = new Date();
+ // do .value instead of .setAttribute("value", xxx);
+ // to work around for bug #179412
+ // (caused by bug #157210)
+ //
+ // the searchvalue widget has two textboxes
+ // one for text, one as a placeholder for a date / calendar widget
+ datePicker.value = convertDateToString(time);
+
+ // initialize the address book picker
+ this.initialize(document.getAnonymousNodes(this)[4], bundle);
+
+ // initialize the junk status picker
+ this.initialize(document.getAnonymousNodes(this)[6], bundle);
+
+ // initialize the has attachment status picker
+ this.initialize(document.getAnonymousNodes(this)[7], bundle);
+
+ // initialize the junk score origin picker
+ this.initialize(document.getAnonymousNodes(this)[8], bundle);
+
+ // initialize the tag list
+ fillInTags();
+ ]]>
+ </constructor>
+ </implementation>
+ <handlers>
+ <handler event="keypress" keycode="VK_RETURN" modifiers="accel any"
+ action="onEnterInSearchTerm(event);" preventdefault="true"/>
+ </handlers>
+ </binding>
+
+ <binding id="folderSummary-popup" extends="chrome://global/content/bindings/popup.xml#tooltip">
+ <content>
+ <children>
+ <xul:folderSummary/>
+ </children>
+ </content>
+ <handlers>
+ <handler event="popupshowing">
+ <![CDATA[
+ let msgFolder = gFolderTreeView.getFolderAtCoords(event.clientX,
+ event.clientY);
+ if (!msgFolder)
+ return false;
+
+ let tooltipnode = document.getAnonymousNodes(this)[0];
+ let asyncResults = {};
+ if (tooltipnode.parseFolder(msgFolder, null, asyncResults))
+ return true;
+
+ let row = {}, col = {};
+ gFolderTreeView._tree.getCellAt(event.clientX, event.clientY, row,
+ col, {});
+ if (col.value.id == "folderNameCol") {
+ let cropped = gFolderTreeView._tree.isCellCropped(row.value,
+ col.value);
+ if (tooltipnode.addLocationInfo(msgFolder, cropped))
+ return true;
+ }
+
+ let counts = gFolderTreeView.getSummarizedCounts(row.value,
+ col.value.id);
+ if (counts) {
+ if (tooltipnode.addSummarizeExplain(counts))
+ return true;
+ }
+
+ return false;
+ ]]>
+ </handler>
+
+ <handler event="popuphiding">
+ document.getAnonymousNodes(this)[0].clear();
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="folderSummary">
+ <content>
+ <xul:vbox/>
+ </content>
+
+ <implementation>
+ <field name="mMaxMsgHdrsInPopup">8</field>
+ <property name="hasMessages" readonly="true" onget="return document.getAnonymousNodes(this)[0].hasChildNodes();"/>
+ <method name="parseFolder">
+ <parameter name="aFolder"/>
+ <parameter name="aUrlListener"/>
+ <parameter name="aOutAsync"/>
+ <body>
+ <![CDATA[
+ // Skip servers, Trash and Junk folders, and newgroups.
+ if (!aFolder || aFolder.isServer || !aFolder.hasNewMessages ||
+ aFolder.getFlag(Ci.nsMsgFolderFlags.Junk) ||
+ aFolder.getFlag(Ci.nsMsgFolderFlags.Trash) ||
+ (aFolder.server instanceof Ci.nsINntpIncomingServer))
+ return false;
+ let showPreviewText = this.Services.prefs.getBoolPref("mail.biff.alert.show_preview");
+ let folderArray = [];
+ let msgDatabase;
+ try {
+ msgDatabase = aFolder.msgDatabase;
+ } catch(e) {
+ // The database for this folder may be missing
+ // (e.g. outdated/missing .msf), so just skip this folder.
+ return false;
+ }
+
+ if (aFolder.flags & Ci.nsMsgFolderFlags.Virtual)
+ {
+ let dbFolderInfo = msgDatabase.dBFolderInfo;
+ var srchFolderUri = dbFolderInfo.getCharProperty("searchFolderUri");
+ var srchFolderUriArray = srchFolderUri.split('|');
+ var foldersAdded = 0;
+ var RDF = Cc['@mozilla.org/rdf/rdf-service;1']
+ .getService(Ci.nsIRDFService);
+ for (var i in srchFolderUriArray)
+ {
+ var realFolder = RDF.GetResource(srchFolderUriArray[i])
+ .QueryInterface(Ci.nsIMsgFolder);
+ if (!realFolder.isServer)
+ folderArray[foldersAdded++] = realFolder;
+ }
+ }
+ else {
+ folderArray[0] = aFolder;
+ }
+
+ var foundNewMsg = false;
+ for (var folderIndex = 0; folderIndex < folderArray.length; folderIndex++)
+ {
+ aFolder = folderArray[folderIndex];
+ // now get the database
+ try {
+ msgDatabase = aFolder.msgDatabase;
+ } catch(e) {
+ // The database for this folder may be missing
+ // (e.g. outdated/missing .msf), then just skip this folder.
+ continue;
+ }
+
+ aFolder.msgDatabase = null;
+ let msgKeys = msgDatabase.getNewList();
+
+ if (!msgKeys.length)
+ continue;
+
+ if (showPreviewText)
+ {
+ // fetchMsgPreviewText forces the previewText property to get generated
+ // for each of the message keys.
+ try {
+ aOutAsync.value = aFolder.fetchMsgPreviewText(msgKeys, aUrlListener);
+ aFolder.msgDatabase = null;
+ }
+ catch (ex)
+ {
+ // fetchMsgPreviewText throws an error when we call it on a news folder, we should just not show
+ // the tooltip if this method returns an error.
+ aFolder.msgDatabase = null;
+ continue;
+ }
+ }
+ // if fetching the preview text is going to be an asynch operation and the caller
+ // is set up to handle that fact, then don't bother filling in any of the fields since
+ // we'll have to do this all over again when the fetch for the preview text completes.
+ // We don't expect to get called with a urlListener if we're doing a virtual folder.
+ if (aOutAsync.value && aUrlListener)
+ return false;
+ var unicodeConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ unicodeConverter.charset = "UTF-8";
+ foundNewMsg = true;
+
+ var index = 0;
+ while (document.getAnonymousNodes(this)[0].childNodes.length < this.mMaxMsgHdrsInPopup && index < msgKeys.length)
+ {
+ var msgPopup = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "folderSummaryMessage");
+ var msgHdr = msgDatabase.GetMsgHdrForKey(msgKeys[index++]);
+
+ var msgSubject = msgHdr.mime2DecodedSubject;
+ const kMsgFlagHasRe = 0x0010; // MSG_FLAG_HAS_RE
+ if(msgHdr.flags & kMsgFlagHasRe)
+ msgSubject = (msgSubject) ? "Re: " + msgSubject : "Re: ";
+
+ msgPopup.setAttribute('subject', msgSubject);
+
+ var previewText = msgHdr.getStringProperty('preview');
+ // convert the preview text from utf-8 to unicode
+ if (previewText)
+ {
+ try
+ {
+ var text = unicodeConverter.ConvertToUnicode(previewText);
+ if (text)
+ msgPopup.setAttribute('previewText', text);
+ }
+ catch (ex) { }
+ }
+
+ var names = {};
+ var emails = {};
+ var numAddresses = MailServices.headerParser.parseHeadersWithArray(msgHdr.mime2DecodedAuthor, emails, names, {});
+ msgPopup.setAttribute('sender', names.value[0] ? names.value[0] : emails.value[0]);
+ msgPopup.messageUri = aFolder.getUriForMsg(msgHdr);
+ msgPopup.folderUri = aFolder.URI;
+ msgPopup.msgKey = msgHdr.messageKey;
+ document.getAnonymousNodes(this)[0].appendChild(msgPopup);
+ }
+ if (document.getAnonymousNodes(this)[0].childNodes.length >= this.mMaxMsgHdrsInPopup)
+ return true;
+ }
+ return foundNewMsg;
+ ]]>
+ </body>
+ </method>
+
+ <method name="addLocationInfo">
+ <parameter name="aFolder"/>
+ <parameter name="aCropped"/>
+ <body>
+ <![CDATA[
+ let popupValue = null;
+ // Display also server name for items that are on level 0 and are
+ // not server names by themselves and do not have server name
+ // already appended in their label.
+ let folderIndex = gFolderTreeView.getIndexOfFolder(aFolder);
+ if (!aFolder.isServer &&
+ gFolderTreeView.getLevel(folderIndex) == 0 &&
+ !gFolderTreeView.getServerNameAdded(folderIndex)) {
+ let midPath = "";
+ let midFolder = aFolder.parent;
+ while (aFolder.server.rootFolder != midFolder) {
+ midPath = midFolder.name + " - " + midPath;
+ midFolder = midFolder.parent;
+ }
+ popupValue = aFolder.server.prettyName + " - " + midPath +
+ aFolder.name;
+ }
+ // If folder name is cropped or is a newsgroup and abbreviated per
+ // pref, use the full name as a tooltip.
+ else if (aCropped ||
+ ((aFolder.server instanceof Ci.nsINntpIncomingServer) &&
+ !(aFolder.flags & Ci.nsMsgFolderFlags.Virtual) &&
+ aFolder.server.abbreviate) && !aFolder.isServer) {
+ popupValue = aFolder.name;
+ }
+
+ if (popupValue) {
+ let loc = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "folderSummaryLocation");
+ loc.setAttribute("location", popupValue);
+ document.getAnonymousNodes(this)[0].appendChild(loc);
+ return true;
+ }
+
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="addSummarizeExplain">
+ <parameter name="aCounts"/>
+ <body>
+ <![CDATA[
+ if (!aCounts || !aCounts[1])
+ return false;
+ let expl = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "folderSummarySubfoldersSummary");
+ let sumString = document.getElementById("bundle_messenger")
+ .getFormattedString("subfoldersExplanation", [aCounts[0], aCounts[1]], 2);
+ expl.setAttribute("subfolders", sumString);
+ document.getAnonymousNodes(this)[0].appendChild(expl);
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="clear">
+ <body>
+ <![CDATA[
+ var containingBox = document.getAnonymousNodes(this)[0];
+ while (containingBox.hasChildNodes())
+ containingBox.lastChild.remove();
+ ]]>
+ </body>
+ </method>
+ <constructor>
+ <![CDATA[
+ var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ ChromeUtils.import("resource://gre/modules/Services.jsm", this);
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="folderSummary-location">
+ <content>
+ <xul:hbox>
+ <xul:label anonid="location" xbl:inherits="value=location"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="folderSummary-subfoldersSummary">
+ <content>
+ <xul:hbox>
+ <xul:label anonid="subfolders" xbl:inherits="value=subfolders"/>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="folderSummary-message">
+ <content>
+ <xul:vbox class="folderSummaryMessage">
+ <xul:hbox class="folderSummary-message-row">
+ <xul:label anonid="subject" flex="1" class="folderSummary-subject" xbl:inherits="value=subject" crop="right"/>
+ <xul:label anonid="sender" class="folderSummary-sender" xbl:inherits="value=sender" crop="right"/>
+ <xul:spring anonid="spring" flex="100%"/>
+ </xul:hbox>
+ <xul:description anonid="preview" class="folderSummary-message-row folderSummary-previewText" xbl:inherits="value=previewText" crop="right"></xul:description>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <constructor>
+ <![CDATA[
+ var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ ChromeUtils.import("resource://gre/modules/Services.jsm", this);
+
+ if (!this.Services.prefs.getBoolPref("mail.biff.alert.show_preview"))
+ document.getAnonymousElementByAttribute(this, "anonid", "preview").hidden = true;
+ var hideSubject = !this.Services.prefs.getBoolPref("mail.biff.alert.show_subject");
+ var hideSender = !this.Services.prefs.getBoolPref("mail.biff.alert.show_sender");
+ if (hideSubject)
+ document.getAnonymousElementByAttribute(this, "anonid", "subject").hidden = true;
+ if (hideSender)
+ document.getAnonymousElementByAttribute(this, "anonid", "sender").hidden = true;
+ if (hideSubject && hideSender)
+ document.getAnonymousElementByAttribute(this, "anonid", "spring").hidden = true;
+ ]]>
+ </constructor>
+ </implementation>
+ <handlers>
+ <handler event="click" button="0">
+ <![CDATA[
+ var topmostMsgWindow;
+ try {
+ topmostMsgWindow = MailServices.mailSession.topmostMsgWindow;
+ } catch (ex) {}
+
+ if (topmostMsgWindow)
+ {
+ // Bring window to the front
+ topmostMsgWindow.domWindow.focus();
+
+ try {
+ // SelectFolder throws an exception if the folder is not in the current folder view
+ MailServices.mailSession.topmostMsgWindow.windowCommands.selectFolder(this.folderUri);
+ MailServices.mailSession.topmostMsgWindow.windowCommands.selectMessage(this.messageUri);
+ } catch (ex) {}
+ }
+ else
+ {
+ // open a new window
+ var mailWindowService = Cc["@mozilla.org/messenger/windowservice;1"].
+ getService(Ci.nsIMessengerWindowService);
+ mailWindowService.openMessengerWindowWithUri("mail:3pane", this.folderUri, this.msgKey);
+ }
+
+ if (gAlertListener)
+ gAlertListener.observe(null, "alertclicksimplecallback", "");
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+</bindings>
diff --git a/comm/suite/mailnews/content/mailWindow.js b/comm/suite/mailnews/content/mailWindow.js
new file mode 100644
index 0000000000..388ebe2cb1
--- /dev/null
+++ b/comm/suite/mailnews/content/mailWindow.js
@@ -0,0 +1,593 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+ //This file stores variables common to mail windows
+
+var messenger;
+var statusFeedback;
+var msgWindow;
+
+var msgComposeService;
+var accountManager;
+var RDF;
+var msgComposeType;
+var msgComposeFormat;
+
+var gMessengerBundle;
+var gBrandBundle;
+
+var accountCentralBox = null;
+var gDisableViewsSearch = null;
+var gAccountCentralLoaded = true;
+//End progress and Status variables
+
+var gOfflineManager;
+
+function OnMailWindowUnload()
+{
+ RemoveMailOfflineObserver();
+ ClearPendingReadTimer();
+
+ var searchSession = GetSearchSession();
+ if (searchSession)
+ {
+ removeGlobalListeners();
+ if (gPreQuickSearchView) //close the cached pre quick search view
+ gPreQuickSearchView.close();
+ }
+
+ var dbview = GetDBView();
+ if (dbview) {
+ dbview.close();
+ }
+
+ MailServices.mailSession.RemoveFolderListener(folderListener);
+
+ MailServices.mailSession.RemoveMsgWindow(msgWindow);
+ messenger.setWindow(null, null);
+
+ msgWindow.closeWindow();
+
+ msgWindow.msgHeaderSink = null;
+ msgWindow.notificationCallbacks = null;
+ gDBView = null;
+}
+
+/**
+ * When copying/dragging, convert imap/mailbox URLs of images into data URLs so
+ * that the images can be accessed in a paste elsewhere.
+ */
+function onCopyOrDragStart(e) {
+ let browser = getBrowser();
+ if (!browser) {
+ return;
+ }
+ let sourceDoc = browser.contentDocument;
+ if (e.target.ownerDocument != sourceDoc) {
+ // We're only interested if this is in the message content.
+ return;
+ }
+
+ let imgMap = new Map(); // Mapping img.src -> dataURL.
+
+ // For copy, the data of what is to be copied is not accessible at this point.
+ // Figure out what images are a) part of the selection and b) visible in
+ // the current document. If their source isn't http or data already, convert
+ // them to data URLs.
+ let selection = sourceDoc.getSelection();
+ let draggedImg = selection.isCollapsed ? e.target : null;
+ for (let img of sourceDoc.images) {
+ if (/^(https?|data):/.test(img.src)) {
+ continue;
+ }
+
+ if (img.naturalWidth == 0) {
+ // Broken/inaccessible image then...
+ continue;
+ }
+
+ if (!draggedImg && !selection.containsNode(img, true)) {
+ continue;
+ }
+
+ let style = window.getComputedStyle(img);
+ if (style.display == "none" || style.visibility == "hidden") {
+ continue;
+ }
+
+ // Do not convert if the image is specifically flagged to not snarf.
+ if (img.getAttribute("moz-do-not-send") == "true") {
+ continue;
+ }
+
+ // We don't need to wait for the image to load. If it isn't already loaded
+ // in the source document, we wouldn't want it anyway.
+ let canvas = sourceDoc.createElement("canvas");
+ canvas.width = img.width;
+ canvas.height = img.height;
+ canvas.getContext("2d").drawImage(img, 0, 0, img.width, img.height);
+
+ let type = /\.jpe?g$/i.test(img.src) ? "image/jpg" : "image/png";
+ imgMap.set(img.src, canvas.toDataURL(type));
+ }
+
+ if (imgMap.size == 0) {
+ // Nothing that needs converting!
+ return;
+ }
+
+ let clonedSelection = draggedImg ? draggedImg.cloneNode(false) :
+ selection.getRangeAt(0).cloneContents();
+ let div = sourceDoc.createElement("div");
+ div.appendChild(clonedSelection);
+
+ let images = div.querySelectorAll("img");
+ for (let img of images) {
+ if (!imgMap.has(img.src)) {
+ continue;
+ }
+ img.src = imgMap.get(img.src);
+ }
+
+ let html = div.innerHTML;
+ let parserUtils = Cc["@mozilla.org/parserutils;1"]
+ .getService(Ci.nsIParserUtils);
+ let plain =
+ parserUtils.convertToPlainText(html,
+ Ci.nsIDocumentEncoder.OutputForPlainTextClipboardCopy,
+ 0);
+
+ // Copy operation.
+ if ("clipboardData" in e) {
+ e.clipboardData.setData("text/html", html);
+ e.clipboardData.setData("text/plain", plain);
+ e.preventDefault();
+ }
+ // Drag operation.
+ else if ("dataTransfer" in e) {
+ e.dataTransfer.setData("text/html", html);
+ e.dataTransfer.setData("text/plain", plain);
+ }
+}
+
+function CreateMailWindowGlobals()
+{
+ // Get the messenger instance.
+ messenger = Cc["@mozilla.org/messenger;1"]
+ .createInstance(Ci.nsIMessenger);
+
+ // Create windows status feedback
+ // set the JS implementation of status feedback before creating the c++ one..
+ window.MsgStatusFeedback = new nsMsgStatusFeedback();
+ // Double register the status feedback object as the xul browser window
+ // implementation.
+ window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem).treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIXULWindow)
+ .XULBrowserWindow = window.MsgStatusFeedback;
+
+ statusFeedback = Cc["@mozilla.org/messenger/statusfeedback;1"]
+ .createInstance(Ci.nsIMsgStatusFeedback);
+ statusFeedback.setWrappedStatusFeedback(window.MsgStatusFeedback);
+
+ window.MsgWindowCommands = new nsMsgWindowCommands();
+
+ //Create message window object
+ msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"]
+ .createInstance(Ci.nsIMsgWindow);
+
+ msgComposeService = Cc['@mozilla.org/messengercompose;1']
+ .getService(Ci.nsIMsgComposeService);
+
+ accountManager = MailServices.accounts;
+
+ RDF = Cc['@mozilla.org/rdf/rdf-service;1']
+ .getService(Ci.nsIRDFService);
+
+ msgComposeType = Ci.nsIMsgCompType;
+ msgComposeFormat = Ci.nsIMsgCompFormat;
+
+ gMessengerBundle = document.getElementById("bundle_messenger");
+ gBrandBundle = document.getElementById("bundle_brand");
+
+ msgWindow.notificationCallbacks = new nsMsgBadCertHandler();
+}
+
+function InitMsgWindow()
+{
+ msgWindow.windowCommands = new nsMsgWindowCommands();
+ // set the domWindow before setting the status feedback and header sink objects
+ msgWindow.domWindow = window;
+ msgWindow.statusFeedback = statusFeedback;
+ msgWindow.msgHeaderSink = messageHeaderSink;
+ MailServices.mailSession.AddMsgWindow(msgWindow);
+
+ var messagepane = getMessageBrowser();
+ messagepane.docShell.allowAuth = false;
+ messagepane.docShell.allowDNSPrefetch = false;
+ msgWindow.rootDocShell.allowAuth = true;
+ msgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL;
+ // Ensure we don't load xul error pages into the main window
+ msgWindow.rootDocShell.useErrorPages = false;
+
+ document.addEventListener("copy", onCopyOrDragStart, true);
+ document.addEventListener("dragstart", onCopyOrDragStart, true);
+}
+
+function messagePaneOnResize(event)
+{
+ // scale any overflowing images
+ var messagepane = getMessageBrowser();
+ var doc = messagepane.contentDocument;
+ var imgs = doc.images;
+ for (var img of imgs)
+ {
+ if (img.className == "moz-attached-image")
+ {
+ if (img.naturalWidth <= doc.body.clientWidth)
+ {
+ img.removeAttribute("isshrunk");
+ img.removeAttribute("overflowing");
+ }
+ else if (img.hasAttribute("shrinktofit"))
+ {
+ img.setAttribute("isshrunk", "true");
+ img.removeAttribute("overflowing");
+ }
+ else
+ {
+ img.setAttribute("overflowing", "true");
+ img.removeAttribute("isshrunk");
+ }
+ }
+ }
+
+}
+
+function messagePaneOnClick(event)
+{
+ // if this is stand alone mail (no browser)
+ // or this isn't a simple left click, do nothing, and let the normal code execute
+ if (event.button != 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
+ return contentAreaClick(event);
+
+ // try to determine the href for what you are clicking on.
+ // for example, it might be "" if you aren't left clicking on a link
+ var ceParams = hrefAndLinkNodeForClickEvent(event);
+ if (!ceParams && !event.button)
+ {
+ var target = event.target;
+ // is this an image that we might want to scale?
+ if (target instanceof Ci.nsIImageLoadingContent)
+ {
+ // make sure it loaded successfully
+ var req = target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ if (!req || req.imageStatus & Ci.imgIRequest.STATUS_ERROR)
+ return true;
+ // is it an inline attachment?
+ if (/^moz-attached-image/.test(target.className))
+ {
+ if (target.hasAttribute("isshrunk"))
+ {
+ // currently shrunk to fit, so unshrink it
+ target.removeAttribute("isshrunk");
+ target.removeAttribute("shrinktofit");
+ target.setAttribute("overflowing", "true");
+ }
+ else if (target.hasAttribute("overflowing"))
+ {
+ // user wants to shrink now
+ target.setAttribute("isshrunk", "true");
+ target.setAttribute("shrinktofit", "true");
+ target.removeAttribute("overflowing");
+ }
+ }
+ }
+ return true;
+ }
+ var href = ceParams.href;
+
+ // we know that http://, https://, ftp://, file://, chrome://,
+ // resource://, and about, should load in a browser. but if
+ // we don't have one of those (examples are mailto, imap, news, mailbox, snews,
+ // nntp, ldap, and externally handled schemes like aim) we may or may not
+ // want a browser window, in which case we return here and let the normal code
+ // handle it
+ var needABrowser = /(^http(s)?:|^ftp:|^file:|^chrome:|^resource:|^about:)/i;
+ if (href.search(needABrowser) == -1)
+ return true;
+
+ // however, if the protocol should not be loaded internally, then we should
+ // not put up a new browser window. we should just let the usual processing
+ // take place.
+ try {
+ var extProtService = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService);
+ var scheme = href.substring(0, href.indexOf(":"));
+ if (!extProtService.isExposedProtocol(scheme))
+ return true;
+ }
+ catch (ex) {} // ignore errors, and just assume that we can proceed.
+
+ // if you get here, the user did a simple left click on a link
+ // that we know should be in a browser window.
+ // since we are in the message pane, send it to the top most browser window
+ // (or open one) right away, instead of waiting for us to get some data and
+ // determine the content type, and then open a browser window
+ // we want to preventDefault, so that in
+ // nsGenericHTMLElement::HandleDOMEventForAnchors(), we don't try to handle the click again
+ event.preventDefault();
+ if (isPhishingURL(ceParams.linkNode, false, href))
+ return false;
+
+ openAsExternal(href);
+ return true;
+}
+
+// We're going to implement our status feedback for the mail window in JS now.
+// the following contains the implementation of our status feedback object
+
+function nsMsgStatusFeedback()
+{
+}
+
+nsMsgStatusFeedback.prototype =
+{
+ // global variables for status / feedback information....
+ statusTextFld : null,
+ statusBar : null,
+ statusPanel : null,
+ throbber : null,
+ stopCmd : null,
+ startTimeoutID : null,
+ stopTimeoutID : null,
+ pendingStartRequests : 0,
+ meteorsSpinning : false,
+ myDefaultStatus : "",
+
+ ensureStatusFields : function()
+ {
+ if (!this.statusTextFld ) this.statusTextFld = document.getElementById("statusText");
+ if (!this.statusBar) this.statusBar = document.getElementById("statusbar-icon");
+ if (!this.statusPanel) this.statusPanel = document.getElementById("statusbar-progresspanel");
+ if (!this.throbber) this.throbber = document.getElementById("navigator-throbber");
+ if (!this.stopCmd) this.stopCmd = document.getElementById("cmd_stop");
+ },
+
+ // nsIXULBrowserWindow implementation
+ setJSStatus : function(status)
+ {
+ if (status.length > 0)
+ this.showStatusString(status);
+ },
+ setOverLink : function(link, context)
+ {
+ this.ensureStatusFields();
+ this.statusTextFld.label = link;
+ },
+
+ // Called before links are navigated to to allow us to retarget them if needed.
+ onBeforeLinkTraversal: function(aOriginalTarget, aLinkURI, aLinkNode, aIsAppTab)
+ {
+ return aOriginalTarget;
+ },
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Ci.nsIMsgStatusFeedback) ||
+ iid.equals(Ci.nsIXULBrowserWindow) ||
+ iid.equals(Ci.nsISupportsWeakReference) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ // nsIMsgStatusFeedback implementation.
+ showStatusString : function(statusText)
+ {
+ this.ensureStatusFields();
+ if ( !statusText.length )
+ statusText = this.myDefaultStatus;
+ else
+ this.myDefaultStatus = "";
+ this.statusTextFld.label = statusText;
+ },
+ setStatusString : function(status)
+ {
+ if (status.length > 0)
+ {
+ this.myDefaultStatus = status;
+ this.statusTextFld.label = status;
+ }
+ },
+ _startMeteors : function()
+ {
+ this.ensureStatusFields();
+
+ this.meteorsSpinning = true;
+ this.startTimeoutID = null;
+
+ // Show progress meter
+ this.statusPanel.collapsed = false;
+
+ // Turn progress meter on.
+ this.statusBar.setAttribute("mode","undetermined");
+
+ // start the throbber
+ if (this.throbber)
+ this.throbber.setAttribute("busy", true);
+
+ //turn on stop button and menu
+ if (this.stopCmd)
+ this.stopCmd.removeAttribute("disabled");
+ },
+ startMeteors : function()
+ {
+ this.pendingStartRequests++;
+ // if we don't already have a start meteor timeout pending
+ // and the meteors aren't spinning, then kick off a start
+ if (!this.startTimeoutID && !this.meteorsSpinning && window.MsgStatusFeedback)
+ this.startTimeoutID = setTimeout('window.MsgStatusFeedback._startMeteors();', 500);
+
+ // since we are going to start up the throbber no sense in processing
+ // a stop timeout...
+ if (this.stopTimeoutID)
+ {
+ clearTimeout(this.stopTimeoutID);
+ this.stopTimeoutID = null;
+ }
+ },
+ _stopMeteors : function()
+ {
+ this.ensureStatusFields();
+ this.showStatusString(this.myDefaultStatus);
+
+ // stop the throbber
+ if (this.throbber)
+ this.throbber.setAttribute("busy", false);
+
+ // Turn progress meter off.
+ this.statusPanel.collapsed = true;
+ this.statusBar.setAttribute("mode","normal");
+ this.statusBar.value = 0; // be sure to clear the progress bar
+ this.statusBar.label = "";
+ if (this.stopCmd)
+ this.stopCmd.setAttribute("disabled", "true");
+
+ this.meteorsSpinning = false;
+ this.stopTimeoutID = null;
+ },
+ stopMeteors : function()
+ {
+ if (this.pendingStartRequests > 0)
+ this.pendingStartRequests--;
+
+ // if we are going to be starting the meteors, cancel the start
+ if (this.pendingStartRequests == 0 && this.startTimeoutID)
+ {
+ clearTimeout(this.startTimeoutID);
+ this.startTimeoutID = null;
+ }
+
+ // if we have no more pending starts and we don't have a stop timeout already in progress
+ // AND the meteors are currently running then fire a stop timeout to shut them down.
+ if (this.pendingStartRequests == 0 && !this.stopTimeoutID)
+ {
+ if (this.meteorsSpinning && window.MsgStatusFeedback)
+ this.stopTimeoutID = setTimeout('window.MsgStatusFeedback._stopMeteors();', 500);
+ }
+ },
+ showProgress : function(percentage)
+ {
+ this.ensureStatusFields();
+ if (percentage >= 0)
+ {
+ this.statusBar.setAttribute("mode", "normal");
+ this.statusBar.value = percentage;
+ this.statusBar.label = Math.round(percentage) + "%";
+ }
+ }
+}
+
+
+function nsMsgWindowCommands()
+{
+}
+
+nsMsgWindowCommands.prototype =
+{
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Ci.nsIMsgWindowCommands) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ selectFolder: function(folderUri)
+ {
+ gFolderTreeView.selectFolder(MailUtils.getFolderForURI(folderUri));
+ },
+
+ selectMessage: function(messageUri)
+ {
+ SelectMessage(messageUri);
+ },
+
+ clearMsgPane: function()
+ {
+ if (gDBView)
+ setTitleFromFolder(gDBView.msgFolder,null);
+ else
+ setTitleFromFolder(null,null);
+ ClearMessagePane();
+ }
+}
+
+function StopUrls()
+{
+ msgWindow.StopUrls();
+}
+
+function loadStartPage()
+{
+ try
+ {
+ gMessageNotificationBar.clearMsgNotifications();
+
+ var startpageenabled = Services.prefs.getBoolPref("mailnews.start_page.enabled");
+ if (startpageenabled)
+ {
+ var startpage = GetLocalizedStringPref("mailnews.start_page.url");
+ if (startpage)
+ {
+ GetMessagePaneFrame().location.href = startpage;
+ //dump("start message pane with: " + startpage + "\n");
+ ClearMessageSelection();
+ }
+ }
+ }
+ catch (ex)
+ {
+ dump("Error loading start page.\n");
+ return;
+ }
+}
+
+// Given the server, open the twisty and the set the selection
+// on inbox of that server.
+// prompt if offline.
+function OpenInboxForServer(server)
+{
+ ShowThreadPane();
+ gFolderTreeView.selectFolder(GetInboxFolder(server));
+
+ if (!Services.io.offline) {
+ if (server.type != "imap")
+ GetMessagesForInboxOnServer(server);
+ }
+ else if (DoGetNewMailWhenOffline()) {
+ GetMessagesForInboxOnServer(server);
+ }
+}
+
+function GetSearchSession()
+{
+ if (("gSearchSession" in top) && gSearchSession)
+ return gSearchSession;
+ else
+ return null;
+}
+
+function MailSetCharacterSet(aEvent)
+{
+ if (aEvent.target.hasAttribute("charset")) {
+ msgWindow.mailCharacterSet = aEvent.target.getAttribute("charset");
+ msgWindow.charsetOverride = true;
+ }
+ messenger.setDocumentCharset(msgWindow.mailCharacterSet);
+}
diff --git a/comm/suite/mailnews/content/mailWindowOverlay.js b/comm/suite/mailnews/content/mailWindowOverlay.js
new file mode 100644
index 0000000000..223587e914
--- /dev/null
+++ b/comm/suite/mailnews/content/mailWindowOverlay.js
@@ -0,0 +1,2695 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+var {FeedUtils} = ChromeUtils.import("resource:///modules/FeedUtils.jsm");
+var { FolderUtils } = ChromeUtils.import("resource:///modules/FolderUtils.jsm");
+var {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm");
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js");
+
+var kClassicMailLayout = 0;
+var kWideMailLayout = 1;
+var kVerticalMailLayout = 2;
+
+var kMouseButtonLeft = 0;
+var kMouseButtonMiddle = 1;
+var kMouseButtonRight = 2;
+
+// Per message header flags to keep track of whether the user is allowing remote
+// content for a particular message.
+// if you change or add more values to these constants, be sure to modify
+// the corresponding definitions in nsMsgContentPolicy.cpp
+var kNoRemoteContentPolicy = 0;
+var kBlockRemoteContent = 1;
+var kAllowRemoteContent = 2;
+
+var kIsAPhishMessage = 0;
+var kNotAPhishMessage = 1;
+
+var kMsgForwardAsAttachment = 0;
+
+var gMessengerBundle;
+var gOfflineManager;
+// Timer to mark read, if the user has configured the app to mark a message as
+// read if it is viewed for more than n seconds.
+var gMarkViewedMessageAsReadTimer = null;
+
+// The user preference, if HTML is not allowed. Assume, that the user could have
+// set this to a value > 1 in their prefs.js or user.js, but that the value will
+// not change during runtime other than through the MsgBody*() functions below.
+var gDisallow_classes_no_html = 1;
+
+// Disable the File | New | Account... menu item if the account preference is
+// locked. Two other affected areas are the account central and the account
+// manager dialogs.
+function menu_new_init() {
+ let folders = GetSelectedMsgFolders();
+ if (folders.length != 1)
+ return;
+
+ let folder = folders[0];
+
+ if (!gMessengerBundle)
+ gMessengerBundle = document.getElementById("bundle_messenger");
+
+ if (Services.prefs.prefIsLocked("mail.disable_new_account_addition"))
+ document.getElementById("newAccountMenuItem")
+ .setAttribute("disabled", "true");
+
+ let isInbox = folder.isSpecialFolder(Ci.nsMsgFolderFlags.Inbox, false);
+ let showNew = folder.canCreateSubfolders ||
+ (isInbox && !(folder.flags & Ci.nsMsgFolderFlags.Virtual));
+ ShowMenuItem("menu_newFolder", showNew);
+ ShowMenuItem("menu_newVirtualFolder", showNew);
+ EnableMenuItem("menu_newFolder", folder.server.type != "imap" ||
+ !Services.io.offline);
+ if (showNew) {
+ // Change "New Folder..." menu according to the context.
+ let label = (folder.isServer || isInbox) ? "newFolderMenuItem" :
+ "newSubfolderMenuItem";
+ SetMenuItemLabel("menu_newFolder", gMessengerBundle.getString(label));
+ }
+}
+
+function goUpdateMailMenuItems(commandset) {
+ for (var i = 0; i < commandset.childNodes.length; i++) {
+ var commandID = commandset.childNodes[i].getAttribute("id");
+ if (commandID)
+ goUpdateCommand(commandID);
+ }
+}
+
+function file_init() {
+ document.commandDispatcher.updateCommands("create-menu-file");
+}
+
+function InitEditMessagesMenu() {
+ goSetMenuValue("cmd_delete", "valueDefault");
+ goSetAccessKey("cmd_delete", "valueDefaultAccessKey");
+ document.commandDispatcher.updateCommands("create-menu-edit");
+
+ // initialize the favorite Folder checkbox in the edit menu
+ let favoriteFolderMenu = document.getElementById("menu_favoriteFolder");
+ if (!favoriteFolderMenu.hasAttribute("disabled")) {
+ let folders = GetSelectedMsgFolders();
+ if (folders.length == 1 && !folders[0].isServer) {
+ let checked = folders[0].getFlag(Ci.nsMsgFolderFlags.Favorite);
+ // Adjust the checked state on the menu item.
+ favoriteFolderMenu.setAttribute("checked", checked);
+ favoriteFolderMenu.hidden = false;
+ } else {
+ favoriteFolderMenu.hidden = true;
+ }
+ }
+}
+
+function InitGoMessagesMenu() {
+ // deactivate the folders in the go menu if we don't have a folderpane
+ document.getElementById("goFolderMenu")
+ .setAttribute("disabled", IsFolderPaneCollapsed());
+ document.commandDispatcher.updateCommands("create-menu-go");
+}
+
+function view_init() {
+ if (!gMessengerBundle)
+ gMessengerBundle = document.getElementById("bundle_messenger");
+
+ var message_menuitem = document.getElementById("menu_showMessagePane");
+ if (message_menuitem && !message_menuitem.hidden) {
+ message_menuitem.setAttribute("checked", !IsMessagePaneCollapsed());
+ message_menuitem.setAttribute("disabled", gAccountCentralLoaded);
+ }
+
+ var threadpane_menuitem = document.getElementById("menu_showThreadPane");
+ if (threadpane_menuitem && !threadpane_menuitem.hidden) {
+ threadpane_menuitem.setAttribute("checked", !IsDisplayDeckCollapsed());
+ threadpane_menuitem.setAttribute("disabled", gAccountCentralLoaded);
+ }
+
+ var folderPane_menuitem = document.getElementById("menu_showFolderPane");
+ if (folderPane_menuitem && !folderPane_menuitem.hidden)
+ folderPane_menuitem.setAttribute("checked", !IsFolderPaneCollapsed());
+
+ document.getElementById("viewSortMenu").disabled = gAccountCentralLoaded;
+ document.getElementById("viewMessageViewMenu").disabled = gAccountCentralLoaded;
+ document.getElementById("viewMessagesMenu").disabled = gAccountCentralLoaded;
+ document.getElementById("charsetMenu").disabled = !gMessageDisplay.displayedMessage;
+
+ // Initialize the Message Body menuitem
+ let isFeed = gFolderDisplay &&
+ ((gFolderDisplay.displayedFolder &&
+ gFolderDisplay.displayedFolder.server.type == "rss") ||
+ gFolderDisplay.selectedMessageIsFeed);
+ document.getElementById("viewBodyMenu").hidden = isFeed;
+
+ // Initialize the Show Feed Summary menu
+ let viewFeedSummary = document.getElementById("viewFeedSummary");
+ viewFeedSummary.hidden = !isFeed ||
+ document.documentElement.getAttribute("windowtype") != "mail:3pane";
+
+ let viewRssMenuItemIds = ["bodyFeedGlobalWebPage",
+ "bodyFeedGlobalSummary",
+ "bodyFeedPerFolderPref"];
+ let checked = FeedMessageHandler.onSelectPref;
+ for (let [index, id] of viewRssMenuItemIds.entries()) {
+ document.getElementById(id)
+ .setAttribute("checked", index == checked);
+ }
+
+ // Initialize the Display Attachments Inline menu.
+ var viewAttachmentInline = Services.prefs.getBoolPref("mail.inline_attachments");
+ document.getElementById("viewAttachmentsInlineMenuitem")
+ .setAttribute("checked", viewAttachmentInline);
+
+ document.commandDispatcher.updateCommands("create-menu-view");
+}
+
+function InitViewLayoutStyleMenu(event) {
+ var paneConfig = Services.prefs.getIntPref("mail.pane_config.dynamic");
+ var layoutStyleMenuitem = event.target.childNodes[paneConfig];
+ if (layoutStyleMenuitem)
+ layoutStyleMenuitem.setAttribute("checked", "true");
+}
+
+function setSortByMenuItemCheckState(id, value) {
+ var menuitem = document.getElementById(id);
+ if (menuitem) {
+ menuitem.setAttribute("checked", value);
+ }
+}
+
+function InitViewSortByMenu() {
+ var sortType = gDBView.sortType;
+
+ setSortByMenuItemCheckState("sortByDateMenuitem",
+ sortType == Ci.nsMsgViewSortType.byDate);
+ setSortByMenuItemCheckState("sortByReceivedMenuitem",
+ sortType == Ci.nsMsgViewSortType.byReceived);
+ setSortByMenuItemCheckState("sortByFlagMenuitem",
+ sortType == Ci.nsMsgViewSortType.byFlagged);
+ setSortByMenuItemCheckState("sortByOrderReceivedMenuitem",
+ sortType == Ci.nsMsgViewSortType.byId);
+ setSortByMenuItemCheckState("sortByPriorityMenuitem",
+ sortType == Ci.nsMsgViewSortType.byPriority);
+ setSortByMenuItemCheckState("sortBySizeMenuitem",
+ sortType == Ci.nsMsgViewSortType.bySize);
+ setSortByMenuItemCheckState("sortByStatusMenuitem",
+ sortType == Ci.nsMsgViewSortType.byStatus);
+ setSortByMenuItemCheckState("sortBySubjectMenuitem",
+ sortType == Ci.nsMsgViewSortType.bySubject);
+ setSortByMenuItemCheckState("sortByUnreadMenuitem",
+ sortType == Ci.nsMsgViewSortType.byUnread);
+ setSortByMenuItemCheckState("sortByTagsMenuitem",
+ sortType == Ci.nsMsgViewSortType.byTags);
+ setSortByMenuItemCheckState("sortByJunkStatusMenuitem",
+ sortType == Ci.nsMsgViewSortType.byJunkStatus);
+ setSortByMenuItemCheckState("sortByFromMenuitem",
+ sortType == Ci.nsMsgViewSortType.byAuthor);
+ setSortByMenuItemCheckState("sortByRecipientMenuitem",
+ sortType == Ci.nsMsgViewSortType.byRecipient);
+ setSortByMenuItemCheckState("sortByAttachmentsMenuitem",
+ sortType == Ci.nsMsgViewSortType.byAttachments);
+
+ var sortOrder = gDBView.sortOrder;
+ var sortTypeSupportsGrouping = (sortType == Ci.nsMsgViewSortType.byAuthor ||
+ sortType == Ci.nsMsgViewSortType.byDate ||
+ sortType == Ci.nsMsgViewSortType.byReceived ||
+ sortType == Ci.nsMsgViewSortType.byPriority ||
+ sortType == Ci.nsMsgViewSortType.bySubject ||
+ sortType == Ci.nsMsgViewSortType.byTags ||
+ sortType == Ci.nsMsgViewSortType.byRecipient ||
+ sortType == Ci.nsMsgViewSortType.byFlagged ||
+ sortType == Ci.nsMsgViewSortType.byAttachments);
+
+ setSortByMenuItemCheckState("sortAscending",
+ sortOrder == Ci.nsMsgViewSortOrder.ascending);
+ setSortByMenuItemCheckState("sortDescending",
+ sortOrder == Ci.nsMsgViewSortOrder.descending);
+
+ var grouped = ((gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort) != 0);
+ var threaded = ((gDBView.viewFlags & Ci.nsMsgViewFlagsType.kThreadedDisplay) != 0 && !grouped);
+ var sortThreadedMenuItem = document.getElementById("sortThreaded");
+ var sortUnthreadedMenuItem = document.getElementById("sortUnthreaded");
+
+ sortThreadedMenuItem.setAttribute("checked", threaded);
+ sortUnthreadedMenuItem.setAttribute("checked", !threaded && !grouped);
+
+ var groupBySortOrderMenuItem = document.getElementById("groupBySort");
+
+ groupBySortOrderMenuItem.setAttribute("disabled", !sortTypeSupportsGrouping);
+ groupBySortOrderMenuItem.setAttribute("checked", grouped);
+}
+
+function InitViewMessagesMenu() {
+ var viewFlags = gDBView ? gDBView.viewFlags : 0;
+ var viewType = gDBView ? gDBView.viewType : 0;
+
+ document.getElementById("viewAllMessagesMenuItem").setAttribute("checked",
+ (viewFlags & Ci.nsMsgViewFlagsType.kUnreadOnly) == 0 &&
+ (viewType == Ci.nsMsgViewType.eShowAllThreads));
+
+ document.getElementById("viewUnreadMessagesMenuItem").setAttribute("checked",
+ (viewFlags & Ci.nsMsgViewFlagsType.kUnreadOnly) != 0);
+
+ document.getElementById("viewThreadsWithUnreadMenuItem").setAttribute("checked",
+ viewType == Ci.nsMsgViewType.eShowThreadsWithUnread);
+
+ document.getElementById("viewWatchedThreadsWithUnreadMenuItem").setAttribute("checked",
+ viewType == Ci.nsMsgViewType.eShowWatchedThreadsWithUnread);
+
+ document.getElementById("viewIgnoredThreadsMenuItem").setAttribute("checked",
+ (viewFlags & Ci.nsMsgViewFlagsType.kShowIgnored) != 0);
+}
+
+function InitMessageMenu() {
+ var selectedMsg = gFolderDisplay.selectedMessage;
+ var isNews = gFolderDisplay.selectedMessageIsNews;
+ var isFeed = gFolderDisplay.selectedMessageIsFeed;
+
+ // We show Reply to Newsgroups only for news messages.
+ document.getElementById("replyNewsgroupMainMenu").hidden = !isNews;
+
+ // We show Reply to List only for list posts.
+ document.getElementById("replyListMainMenu").hidden = isNews || !IsListPost();
+
+ // For mail messages we say reply. For news we say ReplyToSender.
+ document.getElementById("replyMainMenu").hidden = isNews;
+ document.getElementById("replySenderMainMenu").hidden = !isNews;
+
+ // We show Reply to Sender and Newsgroup only for news messages.
+ document.getElementById("replySenderAndNewsgroupMainMenu").hidden = !isNews;
+
+ // For mail messages we say reply all. For news we say ReplyToAllRecipients.
+ document.getElementById("replyallMainMenu").hidden = isNews;
+ document.getElementById("replyAllRecipientsMainMenu").hidden = !isNews;
+
+ // We only show Ignore Thread and Watch Thread menu items for news.
+ document.getElementById("threadItemsSeparator").hidden = !isNews;
+ document.getElementById("killThread").hidden = !isNews;
+ document.getElementById("killSubthread").hidden = !isNews;
+ document.getElementById("watchThread").hidden = !isNews;
+ document.getElementById("menu_cancel").hidden = !isNews;
+
+ // Disable the Move and Copy menus if there are no messages selected.
+ // Disable the Move menu if we can't delete messages from the folder.
+ var msgFolder = GetLoadedMsgFolder();
+ var enableMenuItem = !isNews && selectedMsg &&
+ msgFolder && msgFolder.canDeleteMessages;
+ document.getElementById("moveMenu").disabled = !enableMenuItem;
+
+ // Also disable copy when no folder is loaded (like for .eml files).
+ var canCopy = selectedMsg && (!gMessageDisplay.isDummy ||
+ window.arguments[0].scheme == "file");
+ document.getElementById("copyMenu").disabled = !canCopy;
+
+ // Disable the Forward as/Tag menu items if no message is selected.
+ document.getElementById("forwardAsMenu").disabled = !selectedMsg;
+ document.getElementById("tagMenu").disabled = !selectedMsg;
+
+ // Show "Edit Draft Message" menus only in a drafts folder;
+ // otherwise hide them.
+ showCommandInSpecialFolder("cmd_editDraftMsg", Ci.nsMsgFolderFlags.Drafts);
+ // Show "New Message from Template" and "Edit Template" menus only in a
+ // templates folder; otherwise hide them.
+ showCommandInSpecialFolder("cmd_newMsgFromTemplate",
+ Ci.nsMsgFolderFlags.Templates);
+ showCommandInSpecialFolder("cmd_editTemplateMsg",
+ Ci.nsMsgFolderFlags.Templates);
+
+ // Initialize the Open Message menuitem
+ var winType = document.documentElement.getAttribute("windowtype");
+ if (winType == "mail:3pane")
+ document.getElementById("openMessageWindowMenuitem").hidden = isFeed;
+
+ // Initialize the Open Feed Message handler menu
+ let index = FeedMessageHandler.onOpenPref;
+ document.getElementById("menu_openFeedMessage")
+ .childNodes[index].setAttribute("checked", true);
+
+ let openRssMenu = document.getElementById("openFeedMessage");
+ openRssMenu.hidden = !isFeed;
+ if (winType != "mail:3pane")
+ openRssMenu.hidden = true;
+
+ // Disable the Mark menu when we're not in a folder.
+ document.getElementById("markMenu").disabled = !msgFolder;
+
+ document.commandDispatcher.updateCommands("create-menu-message");
+}
+
+/**
+ * Show folder-specific menu items only for messages in special folders, e.g.
+ * show 'cmd_editDraftMsg' in Drafts folder.
+ * show 'cmd_newMsgFromTemplate' in Templates folder.
+ *
+ * aCommandId the ID of a command to be shown in folders having aFolderFlag
+ * aFolderFlag the nsMsgFolderFlag that the folder must have to show the
+ * command
+ */
+function showCommandInSpecialFolder(aCommandId, aFolderFlag) {
+ let msg = gFolderDisplay.selectedMessage;
+ let folder = gFolderDisplay.displayedFolder;
+ // Check msg.folder exists as messages opened from a file have none.
+ let inSpecialFolder = (msg &&
+ msg.folder &&
+ msg.folder.isSpecialFolder(aFolderFlag, true)) ||
+ (folder && folder.getFlag(aFolderFlag));
+ document.getElementById(aCommandId).setAttribute("hidden", !inSpecialFolder);
+ return inSpecialFolder;
+}
+
+function InitViewHeadersMenu() {
+ var headerchoice =
+ Services.prefs.getIntPref("mail.show_headers",
+ Ci.nsMimeHeaderDisplayTypes.NormalHeaders);
+ document
+ .getElementById("cmd_viewAllHeader")
+ .setAttribute("checked",
+ headerchoice == Ci.nsMimeHeaderDisplayTypes.AllHeaders);
+ document
+ .getElementById("cmd_viewNormalHeader")
+ .setAttribute("checked",
+ headerchoice == Ci.nsMimeHeaderDisplayTypes.NormalHeaders);
+ document.commandDispatcher.updateCommands("create-menu-mark");
+}
+
+function InitViewBodyMenu() {
+ // Separate render prefs not implemented for feeds, bug 458606. Show the
+ // checked item for feeds as for the regular pref.
+ // let html_as = Services.prefs.getIntPref("rss.display.html_as");
+ // let prefer_plaintext = Services.prefs.getBoolPref("rss.display.prefer_plaintext");
+ // let disallow_classes = Services.prefs.getIntPref("rss.display.disallow_mime_handlers");
+
+ let html_as = Services.prefs.getIntPref("mailnews.display.html_as");
+ let prefer_plaintext = Services.prefs.getBoolPref("mailnews.display.prefer_plaintext");
+ let disallow_classes = Services.prefs.getIntPref("mailnews.display.disallow_mime_handlers");
+ let isFeed = gFolderDisplay.selectedMessageIsFeed;
+ const defaultIDs = ["bodyAllowHTML",
+ "bodySanitized",
+ "bodyAsPlaintext",
+ "bodyAllParts"];
+ const rssIDs = ["bodyFeedSummaryAllowHTML",
+ "bodyFeedSummarySanitized",
+ "bodyFeedSummaryAsPlaintext"];
+ let menuIDs = isFeed ? rssIDs : defaultIDs;
+
+ if (disallow_classes > 0)
+ gDisallow_classes_no_html = disallow_classes;
+ // else gDisallow_classes_no_html keeps its inital value (see top)
+
+ let AllowHTML_menuitem = document.getElementById(menuIDs[0]);
+ let Sanitized_menuitem = document.getElementById(menuIDs[1]);
+ let AsPlaintext_menuitem = document.getElementById(menuIDs[2]);
+ let AllBodyParts_menuitem;
+ if (!isFeed) {
+ AllBodyParts_menuitem = document.getElementById(menuIDs[3]);
+ AllBodyParts_menuitem.hidden =
+ !Services.prefs.getBoolPref("mailnews.display.show_all_body_parts_menu");
+ }
+
+ if (!prefer_plaintext && !html_as && !disallow_classes &&
+ AllowHTML_menuitem)
+ AllowHTML_menuitem.setAttribute("checked", true);
+ else if (!prefer_plaintext && html_as == 3 && disallow_classes > 0 &&
+ Sanitized_menuitem)
+ Sanitized_menuitem.setAttribute("checked", true);
+ else if (prefer_plaintext && html_as == 1 && disallow_classes > 0 &&
+ AsPlaintext_menuitem)
+ AsPlaintext_menuitem.setAttribute("checked", true);
+ else if (!prefer_plaintext && html_as == 4 && !disallow_classes &&
+ AllBodyParts_menuitem)
+ AllBodyParts_menuitem.setAttribute("checked", true);
+ // else (the user edited prefs/user.js) check none of the radio menu items
+
+ if (isFeed) {
+ AllowHTML_menuitem.hidden = !FeedMessageHandler.gShowSummary;
+ Sanitized_menuitem.hidden = !FeedMessageHandler.gShowSummary;
+ AsPlaintext_menuitem.hidden = !FeedMessageHandler.gShowSummary;
+ document.getElementById("viewFeedSummarySeparator").hidden = !FeedMessageHandler.gShowSummary;
+ }
+}
+
+function SetMenuItemLabel(menuItemId, customLabel) {
+ var menuItem = document.getElementById(menuItemId);
+ if (menuItem)
+ menuItem.setAttribute("label", customLabel);
+}
+
+function RemoveAllMessageTags() {
+ var selectedMessages = gFolderDisplay.selectedMessages;
+ if (!selectedMessages.length)
+ return;
+
+ var messages = [];
+ var tagArray = MailServices.tags.getAllTags();
+
+ var allKeys = "";
+ for (let j = 0; j < tagArray.length; ++j) {
+ if (j)
+ allKeys += " ";
+ allKeys += tagArray[j].key;
+ }
+
+ var prevHdrFolder = null;
+ // this crudely handles cross-folder virtual folders with selected messages
+ // that spans folders, by coalescing consecutive messages in the selection
+ // that happen to be in the same folder. nsMsgSearchDBView does this better,
+ // but nsIMsgDBView doesn't handle commands with arguments, and untag takes a
+ // key argument. Furthermore, we only delete legacy labels and known tags,
+ // keeping other keywords like (non)junk intact.
+
+ for (let i = 0; i < selectedMessages.length; ++i) {
+ var msgHdr = selectedMessages[i];
+ msgHdr.label = 0; // remove legacy label
+ if (prevHdrFolder != msgHdr.folder) {
+ if (prevHdrFolder)
+ prevHdrFolder.removeKeywordsFromMessages(messages, allKeys);
+ messages = [];
+ prevHdrFolder = msgHdr.folder;
+ }
+ messages.push(msgHdr);
+ }
+ if (prevHdrFolder)
+ prevHdrFolder.removeKeywordsFromMessages(messages, allKeys);
+ OnTagsChange();
+}
+
+function InitNewMsgMenu(aPopup) {
+ var identity = null;
+ var folder = GetFirstSelectedMsgFolder();
+ if (folder)
+ identity = getIdentityForServer(folder.server);
+ if (!identity) {
+ let defaultAccount = MailServices.accounts.defaultAccount;
+ if (defaultAccount)
+ identity = defaultAccount.defaultIdentity;
+ }
+
+ // If the identity is not found, use the mail.html_compose pref to
+ // determine the message compose type (HTML or PlainText).
+ var composeHTML = identity ? identity.composeHtml
+ : Services.prefs.getBoolPref("mail.html_compose");
+ const kIDs = {true: "button-newMsgHTML", false: "button-newMsgPlain"};
+ document.getElementById(kIDs[composeHTML]).setAttribute("default", "true");
+ document.getElementById(kIDs[!composeHTML]).removeAttribute("default");
+}
+
+function InitMessageReply(aPopup) {
+ var isNews = gFolderDisplay.selectedMessageIsNews;
+ // For mail messages we say reply. For news we say ReplyToSender.
+ // We show Reply to Newsgroups only for news messages.
+ aPopup.childNodes[0].hidden = isNews; // Reply
+ aPopup.childNodes[1].hidden = isNews || !IsListPost(); // Reply to List
+ aPopup.childNodes[2].hidden = !isNews; // Reply to Newsgroup
+ aPopup.childNodes[3].hidden = !isNews; // Reply to Sender Only
+}
+
+function InitMessageForward(aPopup) {
+ var forwardType = Services.prefs.getIntPref("mail.forward_message_mode");
+
+ if (forwardType != kMsgForwardAsAttachment) {
+ // forward inline is the first menuitem
+ aPopup.firstChild.setAttribute("default", "true");
+ aPopup.lastChild.removeAttribute("default");
+ } else {
+ // attachment is the last menuitem
+ aPopup.lastChild.setAttribute("default", "true");
+ aPopup.firstChild.removeAttribute("default");
+ }
+}
+
+function ToggleMessageTagKey(index) {
+ // toggle the tag state based upon that of the first selected message
+ var msgHdr = gFolderDisplay.selectedMessage;
+ if (!msgHdr)
+ return;
+
+ var tagArray = MailServices.tags.getAllTags();
+ for (var i = 0; i < tagArray.length; ++i) {
+ var key = tagArray[i].key;
+ if (!--index) {
+ // found the key, now toggle its state
+ var curKeys = msgHdr.getStringProperty("keywords");
+ if (msgHdr.label)
+ curKeys += " $label" + msgHdr.label;
+ var addKey = !(" " + curKeys + " ").includes(" " + key + " ");
+ ToggleMessageTag(key, addKey);
+ return;
+ }
+ }
+}
+
+function ToggleMessageTagMenu(target) {
+ var key = target.getAttribute("value");
+ var addKey = target.getAttribute("checked") == "true";
+ ToggleMessageTag(key, addKey);
+}
+
+function ToggleMessageTag(key, addKey) {
+ var messages = [];
+ var selectedMessages = gFolderDisplay.selectedMessages;
+ var toggler = addKey ? "addKeywordsToMessages" : "removeKeywordsFromMessages";
+ var prevHdrFolder = null;
+ // this crudely handles cross-folder virtual folders with selected messages
+ // that spans folders, by coalescing consecutive msgs in the selection
+ // that happen to be in the same folder. nsMsgSearchDBView does this
+ // better, but nsIMsgDBView doesn't handle commands with arguments,
+ // and (un)tag takes a key argument.
+ for (let i = 0; i < selectedMessages.length; ++i) {
+ var msgHdr = selectedMessages[i];
+ if (msgHdr.label) {
+ // Since we touch all these messages anyway, migrate the label now.
+ // If we don't, the thread tree won't always show the correct tag state,
+ // because resetting a label doesn't update the tree anymore...
+ msgHdr.folder.addKeywordsToMessages([msgHdr], "$label" + msgHdr.label);
+ msgHdr.label = 0; // remove legacy label
+ }
+ if (prevHdrFolder != msgHdr.folder) {
+ if (prevHdrFolder)
+ prevHdrFolder[toggler](messages, key);
+ messages = [];
+ prevHdrFolder = msgHdr.folder;
+ }
+ messages.push(msgHdr);
+ }
+ if (prevHdrFolder)
+ prevHdrFolder[toggler](messages, key);
+ OnTagsChange();
+}
+
+function SetMessageTagLabel(menuitem, index, name) {
+ // if a <key> is defined for this tag, use its key as the accesskey
+ // (the key for the tag at index n needs to have the id key_tag<n>)
+ var shortcutkey = document.getElementById("key_tag" + index);
+ var accesskey = shortcutkey ? shortcutkey.getAttribute("key") : "";
+ if (accesskey)
+ menuitem.setAttribute("accesskey", accesskey);
+ var label = gMessengerBundle.getFormattedString("mailnews.tags.format",
+ [accesskey, name]);
+ menuitem.setAttribute("label", label);
+}
+
+function InitMessageTags(menuPopup) {
+ var tagArray = MailServices.tags.getAllTags();
+ var tagCount = tagArray.length;
+
+ // remove any existing non-static entries...
+ var menuseparator = menuPopup.lastChild.previousSibling;
+ for (var i = menuPopup.childNodes.length; i > 4; --i)
+ menuseparator.previousSibling.remove();
+
+ // hide double menuseparator
+ menuseparator.previousSibling.hidden = !tagCount;
+
+ // create label and accesskey for the static remove item
+ var tagRemoveLabel = gMessengerBundle.getString("mailnews.tags.remove");
+ SetMessageTagLabel(menuPopup.firstChild, 0, tagRemoveLabel);
+
+ // now rebuild the list
+ var msgHdr = gFolderDisplay.selectedMessage;
+ var curKeys = msgHdr.getStringProperty("keywords");
+ if (msgHdr.label)
+ curKeys += " $label" + msgHdr.label;
+ for (var i = 0; i < tagCount; ++i) {
+ var taginfo = tagArray[i];
+ var removeKey = (" " + curKeys + " ").includes(" " + taginfo.key + " ");
+ if (taginfo.ordinal.includes("~AUTOTAG") && !removeKey)
+ continue;
+
+ // TODO we want to either remove or "check" the tags that already exist
+ var newMenuItem = document.createElement("menuitem");
+ SetMessageTagLabel(newMenuItem, i + 1, taginfo.tag);
+ newMenuItem.setAttribute("value", taginfo.key);
+ newMenuItem.setAttribute("type", "checkbox");
+ newMenuItem.setAttribute("checked", removeKey);
+ newMenuItem.setAttribute("oncommand", "ToggleMessageTagMenu(event.target);");
+ var color = taginfo.color;
+ if (color)
+ newMenuItem.setAttribute("class", "lc-" + color.substr(1));
+ menuPopup.insertBefore(newMenuItem, menuseparator);
+ }
+}
+
+function InitBackToolbarMenu(menuPopup) {
+ PopulateHistoryMenu(menuPopup, -1);
+}
+
+function InitForwardToolbarMenu(menuPopup) {
+ PopulateHistoryMenu(menuPopup, 1);
+}
+
+function PopulateHistoryMenu(menuPopup, navOffset) {
+ // remove existing entries
+ while (menuPopup.hasChildNodes())
+ menuPopup.lastChild.remove();
+
+ let startPos = messenger.navigatePos;
+ let historyArray = messenger.getNavigateHistory();
+ let maxPos = historyArray.length / 2; // Array consists of pairs.
+ if (GetLoadedMessage())
+ startPos += navOffset;
+
+ // starting from the current entry, march through history until we reach
+ // the array border or our menuitem limit
+ for (var i = startPos, itemCount = 0;
+ (i >= 0) && (i < maxPos) && (itemCount < 25);
+ i += navOffset, ++itemCount) {
+ var menuText = "";
+ let folder = MailUtils.getFolderForURI(historyArray[i * 2 + 1]);
+ if (!IsCurrentLoadedFolder(folder))
+ menuText += folder.prettyName + ": ";
+
+ var msgHdr = messenger.msgHdrFromURI(historyArray[i * 2]);
+ var subject = "";
+ if (msgHdr.flags & Ci.nsMsgMessageFlags.HasRe)
+ subject = "Re: ";
+ if (msgHdr.mime2DecodedSubject)
+ subject += msgHdr.mime2DecodedSubject;
+ if (subject)
+ menuText += subject + " - ";
+ menuText += msgHdr.mime2DecodedAuthor;
+
+ var newMenuItem = document.createElement("menuitem");
+ newMenuItem.setAttribute("label", menuText);
+ newMenuItem.setAttribute("value", i - startPos);
+ newMenuItem.folder = folder;
+ menuPopup.appendChild(newMenuItem);
+ }
+}
+
+function NavigateToUri(target) {
+ var historyIndex = target.getAttribute("value");
+ var msgUri = messenger.getMsgUriAtNavigatePos(historyIndex);
+ let msgHdrKey = messenger.msgHdrFromURI(msgUri).messageKey;
+ messenger.navigatePos += Number(historyIndex);
+ if (target.folder.URI == GetThreadPaneFolder().URI) {
+ gDBView.selectMsgByKey(msgHdrKey);
+ } else {
+ gStartMsgKey = msgHdrKey;
+ SelectMsgFolder(target.folder);
+ }
+}
+
+function InitMessageMark() {
+ document.getElementById("cmd_markAsFlagged")
+ .setAttribute("checked", SelectedMessagesAreFlagged());
+
+ document.commandDispatcher.updateCommands("create-menu-mark");
+}
+
+function UpdateJunkToolbarButton() {
+ var junkButtonDeck = document.getElementById("junk-deck");
+ // Wallpaper over Bug 491676 by using the attribute instead of the property.
+ junkButtonDeck.setAttribute("selectedIndex", SelectedMessagesAreJunk() ? 1 : 0);
+}
+
+function UpdateDeleteToolbarButton(aFolderPaneHasFocus) {
+ var deleteButtonDeck = document.getElementById("delete-deck");
+ var selectedIndex = 0;
+
+ // Never show "Undelete" in the 3-pane for folders, when delete would
+ // apply to the selected folder.
+ if (!aFolderPaneHasFocus && SelectedMessagesAreDeleted())
+ selectedIndex = 1;
+
+ // Wallpaper over Bug 491676 by using the attribute instead of the property.
+ deleteButtonDeck.setAttribute("selectedIndex", selectedIndex);
+}
+
+function UpdateDeleteCommand() {
+ var value = "value";
+ if (SelectedMessagesAreDeleted())
+ value += "IMAPDeleted";
+ if (GetNumSelectedMessages() < 2)
+ value += "Message";
+ else
+ value += "Messages";
+ goSetMenuValue("cmd_delete", value);
+ goSetAccessKey("cmd_delete", value + "AccessKey");
+}
+
+function SelectedMessagesAreDeleted() {
+ var firstSelectedMessage = gFolderDisplay.selectedMessage;
+ return firstSelectedMessage &&
+ (firstSelectedMessage.flags &
+ Ci.nsMsgMessageFlags.IMAPDeleted);
+}
+
+function SelectedMessagesAreJunk() {
+ var firstSelectedMessage = gFolderDisplay.selectedMessage;
+ if (!firstSelectedMessage)
+ return false;
+
+ var junkScore = firstSelectedMessage.getStringProperty("junkscore");
+ return (junkScore != "") && (junkScore != "0");
+}
+
+function SelectedMessagesAreRead() {
+ let messages = gFolderDisplay.selectedMessages;
+ if (messages.length == 0)
+ return undefined;
+ if (messages.every(function(msg) { return msg.isRead; }))
+ return true;
+ if (messages.every(function(msg) { return !msg.isRead; }))
+ return false;
+ return undefined;
+}
+
+function SelectedMessagesAreFlagged() {
+ var firstSelectedMessage = gFolderDisplay.selectedMessage;
+ return firstSelectedMessage && firstSelectedMessage.isFlagged;
+}
+
+function getMsgToolbarMenu_init() {
+ document.commandDispatcher.updateCommands("create-menu-getMsgToolbar");
+}
+
+function GetFirstSelectedMsgFolder() {
+ var selectedFolders = GetSelectedMsgFolders();
+ return (selectedFolders.length > 0) ? selectedFolders[0] : null;
+}
+
+function GetInboxFolder(server) {
+ try {
+ var rootMsgFolder = server.rootMsgFolder;
+
+ // Now find Inbox.
+ return rootMsgFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+ } catch (ex) {
+ dump(ex + "\n");
+ }
+ return null;
+}
+
+function GetMessagesForInboxOnServer(server) {
+ var inboxFolder = GetInboxFolder(server);
+
+ // If the server doesn't support an inbox it could be an RSS server or
+ // some other server type, just use the root folder and the server
+ // implementation can figure out what to do.
+ if (!inboxFolder)
+ inboxFolder = server.rootFolder;
+
+ GetNewMsgs(server, inboxFolder);
+}
+
+function MsgGetMessage() {
+ // if offline, prompt for getting messages
+ if (DoGetNewMailWhenOffline())
+ GetFolderMessages();
+}
+
+function MsgGetMessagesForAllServers(defaultServer) {
+ MailTasksGetMessagesForAllServers(true, msgWindow, defaultServer);
+}
+
+/**
+ * Get messages for all those accounts which have the capability
+ * of getting messages and have session password available i.e.,
+ * curretnly logged in accounts.
+ * if offline, prompt for getting messages.
+ */
+function MsgGetMessagesForAllAuthenticatedAccounts() {
+ if (DoGetNewMailWhenOffline())
+ MailTasksGetMessagesForAllServers(false, msgWindow, null);
+}
+
+/**
+ * Get messages for the account selected from Menu dropdowns.
+ * if offline, prompt for getting messages.
+ *
+ * @param aFolder (optional) a folder in the account for which messages should
+ * be retrieved. If null, all accounts will be used.
+ */
+function MsgGetMessagesForAccount(aFolder) {
+ if (!aFolder) {
+ goDoCommand("cmd_getNewMessages");
+ return;
+ }
+
+ if (DoGetNewMailWhenOffline())
+ GetMessagesForInboxOnServer(aFolder.server);
+}
+
+// if offline, prompt for getNextNMessages
+function MsgGetNextNMessages() {
+ if (DoGetNewMailWhenOffline()) {
+ var folder = GetFirstSelectedMsgFolder();
+ if (folder)
+ GetNextNMessages(folder);
+ }
+}
+
+function MsgDeleteMessage(aReallyDelete) {
+ // If the user deletes a message before its mark as read timer goes off,
+ // we should mark it as read (unless the user changed the pref). This
+ // ensures that we clear the biff indicator from the system tray when
+ // the user deletes the new message.
+ if (Services.prefs.getBoolPref("mailnews.ui.deleteMarksRead"))
+ MarkSelectedMessagesRead(true);
+ SetNextMessageAfterDelete();
+
+ // determine if we're using the IMAP delete model
+ var server = GetFirstSelectedMsgFolder().server;
+ const kIMAPDelete = Ci.nsMsgImapDeleteModels.IMAPDelete;
+ var imapDeleteModelUsed = server instanceof Ci.nsIImapIncomingServer &&
+ server.deleteModel == kIMAPDelete;
+
+ // execute deleteNoTrash only if IMAP delete model is not used
+ if (aReallyDelete && !imapDeleteModelUsed)
+ gDBView.doCommand(nsMsgViewCommandType.deleteNoTrash);
+ else
+ gDBView.doCommand(nsMsgViewCommandType.deleteMsg);
+}
+
+/**
+ * Copies the selected messages to the destination folder
+ * @param aDestFolder the destination folder
+ */
+function MsgCopyMessage(aDestFolder) {
+ if (gMessageDisplay.isDummy) {
+ let file = window.arguments[0].QueryInterface(Ci.nsIFileURL).file;
+ MailServices.copy.copyFileMessage(file, aDestFolder, null, false,
+ Ci.nsMsgMessageFlags.Read,
+ "", null, msgWindow);
+ } else {
+ gDBView.doCommandWithFolder(nsMsgViewCommandType.copyMessages, aDestFolder);
+ }
+}
+
+/**
+ * Moves the selected messages to the destination folder
+ * @param aDestFolder the destination folder
+ */
+function MsgMoveMessage(aDestFolder) {
+ SetNextMessageAfterDelete();
+ gDBView.doCommandWithFolder(nsMsgViewCommandType.moveMessages, aDestFolder);
+}
+
+/**
+ * Calls the ComposeMessage function with the desired type and proper default
+ * based on the event that fired it.
+ *
+ * @param aCompType The nsIMsgCompType to pass to the function.
+ * @param aEvent (optional) The event that triggered the call.
+ * @param aFormat (optional) Override the message format.
+ */
+function ComposeMsgByType(aCompType, aEvent, aFormat) {
+ var format = aFormat || ((aEvent && aEvent.shiftKey) ? msgComposeFormat.OppositeOfDefault : msgComposeFormat.Default);
+
+ ComposeMessage(aCompType,
+ format,
+ GetFirstSelectedMsgFolder(),
+ gFolderDisplay ? gFolderDisplay.selectedMessageUris : null);
+}
+
+function MsgNewMessage(aEvent) {
+ var mode = aEvent && aEvent.target.getAttribute("mode");
+ ComposeMsgByType(msgComposeType.New, aEvent, mode && msgComposeFormat[mode]);
+}
+
+function MsgReplyMessage(aEvent) {
+ if (gFolderDisplay.selectedMessageIsNews)
+ MsgReplyGroup(aEvent);
+ else if (!gFolderDisplay.selectedMessageIsFeed)
+ MsgReplySender(aEvent);
+}
+
+function MsgReplyList(aEvent) {
+ ComposeMsgByType(msgComposeType.ReplyToList, aEvent);
+}
+
+function MsgReplyGroup(aEvent) {
+ ComposeMsgByType(msgComposeType.ReplyToGroup, aEvent);
+}
+
+function MsgReplySender(aEvent) {
+ ComposeMsgByType(msgComposeType.ReplyToSender, aEvent);
+}
+
+function MsgReplyToAllMessage(aEvent) {
+ var loadedFolder = GetLoadedMsgFolder();
+ var server = loadedFolder.server;
+
+ if (server && server.type == "nntp")
+ MsgReplyToSenderAndGroup(aEvent);
+ else
+ MsgReplyToAllRecipients(aEvent);
+}
+
+function MsgReplyToAllRecipients(aEvent) {
+ ComposeMsgByType(msgComposeType.ReplyAll, aEvent);
+}
+
+function MsgReplyToSenderAndGroup(aEvent) {
+ ComposeMsgByType(msgComposeType.ReplyToSenderAndGroup, aEvent);
+}
+
+
+// Message Archive function
+
+function BatchMessageMover() {
+ this._batches = {};
+ this._currentKey = null;
+ this._dstFolderParent = null;
+ this._dstFolderName = null;
+}
+
+BatchMessageMover.prototype =
+{
+ archiveMessages(aMsgHdrs) {
+ if (!aMsgHdrs.length)
+ return;
+
+ // We need to get the index of the message to select after archiving
+ // completes but reset the global variable to prevent the DBview from
+ // updating the selection; we'll do it manually at the end of
+ // processNextBatch.
+ SetNextMessageAfterDelete();
+ this.messageToSelectAfterWereDone = gNextMessageViewIndexAfterDelete;
+ gNextMessageViewIndexAfterDelete = -2;
+
+ for (let i = 0; i < aMsgHdrs.length; ++i) {
+ let msgHdr = aMsgHdrs[i];
+ let server = msgHdr.folder.server;
+ let msgDate = new Date(msgHdr.date / 1000); // convert date to JS date object
+ let msgYear = msgDate.getFullYear().toString();
+ let monthFolderName = msgYear + "-" + (msgDate.getMonth() + 1).toString().padStart(2, "0");
+
+ let archiveFolderUri;
+ let archiveGranularity;
+ let archiveKeepFolderStructure;
+ if (server.type == "rss") {
+ // RSS servers don't have an identity so we special case the archives URI.
+ archiveFolderUri = server.serverURI + "/Archives";
+ archiveGranularity =
+ Services.prefs.getIntPref("mail.identity.default.archive_granularity");
+ archiveKeepFolderStructure =
+ Services.prefs.getBoolPref("mail.identity.default.archive_keep_folder_structure");
+ } else {
+ let identity = GetIdentityForHeader(msgHdr,
+ Ci.nsIMsgCompType.ReplyAll);
+ archiveFolderUri = identity.archiveFolder;
+ archiveGranularity = identity.archiveGranularity;
+ archiveKeepFolderStructure = identity.archiveKeepFolderStructure;
+ }
+ let archiveFolder = MailUtils.getFolderForURI(archiveFolderUri, false);
+
+ let copyBatchKey = msgHdr.folder.URI + "\0" + monthFolderName;
+ if (!(copyBatchKey in this._batches))
+ this._batches[copyBatchKey] = [msgHdr.folder,
+ archiveFolderUri,
+ archiveGranularity,
+ archiveKeepFolderStructure,
+ msgYear,
+ monthFolderName];
+ this._batches[copyBatchKey].push(msgHdr);
+ }
+
+ MailServices.mfn.addListener(this, MailServices.mfn.folderAdded);
+
+ // Now we launch the code iterating over all message copies, one in turn.
+ this.processNextBatch();
+ },
+
+ processNextBatch() {
+ for (let key in this._batches) {
+ this._currentBatch = this._batches[key];
+ delete this._batches[key];
+ return this.filterBatch();
+ }
+
+ // all done
+ MailServices.mfn.removeListener(this);
+
+ // We're just going to select the message now.
+ let treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ treeView.selection.select(this.messageToSelectAfterWereDone);
+ treeView.selectionChanged();
+
+ },
+
+ filterBatch() {
+ let batch = this._currentBatch;
+ // Apply filters to this batch.
+ let msgs = batch.slice(6);
+ let srcFolder = batch[0];
+ MailServices.filters.applyFilters(
+ Ci.nsMsgFilterType.Archive,
+ msgs, srcFolder, msgWindow, this);
+ // continues with onStopOperation
+ },
+
+ onStopOperation(aResult) {
+ if (!Components.isSuccessCode(aResult)) {
+ Cu.reportError("Archive filter failed: " + aResult);
+ // We don't want to effectively disable archiving because a filter
+ // failed, so we'll continue after reporting the error.
+ }
+ // Now do the default archive processing
+ this.continueBatch();
+ },
+
+ // continue processing of default archive operations
+ continueBatch() {
+ let batch = this._currentBatch;
+ let [srcFolder, archiveFolderUri, granularity, keepFolderStructure, msgYear, msgMonth] = batch;
+ let msgs = batch.slice(6);
+
+ let moveArray = [];
+ // Don't move any items that the filter moves or deleted
+ for (let item of msgs) {
+ if (srcFolder.msgDatabase.ContainsKey(item.messageKey) &&
+ !(srcFolder.getProcessingFlags(item.messageKey) &
+ Ci.nsMsgProcessingFlags.FilterToMove)) {
+ moveArray.push(item);
+ }
+ }
+
+ if (moveArray.length == 0)
+ return this.processNextBatch(); // continue processing
+
+ let archiveFolder = MailUtils.getFolderForURI(archiveFolderUri, false);
+ let dstFolder = archiveFolder;
+ // For folders on some servers (e.g. IMAP), we need to create the
+ // sub-folders asynchronously, so we chain the urls using the listener
+ // called back from createStorageIfMissing. For local,
+ // createStorageIfMissing is synchronous.
+ let isAsync = archiveFolder.server.protocolInfo.foldersCreatedAsync;
+ if (!archiveFolder.parent) {
+ archiveFolder.setFlag(Ci.nsMsgFolderFlags.Archive);
+ archiveFolder.createStorageIfMissing(this);
+ if (isAsync)
+ return; // continues with OnStopRunningUrl
+ }
+ if (!archiveFolder.canCreateSubfolders)
+ granularity = Ci.nsIMsgIdentity.singleArchiveFolder;
+ if (granularity >= Ci.nsIMsgIdentity.perYearArchiveFolders) {
+ archiveFolderUri += "/" + msgYear;
+ dstFolder = MailUtils.getFolderForURI(archiveFolderUri, false);
+ if (!dstFolder.parent) {
+ dstFolder.createStorageIfMissing(this);
+ if (isAsync)
+ return; // continues with OnStopRunningUrl
+ }
+ }
+ if (granularity >= Ci.nsIMsgIdentity.perMonthArchiveFolders) {
+ archiveFolderUri += "/" + msgMonth;
+ dstFolder = MailUtils.getFolderForURI(archiveFolderUri, false);
+ if (!dstFolder.parent) {
+ dstFolder.createStorageIfMissing(this);
+ if (isAsync)
+ return; // continues with OnStopRunningUrl
+ }
+ }
+
+ // Create the folder structure in Archives.
+ // For imap folders, we need to create the sub-folders asynchronously,
+ // so we chain the actions using the listener called back from
+ // createSubfolder. For local, createSubfolder is synchronous.
+ if (archiveFolder.canCreateSubfolders && keepFolderStructure) {
+ // Collect in-order list of folders of source folder structure,
+ // excluding top-level INBOX folder
+ let folderNames = [];
+ let rootFolder = srcFolder.server.rootFolder;
+ let inboxFolder = GetInboxFolder(srcFolder.server);
+ let folder = srcFolder;
+ while (folder != rootFolder && folder != inboxFolder) {
+ folderNames.unshift(folder.name);
+ folder = folder.parent;
+ }
+ // Determine Archive folder structure.
+ for (let i = 0; i < folderNames.length; ++i) {
+ let folderName = folderNames[i];
+ if (!dstFolder.containsChildNamed(folderName)) {
+ // Create Archive sub-folder (IMAP: async).
+ if (isAsync) {
+ this._dstFolderParent = dstFolder;
+ this._dstFolderName = folderName;
+ }
+ dstFolder.createSubfolder(folderName, msgWindow);
+ if (isAsync)
+ return; // continues with folderAdded
+ }
+ dstFolder = dstFolder.getChildNamed(folderName);
+ }
+ }
+
+ if (dstFolder != srcFolder) {
+ // Make sure the target folder is visible in the folder tree.
+ EnsureFolderIndex(gFolderTreeView, dstFolder);
+
+ let isNews = srcFolder.flags & Ci.nsMsgFolderFlags.Newsgroup;
+
+ // If the source folder doesn't support deleting messages, we
+ // make archive a copy, not a move.
+ MailServices.copy.copyMessages(srcFolder, moveArray, dstFolder,
+ srcFolder.canDeleteMessages && !isNews,
+ this, msgWindow, true);
+ return; // continues with OnStopCopy
+ }
+ return this.processNextBatch();
+ },
+
+
+ // This also implements nsIUrlListener, but we only care about the
+ // OnStopRunningUrl (createStorageIfMissing callback).
+ OnStartRunningUrl(aUrl) {
+ },
+ OnStopRunningUrl(aUrl, aExitCode) {
+ // This will always be a create folder url, afaik.
+ if (Components.isSuccessCode(aExitCode))
+ this.continueBatch();
+ else {
+ Cu.reportError("Archive failed to create folder: " + aExitCode);
+ this._batches = null;
+ this.processNextBatch(); // for cleanup and exit
+ }
+ },
+
+ // This also implements nsIMsgCopyServiceListener, but we only care
+ // about the OnStopCopy (copyMessages callback).
+ OnStartCopy() {
+ },
+ OnProgress(aProgress, aProgressMax) {
+ },
+ SetMessageKey(aKey) {
+ },
+ GetMessageId() {
+ },
+ OnStopCopy(aStatus) {
+ if (Components.isSuccessCode(aStatus)) {
+ return this.processNextBatch();
+ }
+
+ Cu.reportError("Archive failed to copy: " + aStatus);
+ this._batches = null;
+ this.processNextBatch(); // for cleanup and exit
+
+ },
+
+ // This also implements nsIMsgFolderListener, but we only care about the
+ // folderAdded (createSubfolder callback).
+ folderAdded(aFolder) {
+ // Check that this is the folder we're interested in.
+ if (aFolder.parent == this._dstFolderParent &&
+ aFolder.name == this._dstFolderName) {
+ this._dstFolderParent = null;
+ this._dstFolderName = null;
+ this.continueBatch();
+ }
+ },
+
+ QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIUrlListener) ||
+ aIID.equals(Ci.nsIMsgCopyServiceListener) ||
+ aIID.equals(Ci.nsIMsgFolderListener) ||
+ aIID.equals(Ci.nsIMsgOperationListener) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+function MsgArchiveSelectedMessages(aEvent) {
+ let batchMover = new BatchMessageMover();
+ batchMover.archiveMessages(gFolderDisplay.selectedMessages);
+}
+
+
+function MsgForwardMessage(event) {
+ var forwardType = Services.prefs.getIntPref("mail.forward_message_mode");
+
+ // mail.forward_message_mode could be 1, if the user migrated from 4.x
+ // 1 (forward as quoted) is obsolete, so we treat is as forward inline
+ // since that is more like forward as quoted then forward as attachment
+ if (forwardType == kMsgForwardAsAttachment)
+ MsgForwardAsAttachment(event);
+ else
+ MsgForwardAsInline(event);
+}
+
+function MsgForwardAsAttachment(event) {
+ ComposeMsgByType(msgComposeType.ForwardAsAttachment, event);
+}
+
+function MsgForwardAsInline(event) {
+ ComposeMsgByType(msgComposeType.ForwardInline, event);
+}
+
+function MsgEditMessageAsNew(aEvent) {
+ ComposeMsgByType(msgComposeType.EditAsNew, aEvent);
+}
+
+function MsgEditDraftMessage(aEvent) {
+ ComposeMsgByType(msgComposeType.Draft, aEvent);
+}
+
+function MsgNewMessageFromTemplate(aEvent) {
+ ComposeMsgByType(msgComposeType.Template, aEvent);
+}
+
+function MsgEditTemplateMessage(aEvent) {
+ ComposeMsgByType(msgComposeType.EditTemplate, aEvent);
+}
+
+function MsgComposeDraftMessage() {
+ ComposeMsgByType(msgComposeType.Draft, null, msgComposeFormat.Default);
+}
+
+function MsgCreateFilter() {
+ // retrieve Sender direct from selected message's headers
+ var msgHdr = gFolderDisplay.selectedMessage;
+ var emailAddress =
+ MailServices.headerParser.extractHeaderAddressMailboxes(msgHdr.author);
+ var accountKey = msgHdr.accountKey;
+ var folder;
+ if (accountKey.length > 0) {
+ var account = accountManager.getAccount(accountKey);
+ if (account) {
+ server = account.incomingServer;
+ if (server)
+ folder = server.rootFolder;
+ }
+ }
+ if (!folder)
+ folder = GetFirstSelectedMsgFolder();
+
+ if (emailAddress)
+ top.MsgFilters(emailAddress, folder);
+}
+
+function MsgSubscribe(folder) {
+ var preselectedFolder = folder || GetFirstSelectedMsgFolder();
+
+ if (preselectedFolder && preselectedFolder.server.type == "rss")
+ openSubscriptionsDialog(preselectedFolder); // open feed subscription dialog
+ else
+ Subscribe(preselectedFolder); // open imap/nntp subscription dialog
+}
+
+/**
+ * Show a confirmation dialog - check if the user really want to unsubscribe
+ * from the given newsgroup/s.
+ * @folders an array of newsgroup folders to unsubscribe from
+ * @return true if the user said it's ok to unsubscribe
+ */
+function ConfirmUnsubscribe(folders) {
+ if (!gMessengerBundle)
+ gMessengerBundle = document.getElementById("bundle_messenger");
+
+ let titleMsg = gMessengerBundle.getString("confirmUnsubscribeTitle");
+ let dialogMsg = (folders.length == 1) ?
+ gMessengerBundle.getFormattedString("confirmUnsubscribeText",
+ [folders[0].name], 1) :
+ gMessengerBundle.getString("confirmUnsubscribeManyText");
+
+ return Services.prompt.confirm(window, titleMsg, dialogMsg);
+}
+
+/**
+ * Unsubscribe from selected or passed in newsgroup/s.
+ * @param newsgroups (optional param) the newsgroup folders to unsubscribe from
+ */
+function MsgUnsubscribe(newsgroups) {
+ let folders = newsgroups || GetSelectedMsgFolders();
+ if (!ConfirmUnsubscribe(folders))
+ return;
+
+ for (let folder of folders) {
+ let subscribableServer =
+ folder.server.QueryInterface(Ci.nsISubscribableServer);
+ subscribableServer.unsubscribe(folder.name);
+ subscribableServer.commitSubscribeChanges();
+ }
+}
+
+function ToggleFavoriteFolderFlag() {
+ var folder = GetFirstSelectedMsgFolder();
+ folder.toggleFlag(Ci.nsMsgFolderFlags.Favorite);
+}
+
+function MsgSaveAsFile() {
+ SaveAsFile(gFolderDisplay.selectedMessageUris);
+}
+
+function MsgSaveAsTemplate() {
+ SaveAsTemplate(gFolderDisplay.selectedMessageUris);
+}
+
+function MsgOpenFromFile() {
+ var fp = Cc["@mozilla.org/filepicker;1"]
+ .createInstance(Ci.nsIFilePicker);
+
+ var filterLabel = gMessengerBundle.getString("EMLFiles");
+ var windowTitle = gMessengerBundle.getString("OpenEMLFiles");
+
+ fp.init(window, windowTitle, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilter(filterLabel, "*.eml; *.msg");
+
+ // Default or last filter is "All Files".
+ fp.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ fp.open(rv => {
+ if (rv != Ci.nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ let uri = fp.fileURL.QueryInterface(Ci.nsIURL);
+ uri.query = "type=application/x-message-display";
+
+ window.openDialog("chrome://messenger/content/messageWindow.xul", "_blank",
+ "all,chrome,dialog=no,status,toolbar", uri);
+ });
+}
+
+function MsgOpenNewWindowForFolder(folderURI, msgKeyToSelect) {
+ let mailWindowService = Cc["@mozilla.org/messenger/windowservice;1"]
+ .getService(Ci.nsIMessengerWindowService);
+ if (!mailWindowService)
+ return;
+
+ if (folderURI) {
+ mailWindowService.openMessengerWindowWithUri("mail:3pane", folderURI,
+ msgKeyToSelect);
+ return;
+ }
+
+ // If there is a right-click happening, GetSelectedMsgFolders()
+ // will tell us about it (while the selection's currentIndex would reflect
+ // the node that was selected/displayed before the right-click.)
+ for (let folder of GetSelectedMsgFolders()) {
+ mailWindowService.openMessengerWindowWithUri("mail:3pane", folder.URI,
+ msgKeyToSelect);
+ }
+}
+
+function MsgOpenSelectedMessages() {
+ // Toggle message body (feed summary) and content-base url in message pane or
+ // load in browser, per pref, otherwise open summary or web page in new window
+ // or tab, per that pref.
+ if (gFolderDisplay.selectedMessageIsFeed) {
+ let msgHdr = gFolderDisplay.selectedMessage;
+ if (document.documentElement.getAttribute("windowtype") == "mail:3pane" &&
+ FeedMessageHandler.onOpenPref == FeedMessageHandler.kOpenToggleInMessagePane) {
+ let showSummary = FeedMessageHandler.shouldShowSummary(msgHdr, true);
+ FeedMessageHandler.setContent(msgHdr, showSummary);
+ FeedMessageHandler.onSelectPref =
+ showSummary ? FeedMessageHandler.kSelectOverrideSummary :
+ FeedMessageHandler.kSelectOverrideWebPage;
+ return;
+ }
+ if (FeedMessageHandler.onOpenPref == FeedMessageHandler.kOpenLoadInBrowser) {
+ setTimeout(FeedMessageHandler.loadWebPage, 20, msgHdr, {browser: true});
+ return;
+ }
+ }
+
+ var dbView = GetDBView();
+ var indices = GetSelectedIndices(dbView);
+ var numMessages = indices.length;
+
+ // This is a radio type button pref, currently with only 2 buttons.
+ // We need to keep the pref type as 'bool' for backwards compatibility
+ // with 4.x migrated prefs. For future radio button(s), please use another
+ // pref (either 'bool' or 'int' type) to describe it.
+ //
+ // mailnews.reuse_message_window values:
+ // false: open new standalone message window for each message
+ // true : reuse existing standalone message window for each message
+ if (Services.prefs.getBoolPref("mailnews.reuse_message_window") &&
+ numMessages == 1 &&
+ MsgOpenSelectedMessageInExistingWindow())
+ return;
+
+ var openWindowWarning = Services.prefs.getIntPref("mailnews.open_window_warning");
+ if ((openWindowWarning > 1) && (numMessages >= openWindowWarning)) {
+ InitPrompts();
+ if (!gMessengerBundle)
+ gMessengerBundle = document.getElementById("bundle_messenger");
+ var title = gMessengerBundle.getString("openWindowWarningTitle");
+ var text = PluralForm.get(numMessages,
+ gMessengerBundle.getString("openWindowWarningConfirmation"))
+ .replace("#1", numMessages);
+ if (!Services.prompt.confirm(window, title, text))
+ return;
+ }
+
+ for (var i = 0; i < numMessages; i++) {
+ MsgOpenNewWindowForMessage(dbView.getURIForViewIndex(indices[i]), dbView.getFolderForViewIndex(indices[i]).URI);
+ }
+}
+
+function MsgOpenSelectedMessageInExistingWindow() {
+ var windowID = Services.wm.getMostRecentWindow("mail:messageWindow");
+ if (!windowID)
+ return false;
+
+ try {
+ var messageURI = gDBView.URIForFirstSelectedMessage;
+ var msgHdr = gDBView.hdrForFirstSelectedMessage;
+
+ // Reset the window's message uri and folder uri vars, and
+ // update the command handlers to what's going to be used.
+ // This has to be done before the call to CreateView().
+ windowID.gCurrentMessageUri = messageURI;
+ windowID.gCurrentFolderUri = msgHdr.folder.URI;
+ windowID.UpdateMailToolbar("MsgOpenExistingWindowForMessage");
+
+ // even if the folder uri's match, we can't use the existing view
+ // (msgHdr.folder.URI == windowID.gCurrentFolderUri)
+ // the reason is quick search and mail views.
+ // see bug #187673
+ //
+ // for the sake of simplicity,
+ // let's always call CreateView(gDBView)
+ // which will clone gDBView
+ windowID.CreateView(gDBView);
+ windowID.OnLoadMessageWindowDelayed(false);
+
+ // bring existing window to front
+ windowID.focus();
+ return true;
+ } catch (ex) {
+ dump("reusing existing standalone message window failed: " + ex + "\n");
+ }
+ return false;
+}
+
+function MsgOpenSearch(aSearchStr, aEvent) {
+ // If you change /suite/navigator/navigator.js->BrowserSearch::loadSearch()
+ // make sure you make corresponding changes here.
+ var submission = Services.search.defaultEngine.getSubmission(aSearchStr);
+ if (!submission)
+ return;
+
+ var newTabPref = Services.prefs.getBoolPref("browser.search.opentabforcontextsearch");
+ var where = newTabPref ? aEvent && aEvent.shiftKey ? "tabshifted" : "tab" : "window";
+ openUILinkIn(submission.uri.spec, where, null, submission.postData);
+}
+
+function MsgOpenNewWindowForMessage(messageUri, folderUri) {
+ if (!messageUri)
+ messageUri = gFolderDisplay.selectedMessageUri;
+
+ if (!folderUri)
+ // Use GetSelectedMsgFolders() to find out which message to open
+ // instead of gDBView.getURIForViewIndex(currentIndex). This is
+ // required because on a right-click, the currentIndex value will be
+ // different from the actual row that is highlighted.
+ // GetSelectedMsgFolders() will return the message that is
+ // highlighted.
+ folderUri = GetSelectedMsgFolders()[0].URI;
+
+ // be sure to pass in the current view....
+ if (messageUri && folderUri) {
+ window.openDialog( "chrome://messenger/content/messageWindow.xul", "_blank", "all,chrome,dialog=no,status,toolbar", messageUri, folderUri, gDBView );
+ }
+}
+
+function CloseMailWindow() {
+ window.close();
+}
+
+function MsgJunk() {
+ MsgJunkMailInfo(true);
+ JunkSelectedMessages(!SelectedMessagesAreJunk());
+}
+
+/**
+ * Checks if the selected messages can be marked as read or unread
+ *
+ * @param read true if trying to mark messages as read, false otherwise
+ * @return true if the chosen operation can be performed
+ */
+function CanMarkMsgAsRead(read) {
+ return SelectedMessagesAreRead() != read;
+}
+
+/**
+ * Marks the selected messages as read or unread
+ *
+ * @param read true if trying to mark messages as read, false if marking unread,
+ * undefined if toggling the read status
+ */
+function MsgMarkMsgAsRead(read) {
+ if (read == undefined)
+ read = !SelectedMessagesAreRead();
+ MarkSelectedMessagesRead(read);
+}
+
+function MsgMarkAsFlagged() {
+ MarkSelectedMessagesFlagged(!SelectedMessagesAreFlagged());
+}
+
+function MsgMarkReadByDate() {
+ window.openDialog("chrome://messenger/content/markByDate.xul", "",
+ "chrome,modal,titlebar,centerscreen",
+ GetLoadedMsgFolder());
+}
+
+function MsgMarkAllRead() {
+ let folders = GetSelectedMsgFolders();
+ for (let folder of folders)
+ folder.markAllMessagesRead(msgWindow);
+}
+
+function MsgDownloadFlagged() {
+ gDBView.doCommand(nsMsgViewCommandType.downloadFlaggedForOffline);
+}
+
+function MsgDownloadSelected() {
+ gDBView.doCommand(nsMsgViewCommandType.downloadSelectedForOffline);
+}
+
+function MsgMarkThreadAsRead() {
+ ClearPendingReadTimer();
+ gDBView.doCommand(nsMsgViewCommandType.markThreadRead);
+}
+
+function MsgViewPageSource() {
+ ViewPageSource(gFolderDisplay.selectedMessageUris);
+}
+
+var gFindInstData;
+function getFindInstData() {
+ if (!gFindInstData) {
+ gFindInstData = new nsFindInstData();
+ gFindInstData.browser = getMessageBrowser();
+ gFindInstData.rootSearchWindow = window.top.content;
+ gFindInstData.currentSearchWindow = window.top.content;
+ }
+ return gFindInstData;
+}
+
+function MsgFind() {
+ findInPage(getFindInstData());
+}
+
+function MsgFindAgain(reverse) {
+ findAgainInPage(getFindInstData(), reverse);
+}
+
+function MsgCanFindAgain() {
+ return canFindAgainInPage();
+}
+
+/**
+ * Go through each selected server and mark all its folders read.
+ */
+function MsgMarkAllFoldersRead() {
+ if (!Services.prompt.confirm(window,
+ gMessengerBundle.getString("confirmMarkAllFoldersReadTitle"),
+ gMessengerBundle.getString("confirmMarkAllFoldersReadMessage"))) {
+ return;
+ }
+
+ const selectedFolders = GetSelectedMsgFolders();
+ if (selectedFolders) {
+ const selectedServers = selectedFolders.filter(folder => folder.isServer);
+
+ selectedServers.forEach(function(server) {
+ for (let folder of server.rootFolder.descendants) {
+ folder.markAllMessagesRead(msgWindow);
+ }
+ });
+ }
+}
+
+function MsgFilters(emailAddress, folder) {
+ if (!folder)
+ folder = GetFirstSelectedMsgFolder();
+ var args;
+ if (emailAddress) {
+ // Prefill the filterEditor with the emailAddress.
+ args = {filterList: folder.getEditableFilterList(msgWindow), filterName: emailAddress};
+ window.openDialog("chrome://messenger/content/FilterEditor.xul", "",
+ "chrome, modal, resizable,centerscreen,dialog", args);
+
+ // If the user hits ok in the filterEditor dialog we set args.refresh=true
+ // there and we check this here in args to show filterList dialog.
+ // We also received the filter created via args.newFilter.
+ if ("refresh" in args && args.refresh) {
+ args = { refresh: true, folder, filter: args.newFilter };
+ MsgFilterList(args);
+ }
+ } else // just launch filterList dialog
+ {
+ args = { refresh: false, folder };
+ MsgFilterList(args);
+ }
+}
+
+function MsgApplyFilters() {
+ var preselectedFolder = GetFirstSelectedMsgFolder();
+
+ var curFilterList = preselectedFolder.getFilterList(msgWindow);
+ // create a new filter list and copy over the enabled filters to it.
+ // We do this instead of having the filter after the fact code ignore
+ // disabled filters because the Filter Dialog filter after the fact
+ // code would have to clone filters to allow disabled filters to run,
+ // and we don't support cloning filters currently.
+ var tempFilterList =
+ MailServices.filters.getTempFilterList(preselectedFolder);
+ var numFilters = curFilterList.filterCount;
+ // make sure the temp filter list uses the same log stream
+ tempFilterList.loggingEnabled = curFilterList.loggingEnabled;
+ tempFilterList.logStream = curFilterList.logStream;
+ var newFilterIndex = 0;
+ for (var i = 0; i < numFilters; i++) {
+ var curFilter = curFilterList.getFilterAt(i);
+ // only add enabled, UI visibile filters that are in the manual context
+ if (curFilter.enabled && !curFilter.temporary &&
+ (curFilter.filterType & Ci.nsMsgFilterType.Manual)) {
+ tempFilterList.insertFilterAt(newFilterIndex, curFilter);
+ newFilterIndex++;
+ }
+ }
+ MailServices.filters.applyFiltersToFolders(tempFilterList,
+ [preselectedFolder],
+ msgWindow);
+}
+
+function MsgApplyFiltersToSelection() {
+ var folder = gDBView.msgFolder;
+ var indices = GetSelectedIndices(gDBView);
+ if (indices && indices.length) {
+ var selectedMsgs = [];
+ for (var i = 0; i < indices.length; i++) {
+ try {
+ // Getting the URI will tell us if the item is real or a dummy header
+ var uri = gDBView.getURIForViewIndex(indices[i]);
+ if (uri) {
+ var msgHdr = folder.GetMessageHeader(gDBView.getKeyAt(indices[i]));
+ if (msgHdr)
+ selectedMsgs.push(msgHdr);
+ }
+ } catch (ex) {}
+ }
+
+ MailServices.filters.applyFilters(Ci.nsMsgFilterType.Manual, selectedMsgs,
+ folder, msgWindow);
+ }
+}
+
+function ChangeMailLayout(newLayout) {
+ Services.prefs.setIntPref("mail.pane_config.dynamic", newLayout);
+}
+
+function MsgViewAllHeaders() {
+ Services.prefs.setIntPref("mail.show_headers",
+ Ci.nsMimeHeaderDisplayTypes.AllHeaders);
+}
+
+function MsgViewNormalHeaders() {
+ Services.prefs.setIntPref("mail.show_headers",
+ Ci.nsMimeHeaderDisplayTypes.NormalHeaders);
+}
+
+function MsgBodyAllowHTML() {
+ ChangeMsgBodyDisplay(false, 0, 0);
+}
+
+function MsgBodySanitized() {
+ ChangeMsgBodyDisplay(false, 3, gDisallow_classes_no_html);
+}
+
+function MsgBodyAsPlaintext() {
+ ChangeMsgBodyDisplay(true, 1, gDisallow_classes_no_html);
+}
+
+function MsgBodyAllParts() {
+ ChangeMsgBodyDisplay(false, 4, 0);
+}
+
+function ChangeMsgBodyDisplay(plaintext, html, mime) {
+ Services.prefs.setBoolPref("mailnews.display.prefer_plaintext", plaintext);
+ Services.prefs.setIntPref("mailnews.display.disallow_mime_handlers", mime);
+ Services.prefs.setIntPref("mailnews.display.html_as", html);
+}
+
+function MsgFeedBodyRenderPrefs(plaintext, html, mime) {
+ // Separate render prefs not implemented for feeds, bug 458606.
+ // Services.prefs.setBoolPref("rss.display.prefer_plaintext", plaintext);
+ // Services.prefs.setIntPref("rss.display.disallow_mime_handlers", mime);
+ // Services.prefs.setIntPref("rss.display.html_as", html)
+
+ Services.prefs.setBoolPref("mailnews.display.prefer_plaintext", plaintext);
+ Services.prefs.setIntPref("mailnews.display.disallow_mime_handlers", mime);
+ Services.prefs.setIntPref("mailnews.display.html_as", html);
+}
+
+function ToggleInlineAttachment(target) {
+ var viewInline = !Services.prefs.getBoolPref("mail.inline_attachments");
+ Services.prefs.setBoolPref("mail.inline_attachments", viewInline);
+ target.setAttribute("checked", viewInline ? "true" : "false");
+}
+
+function MsgStop() {
+ StopUrls();
+}
+
+function MsgSendUnsentMsgs() {
+ // if offline, prompt for sendUnsentMessages
+ if (!Services.io.offline) {
+ SendUnsentMessages();
+ } else {
+ var option = PromptMessagesOffline("send");
+ if (option == 0) {
+ if (!gOfflineManager)
+ GetOfflineMgrService();
+ gOfflineManager.goOnline(false /* sendUnsentMessages */,
+ false /* playbackOfflineImapOperations */,
+ msgWindow);
+ SendUnsentMessages();
+ }
+ }
+}
+
+function PrintEnginePrintInternal(aDoPrintPreview, aMsgType) {
+ var messageList = gFolderDisplay.selectedMessageUris;
+ if (!messageList) {
+ dump("PrintEnginePrint(): No messages selected.\n");
+ return false;
+ }
+
+ window.openDialog("chrome://messenger/content/msgPrintEngine.xul", "",
+ "chrome,dialog=no,all,centerscreen",
+ messageList.length, messageList, statusFeedback,
+ aDoPrintPreview, aMsgType);
+ return true;
+
+}
+
+function PrintEnginePrint() {
+ return PrintEnginePrintInternal(false, Ci.nsIMsgPrintEngine.MNAB_PRINT_MSG);
+}
+
+function PrintEnginePrintPreview() {
+ return PrintEnginePrintInternal(true, Ci.nsIMsgPrintEngine.MNAB_PRINTPREVIEW_MSG);
+}
+
+// Kept for add-on compatibility.
+function SelectFolder(folderUri) {
+ SelectMsgFolder(MailUtils.getFolderForURI(folderUri));
+}
+
+function IsMailFolderSelected() {
+ var selectedFolders = GetSelectedMsgFolders();
+ var folder = selectedFolders.length ? selectedFolders[0] : null;
+ return folder && folder.server.type != "nntp";
+}
+
+function IsGetNewMessagesEnabled() {
+ // users don't like it when the "Get Msgs" button is disabled
+ // so let's never do that.
+ // we'll just handle it as best we can in GetFolderMessages()
+ // when they click "Get Msgs" and
+ // Local Folders or a news server is selected
+ // see bugs #89404 and #111102
+ return true;
+}
+
+function IsGetNextNMessagesEnabled() {
+ var selectedFolders = GetSelectedMsgFolders();
+ var folder = selectedFolders.length ? selectedFolders[0] : null;
+
+ var menuItem = document.getElementById("menu_getnextnmsg");
+ if (folder && !folder.isServer &&
+ folder.server instanceof Ci.nsINntpIncomingServer) {
+ var menuLabel = PluralForm.get(folder.server.maxArticles,
+ gMessengerBundle.getString("getNextNewsMessages"))
+ .replace("#1", folder.server.maxArticles);
+ menuItem.setAttribute("label", menuLabel);
+ menuItem.removeAttribute("hidden");
+ return true;
+ }
+
+ menuItem.setAttribute("hidden", "true");
+ return false;
+}
+
+function SetUpToolbarButtons(uri) {
+ let deleteButton = document.getElementById("button-delete");
+ let replyAllButton = document.getElementById("button-replyall");
+
+ // Eventually, we might want to set up the toolbar differently for imap,
+ // pop, and news. For now, just tweak it based on if it is news or not.
+ let forNews = isNewsURI(uri);
+
+ deleteButton.hidden = forNews;
+ if (forNews) {
+ replyAllButton.setAttribute("type", "menu-button");
+ replyAllButton.setAttribute("tooltiptext",
+ replyAllButton.getAttribute("tooltiptextnews"));
+ } else {
+ replyAllButton.removeAttribute("type");
+ replyAllButton.setAttribute("tooltiptext",
+ replyAllButton.getAttribute("tooltiptextmail"));
+ }
+}
+
+function getMessageBrowser() {
+ return document.getElementById("messagepane");
+}
+
+// The zoom manager, view source and possibly some other functions still rely
+// on the getBrowser function.
+function getBrowser() {
+ return GetTabMail() ? GetTabMail().getBrowserForSelectedTab() :
+ getMessageBrowser();
+}
+
+function MsgSynchronizeOffline() {
+ window.openDialog("chrome://messenger/content/msgSynchronize.xul", "",
+ "centerscreen,chrome,modal,titlebar,resizable",
+ {msgWindow});
+}
+
+function MsgOpenAttachment() {}
+function MsgUpdateMsgCount() {}
+function MsgImport() {}
+function MsgSynchronize() {}
+function MsgGetSelectedMsg() {}
+function MsgGetFlaggedMsg() {}
+function MsgSelectThread() {}
+function MsgShowFolders() {}
+function MsgShowLocationbar() {}
+function MsgViewAttachInline() {}
+function MsgWrapLongLines() {}
+function MsgIncreaseFont() {}
+function MsgDecreaseFont() {}
+function MsgShowImages() {}
+function MsgRefresh() {}
+function MsgViewPageInfo() {}
+function MsgFirstUnreadMessage() {}
+function MsgFirstFlaggedMessage() {}
+function MsgAddSenderToAddressBook() {}
+function MsgAddAllToAddressBook() {}
+
+function SpaceHit(event) {
+ var contentWindow = document.commandDispatcher.focusedWindow;
+ if (contentWindow.top == window)
+ contentWindow = content;
+ else if (document.commandDispatcher.focusedElement &&
+ !hrefAndLinkNodeForClickEvent(event))
+ return;
+ var rssiframe = content.document.getElementById("_mailrssiframe");
+
+ // If we are displaying an RSS article, we really want to scroll
+ // the nested iframe.
+ if (contentWindow == content && rssiframe)
+ contentWindow = rssiframe.contentWindow;
+
+ if (event && event.shiftKey) {
+ // if at the start of the message, go to the previous one
+ if (contentWindow.scrollY > 0)
+ contentWindow.scrollByPages(-1);
+ else if (Services.prefs.getBoolPref("mail.advance_on_spacebar"))
+ goDoCommand("cmd_previousUnreadMsg");
+ } else {
+ // if at the end of the message, go to the next one
+ if (contentWindow.scrollY < contentWindow.scrollMaxY)
+ contentWindow.scrollByPages(1);
+ else if (Services.prefs.getBoolPref("mail.advance_on_spacebar"))
+ goDoCommand("cmd_nextUnreadMsg");
+ }
+}
+
+function IsAccountOfflineEnabled() {
+ var selectedFolders = GetSelectedMsgFolders();
+
+ if (selectedFolders && (selectedFolders.length == 1))
+ return selectedFolders[0].supportsOffline;
+
+ return false;
+}
+
+function DoGetNewMailWhenOffline() {
+ if (!Services.io.offline)
+ return true;
+
+ if (PromptMessagesOffline("get") == 0) {
+ var sendUnsent = false;
+ if (this.CheckForUnsentMessages != undefined && CheckForUnsentMessages()) {
+ sendUnsent =
+ Services.prefs.getIntPref("offline.send.unsent_messages") == 1 ||
+ Services.prompt.confirmEx(
+ window,
+ gOfflinePromptsBundle.getString("sendMessagesOfflineWindowTitle"),
+ gOfflinePromptsBundle.getString("sendMessagesLabel2"),
+ Services.prompt.BUTTON_TITLE_IS_STRING *
+ (Services.prompt.BUTTON_POS_0 + Services.prompt.BUTTON_POS_1),
+ gOfflinePromptsBundle.getString("sendMessagesSendButtonLabel"),
+ gOfflinePromptsBundle.getString("sendMessagesNoSendButtonLabel"),
+ null, null, {value: false}) == 0;
+ }
+ if (!gOfflineManager)
+ GetOfflineMgrService();
+ gOfflineManager.goOnline(sendUnsent /* sendUnsentMessages */,
+ false /* playbackOfflineImapOperations */,
+ msgWindow);
+ return true;
+ }
+ return false;
+}
+
+// prompt for getting/sending messages when offline
+function PromptMessagesOffline(aPrefix) {
+ InitPrompts();
+ var checkValue = {value: false};
+ return Services.prompt.confirmEx(
+ window,
+ gOfflinePromptsBundle.getString(aPrefix + "MessagesOfflineWindowTitle"),
+ gOfflinePromptsBundle.getString(aPrefix + "MessagesOfflineLabel"),
+ (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) +
+ (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1),
+ gOfflinePromptsBundle.getString(aPrefix + "MessagesOfflineGoButtonLabel"),
+ null, null, null, checkValue);
+}
+
+function GetDefaultAccountRootFolder() {
+ var account = accountManager.defaultAccount;
+ if (account) {
+ return account.incomingServer.rootMsgFolder;
+ }
+ return null;
+}
+
+/**
+ * Check for new messages for all selected folders, or for the default account
+ * in case no folders are selected.
+ */
+function GetFolderMessages() {
+ var selectedFolders = GetSelectedMsgFolders();
+ var defaultAccountRootFolder = GetDefaultAccountRootFolder();
+
+ var folders = (selectedFolders.length) ? selectedFolders
+ : [defaultAccountRootFolder];
+
+ if (!folders[0]) {
+ return;
+ }
+
+ for (let folder of folders) {
+ var serverType = folder.server.type;
+ if (folder.isServer && (serverType == "nntp")) {
+ // If we're doing "get msgs" on a news server,
+ // update unread counts on this server.
+ folder.server.performExpand(msgWindow);
+ } else if (serverType == "none") {
+ // If "Local Folders" is selected and the user does "Get Msgs" and
+ // LocalFolders is not deferred to, get new mail for the default account
+ //
+ // XXX TODO
+ // Should shift click get mail for all (authenticated) accounts?
+ // see bug #125885.
+ if (!folder.server.isDeferredTo) {
+ if (!defaultAccountRootFolder) {
+ continue;
+ }
+ GetNewMsgs(defaultAccountRootFolder.server, defaultAccountRootFolder);
+ } else {
+ GetNewMsgs(folder.server, folder);
+ }
+ } else {
+ GetNewMsgs(folder.server, folder);
+ }
+ }
+}
+
+/**
+ * Gets new messages for the given server, for the given folder.
+ * @param server which nsIMsgIncomingServer to check for new messages
+ * @param folder which nsIMsgFolder folder to check for new messages
+ */
+function GetNewMsgs(server, folder) {
+ // Note that for Global Inbox folder.server != server when we want to get
+ // messages for a specific account.
+
+ // Whenever we do get new messages, clear the old new messages.
+ folder.biffState = Ci.nsIMsgFolder.nsMsgBiffState_NoMail;
+ folder.clearNewMessages();
+ server.getNewMessages(folder, msgWindow, null);
+}
+
+function SendUnsentMessages() {
+ let msgSendlater = Cc["@mozilla.org/messengercompose/sendlater;1"]
+ .getService(Ci.nsIMsgSendLater);
+
+ let allIdentities = MailServices.accounts.allIdentities;
+ for (let currentIdentity of allIdentities) {
+ let msgFolder = msgSendlater.getUnsentMessagesFolder(currentIdentity);
+ if (msgFolder) {
+ let numMessages = msgFolder.getTotalMessages(false /* include subfolders */);
+ if (numMessages > 0) {
+ msgSendlater.statusFeedback = statusFeedback;
+ msgSendlater.sendUnsentMessages(currentIdentity);
+ // Right now, all identities point to the same unsent messages
+ // folder, so to avoid sending multiple copies of the
+ // unsent messages, we only call messenger.SendUnsentMessages() once
+ // see bug #89150 for details
+ break;
+ }
+ }
+ }
+}
+
+function CommandUpdate_UndoRedo() {
+ EnableMenuItem("menu_undo", SetupUndoRedoCommand("cmd_undo"));
+ EnableMenuItem("menu_redo", SetupUndoRedoCommand("cmd_redo"));
+}
+
+function SetupUndoRedoCommand(command) {
+ // If we have selected a server, and are viewing account central
+ // there is no loaded folder.
+ var loadedFolder = GetLoadedMsgFolder();
+ if (!loadedFolder || !loadedFolder.server.canUndoDeleteOnServer)
+ return false;
+
+ var canUndoOrRedo = false;
+ var txnType = 0;
+
+ if (command == "cmd_undo") {
+ canUndoOrRedo = messenger.canUndo();
+ txnType = messenger.getUndoTransactionType();
+ } else {
+ canUndoOrRedo = messenger.canRedo();
+ txnType = messenger.getRedoTransactionType();
+ }
+
+ if (canUndoOrRedo) {
+ switch (txnType) {
+ default:
+ case Ci.nsIMessenger.eUnknown:
+ goSetMenuValue(command, "valueDefault");
+ break;
+ case Ci.nsIMessenger.eDeleteMsg:
+ goSetMenuValue(command, "valueDeleteMsg");
+ break;
+ case Ci.nsIMessenger.eMoveMsg:
+ goSetMenuValue(command, "valueMoveMsg");
+ break;
+ case Ci.nsIMessenger.eCopyMsg:
+ goSetMenuValue(command, "valueCopyMsg");
+ break;
+ case Ci.nsIMessenger.eMarkAllMsg:
+ goSetMenuValue(command, "valueUnmarkAllMsgs");
+ break;
+ }
+ } else {
+ goSetMenuValue(command, "valueDefault");
+ }
+ return canUndoOrRedo;
+}
+
+function HandleJunkStatusChanged(folder) {
+ // This might be the stand alone window, open to a message that was
+ // and attachment (or on disk), in which case, we want to ignore it.
+ var loadedMessage = GetLoadedMessage();
+ if (!loadedMessage ||
+ /type=application\/x-message-display/.test(loadedMessage) ||
+ !IsCurrentLoadedFolder(folder))
+ return;
+
+ // If multiple message are selected and we change the junk status
+ // we don't want to show the junk bar (since the message pane is blank).
+ var msgHdr = null;
+ if (GetNumSelectedMessages() == 1)
+ msgHdr = messenger.msgHdrFromURI(loadedMessage);
+
+ var junkBarWasDisplayed = gMessageNotificationBar.isShowingJunkNotification();
+ gMessageNotificationBar.setJunkMsg(msgHdr);
+
+ // Only reload message if junk bar display state has changed.
+ if (msgHdr && junkBarWasDisplayed != gMessageNotificationBar.isShowingJunkNotification()) {
+ // We may be forcing junk mail to be rendered with sanitized html.
+ // In that scenario, we want to reload the message if the status has just
+ // changed to not junk.
+ var sanitizeJunkMail = Services.prefs.getBoolPref("mail.spam.display.sanitize");
+
+ // Only bother doing this if we are modifying the html for junk mail...
+ if (sanitizeJunkMail) {
+ let junkScore = msgHdr.getStringProperty("junkscore");
+ let isJunk = (junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE);
+
+ // If the current row isn't going to change, reload to show sanitized or
+ // unsanitized. Otherwise we wouldn't see the reloaded version anyway.
+
+ // XXX: need to special handle last message in view, for imap mark as deleted
+
+ // 1) When marking as non-junk, the msg would move back to the inbox.
+ // 2) When marking as junk, the msg will move or delete, if manualMark is set.
+ // 3) Marking as junk in the junk folder just changes the junk status.
+ if ((!isJunk && folder.isSpecialFolder(Ci.nsMsgFolderFlags.Inbox)) ||
+ (isJunk && !folder.server.spamSettings.manualMark) ||
+ (isJunk && folder.isSpecialFolder(Ci.nsMsgFolderFlags.Junk)))
+ ReloadMessage();
+ }
+ }
+}
+
+var gMessageNotificationBar =
+{
+ get mStringBundle() {
+ delete this.mStringBundle;
+
+ return this.mStringBundle = document.getElementById("bundle_messenger");
+ },
+
+ get mBrandBundle() {
+ delete this.mBrandBundle;
+
+ return this.mBrandBundle = document.getElementById("bundle_brand");
+ },
+
+ get mMsgNotificationBar() {
+ delete this.mMsgNotificationBar;
+
+ return this.mMsgNotificationBar = document.getElementById("messagepanebox");
+ },
+
+ setJunkMsg(aMsgHdr) {
+ let isJunk = false;
+ if (aMsgHdr) {
+ let junkScore = aMsgHdr.getStringProperty("junkscore");
+ isJunk = ((junkScore != "") && (junkScore != "0"));
+ }
+
+ goUpdateCommand("button_junk");
+
+ if (isJunk) {
+ if (!this.isShowingJunkNotification()) {
+ let brandName = this.mBrandBundle.getString("brandShortName");
+ let junkBarMsg = this.mStringBundle.getFormattedString("junkBarMessage",
+ [brandName]);
+
+ let buttons = [{
+ label: this.mStringBundle.getString("junkBarInfoButton"),
+ accessKey: this.mStringBundle.getString("junkBarInfoButtonKey"),
+ popup: null,
+ callback() {
+ MsgJunkMailInfo(false);
+ return true;
+ }
+ },
+ {
+ label: this.mStringBundle.getString("junkBarButton"),
+ accessKey: this.mStringBundle.getString("junkBarButtonKey"),
+ popup: null,
+ callback() {
+ JunkSelectedMessages(false);
+ return true;
+ }
+ }];
+ this.mMsgNotificationBar.appendNotification(junkBarMsg, "junkContent",
+ null, this.mMsgNotificationBar.PRIORITY_WARNING_HIGH, buttons);
+ this.mMsgNotificationBar.collapsed = false;
+ }
+ }
+ },
+
+ remoteOrigins: null,
+
+ isShowingJunkNotification() {
+ return !!this.mMsgNotificationBar.getNotificationWithValue("junkContent");
+ },
+
+ setRemoteContentMsg(aMsgHdr, aContentURI, aCanOverride) {
+ // remoteOrigins is a Set of all blockable Origins.
+ if (!this.remoteOrigins)
+ this.remoteOrigins = new Set();
+
+ var origin = aContentURI.spec;
+ try {
+ origin = aContentURI.scheme + "://" + aContentURI.hostPort;
+ }
+ // No hostport so likely a special url. Try to use the whole url and see
+ // what comes of it.
+ catch (e) { }
+
+ this.remoteOrigins.add(origin);
+
+ if (this.mMsgNotificationBar.getNotificationWithValue("remoteContent"))
+ return;
+
+ var headerParser = MailServices.headerParser;
+ // update the allow remote content for sender string
+ var mailbox = headerParser.extractHeaderAddressMailboxes(aMsgHdr.author);
+ var emailAddress = mailbox || aMsgHdr.author;
+ var displayName = headerParser.extractFirstName(aMsgHdr.mime2DecodedAuthor);
+ var brandName = this.mBrandBundle.getString("brandShortName");
+ var remoteContentMsg = this.mStringBundle
+ .getFormattedString("remoteContentBarMessage",
+ [brandName]);
+ var buttons = [{
+ label: this.mStringBundle.getString("remoteContentPrefLabel"),
+ accessKey: this.mStringBundle.getString("remoteContentPrefAccesskey"),
+ popup: "remoteContentOptions"
+ }];
+
+ this.mMsgNotificationBar
+ .appendNotification(remoteContentMsg,
+ "remoteContent",
+ null,
+ this.mMsgNotificationBar.PRIORITY_WARNING_MEDIUM,
+ (aCanOverride ? buttons : []));
+ },
+
+ // aUrl is the nsIURI for the message currently loaded in the message pane
+ setPhishingMsg(aUrl) {
+ // if we've explicitly marked this message as not being an email scam, then don't
+ // bother checking it with the phishing detector.
+ var phishingMsg = false;
+
+ if (!checkMsgHdrPropertyIsNot("notAPhishMessage", kIsAPhishMessage))
+ phishingMsg = isMsgEmailScam(aUrl);
+
+ var oldNotif = this.mMsgNotificationBar.getNotificationWithValue("phishingContent");
+ if (phishingMsg) {
+ if (!oldNotif) {
+ let brandName = this.mBrandBundle.getString("brandShortName");
+ let phishingMsgNote = this.mStringBundle.getFormattedString("phishingBarMessage",
+ [brandName]);
+
+ let buttons = [{
+ label: this.mStringBundle.getString("phishingBarIgnoreButton"),
+ accessKey: this.mStringBundle.getString("phishingBarIgnoreButtonKey"),
+ popup: null,
+ callback() {
+ MsgIsNotAScam();
+ }
+ }];
+
+ this.mMsgNotificationBar.appendNotification(phishingMsgNote, "phishingContent",
+ null, this.mMsgNotificationBar.PRIORITY_CRITICAL_MEDIUM, buttons);
+ }
+ }
+ },
+
+ setMDNMsg(aMdnGenerator, aMsgHeader, aMimeHdr) {
+ this.mdnGenerator = aMdnGenerator;
+ // Return receipts can be RFC 3798 "Disposition-Notification-To",
+ // or non-standard "Return-Receipt-To".
+ var mdnHdr = aMimeHdr.extractHeader("Disposition-Notification-To", false) ||
+ aMimeHdr.extractHeader("Return-Receipt-To", false); // not
+ var fromHdr = aMimeHdr.extractHeader("From", false);
+
+ var mdnAddr = MailServices.headerParser
+ .extractHeaderAddressMailboxes(mdnHdr);
+ var fromAddr = MailServices.headerParser
+ .extractHeaderAddressMailboxes(fromHdr);
+
+ var authorName = MailServices.headerParser
+ .extractFirstName(aMsgHeader.mime2DecodedAuthor)
+ || aMsgHeader.author;
+
+ var barMsg;
+ // If the return receipt doesn't go to the sender address, note that in the
+ // notification.
+ if (mdnAddr != fromAddr)
+ barMsg = this.mStringBundle.getFormattedString("mdnBarMessageAddressDiffers",
+ [authorName, mdnAddr]);
+ else
+ barMsg = this.mStringBundle.getFormattedString("mdnBarMessageNormal", [authorName]);
+
+ var oldNotif = this.mMsgNotificationBar.getNotificationWithValue("mdnContent");
+ if (!oldNotif) {
+ let buttons = [{
+ label: this.mStringBundle.getString("mdnBarSendReqButton"),
+ accessKey: this.mStringBundle.getString("mdnBarSendReqButtonKey"),
+ popup: null,
+ callback: SendMDNResponse
+ },
+ {
+ label: this.mStringBundle.getString("mdnBarIgnoreButton"),
+ accessKey: this.mStringBundle.getString("mdnBarIgnoreButtonKey"),
+ popup: null,
+ callback: IgnoreMDNResponse
+ }];
+
+ this.mMsgNotificationBar.appendNotification(barMsg, "mdnContent",
+ null, this.mMsgNotificationBar.PRIORITY_INFO_MEDIUM, buttons);
+ }
+ },
+
+ clearMsgNotifications() {
+ }
+};
+
+/**
+ * LoadMsgWithRemoteContent
+ * Reload the current message, allowing remote content
+ */
+function LoadMsgWithRemoteContent() {
+ // we want to get the msg hdr for the currently selected message
+ // change the "remoteContentBar" property on it
+ // then reload the message
+
+ setMsgHdrPropertyAndReload("remoteContentPolicy", kAllowRemoteContent);
+ window.content.focus();
+}
+
+/**
+ * Populate the remote content options for the current message.
+ */
+function onRemoteContentOptionsShowing(aEvent) {
+ var origins = [...gMessageNotificationBar.remoteOrigins];
+
+ var addresses = {};
+ MailServices.headerParser.parseHeadersWithArray(
+ gMessageDisplay.displayedMessage.author, addresses, {}, {});
+ var authorEmailAddress = addresses.value[0];
+
+ var emailURI = Services.io.newURI(
+ "chrome://messenger/content/email=" + authorEmailAddress);
+ var principal = Services.scriptSecurityManager
+ .createCodebasePrincipal(emailURI, {});
+ // Put author email first in the menu.
+ origins.unshift(principal.origin);
+
+ // Out with the old...
+ let childNodes = aEvent.target.querySelectorAll(".allow-remote-uri");
+ for (let child of childNodes)
+ child.remove();
+
+ var messengerBundle = gMessageNotificationBar.mStringBundle;
+ var separator = document.getElementById("remoteContentSettingsMenuSeparator")
+
+ // ... and in with the new.
+ for (let origin of origins) {
+ let menuitem = document.createElement("menuitem");
+ let host = origin.replace("chrome://messenger/content/email=", "");
+ let hostString = messengerBundle.getFormattedString("remoteContentAllow", [host]);
+ menuitem.setAttribute("label", hostString);
+ menuitem.setAttribute("value", origin);
+ menuitem.setAttribute("class", "allow-remote-uri");
+ aEvent.target.insertBefore(menuitem, separator);
+ }
+}
+
+/**
+ * Add privileges to display remote content for the given uri.
+ * @param aItem |Node| Item that was selected. The origin
+ * is extracted and converted to a uri and used to add
+ * permissions for the site.
+ */
+function allowRemoteContentForURI(aItem) {
+
+ var origin = aItem.getAttribute("value");
+
+ if (!origin)
+ return;
+
+ let uri = Services.io.newURI(origin);
+ Services.perms.add(uri, "image", Services.perms.ALLOW_ACTION);
+
+ ReloadMessage();
+}
+
+/**
+ * Displays fine-grained, per-site permissions for remote content.
+ */
+function editRemoteContentSettings() {
+ toDataManager("|permissions");
+ if (!Services.prefs.getBoolPref("browser.preferences.instantApply"))
+ ReloadMessage();
+}
+
+/**
+ * msgHdrForCurrentMessage
+ * Returns the msg hdr associated with the current loaded message.
+ */
+function msgHdrForCurrentMessage() {
+ var msgURI = GetLoadedMessage();
+ return (msgURI && !(/type=application\/x-message-display/.test(msgURI))) ? messenger.msgHdrFromURI(msgURI) : null;
+}
+
+function MsgIsNotAScam() {
+ // we want to get the msg hdr for the currently selected message
+ // change the "isPhishingMsg" property on it
+ // then reload the message
+
+ setMsgHdrPropertyAndReload("notAPhishMessage", kNotAPhishMessage);
+}
+
+function setMsgHdrPropertyAndReload(aProperty, aValue) {
+ // we want to get the msg hdr for the currently selected message
+ // change the appropiate property on it then reload the message
+
+ var msgHdr = msgHdrForCurrentMessage();
+ if (msgHdr) {
+ msgHdr.setUint32Property(aProperty, aValue);
+ ReloadMessage();
+ }
+}
+
+function checkMsgHdrPropertyIsNot(aProperty, aValue) {
+ // we want to get the msg hdr for the currently selected message,
+ // get the appropiate property on it and then test against value.
+
+ var msgHdr = msgHdrForCurrentMessage();
+ return (msgHdr && msgHdr.getUint32Property(aProperty) != aValue);
+}
+
+/**
+ * Mark a specified message as read.
+ * @param msgHdr header (nsIMsgDBHdr) of the message to mark as read
+ */
+function MarkMessageAsRead(msgHdr) {
+ ClearPendingReadTimer();
+ msgHdr.folder.markMessagesRead([msgHdr], true);
+}
+
+function ClearPendingReadTimer() {
+ if (gMarkViewedMessageAsReadTimer) {
+ clearTimeout(gMarkViewedMessageAsReadTimer);
+ gMarkViewedMessageAsReadTimer = null;
+ }
+}
+
+function OnMsgParsed(aUrl) {
+ gMessageNotificationBar.setPhishingMsg(aUrl);
+
+ // notify anyone (e.g., extensions) who's interested in when a message is loaded.
+ var msgURI = GetLoadedMessage();
+ Services.obs.notifyObservers(msgWindow.msgHeaderSink,
+ "MsgMsgDisplayed", msgURI);
+
+ // scale any overflowing images
+ var doc = getMessageBrowser().contentDocument;
+ var imgs = doc.getElementsByTagName("img");
+ for (var img of imgs) {
+ if (img.className == "moz-attached-image" &&
+ img.naturalWidth > doc.body.clientWidth) {
+ if (img.hasAttribute("shrinktofit"))
+ img.setAttribute("isshrunk", "true");
+ else
+ img.setAttribute("overflowing", "true");
+ }
+ }
+}
+
+function OnMsgLoaded(aUrl) {
+ if (!aUrl)
+ return;
+
+ // nsIMsgMailNewsUrl.folder throws an error when opening .eml files.
+ var folder;
+ try {
+ folder = aUrl.folder;
+ } catch (ex) {}
+
+ var msgURI = GetLoadedMessage();
+
+ if (!folder || !msgURI)
+ return;
+
+ // If we are in the middle of a delete or move operation, make sure that
+ // if the user clicks on another message then that message stays selected
+ // and the selection does not "snap back" to the message chosen by
+ // SetNextMessageAfterDelete() when the operation completes (bug 243532).
+ var wintype = document.documentElement.getAttribute("windowtype");
+ gNextMessageViewIndexAfterDelete = -2;
+
+ var msgHdr = msgHdrForCurrentMessage();
+ gMessageNotificationBar.setJunkMsg(msgHdr);
+ // Reset the blocked origins so we can populate it again for this message.
+ // Reset to null so it's only a Set if there's something in the Set.
+ gMessageNotificationBar.remoteOrigins = null;
+
+ var markReadAutoMode = Services.prefs.getBoolPref("mailnews.mark_message_read.auto");
+
+ // We just finished loading a message. If messages are to be marked as read
+ // automatically, set a timer to mark the message is read after n seconds
+ // where n can be configured by the user.
+ if (msgHdr && !msgHdr.isRead && markReadAutoMode) {
+ let markReadOnADelay = Services.prefs.getBoolPref("mailnews.mark_message_read.delay");
+ // Only use the timer if viewing using the 3-pane preview pane and the
+ // user has set the pref.
+ if (markReadOnADelay && wintype == "mail:3pane") // 3-pane window
+ {
+ ClearPendingReadTimer();
+ let markReadDelayTime = Services.prefs.getIntPref("mailnews.mark_message_read.delay.interval");
+ if (markReadDelayTime == 0)
+ MarkMessageAsRead(msgHdr);
+ else
+ gMarkViewedMessageAsReadTimer = setTimeout(MarkMessageAsRead,
+ markReadDelayTime * 1000,
+ msgHdr);
+ } else // standalone msg window
+ {
+ MarkMessageAsRead(msgHdr);
+ }
+ }
+
+ // See if MDN was requested but has not been sent.
+ HandleMDNResponse(aUrl);
+}
+
+/*
+ * This function handles all mdn response generation (ie, imap and pop).
+ * For pop the msg uid can be 0 (ie, 1st msg in a local folder) so no
+ * need to check uid here. No one seems to set mimeHeaders to null so
+ * no need to check it either.
+ */
+function HandleMDNResponse(aUrl) {
+ if (!aUrl)
+ return;
+
+ var msgFolder = aUrl.folder;
+ var msgHdr = gFolderDisplay.selectedMessage;
+ if (!msgFolder || !msgHdr || gFolderDisplay.selectedMessageIsNews)
+ return;
+
+ // if the message is marked as junk, do NOT attempt to process a return receipt
+ // in order to better protect the user
+ if (SelectedMessagesAreJunk())
+ return;
+
+ var mimeHdr;
+
+ try {
+ mimeHdr = aUrl.mimeHeaders;
+ } catch (ex) {
+ return;
+ }
+
+ // If we didn't get the message id when we downloaded the message header,
+ // we cons up an md5: message id. If we've done that, we'll try to extract
+ // the message id out of the mime headers for the whole message.
+ var msgId = msgHdr.messageId;
+ if (msgId.split(":")[0] == "md5") {
+ var mimeMsgId = mimeHdr.extractHeader("Message-Id", false);
+ if (mimeMsgId)
+ msgHdr.messageId = mimeMsgId;
+ }
+
+ // After a msg is downloaded it's already marked READ at this point so we must check if
+ // the msg has a "Disposition-Notification-To" header and no MDN report has been sent yet.
+ if (msgHdr.flags & Ci.nsMsgMessageFlags.MDNReportSent)
+ return;
+
+ var DNTHeader = mimeHdr.extractHeader("Disposition-Notification-To", false);
+ var oldDNTHeader = mimeHdr.extractHeader("Return-Receipt-To", false);
+ if (!DNTHeader && !oldDNTHeader)
+ return;
+
+ // Everything looks good so far, let's generate the MDN response.
+ var mdnGenerator = Cc["@mozilla.org/messenger-mdn/generator;1"]
+ .createInstance(Ci.nsIMsgMdnGenerator);
+ var askUser = mdnGenerator.process(Ci.nsIMsgMdnGenerator.eDisplayed,
+ msgWindow,
+ msgFolder,
+ msgHdr.messageKey,
+ mimeHdr,
+ false);
+ if (askUser)
+ gMessageNotificationBar.setMDNMsg(mdnGenerator, msgHdr, mimeHdr);
+}
+
+function SendMDNResponse() {
+ gMessageNotificationBar.mdnGenerator.userAgreed();
+}
+
+function IgnoreMDNResponse() {
+ gMessageNotificationBar.mdnGenerator.userDeclined();
+}
+
+/**
+ * Opens a search window with the given folder, or the displayed one if none is
+ * chosen.
+ *
+ * @param [aFolder] the folder to open the search window for, if different from
+ * the displayed one
+ */
+function MsgSearchMessages(aFolder) {
+ let folder = aFolder || gFolderDisplay.displayedFolder;
+ OpenOrFocusWindow({ folder }, "mailnews:search",
+ "chrome://messenger/content/SearchDialog.xul");
+}
+
+function MsgJunkMailInfo(aCheckFirstUse) {
+ if (aCheckFirstUse) {
+ if (!Services.prefs.getBoolPref("mailnews.ui.junk.firstuse"))
+ return;
+ Services.prefs.setBoolPref("mailnews.ui.junk.firstuse", false);
+
+ // Check to see if this is an existing profile where the user has started
+ // using the junk mail feature already.
+ if (MailServices.junk.userHasClassified)
+ return;
+ }
+
+ var desiredWindow = Services.wm.getMostRecentWindow("mailnews:junkmailinfo");
+
+ if (desiredWindow)
+ desiredWindow.focus();
+ else
+ window.openDialog("chrome://messenger/content/junkMailInfo.xul", "mailnews:junkmailinfo", "centerscreen,resizeable=no,titlebar,chrome,modal", null);
+}
+
+function MsgSearchAddresses() {
+ var args = { directory: null };
+ OpenOrFocusWindow(args, "mailnews:absearch", "chrome://messenger/content/ABSearchDialog.xul");
+}
+
+function MsgFilterList(args) {
+ OpenOrFocusWindow(args, "mailnews:filterlist", "chrome://messenger/content/FilterListDialog.xul");
+}
+
+function OpenOrFocusWindow(args, windowType, chromeURL) {
+ var desiredWindow = Services.wm.getMostRecentWindow(windowType);
+
+ if (desiredWindow) {
+ desiredWindow.focus();
+ if ("refresh" in args && args.refresh)
+ desiredWindow.refresh(args);
+ } else
+ window.openDialog(chromeURL, "", "chrome,resizable,status,centerscreen,dialog=no", args);
+}
+
+function getMailToolbox() {
+ return document.getElementById("mail-toolbox");
+}
+
+function MailToolboxCustomizeInit() {
+ toolboxCustomizeInit("mail-menubar");
+}
+
+function MailToolboxCustomizeDone(aToolboxChanged) {
+ toolboxCustomizeDone("mail-menubar", getMailToolbox(), aToolboxChanged);
+
+ // Make sure the folder location picker is initialized.
+ let folderContainer = document.getElementById("folder-location-container");
+ if (folderContainer &&
+ folderContainer.parentNode.localName != "toolbarpalette") {
+ FolderPaneSelectionChange();
+ }
+}
+
+function MailToolboxCustomizeChange(event) {
+ toolboxCustomizeChange(getMailToolbox(), event);
+}
diff --git a/comm/suite/mailnews/content/mailWindowOverlay.xul b/comm/suite/mailnews/content/mailWindowOverlay.xul
new file mode 100644
index 0000000000..61a7b4ff65
--- /dev/null
+++ b/comm/suite/mailnews/content/mailWindowOverlay.xul
@@ -0,0 +1,1929 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/smime/msgReadSMIMEOverlay.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/content/bindings.css" type="text/css"?>
+
+
+<?xul-overlay href="chrome://communicator/content/charsetOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/contentAreaContextOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/viewZoomOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/viewApplyThemeOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/msgHdrViewOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/mailOverlay.xul"?>
+<?xul-overlay href="chrome://messenger/content/mailKeysOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?>
+<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?>
+
+<!DOCTYPE overlay [
+ <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd">
+ %messengerDTD;
+ <!ENTITY % mailKeysDTD SYSTEM "chrome://messenger/locale/mailKeysOverlay.dtd">
+ %mailKeysDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+ <!ENTITY % msgViewPickerDTD SYSTEM "chrome://messenger/locale/msgViewPickerOverlay.dtd">
+ %msgViewPickerDTD;
+ <!ENTITY % msgHdrViewPopupDTD SYSTEM "chrome://messenger/locale/msgHdrViewPopup.dtd">
+ %msgHdrViewPopupDTD;
+ <!ENTITY % contentAreaCommandsDTD SYSTEM "chrome://communicator/locale/contentAreaCommands.dtd">
+ %contentAreaCommandsDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % msgReadSMIMEDTD SYSTEM "chrome://messenger-smime/locale/msgReadSMIMEOverlay.dtd">
+ %msgReadSMIMEDTD;
+]>
+
+<overlay
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script src="chrome://messenger/content/mailCommands.js"/>
+<script src="chrome://messenger/content/junkCommands.js"/>
+<script src="chrome://messenger/content/mailWindowOverlay.js"/>
+<script src="chrome://messenger/content/msgViewPickerOverlay.js"/>
+<script src="chrome://messenger-newsblog/content/newsblogOverlay.js"/>
+<script src="chrome://messenger/content/mail-offline.js"/>
+<script src="chrome://communicator/content/findUtils.js"/>
+<script src="chrome://global/content/printUtils.js"/>
+<script src="chrome://messenger/content/folderDisplay.js"/>
+<script src="chrome://messenger-smime/content/msgReadSMIMEOverlay.js"/>
+<script>
+<![CDATA[
+ ChromeUtils.import("resource://gre/modules/PlacesUtils.jsm");
+ ChromeUtils.import("resource:///modules/PlacesUIUtils.jsm");
+]]></script>
+
+<stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_offlinePrompts" src="chrome://messenger/locale/offline.properties"/>
+ <stringbundle id="bundle_read_smime"
+ src="chrome://messenger-smime/locale/msgReadSMIMEOverlay.properties"/>
+ <stringbundle id="bundle_viewZoom"/>
+ <stringbundle id="bundle_viewApplyTheme"/>
+ <stringbundle id="findBundle" src="chrome://global/locale/finddialog.properties"/>
+</stringbundleset>
+
+<!-- Performance optimization...we include utilityOverlay.xul which defines some command sets
+ which are updated based on events like focus and select. We have our own custom events
+ which we use to optmize when we do command updating. To avoid unnecessary command updating,
+ we are going to override the events the global edit menu items and select edit menu items
+ are updated on with events of our own controlling.
+ -->
+
+<commandset id="globalEditMenuItems"
+ commandupdater="true"
+ events="create-menu-edit"
+ oncommandupdate="goUpdateGlobalEditMenuItems()"/>
+<commandset id="selectEditMenuItems"
+ commandupdater="true"
+ events="create-menu-edit"
+ oncommandupdate="goUpdateSelectEditMenuItems()"/>
+
+<!-- End command set merging -->
+
+<commandset id="mailDownloadCommands">
+ <command id="cmd_downloadFlagged" oncommand="goDoCommand('cmd_downloadFlagged')"/>
+ <command id="cmd_downloadSelected" oncommand="goDoCommand('cmd_downloadSelected')"/>
+</commandset>
+
+<commandset id="mailFileMenuItems"
+ commandupdater="true"
+ events="create-menu-file, message-header-pane"
+ oncommandupdate="goUpdateMailMenuItems(this)">
+
+ <command id="cmd_getNewMessages" oncommand="goDoCommand('cmd_getNewMessages')" disabled="true"/>
+ <command id="cmd_open" oncommand="goDoCommand('cmd_open')"/>
+
+ <command id="cmd_emptyTrash" oncommand="goDoCommand('cmd_emptyTrash')" disabled="true"/>
+ <command id="cmd_compactFolder" oncommand="goDoCommand('cmd_compactFolder')" disabled="true"/>
+
+ <command id="cmd_printSetup" oncommand="goDoCommand('cmd_printSetup')" disabled="true"/>
+ <command id="cmd_print" oncommand="goDoCommand('cmd_print')" disabled="true"/>
+ <command id="cmd_printpreview" oncommand="goDoCommand('cmd_printpreview')" disabled="true"/>
+ <command id="cmd_saveAsFile" oncommand="goDoCommand('cmd_saveAsFile')" disabled="true"/>
+ <command id="cmd_saveAsTemplate" oncommand="goDoCommand('cmd_saveAsTemplate')" disabled="true"/>
+ <command id="cmd_getNextNMessages" oncommand="goDoCommand('cmd_getNextNMessages')" disabled="true"/>
+ <command id="cmd_renameFolder" oncommand="goDoCommand('cmd_renameFolder')" />
+ <command id="cmd_sendUnsentMsgs" oncommand="goDoCommand('cmd_sendUnsentMsgs')" />
+ <command id="cmd_subscribe" oncommand="goDoCommand('cmd_subscribe')" disabled="true"/>
+ <command id="cmd_synchronizeOffline" oncommand="goDoCommand('cmd_synchronizeOffline');" disabled="true"/>
+ <command id="cmd_settingsOffline" oncommand="goDoCommand('cmd_settingsOffline');" disabled="true"/>
+</commandset>
+
+<commandset id="mailCommands">
+ <command id="cmd_newNavigator"/>
+ <command id="cmd_newPrivateWindow"/>
+ <command id="cmd_newEditor"/>
+ <command id="cmd_createFilterFromPopup" oncommand="goDoCommand('cmd_createFilterFromPopup')"/>
+ <command id="cmd_pageSetup"/>
+</commandset>
+
+<commandset id="mailViewMenuItems"
+ commandupdater="true"
+ events="create-menu-view"
+ oncommandupdate="goUpdateMailMenuItems(this)">
+
+ <command id="cmd_viewPageSource" oncommand="goDoCommand('cmd_viewPageSource')" disabled="true"/>
+ <command id="cmd_setFolderCharset" oncommand="goDoCommand('cmd_setFolderCharset')" />
+ <command id="cmd_reload" oncommand="goDoCommand('cmd_reload')" disabled="true"/>
+
+ <command id="cmd_expandAllThreads" oncommand="goDoCommand('cmd_expandAllThreads')" disabled="true"/>
+ <command id="cmd_collapseAllThreads" oncommand="goDoCommand('cmd_collapseAllThreads')" disabled="true"/>
+ <command id="cmd_viewAllMsgs" oncommand="goDoCommand('cmd_viewAllMsgs')" disabled="true"/>
+ <command id="cmd_viewUnreadMsgs" oncommand="goDoCommand('cmd_viewUnreadMsgs')" disabled="true"/>
+ <command id="cmd_viewThreadsWithUnread" oncommand="goDoCommand('cmd_viewThreadsWithUnread')" disabled="true"/>
+ <command id="cmd_viewWatchedThreadsWithUnread" oncommand="goDoCommand('cmd_viewWatchedThreadsWithUnread')" disabled="true"/>
+ <command id="cmd_viewIgnoredThreads" oncommand="goDoCommand('cmd_viewIgnoredThreads')" disabled="true"/>
+ <!-- Needed to support the Lightning Task filter See Bug 316916 -->
+ <command id="cmd_showQuickFilterBar" oncommand="goDoCommand('cmd_showQuickFilterBar');"/>
+ <commandset id="viewZoomCommands"/>
+ <command id="cmd_viewSecurityStatus" oncommand="showMessageReadSecurityInfo();" disabled="true"/>
+</commandset>
+
+<commandset id="mailEditMenuItems"
+ commandupdater="true"
+ events="create-menu-edit, message-header-pane"
+ oncommandupdate="goUpdateMailMenuItems(this)">
+
+ <command id="cmd_undo"
+ valueDeleteMsg="&undoDeleteMsgCmd.label;"
+ valueMoveMsg="&undoMoveMsgCmd.label;"
+ valueCopyMsg="&undoCopyMsgCmd.label;"
+ valueUnmarkAllMsgs="&undoMarkAllCmd.label;"
+ valueDefault="&undoDefaultCmd.label;"/>
+ <command id="cmd_redo"
+ valueDeleteMsg="&redoDeleteMsgCmd.label;"
+ valueMoveMsg="&redoMoveMsgCmd.label;"
+ valueCopyMsg="&redoCopyMsgCmd.label;"
+ valueUnmarkAllMsgs="&redoMarkAllCmd.label;"
+ valueDefault="&redoDefaultCmd.label;"/>
+ <command id="cmd_cut"/>
+ <command id="cmd_copy"/>
+ <command id="cmd_paste"/>
+ <command id="cmd_delete"
+ valueFolder="&deleteFolderCmd.label;"
+ valueFolderAccessKey="&deleteFolderCmd.accesskey;"
+ valueNewsgroup="&unsubscribeNewsgroupCmd.label;"
+ valueNewsgroupAccessKey="&unsubscribeNewsgroupCmd.accesskey;"
+ valueMessage="&deleteMsgCmd.label;"
+ valueMessageAccessKey="&deleteMsgCmd.accesskey;"
+ valueIMAPDeletedMessage="&undeleteMsgCmd.label;"
+ valueIMAPDeletedMessageAccessKey="&undeleteMsgCmd.accesskey;"
+ valueMessages="&deleteMsgsCmd.label;"
+ valueMessagesAccessKey="&deleteMsgsCmd.accesskey;"
+ valueIMAPDeletedMessages="&undeleteMsgsCmd.label;"
+ valueIMAPDeletedMessagesAccessKey="&undeleteMsgsCmd.accesskey;"/>
+ <command id="cmd_selectAll"/>
+ <command id="cmd_selectThread" oncommand="goDoCommand('cmd_selectThread')"/>
+ <command id="cmd_selectFlagged" oncommand="goDoCommand('cmd_selectFlagged')"/>
+ <command id="cmd_properties" oncommand="goDoCommand('cmd_properties')"
+ valueNewsgroup="&folderPropsNewsgroupCmd.label;"
+ valueFolder="&folderPropsFolderCmd.label;"
+ valueGeneric="&folderPropsCmd.label;"/>
+ <command id="cmd_find" oncommand="goDoCommand('cmd_find')" disabled="true"/>
+ <command id="cmd_findNext"
+ oncommand="goDoCommand('cmd_findNext');"
+ disabled="true"/>
+ <command id="cmd_findPrev"
+ oncommand="goDoCommand('cmd_findPrev');"
+ disabled="true"/>
+ <command id="cmd_findTypeText"/>
+ <command id="cmd_findTypeLinks"/>
+ <command id="cmd_search" oncommand="goDoCommand('cmd_search');"/>
+ <command id="cmd_stop" oncommand="MsgStop();"/>
+</commandset>
+
+<commandset id="mailEditContextMenuItems">
+ <command id="cmd_copyLink"/>
+ <command id="cmd_copyImage"/>
+</commandset>
+
+<commandset id="mailGoMenuItems"
+ commandupdater="true"
+ events="create-menu-go"
+ oncommandupdate="goUpdateMailMenuItems(this)">
+
+ <command id="cmd_nextMsg" oncommand="goDoCommand('cmd_nextMsg')" disabled="true"/>
+ <command id="cmd_nextUnreadMsg" oncommand="goDoCommand('cmd_nextUnreadMsg')" disabled="true"/>
+ <command id="cmd_nextFlaggedMsg" oncommand="goDoCommand('cmd_nextFlaggedMsg')" disabled="true"/>
+ <command id="cmd_nextUnreadThread" oncommand="goDoCommand('cmd_nextUnreadThread')" disabled="true"/>
+ <command id="cmd_previousMsg" oncommand="goDoCommand('cmd_previousMsg')" disabled="true"/>
+ <command id="cmd_previousUnreadMsg" oncommand="goDoCommand('cmd_previousUnreadMsg')" disabled="true"/>
+ <command id="cmd_previousFlaggedMsg" oncommand="goDoCommand('cmd_previousFlaggedMsg')" disabled="true"/>
+ <command id="cmd_goStartPage" oncommand="goDoCommand('cmd_goStartPage');"/>
+ <command id="cmd_goBack" oncommand="goDoCommand('cmd_goBack')" disabled="true"/>
+ <command id="cmd_goForward" oncommand="goDoCommand('cmd_goForward');" disabled="true"/>
+</commandset>
+
+<commandset id="mailMessageMenuItems"
+ commandupdater="true"
+ events="create-menu-message"
+ oncommandupdate="goUpdateMailMenuItems(this)">
+ <command id="cmd_archive" oncommand="goDoCommand('cmd_archive')"/>
+ <command id="cmd_reply" oncommand="goDoCommand('cmd_reply')"/>
+ <command id="cmd_replyList" oncommand="goDoCommand('cmd_replyList')"/>
+ <command id="cmd_replyGroup" oncommand="goDoCommand('cmd_replyGroup')"/>
+ <command id="cmd_replySender" oncommand="goDoCommand('cmd_replySender')"/>
+ <command id="cmd_replyall" oncommand="goDoCommand('cmd_replyall')"/>
+ <command id="cmd_replySenderAndGroup" oncommand="goDoCommand('cmd_replySenderAndGroup')"/>
+ <command id="cmd_replyAllRecipients" oncommand="goDoCommand('cmd_replyAllRecipients')"/>
+ <command id="cmd_forward" oncommand="goDoCommand('cmd_forward')"/>
+ <command id="cmd_forwardInline" oncommand="goDoCommand('cmd_forwardInline')"/>
+ <command id="cmd_forwardAttachment" oncommand="goDoCommand('cmd_forwardAttachment')"/>
+ <command id="cmd_editAsNew" oncommand="MsgEditMessageAsNew(event);"/>
+ <command id="cmd_editDraftMsg" oncommand="MsgEditDraftMessage(event);"/>
+ <command id="cmd_newMsgFromTemplate"
+ oncommand="MsgNewMessageFromTemplate(event);"/>
+ <command id="cmd_editTemplateMsg" oncommand="MsgEditTemplateMessage(event);"/>
+ <command id="cmd_openMessage" oncommand="goDoCommand('cmd_openMessage')"/>
+ <command id="cmd_createFilterFromMenu" oncommand="goDoCommand('cmd_createFilterFromMenu')"/>
+ <command id="cmd_cancel" oncommand="goDoCommand('cmd_cancel')"/>
+ <command id="cmd_killThread" oncommand="goDoCommand('cmd_killThread')"/>
+ <command id="cmd_killSubthread" oncommand="goDoCommand('cmd_killSubthread')"/>
+ <command id="cmd_watchThread" oncommand="goDoCommand('cmd_watchThread')"/>
+</commandset>
+
+<commandset id="mailToolbarItems"
+ commandupdater="true"
+ events="mail-toolbar"
+ oncommandupdate="goUpdateMailMenuItems(this);
+ /* update cmd_delete manually to avoid a doubled id */
+ goUpdateCommand('cmd_delete');">
+ <command id="button_reply"/>
+ <command id="button_replyall"/>
+ <command id="button_forward"/>
+ <command id="button_delete"/>
+ <command id="button_mark"/>
+ <command id="button_getNewMessages"/>
+ <command id="button_print"/>
+ <command id="button_next"/>
+ <command id="button_goBack"/>
+ <command id="button_goForward"/>
+ <command id="button_file"/>
+ <command id="cmd_shiftDelete" oncommand="goDoCommand('cmd_shiftDelete');"/>
+ <command id="button_junk"/>
+ <command id="button_search"/>
+</commandset>
+
+
+<commandset id="mailGetMsgMenuItems"
+ commandupdater="true"
+ events="create-menu-getMsgToolbar,create-menu-file"
+ oncommandupdate="goUpdateMailMenuItems(this)">
+
+ <command id="cmd_getMsgsForAuthAccounts"
+ oncommand="goDoCommand('cmd_getMsgsForAuthAccounts'); event.stopPropagation()"
+ disabled="true"/>
+</commandset>
+
+<commandset id="mailMarkMenuItems"
+ commandupdater="true"
+ events="create-menu-mark"
+ oncommandupdate="goUpdateMailMenuItems(this)">
+
+ <command id="cmd_markAsRead" oncommand="goDoCommand('cmd_markAsRead'); event.stopPropagation()" disabled="true"/>
+ <command id="cmd_markAsUnread" oncommand="goDoCommand('cmd_markAsUnread'); event.stopPropagation();" disabled="true"/>
+ <command id="cmd_markAllRead" oncommand="goDoCommand('cmd_markAllRead'); event.stopPropagation()" disabled="true"/>
+ <command id="cmd_markThreadAsRead" oncommand="goDoCommand('cmd_markThreadAsRead'); event.stopPropagation()" disabled="true"/>
+ <command id="cmd_markReadByDate" oncommand="goDoCommand('cmd_markReadByDate');" disabled="true"/>
+ <command id="cmd_markAsFlagged" oncommand="goDoCommand('cmd_markAsFlagged'); event.stopPropagation()" disabled="true"/>
+ <command id="cmd_markAsJunk" oncommand="goDoCommand('cmd_markAsJunk'); event.stopPropagation()" disabled="true"/>
+ <command id="cmd_markAsNotJunk" oncommand="goDoCommand('cmd_markAsNotJunk'); event.stopPropagation()" disabled="true"/>
+ <command id="cmd_recalculateJunkScore" oncommand="goDoCommand('cmd_recalculateJunkScore');" disabled="true"/>
+ <command id="cmd_markAsShowRemote" oncommand="goDoCommand('cmd_markAsShowRemote'); event.stopPropagation()" disabled="true"/>
+ <command id="cmd_markAsNotPhish" oncommand="goDoCommand('cmd_markAsNotPhish'); event.stopPropagation()" disabled="true"/>
+ <command id="cmd_viewAllHeader"
+ oncommand="goDoCommand('cmd_viewAllHeader');"
+ disabled="true"/>
+ <command id="cmd_viewNormalHeader"
+ oncommand="goDoCommand('cmd_viewNormalHeader');"
+ disabled="true"/>
+</commandset>
+
+<commandset id="mailToolsMenuItems"
+ commandupdater="true"
+ events="create-menu-tasks"
+ oncommandupdate="goUpdateMailMenuItems(this)">
+ <command id="cmd_displayMsgFilters"
+ disabled="true"
+ oncommand="goDoCommand('cmd_displayMsgFilters');"/>
+ <command id="cmd_applyFilters" oncommand="goDoCommand('cmd_applyFilters');" disabled="true"/>
+ <command id="cmd_applyFiltersToSelection"
+ oncommand="goDoCommand('cmd_applyFiltersToSelection');"
+ disabled="true"
+ valueSelection="&filtersApplyToSelection.label;"
+ valueSelectionAccessKey="&filtersApplyToSelection.accesskey;"
+ valueMessage="&filtersApplyToMessage.label;"
+ valueMessageAccessKey="&filtersApplyToMessage.accesskey;"/>
+ <command id="cmd_runJunkControls" oncommand="goDoCommand('cmd_runJunkControls');" disabled="true"/>
+ <command id="cmd_deleteJunk" oncommand="goDoCommand('cmd_deleteJunk');" disabled="true"/>
+</commandset>
+
+<keyset id="mailKeys">
+ <key id="space" key=" " modifiers="shift any" oncommand="SpaceHit(event);"/>
+
+ <!-- File Menu -->
+ <key id="key_newTab"
+ key="&newTabCmd.key;"
+ modifiers="accel"
+ oncommand="MsgOpenNewTab();"/>
+ <key id="key_newNavigator"/>
+ <key id="key_newPrivateWindow"/>
+ <key id="key_newBlankPage"/>
+ <key id="key_close"/>
+ <!-- Edit Menu -->
+ <key id="key_undo"/>
+ <key id="key_redo"/>
+ <key id="key_cut"/>
+ <key id="key_copy"/>
+ <key id="key_paste"/>
+ <key id="key_selectThread" key="&selectThreadCmd.key;" oncommand="goDoCommand('cmd_selectThread');" modifiers="alt, shift"/>
+
+ <key id="key_markJunk" key="&markAsJunkCmd.key;" oncommand="goDoCommand('cmd_markAsJunk');"/>
+ <key id="key_markNotJunk" key="&markAsNotJunkCmd.key;" oncommand="goDoCommand('cmd_markAsNotJunk');"
+ modifiers="shift"/>
+ <key id="key_markShowRemote" key="&markAsShowRemoteCmd.key;" oncommand="goDoCommand('cmd_markAsShowRemote');"
+ modifiers="shift"/>
+ <key id="key_markNotPhish" key="&markAsNotPhishCmd.key;" oncommand="goDoCommand('cmd_markAsNotPhish');"
+ modifiers="shift"/>
+ <key id="key_markAllRead" key="&markAllReadCmd.key;" oncommand="goDoCommand('cmd_markAllRead');" modifiers="accel, shift"/>
+ <key id="key_markThreadAsRead" key="&markThreadAsReadCmd.key;" oncommand="goDoCommand('cmd_markThreadAsRead')"/>
+ <key id="key_markReadByDate" key="&markReadByDateCmd.key;" oncommand="goDoCommand('cmd_markReadByDate')"/>
+ <key id="key_nextMsg" key="&nextMsgCmd.key;" oncommand="goDoCommand('cmd_nextMsg')"/>
+ <key id="key_nextUnreadMsg" key="&nextUnreadMsgCmd.key;" oncommand="goDoCommand('cmd_nextUnreadMsg')"/>
+ <key id="key_expandAllThreads" key="&expandAllThreadsCmd.key;" oncommand="goDoCommand('cmd_expandAllThreads')"/>
+ <key key="&expandAllThreadsCmd.key;" modifiers="shift" oncommand="goDoCommand('cmd_expandAllThreads')"/>
+ <key id="key_collapseAllThreads" key="&collapseAllThreadsCmd.key;" oncommand="goDoCommand('cmd_collapseAllThreads')"/>
+ <key key="&collapseAllThreadsCmd.key;" modifiers="shift" oncommand="goDoCommand('cmd_collapseAllThreads')"/>
+ <key id="key_nextUnreadThread" key="&nextUnreadThread.key;" oncommand="goDoCommand('cmd_nextUnreadThread')"/>
+ <key id="key_previousMsg" key="&prevMsgCmd.key;" oncommand="goDoCommand('cmd_previousMsg')"/>
+ <key id="key_previousUnreadMsg" key="&prevUnreadMsgCmd.key;" oncommand="goDoCommand('cmd_previousUnreadMsg')"/>
+ <key id="key_archive" key="&archiveMsgCmd.key;" oncommand="goDoCommand('cmd_archive')" modifiers="shift"/>
+ <key id="key_goBack" key="&goBackCmd.commandKey;" oncommand="goDoCommand('cmd_goBack')"/>
+ <key id="key_goForward" key="&goForwardCmd.commandKey;" oncommand="goDoCommand('cmd_goForward');"/>
+ <key id="key_reply" key="&replyMsgCmd.key;" oncommand="goDoCommand('cmd_reply')" modifiers="accel"/>
+ <key id="key_replyall" key="&replyToAllMsgCmd.key;" oncommand="goDoCommand('cmd_replyall')" modifiers="accel, shift"/>
+ <key id="key_forward" key="&forwardMsgCmd.key;" oncommand="goDoCommand('cmd_forward')" modifiers="accel"/>
+ <key id="key_editAsNew"
+ key="&editAsNewMsgCmd.key;"
+ modifiers="accel"
+ oncommand="goDoCommand('cmd_editAsNew');"/>
+ <!-- for display on menus only -->
+ <key id="key_newMsgFromTemplate"
+ keycode="&newMsgFromTemplateCmd.keycode;"/>
+ <key id="key_watchThread" key="&watchThreadMenu.key;" oncommand="goDoCommand('cmd_watchThread')" />
+ <key id="key_killThread" key="&killThreadMenu.key;" oncommand="goDoCommand('cmd_killThread')" />
+ <key id="key_killSubthread" key="&killSubthreadMenu.key;" oncommand="goDoCommand('cmd_killSubthread')" modifiers="shift" />
+ <key id="key_print"/>
+ <key id="key_saveAsFile" key="&saveAsFileCmd.key;" oncommand="goDoCommand('cmd_saveAsFile')" modifiers="accel"/>
+ <key id="key_viewPageSource" key="&pageSourceCmd.key;" oncommand="goDoCommand('cmd_viewPageSource')" modifiers="accel"/>
+ <key id="key_getNewMessages" key="&getNewMsgCmd2.key;" oncommand="goDoCommand('cmd_getNewMessages')" modifiers="accel"/>
+ <key id="key_getAllNewMessages"
+ key="&getAllNewMsgCmd2.key;"
+ oncommand="goDoCommand('cmd_getMsgsForAuthAccounts');"
+ modifiers="accel, shift"/>
+ <keyset id="findKeys"/>
+ <key id="key_stop" keycode="VK_ESCAPE" command="cmd_stop"/>
+ <keyset id="viewZoomKeys"/>
+#ifndef XP_MACOSX
+ <key id="key_reload" keycode="VK_F5" oncommand="ReloadMessage();"/>
+#endif
+
+ <!-- View Toggle Keys -->
+#ifndef XP_MACOSX
+ <key id="key_toggleFolderPane"
+ keycode="VK_F9"
+ oncommand="MsgToggleFolderPane(true);"
+ observes="mailDisableKeys"/>
+#else
+ <key id="key_toggleFolderPane"
+ key="&toggleFolderPaneCmd.key;"
+ modifiers="accel,alt"
+ oncommand="MsgToggleFolderPane(true);"
+ observes="mailDisableKeys"/>
+#endif
+ <key id="key_toggleThreadPane"
+ keycode="VK_F8"
+ modifiers="shift"
+ oncommand="MsgToggleThreadPane();"
+ disabled="true"/>
+ <key id="key_toggleMessagePane"
+ keycode="VK_F8"
+ oncommand="MsgToggleMessagePane(true);"
+ disabled="true"/>
+
+ <key id="key_searchMail" key="&searchMailCmd.key;" oncommand="goDoCommand('cmd_search')" modifiers="accel, shift"/>
+
+ <key key="&focusSearchInput.key;"
+ modifiers="accel"
+ oncommand="focusElement(document.getElementById('searchInput'));"/>
+
+ <!-- Needed to support the Lightning Task filter See Bug 316916 -->
+ <key id="key_qfb_show"
+ key="&quickFilterBar.show.key2;"
+ modifiers="accel,shift"
+ command="cmd_showQuickFilterBar"/>
+</keyset>
+
+ <menupopup id="folderPaneContext"
+ onpopupshowing="return FillFolderPaneContextMenu();"
+ onpopuphiding="if (event.target == this) FolderPaneOnPopupHiding();">
+ <menuitem id="folderPaneContext-getMessages"
+ label="&folderContextGetMessages.label;"
+ accesskey="&folderContextGetMessages.accesskey;"
+ oncommand="MsgGetMessage();"/>
+ <menuitem id="folderPaneContext-openNewTab"
+ label="&folderContextOpenNewTab.label;"
+ accesskey="&folderContextOpenNewTab.accesskey;"
+ oncommand="FolderPaneContextMenuNewTab(event);"/>
+ <menuitem id="folderPaneContext-openNewWindow"
+ label="&folderContextOpenNewWindow.label;"
+ accesskey="&folderContextOpenNewWindow.accesskey;"
+ oncommand="MsgOpenNewWindowForFolder(null,-1);"/>
+ <menuitem id="folderPaneContext-searchMessages"
+ label="&folderContextSearchMessages.label;"
+ accesskey="&folderContextSearchMessages.accesskey;"
+ oncommand="gFolderTreeController.searchMessages();"/>
+ <menuitem id="folderPaneContext-subscribe"
+ label="&folderContextSubscribe.label;"
+ accesskey="&folderContextSubscribe.accesskey;"
+ oncommand="MsgSubscribe();"/>
+ <menuitem id="folderPaneContext-newsUnsubscribe"
+ label="&folderContextUnsubscribe.label;"
+ accesskey="&folderContextUnsubscribe.accesskey;"
+ oncommand="MsgUnsubscribe();"/>
+
+ <menuseparator id="folderPaneContext-sep1"/>
+
+ <menuitem id="folderPaneContext-new"
+ label="&folderContextNew.label;"
+ accesskey="&folderContextNew.accesskey;"
+ oncommand="gFolderTreeController.newFolder();"/>
+ <menuitem id="folderPaneContext-remove"
+ label="&folderContextRemove.label;"
+ accesskey="&folderContextRemove.accesskey;"
+ oncommand="gFolderTreeController.deleteFolder();"/>
+ <menuitem id="folderPaneContext-rename"
+ label="&folderContextRename.label;"
+ accesskey="&folderContextRename.accesskey;"
+ oncommand="gFolderTreeController.renameFolder();"/>
+
+ <menuitem id="folderPaneContext-compact"
+ label="&folderContextCompact.label;"
+ accesskey="&folderContextCompact.accesskey;"
+ oncommand="gFolderTreeController.compactFolders();"/>
+ <menuitem id="folderPaneContext-markMailFolderAllRead"
+ label="&folderContextMarkMailFolderRead.label;"
+ accesskey="&folderContextMarkMailFolderRead.accesskey;"
+ oncommand="MsgMarkAllRead();"/>
+ <menuitem id="folderPaneContext-markNewsgroupAllRead"
+ label="&folderContextMarkNewsgroupRead.label;"
+ accesskey="&folderContextMarkNewsgroupRead.accesskey;"
+ oncommand="MsgMarkAllRead();"/>
+ <menuitem id="folderPaneContext-emptyTrash"
+ label="&folderContextEmptyTrash.label;"
+ accesskey="&folderContextEmptyTrash.accesskey;"
+ oncommand="gFolderTreeController.emptyTrash();"/>
+ <menuitem id="folderPaneContext-emptyJunk"
+ label="&folderContextEmptyJunk.label;"
+ accesskey="&folderContextEmptyJunk.accesskey;"
+ oncommand="gFolderTreeController.emptyJunk();"/>
+ <menuitem id="folderPaneContext-sendUnsentMessages"
+ label="&folderContextSendUnsentMessages.label;"
+ accesskey="&folderContextSendUnsentMessages.accesskey;"
+ oncommand="goDoCommand('cmd_sendUnsentMsgs')"/>
+
+ <menuseparator id="folderPaneContext-sep-edit"/>
+
+ <menuitem id="folderPaneContext-favoriteFolder"
+ type="checkbox"
+ label="&folderContextFavoriteFolder.label;"
+ accesskey="&folderContextFavoriteFolder.accesskey;"
+ checked="false"
+ oncommand="ToggleFavoriteFolderFlag();"/>
+ <menuitem id="folderPaneContext-properties"
+ label="&folderContextProperties.label;"
+ accesskey="&folderContextProperties.accesskey;"
+ oncommand="gFolderTreeController.editFolder();"/>
+ <menuitem id="folderPaneContext-markAllFoldersRead"
+ label="&folderContextMarkAllFoldersRead.label;"
+ accesskey="&folderContextMarkAllFoldersRead.accesskey;"
+ oncommand="MsgMarkAllFoldersRead();"/>
+ <menuseparator id="folderPaneContext-sep4"/>
+ <menuitem id="folderPaneContext-settings"
+ label="&folderContextSettings.label;"
+ accesskey="&folderContextSettings.accesskey;"
+ oncommand="gFolderTreeController.editFolder();"/>
+ </menupopup>
+
+ <menupopup id="mailContext"
+ onpopupshowing="return FillMailContextMenu(this, event);"
+ onpopuphiding="MailContextOnPopupHiding(this, event);">
+ <menuitem id="context-openlinkintab"
+ label="&openLinkCmdInTab.label;"
+ accesskey="&openLinkCmdInTab.accesskey;"
+ usercontextid="0"
+ oncommand="gContextMenu.openLinkInTab(event);"/>
+ <menuitem id="context-openlink"
+ label="&openLinkCmd.label;"
+ accesskey="&openLinkCmd.accesskey;"
+ oncommand="gContextMenu.openLinkInWindow();"/>
+ <menuitem id="context-openlinkinprivatewindow"
+ label="&openLinkCmdInPrivateWindow.label;"
+ accesskey="&openLinkCmdInPrivateWindow.accesskey;"
+ oncommand="gContextMenu.openLinkInPrivateWindow();"/>
+ <menuseparator id="mailContext-sep-link"/>
+ <menuitem id="context-selectall"/>
+ <menuitem id="context-copy"/>
+ <menuitem id="context-searchselect"
+ oncommand="MsgOpenSearch(gContextMenu.searchSelected(), event);"/>
+ <menuitem id="mailContext-openNewTab"
+ label="&contextOpenNewTab.label;"
+ accesskey="&contextOpenNewTab.accesskey;"
+ oncommand="OpenMessageInNewTab(event);"/>
+ <menuitem id="mailContext-openNewWindow"
+ label="&contextOpenNewWindow.label;"
+ accesskey="&contextOpenNewWindow.accesskey;"
+ oncommand="MsgOpenNewWindowForMessage();"/>
+ <menuseparator id="mailContext-sep-open"/>
+ <menuitem id="mailContext-replySender"
+ label="&contextReplySender.label;"
+ accesskey="&contextReplySender.accesskey;"
+ oncommand="MsgReplySender(event);"/>
+ <menuitem id="mailContext-replyList"
+ label="&contextReplyList.label;"
+ accesskey="&contextReplyList.accesskey;"
+ oncommand="MsgReplyList(event);"/>
+ <menuitem id="mailContext-replyNewsgroup"
+ label="&contextReplyNewsgroup.label;"
+ accesskey="&contextReplyNewsgroup.accesskey;"
+ oncommand="MsgReplyGroup(event);"/>
+ <menuitem id="mailContext-replySenderAndNewsgroup"
+ label="&contextReplySenderAndNewsgroup.label;"
+ accesskey="&contextReplySenderAndNewsgroup.accesskey;"
+ oncommand="MsgReplyToSenderAndGroup(event);"/>
+ <menuitem id="mailContext-replyAll"
+ label="&contextReplyAll.label;"
+ accesskey="&contextReplyAll.accesskey;"
+ oncommand="MsgReplyToAllRecipients(event);"/>
+ <menuitem id="mailContext-forward"
+ label="&contextForward.label;"
+ accesskey="&contextForward.accesskey;"
+ oncommand="MsgForwardMessage(event);"/>
+ <menuitem id="mailContext-forwardAsAttachment"
+ label="&contextForwardAsAttachment.label;"
+ accesskey="&contextForwardAsAttachment.accesskey;"
+ oncommand="MsgForwardAsAttachment(event);"/>
+ <menuitem id="mailContext-editAsNew"
+ label="&contextEditMsgAsNew.label;"
+ accesskey="&contextEditMsgAsNew.accesskey;"
+ oncommand="MsgEditMessageAsNew(event);"/>
+ <menuitem id="mailContext-editDraftMsg"
+ label="&contextEditDraftMsg.label;"
+ default="true"
+ oncommand="MsgEditDraftMessage(event);"/>
+ <menuitem id="mailContext-newMsgFromTemplate"
+ label="&contextNewMsgFromTemplate.label;"
+ default="true"
+ oncommand="MsgNewMessageFromTemplate(event);"/>
+ <menuitem id="mailContext-editTemplateMsg"
+ label="&contextEditTemplate.label;"
+ accesskey="&contextEditTemplate.accesskey;"
+ oncommand="MsgEditTemplateMessage(event);"/>
+ <menuseparator id="mailContext-sep-tags"/>
+ <menu id="mailContext-tags"
+ label="&tagMenu.label;"
+ accesskey="&tagMenu.accesskey;">
+ <menupopup id="mailContext-tagpopup"
+ onpopupshowing="InitMessageTags(this)">
+ <menuitem id="mailContext-tagRemoveAll"
+ oncommand="RemoveAllMessageTags();"/>
+ <menuseparator id="mailContext-sep-afterTagRemoveAll"/>
+ <menuseparator id="mailContext-sep-beforeAddNewTag"/>
+ <menuitem id="mailContext-tagCustomize"
+ label="&tagCustomize.label;"
+ accesskey="&tagCustomize.accesskey;"
+ oncommand="goPreferences('tags_pane');"/>
+ </menupopup>
+ </menu>
+ <menu id="mailContext-mark"
+ label="&markMenu.label;"
+ accesskey="&markMenu.accesskey;">
+ <menupopup id="mailContext-markPopup"
+ onpopupshowing="InitMessageMark()">
+ <menuitem id="mailContext-markRead"
+ label="&markAsReadCmd.label;"
+ accesskey="&markAsReadCmd.accesskey;"
+ command="cmd_markAsRead"/>
+ <menuitem id="mailContext-markUnread"
+ label="&markAsUnreadCmd.label;"
+ accesskey="&markAsUnreadCmd.accesskey;"
+ command="cmd_markAsUnread"/>
+ <menuitem id="mailContext-markThreadAsRead"
+ label="&markThreadAsReadCmd.label;"
+ accesskey="&markThreadAsReadCmd.accesskey;"
+ command="cmd_markThreadAsRead"/>
+ <menuitem id="mailContext-markReadByDate"
+ label="&markReadByDateCmd.label;"
+ accesskey="&markReadByDateCmd.accesskey;"
+ command="cmd_markReadByDate"/>
+ <menuitem id="mailContext-markAllRead"
+ label="&markAllReadCmd.label;"
+ accesskey="&markAllReadCmd.accesskey;"
+ command="cmd_markAllRead"/>
+ <menuseparator id="mailContext-sep-afterMarkAllRead"/>
+ <menuitem id="mailContext-markFlagged"
+ type="checkbox"
+ label="&markFlaggedCmd.label;"
+ accesskey="&markFlaggedCmd.accesskey;"
+ command="cmd_markAsFlagged"/>
+ <menuseparator id="mailContext-sep-afterMarkFlagged"/>
+ <menuitem id="mailContext-markAsJunk"
+ label="&markAsJunkCmd.label;"
+ accesskey="&markAsJunkCmd.accesskey;"
+ command="cmd_markAsJunk"/>
+ <menuitem id="mailContext-markAsNotJunk"
+ label="&markAsNotJunkCmd.label;"
+ accesskey="&markAsNotJunkCmd.accesskey;"
+ command="cmd_markAsNotJunk"/>
+ <menuitem id="mailContext-recalculateJunkScore"
+ label="&recalculateJunkScoreCmd.label;"
+ accesskey="&recalculateJunkScoreCmd.accesskey;"
+ command="cmd_recalculateJunkScore"/>
+ <menuitem id="mailContext-markAsShowRemote"
+ label="&markAsShowRemoteCmd.label;"
+ accesskey="&markAsShowRemoteCmd.accesskey;"
+ command="cmd_markAsShowRemote"/>
+ <menuitem id="mailContext-markAsNotPhish"
+ label="&markAsNotPhishCmd.label;"
+ accesskey="&markAsNotPhishCmd.accesskey;"
+ command="cmd_markAsNotPhish"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="mailContext-sep-mark"/>
+ <menuitem id="mailContext-downloadflagged"
+ label="&downloadFlaggedCmd.label;"
+ accesskey="&downloadFlaggedCmd.accesskey;"
+ command="cmd_downloadFlagged"/>
+ <menuitem id="mailContext-downloadselected"
+ label="&downloadSelectedCmd.label;"
+ accesskey="&downloadSelectedCmd.accesskey;"
+ command="cmd_downloadSelected"/>
+ <menuseparator id="mailContext-sep-move"/>
+ <menuitem id="mailContext-copyMessageUrl"
+ label="&copyMessageLocation.label;"
+ accesskey="&copyMessageLocation.accesskey;"
+ oncommand="CopyMessageUrl()"/>
+ <menuitem id="mailContext-archive"
+ label="&contextArchive.label;"
+ accesskey="&contextArchive.accesskey;"
+ oncommand="MsgArchiveSelectedMessages(event);"/>
+ <menu id="mailContext-moveMenu"
+ label="&contextMoveMsgMenu.label;"
+ accesskey="&contextMoveMsgMenu.accesskey;"
+ oncommand="MsgMoveMessage(event.target._folder);">
+ <menupopup id="mailContext-fileHereMenu"
+ type="folder"
+ mode="filing"
+ showFileHereLabel="true"
+ showRecent="true"
+ recentLabel="&contextMoveCopyMsgRecentMenu.label;"
+ recentAccessKey="&contextMoveCopyMsgRecentMenu.accesskey;"
+ showFavorites="true"
+ favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+ favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
+ </menu>
+ <menu id="mailContext-copyMenu"
+ label="&contextCopyMsgMenu.label;"
+ accesskey="&contextCopyMsgMenu.accesskey;"
+ oncommand="MsgCopyMessage(event.target._folder);">
+ <menupopup id="mailContext-copyHereMenu"
+ type="folder"
+ mode="filing"
+ showFileHereLabel="true"
+ showRecent="true"
+ recentLabel="&contextMoveCopyMsgRecentMenu.label;"
+ recentAccessKey="&contextMoveCopyMsgRecentMenu.accesskey;"
+ showFavorites="true"
+ favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+ favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
+ </menu>
+ <menuitem id="mailContext-saveAs"
+ label="&contextSaveAs.label;"
+ accesskey="&contextSaveAs.accesskey;"
+ oncommand="MsgSaveAsFile();"/>
+ <menuitem id="mailContext-delete"
+ command="cmd_delete"/>
+ <menuseparator id="mailContext-sep-print"/>
+#ifndef XP_MACOSX
+ <menuitem id="mailContext-printpreview"
+ label="&contextPrintPreview.label;"
+ accesskey="&contextPrintPreview.accesskey;"
+ oncommand="PrintEnginePrintPreview();"/>
+#endif
+ <menuitem id="mailContext-print"
+ label="&contextPrint.label;"
+ accesskey="&contextPrint.accesskey;"
+ oncommand="PrintEnginePrint();"/>
+ <menuseparator id="mailContext-sep-edit"/>
+ <menuitem id="context-copylink"
+ label="&copyLinkCmd.label;"
+ accesskey="&copyLinkCmd.accesskey;"
+ command="cmd_copyLink"/>
+ <menuitem id="context-copyimage"
+ label="&copyImageCmd.label;"
+ accesskey="&copyImageCmd.accesskey;"
+ command="cmd_copyImage"/>
+ <menuitem id="context-viewimage"
+ label="&viewImageCmd.label;"
+ accesskey="&viewImageCmd.accesskey;"
+ oncommand="gContextMenu.viewMedia();"/>
+ <menuitem id="context-addemail"
+ label="&AddToAddressBook.label;"
+ accesskey="&AddToAddressBook.accesskey;"
+ oncommand="AddEmailToAddressBook(gContextMenu.getEmail(), gContextMenu.linkText());"/>
+ <menuseparator id="mailContext-sep-image"/>
+ <menuitem id="context-blockimage"
+ oncommand="gContextMenu.toggleImageBlocking(true);"/>
+ <menuitem id="context-unblockimage"
+ oncommand="gContextMenu.toggleImageBlocking(false);"/>
+ <menuseparator id="mailContext-sep-blockimage"/>
+ <menuitem id="context-composeemailto"
+ label="&SendMailTo.label;"
+ accesskey="&SendMailTo.accesskey;"
+ oncommand="SendMailTo(gContextMenu.getEmail(), event);"/>
+ <menuitem id="context-createfilterfrom"
+ label="&CreateFilterFrom.label;"
+ accesskey="&CreateFilterFrom.accesskey;"
+ oncommand="CreateFilterFromMail(gContextMenu.getEmail());"/>
+ <menuitem id="context-copyemail"
+ label="&copyEmailCmd.label;"
+ accesskey="&copyEmailCmd.accesskey;"
+ oncommand="gContextMenu.copyEmail();"/>
+ <menuseparator id="mailContext-sep-copy"/>
+ <menuitem id="context-savelink"
+ label="&saveLinkCmd.label;"
+ accesskey="&saveLinkCmd.accesskey;"
+ oncommand="gContextMenu.saveLink();"/>
+ <menuitem id="context-saveimage"
+ label="&saveImageCmd.label;"
+ accesskey="&saveImageCmd.accesskey;"
+ oncommand="gContextMenu.saveImage();"/>
+ <menuitem id="context-bookmarklink"
+ label="&bookmarkLinkCmd.label;"
+ accesskey="&bookmarkLinkCmd.accesskey;"
+ oncommand="PlacesUIUtils.showMinimalAddBookmarkUI(makeURI(gContextMenu.linkURL),
+ gContextMenu.linkText());"/>
+ </menupopup>
+
+ <menupopup id="remoteContentOptions"
+ onpopupshowing="onRemoteContentOptionsShowing(event);"
+ oncommand="allowRemoteContentForURI(event.target);">
+ <menuitem id="remoteContentOptionAllowForMsg"
+ label="&remoteContentOptionsAllowForMsg.label;"
+ accesskey="&remoteContentOptionsAllowForMsg.accesskey;"
+ oncommand="LoadMsgWithRemoteContent();"/>
+ <menuseparator id="remoteContentSettingsMenuSeparator"/>
+ <menuitem id="editRemoteContentSettings"
+ label="&editRemoteContentSettings.label;"
+ accesskey="&editRemoteContentSettings.accesskey;"
+ oncommand="editRemoteContentSettings();"/>
+ </menupopup>
+
+ <toolbar type="menubar"
+ id="mail-toolbar-menubar2"
+ class="chromeclass-menubar"
+ persist="collapsed"
+ grippytooltiptext="&menuBar.tooltip;"
+ customizable="true"
+ defaultset="menubar-items"
+ mode="icons"
+ iconsize="small"
+ defaultmode="icons"
+ defaulticonsize="small"
+ context="toolbar-context-menu">
+ <toolbaritem id="menubar-items"
+ class="menubar-items"
+ align="center">
+ </toolbaritem>
+ </toolbar>
+
+<menubar id="mail-menubar">
+ <menu id="menu_File" >
+ <menupopup id="menu_FilePopup" onpopupshowing="file_init();">
+ <menu id="menu_New">
+ <menupopup id="menu_NewPopup" onpopupshowing="menu_new_init();">
+ <menuitem id="newNewMsgCmd"
+ label="&newNewMsgCmd.label;"
+ accesskey="&newNewMsgCmd.accesskey;"
+ key="key_newMessage"
+ oncommand="MsgNewMessage(null);"/>
+ <menuitem id="menu_newFolder"
+ label="&newFolderCmd.label;"
+ accesskey="&newFolderCmd.accesskey;"
+ oncommand="gFolderTreeController.newFolder();"/>
+ <menuitem id="menu_newVirtualFolder" label="&newVirtualFolderCmd.label;"
+ oncommand="gFolderTreeController.newVirtualFolder();"
+ accesskey="&newVirtualFolderCmd.accesskey;"/>
+ <menuitem id="newAccountMenuItem"
+ label="&newAccountCmd.label;"
+ accesskey="&newAccountCmd.accesskey;"
+ oncommand="MsgAccountWizard();"/>
+ <menuseparator id="newPopupMenuSeparator"/>
+ <menuitem id="menu_newCard"/>
+ <menuitem id="menu_newTab"
+ label="&newTabCmd.label;"
+ accesskey="&newTabCmd.accesskey;"
+ key="key_newTab"
+ oncommand="MsgOpenNewTab();"/>
+ <menuitem id="menu_newNavigator"/>
+ <menuitem id="menu_newPrivateWindow"/>
+ <menuitem id="menu_newEditor"/>
+ </menupopup>
+ </menu>
+ <menuitem id="openMessageFileMenuitem" label="&openMessageFileCmd.label;"
+ key="key_openFileMessage"
+ accesskey="&openMessageFileCmd.accesskey;"
+ oncommand="MsgOpenFromFile();"/>
+ <menuitem id="menu_close"/>
+ <menuseparator id="fileMenuAfterCloseSeparator"/>
+ <menu id="menu_saveAs" label="&saveAsMenu.label;" accesskey="&saveAsMenu.accesskey;">
+ <menupopup id="menu_SavePopup">
+ <menuitem id="menu_saveAsFile"
+ label="&saveAsFileCmd.label;"
+ accesskey="&saveAsFileCmd.accesskey;"
+ key="key_saveAsFile"
+ command="cmd_saveAsFile"/>
+ <menuitem id="menu_saveAsTemplate"
+ label="&saveAsTemplateCmd.label;"
+ accesskey="&saveAsTemplateCmd.accesskey;"
+ command="cmd_saveAsTemplate"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="fileMenuAfterSaveSeparator"/>
+ <menuitem id="menu_getNewMsg"
+ label="&getNewMsgCmd.label;"
+ accesskey="&getNewMsgCmd.accesskey;"
+ key="key_getNewMessages"
+ command="cmd_getNewMessages"/>
+ <menu id="menu_getAllNewMsg"
+ label="&getNewMsgForCmd.label;"
+ accesskey="&getNewMsgForCmd.accesskey;"
+ oncommand="MsgGetMessagesForAccount();">
+ <menupopup id="menu_getAllNewMsgPopup"
+ type="folder"
+ mode="getMail"
+ expandFolders="false"
+ oncommand="MsgGetMessagesForAccount(event.target._folder); event.stopPropagation();">
+ <menuitem id="menu_getAllNewMsgPopupMenu"
+ label="&getAllNewMsgCmdPopupMenu.label;"
+ accesskey="&getAllNewMsgCmdPopupMenu.accesskey;"
+ key="key_getAllNewMessages"
+ command="cmd_getMsgsForAuthAccounts"/>
+ <menuseparator id="fileMenuAfterGetNewMsgSeparator"/>
+ </menupopup>
+ </menu>
+ <menuitem id="menu_getnextnmsg" label="&getNextNMsgCmd.label;"
+ accesskey="&getNextNMsgCmd.accesskey;"
+ command="cmd_getNextNMessages"/>
+ <menuitem id="menu_sendunsentmsgs" label="&sendUnsentCmd.label;"
+ accesskey="&sendUnsentCmd.accesskey;"
+ command="cmd_sendUnsentMsgs"/>
+ <menuitem label="&subscribeCmd.label;"
+ accesskey="&subscribeCmd.accesskey;"
+ command="cmd_subscribe"/>
+ <menuseparator id="fileMenuAfterSubscribeSeparator"/>
+ <menuitem id="menu_renameFolder" label="&renameFolder.label;"
+ accesskey="&renameFolder.accesskey;"
+ command="cmd_renameFolder"
+ observes="mailHideMenus"/>
+ <menuitem id="menu_compactFolder" label="&compactFolders.label;"
+ accesskey="&compactFolders.accesskey;"
+ command="cmd_compactFolder"
+ observes="mailHideMenus"/>
+ <menuitem id="menu_emptyTrash" label="&emptyTrashCmd.label;"
+ accesskey="&emptyTrashCmd.accesskey;"
+ command="cmd_emptyTrash"
+ observes="mailHideMenus"/>
+ <menuseparator id="trashMenuSeparator" observes="mailHideMenus"/>
+ <menu id="menu_Offline"
+ label="&offlineMenu.label;"
+ accesskey="&offlineMenu.accesskey;">
+ <menupopup id="menu_OfflinePopup">
+ <menuitem id="offlineGoOfflineCmd"/>
+ <menuseparator id="offlineMenuAfterGoSeparator"/>
+ <menuitem id="menu_synchronizeOffline"
+ label="&synchronizeOfflineCmd.label;"
+ accesskey="&synchronizeOfflineCmd.accesskey;"
+ command="cmd_synchronizeOffline"/>
+ <menuitem id="menu_settingsOffline"
+ label="&settingsOfflineCmd.label;"
+ accesskey="&settingsOfflineCmd.accesskey;"
+ command="cmd_settingsOffline"/>
+ <menuseparator id="offlineMenuAfterSettingsSeparator"/>
+ <menuitem id="menu_downloadFlagged"
+ label="&downloadFlaggedCmd.label;"
+ accesskey="&downloadFlaggedCmd.accesskey;"
+ command="cmd_downloadFlagged"/>
+ <menuitem id="menu_downloadSelected"
+ label="&downloadSelectedCmd.label;"
+ accesskey="&downloadSelectedCmd.accesskey;"
+ command="cmd_downloadSelected"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="fileMenuAfterOfflineSeparator"/>
+ <menuitem id="menu_printSetup"/>
+ <menuitem id="menu_printPreview"/>
+ <menuitem id="menu_print"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_Edit" oncommand="CommandUpdate_UndoRedo();">
+ <menupopup id="menu_EditPopup" onpopupshowing="InitEditMessagesMenu()">
+ <menuitem id="menu_undo"/>
+ <menuitem id="menu_redo"/>
+ <menuseparator id="editMenuAfterRedoSeparator"/>
+ <menuitem id="menu_cut"/>
+ <menuitem id="menu_copy"/>
+ <menuitem id="menu_paste"/>
+ <menuitem id="menu_delete" command="cmd_delete"/>
+ <menuseparator id="editMenuAfterDeleteSeparator"/>
+ <menu id="menu_select" label="&selectMenu.label;"
+ accesskey="&selectMenu.accesskey;">
+ <menupopup id="menu_SelectPopup">
+ <menuitem id="menu_mailSelectAll"
+ label="&all.label;"
+ accesskey="&all.accesskey;" key="key_selectAll"
+ command="cmd_selectAll"/>
+ <menuseparator id="selectMenuSeparator"/>
+ <menuitem id="menu_selectThread"
+ label="&selectThreadCmd.label;"
+ accesskey="&selectThreadCmd.accesskey;" key="key_selectThread"
+ command="cmd_selectThread"/>
+ <menuitem id="menu_selectFlagged"
+ label="&selectFlaggedCmd.label;"
+ accesskey="&selectFlaggedCmd.accesskey;"
+ command="cmd_selectFlagged"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="editMenuAfterSelectSeparator"/>
+ <menuitem id="menu_find" label="&findCmd.label;"/>
+ <menuitem id="menu_findNext"/>
+ <menuitem id="menu_findPrev"/>
+ <menuseparator id="editMenuAfterFindSeparator"/>
+ <menuitem id="menu_findTypeLinks"/>
+ <menuitem id="menu_findTypeText"/>
+ <menuseparator id="editPropertiesSeparator"/>
+ <menuitem id="menu_favoriteFolder"
+ type="checkbox"
+ label="&menuFavoriteFolder.label;"
+ accesskey="&menuFavoriteFolder.accesskey;"
+ checked="false"
+ oncommand="ToggleFavoriteFolderFlag();"
+ observes="mailHideMenus"/>
+ <menuitem id="menu_properties" label="&folderPropsCmd.label;"
+ accesskey="&folderPropsCmd.accesskey;"
+ command="cmd_properties"
+ observes="mailHideMenus"/>
+ <menuitem id="menu_accountmgr"
+ label="&accountManagerCmd.label;"
+ accesskey="&accountManagerCmd.accesskey;"
+ oncommand="MsgAccountManager(null);"/>
+ <menuitem id="menu_preferences" oncommand="goPreferences('mailnews_pane')"/>
+ </menupopup>
+ </menu>
+
+ <menu id="menu_View">
+ <menupopup id="menu_View_Popup" onpopupshowing="view_init()">
+ <menu id="menu_Toolbars">
+ <menupopup id="view_toolbars_popup"
+ onpopupshowing="onViewToolbarsPopupShowing(event)"
+ oncommand="onViewToolbarCommand(event);">
+ <menuitem id="menu_showTaskbar"/>
+ </menupopup>
+ </menu>
+ <menu id="menu_MessagePaneLayout" label="&messagePaneLayoutStyle.label;"
+ accesskey="&messagePaneLayoutStyle.accesskey;" observes="mailHideMenus">
+ <menupopup id="view_layout_popup" onpopupshowing="InitViewLayoutStyleMenu(event)">
+ <menuitem id="messagePaneClassic" type="radio" label="&messagePaneClassic.label;" name="viewlayoutgroup"
+ accesskey="&messagePaneClassic.accesskey;" oncommand="ChangeMailLayout(kClassicMailLayout);"/>
+ <menuitem id="messagePaneWide" type="radio" label="&messagePaneWide.label;" name="viewlayoutgroup"
+ accesskey="&messagePaneWide.accesskey;" oncommand="ChangeMailLayout(kWideMailLayout);"/>
+ <menuitem id="messagePaneVertical" type="radio" label="&messagePaneVertical.label;" name="viewlayoutgroup"
+ accesskey="&messagePaneVertical.accesskey;" oncommand="ChangeMailLayout(kVerticalMailLayout);"/>
+ <menuseparator id="viewMenuAfterPaneVerticalSeparator"/>
+ <menuitem id="menu_showMessagePane"
+ type="checkbox"
+ label="&showMessagePaneCmd.label;"
+ accesskey="&showMessagePaneCmd.accesskey;"
+ key="key_toggleMessagePane"
+ oncommand="MsgToggleMessagePane(true);"
+ observes="mailHideMenus"/>
+ <menuitem id="menu_showThreadPane"
+ type="checkbox"
+ label="&showThreadPaneCmd.label;"
+ accesskey="&showThreadPaneCmd.accesskey;"
+ key="key_toggleThreadPane"
+ oncommand="MsgToggleThreadPane();"
+ observes="mailHideMenus"/>
+ <menuitem id="menu_showFolderPane"
+ type="checkbox"
+ label="&showFolderPaneCmd.label;"
+ accesskey="&showFolderPaneCmd.accesskey;"
+ key="key_toggleFolderPane"
+ oncommand="MsgToggleFolderPane(true);"
+ observes="mailHideMenus"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="viewMessagesMenuSeparator" observes="mailHideMenus"/>
+ <menu id="viewSortMenu" label="&sortMenu.label;"
+ accesskey="&sortMenu.accesskey;" observes="mailHideMenus">
+ <menupopup id="menu_viewSortPopup" onpopupshowing="InitViewSortByMenu()">
+ <menuitem id="sortByDateMenuitem" type="radio" name="sortby" label="&sortByDateCmd.label;" accesskey="&sortByDateCmd.accesskey;" oncommand="MsgSortThreadPane('byDate')"/>
+ <menuitem id="sortByReceivedMenuitem" type="radio" name="sortby" label="&sortByReceivedCmd.label;" accesskey="&sortByReceivedCmd.accesskey;" oncommand="MsgSortThreadPane('byReceived')"/>
+ <menuitem id="sortByFlagMenuitem" type="radio" name="sortby" label="&sortByFlagCmd.label;" accesskey="&sortByFlagCmd.accesskey;" oncommand="MsgSortThreadPane('byFlagged')"/>
+ <menuitem id="sortByOrderReceivedMenuitem" type="radio" name="sortby" label="&sortByOrderReceivedCmd.label;" accesskey="&sortByOrderReceivedCmd.accesskey;" oncommand="MsgSortThreadPane('byId')"/>
+ <menuitem id="sortByPriorityMenuitem" type="radio" name="sortby" label="&sortByPriorityCmd.label;" accesskey="&sortByPriorityCmd.accesskey;" oncommand="MsgSortThreadPane('byPriority')"/>
+ <menuitem id="sortByFromMenuitem" type="radio" name="sortby" label="&sortByFromCmd.label;" accesskey="&sortByFromCmd.accesskey;" oncommand="MsgSortThreadPane('byAuthor')"/>
+ <menuitem id="sortByRecipientMenuitem" type="radio" name="sortby" label="&sortByRecipientCmd.label;" accesskey="&sortByRecipientCmd.accesskey;" oncommand="MsgSortThreadPane('byRecipient')"/>
+ <menuitem id="sortBySizeMenuitem" type="radio" name="sortby" label="&sortBySizeCmd.label;" accesskey="&sortBySizeCmd.accesskey;" oncommand="MsgSortThreadPane('bySize')"/>
+ <menuitem id="sortByStatusMenuitem" type="radio" name="sortby" label="&sortByStatusCmd.label;" accesskey="&sortByStatusCmd.accesskey;" oncommand="MsgSortThreadPane('byStatus')"/>
+ <menuitem id="sortBySubjectMenuitem" type="radio" name="sortby" label="&sortBySubjectCmd.label;" accesskey="&sortBySubjectCmd.accesskey;" oncommand="MsgSortThreadPane('bySubject')"/>
+ <menuitem id="sortByUnreadMenuitem" type="radio" name="sortby" label="&sortByUnreadCmd.label;" accesskey="&sortByUnreadCmd.accesskey;" oncommand="MsgSortThreadPane('byUnread')"/>
+ <menuitem id="sortByTagsMenuitem" type="radio" name="sortby" label="&sortByTagsCmd.label;" accesskey="&sortByTagsCmd.accesskey;" oncommand="MsgSortThreadPane('byTags')"/>
+ <menuitem id="sortByJunkStatusMenuitem" type="radio" name="sortby" label="&sortByJunkStatusCmd.label;" accesskey="&sortByJunkStatusCmd.accesskey;" oncommand="MsgSortThreadPane('byJunkStatus')"/>
+ <menuitem id="sortByAttachmentsMenuitem" type="radio" name="sortby" label="&sortByAttachmentsCmd.label;" accesskey="&sortByAttachmentsCmd.accesskey;" oncommand="MsgSortThreadPane('byAttachments')"/>
+ <menuseparator id="sortAfterAttachmentSeparator"/>
+ <menuitem id="sortAscending" type="radio" name="sortdirection" label="&sortAscending.label;" accesskey="&sortAscending.accesskey;" oncommand="MsgSortAscending()"/>
+ <menuitem id="sortDescending" type="radio" name="sortdirection" label="&sortDescending.label;" accesskey="&sortDescending.accesskey;" oncommand="MsgSortDescending()"/>
+ <menuseparator id="sortAfterDescendingSeparator"/>
+ <menuitem id="sortThreaded" type="radio" name="threaded" label="&sortThreaded.label;" accesskey="&sortThreaded.accesskey;" oncommand="MsgSortThreaded();"/>
+ <menuitem id="sortUnthreaded" type="radio" name="threaded" label="&sortUnthreaded.label;" accesskey="&sortUnthreaded.accesskey;" oncommand="MsgSortUnthreaded();"/>
+ <menuitem id="groupBySort" type="radio" name="group" label="&groupBySort.label;" accesskey="&groupBySort.accesskey;" oncommand="MsgGroupBySort();"/>
+ </menupopup>
+ </menu>
+ <menu id="viewMessageViewMenu" label="&msgsMenu.label;" accesskey="&msgsMenu.accesskey;"
+ observes="mailHideMenus" oncommand="ViewChangeByMenuitem(event.target);">
+ <menupopup id="viewMessagePopup"
+ onpopupshowing="RefreshViewPopup(this);">
+ <menuitem id="viewMessageAll"
+ label="&viewAll.label;"
+ accesskey="&viewAll.accesskey;"
+ type="radio"
+ name="viewmessages"
+ value="0"/>
+ <menuitem id="viewMessageUnread"
+ label="&viewUnread.label;"
+ accesskey="&viewUnread.accesskey;"
+ type="radio"
+ name="viewmessages"
+ value="1"/>
+ <menuitem id="viewMessageNotDeleted"
+ label="&viewNotDeleted.label;"
+ accesskey="&viewNotDeleted.accesskey;"
+ type="radio"
+ name="viewmessages"
+ value="3"/>
+ <menuseparator id="messageViewAfterUnreadSeparator"/>
+ <menu id="viewMessageTags" label="&viewTags.label;" accesskey="&viewTags.accesskey;">
+ <menupopup id="viewMessageTagsPopup"
+ onpopupshowing="RefreshTagsPopup(this);"/>
+ </menu>
+ <menu id="viewMessageCustomViews" label="&viewCustomViews.label;" accesskey="&viewCustomViews.accesskey;">
+ <menupopup id="viewMessageCustomViewsPopup"
+ onpopupshowing="RefreshCustomViewsPopup(this);"/>
+ </menu>
+ <menuseparator id="messageViewAfterCustomSeparator"/>
+ <menuitem id="viewMessageVirtualFolder" value="7" label="&viewVirtualFolder.label;" accesskey="&viewVirtualFolder.accesskey;"/>
+ <menuitem id="viewMessageCustomize" value="8" label="&viewCustomizeView.label;" accesskey="&viewCustomizeView.accesskey;"/>
+ </menupopup>
+ </menu>
+ <menu id="viewMessagesMenu" label="&threads.label;"
+ accesskey="&threads.accesskey;" observes="mailHideMenus">
+ <menupopup id="menu_ThreadsPopup" onpopupshowing="InitViewMessagesMenu()">
+ <menuitem id="viewAllMessagesMenuItem" type="radio" name="viewmessages" label="&allMsgsCmd.label;" accesskey="&allMsgsCmd.accesskey;" disabled="true" command="cmd_viewAllMsgs"/>
+ <menuitem id="viewUnreadMessagesMenuItem" type="radio" name="viewmessages" label="&unreadMsgsCmd.label;" accesskey="&unreadMsgsCmd.accesskey;" disabled="true" command="cmd_viewUnreadMsgs"/>
+ <menuitem id="viewThreadsWithUnreadMenuItem" type="radio" name="viewmessages" label="&threadsWithUnreadCmd.label;" accesskey="&threadsWithUnreadCmd.accesskey;" disabled="true" command="cmd_viewThreadsWithUnread"/>
+ <menuitem id="viewWatchedThreadsWithUnreadMenuItem" type="radio" name="viewmessages" label="&watchedThreadsWithUnreadCmd.label;" accesskey="&watchedThreadsWithUnreadCmd.accesskey;" disabled="true" command="cmd_viewWatchedThreadsWithUnread"/>
+ <menuseparator id="threadsAfterWatchedSeparator"/>
+ <menuitem id="viewIgnoredThreadsMenuItem" type="checkbox" label="&ignoredThreadsCmd.label;" disabled="true" command="cmd_viewIgnoredThreads" accesskey="&ignoredThreadsCmd.accesskey;"/>
+ <menuseparator id="threadsAfterIgnoredSeparator"/>
+ <menuitem label="&expandAllThreadsCmd.label;" accesskey="&expandAllThreadsCmd.accesskey;" key="key_expandAllThreads" disabled="true" command="cmd_expandAllThreads"/>
+ <menuitem label="&collapseAllThreadsCmd.label;" accesskey="&collapseAllThreadsCmd.accesskey;" key="key_collapseAllThreads" disabled="true" command="cmd_collapseAllThreads"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="viewAfterThreadsSeparator"/>
+ <menu id="viewheadersmenu" label="&headersMenu.label;" accesskey="&headersMenu.accesskey;">
+ <menupopup id="menu_HeadersPopup" onpopupshowing="InitViewHeadersMenu();">
+ <menuitem id="viewallheaders"
+ type="radio"
+ name="viewheadergroup"
+ label="&headersAllCmd.label;"
+ accesskey="&headersAllCmd.accesskey;"
+ command="cmd_viewAllHeader"/>
+ <menuitem id="viewnormalheaders"
+ type="radio"
+ name="viewheadergroup"
+ label="&headersNormalCmd.label;"
+ accesskey="&headersNormalCmd.accesskey;"
+ command="cmd_viewNormalHeader"/>
+ </menupopup>
+ </menu>
+ <menu id="viewBodyMenu" accesskey="&bodyMenu.accesskey;" label="&bodyMenu.label;">
+ <menupopup id="viewBodyPopMenu" onpopupshowing="InitViewBodyMenu()">
+ <menuitem id="bodyAllowHTML"
+ type="radio"
+ name="bodyPlaintextVsHTMLPref"
+ label="&bodyAllowHTML.label;"
+ accesskey="&bodyAllowHTML.accesskey;"
+ oncommand="MsgBodyAllowHTML()"/>
+ <menuitem id="bodySanitized"
+ type="radio"
+ name="bodyPlaintextVsHTMLPref"
+ label="&bodySanitized.label;"
+ accesskey="&bodySanitized.accesskey;"
+ oncommand="MsgBodySanitized()"/>
+ <menuitem id="bodyAsPlaintext"
+ type="radio"
+ name="bodyPlaintextVsHTMLPref"
+ label="&bodyAsPlaintext.label;"
+ accesskey="&bodyAsPlaintext.accesskey;"
+ oncommand="MsgBodyAsPlaintext()"/>
+ <menuitem id="bodyAllParts"
+ type="radio"
+ name="bodyPlaintextVsHTMLPref"
+ label="&bodyAllParts.label;"
+ accesskey="&bodyAllParts.accesskey;"
+ oncommand="MsgBodyAllParts();"/>
+ </menupopup>
+ </menu>
+ <menu id="viewFeedSummary"
+ label="&bodyMenuFeed.label;"
+ accesskey="&bodyMenuFeed.accesskey;">
+ <menupopup id="viewFeedSummaryPopupMenu"
+ onpopupshowing="InitViewBodyMenu()">
+ <menuitem id="bodyFeedSummaryAllowHTML"
+ type="radio"
+ name="viewFeedBodyHTMLGroup"
+ label="&bodyAllowHTML.label;"
+ accesskey="&bodyAllowHTML.accesskey;"
+ oncommand="MsgFeedBodyRenderPrefs(false, 0, 0)"/>
+ <menuitem id="bodyFeedSummarySanitized"
+ type="radio"
+ name="viewFeedBodyHTMLGroup"
+ label="&bodySanitized.label;"
+ accesskey="&bodySanitized.accesskey;"
+ oncommand="MsgFeedBodyRenderPrefs(false, 3, gDisallow_classes_no_html)"/>
+ <menuitem id="bodyFeedSummaryAsPlaintext"
+ type="radio"
+ name="viewFeedBodyHTMLGroup"
+ label="&bodyAsPlaintext.label;"
+ accesskey="&bodyAsPlaintext.accesskey;"
+ oncommand="MsgFeedBodyRenderPrefs(true, 1, gDisallow_classes_no_html)"/>
+ <menuseparator id="viewFeedSummarySeparator"/>
+ <menuitem id="bodyFeedGlobalWebPage"
+ type="radio"
+ name="viewFeedSummaryGroup"
+ label="&viewFeedWebPage.label;"
+ accesskey="&viewFeedWebPage.accesskey;"
+ oncommand="FeedMessageHandler.onSelectPref = 0"/>
+ <menuitem id="bodyFeedGlobalSummary"
+ type="radio"
+ name="viewFeedSummaryGroup"
+ label="&viewFeedSummary.label;"
+ accesskey="&viewFeedSummary.accesskey;"
+ oncommand="FeedMessageHandler.onSelectPref = 1"/>
+ <menuitem id="bodyFeedPerFolderPref"
+ type="radio"
+ name="viewFeedSummaryGroup"
+ label="&viewFeedSummaryFeedPropsPref.label;"
+ accesskey="&viewFeedSummaryFeedPropsPref.accesskey;"
+ oncommand="FeedMessageHandler.onSelectPref = 2"/>
+ </menupopup>
+ </menu>
+ <menuitem id="viewAttachmentsInlineMenuitem"
+ type="checkbox"
+ checked="true"
+ label="&viewAttachmentsInlineCmd.label;"
+ accesskey="&viewAttachmentsInlineCmd.accesskey;"
+ oncommand="ToggleInlineAttachment(event.target)"/>
+ <menuseparator id="viewAfterAttachmentsSeparator"/>
+ <menuitem id="stopMenuitem"
+ label="&stopCmd.label;"
+ accesskey="&stopCmd.accesskey;"
+ key="key_stop"
+ disabled="true"
+ command="cmd_stop"/>
+ <menuitem id="menu_Stop"
+ label="&reloadCmd.label;"
+ key="key_reload"
+ accesskey="&reloadCmd.accesskey;"
+ command="cmd_reload"/>
+ <menuseparator id="viewAfterStopSeparator"/>
+ <!-- overlayed from viewZoomOverlay.xul -->
+ <menu id="menu_zoom"/>
+ <menu id="charsetMenu"
+ onpopupshowing="UpdateCharsetMenu(msgWindow.mailCharacterSet, this);"
+ oncommand="MailSetCharacterSet(event);"/>
+ <menuseparator id="viewAfterCharsetSeparator"/>
+ <menuitem id="pageSourceMenuItem" label="&pageSourceCmd.label;" key="key_viewPageSource" accesskey="&pageSourceCmd.accesskey;" command="cmd_viewPageSource"/>
+ <menuitem id="appmenu_securityStatus"
+ label="&menu_securityStatus.label;"
+ accesskey="&menu_securityStatus.accesskey;"
+ command="cmd_viewSecurityStatus"/>
+ <menuseparator observes="mailHideMenus"/>
+ <!-- overlayed from viewApplyThemeOverlay.xul -->
+ <menu id="menu_viewApplyTheme" observes="mailHideMenus"/>
+ </menupopup>
+ </menu>
+
+ <menu id="goMenu" label="&goMenu.label;" accesskey="&goMenu.accesskey;">
+ <menupopup id="menu_GoPopup" onpopupshowing="InitGoMessagesMenu();">
+ <menu id="goNextMenu" label="&nextMenu.label;" accesskey="&nextMenu.accesskey;">
+ <menupopup id="menu_GoNextPopup">
+ <menuitem id="nextMsgMenuItem"
+ label="&nextMsgCmd.label;"
+ accesskey="&nextMsgCmd.accesskey;"
+ key="key_nextMsg"
+ command="cmd_nextMsg"/>
+ <menuitem id="nextUnreadMsgMenuItem"
+ label="&nextUnreadMsgCmd.label;"
+ accesskey="&nextUnreadMsgCmd.accesskey;"
+ key="key_nextUnreadMsg"
+ command="cmd_nextUnreadMsg"/>
+ <menuitem id="nextFlaggedMenuItem"
+ label="&nextFlaggedMsgCmd.label;"
+ accesskey="&nextFlaggedMsgCmd.accesskey;"
+ command="cmd_nextFlaggedMsg"/>
+ <menuseparator id="goNextAfterFlaggedSeparator"/>
+ <menuitem id="nextUnreadThreadMenuItem"
+ label="&nextUnreadThread.label;"
+ accesskey="&nextUnreadThread.accesskey;"
+ key="key_nextUnreadThread"
+ command="cmd_nextUnreadThread"/>
+ </menupopup>
+ </menu>
+ <menu id="goPreviousMenu" label="&prevMenu.label;" accesskey="&prevMenu.accesskey;">
+ <menupopup id="menu_GoPreviousPopup">
+ <menuitem id="prevMsgMenuItem"
+ label="&prevMsgCmd.label;"
+ accesskey="&prevMsgCmd.accesskey;"
+ key="key_previousMsg"
+ command="cmd_previousMsg"/>
+ <menuitem id="prevUnreadMsgMenuItem"
+ label="&prevUnreadMsgCmd.label;"
+ accesskey="&prevUnreadMsgCmd.accesskey;"
+ key="key_previousUnreadMsg"
+ command="cmd_previousUnreadMsg"/>
+ <menuitem id="prevFlaggedMenuItem"
+ label="&prevFlaggedMsgCmd.label;"
+ accesskey="&prevFlaggedMsgCmd.accesskey;"
+ command="cmd_previousFlaggedMsg"/>
+ </menupopup>
+ </menu>
+ <menuitem id="menu_goBack"
+ label="&goBackCmd.label;"
+ accesskey="&goBackCmd.accesskey;"
+ key="key_goBack"
+ command="cmd_goBack"/>
+ <menuitem id="menu_goForward"
+ label="&goForwardCmd.label;"
+ accesskey="&goForwardCmd.accesskey;"
+ key="key_goForward"
+ command="cmd_goForward"/>
+ <menuseparator id="goNextAfterForwardSeparator" observes="mailHideMenus"/>
+ <menu id="goFolderMenu"
+ label="&folderMenu.label;"
+ accesskey="&folderMenu.accesskey;"
+ oncommand="SelectMsgFolder(event.target._folder);"
+ observes="mailHideMenus">
+ <menupopup id="menu_GoFolderPopup"
+ type="folder"
+ showFileHereLabel="true"
+ showRecent="true"
+ recentLabel="&contextMoveCopyMsgRecentMenu.label;"
+ recentAccessKey="&contextMoveCopyMsgRecentMenu.accesskey;"
+ showFavorites="true"
+ favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+ favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
+ </menu>
+ <menuseparator id="goFolderSeparator"/>
+ <menuitem id="goStartPage" label="&startPageCmd.label;"
+ accesskey="&startPageCmd.accesskey;" command="cmd_goStartPage"
+ observes="mailHideMenus"/>
+ <menuseparator id="goNextAfterStartPageSeparator" observes="mailHideMenus"/>
+ </menupopup>
+ </menu>
+
+ <menu id="messageMenu" label="&msgMenu.label;" accesskey="&msgMenu.accesskey;">
+ <menupopup id="messageMenuPopup" onpopupshowing="InitMessageMenu();">
+ <menuitem id="newMsgCmd"
+ label="&newMsgCmd.label;"
+ accesskey="&newMsgCmd.accesskey;"
+ key="key_newMessage"
+ oncommand="MsgNewMessage(null);"/>
+ <menuitem id="replyMainMenu"
+ label="&replyMsgCmd.label;"
+ accesskey="&replyMsgCmd.accesskey;"
+ key="key_reply"
+ command="cmd_reply"/>
+ <menuitem id="replyListMainMenu"
+ label="&replyListCmd.label;"
+ accesskey="&replyListCmd.accesskey;"
+ command="cmd_replyList"/>
+ <menuitem id="replyNewsgroupMainMenu"
+ label="&replyNewsgroupCmd.label;"
+ accesskey="&replyNewsgroupCmd.accesskey;"
+ key="key_reply"
+ command="cmd_replyGroup"/>
+ <menuitem id="replySenderMainMenu"
+ label="&replySenderCmd.label;"
+ accesskey="&replySenderCmd.accesskey;"
+ command="cmd_replySender"/>
+ <menuitem id="replyallMainMenu"
+ label="&replyToAllMsgCmd.label;"
+ accesskey="&replyToAllMsgCmd.accesskey;"
+ key="key_replyall"
+ command="cmd_replyall"/>
+ <menuitem id="replySenderAndNewsgroupMainMenu"
+ label="&replyToSenderAndNewsgroupCmd.label;"
+ accesskey="&replyToSenderAndNewsgroupCmd.accesskey;"
+ key="key_replyall" command="cmd_replySenderAndGroup"/>
+ <menuitem id="replyAllRecipientsMainMenu"
+ label="&replyToAllRecipientsCmd.label;"
+ accesskey="&replyToAllRecipientsCmd.accesskey;"
+ command="cmd_replyAllRecipients"/>
+ <menuitem id="menu_forwardMsg"
+ label="&forwardMsgCmd.label;"
+ accesskey="&forwardMsgCmd.accesskey;"
+ key="key_forward"
+ command="cmd_forward"/>
+ <menu id="forwardAsMenu" label="&forwardAsMenu.label;" accesskey="&forwardAsMenu.accesskey;">
+ <menupopup id="menu_forwardAsPopup">
+ <menuitem id="menu_forwardAsInline"
+ label="&forwardAsInline.label;"
+ accesskey="&forwardAsInline.accesskey;"
+ command="cmd_forwardInline"/>
+ <menuitem id="menu_forwardAsAttachment"
+ label="&forwardAsAttachmentCmd.label;"
+ accesskey="&forwardAsAttachmentCmd.accesskey;"
+ command="cmd_forwardAttachment"/>
+ </menupopup>
+ </menu>
+ <menuitem id="menu_editMsgAsNew"
+ label="&editAsNewMsgCmd.label;"
+ accesskey="&editAsNewMsgCmd.accesskey;"
+ key="key_editAsNew"
+ command="cmd_editAsNew"/>
+ <menuitem id="menu_editDraftMsg"
+ label="&editDraftMsgCmd.label;"
+ accesskey="&editDraftMsgCmd.accesskey;"
+ command="cmd_editDraftMsg"/>
+ <menuitem id="menu_newMsgFromTemplate"
+ label="&newMsgFromTemplateCmd.label;"
+ key="key_newMsgFromTemplate"
+ command="cmd_newMsgFromTemplate"/>
+ <menuitem id="menu_editTemplate"
+ label="&editTemplateMsgCmd.label;"
+ accesskey="&editTemplateMsgCmd.accesskey;"
+ command="cmd_editTemplateMsg"/>
+ <menuitem id="openMessageWindowMenuitem"
+ label="&openMessageWindowCmd.label;"
+ command="cmd_openMessage"
+ accesskey="&openMessageWindowCmd.accesskey;"
+ key="key_openMessage" observes="mailHideMenus"/>
+ <menu id="openFeedMessage"
+ label="&openFeedMessage.label;"
+ accesskey="&openFeedMessage.accesskey;">
+ <menupopup id="menu_openFeedMessage">
+ <menuitem id="menu_openFeedWebPageInWindow"
+ type="radio"
+ name="openFeedGroup"
+ label="&openFeedWebPageInWindow.label;"
+ accesskey="&openFeedWebPageInWindow.accesskey;"
+ oncommand="FeedMessageHandler.onOpenPref = 0"/>
+ <menuitem id="menu_openFeedSummaryInWindow"
+ type="radio"
+ name="openFeedGroup"
+ label="&openFeedSummaryInWindow.label;"
+ accesskey="&openFeedSummaryInWindow.accesskey;"
+ oncommand="FeedMessageHandler.onOpenPref = 1"/>
+ <menuitem id="menu_openFeedWebPageInMessagePane"
+ type="radio"
+ name="openFeedGroup"
+ label="&openFeedWebPageInMP.label;"
+ accesskey="&openFeedWebPageInMP.accesskey;"
+ oncommand="FeedMessageHandler.onOpenPref = 2"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="messageAfterOpenMsgSeparator"/>
+ <menu id="msgAttachmentMenu" label="&openAttachmentCmd.label;"
+ accesskey="&openAttachmentCmd.accesskey;" disabled="true">
+ <menupopup id="attachmentMenuList" onpopupshowing="FillAttachmentListPopup(this);"/>
+ </menu>
+ <menuseparator id="messageAfterAttachmentMenuSeparator"/>
+ <menuitem id="archiveMainMenu"
+ label="&archiveMsgCmd.label;"
+ accesskey="&archiveMsgCmd.accesskey;"
+ key="key_archive"
+ command="cmd_archive"/>
+ <menu id="moveMenu"
+ label="&moveMsgToMenu.label;"
+ accesskey="&moveMsgToMenu.accesskey;"
+ oncommand="MsgMoveMessage(event.target._folder);">
+ <menupopup id="menu_MovePopup"
+ type="folder"
+ mode="filing"
+ showFileHereLabel="true"
+ showRecent="true"
+ recentLabel="&moveCopyMsgRecentMenu.label;"
+ recentAccessKey="&moveCopyMsgRecentMenu.accesskey;"
+ showFavorites="true"
+ favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+ favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
+ </menu>
+ <menu id="copyMenu"
+ label="&copyMsgToMenu.label;"
+ accesskey="&copyMsgToMenu.accesskey;"
+ oncommand="MsgCopyMessage(event.target._folder);">
+ <menupopup id="menu_copyPopup"
+ type="folder"
+ mode="filing"
+ showFileHereLabel="true"
+ showRecent="true"
+ recentLabel="&moveCopyMsgRecentMenu.label;"
+ recentAccessKey="&moveCopyMsgRecentMenu.accesskey;"
+ showFavorites="true"
+ favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+ favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
+ </menu>
+ <menu id="tagMenu" label="&tagMenu.label;" accesskey="&tagMenu.accesskey;">
+ <menupopup id="tagMenu-tagpopup" onpopupshowing="InitMessageTags(this)">
+ <menuitem id="tagMenu-tagRemoveAll" oncommand="RemoveAllMessageTags();"/>
+ <menuseparator id="tagMenuAfterRemoveSeparator"/>
+ <menuseparator id="tagMenuBeforeCustomizeSeparator"/>
+ <menuitem id="tagMenu-tagCustomize"
+ label="&tagCustomize.label;"
+ accesskey="&tagCustomize.accesskey;"
+ oncommand="goPreferences('tags_pane');"/>
+ </menupopup>
+ </menu>
+ <menu id="markMenu" label="&markMenu.label;" accesskey="&markMenu.accesskey;">
+ <menupopup id="menu_MarkPopup" onpopupshowing="InitMessageMark()">
+ <menuitem id="markReadMenuItem"
+ label="&markAsReadCmd.label;"
+ accesskey="&markAsReadCmd.accesskey;"
+ key="key_markAsRead"
+ command="cmd_markAsRead"/>
+ <menuitem id="markUnreadMenuItem"
+ label="&markAsUnreadCmd.label;"
+ accesskey="&markAsUnreadCmd.accesskey;"
+ key="key_markAsUnread"
+ command="cmd_markAsUnread"/>
+ <menuitem id="markThreadReadMenuItem"
+ label="&markThreadAsReadCmd.label;"
+ accesskey="&markThreadAsReadCmd.accesskey;"
+ key="key_markThreadAsRead"
+ command="cmd_markThreadAsRead"/>
+ <menuitem id="markReadByDateMenuItem"
+ label="&markReadByDateCmd.label;"
+ accesskey="&markReadByDateCmd.accesskey;"
+ key="key_markReadByDate"
+ command="cmd_markReadByDate"/>
+ <menuitem id="markAllReadMenuItem"
+ label="&markAllReadCmd.label;"
+ accesskey="&markAllReadCmd.accesskey;"
+ key="key_markAllRead"
+ command="cmd_markAllRead"/>
+ <menuseparator id="markMenuAfterAllReadSeparator"/>
+ <menuitem id="markFlaggedMenuItem"
+ type="checkbox"
+ label="&markFlaggedCmd.label;"
+ accesskey="&markFlaggedCmd.accesskey;"
+ key="key_toggleFlagged"
+ command="cmd_markAsFlagged"/>
+ <menuseparator id="markMenuAfterFlaggedSeparator"/>
+ <menuitem id="markAsJunkMenuItem"
+ label="&markAsJunkCmd.label;"
+ accesskey="&markAsJunkCmd.accesskey;"
+ key="key_markJunk"
+ command="cmd_markAsJunk"/>
+ <menuitem id="markAsNotJunkMenuItem"
+ label="&markAsNotJunkCmd.label;"
+ accesskey="&markAsNotJunkCmd.accesskey;"
+ key="key_markNotJunk"
+ command="cmd_markAsNotJunk"/>
+ <menuitem id="recalculateJunkScoreMenuItem"
+ label="&recalculateJunkScoreCmd.label;"
+ accesskey="&recalculateJunkScoreCmd.accesskey;"
+ command="cmd_recalculateJunkScore"/>
+ <menuitem id="markAsShowRemoteMenuitem"
+ label="&markAsShowRemoteCmd.label;"
+ accesskey="&markAsShowRemoteCmd.accesskey;"
+ key="key_markShowRemote"
+ command="cmd_markAsShowRemote"/>
+ <menuitem id="markAsNotPhishMenuItem"
+ label="&markAsNotPhishCmd.label;"
+ accesskey="&markAsNotPhishCmd.accesskey;"
+ key="key_markNotPhish"
+ command="cmd_markAsNotPhish"/>
+ </menupopup>
+ </menu>
+ <menuseparator id="messageMenuAfterMarkSeparator"/>
+ <menuitem id="createFilter"
+ label="&createFilter.label;"
+ accesskey="&createFilter.accesskey;"
+ command="cmd_createFilterFromMenu"/>
+ <menuseparator id="threadItemsSeparator"/>
+ <menuitem id="menu_cancel"
+ label="&cancelNewsMsgCmd.label;"
+ accesskey="&cancelNewsMsgCmd.accesskey;"
+ command="cmd_cancel"/>
+ <menuitem id="killThread"
+ label="&killThreadMenu.label;"
+ accesskey="&killThreadMenu.accesskey;"
+ key="key_killThread" command="cmd_killThread"/>
+ <menuitem id="killSubthread"
+ label="&killSubthreadMenu.label;"
+ accesskey="&killSubthreadMenu.accesskey;"
+ key="key_killSubthread" command="cmd_killSubthread"/>
+ <menuitem id="watchThread"
+ label="&watchThreadMenu.label;"
+ accesskey="&watchThreadMenu.accesskey;"
+ key="key_watchThread" command="cmd_watchThread"/>
+ </menupopup>
+</menu>
+
+<menu id="tasksMenu">
+ <menupopup id="taskPopup" onpopupshowing="document.commandDispatcher.updateCommands('create-menu-tasks')">
+ <menuitem id="menu_SearchMail"
+ label="&searchMailCmd.label;"
+ key="key_searchMail"
+ accesskey="&searchMailCmd.accesskey;"
+ command="cmd_search"/>
+ <menuitem id="menu_SearchAddresses"
+ label="&searchAddressesCmd.label;"
+ accesskey="&searchAddressesCmd.accesskey;"
+ oncommand="MsgSearchAddresses()"/>
+ <menuseparator id="tasksMenuAfterAddressesSeparator"/>
+ <menuitem id="menu_Filters"
+ label="&filtersCmd.label;"
+ accesskey="&filtersCmd.accesskey;"
+ command="cmd_displayMsgFilters"/>
+ <menuitem id="applyFilters"
+ label="&filtersApply.label;"
+ accesskey="&filtersApply.accesskey;"
+ command="cmd_applyFilters"/>
+ <menuitem id="applyFiltersToSelection"
+ label="&filtersApplyToMessage.label;"
+ accesskey="&filtersApplyToMessage.accesskey;"
+ command="cmd_applyFiltersToSelection"/>
+ <menuseparator id="tasksMenuAfterApplySeparator"/>
+ <menuitem id="runJunkControls"
+ label="&runJunkControls.label;"
+ accesskey="&runJunkControls.accesskey;"
+ command="cmd_runJunkControls"/>
+ <menuitem id="deleteJunk"
+ label="&deleteJunk.label;"
+ accesskey="&deleteJunk.accesskey;"
+ command="cmd_deleteJunk"/>
+ <menuseparator id="tasksMenuAfterDeleteSeparator"/>
+ <menuitem id="menu_import"
+ label="&importCmd.label;"
+ accesskey="&importCmd.accesskey;"
+ oncommand="toImport();"/>
+ <menuseparator/>
+ </menupopup>
+</menu>
+<menu id="windowMenu"/>
+<menu id="menu_Help"/>
+<spacer flex="100%"/>
+</menubar>
+
+<toolbox id="mail-toolbox"
+ mode="full"
+ defaultmode="full">
+ <toolbar class="toolbar-primary chromeclass-toolbar"
+ id="msgToolbar"
+ persist="collapsed"
+ grippytooltiptext="&mailToolbar.tooltip;"
+ toolbarname="&showMessengerToolbarCmd.label;"
+ accesskey="&showMessengerToolbarCmd.accesskey;"
+ customizable="true"
+ defaultset="button-getmsg,button-newmsg,separator,button-reply,button-replyall,button-forward,separator,button-goback,button-goforward,button-next,button-junk,button-delete,button-mark,spring,throbber-box"
+ context="toolbar-context-menu">
+ </toolbar>
+ <toolbarset id="customToolbars" context="toolbar-context-menu"/>
+
+ <toolbarpalette id="MailToolbarPalette">
+ <toolbarbutton id="button-getmsg"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&getMsgButton.label;"
+ tooltiptext="&getMsgButton.tooltip;"
+ observes="button_getNewMessages"
+ oncommand="MsgGetMessagesForAccount();">
+ <menupopup id="button-getMsgPopup"
+ type="folder"
+ mode="getMail"
+ expandFolders="false"
+ onpopupshowing="getMsgToolbarMenu_init();"
+ oncommand="MsgGetMessagesForAccount(event.target._folder); event.stopPropagation();">
+ <menuitem id="button-getAllNewMsg"
+ label="&getAllNewMsgCmd.label;"
+ accesskey="&getAllNewMsgCmd.accesskey;"
+ command="cmd_getMsgsForAuthAccounts"/>
+ <menuseparator id="button-getAllNewMsgSeparator"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-newmsg"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&newMsgButton.label;"
+ tooltiptext="&newMsgButton.tooltip;"
+ oncommand="MsgNewMessage(event)">
+ <menupopup id="button-newMsgPopup"
+ onpopupshowing="InitNewMsgMenu(this);">
+ <menuitem id="button-newMsgHTML"
+ label="&newHTMLMessageCmd.label;"
+ accesskey="&newHTMLMessageCmd.accesskey;"
+ mode="HTML"/>
+ <menuitem id="button-newMsgPlain"
+ label="&newPlainTextMessageCmd.label;"
+ accesskey="&newPlainTextMessageCmd.accesskey;"
+ mode="PlainText"/>
+ <menuitem id="newMsgButton-mail-menuitem" hidden="true"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-reply"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&replyButton.label;"
+ tooltiptext="&replyButton.tooltip;"
+ observes="button_reply"
+ oncommand="MsgReplyMessage(event)">
+ <menupopup id="button-replyPopup"
+ onpopupshowing="InitMessageReply(this);">
+ <menuitem label="&replyMsgCmd.label;"
+ accesskey="&replyMsgCmd.accesskey;"
+ command="cmd_reply"
+ default="true"/>
+ <menuitem label="&replyListCmd.label;"
+ accesskey="&replyListCmd.accesskey;"
+ command="cmd_replyList"/>
+ <menuitem label="&replyNewsgroupCmd.label;"
+ accesskey="&replyNewsgroupCmd.accesskey;"
+ command="cmd_replyGroup"
+ default="true"/>
+ <menuitem label="&replySenderCmd.label;"
+ accesskey="&replySenderCmd.accesskey;"
+ command="cmd_replySender"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-replyall"
+ class="toolbarbutton-1"
+ label="&replyAllButton.label;"
+ tooltiptext="&replyAllButton.tooltip;"
+ tooltiptextmail="&replyAllButton.tooltip;"
+ tooltiptextnews="&replyAllButtonNews.tooltip;"
+ observes="button_replyall"
+ oncommand="MsgReplyToAllMessage(event)">
+ <menupopup id="button-replyallPopup">
+ <menuitem label="&replyToSenderAndNewsgroupCmd.label;"
+ accesskey="&replyToSenderAndNewsgroupCmd.accesskey;"
+ command="cmd_replySenderAndGroup"
+ default="true"/>
+ <menuitem label="&replyToAllRecipientsCmd.label;"
+ accesskey="&replyToAllRecipientsCmd.accesskey;"
+ command="cmd_replyAllRecipients"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-forward"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&forwardButton.label;"
+ tooltiptext="&forwardButton.tooltip;"
+ observes="button_forward"
+ oncommand="MsgForwardMessage(event)">
+ <menupopup id="button-forwardPopup"
+ onpopupshowing="InitMessageForward(this);">
+ <menuitem label="&forwardAsInline.label;"
+ accesskey="&forwardAsInline.accesskey;"
+ command="cmd_forwardInline"/>
+ <menuitem label="&forwardAsAttachmentCmd.label;"
+ accesskey="&forwardAsAttachmentCmd.accesskey;"
+ command="cmd_forwardAttachment"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="button-file"
+ type="menu"
+ class="toolbarbutton-1"
+ label="&fileButton.label;"
+ observes="button_file"
+ tooltiptext="&fileButton.tooltip;"
+ oncommand="MsgMoveMessage(event.target._folder);">
+ <menupopup id="button-filePopup"
+ type="folder"
+ mode="filing"
+ showRecent="true"
+ showFileHereLabel="true"
+ recentLabel="&moveCopyMsgRecentMenu.label;"
+ recentAccessKey="&moveCopyMsgRecentMenu.accesskey;"
+ showFavorites="true"
+ favoritesLabel="&contextMoveCopyMsgFavoritesMenu.label;"
+ favoritesAccessKey="&contextMoveCopyMsgFavoritesMenu.accesskey;"/>
+ </toolbarbutton>
+
+ <toolbarbutton id="button-goback"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&goBackButton.label;"
+ tooltiptext="&goBackButton.tooltip;"
+ observes="button_goBack"
+ oncommand="goDoCommand('cmd_goBack')">
+ <menupopup id="button-goBackPopup"
+ onpopupshowing="InitBackToolbarMenu(this)"
+ oncommand="NavigateToUri(event.target);"/>
+ </toolbarbutton>
+
+ <toolbarbutton id="button-goforward"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&goForwardButton.label;"
+ tooltiptext="&goForwardButton.tooltip;"
+ observes="button_goForward"
+ oncommand="goDoCommand('cmd_goForward')">
+ <menupopup id="button-goForwardPopup"
+ onpopupshowing="InitForwardToolbarMenu(this)"
+ oncommand="NavigateToUri(event.target);"/>
+ </toolbarbutton>
+
+ <toolbarbutton id="button-next"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&nextButton.label;"
+ tooltiptext="&nextButton.tooltip;"
+ observes="button_next"
+ oncommand="goDoCommand('button_next')">
+ <menupopup id="button-nextPopup"
+ onpopupshowing="InitGoMessagesMenu();">
+ <menuitem label="&nextMsgCmd.label;"
+ accesskey="&nextMsgCmd.accesskey;"
+ command="cmd_nextMsg"/>
+ <menuitem label="&nextUnreadMsgCmd.label;"
+ accesskey="&nextUnreadMsgCmd.accesskey;"
+ command="cmd_nextUnreadMsg" default="true"/>
+ <menuitem label="&nextFlaggedMsgCmd.label;"
+ accesskey="&nextFlaggedMsgCmd.accesskey;"
+ command="cmd_nextFlaggedMsg"/>
+ <menuseparator/>
+ <menuitem label="&nextUnreadThread.label;"
+ accesskey="&nextUnreadThread.accesskey;"
+ command="cmd_nextUnreadThread"/>
+ </menupopup>
+ </toolbarbutton>
+
+ <toolbaritem id="button-junk"
+ title="&junkButton.label;"
+ observes="button_junk">
+ <deck id="junk-deck"
+ oncommand="goDoCommand('button_junk')">
+ <toolbarbutton id="button-isJunk"
+ class="toolbarbutton-1"
+ label="&junkButton.label;"
+ tooltiptext="&junkButton.tooltip;"
+ observes="button-junk"/>
+ <toolbarbutton id="button-notJunk"
+ class="toolbarbutton-1"
+ label="&notJunkButton.label;"
+ tooltiptext="&notJunkButton.tooltip;"
+ observes="button-junk"/>
+ </deck>
+ </toolbaritem>
+
+ <toolbaritem id="button-delete"
+ title="&deleteButton.label;"
+ observes="button_delete">
+ <deck id="delete-deck">
+ <toolbarbutton id="button-mark-deleted"
+ class="toolbarbutton-1"
+ label="&deleteButton.label;"
+ tooltiptext="&deleteButton.tooltip;"
+ observes="button-delete"
+ oncommand="goDoCommand(event.shiftKey ? 'button_shiftDelete' : 'button_delete')"/>
+ <toolbarbutton id="button-mark-undelete"
+ class="toolbarbutton-1"
+ label="&undeleteButton.label;"
+ tooltiptext="&undeleteButton.tooltip;"
+ observes="button-delete"
+ oncommand="goDoCommand('button_delete')"/>
+ </deck>
+ </toolbaritem>
+
+ <toolbarbutton id="button-mark"
+ class="toolbarbutton-1"
+ type="menu-button"
+ label="&markButton.label;"
+ oncommand="goDoCommand('button_mark')"
+ observes="button_mark" tooltiptext="&markButton.tooltip;">
+ <menupopup id="button-markPopup"
+ onpopupshowing="InitMessageMark()">
+ <menuitem id="markReadToolbarItem"
+ label="&markAsReadCmd.label;"
+ accesskey="&markAsReadCmd.accesskey;"
+ command="cmd_markAsRead"/>
+ <menuitem id="markUnreadToolbarItem"
+ label="&markAsUnreadCmd.label;"
+ accesskey="&markAsUnreadCmd.accesskey;"
+ command="cmd_markAsUnread"/>
+ <menuitem id="button-markThreadAsRead"
+ label="&markThreadAsReadCmd.label;"
+ accesskey="&markThreadAsReadCmd.accesskey;"
+ command="cmd_markThreadAsRead"/>
+ <menuitem id="button-markReadByDate"
+ label="&markReadByDateCmd.label;"
+ accesskey="&markReadByDateCmd.accesskey;"
+ command="cmd_markReadByDate"/>
+ <menuitem id="button-markAllRead"
+ label="&markAllReadCmd.label;"
+ accesskey="&markAllReadCmd.accesskey;"
+ command="cmd_markAllRead"/>
+ <menuseparator id="button-markAllReadSeparator"/>
+ <menuitem id="markFlaggedToolbarItem"
+ type="checkbox"
+ label="&markFlaggedCmd.label;"
+ accesskey="&markFlaggedCmd.accesskey;"
+ command="cmd_markAsFlagged"/>
+ </menupopup>
+ </toolbarbutton>
+ <toolbarbutton id="print-button"
+ label="&printButton.label;"
+ tooltiptext="&printButton.tooltip;"
+ observes="button_print"/>
+ <toolbarbutton id="button-stop"
+ class="toolbarbutton-1"
+ label="&stopButton.label;"
+ tooltiptext="&stopButton.tooltip;"
+ command="cmd_stop"/>
+ <toolbaritem id="button-search-container"
+ title="&searchButton.title;"
+ align="center"
+ class="toolbaritem-noline chromeclass-toolbar-additional">
+ <button id="button-search"
+ label="&searchButton.label;"
+ accesskey="&searchButton.accesskey;"
+ tooltiptext="&advancedButton.tooltip;"
+ observes="button_search"
+ oncommand="goDoCommand('button_search')"/>
+ <button id="button-advanced"
+ label="&advancedButton.label;"
+ accesskey="&advancedButton.accesskey;"
+ tooltiptext="&advancedButton.tooltip;"
+ observes="button_search"
+ oncommand="goDoCommand('button_search')"/>
+ </toolbaritem>
+ <toolbaritem id="throbber-box"/>
+ <!-- see utilityOverlay.xul
+ <toolbarbutton id="sync-button"/> -->
+ </toolbarpalette>
+
+</toolbox>
+
+<statusbar id="status-bar"
+ class="chromeclass-status" >
+ <statusbarpanel id="component-bar"/>
+ <statusbarpanel id="statusText"
+ label="&statusText.label;"
+ crop="right"
+ flex="1"/>
+ <statusbarpanel id="statusbar-progresspanel"
+ class="statusbarpanel-progress"
+ collapsed="true">
+ <progressmeter id="statusbar-icon"
+ class="progressmeter-statusbar"
+ mode="normal"
+ value="0"/>
+ </statusbarpanel>
+ <statusbarpanel id="unreadMessageCount"
+ hidden="true"/>
+ <statusbarpanel id="totalMessageCount"
+ hidden="true"/>
+ <statusbarpanel id="signed-status"
+ class="statusbarpanel-iconic"
+ collapsed="true"
+ oncommand="showMessageReadSecurityInfo();"/>
+ <statusbarpanel id="encrypted-status"
+ class="statusbarpanel-iconic"
+ collapsed="true"
+ oncommand="showMessageReadSecurityInfo();"/>
+ <statusbarpanel id="offline-status"
+ class="statusbarpanel-iconic"
+ checkfunc="MailCheckBeforeOfflineChange();" />
+</statusbar>
+
+</overlay>
diff --git a/comm/suite/mailnews/content/messageWindow.js b/comm/suite/mailnews/content/messageWindow.js
new file mode 100644
index 0000000000..fd61c6737f
--- /dev/null
+++ b/comm/suite/mailnews/content/messageWindow.js
@@ -0,0 +1,1044 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js");
+
+/* This is where functions related to the standalone message window are kept */
+
+// from MailNewsTypes.h
+const nsMsgKey_None = 0xFFFFFFFF;
+const nsMsgViewIndex_None = 0xFFFFFFFF;
+
+/* globals for a particular window */
+
+var gCurrentMessageUri;
+var gCurrentFolderUri;
+var gThreadPaneCommandUpdater = null;
+var gCurrentMessageIsDeleted = false;
+var gNextMessageViewIndexAfterDelete = -2;
+var gCurrentFolderToRerootForStandAlone;
+var gRerootOnFolderLoadForStandAlone = false;
+var gNextMessageAfterLoad = null;
+
+// the folderListener object
+var folderListener = {
+ onFolderAdded: function(parentFolder, child) {},
+ onMessageAdded: function(parentFolder, msg) {},
+ onFolderRemoved: function(parentFolder, child) {},
+ onMessageRemoved: function(parentFolder, msg)
+ {
+ if (parentFolder.URI != gCurrentFolderUri)
+ return;
+ if (extractMsgKeyFromURI() == msg.messageKey)
+ gCurrentMessageIsDeleted = true;
+ },
+
+ onFolderPropertyChanged: function(item, property, oldValue, newValue) {},
+ onFolderIntPropertyChanged: function(item, property, oldValue, newValue) {
+ if (item.URI == gCurrentFolderUri) {
+ if (property == "TotalMessages" || property == "TotalUnreadMessages") {
+ UpdateStandAloneMessageCounts();
+ }
+ }
+ },
+ onFolderBoolPropertyChanged: function(item, property, oldValue, newValue) {},
+ onFolderUnicharPropertyChanged: function(item, property, oldValue, newValue){},
+ onFolderPropertyFlagChanged: function(item, property, oldFlag, newFlag) {},
+
+ onFolderEvent: function(folder, event) {
+ if (event == "DeleteOrMoveMsgCompleted")
+ HandleDeleteOrMoveMsgCompleted(folder);
+ else if (event == "DeleteOrMoveMsgFailed")
+ HandleDeleteOrMoveMsgFailed(folder);
+ else if (event == "FolderLoaded") {
+ if (folder) {
+ var uri = folder.URI;
+ if (uri == gCurrentFolderToRerootForStandAlone) {
+ gCurrentFolderToRerootForStandAlone = null;
+ folder.endFolderLoading();
+ if (gRerootOnFolderLoadForStandAlone) {
+ RerootFolderForStandAlone(uri);
+ }
+ }
+ }
+ }
+ else if (event == "JunkStatusChanged") {
+ HandleJunkStatusChanged(folder);
+ }
+ }
+}
+
+var messagepaneObserver = {
+ onDrop(aEvent) {
+ let dragSession = Cc["@mozilla.org/widget/dragservice;1"]
+ .getService(Ci.nsIDragService)
+ .getCurrentSession();
+ if (!this.canDrop(aEvent, dragSession)) {
+ return;
+ }
+ let sourceUri = aEvent.dataTransfer.getData("text/x-moz-message");
+ if (sourceUri != gCurrentMessageUri)
+ {
+ var msgHdr = GetMsgHdrFromUri(sourceUri);
+
+ // Reset the window's message uri and folder uri vars, and
+ // update the command handlers to what's going to be used.
+ // This has to be done before the call to CreateView().
+ gCurrentMessageUri = sourceUri;
+ gCurrentFolderUri = msgHdr.folder.URI;
+ UpdateMailToolbar('onDrop');
+
+ // even if the folder uri's match, we can't use the existing view
+ // (msgHdr.folder.URI == windowID.gCurrentFolderUri)
+ // the reason is quick search and mail views.
+ // see bug #187673
+ CreateView(dragSession.sourceNode.ownerDocument.defaultView.gDBView);
+ LoadMessageByMsgKey(msgHdr.messageKey);
+ }
+ aEvent.stopPropagation();
+ },
+
+ onDragOver(aEvent) {
+ var messagepanebox = document.getElementById("messagepanebox");
+ messagepanebox.setAttribute("dragover", "true");
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ },
+
+ onDragExit(aEvent) {
+ var messagepanebox = document.getElementById("messagepanebox");
+ messagepanebox.removeAttribute("dragover");
+ },
+
+ canDrop(aEvent, aDragSession) {
+ // Allow drop from mail:3pane window only - 4xp.
+ var doc = aDragSession.sourceNode.ownerDocument;
+ var elem = doc.getElementById("messengerWindow");
+ return (elem && (elem.getAttribute("windowtype") == "mail:3pane"));
+ },
+};
+
+function nsMsgDBViewCommandUpdater()
+{}
+
+function UpdateStandAloneMessageCounts()
+{
+ // hook for extra toolbar items
+ Services.obs.notifyObservers(window,
+ "mail:updateStandAloneMessageCounts");
+}
+
+nsMsgDBViewCommandUpdater.prototype =
+{
+ updateCommandStatus : function()
+ {
+ // the back end is smart and is only telling us to update command status
+ // when the # of items in the selection has actually changed.
+ UpdateMailToolbar("dbview, std alone window");
+ },
+
+ displayMessageChanged : function(aFolder, aSubject, aKeywords)
+ {
+ setTitleFromFolder(aFolder, aSubject);
+ ClearPendingReadTimer(); // we are loading / selecting a new message so kill the mark as read timer for the currently viewed message
+ gCurrentMessageUri = gDBView.URIForFirstSelectedMessage;
+ UpdateStandAloneMessageCounts();
+ goUpdateCommand("button_delete");
+ goUpdateCommand("button_junk");
+ goUpdateCommand("button_goBack");
+ goUpdateCommand("button_goForward");
+ },
+
+ updateNextMessageAfterDelete : function()
+ {
+ SetNextMessageAfterDelete();
+ },
+
+ summarizeSelection: function() {return false},
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Ci.nsIMsgDBViewCommandUpdater) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_NOINTERFACE;
+ }
+}
+
+function HandleDeleteOrMoveMsgCompleted(folder)
+{
+ if ((folder.URI == gCurrentFolderUri) && gCurrentMessageIsDeleted)
+ {
+ gDBView.onDeleteCompleted(true);
+ gCurrentMessageIsDeleted = false;
+ if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None)
+ {
+ var nextMstKey = gDBView.getKeyAt(gNextMessageViewIndexAfterDelete);
+ if (nextMstKey != nsMsgKey_None &&
+ !Services.prefs.getBoolPref("mail.close_message_window.on_delete"))
+ LoadMessageByViewIndex(gNextMessageViewIndexAfterDelete);
+ else
+ window.close();
+ }
+ else
+ {
+ // close the stand alone window because there are no more messages in the folder
+ window.close();
+ }
+ }
+}
+
+function HandleDeleteOrMoveMsgFailed(folder)
+{
+ gDBView.onDeleteCompleted(false);
+ if ((folder.URI == gCurrentFolderUri) && gCurrentMessageIsDeleted)
+ gCurrentMessageIsDeleted = false;
+}
+
+function IsCurrentLoadedFolder(folder)
+{
+ return (folder.URI == gCurrentFolderUri);
+}
+
+function OnLoadMessageWindow()
+{
+ AddMailOfflineObserver();
+ CreateMailWindowGlobals();
+ verifyAccounts(null);
+
+ InitMsgWindow();
+
+ messenger.setWindow(window, msgWindow);
+ // FIX ME - later we will be able to use onload from the overlay
+ OnLoadMsgHeaderPane();
+
+ var nsIFolderListener = Ci.nsIFolderListener;
+ var notifyFlags = nsIFolderListener.removed | nsIFolderListener.event |
+ nsIFolderListener.intPropertyChanged;
+ MailServices.mailSession.AddFolderListener(folderListener, notifyFlags);
+
+ var originalView = null;
+ var folder = null;
+ var messageUri;
+ var loadCustomMessage = false; //set to true when either loading a message/rfc822 attachment or a .eml file
+ if (window.arguments)
+ {
+ if (window.arguments[0])
+ {
+ try
+ {
+ messageUri = window.arguments[0];
+ if (messageUri instanceof Ci.nsIURI)
+ {
+ loadCustomMessage = /type=application\/x-message-display/.test(messageUri.spec);
+ gCurrentMessageUri = messageUri.spec;
+ if (messageUri instanceof Ci.nsIMsgMailNewsUrl)
+ folder = messageUri.folder;
+ }
+ }
+ catch(ex)
+ {
+ folder = null;
+ dump("## ex=" + ex + "\n");
+ }
+
+ if (!gCurrentMessageUri)
+ gCurrentMessageUri = window.arguments[0];
+ }
+ else
+ gCurrentMessageUri = null;
+
+ if (window.arguments[1])
+ gCurrentFolderUri = window.arguments[1];
+ else
+ gCurrentFolderUri = folder ? folder.URI : null;
+
+ if (window.arguments[2])
+ originalView = window.arguments[2];
+
+ }
+
+ CreateView(originalView);
+
+ // Before and after callbacks for the customizeToolbar code
+ var mailToolbox = getMailToolbox();
+ mailToolbox.customizeInit = MailToolboxCustomizeInit;
+ mailToolbox.customizeDone = MailToolboxCustomizeDone;
+ mailToolbox.customizeChange = MailToolboxCustomizeChange;
+
+ setTimeout(OnLoadMessageWindowDelayed, 0, loadCustomMessage);
+
+ SetupCommandUpdateHandlers();
+
+ window.addEventListener("AppCommand", HandleAppCommandEvent, true);
+}
+
+function HandleAppCommandEvent(evt)
+{
+ evt.stopPropagation();
+ switch (evt.command)
+ {
+ case "Back":
+ goDoCommand('cmd_goBack');
+ break;
+ case "Forward":
+ goDoCommand('cmd_goForward');
+ break;
+ case "Stop":
+ goDoCommand('cmd_stop');
+ break;
+ case "Search":
+ goDoCommand('cmd_search');
+ break;
+ case "Bookmarks":
+ toAddressBook();
+ break;
+ case "Reload":
+ goDoCommand('cmd_reload');
+ break;
+ case "Home":
+ default:
+ break;
+ }
+}
+
+function OnLoadMessageWindowDelayed(loadCustomMessage)
+{
+ gDBView.suppressMsgDisplay = false;
+ if (loadCustomMessage)
+ gDBView.loadMessageByUrl(gCurrentMessageUri);
+ else
+ {
+ var msgKey = extractMsgKeyFromURI(gCurrentMessageUri);
+ var viewIndex = gDBView.findIndexFromKey(msgKey, true);
+ // the message may not appear in the view if loaded from a search dialog
+ if (viewIndex != nsMsgViewIndex_None)
+ LoadMessageByViewIndex(viewIndex);
+ else
+ messenger.openURL(gCurrentMessageUri);
+ }
+ gNextMessageViewIndexAfterDelete = gDBView.msgToSelectAfterDelete;
+ UpdateStandAloneMessageCounts();
+
+ // set focus to the message pane
+ window.content.focus();
+
+ // since we just changed the pane with focus we need to update the toolbar to reflect this
+ // XXX TODO
+ // can we optimize
+ // and just update cmd_delete and button_delete?
+ UpdateMailToolbar("focus");
+}
+
+function CreateView(originalView)
+{
+ var msgFolder = GetLoadedMsgFolder();
+
+ // extract the sort type, the sort order,
+ var sortType;
+ var sortOrder;
+ var viewFlags;
+ var viewType;
+
+ if (originalView)
+ {
+ viewType = originalView.viewType;
+ viewFlags = originalView.viewFlags;
+ sortType = originalView.sortType;
+ sortOrder = originalView.sortOrder;
+ }
+ else if (msgFolder)
+ {
+ var msgDatabase = msgFolder.msgDatabase;
+ if (msgDatabase)
+ {
+ var dbFolderInfo = msgDatabase.dBFolderInfo;
+ sortType = dbFolderInfo.sortType;
+ sortOrder = dbFolderInfo.sortOrder;
+ viewFlags = dbFolderInfo.viewFlags;
+ viewType = dbFolderInfo.viewType;
+ msgDatabase = null;
+ dbFolderInfo = null;
+ }
+ }
+ else
+ {
+ viewType = nsMsgViewType.eShowSearch;
+ }
+
+ // create a db view
+ CreateBareDBView(originalView, msgFolder, viewType, viewFlags, sortType, sortOrder);
+
+ var uri;
+ if (gCurrentMessageUri)
+ uri = gCurrentMessageUri;
+ else if (gCurrentFolderUri)
+ uri = gCurrentFolderUri;
+ else
+ uri = null;
+
+ SetUpToolbarButtons(uri);
+
+ // hook for extra toolbar items
+ Services.obs.notifyObservers(window, "mail:setupToolbarItems", uri);
+}
+
+function extractMsgKeyFromURI()
+{
+ var msgKey = -1;
+ var msgHdr = messenger.msgHdrFromURI(gCurrentMessageUri);
+ if (msgHdr)
+ msgKey = msgHdr.messageKey;
+ return msgKey;
+}
+
+function OnUnloadMessageWindow()
+{
+ window.removeEventListener("AppCommand", HandleAppCommandEvent, true);
+
+ UnloadCommandUpdateHandlers();
+
+ // FIX ME - later we will be able to use onunload from the overlay
+ OnUnloadMsgHeaderPane();
+
+ OnMailWindowUnload();
+}
+
+function GetSelectedMsgFolders()
+{
+ var msgFolder = GetLoadedMsgFolder();
+ return msgFolder ? [msgFolder] : [];
+}
+
+function GetNumSelectedMessages()
+{
+ return (gCurrentMessageUri) ? 1 : 0;
+}
+
+function GetSelectedIndices(dbView)
+{
+ try {
+ return dbView.getIndicesForSelection();
+ }
+ catch (ex) {
+ dump("ex = " + ex + "\n");
+ return null;
+ }
+}
+
+function GetLoadedMsgFolder()
+{
+ return gCurrentFolderUri ? MailUtils.getFolderForURI(gCurrentFolderUri)
+ : null;
+}
+
+function GetLoadedMessage()
+{
+ return gCurrentMessageUri;
+}
+
+//Clear everything related to the current message. called after load start page.
+function ClearMessageSelection()
+{
+ gCurrentMessageUri = null;
+ gCurrentFolderUri = null;
+ UpdateMailToolbar("clear msg, std alone window");
+}
+
+function SetNextMessageAfterDelete()
+{
+ gNextMessageViewIndexAfterDelete = gDBView.msgToSelectAfterDelete;
+}
+
+function SelectMsgFolder(msgfolder) {
+ if (!msgfolder || msgfolder.isServer)
+ return;
+
+ let folderUri = msgfolder.URI;
+ if (folderUri == gCurrentFolderUri)
+ return;
+
+ // close old folder view
+ var dbview = GetDBView();
+ if (dbview)
+ dbview.close();
+
+ gCurrentFolderToRerootForStandAlone = folderUri;
+
+ if (msgfolder.manyHeadersToDownload)
+ {
+ gRerootOnFolderLoadForStandAlone = true;
+ try
+ {
+ msgfolder.startFolderLoading();
+ msgfolder.updateFolder(msgWindow);
+ }
+ catch(ex)
+ {
+ dump("Error loading with many headers to download: " + ex + "\n");
+ }
+ }
+ else
+ {
+ RerootFolderForStandAlone(folderUri);
+ gRerootOnFolderLoadForStandAlone = false;
+ msgfolder.startFolderLoading();
+
+ //Need to do this after rerooting folder. Otherwise possibility of receiving folder loaded
+ //notification before folder has actually changed.
+ msgfolder.updateFolder(msgWindow);
+ }
+}
+
+function RerootFolderForStandAlone(uri)
+{
+ gCurrentFolderUri = uri;
+
+ // create new folder view
+ CreateView(null);
+
+ // now do the work to load the appropriate message
+ if (gNextMessageAfterLoad) {
+ var type = gNextMessageAfterLoad;
+ gNextMessageAfterLoad = null;
+ LoadMessageByNavigationType(type);
+ }
+
+ SetUpToolbarButtons(gCurrentFolderUri);
+
+ UpdateMailToolbar("reroot folder in stand alone window");
+
+ // hook for extra toolbar items
+ Services.obs.notifyObservers(window, "mail:setupToolbarItems", uri);
+}
+
+function GetMsgHdrFromUri(messageUri)
+{
+ return messenger.msgHdrFromURI(messageUri);
+}
+
+function SelectMessage(messageUri)
+{
+ var msgHdr = GetMsgHdrFromUri(messageUri);
+ LoadMessageByMsgKey(msgHdr.messageKey);
+}
+
+function ReloadMessage()
+{
+ gDBView.reloadMessage();
+}
+
+// MessageWindowController object (handles commands when one of the trees does not have focus)
+var MessageWindowController =
+{
+ supportsCommand: function(command)
+ {
+ switch (command)
+ {
+ case "cmd_delete":
+ case "cmd_stop":
+ case "cmd_undo":
+ case "cmd_redo":
+ case "cmd_killThread":
+ case "cmd_killSubthread":
+ case "cmd_watchThread":
+ case "button_delete":
+ case "button_shiftDelete":
+ case "button_junk":
+ case "cmd_shiftDelete":
+ case "cmd_saveAsTemplate":
+ case "cmd_getMsgsForAuthAccounts":
+ case "button_mark":
+ case "cmd_markAsRead":
+ case "cmd_markAsUnread":
+ case "cmd_markAllRead":
+ case "cmd_markThreadAsRead":
+ case "cmd_markReadByDate":
+ case "cmd_markAsFlagged":
+ case "button_file":
+ case "cmd_markAsJunk":
+ case "cmd_markAsNotJunk":
+ case "cmd_recalculateJunkScore":
+ case "cmd_markAsShowRemote":
+ case "cmd_markAsNotPhish":
+ case "cmd_applyFiltersToSelection":
+ case "cmd_applyFilters":
+ case "cmd_runJunkControls":
+ case "cmd_deleteJunk":
+ case "cmd_nextMsg":
+ case "button_next":
+ case "cmd_nextUnreadMsg":
+ case "cmd_nextFlaggedMsg":
+ case "cmd_nextUnreadThread":
+ case "cmd_previousMsg":
+ case "cmd_previousUnreadMsg":
+ case "cmd_previousFlaggedMsg":
+ case "cmd_goBack":
+ case "button_goBack":
+ case "cmd_goForward":
+ case "button_goForward":
+ return (gDBView.keyForFirstSelectedMessage != nsMsgKey_None);
+ case "cmd_viewPageSource":
+ return GetNumSelectedMessages() > 0;
+ case "cmd_reply":
+ case "button_reply":
+ case "cmd_replyList":
+ case "cmd_replyGroup":
+ case "cmd_replySender":
+ case "cmd_replyall":
+ case "cmd_replySenderAndGroup":
+ case "cmd_replyAllRecipients":
+ case "button_replyall":
+ case "cmd_forward":
+ case "button_forward":
+ case "cmd_forwardInline":
+ case "cmd_forwardAttachment":
+ case "cmd_editAsNew":
+ case "cmd_editDraftMsg":
+ case "cmd_newMsgFromTemplate":
+ case "cmd_editTemplateMsg":
+ case "cmd_getNextNMessages":
+ case "cmd_find":
+ case "cmd_findNext":
+ case "cmd_findPrev":
+ case "button_search":
+ case "cmd_search":
+ case "cmd_reload":
+ case "cmd_saveAsFile":
+ case "cmd_getNewMessages":
+ case "button_getNewMessages":
+ case "button_print":
+ case "cmd_print":
+ case "cmd_printpreview":
+ case "cmd_printSetup":
+ case "cmd_settingsOffline":
+ case "cmd_createFilterFromPopup":
+ case "cmd_createFilterFromMenu":
+ case "cmd_viewAllHeader":
+ case "cmd_viewNormalHeader":
+ return true;
+ case "cmd_synchronizeOffline":
+ case "cmd_downloadFlagged":
+ case "cmd_downloadSelected":
+ return !Services.io.offline;
+ default:
+ return false;
+ }
+ },
+
+ isCommandEnabled: function(command)
+ {
+ var loadedFolder;
+ var enabled = new Object();
+ enabled.value = false;
+ var checkStatus = new Object();
+
+ switch (command)
+ {
+ case "cmd_createFilterFromPopup":
+ case "cmd_createFilterFromMenu":
+ loadedFolder = GetLoadedMsgFolder();
+ return (loadedFolder && loadedFolder.server.canHaveFilters);
+ case "cmd_delete":
+ UpdateDeleteCommand();
+ // fall through
+ case "button_delete":
+ if (command == "button_delete")
+ UpdateDeleteToolbarButton(false);
+ // fall through
+ case "cmd_shiftDelete":
+ case "button_shiftDelete":
+ loadedFolder = GetLoadedMsgFolder();
+ return gCurrentMessageUri && loadedFolder && loadedFolder.canDeleteMessages;
+ case "button_junk":
+ UpdateJunkToolbarButton();
+ // fall through
+ case "cmd_markAsJunk":
+ case "cmd_markAsNotJunk":
+ if (gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.junk, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_recalculateJunkScore":
+ if (GetNumSelectedMessages() > 0 && gDBView)
+ gDBView.getCommandStatus(nsMsgViewCommandType.runJunkControls, enabled, checkStatus);
+ return enabled.value;
+ case "cmd_reply":
+ case "button_reply":
+ case "cmd_replyList":
+ case "cmd_replyGroup":
+ case "cmd_replySender":
+ case "cmd_replyall":
+ case "button_replyall":
+ case "cmd_replySenderAndGroup":
+ case "cmd_replyAllRecipients":
+ case "cmd_forward":
+ case "button_forward":
+ case "cmd_forwardInline":
+ case "cmd_forwardAttachment":
+ case "cmd_editAsNew":
+ case "cmd_editDraftMsg":
+ case "cmd_newMsgFromTemplate":
+ case "cmd_editTemplateMsg":
+ case "cmd_print":
+ case "cmd_printpreview":
+ case "button_print":
+ case "cmd_saveAsFile":
+ return true;
+ case "cmd_saveAsTemplate":
+ var target = getMessageBrowser().contentPrincipal.URI.scheme;
+ return target != "news";
+ case "cmd_viewPageSource":
+ case "cmd_reload":
+ case "cmd_find":
+ case "button_mark":
+ case "cmd_markAllRead":
+ case "cmd_markThreadAsRead":
+ case "cmd_markReadByDate":
+ case "cmd_viewAllHeader":
+ case "cmd_viewNormalHeader":
+ return true;
+ case "cmd_markAsRead":
+ return CanMarkMsgAsRead(true);
+ case "cmd_markAsUnread":
+ return CanMarkMsgAsRead(false);
+ case "cmd_markAsFlagged":
+ case "button_file":
+ return (gCurrentMessageUri != null);
+ case "cmd_markAsShowRemote":
+ return (GetNumSelectedMessages() > 0 && checkMsgHdrPropertyIsNot("remoteContentPolicy", kAllowRemoteContent));
+ case "cmd_markAsNotPhish":
+ return (GetNumSelectedMessages() > 0 && checkMsgHdrPropertyIsNot("notAPhishMessage", kNotAPhishMessage));
+ case "cmd_printSetup":
+ return true;
+ case "cmd_getNewMessages":
+ case "button_getNewMessages":
+ case "cmd_getMsgsForAuthAccounts":
+ return IsGetNewMessagesEnabled();
+ case "cmd_getNextNMessages":
+ return IsGetNextNMessagesEnabled();
+ case "cmd_downloadFlagged":
+ case "cmd_downloadSelected":
+ case "cmd_synchronizeOffline":
+ return !Services.io.offline;
+ case "cmd_settingsOffline":
+ return IsAccountOfflineEnabled();
+ case "cmd_nextMsg":
+ case "button_next":
+ case "cmd_nextUnreadMsg":
+ case "cmd_nextFlaggedMsg":
+ case "cmd_nextUnreadThread":
+ case "cmd_previousMsg":
+ case "cmd_previousUnreadMsg":
+ case "cmd_previousFlaggedMsg":
+ case "cmd_applyFiltersToSelection":
+ return true;
+ case "cmd_findNext":
+ case "cmd_findPrev":
+ return MsgCanFindAgain();
+ case "cmd_goBack":
+ case "button_goBack":
+ return gDBView && gDBView.navigateStatus(nsMsgNavigationType.back);
+ case "cmd_goForward":
+ case "button_goForward":
+ return gDBView && gDBView.navigateStatus(nsMsgNavigationType.forward);
+ case "button_search":
+ case "cmd_search":
+ loadedFolder = GetLoadedMsgFolder();
+ return (loadedFolder && loadedFolder.server.canSearchMessages);
+ case "cmd_stop":
+ return true;
+ case "cmd_undo":
+ case "cmd_redo":
+ return SetupUndoRedoCommand(command);
+ case "cmd_applyFilters":
+ case "cmd_runJunkControls":
+ case "cmd_deleteJunk":
+ return false;
+ default:
+ return false;
+ }
+ },
+
+ doCommand: function(command)
+ {
+ // if the user invoked a key short cut then it is possible that we got here for a command which is
+ // really disabled. kick out if the command should be disabled.
+ if (!this.isCommandEnabled(command)) return;
+
+ var navigationType = nsMsgNavigationType.nextUnreadMessage;
+
+ switch ( command )
+ {
+ case "cmd_getNewMessages":
+ MsgGetMessage();
+ break;
+ case "cmd_undo":
+ messenger.undo(msgWindow);
+ break;
+ case "cmd_redo":
+ messenger.redo(msgWindow);
+ break;
+ case "cmd_getMsgsForAuthAccounts":
+ MsgGetMessagesForAllAuthenticatedAccounts();
+ break;
+ case "cmd_getNextNMessages":
+ MsgGetNextNMessages();
+ break;
+ case "cmd_reply":
+ MsgReplyMessage(null);
+ break;
+ case "cmd_replyList":
+ MsgReplyList(null);
+ break;
+ case "cmd_replyGroup":
+ MsgReplyGroup(null);
+ break;
+ case "cmd_replySender":
+ MsgReplySender(null);
+ break;
+ case "cmd_replyall":
+ MsgReplyToAllMessage(null);
+ break;
+ case "cmd_replySenderAndGroup":
+ MsgReplyToSenderAndGroup(null);
+ break;
+ case "cmd_replyAllRecipients":
+ MsgReplyToAllRecipients(null);
+ break;
+ case "cmd_forward":
+ MsgForwardMessage(null);
+ break;
+ case "cmd_forwardInline":
+ MsgForwardAsInline(null);
+ break;
+ case "cmd_forwardAttachment":
+ MsgForwardAsAttachment(null);
+ break;
+ case "cmd_editAsNew":
+ MsgEditMessageAsNew(null);
+ break;
+ case "cmd_editDraftMsg":
+ MsgEditDraftMessage(null);
+ break;
+ case "cmd_newMsgFromTemplate":
+ MsgNewMessageFromTemplate(null);
+ break;
+ case "cmd_editTemplateMsg":
+ MsgEditTemplateMessage(null);
+ break;
+ case "cmd_createFilterFromPopup":
+ CreateFilter(document.popupNode);
+ break;
+ case "cmd_createFilterFromMenu":
+ MsgCreateFilter();
+ break;
+ case "cmd_delete":
+ case "button_delete":
+ MsgDeleteMessage(false);
+ UpdateDeleteToolbarButton(false);
+ break;
+ case "cmd_shiftDelete":
+ case "button_shiftDelete":
+ MsgDeleteMessage(true);
+ break;
+ case "button_junk":
+ MsgJunk();
+ break;
+ case "cmd_stop":
+ MsgStop();
+ break;
+ case "cmd_printSetup":
+ PrintUtils.showPageSetup();
+ break;
+ case "cmd_print":
+ PrintEnginePrint();
+ break;
+ case "cmd_printpreview":
+ PrintEnginePrintPreview();
+ break;
+ case "cmd_saveAsFile":
+ MsgSaveAsFile();
+ break;
+ case "cmd_saveAsTemplate":
+ MsgSaveAsTemplate();
+ break;
+ case "cmd_viewPageSource":
+ MsgViewPageSource();
+ break;
+ case "cmd_reload":
+ ReloadMessage();
+ break;
+ case "cmd_find":
+ MsgFind();
+ break;
+ case "cmd_findNext":
+ MsgFindAgain(false);
+ break;
+ case "cmd_findPrev":
+ MsgFindAgain(true);
+ break;
+ case "button_search":
+ case "cmd_search":
+ MsgSearchMessages();
+ break;
+ case "button_mark":
+ MsgMarkMsgAsRead();
+ return;
+ case "cmd_markAsRead":
+ MsgMarkMsgAsRead(true);
+ return;
+ case "cmd_markAsUnread":
+ MsgMarkMsgAsRead(false);
+ return;
+ case "cmd_markThreadAsRead":
+ MsgMarkThreadAsRead();
+ return;
+ case "cmd_markAllRead":
+ MsgMarkAllRead();
+ return;
+ case "cmd_markReadByDate":
+ MsgMarkReadByDate();
+ return;
+ case "cmd_viewAllHeader":
+ MsgViewAllHeaders();
+ return;
+ case "cmd_viewNormalHeader":
+ MsgViewNormalHeaders();
+ return;
+ case "cmd_markAsFlagged":
+ MsgMarkAsFlagged();
+ return;
+ case "cmd_markAsJunk":
+ JunkSelectedMessages(true);
+ return;
+ case "cmd_markAsNotJunk":
+ JunkSelectedMessages(false);
+ return;
+ case "cmd_recalculateJunkScore":
+ analyzeMessagesForJunk();
+ return;
+ case "cmd_markAsShowRemote":
+ LoadMsgWithRemoteContent();
+ return;
+ case "cmd_markAsNotPhish":
+ MsgIsNotAScam();
+ return;
+ case "cmd_downloadFlagged":
+ MsgDownloadFlagged();
+ return;
+ case "cmd_downloadSelected":
+ MsgDownloadSelected();
+ return;
+ case "cmd_synchronizeOffline":
+ MsgSynchronizeOffline();
+ return;
+ case "cmd_settingsOffline":
+ MsgSettingsOffline();
+ return;
+ case "cmd_nextUnreadMsg":
+ case "button_next":
+ performNavigation(nsMsgNavigationType.nextUnreadMessage);
+ break;
+ case "cmd_nextUnreadThread":
+ performNavigation(nsMsgNavigationType.nextUnreadThread);
+ break;
+ case "cmd_nextMsg":
+ performNavigation(nsMsgNavigationType.nextMessage);
+ break;
+ case "cmd_nextFlaggedMsg":
+ performNavigation(nsMsgNavigationType.nextFlagged);
+ break;
+ case "cmd_previousMsg":
+ performNavigation(nsMsgNavigationType.previousMessage);
+ break;
+ case "cmd_previousUnreadMsg":
+ performNavigation(nsMsgNavigationType.previousUnreadMessage);
+ break;
+ case "cmd_previousFlaggedMsg":
+ performNavigation(nsMsgNavigationType.previousFlagged);
+ break;
+ case "cmd_goBack":
+ performNavigation(nsMsgNavigationType.back);
+ break;
+ case "cmd_goForward":
+ performNavigation(nsMsgNavigationType.forward);
+ break;
+ case "cmd_applyFiltersToSelection":
+ MsgApplyFiltersToSelection();
+ break;
+ }
+ },
+
+ onEvent: function(event)
+ {
+ }
+};
+
+function LoadMessageByNavigationType(type)
+{
+ var resultId = new Object;
+ var resultIndex = new Object;
+ var threadIndex = new Object;
+
+ gDBView.viewNavigate(type, resultId, resultIndex, threadIndex, true /* wrap */);
+
+ // if we found something....display it.
+ if ((resultId.value != nsMsgKey_None) && (resultIndex.value != nsMsgKey_None))
+ {
+ // load the message key
+ LoadMessageByMsgKey(resultId.value);
+ // if we changed folders, the message counts changed.
+ UpdateStandAloneMessageCounts();
+
+ // new message has been loaded
+ return true;
+ }
+
+ // no message found to load
+ return false;
+}
+
+function performNavigation(type)
+{
+ // Try to load a message by navigation type if we can find
+ // the message in the same folder.
+ if (LoadMessageByNavigationType(type))
+ return;
+
+ CrossFolderNavigation(type);
+}
+
+function SetupCommandUpdateHandlers()
+{
+ top.controllers.insertControllerAt(0, MessageWindowController);
+}
+
+function UnloadCommandUpdateHandlers()
+{
+ top.controllers.removeController(MessageWindowController);
+}
+
+function GetDBView()
+{
+ return gDBView;
+}
+
+function LoadMessageByMsgKey(messageKey)
+{
+ var viewIndex = gDBView.findIndexFromKey(messageKey, true);
+ gDBView.loadMessageByViewIndex(viewIndex);
+ // we only want to update the toolbar if there was no previous selected message.
+ if (nsMsgKey_None == gDBView.keyForFirstSelectedMessage)
+ UpdateMailToolbar("update toolbar for message Window");
+}
+
+function LoadMessageByViewIndex(viewIndex)
+{
+ gDBView.loadMessageByViewIndex(viewIndex);
+ // we only want to update the toolbar if there was no previous selected message.
+ if (nsMsgKey_None == gDBView.keyForFirstSelectedMessage)
+ UpdateMailToolbar("update toolbar for message Window");
+}
diff --git a/comm/suite/mailnews/content/messageWindow.xul b/comm/suite/mailnews/content/messageWindow.xul
new file mode 100644
index 0000000000..9ec502086c
--- /dev/null
+++ b/comm/suite/mailnews/content/messageWindow.xul
@@ -0,0 +1,141 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://messenger/skin/messageWindow.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/mailWindowOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
+%messengerDTD;
+]>
+
+<window id="messengerWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ title="&messengerWindow.title;"
+ titlemodifier="&titleModifier.label;"
+ titlemenuseparator="&titleSeparator.label;"
+ onload="OnLoadMessageWindow()"
+ onunload="OnUnloadMessageWindow()"
+ width="750"
+ height="500"
+ persist="width height screenX screenY sizemode"
+ toggletoolbar="true"
+ lightweightthemes="true"
+ lightweightthemesfooter="status-bar"
+ macanimationtype="document"
+ drawtitle="true"
+ windowtype="mail:messageWindow">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_offlinePrompts" src="chrome://messenger/locale/offline.properties"/>
+ </stringbundleset>
+
+ <script src="chrome://messenger/content/commandglue.js"/>
+ <script src="chrome://messenger/content/mailWindow.js"/>
+ <script src="chrome://messenger/content/messageWindow.js"/>
+ <script src="chrome://messenger/content/accountUtils.js"/>
+ <script src="chrome://messenger/content/mailContextMenus.js"/>
+ <script src="chrome://messenger/content/phishingDetector.js"/>
+ <script src="chrome://communicator/content/contentAreaClick.js"/>
+ <script src="chrome://global/content/nsDragAndDrop.js"/>
+ <script src="chrome://messenger/content/msgViewNavigation.js"/>
+ <script src="chrome://messenger/content/tabmail.js"/>
+
+ <commandset id="mailCommands">
+ <commandset id="mailFileMenuItems"/>
+ <commandset id="mailDownloadCommands"/>
+ <commandset id="mailViewMenuItems"/>
+ <commandset id="mailEditMenuItems"/>
+ <commandset id="mailSearchMenuItems"/>
+ <commandset id="mailGoMenuItems"/>
+ <commandset id="mailMessageMenuItems"/>
+ <commandset id="mailToolbarItems"/>
+ <commandset id="mailGetMsgMenuItems"/>
+ <commandset id="mailMarkMenuItems"/>
+ <commandset id="mailToolsMenuItems"/>
+ <commandset id="mailEditContextMenuItems"/>
+ <commandset id="tasksCommands"/>
+ <commandset id="commandKeys"/>
+ <command id="cmd_close" oncommand="window.close();"/>
+ </commandset>
+
+ <broadcasterset id="mailBroadcasters">
+ <broadcaster id="mailHideMenus" hidden="true"/>
+ <broadcaster id="mailDisableKeys" disabled="true"/>
+ <!-- File Menu -->
+ <broadcaster id="Communicator:WorkMode"/>
+ </broadcasterset>
+
+ <broadcasterset id="mainBroadcasterSet"/>
+
+ <keyset id="mailKeys">
+ <keyset id="tasksKeys"/>
+ <key keycode="VK_ESCAPE" oncommand="window.close();"/>
+ </keyset>
+
+ <popupset id="messagePopupSet">
+ <menupopup id="mailContext"/>
+ <menupopup id="attachmentListContext"/>
+ <menupopup id="copyUrlPopup"/>
+ <menupopup id="messageIdContext"/>
+ <menupopup id="emailAddressPopup"/>
+ <menupopup id="toolbar-context-menu"/>
+ <menupopup id="remoteContentOptions"/>
+ <tooltip id="aHTMLTooltip"
+ onpopupshowing="return FillInHTMLTooltip(document.tooltipNode);"/>
+ <panel id="customizeToolbarSheetPopup"/>
+ </popupset>
+
+ <vbox id="titlebar"/>
+
+ <toolbox id="mail-toolbox">
+ <toolbar id="mail-toolbar-menubar2">
+ <toolbaritem id="menubar-items">
+ <menubar id="mail-menubar"/>
+ </toolbaritem>
+ </toolbar>
+ <toolbar id="msgToolbar"/>
+ <toolbarset id="customToolbars"/>
+ </toolbox>
+
+ <!-- msg header view -->
+<vbox id="messagesBox" flex="1">
+ <notificationbox id="messagepanebox"
+ class="browser-notificationbox"
+ flex="3"
+ persist="collapsed"
+ ondragover="messagepaneObserver.onDragOver(event);"
+ ondrop="messagepaneObserver.onDrop(event);"
+ ondragexit="messagepaneObserver.onDragExit(event);">
+
+ <hbox id="msgHeaderView"/>
+
+ <!-- message view -->
+ <browser id="messagepane"
+ name="messagepane"
+ height="0"
+ flex="1"
+ minwidth="1"
+ minheight="1"
+ context="mailContext"
+ tooltip="aHTMLTooltip"
+ disablesecurity="true"
+ disablehistory="true"
+ autofind="false"
+ type="content"
+ primary="true"
+ onresize="return messagePaneOnResize(event);"
+ onclick="return messagePaneOnClick(event);"/>
+ </notificationbox>
+</vbox>
+
+ <statusbar class="chromeclass-status" id="status-bar"/>
+
+</window>
diff --git a/comm/suite/mailnews/content/messenger.css b/comm/suite/mailnews/content/messenger.css
new file mode 100644
index 0000000000..785c1178e8
--- /dev/null
+++ b/comm/suite/mailnews/content/messenger.css
@@ -0,0 +1,236 @@
+/* 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/. */
+
+/* ===== messenger.css ==================================================
+ == Content specific styles for Messenger.
+ ======================================================================= */
+
+@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+/* ::::: mail xbl bindings ::::: */
+
+description[selectable="true"] {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#extdescription");
+}
+
+descriptionitem {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#descriptionitem");
+}
+
+.descriptionitem-iconic {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#descriptionitem-iconic");
+}
+
+mail-messageid {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-messageid");
+}
+
+mail-messageids-headerfield {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-messageids-headerfield");
+}
+
+mail-emailaddress {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-emailaddress");
+ -moz-user-focus: normal;
+}
+
+mail-emailheaderfield {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-emailheaderfield");
+}
+
+mail-toggle-headerfield {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-toggle-headerfield");
+}
+
+mail-multi-emailHeaderField {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-multi-emailHeaderField");
+}
+
+mail-headerfield {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-headerfield");
+}
+
+mail-urlfield {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-urlfield");
+}
+
+mail-tagfield {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#mail-headerfield-tags");
+}
+
+menupopup[type="folder"] {
+ -moz-binding: url("chrome://messenger/content/folderWidgets.xml#folder-menupopup");
+}
+
+.addrbooksPopup {
+ -moz-binding: url("chrome://messenger/content/addressbook/addrbookWidgets.xml#addrbooks-menupopup");
+}
+
+.map-list {
+ -moz-binding: url("chrome://messenger/content/addressbook/addrbookWidgets.xml#map-list");
+}
+
+#searchTermList > listitem {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#listitem");
+}
+
+searchattribute {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#searchattribute");
+}
+
+searchoperator {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#searchoperator");
+}
+
+searchvalue {
+ display: -moz-deck;
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#searchvalue");
+}
+
+searchterm {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#searchterm");
+}
+
+.ruleaction {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleaction");
+}
+
+.ruleactiontype {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontype-menulist");
+}
+
+.ruleactiontarget[type] {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base");
+}
+
+.ruleactiontarget[type="movemessage"], .ruleactiontarget[type="copymessage"] {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontarget-folder");
+}
+
+.ruleactiontarget[type="addtagtomessage"] {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontarget-tag");
+}
+
+.ruleactiontarget[type="setpriorityto"] {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontarget-priority");
+}
+
+.ruleactiontarget[type="setjunkscore"] {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontarget-junkscore");
+}
+
+.ruleactiontarget[type="forwardmessage"] {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontarget-forwardto");
+}
+
+.ruleactiontarget[type="replytomessage"] {
+ -moz-binding: url("chrome://messenger/content/searchWidgets.xml#ruleactiontarget-replyto");
+}
+
+.folderSummaryPopup
+{
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#folderSummary-popup");
+}
+
+folderSummary
+{
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#folderSummary");
+}
+
+folderSummaryMessage
+{
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#folderSummary-message");
+}
+
+folderSummaryLocation
+{
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#folderSummary-location");
+}
+
+folderSummarySubfoldersSummary
+{
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#folderSummary-subfoldersSummary");
+}
+
+dummy.usesMailWidgets {
+ -moz-binding: url("chrome://messenger/content/mailWidgets.xml#dummy");
+}
+
+/* tabmail */
+
+#tabmail
+{
+ -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail");
+}
+
+.tabmail-tabs {
+ -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-tabs");
+}
+
+.tabmail-arrowscrollbox {
+ -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-arrowscrollbox");
+}
+
+.tabmail-tab {
+ -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-tab");
+}
+
+.tabs-newbutton {
+ -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-new-tab-button");
+}
+
+.tab-close-button,
+.tabs-closebutton {
+ -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-close-tab-button");
+}
+
+.tab-close-button {
+ display: none;
+}
+
+.tabmail-tabs:not([closebuttons="noclose"]):not([closebuttons="closeatend"]) > .tabmail-tab[selected="true"] > .tab-close-button {
+ display: -moz-box;
+}
+
+.tabmail-tabs[closebuttons="alltabs"] .tab-close-button {
+ display: -moz-box;
+}
+
+.tabs-alltabs-popup {
+ /* override toolkit's .menulist-menupopup binding */
+ -moz-binding: url("chrome://messenger/content/tabmail.xml#tabmail-alltabs-popup") ! important;
+}
+
+/* Used for selecting appropriate button for when next to search box */
+
+#button-search {
+ display: -moz-box;
+}
+
+#search-container + #button-search-container > #button-search,
+#wrapper-search-container + toolbarpaletteitem[place="toolbar"] > #button-search-container > #button-search {
+ display: none;
+}
+
+#button-advanced {
+ display: none;
+}
+
+#search-container + #button-search-container > #button-advanced,
+#wrapper-search-container + toolbarpaletteitem[place="toolbar"] > #button-search-container > #button-advanced {
+ display: -moz-box;
+}
+
+/* Wallpaper patch for Bug 517924 */
+
+#expandedHeaderView {
+ overflow-y: auto;
+ overflow-x: hidden;
+ max-height: 14em;
+}
+
+/* Lightning toobar menu button */
+.button-appmenu {
+display: none;
+}
diff --git a/comm/suite/mailnews/content/messenger.xul b/comm/suite/mailnews/content/messenger.xul
new file mode 100644
index 0000000000..42c73a96ec
--- /dev/null
+++ b/comm/suite/mailnews/content/messenger.xul
@@ -0,0 +1,275 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://messenger/skin/mailWindow1.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/threadPane.xul"?>
+<?xul-overlay href="chrome://messenger/content/folderPane.xul"?>
+<?xul-overlay href="chrome://messenger/content/mailWindowOverlay.xul"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
+%messengerDTD;
+]>
+
+<window id="messengerWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ title="&messengerWindow.title;"
+ titlemodifier="&titleModifier.label;"
+ titlemenuseparator="&titleSeparator.label;"
+ onload="OnLoadMessenger()"
+ onunload="OnUnloadMessenger()"
+ onclose="return MailWindowIsClosing();"
+ screenX="10" screenY="10"
+ persist="width height screenX screenY sizemode"
+ toggletoolbar="true"
+ lightweightthemes="true"
+ lightweightthemesfooter="status-bar"
+ macanimationtype="document"
+ drawtitle="true"
+ windowtype="mail:3pane">
+
+<stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/>
+</stringbundleset>
+
+<script src="chrome://messenger/content/commandglue.js"/>
+<script src="chrome://messenger/content/msgViewNavigation.js"/>
+<script src="chrome://messenger/content/mailWindow.js"/>
+<script src="chrome://messenger/content/msgMail3PaneWindow.js"/>
+<script src="chrome://messenger/content/mail3PaneWindowCommands.js"/>
+<script src="chrome://messenger/content/mailContextMenus.js"/>
+<script src="chrome://messenger/content/accountUtils.js"/>
+<script src="chrome://messenger/content/folderPane.js"/>
+<script src="chrome://messenger/content/phishingDetector.js"/>
+<script src="chrome://communicator/content/contentAreaClick.js"/>
+<script src="chrome://global/content/nsDragAndDrop.js"/>
+<script src="chrome://messenger/content/searchBar.js"/>
+<script src="chrome://messenger/content/tabmail.js"/>
+
+<commandset id="mailCommands">
+ <commandset id="mailFileMenuItems"/>
+ <commandset id="mailDownloadCommands"/>
+ <commandset id="mailViewMenuItems"/>
+ <commandset id="mailEditMenuItems"/>
+ <commandset id="mailEditContextMenuItems"/>
+ <commandset id="mailSearchMenuItems"/>
+ <commandset id="mailGoMenuItems"/>
+ <commandset id="mailMessageMenuItems"/>
+ <commandset id="mailToolbarItems"/>
+ <commandset id="mailGetMsgMenuItems"/>
+ <commandset id="mailMarkMenuItems"/>
+ <commandset id="mailToolsMenuItems"/>
+ <commandset id="globalEditMenuItems"/>
+ <commandset id="selectEditMenuItems"/>
+ <commandset id="clipboardEditMenuItems"/>
+ <commandset id="FocusRingUpdate_Mail"
+ commandupdater="true"
+ events="focus"
+ oncommandupdate="FocusRingUpdate_Mail()"/>
+ <commandset id="tasksCommands"/>
+ <command id="cmd_close" oncommand="MsgCloseTabOrWindow();"/>
+</commandset>
+
+<broadcasterset id="mailBroadcasters">
+ <broadcaster id="mailHideMenus"/>
+ <broadcaster id="mailDisableKeys"/>
+ <broadcaster id="mailDisableViewsSearch" disabled="true"/>
+ <!-- File Menu -->
+ <broadcaster id="Communicator:WorkMode"/>
+</broadcasterset>
+
+<broadcasterset id="mainBroadcasterSet"/>
+
+<keyset id="mailKeys">
+ <!-- Tab/F6 Keys -->
+ <key keycode="VK_TAB" oncommand="SwitchPaneFocus(event);" modifiers="control,shift"/>
+ <key keycode="VK_TAB" oncommand="SwitchPaneFocus(event);" modifiers="control"/>
+ <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="control,shift"/>
+ <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="control"/>
+ <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="shift"/>
+ <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);"/>
+
+ <keyset id="tasksKeys"/>
+</keyset>
+
+ <popupset id="mainPopupSet">
+ <menupopup id="mailContext"/>
+ <menupopup id="folderPaneContext"/>
+ <menupopup id="attachmentListContext"/>
+ <tooltip id="attachmentListTooltip"/>
+ <menupopup id="copyUrlPopup"/>
+ <menupopup id="messageIdContext"/>
+ <menupopup id="emailAddressPopup"/>
+ <menupopup id="toolbar-context-menu"/>
+ <tooltip id="folderpopup" class="folderSummaryPopup"/>
+ <tooltip id="aHTMLTooltip"
+ onpopupshowing="return FillInHTMLTooltip(document.tooltipNode);"/>
+ <panel id="customizeToolbarSheetPopup"/>
+ <menupopup id="networkProperties"/>
+ <menupopup id="remoteContentOptions"/>
+ </popupset>
+
+ <vbox id="titlebar"/>
+
+ <toolbox id="mail-toolbox" class="toolbox-top">
+ <toolbar id="mail-toolbar-menubar2"
+ type="menubar">
+ <toolbaritem id="menubar-items">
+ <menubar id="mail-menubar"/>
+ </toolbaritem>
+ </toolbar>
+ <toolbar id="msgToolbar"/>
+ <toolbarset id="customToolbars"/>
+ <toolbar id="searchToolbar"
+ class="chromeclass-toolbar"
+ persist="collapsed"
+ grippytooltiptext="&searchToolbar.tooltip;"
+ toolbarname="&showSearchToolbarCmd.label;"
+ accesskey="&showSearchToolbarCmd.accesskey;"
+ customizable="true"
+ nowindowdrag="true"
+ mode="full"
+ iconsize="small"
+ labelalign="end"
+ defaultmode="full"
+ defaulticonsize="small"
+ defaultlabelalign="end"
+ defaultset="mailviews-container,spring,search-container,button-search-container"
+ context="toolbar-context-menu"/>
+ </toolbox>
+
+ <!-- XXX This extension point (tabmail-container) is only temporary!
+ (See bug 460252 for details.)
+ We will readd a mechanism for sidebar panes in bug 178003.
+ -->
+ <hbox id="tabmail-container" flex="1">
+ <tabmail id="tabmail" flex="1" panelcontainer="tabpanelcontainer">
+ <box id="tabmail-buttons" orientation="horizontal"/>
+ <toolbar id="tabbar-toolbar"
+ xpfe="false"
+ toolboxid="mail-toolbox"
+ toolbarname="&showTabsToolbarCmd.label;"
+ accesskey="&showTabsToolbarCmd.accesskey;"
+ customizable="true"
+ nowindowdrag="true"
+ mode="icons"
+ iconsize="small"
+ labelalign="end"
+ defaultmode="icons"
+ defaulticonsize="small"
+ defaultlabelalign="end"
+ context="toolbar-context-menu"/>
+ <tabpanels id="tabpanelcontainer" flex="1" class="plain" selectedIndex="0">
+ <!-- The main mail three pane frame -->
+ <box id="mailContent" orient="vertical" flex="1">
+ <box id="messengerBox"
+ orient="horizontal"
+ flex="1"
+ minheight="100"
+ height="100"
+ persist="height">
+ <vbox id="folderPaneBox"
+ minwidth="100"
+ width="200"
+ persist="collapsed width hidden">
+ <tree id="folderTree">
+ <treechildren tooltip="folderpopup"/>
+ </tree>
+ </vbox>
+
+ <splitter id="folderpane-splitter"
+ collapse="before"
+ resizeafter="grow"
+ persist="state collapsed"
+ oncommand="MsgToggleFolderPane(false);">
+ <grippy/>
+ </splitter>
+
+ <box id="messagesBox"
+ orient="vertical"
+ flex="1"
+ minwidth="100"
+ width="100"
+ persist="width">
+ <deck id="displayDeck"
+ flex="1"
+ selectedIndex="0"
+ minheight="100"
+ height="100"
+ persist="height"
+ onselect="ObserveDisplayDeckChange(event);">
+ <!-- first panel in displayDeck is Account Central -->
+ <vbox id="accountCentralBox">
+ <iframe name="accountCentralPane"
+ width="150"
+ flex="1"
+ src="about:blank"/>
+ </vbox>
+ <!-- second panel is the threadPane -->
+ <vbox id="threadPaneBox">
+ <tree id="threadTree"
+ treelines="true"
+ keepcurrentinview="true"
+ flex="1"
+ context="mailContext"
+ class="window-focusborder"
+ focusring="false"/>
+ </vbox>
+ <!-- extensions may overlay in additional panels; don't assume that there are only 2! -->
+ </deck>
+
+ <!-- if you change this id, please change GetThreadAndMessagePaneSplitter() and MsgToggleMessagePane() -->
+ <splitter id="threadpane-splitter"
+ collapse="after"
+ persist="state collapsed hidden"
+ collapsed="true"
+ oncommand="MsgToggleMessagePane(false);">
+ <grippy/>
+ </splitter>
+
+ <notificationbox id="messagepanebox"
+ flex="2"
+ minheight="100"
+ height="200"
+ minwidth="100"
+ width="200"
+ persist="height width"
+ class="browser-notificationbox window-focusborder"
+ focusring="false">
+ <hbox id="msgHeaderView"/>
+ <!-- The messagepanewrapper hbox exists to allow extensions
+ to add sidebars to the message pane. -->
+ <hbox id="messagepanewrapper" flex="1">
+ <browser id="messagepane"
+ name="messagepane"
+ height="0"
+ flex="1"
+ minwidth="1"
+ minheight="1"
+ tooltip="aHTMLTooltip"
+ context="mailContext"
+ disablesecurity="true"
+ disablehistory="true"
+ autofind="false"
+ type="content"
+ primary="true"
+ onresize="return messagePaneOnResize(event);"
+ onclick="return messagePaneOnClick(event);"/>
+ </hbox>
+ </notificationbox>
+ </box>
+ </box>
+ </box>
+ </tabpanels>
+ </tabmail>
+ </hbox>
+
+ <statusbar id="status-bar" class="chromeclass-status mailwindow-statusbar"/>
+</window>
diff --git a/comm/suite/mailnews/content/msgFolderPickerOverlay.js b/comm/suite/mailnews/content/msgFolderPickerOverlay.js
new file mode 100644
index 0000000000..b097cd553e
--- /dev/null
+++ b/comm/suite/mailnews/content/msgFolderPickerOverlay.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gMessengerBundle;
+
+// call this from dialog onload() to set the menu item to the correct value
+function MsgFolderPickerOnLoad(pickerID) {
+ var uri = null;
+ try {
+ uri = window.arguments[0].preselectedURI;
+ } catch (ex) {
+ uri = null;
+ }
+
+ if (uri) {
+ // dump("on loading, set titled button to " + uri + "\n");
+
+ // verify that the value we are attempting to
+ // pre-flight the menu with is valid for this
+ // picker type
+ var msgfolder = MailUtils.getExistingFolder(uri);
+ if (!msgfolder) {
+ return;
+ }
+
+ var verifyFunction = null;
+
+ switch (pickerID) {
+ case "msgNewFolderPicker":
+ verifyFunction = msgfolder.canCreateSubfolders;
+ break;
+ case "msgRenameFolderPicker":
+ verifyFunction = msgfolder.canRename;
+ break;
+ default:
+ verifyFunction = msgfolder.canFileMessages;
+ break;
+ }
+
+ if (verifyFunction) {
+ SetFolderPicker(uri, pickerID);
+ }
+ }
+}
+
+function PickedMsgFolder(selection, pickerID) {
+ var selectedUri = selection.getAttribute("id");
+ SetFolderPicker(selectedUri, pickerID);
+}
+
+function SetFolderPickerElement(uri, picker) {
+ var msgfolder = MailUtils.getExistingFolder(uri);
+
+ if (!msgfolder) {
+ return;
+ }
+
+ var selectedValue = null;
+ var serverName;
+
+ if (msgfolder.isServer) {
+ selectedValue = msgfolder.name;
+ } else {
+ if (msgfolder.server) {
+ serverName = msgfolder.server.prettyName;
+ } else {
+ dump("Can't find server for " + uri + "\n");
+ serverName = "???";
+ }
+
+ switch (picker.id) {
+ case "runFiltersFolder":
+ selectedValue = msgfolder.name;
+ break;
+ case "msgTrashFolderPicker":
+ selectedValue = msgfolder.name;
+ break;
+ default:
+ if (!gMessengerBundle) {
+ gMessengerBundle = document.getElementById("bundle_messenger");
+ }
+ selectedValue = gMessengerBundle.getFormattedString(
+ "verboseFolderFormat",
+ [msgfolder.name, serverName]
+ );
+ break;
+ }
+ }
+
+ picker.setAttribute("label", selectedValue);
+ picker.setAttribute("uri", uri);
+}
+
+function SetFolderPicker(uri, pickerID) {
+ SetFolderPickerElement(uri, document.getElementById(pickerID));
+}
diff --git a/comm/suite/mailnews/content/msgHdrViewOverlay.js b/comm/suite/mailnews/content/msgHdrViewOverlay.js
new file mode 100644
index 0000000000..c1f12388e7
--- /dev/null
+++ b/comm/suite/mailnews/content/msgHdrViewOverlay.js
@@ -0,0 +1,1971 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const {GlodaUtils} = ChromeUtils.import("resource:///modules/gloda/utils.js");
+
+
+/* This is where functions related to displaying the headers for a selected message in the
+ message pane live. */
+
+////////////////////////////////////////////////////////////////////////////////////
+// Warning: if you go to modify any of these JS routines please get a code review from
+// scott@scott-macgregor.org. It's critical that the code in here for displaying
+// the message headers for a selected message remain as fast as possible. In particular,
+// right now, we only introduce one reflow per message. i.e. if you click on a message in the thread
+// pane, we batch up all the changes for displaying the header pane (to, cc, attachements button, etc.)
+// and we make a single pass to display them. It's critical that we maintain this one reflow per message
+// view in the message header pane.
+////////////////////////////////////////////////////////////////////////////////////
+
+var gViewAllHeaders = false;
+var gCollectIncoming = false;
+var gCollectOutgoing = false;
+var gCollectNewsgroup = false;
+var gCollapsedHeaderViewMode = false;
+var gCollectAddressTimer = null;
+var gBuildAttachmentsForCurrentMsg = false;
+var gBuildAttachmentPopupForCurrentMsg = true;
+var gBuiltExpandedView = false;
+var gBuiltCollapsedView = false;
+var gMessengerBundle;
+
+// Show the friendly display names for people I know, instead of the name + email address.
+var gShowCondensedEmailAddresses;
+
+var abAddressCollector = null;
+
+// other components may listen to on start header & on end header notifications for each message we display
+// to do that you need to add yourself to our gMessageListeners array with an object that supports the three properties:
+// onStartHeaders, onEndHeaders and onEndAttachments.
+var gMessageListeners = new Array();
+
+// For every possible "view" in the message pane, you need to define the header names you want to
+// see in that view. In addition, include information describing how you want that header field to be
+// presented. i.e. if it's an email address field, if you want a toggle inserted on the node in case
+// of multiple email addresses, etc. We'll then use this static table to dynamically generate header view entries
+// which manipulate the UI.
+// When you add a header to one of these view lists you can specify the following properties:
+// name: the name of the header. i.e. "to", "subject". This must be in lower case and the name of the
+// header is used to help dynamically generate ids for objects in the document. (REQUIRED)
+// useToggle: true if the values for this header are multiple email addresses and you want a
+// a toggle icon to show a short vs. long list (DEFAULT: false)
+// useShortView: (only works on some fields like From). If the field has a long presentation and a
+// short presentation we'll use the short one. i.e. if you are showing the From field and you
+// set this to true, we can show just "John Doe" instead of "John Doe <jdoe@netscape.net>".
+// (DEFAULT: false)
+//
+// outputFunction: this is a method which takes a headerEntry (see the definition below) and a header value
+// This allows you to provide your own methods for actually determining how the header value
+// is displayed. (DEFAULT: updateHeaderValue which just sets the header value on the text node)
+
+// Our first view is the collapsed view. This is very light weight view of the data. We only show a couple
+// fields.
+var gCollapsedHeaderList = [ {name:"subject", outputFunction:updateHeaderValueInTextNode},
+ {name:"from", useToggle:true, useShortView:true, outputFunction:OutputEmailAddresses},
+ {name:"date", outputFunction:updateHeaderValueInTextNode}];
+
+// We also have an expanded header view. This shows many of your more common (and useful) headers.
+var gExpandedHeaderList = [ {name:"subject"},
+ {name:"from", useToggle:true, outputFunction:OutputEmailAddresses},
+ {name:"sender", outputFunction:OutputEmailAddresses},
+ {name:"reply-to", useToggle:true, outputFunction:OutputEmailAddresses},
+ {name:"date"},
+ {name:"to", useToggle:true, outputFunction:OutputEmailAddresses},
+ {name:"cc", useToggle:true, outputFunction:OutputEmailAddresses},
+ {name:"bcc", useToggle:true, outputFunction:OutputEmailAddresses},
+ {name:"newsgroups", outputFunction:OutputNewsgroups},
+ {name:"references", outputFunction:OutputMessageIds},
+ {name:"followup-to", outputFunction:OutputNewsgroups},
+ {name:"content-base"},
+ {name:"tags"} ];
+
+// These are all the items that use a mail-multi-emailHeaderField widget and
+// therefore may require updating if the address book changes.
+const gEmailAddressHeaderNames = ["from", "reply-to", "to", "cc", "bcc"];
+
+// Now, for each view the message pane can generate, we need a global table of headerEntries. These
+// header entry objects are generated dynamically based on the static data in the header lists (see above)
+// and elements we find in the DOM based on properties in the header lists.
+var gCollapsedHeaderView = {};
+var gExpandedHeaderView = {};
+
+// currentHeaderData --> this is an array of header name and value pairs for the currently displayed message.
+// it's purely a data object and has no view information. View information is contained in the view objects.
+// for a given entry in this array you can ask for:
+// .headerName ---> name of the header (i.e. 'to'). Always stored in lower case
+// .headerValue --> value of the header "johndoe@netscape.net"
+var currentHeaderData = {};
+
+// For the currently displayed message, we store all the attachment data. When displaying a particular
+// view, it's up to the view layer to extract this attachment data and turn it into something useful.
+// For a given entry in the attachments list, you can ask for the following properties:
+// .contentType --> the content type of the attachment
+// url --> an imap, or mailbox url which can be used to fetch the message
+// uri --> an RDF URI which refers to the message containig the attachment
+// isExternalAttachment --> boolean flag stating whether the attachment is external or not.
+var currentAttachments = new Array();
+
+const nsIAbDirectory = Ci.nsIAbDirectory;
+const nsIAbListener = Ci.nsIAbListener;
+const nsIAbCard = Ci.nsIAbCard;
+
+// createHeaderEntry --> our constructor method which creates a header Entry
+// based on an entry in one of the header lists. A header entry is different from a header list.
+// a header list just describes how you want a particular header to be presented. The header entry
+// actually has knowledge about the DOM and the actual DOM elements associated with the header.
+// prefix --> the name of the view (i.e. "collapsed", "expanded")
+// headerListInfo --> entry from a header list.
+function createHeaderEntry(prefix, headerListInfo)
+{
+ var useShortView = false;
+ var partialIDName = prefix + headerListInfo.name;
+ this.enclosingBox = document.getElementById(partialIDName + 'Box');
+ this.textNode = document.getElementById(partialIDName + 'Value');
+ this.isNewHeader = false;
+ this.isValid = false;
+
+ if ("useShortView" in headerListInfo)
+ {
+ useShortView = headerListInfo.useShortView;
+ if (useShortView)
+ this.enclosingBox = this.textNode;
+ else
+ this.enclosingBox.emailAddressNode = this.textNode;
+ }
+
+ if ("useToggle" in headerListInfo)
+ {
+ this.useToggle = headerListInfo.useToggle;
+ if (this.useToggle) // find the toggle icon in the document
+ {
+ this.toggleIcon = this.enclosingBox.toggleIcon;
+ this.longTextNode = this.enclosingBox.longEmailAddresses;
+ this.textNode = this.enclosingBox.emailAddresses;
+ }
+ }
+ else
+ this.useToggle = false;
+
+ if (this.textNode)
+ this.textNode.useShortView = useShortView;
+
+ if ("outputFunction" in headerListInfo)
+ this.outputFunction = headerListInfo.outputFunction;
+ else
+ this.outputFunction = updateHeaderValue;
+
+ // Stash this so that the <mail-multi-emailheaderfield/> binding can
+ // later attach it to any <mail-emailaddress> tags it creates for later
+ // extraction and use by UpdateEmailNodeDetails.
+ this.enclosingBox.headerName = headerListInfo.name;
+
+}
+
+function initializeHeaderViewTables()
+{
+ // iterate over each header in our header list arrays and create header entries
+ // for each one. These header entries are then stored in the appropriate header table
+ for (let index = 0; index < gCollapsedHeaderList.length; index++)
+ {
+ gCollapsedHeaderView[gCollapsedHeaderList[index].name] =
+ new createHeaderEntry('collapsed', gCollapsedHeaderList[index]);
+ }
+
+ for (let index = 0; index < gExpandedHeaderList.length; index++)
+ {
+ var headerName = gExpandedHeaderList[index].name;
+ gExpandedHeaderView[headerName] = new createHeaderEntry('expanded', gExpandedHeaderList[index]);
+ }
+
+ var extraHeaders = Services.prefs.getCharPref("mailnews.headers.extraExpandedHeaders").match(/[^ ]+/g);
+ if (extraHeaders) {
+ for (let index = 0; index < extraHeaders.length; index++)
+ {
+ let extraHeader = extraHeaders[index];
+ gExpandedHeaderView[extraHeader.toLowerCase()] = new createNewHeaderView(extraHeader, extraHeader + ':');
+ }
+ }
+
+ if (Services.prefs.getBoolPref("mailnews.headers.showOrganization"))
+ {
+ let organizationEntry = {name:"organization", outputFunction:updateHeaderValue};
+ gExpandedHeaderView[organizationEntry.name] = new createHeaderEntry('expanded', organizationEntry);
+ }
+
+ if (Services.prefs.getBoolPref("mailnews.headers.showUserAgent"))
+ {
+ let userAgentEntry = {name:"user-agent", outputFunction:updateHeaderValue};
+ gExpandedHeaderView[userAgentEntry.name] = new createHeaderEntry('expanded', userAgentEntry);
+ }
+
+ if (Services.prefs.getBoolPref("mailnews.headers.showMessageId"))
+ {
+ let messageIdEntry = {name:"message-id", outputFunction:OutputMessageIds};
+ gExpandedHeaderView[messageIdEntry.name] = new createHeaderEntry('expanded', messageIdEntry);
+ }
+}
+
+function OnLoadMsgHeaderPane()
+{
+ // load any preferences that at are global with regards to
+ // displaying a message...
+ gCollectIncoming = Services.prefs.getBoolPref("mail.collect_email_address_incoming");
+ gCollectNewsgroup = Services.prefs.getBoolPref("mail.collect_email_address_newsgroup");
+ gCollectOutgoing = Services.prefs.getBoolPref("mail.collect_email_address_outgoing");
+ gShowCondensedEmailAddresses = Services.prefs.getBoolPref("mail.showCondensedAddresses");
+
+ Services.prefs.addObserver("mail.showCondensedAddresses", MsgHdrViewObserver);
+ Services.prefs.addObserver("mail.show_headers", MsgHdrViewObserver);
+ Services.prefs.addObserver("mailnews.display.html_as", MsgHdrViewObserver);
+ Services.prefs.addObserver("mail.inline_attachments", MsgHdrViewObserver);
+
+ initializeHeaderViewTables();
+
+ // Add an address book listener so we can update the header view when things
+ // change.
+ MailServices.ab.addAddressBookListener(AddressBookListener,
+ Ci.nsIAbListener.all);
+
+ var toggleHeaderView = GetHeaderPane();
+ var initialCollapsedSetting = toggleHeaderView.getAttribute("state");
+ if (initialCollapsedSetting == "true")
+ gCollapsedHeaderViewMode = true;
+
+ // dispatch an event letting any listeners know that we have loaded the message pane
+ toggleHeaderView.dispatchEvent(new Event('messagepane-loaded',
+ { bubbles: false, cancelable: true }));
+}
+
+function OnUnloadMsgHeaderPane()
+{
+ Services.prefs.removeObserver("mail.showCondensedAddresses", MsgHdrViewObserver);
+ Services.prefs.removeObserver("mail.show_headers", MsgHdrViewObserver);
+ Services.prefs.removeObserver("mailnews.display.html_as", MsgHdrViewObserver);
+ Services.prefs.removeObserver("mail.inline_attachments", MsgHdrViewObserver);
+
+ MailServices.ab.removeAddressBookListener(AddressBookListener);
+
+ // dispatch an event letting any listeners know that we have unloaded the message pane
+ GetHeaderPane().dispatchEvent(new Event('messagepane-unloaded',
+ { bubbles: false, cancelable: true }));
+}
+
+var MsgHdrViewObserver = {
+ observe: function(subject, topic, prefName) {
+ // Verify that we're changing mail pane config prefs.
+ if (topic == "nsPref:changed") {
+ if (prefName == "mail.showCondensedAddresses") {
+ gShowCondensedEmailAddresses =
+ Services.prefs.getBoolPref("mail.showCondensedAddresses");
+ ReloadMessage();
+ } else if (prefName == "mail.show_headers" ||
+ prefName == "mailnews.display.html_as" ||
+ prefName == "mail.inline_attachments") {
+ ReloadMessage();
+ }
+ }
+ }
+};
+
+var AddressBookListener =
+{
+ onItemAdded: function(aParentDir, aItem) {
+ OnAddressBookDataChanged(nsIAbListener.itemAdded,
+ aParentDir, aItem);
+ },
+ onItemRemoved: function(aParentDir, aItem) {
+ OnAddressBookDataChanged(aItem instanceof nsIAbCard ?
+ nsIAbListener.directoryItemRemoved :
+ nsIAbListener.directoryRemoved,
+ aParentDir, aItem);
+ },
+ onItemPropertyChanged: function(aItem, aProperty, aOldValue, aNewValue) {
+ // We only need updates for card changes, address book and mailing list
+ // ones don't affect us here.
+ if (aItem instanceof nsIAbCard)
+ OnAddressBookDataChanged(nsIAbListener.itemChanged, null, aItem);
+ }
+};
+
+function OnAddressBookDataChanged(aAction, aParentDir, aItem)
+{
+ gEmailAddressHeaderNames.forEach(function (aHeaderName)
+ {
+ var headerEntry = null;
+
+ // Ensure both collapsed and expanded are updated in case we toggle
+ // between the two.
+ if (aHeaderName in gCollapsedHeaderView)
+ {
+ headerEntry = gCollapsedHeaderView[aHeaderName];
+ if (headerEntry)
+ headerEntry.enclosingBox.updateExtraAddressProcessing(aAction,
+ aParentDir,
+ aItem);
+ }
+ if (aHeaderName in gExpandedHeaderView)
+ {
+ headerEntry = gExpandedHeaderView[aHeaderName];
+ if (headerEntry)
+ headerEntry.enclosingBox.updateExtraAddressProcessing(aAction,
+ aParentDir,
+ aItem);
+ }
+ });
+}
+
+// The messageHeaderSink is the class that gets notified of a message's headers as we display the message
+// through our mime converter.
+
+var messageHeaderSink = {
+ QueryInterface: XPCOMUtils.generateQI(
+ [Ci.nsIMsgHeaderSink]),
+ onStartHeaders: function()
+ {
+ this.mSaveHdr = null;
+ // clear out any pending collected address timers...
+ if (gCollectAddressTimer)
+ {
+ clearTimeout(gCollectAddressTimer);
+ gCollectAddressTimer = null;
+ }
+
+ // every time we start to redisplay a message, check the view all headers pref....
+ var showAllHeadersPref = Services.prefs.getIntPref("mail.show_headers");
+ if (showAllHeadersPref == 2)
+ {
+ gViewAllHeaders = true;
+ }
+ else
+ {
+ if (gViewAllHeaders) // if we currently are in view all header mode, rebuild our header view so we remove most of the header data
+ {
+ hideHeaderView(gExpandedHeaderView);
+ RemoveNewHeaderViews(gExpandedHeaderView);
+ gExpandedHeaderView = {};
+ initializeHeaderViewTables();
+ }
+
+ gViewAllHeaders = false;
+ }
+
+ ClearCurrentHeaders();
+ gBuiltExpandedView = false;
+ gBuiltCollapsedView = false;
+ gBuildAttachmentsForCurrentMsg = false;
+ gBuildAttachmentPopupForCurrentMsg = true;
+ ClearAttachmentList();
+ ClearEditMessageBox("editDraftBox");
+ ClearEditMessageBox("editTemplateBox");
+ gMessageNotificationBar.clearMsgNotifications();
+
+ for (let index in gMessageListeners)
+ gMessageListeners[index].onStartHeaders();
+ },
+
+ onEndHeaders: function()
+ {
+ ClearHeaderView(gCollapsedHeaderView);
+ ClearHeaderView(gExpandedHeaderView);
+
+ EnsureSubjectValue(); // make sure there is a subject even if it's empty so we'll show the subject and the twisty
+
+ // Load feed web page if so configured. This entry point works for
+ // messagepane loads in 3pane folder tab, 3pane message tab, and the
+ // standalone message window.
+ if (!FeedMessageHandler.shouldShowSummary(gMessageDisplay.displayedMessage, false))
+ FeedMessageHandler.setContent(gMessageDisplay.displayedMessage, false);
+
+ ShowMessageHeaderPane();
+ UpdateMessageHeaders();
+ ShowEditMessageBox("editDraftBox", Ci.nsMsgFolderFlags.Drafts);
+ ShowEditMessageBox("editTemplateBox", Ci.nsMsgFolderFlags.Templates);
+
+ for (let index in gMessageListeners)
+ gMessageListeners[index].onEndHeaders();
+ },
+
+ processHeaders: function(headerNameEnumerator, headerValueEnumerator, dontCollectAddress)
+ {
+ this.onStartHeaders();
+
+ const kMailboxSeparator = ", ";
+ var index = 0;
+ while (headerNameEnumerator.hasMore())
+ {
+ var header = new Object;
+ header.headerValue = headerValueEnumerator.getNext();
+ header.headerName = headerNameEnumerator.getNext();
+
+ // For consistency's sake, let us force all header names to be lower
+ // case so we don't have to worry about looking for: Cc and CC, etc.
+ var lowerCaseHeaderName = header.headerName.toLowerCase();
+
+ // If we have an x-mailer, x-mimeole, or x-newsreader string,
+ // put it in the user-agent slot which we know how to handle already.
+ if (/^x-(mailer|mimeole|newsreader)$/.test(lowerCaseHeaderName))
+ lowerCaseHeaderName = "user-agent";
+
+ if (this.mDummyMsgHeader)
+ {
+ if (lowerCaseHeaderName == "from")
+ this.mDummyMsgHeader.author = header.headerValue;
+ else if (lowerCaseHeaderName == "to")
+ this.mDummyMsgHeader.recipients = header.headerValue;
+ else if (lowerCaseHeaderName == "cc")
+ this.mDummyMsgHeader.ccList = header.headerValue;
+ else if (lowerCaseHeaderName == "subject")
+ this.mDummyMsgHeader.subject = header.headerValue;
+ else if (lowerCaseHeaderName == "reply-to")
+ this.mDummyMsgHeader.replyTo = header.headerValue;
+ else if (lowerCaseHeaderName == "message-id")
+ this.mDummyMsgHeader.messageId = header.headerValue;
+ else if (lowerCaseHeaderName == "list-post")
+ this.mDummyMsgHeader.listPost = header.headerValue;
+ else if (lowerCaseHeaderName == "date")
+ this.mDummyMsgHeader.date = Date.parse(header.headerValue) * 1000;
+ }
+
+ // We emit both the original, raw date header and a localized version.
+ // Pretend that the localized version is the real version.
+ if (lowerCaseHeaderName == "date")
+ continue;
+ if (lowerCaseHeaderName == "x-mozilla-localizeddate")
+ {
+ lowerCaseHeaderName = "date";
+ header.headerName = "Date";
+ }
+
+ // according to RFC 2822, certain headers
+ // can occur "unlimited" times
+ if (lowerCaseHeaderName in currentHeaderData)
+ {
+ // sometimes, you can have multiple To or Cc lines....
+ // in this case, we want to append these headers into one.
+ if (lowerCaseHeaderName == 'to' || lowerCaseHeaderName == 'cc')
+ currentHeaderData[lowerCaseHeaderName].headerValue = currentHeaderData[lowerCaseHeaderName].headerValue + ',' + header.headerValue;
+ else
+ {
+ // use the index to create a unique header name like:
+ // received5, received6, etc
+ currentHeaderData[lowerCaseHeaderName + index++] = header;
+ }
+ }
+ else
+ currentHeaderData[lowerCaseHeaderName] = header;
+
+ if (lowerCaseHeaderName == "from")
+ {
+ if (header.headerValue)
+ {
+ try
+ {
+ var createCard = (gCollectIncoming && !dontCollectAddress) || (gCollectNewsgroup && dontCollectAddress);
+ if (createCard || gCollectOutgoing)
+ {
+ // collect, add card if doesn't exist and gCollectOutgoing is set,
+ // otherwise only update existing cards, unknown preferred send format
+ gCollectAddressTimer = setTimeout(collectAddresses,
+ 2000,
+ header.headerValue,
+ createCard);
+ }
+ }
+ catch(ex) {}
+ }
+ } // if lowerCaseHeaderName == "from"
+ } // while we have more headers to parse
+
+ // process message tags as if they were headers in the message
+ SetTagHeader();
+
+ if (("from" in currentHeaderData) && ("sender" in currentHeaderData))
+ {
+ var senderMailbox = kMailboxSeparator +
+ MailServices.headerParser.extractHeaderAddressMailboxes(
+ currentHeaderData.sender.headerValue) + kMailboxSeparator;
+ var fromMailboxes = kMailboxSeparator +
+ MailServices.headerParser.extractHeaderAddressMailboxes(
+ currentHeaderData.from.headerValue) + kMailboxSeparator;
+ if (fromMailboxes.includes(senderMailbox))
+ delete currentHeaderData.sender;
+ }
+
+ this.onEndHeaders();
+ },
+
+ handleAttachment: function(contentType, url, displayName, uri,
+ isExternalAttachment)
+ {
+ this.skipAttachment = true;
+
+ // Don't show vcards as external attachments in the UI. libmime already
+ // renders them inline.
+ try
+ {
+ if (!this.mSaveHdr)
+ this.mSaveHdr = messenger.msgHdrFromURI(uri);
+ }
+ catch (ex) {}
+ if (contentType == "text/x-vcard")
+ {
+ var inlineAttachments = Services.prefs.getBoolPref("mail.inline_attachments");
+ var displayHtmlAs = Services.prefs.getIntPref("mailnews.display.html_as");
+ if (inlineAttachments && !displayHtmlAs)
+ return;
+ }
+
+ var size = null;
+ if (isExternalAttachment)
+ {
+ var file = GetFileFromString(url);
+ if (file && file.exists())
+ size = file.fileSize;
+ else
+ dump("Couldn't open external attachment!");
+ }
+
+ currentAttachments.push(new createNewAttachmentInfo(contentType,
+ url,
+ displayName,
+ uri,
+ isExternalAttachment,
+ size));
+ this.skipAttachment = false;
+
+ // If we have an attachment, set the nsMsgMessageFlags.Attachment flag
+ // on the hdr to cause the "message with attachment" icon to show up
+ // in the thread pane.
+ // We only need to do this on the first attachment.
+ var numAttachments = currentAttachments.length;
+ if (numAttachments == 1) {
+ // we also have to enable the Message/Attachments menuitem
+ var node = document.getElementById("msgAttachmentMenu");
+ if (node)
+ node.removeAttribute("disabled");
+
+ try {
+ // convert the uri into a hdr
+ this.mSaveHdr.markHasAttachments(true);
+ }
+ catch (ex) {
+ dump("ex = " + ex + "\n");
+ }
+ }
+ },
+
+ addAttachmentField: function(aField, aValue)
+ {
+ if (this.skipAttachment)
+ return;
+
+ let last = currentAttachments[currentAttachments.length - 1];
+ if (aField == "X-Mozilla-PartSize" && !last.isExternalAttachment &&
+ last.contentType != "text/x-moz-deleted")
+ {
+ let size = parseInt(aValue);
+ // libmime returns -1 if it never managed to figure out the size.
+ if (size != -1)
+ last.size = size;
+ }
+ else if (aField == "X-Mozilla-PartDownloaded" && aValue == "0")
+ {
+ // We haven't downloaded the attachment, so any size we get from
+ // libmime is almost certainly inaccurate. Just get rid of it. (Note:
+ // this relies on the fact that PartDownloaded comes after PartSize from
+ // the MIME emitter.)
+ last.size = null;
+ }
+ },
+
+ onEndAllAttachments: function()
+ {
+ // AddSaveAllAttachmentsMenu();
+ if (gCollapsedHeaderViewMode)
+ displayAttachmentsForCollapsedView();
+ else
+ displayAttachmentsForExpandedView();
+
+ for (let index in gMessageListeners) {
+ if ("onEndAttachments" in gMessageListeners[index])
+ gMessageListeners[index].onEndAttachments();
+ }
+ },
+
+ onEndMsgDownload: function(url)
+ {
+ // if we don't have any attachments, turn off the attachments flag
+ if (!this.mSaveHdr)
+ {
+ var messageUrl = url.QueryInterface(Ci.nsIMsgMessageUrl);
+ try
+ {
+ this.mSaveHdr = messenger.msgHdrFromURI(messageUrl.uri);
+ }
+ catch (ex) {}
+
+ }
+ if (!currentAttachments.length && this.mSaveHdr)
+ this.mSaveHdr.markHasAttachments(false);
+
+ let browser = getBrowser();
+ if (currentAttachments.length &&
+ Services.prefs.getBoolPref("mail.inline_attachments") &&
+ this.mSaveHdr && gFolderDisplay.selectedMessageIsFeed &&
+ browser && browser.contentDocument && browser.contentDocument.body) {
+ for (let img of browser.contentDocument.body.getElementsByClassName("moz-attached-image")) {
+ for (let attachment of currentAttachments) {
+ let partID = img.src.split("&part=")[1];
+ partID = partID ? partID.split("&")[0] : null;
+ if (attachment.partID && partID == attachment.partID) {
+ img.src = attachment.url;
+ break;
+ }
+ }
+ }
+ }
+
+ OnMsgParsed(url);
+ },
+
+ onEndMsgHeaders: function(url)
+ {
+ OnMsgLoaded(url);
+ },
+
+ onMsgHasRemoteContent: function(aMsgHdr, aContentURI, aCanOverride)
+ {
+ gMessageNotificationBar.setRemoteContentMsg(aMsgHdr, aContentURI, aCanOverride);
+ },
+
+ mSecurityInfo : null,
+ mSaveHdr: null,
+ get securityInfo()
+ {
+ return this.mSecurityInfo;
+ },
+ set securityInfo(aSecurityInfo)
+ {
+ this.mSecurityInfo = aSecurityInfo;
+ },
+
+ mDummyMsgHeader: null,
+
+ get dummyMsgHeader()
+ {
+ if (!this.mDummyMsgHeader)
+ this.mDummyMsgHeader = new nsDummyMsgHeader();
+ return this.mDummyMsgHeader;
+ },
+ mProperties: null,
+ get properties()
+ {
+ if (!this.mProperties)
+ this.mProperties = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag2);
+ return this.mProperties;
+ },
+
+ resetProperties: function()
+ {
+ this.mProperties = null;
+ }
+};
+
+// Private method which generates a space delimited list of tag keys for the
+// current message. This list is then stored in currentHeaderData["tags"].
+function SetTagHeader()
+{
+ // it would be nice if we passed in the msgHdr from the back end
+ var msgHdr;
+ try
+ {
+ msgHdr = gDBView.hdrForFirstSelectedMessage;
+ }
+ catch (ex)
+ {
+ return; // no msgHdr to add our tags to
+ }
+
+ // get the list of known tags
+ var tagArray = MailServices.tags.getAllTags();
+ var tagKeys = {};
+ for (var tagInfo of tagArray)
+ if (tagInfo.tag)
+ tagKeys[tagInfo.key] = true;
+
+ // extract the tag keys from the msgHdr
+ var msgKeyArray = msgHdr.getStringProperty("keywords").split(" ");
+
+ // attach legacy label to the front if not already there
+ var label = msgHdr.label;
+ if (label)
+ {
+ var labelKey = "$label" + label;
+ if (!msgKeyArray.includes(labelKey))
+ msgKeyArray.unshift(labelKey);
+ }
+
+ // Rebuild the keywords string with just the keys that are actual tags or
+ // legacy labels and not other keywords like Junk and NonJunk.
+ // Retain their order, though, with the label as oldest element.
+ for (let i = msgKeyArray.length - 1; i >= 0; --i)
+ if (!(msgKeyArray[i] in tagKeys))
+ msgKeyArray.splice(i, 1); // remove non-tag key
+ var msgKeys = msgKeyArray.join(" ");
+
+ if (msgKeys)
+ currentHeaderData.tags = {headerName: "tags", headerValue: msgKeys};
+ else // no more tags, so clear out the header field
+ delete currentHeaderData.tags;
+}
+
+function EnsureSubjectValue()
+{
+ if (!('subject' in currentHeaderData))
+ {
+ var foo = new Object;
+ foo.headerValue = "";
+ foo.headerName = 'subject';
+ currentHeaderData[foo.headerName] = foo;
+ }
+}
+
+// Private method used by messageHeaderSink::processHeaders.
+function collectAddresses(aAddresses, aCreateCard)
+{
+ if (!abAddressCollector)
+ abAddressCollector = Cc["@mozilla.org/addressbook/services/addressCollector;1"]
+ .getService(Ci.nsIAbAddressCollector);
+ var sendFormat = Ci.nsIAbPreferMailFormat.unknown;
+ abAddressCollector.collectAddress(aAddresses, aCreateCard, sendFormat);
+}
+
+// Public method called by the tag front end code when the tags for the selected
+// message has changed.
+function OnTagsChange()
+{
+ // rebuild the tag headers
+ SetTagHeader();
+
+ // now update the expanded header view to rebuild the tags,
+ // and then show or hide the tag header box.
+ if (gBuiltExpandedView)
+ {
+ var headerEntry = gExpandedHeaderView.tags;
+ if (headerEntry)
+ {
+ headerEntry.valid = ("tags" in currentHeaderData);
+ if (headerEntry.valid)
+ headerEntry.outputFunction(headerEntry, currentHeaderData.tags.headerValue);
+
+ // if we are showing the expanded header view then we may need to collapse or
+ // show the tag header box...
+ if (!gCollapsedHeaderViewMode)
+ headerEntry.enclosingBox.collapsed = !headerEntry.valid;
+ }
+ }
+}
+
+// flush out any local state being held by a header entry for a given
+// table
+function ClearHeaderView(headerTable)
+{
+ for (let index in headerTable)
+ {
+ let headerEntry = headerTable[index];
+ if (headerEntry.useToggle)
+ {
+ headerEntry.enclosingBox.clearHeaderValues();
+ }
+
+ headerEntry.valid = false;
+ }
+}
+
+// make sure that any valid header entry in the table is collapsed
+function hideHeaderView(headerTable)
+{
+ for (let index in headerTable)
+ {
+ headerTable[index].enclosingBox.collapsed = true;
+ }
+}
+
+// make sure that any valid header entry in the table specified is
+// visible
+function showHeaderView(headerTable)
+{
+ for (let index in headerTable)
+ {
+ let headerEntry = headerTable[index];
+ if (headerEntry.valid)
+ {
+ headerEntry.enclosingBox.collapsed = false;
+ }
+ else // if the entry is invalid, always make sure it's collapsed
+ headerEntry.enclosingBox.collapsed = true;
+ }
+}
+
+// make sure the appropriate fields within the currently displayed view header mode
+// are collapsed or visible...
+function updateHeaderViews()
+{
+ if (gCollapsedHeaderViewMode)
+ {
+ showHeaderView(gCollapsedHeaderView);
+ displayAttachmentsForCollapsedView();
+ }
+ else
+ {
+ showHeaderView(gExpandedHeaderView);
+ displayAttachmentsForExpandedView();
+ }
+}
+
+function ToggleHeaderView()
+{
+ var expandedNode = document.getElementById("expandedHeaderView");
+ var collapsedNode = document.getElementById("collapsedHeaderView");
+
+ if (gCollapsedHeaderViewMode)
+ {
+ gCollapsedHeaderViewMode = false;
+ // hide the current view
+ hideHeaderView(gCollapsedHeaderView);
+ // update the current view
+ UpdateMessageHeaders();
+
+ // now uncollapse / collapse the right views
+ expandedNode.collapsed = false;
+ collapsedNode.collapsed = true;
+ }
+ else
+ {
+ gCollapsedHeaderViewMode = true;
+ // hide the current view
+ hideHeaderView(gExpandedHeaderView);
+ // update the current view
+ UpdateMessageHeaders();
+
+ // now uncollapse / collapse the right views
+ collapsedNode.collapsed = false;
+ expandedNode.collapsed = true;
+ }
+
+ var toggleHeaderView = GetHeaderPane();
+ if (gCollapsedHeaderViewMode)
+ toggleHeaderView.setAttribute("state", "true");
+ else
+ toggleHeaderView.setAttribute("state", "false");
+}
+
+// default method for updating a header value into a header entry
+function updateHeaderValue(headerEntry, headerValue)
+{
+ headerEntry.enclosingBox.headerValue = headerValue;
+}
+
+function updateHeaderValueInTextNode(headerEntry, headerValue)
+{
+ headerEntry.textNode.value = headerValue;
+}
+
+function createNewHeaderView(headerName, label)
+{
+ var idName = 'expanded' + headerName + 'Box';
+ var newHeader = document.createElement("mail-headerfield");
+
+ newHeader.setAttribute('id', idName);
+ newHeader.setAttribute('label', label);
+ // all mail-headerfield elements are keyword related
+ newHeader.setAttribute('keywordrelated','true');
+ newHeader.collapsed = true;
+
+ // this new element needs to be inserted into the view...
+ var topViewNode = document.getElementById('expandedHeaders');
+
+ topViewNode.appendChild(newHeader);
+
+ this.enclosingBox = newHeader;
+ this.isNewHeader = true;
+ this.isValid = false;
+ this.useToggle = false;
+ this.outputFunction = updateHeaderValue;
+}
+
+/**
+ * Removes all non-predefined header nodes from the view.
+ *
+ * @param aHeaderTable Table of header entries.
+ */
+function RemoveNewHeaderViews(aHeaderTable)
+{
+ for (let index in aHeaderTable)
+ {
+ let headerEntry = aHeaderTable[index];
+ if (headerEntry.isNewHeader)
+ headerEntry.enclosingBox.remove();
+ }
+}
+
+// UpdateMessageHeaders: Iterate through all the current header data we received from mime for this message
+// for each header entry table, see if we have a corresponding entry for that header. i.e. does the particular
+// view care about this header value. if it does then call updateHeaderEntry
+function UpdateMessageHeaders()
+{
+ // iterate over each header we received and see if we have a matching entry in each
+ // header view table...
+
+ for (let headerName in currentHeaderData)
+ {
+ let headerField = currentHeaderData[headerName];
+ let headerEntry = null;
+
+ if (headerName == "subject")
+ {
+ try {
+ if (gDBView.keyForFirstSelectedMessage == nsMsgKey_None)
+ {
+ let folder = null;
+ if (gCurrentFolderUri)
+ folder = MailUtils.getFolderForURI(gCurrentFolderUri);
+ setTitleFromFolder(folder, headerField.headerValue);
+ }
+ } catch (ex) {}
+ }
+
+ if (gCollapsedHeaderViewMode && !gBuiltCollapsedView)
+ {
+ if (headerName in gCollapsedHeaderView)
+ headerEntry = gCollapsedHeaderView[headerName];
+ }
+ else if (!gCollapsedHeaderViewMode && !gBuiltExpandedView)
+ {
+ if (headerName in gExpandedHeaderView)
+ headerEntry = gExpandedHeaderView[headerName];
+
+ if (!headerEntry && gViewAllHeaders)
+ {
+ // for view all headers, if we don't have a header field for this value....cheat and create one....then
+ // fill in a headerEntry
+ if (headerName == "message-id" || headerName == "in-reply-to")
+ {
+ let messageIdEntry = {name:headerName, outputFunction:OutputMessageIds};
+ gExpandedHeaderView[headerName] = new createHeaderEntry('expanded', messageIdEntry);
+ }
+ else
+ {
+ gExpandedHeaderView[headerName] = new createNewHeaderView(headerName,
+ currentHeaderData[headerName].headerName + ':');
+ }
+
+ headerEntry = gExpandedHeaderView[headerName];
+ }
+ } // if we are in expanded view....
+
+ if (headerEntry)
+ {
+ let show = Services.prefs.getBoolPref("mailnews.headers.showReferences");
+ if (headerName == "references" &&
+ !(gViewAllHeaders || show ||
+ (gDBView.msgFolder && gDBView.msgFolder.server.type == "nntp")))
+ {
+ // hide references header if view all headers mode isn't selected, the pref show references is
+ // deactivated and the currently displayed message isn't a newsgroup posting
+ headerEntry.valid = false;
+ }
+ else
+ {
+ headerEntry.outputFunction(headerEntry, headerField.headerValue);
+ headerEntry.valid = true;
+ }
+ }
+ }
+
+ if (gCollapsedHeaderViewMode)
+ gBuiltCollapsedView = true;
+ else
+ gBuiltExpandedView = true;
+
+ // now update the view to make sure the right elements are visible
+ updateHeaderViews();
+}
+
+function ClearCurrentHeaders()
+{
+ currentHeaderData = {};
+ currentAttachments = new Array();
+}
+
+function IsListPost()
+{
+ if ("list-post" in currentHeaderData)
+ return /<mailto:.+@.+>/.test(currentHeaderData["list-post"].headerValue);
+
+ return false;
+}
+
+function ShowMessageHeaderPane()
+{
+ var node;
+ if (gCollapsedHeaderViewMode)
+ {
+ node = document.getElementById("collapsedHeaderView");
+ if (node)
+ node.collapsed = false;
+ }
+ else
+ {
+ node = document.getElementById("expandedHeaderView");
+ if (node)
+ node.collapsed = false;
+ }
+
+ /* workaround for 39655 */
+ if (gFolderJustSwitched)
+ {
+ let el = GetHeaderPane();
+ el.setAttribute("style", el.getAttribute("style"));
+ gFolderJustSwitched = false;
+ }
+
+ document.commandDispatcher.updateCommands("message-header-pane");
+}
+
+function HideMessageHeaderPane()
+{
+ var node = document.getElementById("collapsedHeaderView");
+ if (node)
+ node.collapsed = true;
+
+ node = document.getElementById("expandedHeaderView");
+ if (node)
+ node.collapsed = true;
+
+ // we also have to disable the Message/Attachments menuitem
+ node = document.getElementById("msgAttachmentMenu");
+ if (node)
+ node.setAttribute("disabled", "true");
+
+ document.commandDispatcher.updateCommands("message-header-pane");
+}
+
+function OutputNewsgroups(headerEntry, headerValue)
+{
+ headerValue = headerValue.replace(/,/g,", ");
+ updateHeaderValue(headerEntry, headerValue);
+}
+
+// take string of message-ids separated by whitespace, split it
+// into message-ids and send them together with the index number
+// to the corresponding mail-messageids-headerfield element
+function OutputMessageIds(headerEntry, headerValue)
+{
+ var messageIdArray = headerValue.split(/\s+/);
+
+ headerEntry.enclosingBox.clearHeaderValues();
+ for (let i = 0; i < messageIdArray.length; i++)
+ headerEntry.enclosingBox.addMessageIdView(messageIdArray[i]);
+
+ headerEntry.enclosingBox.fillMessageIdNodes();
+}
+
+// OutputEmailAddresses --> knows how to take a comma separated list of email addresses,
+// extracts them one by one, linkifying each email address into a mailto url.
+// Then we add the link-ified email address to the parentDiv passed in.
+//
+// emailAddresses --> comma separated list of the addresses for this header field
+
+function OutputEmailAddresses(headerEntry, emailAddresses)
+{
+ if (!emailAddresses)
+ return;
+
+ // The email addresses are still RFC2047 encoded but libmime has already
+ // converted from "raw UTF-8" to "wide" (UTF-16) characters.
+ var addresses =
+ MailServices.headerParser.parseEncodedHeaderW(emailAddresses);
+
+ for (let addr of addresses) {
+ // If we want to include short/long toggle views and we have a long view,
+ // always add it. If we aren't including a short/long view OR if we are and
+ // we haven't parsed enough addresses to reach the cutoff valve yet then
+ // add it to the default (short) div.
+ let address = {};
+ address.emailAddress = addr.email || "";
+ address.fullAddress = addr.toString() || "";
+ address.displayName = addr.name || "";
+ if (headerEntry.useToggle)
+ headerEntry.enclosingBox.addAddressView(address);
+ else
+ updateEmailAddressNode(headerEntry.enclosingBox.emailAddressNode,
+ address);
+ }
+
+ if (headerEntry.useToggle)
+ headerEntry.enclosingBox.buildViews();
+}
+
+function updateEmailAddressNode(emailAddressNode, address)
+{
+ emailAddressNode.setAttribute("emailAddress", address.emailAddress);
+ emailAddressNode.setAttribute("fullAddress", address.fullAddress);
+ emailAddressNode.setAttribute("displayName", address.displayName);
+
+ UpdateEmailNodeDetails(address.emailAddress, emailAddressNode);
+}
+
+function UpdateEmailNodeDetails(aEmailAddress, aDocumentNode, aCardDetails)
+{
+ // If we haven't been given specific details, search for a card.
+ var cardDetails = aCardDetails || GetCardForEmail(aEmailAddress);
+ aDocumentNode.cardDetails = cardDetails;
+
+ var condense = gShowCondensedEmailAddresses;
+ // Get the id of the mail-multi-emailHeaderField binding parent.
+ var parentElementId = aDocumentNode.parentNode.parentNode.parentNode.id;
+ // Don't condense the address for the from and reply-to fields.
+ // Ids: "collapsedfromValue", "expandedfromBox", "expandedreply-toBox".
+ if (/^(collapsedfromValue|expanded(from|reply-to)Box)$/.test(parentElementId))
+ condense = false;
+
+ var displayName = "";
+ if (condense && cardDetails.card)
+ {
+ if (cardDetails.card.getProperty("PreferDisplayName", true) != true)
+ displayName = aDocumentNode.getAttribute("displayName");
+ if (!displayName)
+ displayName = cardDetails.card.displayName;
+ }
+
+ if (displayName)
+ {
+ aDocumentNode.setAttribute("tooltiptext", aEmailAddress);
+ }
+ else
+ {
+ aDocumentNode.removeAttribute("tooltiptext");
+ displayName = aDocumentNode.getAttribute("fullAddress") ||
+ aDocumentNode.getAttribute("displayName");
+ }
+
+ aDocumentNode.setAttribute("label", displayName);
+}
+
+function UpdateExtraAddressProcessing(aAddressData, aDocumentNode, aAction,
+ aParentDir, aItem)
+{
+ switch (aAction)
+ {
+ case nsIAbListener.itemChanged:
+ if (aAddressData &&
+ aDocumentNode.cardDetails.card &&
+ aItem.hasEmailAddress(aAddressData.emailAddress)) {
+ aDocumentNode.cardDetails.card = aItem;
+ UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode,
+ aDocumentNode.cardDetails);
+ }
+ break;
+ case nsIAbListener.itemAdded:
+ // Is it a new address book?
+ if (aItem instanceof nsIAbDirectory)
+ {
+ // If we don't have a match, search again for updates (e.g. a interface
+ // to an existing book may just have been added).
+ if (!aDocumentNode.cardDetails.card)
+ UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode);
+ }
+ else if (aItem instanceof nsIAbCard)
+ {
+ // If we don't have a card, does this new one match?
+ if (!aDocumentNode.cardDetails.card &&
+ aItem.hasEmailAddress(aAddressData.emailAddress))
+ {
+ // Just in case we have a bogus parent directory.
+ if (aParentDir instanceof nsIAbDirectory)
+ {
+ let cardDetails = { book: aParentDir, card: aItem };
+ UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode,
+ cardDetails);
+ }
+ else
+ {
+ UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode);
+ }
+ }
+ }
+ break;
+ case nsIAbListener.directoryItemRemoved:
+ // Unfortunately we don't necessarily get the same card object back.
+ if (aAddressData &&
+ aDocumentNode.cardDetails.card &&
+ aDocumentNode.cardDetails.book == aParentDir &&
+ aItem.hasEmailAddress(aAddressData.emailAddress))
+ {
+ UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode);
+ }
+ break;
+ case nsIAbListener.directoryRemoved:
+ if (aDocumentNode.cardDetails.book == aItem)
+ UpdateEmailNodeDetails(aAddressData.emailAddress, aDocumentNode);
+ break;
+ }
+}
+
+function SetupEmailAddressPopup(aAddressNode)
+{
+ document.getElementById("emailAddressPlaceHolder")
+ .setAttribute("label", aAddressNode.getAttribute("emailAddress"));
+
+ var addItem = document.getElementById("addToAddressBookItem");
+ var editItem = document.getElementById("editContactItem");
+ var viewItem = document.getElementById("viewContactItem");
+
+ if (aAddressNode.cardDetails.card)
+ {
+ addItem.setAttribute("hidden", true);
+ if (!aAddressNode.cardDetails.book.readOnly)
+ {
+ editItem.removeAttribute("hidden");
+ viewItem.setAttribute("hidden", true);
+ }
+ else
+ {
+ editItem.setAttribute("hidden", true);
+ viewItem.removeAttribute("hidden");
+ }
+ }
+ else
+ {
+ addItem.removeAttribute("hidden");
+ editItem.setAttribute("hidden", true);
+ viewItem.setAttribute("hidden", true);
+ }
+}
+
+/**
+ * Returns an object with two properties, book and card. If the email address
+ * is found in the address books, then book will contain an nsIAbDirectory,
+ * and card will contain an nsIAbCard. If the email address is not found, both
+ * properties will be null.
+ *
+ * @param emailAddress The email address to find.
+ * @return An object with two properties, book and card.
+ * @see nsIAbDirectory.cardForEmailAddress()
+ */
+function GetCardForEmail(aEmailAddress)
+{
+ var books = MailServices.ab.directories;
+
+ var result = { book: null, card: null};
+
+ while (!result.card && books.hasMoreElements())
+ {
+ var ab = books.getNext();
+ if (ab instanceof nsIAbDirectory)
+ {
+ try
+ {
+ var card = ab.cardForEmailAddress(aEmailAddress);
+ if (card)
+ {
+ result.book = ab;
+ result.card = card;
+ }
+ }
+ catch (ex)
+ {
+ // Unsearchable address books throw |NS_ERROR_NOT_IMPLEMENTED|.
+ }
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Create a new attachment object which goes into the data attachment array.
+ * This method checks whether the passed attachment is empty or not.
+ *
+ * @param contentType The attachment's mimetype
+ * @param url The URL for the attachment
+ * @param displayName The name to be displayed for this attachment (usually the
+ filename)
+ * @param uri The URI for the message containing the attachment
+ * @param isExternalAttachment True if the attachment has been detached
+ * @param size The size in bytes of the attachment
+ */
+function createNewAttachmentInfo(contentType, url, displayName, uri,
+ isExternalAttachment, size)
+{
+ this.contentType = contentType;
+ this.displayName = displayName;
+ this.uri = uri;
+ this.isExternalAttachment = isExternalAttachment;
+ this.attachment = this;
+ this.size = size;
+ let match;
+
+ // Remote urls, unlike non external mail part urls, may also contain query
+ // strings starting with ?; PART_RE does not handle this.
+ if (url.startsWith("http") || url.startsWith("file")) {
+ match = url.match(/[?&]part=[^&]+$/);
+ match = match && match[0];
+ this.partID = match && match.split("part=")[1];
+ url = url.replace(match, "");
+ }
+ else {
+ match = GlodaUtils.PART_RE.exec(url);
+ this.partID = match && match[1];
+ }
+
+ // Make sure to communicate it if it's an external http attachment and not a
+ // local attachment. For feeds attachments (enclosures) are always remote,
+ // so there is nothing to communicate.
+ if (isExternalAttachment && url.startsWith("http") &&
+ !gFolderDisplay.selectedMessageIsFeed) {
+ if (this.displayName) {
+ this.displayName = url + " - " + this.displayName;
+ }
+ else {
+ this.displayName = url;
+ }
+ }
+
+ this.url = url;
+
+}
+
+createNewAttachmentInfo.prototype.saveAttachment = function saveAttachment()
+{
+ if (this.isExternalAttachment)
+ // TODO: This displays "Save As" instead of "Save Attachment" in the title
+ internalSave(this.url, null,
+ this.displayName, null,
+ this.contentType, false,
+ null, null, null, document);
+ else
+ messenger.saveAttachment(this.contentType,
+ this.url,
+ encodeURIComponent(this.displayName),
+ this.uri,
+ false);
+}
+
+createNewAttachmentInfo.prototype.viewAttachment = function viewAttachment()
+{
+ var url = this.url;
+ if (!this.isExternalAttachment)
+ url += "&filename=" + encodeURIComponent(this.displayName);
+ openDialog("chrome://global/content/viewSource.xul",
+ "_blank", "all,dialog=no", {URL: url});
+}
+
+createNewAttachmentInfo.prototype.openAttachment = function openAttachment()
+{
+ switch (this.contentType)
+ {
+ // As of bug 599119, isTypeSupported returns true for messages, but
+ // attached messages don't open reliably in the browser, so pretend
+ // they're not supported and open a message window for them instead.
+ case "message/rfc822":
+ var url = this.url + "&type=application/x-message-display";
+ window.openDialog("chrome://messenger/content/messageWindow.xul",
+ "_blank", "all,dialog=no",
+ Services.io.newURI(url));
+ return;
+ case "text/x-moz-deleted":
+ return;
+ }
+
+ var webNavigationInfo =
+ Cc["@mozilla.org/webnavigation-info;1"]
+ .getService(Ci.nsIWebNavigationInfo);
+
+ if (webNavigationInfo.isTypeSupported(this.contentType, null))
+ openAsExternal(this.url);
+ else
+ messenger.openAttachment(this.contentType,
+ this.url,
+ encodeURIComponent(this.displayName),
+ this.uri,
+ this.isExternalAttachment);
+}
+
+createNewAttachmentInfo.prototype.printAttachment = function printAttachment()
+{
+ /* we haven't implemented the ability to print attachments yet...
+ messenger.printAttachment(this.contentType,
+ this.url,
+ encodeURIComponent(this.displayName),
+ this.uri);
+ */
+}
+
+createNewAttachmentInfo.prototype.deleteAttachment = function deleteAttachment()
+{
+ messenger.detachAttachment(this.contentType,
+ this.url,
+ encodeURIComponent(this.displayName),
+ this.uri,
+ false);
+}
+
+createNewAttachmentInfo.prototype.detachAttachment = function detachAttachment()
+{
+ messenger.detachAttachment(this.contentType,
+ this.url,
+ encodeURIComponent(this.displayName),
+ this.uri,
+ true);
+}
+
+function CanDetachAttachments()
+{
+ var canDetach = !gFolderDisplay.selectedMessageIsNews &&
+ (!gFolderDisplay.selectedMessageIsImap ||
+ !Services.io.offline);
+ if (canDetach && ("content-type" in currentHeaderData))
+ canDetach = !ContentTypeIsSMIME(currentHeaderData["content-type"].headerValue);
+ return canDetach;
+}
+
+/** Return true if the content type is an S/MIME one. */
+function ContentTypeIsSMIME(contentType)
+{
+ // S/MIME is application/pkcs7-mime and application/pkcs7-signature
+ // - also match application/x-pkcs7-mime and application/x-pkcs7-signature.
+ return /application\/(x-)?pkcs7-(mime|signature)/.test(contentType);
+}
+
+function onShowAttachmentContextMenu()
+{
+ // if no attachments are selected, disable the Open and Save...
+ var attachmentList = document.getElementById('attachmentList');
+ var selectedAttachments = [...attachmentList.selectedItems];
+ var openMenu = document.getElementById('context-openAttachment');
+ var viewMenu = document.getElementById('context-viewAttachment');
+ var saveMenu = document.getElementById('context-saveAttachment');
+ var detachMenu = document.getElementById('context-detachAttachment');
+ var deleteMenu = document.getElementById('context-deleteAttachment');
+ var saveAllMenu = document.getElementById('context-saveAllAttachments');
+ var detachAllMenu = document.getElementById('context-detachAllAttachments');
+ var deleteAllMenu = document.getElementById('context-deleteAllAttachments');
+
+ var canDetach = CanDetachAttachments();
+ var deletedAmongSelected = false;
+ var detachedAmongSelected = false;
+ var anyDeleted = false; // at least one deleted attachment in the list
+ var anyDetached = false; // at least one detached attachment in the list
+
+ // Check if one or more of the selected attachments are deleted.
+ for (let i = 0; i < selectedAttachments.length && !deletedAmongSelected; i++)
+ deletedAmongSelected =
+ (selectedAttachments[i].attachment.contentType == 'text/x-moz-deleted');
+
+ // Check if one or more of the selected attachments are detached.
+ for (let i = 0; i < selectedAttachments.length && !detachedAmongSelected; i++)
+ detachedAmongSelected = selectedAttachments[i].attachment.isExternalAttachment;
+
+ // Check if any attachments are deleted.
+ for (let i = 0; i < currentAttachments.length && !anyDeleted; i++)
+ anyDeleted = (currentAttachments[i].contentType == 'text/x-moz-deleted');
+
+ // Check if any attachments are detached.
+ for (let i = 0; i < currentAttachments.length && !anyDetached; i++)
+ anyDetached = currentAttachments[i].isExternalAttachment;
+
+ if (!deletedAmongSelected && selectedAttachments.length == 1)
+ {
+ openMenu.removeAttribute('disabled');
+ viewMenu.removeAttribute('disabled');
+ }
+ else
+ {
+ openMenu.setAttribute('disabled', true);
+ viewMenu.setAttribute('disabled', true);
+ }
+
+ saveMenu.setAttribute('disabled', deletedAmongSelected);
+ detachMenu.setAttribute('disabled', !canDetach || deletedAmongSelected
+ || detachedAmongSelected);
+ deleteMenu.setAttribute('disabled', !canDetach || deletedAmongSelected
+ || detachedAmongSelected);
+ saveAllMenu.setAttribute('disabled', anyDeleted);
+ detachAllMenu.setAttribute('disabled', !canDetach || anyDeleted || anyDetached);
+ deleteAllMenu.setAttribute('disabled', !canDetach || anyDeleted || anyDetached);
+}
+
+function MessageIdClick(node, event)
+{
+ if (event.button == 0)
+ {
+ var messageId = GetMessageIdFromNode(node, true);
+ OpenMessageForMessageId(messageId);
+ }
+}
+
+// this is our onclick handler for the attachment list.
+// A double click in a listitem simulates "opening" the attachment....
+function attachmentListClick(event)
+{
+ // we only care about button 0 (left click) events
+ if (event.button != 0)
+ return;
+
+ if (event.detail == 2) // double click
+ {
+ var target = event.target;
+ if (target.localName == "listitem")
+ target.attachment.openAttachment();
+ }
+}
+
+// on command handlers for the attachment list context menu...
+// commandPrefix matches one of our existing functions
+// (openAttachment, saveAttachment, etc.)
+function handleAttachmentSelection(commandPrefix)
+{
+ var attachmentList = document.getElementById('attachmentList');
+ var selectedAttachments = [...attachmentList.selectedItems];
+ if (selectedAttachments.length > 1)
+ HandleMultipleAttachments(commandPrefix, selectedAttachments);
+ else
+ selectedAttachments[0].attachment[commandPrefix]();
+}
+
+function createAttachmentDisplayName(aAttachment)
+{
+ // Strip any white space at the end of the display name to avoid
+ // attachment name spoofing (especially Windows will drop trailing dots
+ // and whitespace from filename extensions). Leading and internal
+ // whitespace will be taken care of by the crop="center" attribute.
+ // We must not change the actual filename, though.
+ return aAttachment.displayName.trimRight();
+}
+
+function displayAttachmentsForExpandedView()
+{
+ var numAttachments = currentAttachments.length;
+ if (numAttachments > 0 && !gBuildAttachmentsForCurrentMsg)
+ {
+ let attachmentList = document.getElementById('attachmentList');
+
+ for (let index in currentAttachments)
+ {
+ let attachment = currentAttachments[index];
+
+ // create a listitem for the attachment listbox
+ let displayName = createAttachmentDisplayName(attachment);
+ let nameAndSize = displayName;
+ if (attachment.size != null)
+ nameAndSize += " (" + messenger.formatFileSize(attachment.size) + ")";
+ let item = attachmentList.appendItem(nameAndSize, "");
+ item.setAttribute("crop", "center");
+ item.setAttribute("class", "listitem-iconic attachment-item");
+ item.setAttribute("tooltiptext", attachment.displayName);
+ item.attachment = attachment;
+ item.setAttribute("attachmentUrl", attachment.url);
+ item.setAttribute("attachmentContentType", attachment.contentType);
+ item.setAttribute("attachmentUri", attachment.uri);
+ item.setAttribute("attachmentSize", attachment.size);
+ if (attachment.contentType == "text/x-moz-deleted")
+ item.setAttribute('disabled', 'true');
+ else
+ setApplicationIconForAttachment(attachment, item);
+ } // for each attachment
+
+ gBuildAttachmentsForCurrentMsg = true;
+ }
+
+ var expandedAttachmentBox = document.getElementById('expandedAttachmentBox');
+ expandedAttachmentBox.collapsed = numAttachments <= 0;
+}
+
+// attachment --> the attachment struct containing all the information on the attachment
+// listitem --> the listitem currently showing the attachment.
+function setApplicationIconForAttachment(attachment, listitem)
+{
+ // generate a moz-icon url for the attachment so we'll show a nice icon next to it.
+ listitem.setAttribute('image', "moz-icon:" + "//" + attachment.displayName + "?size=16&contentType=" + attachment.contentType);
+}
+
+function displayAttachmentsForCollapsedView()
+{
+ var numAttachments = currentAttachments.length;
+ var attachmentNode = document.getElementById('collapsedAttachmentBox');
+ attachmentNode.collapsed = numAttachments <= 0; // make sure the attachment button is visible
+}
+
+// Public method called when we create the attachments file menu
+function FillAttachmentListPopup(popup)
+{
+ // the FE sometimes call this routine TWICE...I haven't been able to figure out why yet...
+ // protect against it...
+ if (!gBuildAttachmentPopupForCurrentMsg)
+ return;
+
+ var attachmentIndex = 0;
+
+ // otherwise we need to build the attachment view...
+ // First clear out the old view...
+ ClearAttachmentMenu(popup);
+
+ var canDetachOrDeleteAll = CanDetachAttachments();
+
+ for (let index in currentAttachments)
+ {
+ ++attachmentIndex;
+ addAttachmentToPopup(popup, currentAttachments[index], attachmentIndex);
+ if (canDetachOrDeleteAll &&
+ (currentAttachments[index].isExternalAttachment ||
+ currentAttachments[index].contentType == 'text/x-moz-deleted'))
+ canDetachOrDeleteAll = false;
+ }
+
+ gBuildAttachmentPopupForCurrentMsg = false;
+
+ var detachAllMenu = document.getElementById('file-detachAllAttachments');
+ var deleteAllMenu = document.getElementById('file-deleteAllAttachments');
+
+ detachAllMenu.setAttribute('disabled', !canDetachOrDeleteAll);
+ deleteAllMenu.setAttribute('disabled', !canDetachOrDeleteAll);
+}
+
+// Public method used to clear the file attachment menu
+function ClearAttachmentMenu(popup)
+{
+ if ( popup )
+ {
+ while (popup.firstChild.localName == 'menu')
+ popup.firstChild.remove();
+ }
+}
+
+// Public method used to determine the number of attachments for the currently displayed message...
+function GetNumberOfAttachmentsForDisplayedMessage()
+{
+ return currentAttachments.length;
+}
+
+// private method used to build up a menu list of attachments
+function addAttachmentToPopup(popup, attachment, attachmentIndex)
+{
+ if (popup)
+ {
+ var item = document.createElement('menu');
+ if ( item )
+ {
+ if (!gMessengerBundle)
+ gMessengerBundle = document.getElementById("bundle_messenger");
+
+ // insert the item just before the separator
+ item = popup.insertBefore(item, popup.childNodes[attachmentIndex - 1]);
+ item.setAttribute('class', 'menu-iconic attachment-item');
+
+ var displayName = createAttachmentDisplayName(attachment);
+ var formattedDisplayNameString = gMessengerBundle.getFormattedString("attachmentDisplayNameFormat",
+ [attachmentIndex, displayName]);
+
+ item.setAttribute("crop", "center");
+ item.setAttribute('label', formattedDisplayNameString);
+ item.setAttribute('accesskey', attachmentIndex);
+
+ var openpopup = document.createElement('menupopup');
+ openpopup = item.appendChild(openpopup);
+ if (attachment.contentType == "text/x-moz-deleted") {
+ item.setAttribute('disabled', 'true');
+ return;
+ }
+ openpopup.attachment = attachment;
+ openpopup.addEventListener('popupshowing', FillAttachmentItemPopup);
+ setApplicationIconForAttachment(attachment, item);
+ }
+ }
+}
+
+function FillAttachmentItemPopup(event)
+{
+ var openpopup = event.target;
+ var canDetach = CanDetachAttachments() && !openpopup.attachment.isExternalAttachment;
+ openpopup.removeEventListener('popupshowing', FillAttachmentItemPopup);
+
+ var menuitementry = document.getElementById("context-openAttachment").cloneNode(false);
+ menuitementry.setAttribute('oncommand', 'this.parentNode.attachment.openAttachment();');
+ menuitementry = openpopup.appendChild(menuitementry);
+
+ menuitementry = document.getElementById("context-viewAttachment").cloneNode(false);
+ menuitementry.setAttribute('oncommand', 'this.parentNode.attachment.viewAttachment();');
+ menuitementry = openpopup.appendChild(menuitementry);
+
+ menuitementry = document.getElementById("context-saveAttachment").cloneNode(false);
+ menuitementry.setAttribute('oncommand', 'this.parentNode.attachment.saveAttachment()');
+ menuitementry = openpopup.appendChild(menuitementry);
+
+ openpopup.appendChild(document.createElement("menuseparator"));
+
+ menuitementry = document.getElementById("context-detachAttachment").cloneNode(false);
+ menuitementry.setAttribute('oncommand', 'this.parentNode.attachment.detachAttachment()');
+ if (!canDetach)
+ menuitementry.setAttribute('disabled', 'true');
+ menuitementry = openpopup.appendChild(menuitementry);
+
+ menuitementry = document.getElementById("context-deleteAttachment").cloneNode(false);
+ menuitementry.setAttribute('oncommand', 'this.attachment.deleteAttachment()');
+ if (!canDetach)
+ menuitementry.setAttribute('disabled', 'true');
+ menuitementry = openpopup.appendChild(menuitementry);
+}
+
+function HandleMultipleAttachments(commandPrefix, selectedAttachments)
+{
+ try
+ {
+ // convert our attachment data into some c++ friendly structs
+ var attachmentContentTypeArray = new Array();
+ var attachmentUrlArray = new Array();
+ var attachmentDisplayNameArray = new Array();
+ var attachmentMessageUriArray = new Array();
+
+ // populate these arrays..
+ for (let index in selectedAttachments)
+ {
+ let attachment = selectedAttachments[index].attachment;
+ attachmentContentTypeArray[index] = attachment.contentType;
+ attachmentUrlArray[index] = attachment.url;
+ attachmentDisplayNameArray[index] = encodeURI(attachment.displayName);
+ attachmentMessageUriArray[index] = attachment.uri;
+ }
+
+ // okay the list has been built... now call our action code...
+ switch (commandPrefix)
+ {
+ case "saveAttachment":
+ messenger.saveAllAttachments(attachmentContentTypeArray,
+ attachmentUrlArray,
+ attachmentDisplayNameArray,
+ attachmentMessageUriArray);
+ break;
+ case "detachAttachment":
+ messenger.detachAllAttachments(attachmentContentTypeArray,
+ attachmentUrlArray,
+ attachmentDisplayNameArray,
+ attachmentMessageUriArray,
+ true /* save */);
+ break;
+ case "deleteAttachment":
+ messenger.detachAllAttachments(attachmentContentTypeArray,
+ attachmentUrlArray,
+ attachmentDisplayNameArray,
+ attachmentMessageUriArray,
+ false /* don't save */);
+ break;
+ default:
+ dump (commandPrefix + "** unknown handle all attachments action **\n");
+ }
+ }
+ catch (ex)
+ {
+ dump ("** failed to handle all attachments **\n");
+ }
+}
+
+function ClearAttachmentList()
+{
+ // we also have to disable the Message/Attachments menuitem
+ var node = document.getElementById("msgAttachmentMenu");
+ if (node)
+ node.setAttribute("disabled", "true");
+
+ // clear selection
+ var list = document.getElementById('attachmentList');
+ list.clearSelection();
+
+ while (list.hasChildNodes())
+ list.lastChild.remove();
+}
+
+function ShowEditMessageBox(aMessageBox, aFlag) {
+ try {
+ // it would be nice if we passed in the msgHdr from the back end
+ var msgHdr = gDBView.hdrForFirstSelectedMessage;
+ if (!msgHdr || !msgHdr.folder)
+ return;
+ if (msgHdr.folder.isSpecialFolder(aFlag, true))
+ document.getElementById(aMessageBox).collapsed = false;
+ }
+ catch (ex) {}
+}
+
+function ClearEditMessageBox(aMessageBox) {
+ var editBox = document.getElementById(aMessageBox);
+ if (editBox)
+ editBox.collapsed = true;
+}
+
+// CopyWebsiteAddress takes the website address title button, extracts
+// the website address we stored in there and copies it to the clipboard
+function CopyWebsiteAddress(websiteAddressNode)
+{
+ if (websiteAddressNode)
+ {
+ var websiteAddress = websiteAddressNode.getAttribute("value");
+
+ var contractid = "@mozilla.org/widget/clipboardhelper;1";
+ var iid = Ci.nsIClipboardHelper;
+ var clipboard = Cc[contractid].getService(iid);
+ clipboard.copyString(websiteAddress);
+ }
+}
+
+function BookmarkWebsite(aWebsiteAddressNode)
+{
+ if (aWebsiteAddressNode)
+ {
+ let websiteAddress = aWebsiteAddressNode.getAttribute("value");
+
+ if (currentHeaderData && "content-base" in currentHeaderData)
+ {
+ let url = currentHeaderData["content-base"].headerValue;
+ if (url != websiteAddress)
+ return;
+
+ let title = currentHeaderData["subject"].headerValue;
+ PlacesUIUtils.showMinimalAddBookmarkUI(makeURI(url), title);
+ }
+ }
+}
+
+var attachmentAreaDNDObserver = {
+ onDragStart(aEvent) {
+ var target = aEvent.target;
+ if (target.localName == "listitem") {
+ let index = 0;
+ let selection = target.parentNode.selectedItems;
+ for (let item of selection) {
+ let attachment = item.attachment;
+ if (attachment.contentType == "text/x-moz-deleted") {
+ continue;
+ }
+
+ let name = attachment.name || attachment.displayName;
+ if (!attachment.url || !name) {
+ continue;
+ }
+
+ let info = attachment.url;
+ // Only add type/filename info for non-file URLs that don't already
+ // have it.
+ if (!/(^file:|&filename=)/.test(info)) {
+ info += "&type=" + attachment.contentType + "&filename=" +
+ encodeURIComponent(name);
+ }
+ let dt = aEvent.dataTransfer;
+ dt.mozSetDataAt("text/x-moz-url",
+ info + "\n" + name + "\n" + attachment.size,
+ index);
+ dt.mozSetDataAt("text/x-moz-url-data", attachment.url, index);
+ dt.mozSetDataAt("text/x-moz-url-desc", name, index);
+ dt.mozSetDataAt("application/x-moz-file-promise-url", attachment.url,
+ index);
+ dt.mozSetDataAt("application/x-moz-file-promise",
+ new nsFlavorDataProvider(), index);
+ index++;
+ }
+ }
+ aEvent.stopPropagation();
+ }
+};
+
+function nsFlavorDataProvider()
+{
+}
+
+nsFlavorDataProvider.prototype =
+{
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFlavorDataProvider]),
+
+ getFlavorData : function(aTransferable, aFlavor, aData, aDataLen)
+ {
+ // get the url for the attachment
+ if (aFlavor == "application/x-moz-file-promise")
+ {
+ var urlPrimitive = { };
+ var dataSize = { };
+ aTransferable.getTransferData("application/x-moz-file-promise-url", urlPrimitive, dataSize);
+
+ var srcUrlPrimitive = urlPrimitive.value.QueryInterface(Ci.nsISupportsString);
+
+ // now get the destination file location from kFilePromiseDirectoryMime
+ var dirPrimitive = {};
+ aTransferable.getTransferData("application/x-moz-file-promise-dir", dirPrimitive, dataSize);
+ var destDirectory = dirPrimitive.value.QueryInterface(Ci.nsIFile);
+
+ // now save the attachment to the specified location
+ // XXX: we need more information than just the attachment url to save it, fortunately, we have an array
+ // of all the current attachments so we can cheat and scan through them
+
+ var attachment = null;
+ for (let index in currentAttachments)
+ {
+ attachment = currentAttachments[index];
+ if (attachment.url == srcUrlPrimitive)
+ break;
+ }
+
+ // call our code for saving attachments
+ if (attachment)
+ {
+ var destFilePath = messenger.saveAttachmentToFolder(attachment.contentType, attachment.url, encodeURIComponent(attachment.displayName), attachment.uri, destDirectory);
+ aData.value = destFilePath.QueryInterface(Ci.nsISupports);
+ aDataLen.value = 4;
+ }
+ }
+ }
+}
+
+function nsDummyMsgHeader()
+{
+}
+
+nsDummyMsgHeader.prototype =
+{
+ mProperties : new Array,
+ getStringProperty : function(aProperty)
+ {
+ return this.mProperties[aProperty];
+ },
+ setStringProperty : function(aProperty, aVal)
+ {
+ this.mProperties[aProperty] = aVal;
+ },
+ getUint32Property : function(aProperty)
+ {
+ if (aProperty in this.mProperties)
+ return parseInt(this.mProperties[aProperty]);
+ return 0;
+ },
+ setUint32Property : function(aProperty, aVal)
+ {
+ this.mProperties[aProperty] = aVal.toString();
+ },
+ markHasAttachments : function(hasAttachments) {},
+ messageSize : 0,
+ recipients : null,
+ from : null,
+ subject : "",
+ get mime2DecodedSubject() { return this.subject; },
+ ccList : null,
+ messageId : null,
+ listPost : null,
+ date : 0,
+ accountKey : "",
+ flags : 0,
+ folder : null
+};
diff --git a/comm/suite/mailnews/content/msgHdrViewOverlay.xul b/comm/suite/mailnews/content/msgHdrViewOverlay.xul
new file mode 100644
index 0000000000..ea2c12e091
--- /dev/null
+++ b/comm/suite/mailnews/content/msgHdrViewOverlay.xul
@@ -0,0 +1,273 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE overlay [
+<!ENTITY % msgHdrViewPopupDTD SYSTEM "chrome://messenger/locale/msgHdrViewPopup.dtd" >
+%msgHdrViewPopupDTD;
+<!ENTITY % msgHdrViewOverlayDTD SYSTEM "chrome://messenger/locale/msgHdrViewOverlay.dtd" >
+%msgHdrViewOverlayDTD;
+]>
+
+<?xml-stylesheet href="chrome://messenger/skin/messageHeader.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messageKeywords.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/smime/msgHdrViewSMIMEOverlay.css" type="text/css"?>
+
+<overlay xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script src="chrome://messenger/content/msgHdrViewOverlay.js"/>
+
+<script src="chrome://messenger-smime/content/msgHdrViewSMIMEOverlay.js"/>
+
+<stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+</stringbundleset>
+
+<menupopup id="messageIdContext" popupanchor="bottomleft"
+ onpopupshowing="FillMessageIdContextMenu(document.popupNode);">
+ <menuitem id="messageIdContext-messageIdTarget"
+ disabled="true"/>
+ <menuseparator/>
+ <menuitem id="messageIdContext-openMessageForMsgId"
+ label="&OpenMessageForMsgId.label;"
+ accesskey="&OpenMessageForMsgId.accesskey;"
+ oncommand="var messageId = GetMessageIdFromNode(document.popupNode, true);
+ OpenMessageForMessageId(messageId)"/>
+ <menuitem id="messageIdContext-openBrowserWithMsgId"
+ label="&OpenBrowserWithMsgId.label;"
+ accesskey="&OpenBrowserWithMsgId.accesskey;"
+ oncommand="var messageId = GetMessageIdFromNode(document.popupNode, true);
+ OpenBrowserWithMessageId(messageId)"/>
+ <menuitem id="messageIdContext-copyMessageId"
+ label="&CopyMessageId.label;"
+ accesskey="&CopyMessageId.accesskey;"
+ oncommand="var messageId = GetMessageIdFromNode(document.popupNode, false);
+ CopyString(messageId);"/>
+</menupopup>
+
+<menupopup id="emailAddressPopup" popupanchor="bottomleft"
+ onpopupshowing="SetupEmailAddressPopup(document.popupNode);
+ goUpdateCommand('cmd_createFilterFromPopup');">
+ <menuitem id="emailAddressPlaceHolder" label="" disabled="true"/>
+ <menuseparator/>
+ <menuitem id="sendMailToItem"
+ label="&SendMailTo.label;"
+ accesskey="&SendMailTo.accesskey;"
+ oncommand="SendMailToNode(document.popupNode, event)"/>
+ <menuitem id="createFilterFromItem"
+ label="&CreateFilterFrom.label;"
+ accesskey="&CreateFilterFrom.accesskey;"
+ command="cmd_createFilterFromPopup"/>
+ <menuitem id="addToAddressBookItem"
+ label="&AddToAddressBook.label;"
+ accesskey="&AddToAddressBook.accesskey;"
+ oncommand="AddContact(document.popupNode);"/>
+ <menuitem id="editContactItem"
+ label="&EditContact.label;"
+ accesskey="&EditContact.accesskey;"
+ hidden="true"
+ oncommand="EditContact(document.popupNode);"/>
+ <menuitem id="viewContactItem"
+ label="&ViewContact.label;"
+ accesskey="&ViewContact.accesskey;"
+ hidden="true"
+ oncommand="EditContact(document.popupNode);"/>
+ <menuitem id="copyEmailAddressItem"
+ label="&CopyEmailAddress.label;"
+ accesskey="&CopyEmailAddress.accesskey;"
+ oncommand="CopyEmailAddress(document.popupNode);"/>
+ <menuitem id="copyNameAndEmailAddressItem"
+ label="&CopyNameAndEmailAddress.label;"
+ accesskey="&CopyNameAndEmailAddress.accesskey;"
+ oncommand="CopyEmailAddress(document.popupNode, true);"/>
+</menupopup>
+
+<menupopup id="attachmentListContext" onpopupshowing="return onShowAttachmentContextMenu();">
+ <menuitem id="context-openAttachment" label="&openAttachmentCmd.label;" accesskey="&openAttachmentCmd.accesskey;"
+ oncommand="handleAttachmentSelection('openAttachment');"/>
+ <menuitem id="context-viewAttachment" label="&viewAttachmentCmd.label;" accesskey="&viewAttachmentCmd.accesskey;"
+ oncommand="handleAttachmentSelection('viewAttachment');"/>
+ <menuitem id="context-saveAttachment" label="&saveAsAttachmentCmd.label;" accesskey="&saveAsAttachmentCmd.accesskey;"
+ oncommand="handleAttachmentSelection('saveAttachment');"/>
+ <menuseparator/>
+ <menuitem id="context-detachAttachment" label="&detachAttachmentCmd.label;" accesskey="&detachAttachmentCmd.accesskey;"
+ oncommand="handleAttachmentSelection('detachAttachment');"/>
+ <menuitem id="context-deleteAttachment" label="&deleteAttachmentCmd.label;" accesskey="&deleteAttachmentCmd.accesskey;"
+ oncommand="handleAttachmentSelection('deleteAttachment');"/>
+ <menuseparator/>
+ <menuitem id="context-saveAllAttachments" oncommand="HandleMultipleAttachments('saveAttachment', currentAttachments);"
+ label="&saveAllAttachmentsCmd.label;" accesskey="&saveAllAttachmentsCmd.accesskey;"/>
+ <menuitem id="context-detachAllAttachments" oncommand="HandleMultipleAttachments('detachAttachment', currentAttachments);"
+ label="&detachAllAttachmentsCmd.label;" accesskey="&detachAllAttachmentsCmd.accesskey;"/>
+ <menuitem id="context-deleteAllAttachments" oncommand="HandleMultipleAttachments('deleteAttachment', currentAttachments);"
+ label="&deleteAllAttachmentsCmd.label;" accesskey="&deleteAllAttachmentsCmd.accesskey;"/>
+</menupopup>
+
+<menupopup id="attachmentMenuList">
+ <menuseparator/>
+ <menuitem id="file-saveAllAttachments" label="&saveAllAttachmentsCmd.label;"
+ accesskey="&saveAllAttachmentsCmd.accesskey;" oncommand="HandleMultipleAttachments('saveAttachment', currentAttachments);"/>
+ <menuitem id="file-detachAllAttachments" label="&detachAllAttachmentsCmd.label;"
+ accesskey="&detachAllAttachmentsCmd.accesskey;" oncommand="HandleMultipleAttachments('detachAttachment', currentAttachments);" />
+ <menuitem id="file-deleteAllAttachments" label="&deleteAllAttachmentsCmd.label;"
+ accesskey="&deleteAllAttachmentsCmd.accesskey;" oncommand="HandleMultipleAttachments('deleteAttachment', currentAttachments);" />
+</menupopup>
+
+<menupopup id="copyUrlPopup">
+ <menuitem label="&openInBrowser.label;"
+ accesskey="&openInBrowser.accesskey;"
+ oncommand="openAsExternal(document.popupNode.getAttribute('value'));"/>
+ <menuitem label="&bookmarkLinkCmd.label;"
+ accesskey="&bookmarkLinkCmd.accesskey;"
+ oncommand="BookmarkWebsite(document.popupNode);"/>
+ <menuitem label="&copyLinkCmd.label;"
+ accesskey="&copyLinkCmd.accesskey;"
+ oncommand="CopyWebsiteAddress(document.popupNode);"/>
+</menupopup>
+
+<hbox id="msgHeaderView" persist="state">
+
+<grid id="collapsedHeaderView" class="header-part1" flex="1" collapsed="true">
+ <rows>
+ <row flex="1"/>
+ </rows>
+ <columns>
+ <column class="collapsedToggleHdrBox">
+ <hbox align="start">
+ <image id="toggleHeaderView" class="collapsedHeaderViewButton"
+ onclick="ToggleHeaderView();"/>
+ </hbox>
+ </column>
+
+ <column id="collapsedsubjectBox" collapsed="true" flex="1">
+ <hbox>
+ <label class="collapsedHeaderDisplayName" value="&subjectField.label;" control="collapsedsubjectValue"/>
+ <textbox id="collapsedsubjectValue"
+ class="collapsedHeaderValue plain"
+ readonly="true" crop="right" flex="1"/>
+ </hbox>
+ </column>
+
+ <column id="collapsedfromBox" flex="1">
+ <hbox align="start">
+ <mail-multi-emailHeaderField id="collapsedfromValue" class="collapsedHeaderDisplayName" label="&fromField.label;" collapsed="true" flex="1"/>
+ </hbox>
+ </column>
+
+ <column id = "collapseddateBox" collapsed="true">
+ <hbox align="start">
+ <textbox id="collapseddateValue"
+ class="collapsedHeaderValue plain"
+ readonly="true"/>
+ </hbox>
+ </column>
+
+ <column id="collapsedKeywordBox">
+ <hbox align="start">
+ <image id="collapsedKeywordImage"/>
+ </hbox>
+ </column>
+
+ <column id="collapsedAttachmentBox" collapsed="true">
+ <hbox align="start">
+ <image id="collapsedAttachment" class="collapsedAttachmentButton" onclick="ToggleHeaderView();" />
+ </hbox>
+ </column>
+ </columns>
+</grid>
+
+<hbox id="expandedHeaderView" class="header-part1" flex="1" collapsed="true">
+
+ <vbox id="expandedHeaders" flex="1">
+ <mail-toggle-headerfield id="expandedsubjectBox"
+ class="subjectvalue"
+ label="&subjectField.label;"
+ ontwistyclick="ToggleHeaderView();"
+ collapsed="true"/>
+
+ <mail-multi-emailHeaderField id="expandedfromBox" label="&fromField.label;" collapsed="true"/>
+ <mail-emailheaderfield id="expandedsenderBox" label="&senderField.label;" collapsed="true"/>
+ <mail-headerfield id="expandedorganizationBox" label="&organizationField.label;" collapsed="true"/>
+ <mail-multi-emailHeaderField id="expandedreply-toBox" label="&replyToField.label;" collapsed="true"/>
+
+ <mail-headerfield id="expandeddateBox"
+ label="&dateField.label;"
+ collapsed="true"/>
+
+ <mail-multi-emailHeaderField id="expandedtoBox" label="&toField.label;" collapsed="true"/>
+ <mail-multi-emailHeaderField id="expandedccBox" label="&ccField.label;" collapsed="true"/>
+ <mail-multi-emailHeaderField id="expandedbccBox" label="&bccField.label;" collapsed="true"/>
+
+ <mail-headerfield id="expandednewsgroupsBox"
+ label="&newsgroupsField.label;"
+ collapsed="true"/>
+ <mail-headerfield id="expandedfollowup-toBox"
+ label="&followupToField.label;"
+ collapsed="true"/>
+ <mail-messageids-headerfield id="expandedmessage-idBox" label="&messageIdField.label;" collapsed="true"/>
+ <mail-messageids-headerfield id="expandedin-reply-toBox" label="&inReplyToField.label;" collapsed="true"/>
+ <mail-messageids-headerfield id="expandedreferencesBox" label="&referencesField.label;" collapsed="true"/>
+ <mail-tagfield id="expandedtagsBox" label="&tagsHdr.label;" collapsed="true"/>
+ <mail-urlfield id="expandedcontent-baseBox" label="&originalWebsite.label;" collapsed="true"/>
+ <mail-headerfield id="expandeduser-agentBox"
+ label="&userAgentField.label;"
+ collapsed="true"/>
+ </vbox>
+
+ <vbox id="smimeBox" collapsed="true">
+ <spacer flex="1"/>
+ <image id="signedHdrIcon"
+ onclick="showMessageReadSecurityInfo();"
+ collapsed="true"/>
+ <image id="encryptedHdrIcon"
+ onclick="showMessageReadSecurityInfo();"
+ collapsed="true"/>
+ <spacer flex="1"/>
+ </vbox>
+
+ <vbox id="expandedKeywordBox">
+ <spacer flex="1"/>
+ <image id="expandedKeywordImage"/>
+ <spacer flex="1"/>
+ </vbox>
+
+ <vbox id="editDraftBox" class="header-part1" collapsed="true">
+ <spacer flex="1"/>
+ <button id="editDraftButton"
+ label="&editDraft.label;"
+ accesskey="&editDraft.accesskey;"
+ oncommand="MsgComposeDraftMessage(null);"/>
+ <spacer flex="1"/>
+ </vbox>
+
+ <vbox id="editTemplateBox" class="header-part1" collapsed="true">
+ <spacer flex="1"/>
+ <button id="editTemplateButton"
+ label="&editTemplate.label;"
+ accesskey="&editTemplate.accesskey;"
+ oncommand="MsgEditTemplateMessage(null);"/>
+ <spacer flex="1"/>
+ </vbox>
+
+ <vbox>
+ <spacer flex="1"/>
+ <image style="padding: 5px" id="fromBuddyIcon"/>
+ <spacer flex="1"/>
+ </vbox>
+
+ <vbox id="expandedAttachmentBox" class="header-part1" collapsed="true">
+ <label id="attachmentText"
+ value="&attachmentsTree.label;"
+ accesskey="&attachmentsTree.accesskey;"
+ crop="right"
+ control="attachmentList"/>
+ <listbox id="attachmentList" rows="3" seltype="multiple"
+ onclick="attachmentListClick(event);"
+ ondragstart="attachmentAreaDNDObserver.onDragStart(event);"
+ context="attachmentListContext"/>
+ </vbox>
+</hbox>
+</hbox>
+</overlay>
diff --git a/comm/suite/mailnews/content/msgMail3PaneWindow.js b/comm/suite/mailnews/content/msgMail3PaneWindow.js
new file mode 100644
index 0000000000..5cd3aa0693
--- /dev/null
+++ b/comm/suite/mailnews/content/msgMail3PaneWindow.js
@@ -0,0 +1,1265 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/* This is where functions related to the 3 pane window are kept */
+const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js");
+const {msgDBCacheManager} = ChromeUtils.import("resource:///modules/msgDBCacheManager.js");
+const {PeriodicFilterManager} = ChromeUtils.import("resource:///modules/PeriodicFilterManager.jsm");
+
+// from MailNewsTypes.h
+const nsMsgKey_None = 0xFFFFFFFF;
+const nsMsgViewIndex_None = 0xFFFFFFFF;
+const kMailCheckOncePrefName = "mail.startup.enabledMailCheckOnce";
+
+var gSearchInput;
+
+var gUnreadCount = null;
+var gTotalCount = null;
+
+var gCurrentLoadingFolderURI;
+var gCurrentFolderToReroot;
+var gCurrentLoadingFolderSortType = 0;
+var gCurrentLoadingFolderSortOrder = 0;
+var gCurrentLoadingFolderViewType = 0;
+var gCurrentLoadingFolderViewFlags = 0;
+var gRerootOnFolderLoad = false;
+var gCurrentDisplayedMessage = null;
+var gNextMessageAfterDelete = null;
+var gNextMessageAfterLoad = null;
+var gNextMessageViewIndexAfterDelete = -2;
+var gCurrentlyDisplayedMessage=nsMsgViewIndex_None;
+var gStartMsgKey = nsMsgKey_None;
+var gSearchEmailAddress = null;
+var gRightMouseButtonDown = false;
+// Global var to keep track of which row in the thread pane has been selected
+// This is used to make sure that the row with the currentIndex has the selection
+// after a Delete or Move of a message that has a row index less than currentIndex.
+var gThreadPaneCurrentSelectedIndex = -1;
+// Account Wizard can exceptionally override this feature.
+var gLoadStartFolder = true;
+
+// Global var to keep track of if the 'Delete Message' or 'Move To' thread pane
+// context menu item was triggered. This helps prevent the tree view from
+// not updating on one of those menu item commands.
+var gThreadPaneDeleteOrMoveOccurred = false;
+
+//If we've loaded a message, set to true. Helps us keep the start page around.
+var gHaveLoadedMessage;
+
+var gDisplayStartupPage = false;
+
+function SelectAndScrollToKey(aMsgKey)
+{
+ // select the desired message
+ // if the key isn't found, we won't select anything
+ if (!gDBView)
+ return false;
+ gDBView.selectMsgByKey(aMsgKey);
+
+ // is there a selection?
+ // if not, bail out.
+ var indicies = GetSelectedIndices(gDBView);
+ if (!indicies || !indicies.length)
+ return false;
+
+ // now scroll to it
+ EnsureRowInThreadTreeIsVisible(indicies[0]);
+ return true;
+}
+
+// A helper routine called after a folder is loaded to make sure
+// we select and scroll to the correct message (could be the first new message,
+// could be the last displayed message, etc.)
+function ScrollToMessageAfterFolderLoad(folder)
+{
+ var scrolled = Services.prefs.getBoolPref("mailnews.scroll_to_new_message") &&
+ ScrollToMessage(nsMsgNavigationType.firstNew, true, false /* selectMessage */);
+ if (!scrolled && folder && Services.prefs.getBoolPref("mailnews.remember_selected_message"))
+ {
+ // If we failed to scroll to a new message,
+ // reselect the last selected message
+ var lastMessageLoaded = folder.lastMessageLoaded;
+ if (lastMessageLoaded != nsMsgKey_None)
+ scrolled = SelectAndScrollToKey(lastMessageLoaded);
+ }
+
+ if (!scrolled)
+ {
+ // if we still haven't scrolled,
+ // scroll to the newest, which might be the top or the bottom
+ // depending on our sort order and sort type
+ if (gDBView && gDBView.sortOrder == nsMsgViewSortOrder.ascending)
+ {
+ switch (gDBView.sortType)
+ {
+ case nsMsgViewSortType.byDate:
+ case nsMsgViewSortType.byReceived:
+ case nsMsgViewSortType.byId:
+ case nsMsgViewSortType.byThread:
+ scrolled = ScrollToMessage(nsMsgNavigationType.lastMessage, true, false /* selectMessage */);
+ break;
+ }
+ }
+
+ // if still we haven't scrolled,
+ // scroll to the top.
+ if (!scrolled)
+ EnsureRowInThreadTreeIsVisible(0);
+ }
+}
+
+// the folderListener object
+var folderListener =
+{
+ onFolderAdded: function(parentFolder, child) {},
+ onMessageAdded: function(parentFolder, msg) {},
+ onFolderRemoved: function(parentFolder, child) {},
+ onMessageRemoved: function(parentFolder, msg) {},
+
+ onFolderPropertyChanged: function(item, property, oldValue, newValue) {},
+ onFolderBoolPropertyChanged: function(item, property, oldValue, newValue) {},
+ onFolderUnicharPropertyChanged: function(item, property, oldValue, newValue) {},
+ onFolderPropertyFlagChanged: function(item, property, oldFlag, newFlag) {},
+
+ onFolderIntPropertyChanged: function(item, property, oldValue, newValue)
+ {
+ // handle the currently visible folder
+ if (item == gMsgFolderSelected)
+ {
+ if (property == "TotalMessages" || property == "TotalUnreadMessages")
+ {
+ UpdateStatusMessageCounts(gMsgFolderSelected);
+ }
+ }
+
+ // check folders shown in tabs
+ if (item instanceof Ci.nsIMsgFolder)
+ {
+ // find corresponding tabinfos
+ // we may have the folder openened in more than one tab
+ let tabmail = GetTabMail();
+ for (let i = 0; i < tabmail.tabInfo.length; ++i)
+ {
+ // if we never switched away from the tab, we only have just one
+ let tabFolder = tabmail.tabInfo[i].msgSelectedFolder || gMsgFolderSelected;
+ if (tabFolder == item)
+ {
+ // update tab title incl. any icon styles
+ tabmail.setTabTitle(tabmail.tabInfo[i]);
+ }
+ }
+ }
+ },
+
+ onFolderEvent: function(folder, event) {
+ if (event == "FolderLoaded") {
+ if (folder) {
+ var scrolled = false;
+ var msgFolder = folder.QueryInterface(Ci.nsIMsgFolder);
+ var uri = folder.URI;
+ var rerootingFolder = (uri == gCurrentFolderToReroot);
+ if (rerootingFolder) {
+ viewDebug("uri = gCurrentFolderToReroot, setting gQSViewIsDirty\n");
+ gQSViewIsDirty = true;
+ gCurrentFolderToReroot = null;
+ if (msgFolder) {
+ msgFolder.endFolderLoading();
+ // Suppress command updating when rerooting the folder.
+ // When rerooting, we'll be clearing the selection
+ // which will cause us to update commands.
+ if (gDBView) {
+ gDBView.suppressCommandUpdating = true;
+ // If the db's view isn't set, something went wrong and we
+ // should reroot the folder, which will re-open the view.
+ if (!gDBView.db)
+ gRerootOnFolderLoad = true;
+ }
+ if (gRerootOnFolderLoad)
+ RerootFolder(uri, msgFolder, gCurrentLoadingFolderViewType, gCurrentLoadingFolderViewFlags, gCurrentLoadingFolderSortType, gCurrentLoadingFolderSortOrder);
+
+ if (gDBView)
+ gDBView.suppressCommandUpdating = false;
+
+ gCurrentLoadingFolderSortType = 0;
+ gCurrentLoadingFolderSortOrder = 0;
+ gCurrentLoadingFolderViewType = 0;
+ gCurrentLoadingFolderViewFlags = 0;
+
+ // Used for rename folder msg loading after folder is loaded.
+ scrolled = LoadCurrentlyDisplayedMessage();
+
+ if (gStartMsgKey != nsMsgKey_None) {
+ scrolled = SelectAndScrollToKey(gStartMsgKey);
+ gStartMsgKey = nsMsgKey_None;
+ }
+
+ if (gNextMessageAfterLoad) {
+ var type = gNextMessageAfterLoad;
+ gNextMessageAfterLoad = null;
+
+ // Scroll to and select the proper message.
+ scrolled = ScrollToMessage(type, true, true /* selectMessage */);
+ }
+ }
+ }
+ if (uri == gCurrentLoadingFolderURI) {
+ viewDebug("uri == current loading folder uri\n");
+ gCurrentLoadingFolderURI = "";
+ // Scroll to message for virtual folders is done in
+ // gSearchNotificationListener.OnSearchDone (see searchBar.js).
+ if (!scrolled && gMsgFolderSelected &&
+ !(gMsgFolderSelected.flags & Ci.nsMsgFolderFlags.Virtual))
+ ScrollToMessageAfterFolderLoad(msgFolder);
+ SetBusyCursor(window, false);
+ }
+ // Folder loading is over,
+ // now issue quick search if there is an email address.
+ if (gVirtualFolderTerms)
+ viewDebug("in folder loaded gVirtualFolderTerms = " +
+ gVirtualFolderTerms + "\n");
+ if (gMsgFolderSelected)
+ viewDebug("in folder loaded gMsgFolderSelected = " +
+ gMsgFolderSelected.URI + "\n");
+ if (rerootingFolder)
+ {
+ if (gSearchEmailAddress)
+ {
+ Search(gSearchEmailAddress);
+ gSearchEmailAddress = null;
+ }
+ else if (gVirtualFolderTerms)
+ {
+ gDefaultSearchViewTerms = null;
+ viewDebug("searching gVirtualFolderTerms\n");
+ gDBView.viewFolder = gMsgFolderSelected;
+ ViewChangeByFolder(gMsgFolderSelected);
+ }
+ else if (gMsgFolderSelected &&
+ gMsgFolderSelected.flags & Ci.nsMsgFolderFlags.Virtual)
+ {
+ viewDebug("selected folder is virtual\n");
+ gDefaultSearchViewTerms = null;
+ }
+ else
+ {
+ // Get the view value from the folder.
+ if (msgFolder)
+ {
+ // If our new view is the same as the old view and we already
+ // have the list of search terms built up for the old view,
+ // just re-use it.
+ var result = GetMailViewForFolder(msgFolder);
+ if (GetSearchInput() && gCurrentViewValue == result && gDefaultSearchViewTerms)
+ {
+ viewDebug("searching gDefaultSearchViewTerms and rerootingFolder\n");
+ Search("");
+ }
+ else
+ {
+ viewDebug("changing view by value\n");
+ ViewChangeByValue(result);
+ }
+ }
+ }
+ }
+ }
+ }
+ else if (event == "ImapHdrDownloaded") {
+ if (folder) {
+ var imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ if (imapFolder) {
+ var hdrParser = imapFolder.hdrParser;
+ if (hdrParser) {
+ var msgHdr = hdrParser.GetNewMsgHdr();
+ if (msgHdr)
+ {
+ var hdrs = hdrParser.headers;
+ if (hdrs && hdrs.includes("X-attachment-size:")) {
+ msgHdr.OrFlags(Ci.nsMsgMessageFlags
+ .Attachment);
+ }
+ if (hdrs && hdrs.includes("X-image-size:")) {
+ msgHdr.setStringProperty("imageSize", "1");
+ }
+ }
+ }
+ }
+ }
+ }
+ else if (event == "DeleteOrMoveMsgCompleted") {
+ HandleDeleteOrMoveMsgCompleted(folder);
+ }
+ else if (event == "DeleteOrMoveMsgFailed") {
+ HandleDeleteOrMoveMsgFailed(folder);
+ }
+ else if (event == "AboutToCompact") {
+ if (gDBView)
+ gCurrentlyDisplayedMessage = gDBView.currentlyDisplayedMessage;
+ }
+ else if (event == "CompactCompleted") {
+ HandleCompactCompleted(folder);
+ }
+ else if (event == "RenameCompleted") {
+ // Clear this so we don't try to clear its new messages.
+ gMsgFolderSelected = null;
+ gFolderTreeView.selectFolder(folder);
+ }
+ else if (event == "JunkStatusChanged") {
+ HandleJunkStatusChanged(folder);
+ }
+ }
+}
+
+function HandleDeleteOrMoveMsgFailed(folder)
+{
+ gDBView.onDeleteCompleted(false);
+ if(IsCurrentLoadedFolder(folder)) {
+ if(gNextMessageAfterDelete) {
+ gNextMessageAfterDelete = null;
+ gNextMessageViewIndexAfterDelete = -2;
+ }
+ }
+
+ // fix me???
+ // ThreadPaneSelectionChange(true);
+}
+
+// WARNING
+// this is a fragile and complicated function.
+// be careful when hacking on it.
+// Don't forget about things like different imap
+// delete models, multiple views (from multiple thread panes,
+// search windows, stand alone message windows)
+function HandleDeleteOrMoveMsgCompleted(folder)
+{
+ // you might not have a db view. this can happen if
+ // biff fires when the 3 pane is set to account central.
+ if (!gDBView)
+ return;
+
+ gDBView.onDeleteCompleted(true);
+
+ if (!IsCurrentLoadedFolder(folder)) {
+ // default value after delete/move/copy is over
+ gNextMessageViewIndexAfterDelete = -2;
+ return;
+ }
+
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var treeSelection = treeView.selection;
+
+ if (gNextMessageViewIndexAfterDelete == -2) {
+ // a move or delete can cause our selection can change underneath us.
+ // this can happen when the user
+ // deletes message from the stand alone msg window
+ // or the search view, or another 3 pane
+ if (treeSelection.count == 0) {
+ // this can happen if you double clicked a message
+ // in the thread pane, and deleted it from the stand alone msg window
+ // see bug #172392
+ treeSelection.clearSelection();
+ setTitleFromFolder(folder, null);
+ ClearMessagePane();
+ UpdateMailToolbar("delete from another view, 0 rows now selected");
+ }
+ else if (treeSelection.count == 1) {
+ // this can happen if you had two messages selected
+ // in the thread pane, and you deleted one of them from another view
+ // (like the view in the stand alone msg window)
+ // since one item is selected, we should load it.
+ var startIndex = {};
+ var endIndex = {};
+ treeSelection.getRangeAt(0, startIndex, endIndex);
+
+ // select the selected item, so we'll load it
+ treeSelection.select(startIndex.value);
+ treeView.selectionChanged();
+
+ EnsureRowInThreadTreeIsVisible(startIndex.value);
+
+ UpdateMailToolbar("delete from another view, 1 row now selected");
+ }
+ else {
+ // this can happen if you have more than 2 messages selected
+ // in the thread pane, and you deleted one of them from another view
+ // (like the view in the stand alone msg window)
+ // since multiple messages are still selected, do nothing.
+ }
+ }
+ else {
+ if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None)
+ {
+ var viewSize = treeView.rowCount;
+ if (gNextMessageViewIndexAfterDelete >= viewSize)
+ {
+ if (viewSize > 0)
+ gNextMessageViewIndexAfterDelete = viewSize - 1;
+ else
+ {
+ gNextMessageViewIndexAfterDelete = nsMsgViewIndex_None;
+
+ // there is nothing to select since viewSize is 0
+ treeSelection.clearSelection();
+ setTitleFromFolder(folder, null);
+ ClearMessagePane();
+ UpdateMailToolbar("delete from current view, 0 rows left");
+ }
+ }
+ }
+
+ // if we are about to set the selection with a new element then DON'T clear
+ // the selection then add the next message to select. This just generates
+ // an extra round of command updating notifications that we are trying to
+ // optimize away.
+ if (gNextMessageViewIndexAfterDelete != nsMsgViewIndex_None)
+ {
+ // When deleting a message we don't update the commands
+ // when the selection goes to 0
+ // (we have a hack in nsMsgDBView which prevents that update)
+ // so there is no need to
+ // update commands when we select the next message after the delete;
+ // the commands already
+ // have the right update state...
+ gDBView.suppressCommandUpdating = true;
+
+ // This check makes sure that the tree does not perform a
+ // selection on a non selected row (row < 0), else assertions will
+ // be thrown.
+ if (gNextMessageViewIndexAfterDelete >= 0)
+ treeSelection.select(gNextMessageViewIndexAfterDelete);
+
+ // If gNextMessageViewIndexAfterDelete has the same value
+ // as the last index we had selected, the tree won't generate a
+ // selectionChanged notification for the tree view. So force a manual
+ // selection changed call.
+ // (don't worry it's cheap if we end up calling it twice).
+ if (treeView)
+ treeView.selectionChanged();
+
+ EnsureRowInThreadTreeIsVisible(gNextMessageViewIndexAfterDelete);
+ gDBView.suppressCommandUpdating = false;
+
+ // hook for extra toolbar items
+ // XXX TODO
+ // I think there is a bug in the suppression code above.
+ // What if I have two rows selected, and I hit delete,
+ // and so we load the next row.
+ // What if I have commands that only enable where
+ // exactly one row is selected?
+ UpdateMailToolbar("delete from current view, at least one row selected");
+ }
+ }
+
+ // default value after delete/move/copy is over
+ gNextMessageViewIndexAfterDelete = -2;
+}
+
+function HandleCompactCompleted(folder)
+{
+ if (folder && folder.server.type != "imap")
+ {
+ let msgFolder = msgWindow.openFolder;
+ if (msgFolder && folder.URI == msgFolder.URI)
+ {
+ // pretend the selection changed, to reselect the current folder+view.
+ gMsgFolderSelected = null;
+ msgWindow.openFolder = null;
+ FolderPaneSelectionChange();
+ LoadCurrentlyDisplayedMessage();
+ }
+ }
+}
+
+function LoadCurrentlyDisplayedMessage()
+{
+ var scrolled = (gCurrentlyDisplayedMessage != nsMsgViewIndex_None);
+ if (scrolled)
+ {
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var treeSelection = treeView.selection;
+ treeSelection.select(gCurrentlyDisplayedMessage);
+ if (treeView)
+ treeView.selectionChanged();
+ EnsureRowInThreadTreeIsVisible(gCurrentlyDisplayedMessage);
+ SetFocusThreadPane();
+ gCurrentlyDisplayedMessage = nsMsgViewIndex_None; //reset
+ }
+ return scrolled;
+}
+
+function IsCurrentLoadedFolder(aFolder)
+{
+ let msgFolderUri = aFolder.QueryInterface(Ci.nsIMsgFolder)
+ .URI;
+ let currentLoadedFolder = GetThreadPaneFolder();
+
+ // If the currently loaded folder is virtual,
+ // check if aFolder is one of its searched folders.
+ if (currentLoadedFolder.flags & Ci.nsMsgFolderFlags.Virtual)
+ {
+ return currentLoadedFolder.msgDatabase.dBFolderInfo
+ .getCharProperty("searchFolderUri").split("|")
+ .includes(msgFolderUri);
+ }
+
+ // Is aFolder the currently loaded folder?
+ return currentLoadedFolder.URI == msgFolderUri;
+}
+
+function ServerContainsFolder(server, folder)
+{
+ if (!folder || !server)
+ return false;
+
+ return server.equals(folder.server);
+}
+
+function SelectServer(server)
+{
+ gFolderTreeView.selectFolder(server.rootFolder);
+}
+
+// we have this incoming server listener in case we need to
+// alter the folder pane selection when a server is removed
+// or changed (currently, when the real username or real hostname change)
+var gThreePaneIncomingServerListener = {
+ onServerLoaded: function(server) {},
+ onServerUnloaded: function(server) {
+ var selectedFolders = GetSelectedMsgFolders();
+ for (var i = 0; i < selectedFolders.length; i++) {
+ if (ServerContainsFolder(server, selectedFolders[i])) {
+ if (accountManager.defaultAccount)
+ SelectServer(accountManager.defaultAccount.incomingServer);
+ // we've made a new selection, we're done
+ return;
+ }
+ }
+
+ // if nothing is selected at this point, better go select the default
+ // this could happen if nothing was selected when the server was removed
+ selectedFolders = GetSelectedMsgFolders();
+ if (selectedFolders.length == 0) {
+ if (accountManager.defaultAccount)
+ SelectServer(accountManager.defaultAccount.incomingServer);
+ }
+ },
+ onServerChanged: function(server) {
+ // if the current selected folder is on the server that changed
+ // and that server is an imap or news server,
+ // we need to update the selection.
+ // on those server types, we'll be reconnecting to the server
+ // and our currently selected folder will need to be reloaded
+ // or worse, be invalid.
+ if (server.type != "imap" && server.type !="nntp")
+ return;
+
+ var selectedFolders = GetSelectedMsgFolders();
+ for (var i = 0; i < selectedFolders.length; i++) {
+ // if the selected item is a server, we don't have to update
+ // the selection
+ if (!(selectedFolders[i].isServer) && ServerContainsFolder(server, selectedFolders[i])) {
+ SelectServer(server);
+ // we've made a new selection, we're done
+ return;
+ }
+ }
+ }
+}
+
+function UpdateMailPaneConfig() {
+ const dynamicIds = ["messagesBox", "mailContent", "messengerBox"];
+ var desiredId = dynamicIds[Services.prefs.getIntPref("mail.pane_config.dynamic")];
+ var messagePane = GetMessagePane();
+ if (messagePane.parentNode.id != desiredId) {
+ ClearAttachmentList();
+ var messagePaneSplitter = GetThreadAndMessagePaneSplitter();
+ var desiredParent = document.getElementById(desiredId);
+ // See Bug 381992. The ctor for the browser element will fire again when we
+ // re-insert the messagePaneBox back into the document.
+ // But the dtor doesn't fire when the element is removed from the document.
+ // Manually call destroy here to avoid a nasty leak.
+ getMessageBrowser().destroy();
+ desiredParent.appendChild(messagePaneSplitter);
+ desiredParent.appendChild(messagePane);
+ messagePaneSplitter.orient = desiredParent.orient;
+ // Reroot message display
+ InvalidateTabDBs();
+ let tabmail = GetTabMail();
+ tabmail.currentTabInfo = null;
+ tabmail.updateCurrentTab();
+ }
+}
+
+var MailPrefObserver = {
+ observe: function observe(subject, topic, prefName) {
+ if (topic == "nsPref:changed") {
+ if (prefName == "mail.pane_config.dynamic") {
+ UpdateMailPaneConfig();
+ } else if (prefName == "mail.showCondensedAddresses") {
+ let currentDisplayNameVersion =
+ Services.prefs.getIntPref("mail.displayname.version");
+ Services.prefs.setIntPref("mail.displayname.version",
+ ++currentDisplayNameVersion);
+
+ // Refresh the thread pane.
+ GetThreadTree().treeBoxObject.invalid();
+ }
+ }
+ }
+};
+
+/* Functions related to startup */
+function OnLoadMessenger()
+{
+ AddMailOfflineObserver();
+ CreateMailWindowGlobals();
+ Services.prefs.addObserver("mail.pane_config.dynamic", MailPrefObserver);
+ Services.prefs.addObserver("mail.showCondensedAddresses", MailPrefObserver);
+ UpdateMailPaneConfig();
+ Create3PaneGlobals();
+ verifyAccounts(null, false);
+ msgDBCacheManager.init();
+
+ // set the messenger default window size relative to the screen size
+ // initial default dimensions are 2/3 and 1/2 of the screen dimensions
+ if (!document.documentElement.hasAttribute("width")) {
+ let screenHeight = window.screen.availHeight;
+ let screenWidth = window.screen.availWidth;
+ let defaultHeight = Math.floor(screenHeight * 2 / 3);
+ let defaultWidth = Math.floor(screenWidth / 2);
+
+ // minimum dimensions are 1024x768 less padding unless restrained by screen
+ const minHeight = 768;
+ const minWidth = 1024;
+
+ if (defaultHeight < minHeight)
+ defaultHeight = Math.min(minHeight, screenHeight);
+ if (defaultWidth < minWidth)
+ defaultWidth = Math.min(minWidth, screenWidth);
+
+ // keep some distance to the borders, accounting for window decoration
+ document.documentElement.setAttribute("height", defaultHeight - 48);
+ document.documentElement.setAttribute("width", defaultWidth - 24);
+ }
+
+ // initialize tabmail system - see tabmail.js and tabmail.xml for details
+ let tabmail = GetTabMail();
+ tabmail.registerTabType(gMailNewsTabsType);
+ tabmail.openFirstTab();
+ Services.obs.addObserver(MailWindowIsClosing,
+ "quit-application-requested");
+
+ InitMsgWindow();
+ messenger.setWindow(window, msgWindow);
+
+ InitPanes();
+
+ MigrateJunkMailSettings();
+
+ accountManager.setSpecialFolders();
+ accountManager.loadVirtualFolders();
+ accountManager.addIncomingServerListener(gThreePaneIncomingServerListener);
+
+ AddToSession();
+
+ var startFolderUri = null;
+ //need to add to session before trying to load start folder otherwise listeners aren't
+ //set up correctly.
+ // argument[0] --> folder uri
+ // argument[1] --> optional message key
+ // argument[2] --> optional email address; // Will come from aim; needs to show msgs from buddy's email address.
+ if ("arguments" in window)
+ {
+ var args = window.arguments;
+ // filter our any feed urls that came in as arguments to the new window...
+ if (args.length && /^feed:/i.test(args[0]))
+ {
+ var feedHandler =
+ Cc["@mozilla.org/newsblog-feed-downloader;1"]
+ .getService(Ci.nsINewsBlogFeedDownloader);
+ if (feedHandler)
+ feedHandler.subscribeToFeed(args[0], null, msgWindow);
+ }
+ else
+ {
+ startFolderUri = (args.length > 0) ? args[0] : null;
+ }
+ gStartMsgKey = (args.length > 1) ? args[1] : nsMsgKey_None;
+ gSearchEmailAddress = (args.length > 2) ? args[2] : null;
+ }
+
+ window.setTimeout(loadStartFolder, 0, startFolderUri);
+
+ Services.obs.notifyObservers(window, "mail-startup-done");
+
+ // FIX ME - later we will be able to use onload from the overlay
+ OnLoadMsgHeaderPane();
+
+ gHaveLoadedMessage = false;
+
+ //Set focus to the Thread Pane the first time the window is opened.
+ SetFocusThreadPane();
+
+ // Before and after callbacks for the customizeToolbar code
+ var mailToolbox = getMailToolbox();
+ mailToolbox.customizeInit = MailToolboxCustomizeInit;
+ mailToolbox.customizeDone = MailToolboxCustomizeDone;
+ mailToolbox.customizeChange = MailToolboxCustomizeChange;
+
+ // initialize the sync UI
+ // gSyncUI.init();
+
+ window.addEventListener("AppCommand", HandleAppCommandEvent, true);
+
+ // Load the periodic filter timer.
+ PeriodicFilterManager.setupFiltering();
+}
+
+function HandleAppCommandEvent(evt)
+{
+ evt.stopPropagation();
+ switch (evt.command)
+ {
+ case "Back":
+ goDoCommand('cmd_goBack');
+ break;
+ case "Forward":
+ goDoCommand('cmd_goForward');
+ break;
+ case "Stop":
+ goDoCommand('cmd_stop');
+ break;
+ case "Search":
+ goDoCommand('cmd_search');
+ break;
+ case "Bookmarks":
+ toAddressBook();
+ break;
+ case "Reload":
+ goDoCommand('cmd_reload');
+ break;
+ case "Home":
+ goDoCommand('cmd_goStartPage');
+ break;
+ default:
+ break;
+ }
+}
+
+function OnUnloadMessenger()
+{
+ Services.prefs.removeObserver("mail.pane_config.dynamic", MailPrefObserver, false);
+ Services.prefs.removeObserver("mail.showCondensedAddresses", MailPrefObserver, false);
+ window.removeEventListener("AppCommand", HandleAppCommandEvent, true);
+ Services.obs.removeObserver(MailWindowIsClosing,
+ "quit-application-requested");
+
+ OnLeavingFolder(gMsgFolderSelected); // mark all read in current folder
+ accountManager.removeIncomingServerListener(gThreePaneIncomingServerListener);
+ GetTabMail().closeTabs();
+
+ // FIX ME - later we will be able to use onload from the overlay
+ OnUnloadMsgHeaderPane();
+ UnloadPanes();
+ OnMailWindowUnload();
+}
+
+// we probably want to warn if more than one tab is closed
+function MailWindowIsClosing(aCancelQuit, aTopic, aData)
+{
+ if (aTopic == "quit-application-requested" &&
+ aCancelQuit instanceof Ci.nsISupportsPRBool &&
+ aCancelQuit.data)
+ return false;
+
+ let tabmail = GetTabMail();
+ let reallyClose = tabmail.warnAboutClosingTabs(tabmail.closingTabsEnum.ALL);
+
+ if (!reallyClose && aTopic == "quit-application-requested")
+ aCancelQuit.data = true;
+
+ return reallyClose;
+}
+
+function Create3PaneGlobals()
+{
+ // Update <mailWindow.js> global variables.
+ accountCentralBox = document.getElementById("accountCentralBox");
+ gDisableViewsSearch = document.getElementById("mailDisableViewsSearch");
+
+ GetMessagePane().collapsed = true;
+}
+
+function loadStartFolder(initialUri)
+{
+ var defaultServer = null;
+ var startFolder;
+ var isLoginAtStartUpEnabled = false;
+
+ //First get default account
+ if (initialUri) {
+ startFolder = MailUtils.getFolderForURI(initialUri);
+ } else {
+ var defaultAccount = accountManager.defaultAccount;
+ if (defaultAccount) {
+ defaultServer = defaultAccount.incomingServer;
+ var rootMsgFolder = defaultServer.rootMsgFolder;
+
+ startFolder = rootMsgFolder;
+ // Enable check new mail once by turning checkmail pref 'on' to bring
+ // all users to one plane. This allows all users to go to Inbox. User can
+ // always go to server settings panel and turn off "Check for new mail at startup"
+ if (!Services.prefs.getBoolPref(kMailCheckOncePrefName))
+ {
+ Services.prefs.setBoolPref(kMailCheckOncePrefName, true);
+ defaultServer.loginAtStartUp = true;
+ }
+
+ // Get the user pref to see if the login at startup is enabled for default account
+ isLoginAtStartUpEnabled = defaultServer.loginAtStartUp;
+
+ // Get Inbox only if login at startup is enabled.
+ if (isLoginAtStartUpEnabled)
+ {
+ //now find Inbox
+ const kInboxFlag = Ci.nsMsgFolderFlags.Inbox;
+ var inboxFolder = rootMsgFolder.getFolderWithFlags(kInboxFlag);
+ if (inboxFolder)
+ startFolder = inboxFolder;
+ }
+ } else {
+ // If no default account then show account central page.
+ ShowAccountCentral();
+ }
+
+ }
+
+ if (startFolder) {
+ try {
+ gFolderTreeView.selectFolder(startFolder);
+ } catch(ex) {
+ // This means we tried to select a folder that isn't in the current
+ // view. Just select the first one in the view then.
+ if (gFolderTreeView._rowMap.length)
+ gFolderTreeView.selectFolder(gFolderTreeView._rowMap[0]._folder);
+ }
+
+ // Perform biff on the server to check for new mail, if:
+ // the login at startup is enabled, and
+ // this feature is not exceptionally overridden, and
+ // the account is not deferred-to or deferred.
+ if (isLoginAtStartUpEnabled &&
+ gLoadStartFolder &&
+ !defaultServer.isDeferredTo &&
+ defaultServer.rootFolder == defaultServer.rootMsgFolder)
+ defaultServer.performBiff(msgWindow);
+ }
+
+ MsgGetMessagesForAllServers(defaultServer);
+
+ if (CheckForUnsentMessages() && !Services.io.offline)
+ {
+ InitPrompts();
+ InitServices();
+
+ var sendUnsentWhenGoingOnlinePref = Services.prefs.getIntPref("offline.send.unsent_messages");
+ if (sendUnsentWhenGoingOnlinePref == 0) // pref is "ask"
+ {
+ var buttonPressed = Services.prompt.confirmEx(window,
+ gOfflinePromptsBundle.getString('sendMessagesOfflineWindowTitle'),
+ gOfflinePromptsBundle.getString('sendMessagesLabel2'),
+ Services.prompt.BUTTON_TITLE_IS_STRING * (Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_POS_1),
+ gOfflinePromptsBundle.getString('sendMessagesSendButtonLabel'),
+ gOfflinePromptsBundle.getString('sendMessagesNoSendButtonLabel'),
+ null, null, {value:0});
+ if (buttonPressed == 0)
+ SendUnsentMessages();
+ }
+ else if(sendUnsentWhenGoingOnlinePref == 1) // pref is "yes"
+ SendUnsentMessages();
+ }
+}
+
+function AddToSession()
+{
+ var nsIFolderListener = Ci.nsIFolderListener;
+ var notifyFlags = nsIFolderListener.intPropertyChanged |
+ nsIFolderListener.event;
+ MailServices.mailSession.AddFolderListener(folderListener, notifyFlags);
+}
+
+function InitPanes()
+{
+ gFolderTreeView.load(document.getElementById("folderTree"),
+ "folderTree.json");
+ var folderTree = document.getElementById("folderTree");
+ folderTree.addEventListener("click", FolderPaneOnClick, true);
+ folderTree.addEventListener("mousedown", TreeOnMouseDown, true);
+
+ OnLoadThreadPane();
+ SetupCommandUpdateHandlers();
+}
+
+function UnloadPanes()
+{
+ var folderTree = document.getElementById("folderTree");
+ folderTree.removeEventListener("click", FolderPaneOnClick, true);
+ folderTree.removeEventListener("mousedown", TreeOnMouseDown, true);
+ gFolderTreeView.unload("folderTree.json");
+ UnloadCommandUpdateHandlers();
+}
+
+function AddMutationObserver(callback)
+{
+ new MutationObserver(callback).observe(callback(), {attributes: true, attributeFilter: ["hidden"]});
+}
+
+function OnLoadThreadPane()
+{
+ AddMutationObserver(UpdateAttachmentCol);
+}
+
+function UpdateAttachmentCol()
+{
+ var attachmentCol = document.getElementById("attachmentCol");
+ var threadTree = GetThreadTree();
+ threadTree.setAttribute("noattachcol", attachmentCol.getAttribute("hidden"));
+ threadTree.treeBoxObject.clearStyleAndImageCaches();
+ return attachmentCol;
+}
+
+function GetSearchInput()
+{
+ if (!gSearchInput)
+ gSearchInput = document.getElementById("searchInput");
+ return gSearchInput;
+}
+
+function GetMessagePaneFrame()
+{
+ return window.content;
+}
+
+function FindInSidebar(currentWindow, id)
+{
+ var item = currentWindow.document.getElementById(id);
+ if (item)
+ return item;
+
+ for (var i = 0; i < currentWindow.frames.length; ++i)
+ {
+ var frameItem = FindInSidebar(currentWindow.frames[i], id);
+ if (frameItem)
+ return frameItem;
+ }
+
+ return null;
+}
+
+function GetUnreadCountElement()
+{
+ if (!gUnreadCount)
+ gUnreadCount = document.getElementById('unreadMessageCount');
+ return gUnreadCount;
+}
+
+function GetTotalCountElement()
+{
+ if (!gTotalCount)
+ gTotalCount = document.getElementById('totalMessageCount');
+ return gTotalCount;
+}
+
+function ClearThreadPaneSelection()
+{
+ try {
+ if (gDBView) {
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var treeSelection = treeView.selection;
+ if (treeSelection)
+ treeSelection.clearSelection();
+ }
+ }
+ catch (ex) {
+ dump("ClearThreadPaneSelection: ex = " + ex + "\n");
+ }
+}
+
+function ClearMessagePane()
+{
+ if (gHaveLoadedMessage)
+ {
+ gHaveLoadedMessage = false;
+ gCurrentDisplayedMessage = null;
+ if (GetMessagePaneFrame().location.href != "about:blank")
+ GetMessagePaneFrame().location.href = "about:blank";
+
+ // hide the message header view AND the message pane...
+ HideMessageHeaderPane();
+ gMessageNotificationBar.clearMsgNotifications();
+ ClearPendingReadTimer();
+ }
+}
+
+// Function to change the highlighted row to where the mouse was clicked
+// without loading the contents of the selected row.
+// It will also keep the outline/dotted line in the original row.
+function ChangeSelectionWithoutContentLoad(event, tree)
+{
+ // usually, we're only interested in tree content clicks, not scrollbars etc.
+ if (event.originalTarget.localName != "treechildren")
+ return;
+
+ var treeBoxObj = tree.treeBoxObject;
+ var treeSelection = tree.view.selection;
+
+ var row = treeBoxObj.getRowAt(event.clientX, event.clientY);
+ // make sure that row.value is valid so that it doesn't mess up
+ // the call to ensureRowIsVisible().
+ if ((row >= 0) && !treeSelection.isSelected(row))
+ {
+ var saveCurrentIndex = treeSelection.currentIndex;
+ treeSelection.selectEventsSuppressed = true;
+ treeSelection.select(row);
+ treeSelection.currentIndex = saveCurrentIndex;
+ treeBoxObj.ensureRowIsVisible(row);
+ treeSelection.selectEventsSuppressed = false;
+
+ // Keep track of which row in the thread pane is currently selected.
+ if (tree.id == "threadTree")
+ gThreadPaneCurrentSelectedIndex = row;
+ }
+ event.stopPropagation();
+}
+
+function TreeOnMouseDown(event)
+{
+ // Detect right mouse click and change the highlight to the row
+ // where the click happened without loading the message headers in
+ // the Folder or Thread Pane.
+ // Same for middle click, which will open the folder/message in a tab.
+ gRightMouseButtonDown = event.button == kMouseButtonRight;
+ if (!gRightMouseButtonDown)
+ gRightMouseButtonDown = AllowOpenTabOnMiddleClick() &&
+ event.button == kMouseButtonMiddle;
+ if (gRightMouseButtonDown)
+ ChangeSelectionWithoutContentLoad(event, event.target.parentNode);
+}
+
+function FolderPaneContextMenuNewTab(event) {
+ var bgLoad = Services.prefs.getBoolPref("mail.tabs.loadInBackground");
+ if (event.shiftKey)
+ bgLoad = !bgLoad;
+ MsgOpenNewTabForFolder(bgLoad);
+}
+
+function FolderPaneOnClick(event)
+{
+ // usually, we're only interested in tree content clicks, not scrollbars etc.
+ if (event.originalTarget.localName != "treechildren")
+ return;
+
+ var folderTree = document.getElementById("folderTree");
+
+ // we may want to open the folder in a new tab on middle click
+ if (event.button == kMouseButtonMiddle)
+ {
+ if (AllowOpenTabOnMiddleClick())
+ {
+ FolderPaneContextMenuNewTab(event);
+ RestoreSelectionWithoutContentLoad(folderTree);
+ return;
+ }
+ }
+
+ // otherwise, we only care about left click events
+ if (event.button != kMouseButtonLeft)
+ return;
+
+ var cell = folderTree.treeBoxObject.getCellAt(event.clientX, event.clientY);
+ if (cell.row == -1)
+ {
+ if (event.originalTarget.localName == "treecol")
+ {
+ // clicking on the name column in the folder pane should not sort
+ event.stopPropagation();
+ }
+ }
+}
+
+function OpenMessageInNewTab(event) {
+ var bgLoad = Services.prefs.getBoolPref("mail.tabs.loadInBackground");
+ if (event.shiftKey)
+ bgLoad = !bgLoad;
+
+ MsgOpenNewTabForMessage(bgLoad);
+}
+
+function GetSelectedMsgFolders()
+{
+ return gFolderTreeView.getSelectedFolders();
+}
+
+function GetSelectedIndices(dbView)
+{
+ try {
+ return dbView.getIndicesForSelection();
+ }
+ catch (ex) {
+ dump("ex = " + ex + "\n");
+ return null;
+ }
+}
+
+function GetLoadedMsgFolder()
+{
+ if (!gDBView) return null;
+ return gDBView.msgFolder;
+}
+
+function GetLoadedMessage()
+{
+ try {
+ return gDBView.URIForFirstSelectedMessage;
+ }
+ catch (ex) {
+ return null;
+ }
+}
+
+//Clear everything related to the current message. called after load start page.
+function ClearMessageSelection()
+{
+ ClearThreadPaneSelection();
+}
+
+// Figures out how many messages are selected (hilighted - does not necessarily
+// have the dotted outline) above a given index row value in the thread pane.
+function NumberOfSelectedMessagesAboveCurrentIndex(index)
+{
+ var numberOfMessages = 0;
+ var indicies = GetSelectedIndices(gDBView);
+
+ if (indicies && indicies.length)
+ {
+ for (var i = 0; i < indicies.length; i++)
+ {
+ if (indicies[i] < index)
+ ++numberOfMessages;
+ else
+ break;
+ }
+ }
+ return numberOfMessages;
+}
+
+function SetNextMessageAfterDelete()
+{
+ var treeSelection = GetThreadTree().view.selection;
+
+ if (treeSelection.isSelected(treeSelection.currentIndex))
+ gNextMessageViewIndexAfterDelete = gDBView.msgToSelectAfterDelete;
+ else if(gDBView.removeRowOnMoveOrDelete)
+ {
+ // Only set gThreadPaneDeleteOrMoveOccurred to true if the message was
+ // truly moved to the trash or deleted, as opposed to an IMAP delete
+ // (where it is only "marked as deleted". This will prevent bug 142065.
+ //
+ // If it's an IMAP delete, then just set gNextMessageViewIndexAfterDelete
+ // to treeSelection.currentIndex (where the outline is at) because nothing
+ // was moved or deleted from the folder.
+ gThreadPaneDeleteOrMoveOccurred = true;
+ gNextMessageViewIndexAfterDelete = treeSelection.currentIndex - NumberOfSelectedMessagesAboveCurrentIndex(treeSelection.currentIndex);
+ }
+ else
+ gNextMessageViewIndexAfterDelete = treeSelection.currentIndex;
+}
+
+function EnsureFolderIndex(treeView, msgFolder) {
+ // Try to get the index of the folder in the tree.
+ let index = treeView.getIndexOfFolder(msgFolder);
+ if (!index) {
+ // If we couldn't find the folder, open the parents.
+ let folder = msgFolder;
+ while (!index && folder) {
+ folder = folder.parent;
+ index = EnsureFolderIndex(treeView, folder);
+ }
+ if (index) {
+ treeView.toggleOpenState(index);
+ index = treeView.getIndexOfFolder(msgFolder);
+ }
+ }
+ return index;
+}
+
+function SelectMsgFolder(msgFolder) {
+ gFolderTreeView.selectFolder(msgFolder);
+}
+
+function SelectMessage(messageUri)
+{
+ var msgHdr = messenger.msgHdrFromURI(messageUri);
+ if (msgHdr)
+ gDBView.selectMsgByKey(msgHdr.messageKey);
+}
+
+function ReloadMessage()
+{
+ gDBView.reloadMessage();
+}
+
+function SetBusyCursor(window, enable)
+{
+ // setCursor() is only available for chrome windows.
+ // However one of our frames is the start page which
+ // is a non-chrome window, so check if this window has a
+ // setCursor method
+ if ("setCursor" in window) {
+ if (enable)
+ window.setCursor("progress");
+ else
+ window.setCursor("auto");
+ }
+
+ var numFrames = window.frames.length;
+ for(var i = 0; i < numFrames; i++)
+ SetBusyCursor(window.frames[i], enable);
+}
+
+function GetDBView()
+{
+ return gDBView;
+}
+
+// Some of the per account junk mail settings have been
+// converted to global prefs. Let's try to migrate some
+// of those settings from the default account.
+function MigrateJunkMailSettings()
+{
+ var junkMailSettingsVersion = Services.prefs.getIntPref("mail.spam.version");
+ if (!junkMailSettingsVersion)
+ {
+ // Get the default account, check to see if we have values for our
+ // globally migrated prefs.
+ var defaultAccount = accountManager.defaultAccount;
+ if (defaultAccount)
+ {
+ // we only care about
+ var prefix = "mail.server." + defaultAccount.incomingServer.key + ".";
+ if (Services.prefs.prefHasUserValue(prefix + "manualMark"))
+ Services.prefs.setBoolPref("mail.spam.manualMark", Services.prefs.getBoolPref(prefix + "manualMark"));
+ if (Services.prefs.prefHasUserValue(prefix + "manualMarkMode"))
+ Services.prefs.setIntPref("mail.spam.manualMarkMode", Services.prefs.getIntPref(prefix + "manualMarkMode"));
+ if (Services.prefs.prefHasUserValue(prefix + "spamLoggingEnabled"))
+ Services.prefs.setBoolPref("mail.spam.logging.enabled", Services.prefs.getBoolPref(prefix + "spamLoggingEnabled"));
+ if (Services.prefs.prefHasUserValue(prefix + "markAsReadOnSpam"))
+ Services.prefs.setBoolPref("mail.spam.markAsReadOnSpam", Services.prefs.getBoolPref(prefix + "markAsReadOnSpam"));
+ }
+ // bump the version so we don't bother doing this again.
+ Services.prefs.setIntPref("mail.spam.version", 1);
+ }
+}
diff --git a/comm/suite/mailnews/content/msgViewNavigation.js b/comm/suite/mailnews/content/msgViewNavigation.js
new file mode 100644
index 0000000000..a7d0496210
--- /dev/null
+++ b/comm/suite/mailnews/content/msgViewNavigation.js
@@ -0,0 +1,243 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+/* This file contains the js functions necessary to implement view navigation within the 3 pane. */
+
+const {FolderUtils} = ChromeUtils.import("resource:///modules/FolderUtils.jsm");
+
+//NOTE: gMessengerBundle must be defined and set or this Overlay won't work
+
+function GetSubFoldersInFolderPaneOrder(folder)
+{
+ var msgFolders = folder.subFolders;
+
+ function compareFolderSortKey(folder1, folder2) {
+ return folder1.compareSortKeys(folder2);
+ }
+
+ // sort the subfolders
+ msgFolders.sort(compareFolderSortKey);
+ return msgFolders;
+}
+
+function FindNextChildFolder(aParent, aAfter)
+{
+ // Search the child folders of aParent for unread messages
+ // but in the case that we are working up from the current folder
+ // we need to skip up to and including the current folder
+ // we skip the current folder in case a mail view is hiding unread messages
+ if (aParent.getNumUnread(true) > 0) {
+ var subFolders = GetSubFoldersInFolderPaneOrder(aParent);
+ var i = 0;
+ var folder = null;
+
+ // Skip folders until after the specified child
+ while (folder != aAfter)
+ folder = subFolders[i++];
+
+ let ignoreFlags = Ci.nsMsgFolderFlags.Trash | Ci.nsMsgFolderFlags.SentMail |
+ Ci.nsMsgFolderFlags.Drafts | Ci.nsMsgFolderFlags.Queue |
+ Ci.nsMsgFolderFlags.Templates | Ci.nsMsgFolderFlags.Junk;
+ while (i < subFolders.length) {
+ folder = subFolders[i++];
+ // if there is unread mail in the trash, sent, drafts, unsent messages
+ // templates or junk special folder,
+ // we ignore it when doing cross folder "next" navigation
+ if (!folder.isSpecialFolder(ignoreFlags, true)) {
+ if (folder.getNumUnread(false) > 0)
+ return folder;
+
+ folder = FindNextChildFolder(folder, null);
+ if (folder)
+ return folder;
+ }
+ }
+ }
+
+ return null;
+}
+
+function FindNextFolder()
+{
+ // look for the next folder, this will only look on the current account
+ // and below us, in the folder pane
+ // note use of gDBView restricts this function to message folders
+ // otherwise you could go next unread from a server
+ var folder = FindNextChildFolder(gDBView.msgFolder, null);
+ if (folder)
+ return folder;
+
+ // didn't find folder in children
+ // go up to the parent, and start at the folder after the current one
+ // unless we are at a server, in which case bail out.
+ for (folder = gDBView.msgFolder; !folder.isServer; ) {
+
+ var parent = folder.parent;
+ folder = FindNextChildFolder(parent, folder);
+ if (folder)
+ return folder;
+
+ // none at this level after the current folder. go up.
+ folder = parent;
+ }
+
+ // nothing in the current account, start with the next account (below)
+ // and try until we hit the bottom of the folder pane
+
+ // start at the account after the current account
+ var rootFolders = GetRootFoldersInFolderPaneOrder();
+ for (var i = 0; i < rootFolders.length; i++) {
+ if (rootFolders[i].URI == gDBView.msgFolder.server.serverURI)
+ break;
+ }
+
+ for (var j = i + 1; j < rootFolders.length; j++) {
+ folder = FindNextChildFolder(rootFolders[j], null);
+ if (folder)
+ return folder;
+ }
+
+ // if nothing from the current account down to the bottom
+ // (of the folder pane), start again at the top.
+ for (j = 0; j <= i; j++) {
+ folder = FindNextChildFolder(rootFolders[j], null);
+ if (folder)
+ return folder;
+ }
+ return null;
+}
+
+function GetRootFoldersInFolderPaneOrder()
+{
+ var accounts = FolderUtils.allAccountsSorted(false);
+
+ var serversMsgFolders = [];
+ for (var account of accounts)
+ serversMsgFolders.push(account.incomingServer.rootMsgFolder);
+
+ return serversMsgFolders;
+}
+
+function CrossFolderNavigation(type)
+{
+ // do cross folder navigation for next unread message/thread and message history
+ if (type != nsMsgNavigationType.nextUnreadMessage &&
+ type != nsMsgNavigationType.nextUnreadThread &&
+ type != nsMsgNavigationType.forward &&
+ type != nsMsgNavigationType.back)
+ return;
+
+ if (type == nsMsgNavigationType.nextUnreadMessage ||
+ type == nsMsgNavigationType.nextUnreadThread)
+ {
+
+ var nextMode = Services.prefs.getIntPref("mailnews.nav_crosses_folders");
+ // 0: "next" goes to the next folder, without prompting
+ // 1: "next" goes to the next folder, and prompts (the default)
+ // 2: "next" does nothing when there are no unread messages
+
+ // not crossing folders, don't find next
+ if (nextMode == 2)
+ return;
+
+ var folder = FindNextFolder();
+ if (folder && (gDBView.msgFolder.URI != folder.URI))
+ {
+ switch (nextMode)
+ {
+ case 0:
+ // do this unconditionally
+ gNextMessageAfterLoad = type;
+ SelectMsgFolder(folder);
+ break;
+ case 1:
+ default:
+ var promptText = gMessengerBundle.getFormattedString("advanceNextPrompt", [ folder.name ], 1);
+ if (Services.prompt.confirmEx(window, null, promptText,
+ Services.prompt.STD_YES_NO_BUTTONS,
+ null, null, null, null, {}) == 0)
+ {
+ gNextMessageAfterLoad = type;
+ SelectMsgFolder(folder);
+ }
+ break;
+ }
+ }
+ }
+ else
+ {
+ // if no message is loaded, relPos should be 0, to
+ // go back to the previously loaded message
+ var relPos = (type == nsMsgNavigationType.forward)
+ ? 1 : ((GetLoadedMessage()) ? -1 : 0);
+ var folderUri = messenger.getFolderUriAtNavigatePos(relPos);
+ var msgHdr = messenger.msgHdrFromURI(messenger.getMsgUriAtNavigatePos(relPos));
+ gStartMsgKey = msgHdr.messageKey;
+ var curPos = messenger.navigatePos;
+ curPos += relPos;
+ messenger.navigatePos = curPos;
+ SelectMsgFolder(MailUtils.getFolderForURI(folderUri));
+ }
+}
+
+
+function ScrollToMessage(type, wrap, selectMessage)
+{
+ try {
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var treeSelection = treeView.selection;
+ var currentIndex = treeSelection.currentIndex;
+
+ var resultId = new Object;
+ var resultIndex = new Object;
+ var threadIndex = new Object;
+
+ let elidedFlag = Ci.nsMsgMessageFlags.Elided;
+ let summarizeSelection =
+ Services.prefs.getBoolPref("mail.operate_on_msgs_in_collapsed_threads");
+
+ // if we're doing next unread, and a collapsed thread is selected, and
+ // the top level message is unread, just set the result manually to
+ // the top level message, without using gDBView.viewNavigate.
+ if (summarizeSelection && type == nsMsgNavigationType.nextUnreadMessage &&
+ currentIndex != -1 &&
+ gDBView.getFlagsAt(currentIndex) & elidedFlag &&
+ gDBView.isContainer(currentIndex) &&
+ ! (gDBView.getFlagsAt(currentIndex) &
+ Ci.nsMsgMessageFlags.Read)) {
+ resultIndex.value = currentIndex;
+ resultId.value = gDBView.getKeyAt(currentIndex);
+ } else {
+ gDBView.viewNavigate(type, resultId, resultIndex, threadIndex, true /* wrap */);
+ }
+
+ // only scroll and select if we found something
+ if ((resultId.value != nsMsgViewIndex_None) && (resultIndex.value != nsMsgViewIndex_None)) {
+ if (gDBView.getFlagsAt(resultIndex.value) & elidedFlag &&
+ summarizeSelection)
+ gDBView.toggleOpenState(resultIndex.value);
+
+ if (selectMessage){
+ treeSelection.select(resultIndex.value);
+ }
+ EnsureRowInThreadTreeIsVisible(resultIndex.value);
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+ catch (ex) {
+ return false;
+ }
+}
+
+function GoNextMessage(type, startFromBeginning)
+{
+ if (!ScrollToMessage(type, startFromBeginning, true))
+ CrossFolderNavigation(type);
+
+ SetFocusThreadPaneIfNotOnMessagePane();
+}
diff --git a/comm/suite/mailnews/content/msgViewPickerOverlay.js b/comm/suite/mailnews/content/msgViewPickerOverlay.js
new file mode 100644
index 0000000000..39b3286b5d
--- /dev/null
+++ b/comm/suite/mailnews/content/msgViewPickerOverlay.js
@@ -0,0 +1,413 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// menuitem value constants
+// tag views have kViewTagMarker + their key as value
+const kViewItemAll = 0;
+const kViewItemUnread = 1;
+const kViewItemTags = 2; // former labels used values 2-6
+const kViewItemNotDeleted = 3;
+const kViewItemVirtual = 7;
+const kViewItemCustomize = 8;
+const kViewItemFirstCustom = 9;
+
+const kViewCurrent = "current-view";
+const kViewCurrentTag = "current-view-tag";
+const kViewTagMarker = ":";
+
+var gMailViewList = null;
+var gCurrentViewValue = kViewItemAll;
+var gCurrentViewLabel = "";
+var gSaveDefaultSVTerms;
+
+var nsMsgSearchScope = Ci.nsMsgSearchScope;
+var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+var nsMsgSearchOp = Ci.nsMsgSearchOp;
+
+
+// perform the view/action requested by the aValue string
+// and set the view picker label to the aLabel string
+function ViewChange(aValue, aLabel)
+{
+ if (aValue == kViewItemCustomize || aValue == kViewItemVirtual)
+ {
+ // restore to the previous view value, in case they cancel
+ UpdateViewPicker(gCurrentViewValue, gCurrentViewLabel);
+ if (aValue == kViewItemCustomize)
+ LaunchCustomizeDialog();
+ else
+ {
+ gFolderTreeController.newVirtualFolder(gCurrentViewLabel,
+ gSaveDefaultSVTerms);
+ }
+ return;
+ }
+
+ // persist the view
+ gCurrentViewValue = aValue;
+ gCurrentViewLabel = aLabel;
+ SetMailViewForFolder(GetFirstSelectedMsgFolder(), gCurrentViewValue)
+ UpdateViewPicker(gCurrentViewValue, gCurrentViewLabel);
+
+ // tag menuitem values are of the form :<keyword>
+ if (isNaN(aValue))
+ {
+ // split off the tag key
+ var tagkey = aValue.substr(kViewTagMarker.length);
+ ViewTagKeyword(tagkey);
+ }
+ else
+ {
+ var numval = Number(aValue);
+ switch (numval)
+ {
+ case kViewItemAll: // View All
+ gDefaultSearchViewTerms = null;
+ break;
+ case kViewItemUnread: // Unread
+ ViewNewMail();
+ break;
+ case kViewItemNotDeleted: // Not deleted
+ ViewNotDeletedMail();
+ break;
+ default:
+ // for legacy reasons, custom views start at index 9
+ LoadCustomMailView(numval - kViewItemFirstCustom);
+ break;
+ }
+ }
+ gSaveDefaultSVTerms = gDefaultSearchViewTerms;
+ onEnterInSearchBar();
+ gQSViewIsDirty = true;
+}
+
+
+function ViewChangeByMenuitem(aMenuitem)
+{
+ // Mac View menu menuitems don't have XBL bindings
+ ViewChange(aMenuitem.getAttribute("value"), aMenuitem.getAttribute("label"));
+}
+
+
+function ViewChangeByValue(aValue)
+{
+ ViewChange(aValue, GetLabelForValue(aValue));
+}
+
+function ViewChangeByFolder(aFolder)
+{
+ var result = GetMailViewForFolder(aFolder);
+ ViewChangeByValue(result);
+}
+
+function GetLabelForValue(aValue)
+{
+ var label = "";
+ var viewPickerPopup = document.getElementById("viewPickerPopup");
+ if (viewPickerPopup)
+ {
+ // grab the label for the menulist from one of its menuitems
+ var selectedItems = viewPickerPopup.getElementsByAttribute("value", aValue);
+ if (!selectedItems || !selectedItems.length)
+ {
+ // we may have a new item
+ RefreshAllViewPopups(viewPickerPopup);
+ selectedItems = viewPickerPopup.getElementsByAttribute("value", aValue);
+ }
+ label = selectedItems && selectedItems.length && selectedItems.item(0).getAttribute("label");
+ }
+ return label;
+}
+
+function UpdateViewPickerByValue(aValue)
+{
+ UpdateViewPicker(aValue, GetLabelForValue(aValue));
+}
+
+function UpdateViewPicker(aValue, aLabel)
+{
+ var viewPicker = document.getElementById("viewPicker");
+ if (viewPicker)
+ {
+ viewPicker.value = aValue;
+ viewPicker.setAttribute("label", aLabel);
+ }
+}
+
+function GetFolderInfo(aFolder)
+{
+ try
+ {
+ var db = aFolder.msgDatabase;
+ if (db)
+ return db.dBFolderInfo;
+ }
+ catch (ex) {}
+ return null;
+}
+
+
+function GetMailViewForFolder(aFolder)
+{
+ var val = "";
+ var folderInfo = GetFolderInfo(aFolder);
+ if (folderInfo)
+ {
+ val = folderInfo.getCharProperty(kViewCurrentTag);
+ if (!val)
+ {
+ // no new view value, thus using the old
+ var numval = folderInfo.getUint32Property(kViewCurrent, kViewItemAll);
+ // and migrate it, if it's a former label view (label views used values 2-6)
+ if ((kViewItemTags <= numval) && (numval < kViewItemVirtual))
+ val = kViewTagMarker + "$label" + (val - 1);
+ else
+ val = numval;
+ }
+ }
+ return val;
+}
+
+
+function SetMailViewForFolder(aFolder, aValue)
+{
+ var folderInfo = GetFolderInfo(aFolder);
+ if (folderInfo)
+ {
+ // we can't map tags back to labels in general,
+ // so set view to all for backwards compatibility in this case
+ folderInfo.setUint32Property (kViewCurrent, isNaN(aValue) ? kViewItemAll : aValue);
+ folderInfo.setCharProperty(kViewCurrentTag, aValue);
+ }
+}
+
+
+function LaunchCustomizeDialog()
+{
+ OpenOrFocusWindow({}, "mailnews:mailviewlist", "chrome://messenger/content/mailViewList.xul");
+}
+
+
+function LoadCustomMailView(index)
+{
+ PrepareForViewChange();
+ var searchTermsArrayForQS = CreateGroupedSearchTerms(gMailViewList.getMailViewAt(index).searchTerms);
+ createSearchTermsWithList(searchTermsArrayForQS);
+ AddVirtualFolderTerms(searchTermsArrayForQS);
+ gDefaultSearchViewTerms = searchTermsArrayForQS;
+}
+
+
+function ViewTagKeyword(keyword)
+{
+ PrepareForViewChange();
+
+ // create an i supports array to store our search terms
+ var searchTermsArray = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ var term = gSearchSession.createTerm();
+ var value = term.value;
+
+ value.str = keyword;
+ value.attrib = nsMsgSearchAttrib.Keywords;
+ term.value = value;
+ term.attrib = nsMsgSearchAttrib.Keywords;
+ term.op = nsMsgSearchOp.Contains;
+ term.booleanAnd = true;
+
+ searchTermsArray.appendElement(term);
+ AddVirtualFolderTerms(searchTermsArray);
+ createSearchTermsWithList(searchTermsArray);
+ gDefaultSearchViewTerms = searchTermsArray;
+}
+
+
+function ViewNewMail()
+{
+ PrepareForViewChange();
+
+ // create an i supports array to store our search terms
+ var searchTermsArray = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ var term = gSearchSession.createTerm();
+ var value = term.value;
+
+ value.status = 1;
+ value.attrib = nsMsgSearchAttrib.MsgStatus;
+ term.value = value;
+ term.attrib = nsMsgSearchAttrib.MsgStatus;
+ term.op = nsMsgSearchOp.Isnt;
+ term.booleanAnd = true;
+ searchTermsArray.appendElement(term);
+
+ AddVirtualFolderTerms(searchTermsArray);
+
+ createSearchTermsWithList(searchTermsArray);
+ // not quite right - these want to be just the view terms...but it might not matter.
+ gDefaultSearchViewTerms = searchTermsArray;
+}
+
+
+function ViewNotDeletedMail()
+{
+ PrepareForViewChange();
+
+ // create an i supports array to store our search terms
+ var searchTermsArray = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ var term = gSearchSession.createTerm();
+ var value = term.value;
+
+ value.status = 0x00200000;
+ value.attrib = nsMsgSearchAttrib.MsgStatus;
+ term.value = value;
+ term.attrib = nsMsgSearchAttrib.MsgStatus;
+ term.op = nsMsgSearchOp.Isnt;
+ term.booleanAnd = true;
+ searchTermsArray.appendElement(term);
+
+ AddVirtualFolderTerms(searchTermsArray);
+
+ createSearchTermsWithList(searchTermsArray);
+ // not quite right - these want to be just the view terms...but it might not matter.
+ gDefaultSearchViewTerms = searchTermsArray;
+}
+
+
+function AddVirtualFolderTerms(searchTermsArray)
+{
+ // add in any virtual folder terms
+ var virtualFolderSearchTerms = (gVirtualFolderTerms || gXFVirtualFolderTerms);
+ if (virtualFolderSearchTerms)
+ {
+ for (let virtualFolderSearchTerm of virtualFolderSearchTerms)
+ {
+ searchTermsArray.appendElement(virtualFolderSearchTerm);
+ }
+ }
+}
+
+
+function PrepareForViewChange()
+{
+ // this is a problem - it saves the current view in gPreQuickSearchView
+ // then we eventually call onEnterInSearchBar, and we think we need to restore the pre search view!
+ initializeSearchBar();
+ ClearThreadPaneSelection();
+ ClearMessagePane();
+}
+
+
+// refresh view popup and its subpopups
+function RefreshAllViewPopups(aViewPopup)
+{
+ var menupopups = aViewPopup.getElementsByTagName("menupopup");
+ if (menupopups.length > 1)
+ {
+ // when we have menupopups, we assume both tags and custom views are there
+ RefreshTagsPopup(menupopups[0]);
+ RefreshCustomViewsPopup(menupopups[1]);
+ }
+}
+
+
+function RefreshViewPopup(aViewPopup)
+{
+ // mark default views if selected
+ let viewAll = aViewPopup.getElementsByAttribute("value", kViewItemAll)[0];
+ viewAll.setAttribute("checked", gCurrentViewValue == kViewItemAll);
+ let viewUnread =
+ aViewPopup.getElementsByAttribute("value", kViewItemUnread)[0];
+ viewUnread.setAttribute("checked", gCurrentViewValue == kViewItemUnread);
+
+ let viewNotDeleted =
+ aViewPopup.getElementsByAttribute("value", kViewItemNotDeleted)[0];
+ var folderArray = GetSelectedMsgFolders();
+ if (folderArray.length == 0)
+ return;
+
+ // Only show the "Not Deleted" item for IMAP servers
+ // that are using the IMAP delete model.
+ viewNotDeleted.setAttribute("hidden", true);
+ var msgFolder = folderArray[0];
+ var server = msgFolder.server;
+ if (server.type == "imap")
+ {
+ let imapServer =
+ server.QueryInterface(Ci.nsIImapIncomingServer);
+ if (imapServer.deleteModel == Ci.nsMsgImapDeleteModels.IMAPDelete)
+ {
+ viewNotDeleted.setAttribute("hidden", false);
+ viewNotDeleted.setAttribute("checked",
+ gCurrentViewValue == kViewItemNotDeleted);
+ }
+ }
+}
+
+
+function RefreshCustomViewsPopup(aMenupopup)
+{
+ // for each mail view in the msg view list, add an entry in our combo box
+ if (!gMailViewList)
+ gMailViewList = Cc["@mozilla.org/messenger/mailviewlist;1"]
+ .getService(Ci.nsIMsgMailViewList);
+ // remove all menuitems
+ while (aMenupopup.hasChildNodes())
+ aMenupopup.lastChild.remove();
+
+ // now rebuild the list
+ var currentView = isNaN(gCurrentViewValue) ? kViewItemAll : Number(gCurrentViewValue);
+ var numItems = gMailViewList.mailViewCount;
+ for (var i = 0; i < numItems; ++i)
+ {
+ var viewInfo = gMailViewList.getMailViewAt(i);
+ var menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("label", viewInfo.prettyName);
+ menuitem.setAttribute("value", kViewItemFirstCustom + i);
+ menuitem.setAttribute("name", "viewmessages");
+ menuitem.setAttribute("type", "radio");
+ if (kViewItemFirstCustom + i == currentView)
+ menuitem.setAttribute("checked", true);
+ aMenupopup.appendChild(menuitem);
+ }
+}
+
+
+function RefreshTagsPopup(aMenupopup)
+{
+ // remove all menuitems
+ while (aMenupopup.hasChildNodes())
+ aMenupopup.lastChild.remove();
+
+ // create tag menuitems
+ var currentTagKey = isNaN(gCurrentViewValue) ? gCurrentViewValue.substr(kViewTagMarker.length) : "";
+ var tagArray = MailServices.tags.getAllTags();
+ for (var i = 0; i < tagArray.length; ++i)
+ {
+ var tagInfo = tagArray[i];
+ var menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("label", tagInfo.tag);
+ menuitem.setAttribute("value", kViewTagMarker + tagInfo.key);
+ menuitem.setAttribute("name", "viewmessages");
+ menuitem.setAttribute("type", "radio");
+ if (tagInfo.key == currentTagKey)
+ menuitem.setAttribute("checked", true);
+ var color = tagInfo.color;
+ if (color)
+ menuitem.setAttribute("class", "lc-" + color.substr(1));
+ aMenupopup.appendChild(menuitem);
+ }
+}
+
+
+function ViewPickerOnLoad()
+{
+ var viewPickerPopup = document.getElementById("viewPickerPopup");
+ if (viewPickerPopup)
+ RefreshAllViewPopups(viewPickerPopup);
+}
+
+
+window.addEventListener("load", ViewPickerOnLoad);
diff --git a/comm/suite/mailnews/content/nsDragAndDrop.js b/comm/suite/mailnews/content/nsDragAndDrop.js
new file mode 100644
index 0000000000..8808e5ecd0
--- /dev/null
+++ b/comm/suite/mailnews/content/nsDragAndDrop.js
@@ -0,0 +1,595 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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 OF THIS API FOR DRAG AND DROP IS DEPRECATED!
+// Do not use this file for new code.
+//
+// For documentation about what to use instead, see:
+// http://developer.mozilla.org/En/DragDrop/Drag_and_Drop
+//
+////////////////////////////////////////////////////////////////////////
+
+
+/**
+ * nsTransferable - a wrapper for nsITransferable that simplifies
+ * javascript clipboard and drag&drop. for use in
+ * these situations you should use the nsClipboard
+ * and nsDragAndDrop wrappers for more convenience
+ **/
+
+var nsTransferable = {
+ /**
+ * nsITransferable set (TransferData aTransferData) ;
+ *
+ * Creates a transferable with data for a list of supported types ("flavours")
+ *
+ * @param TransferData aTransferData
+ * a javascript object in the format described above
+ **/
+ set: function (aTransferDataSet)
+ {
+ var trans = this.createTransferable();
+ for (var i = 0; i < aTransferDataSet.dataList.length; ++i)
+ {
+ var currData = aTransferDataSet.dataList[i];
+ var currFlavour = currData.flavour.contentType;
+ trans.addDataFlavor(currFlavour);
+ var supports = null; // nsISupports data
+ var length = 0;
+ if (currData.flavour.dataIIDKey == "nsISupportsString")
+ {
+ supports = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+
+ supports.data = currData.supports;
+ length = supports.data.length;
+ }
+ else
+ {
+ // non-string data.
+ supports = currData.supports;
+ length = 0; // kFlavorHasDataProvider
+ }
+ trans.setTransferData(currFlavour, supports, length * 2);
+ }
+ return trans;
+ },
+
+ /**
+ * TransferData/TransferDataSet get (FlavourSet aFlavourSet,
+ * Function aRetrievalFunc, Boolean aAnyFlag) ;
+ *
+ * Retrieves data from the transferable provided in aRetrievalFunc, formatted
+ * for more convenient access.
+ *
+ * @param FlavourSet aFlavourSet
+ * a FlavourSet object that contains a list of supported flavours.
+ * @param Function aRetrievalFunc
+ * a reference to a function that returns a nsIArray of nsITransferables
+ * for each item from the specified source (clipboard/drag&drop etc)
+ * @param Boolean aAnyFlag
+ * a flag specifying whether or not a specific flavour is requested. If false,
+ * data of the type of the first flavour in the flavourlist parameter is returned,
+ * otherwise the best flavour supported will be returned.
+ **/
+ get: function (aFlavourSet, aRetrievalFunc, aAnyFlag)
+ {
+ if (!aRetrievalFunc)
+ throw "No data retrieval handler provided!";
+
+ var array = aRetrievalFunc(aFlavourSet);
+ var dataArray = [];
+
+ // Iterate over the number of items returned from aRetrievalFunc. For
+ // clipboard operations, this is 1, for drag and drop (where multiple
+ // items may have been dragged) this could be >1.
+ for (let i = 0; i < array.length; i++)
+ {
+ let trans = array.queryElementAt(i, Ci.nsITransferable);
+ if (!trans)
+ continue;
+
+ var data = { };
+ var length = { };
+
+ var currData = null;
+ if (aAnyFlag)
+ {
+ var flavour = { };
+ trans.getAnyTransferData(flavour, data, length);
+ if (data && flavour)
+ {
+ var selectedFlavour = aFlavourSet.flavourTable[flavour.value];
+ if (selectedFlavour)
+ dataArray[i] = FlavourToXfer(data.value, length.value, selectedFlavour);
+ }
+ }
+ else
+ {
+ var firstFlavour = aFlavourSet.flavours[0];
+ trans.getTransferData(firstFlavour, data, length);
+ if (data && firstFlavour)
+ dataArray[i] = FlavourToXfer(data.value, length.value, firstFlavour);
+ }
+ }
+ return new TransferDataSet(dataArray);
+ },
+
+ /**
+ * nsITransferable createTransferable (void) ;
+ *
+ * Creates and returns a transferable object.
+ **/
+ createTransferable: function ()
+ {
+ const kXferableContractID = "@mozilla.org/widget/transferable;1";
+ const kXferableIID = Ci.nsITransferable;
+ var trans = Cc[kXferableContractID].createInstance(kXferableIID);
+ trans.init(null);
+ return trans;
+ }
+};
+
+/**
+ * A FlavourSet is a simple type that represents a collection of Flavour objects.
+ * FlavourSet is constructed from an array of Flavours, and stores this list as
+ * an array and a hashtable. The rationale for the dual storage is as follows:
+ *
+ * Array: Ordering is important when adding data flavours to a transferable.
+ * Flavours added first are deemed to be 'preferred' by the client.
+ * Hash: Convenient lookup of flavour data using the content type (MIME type)
+ * of data as a key.
+ */
+function FlavourSet(aFlavourList)
+{
+ this.flavours = aFlavourList || [];
+ this.flavourTable = { };
+
+ this._XferID = "FlavourSet";
+
+ for (var i = 0; i < this.flavours.length; ++i)
+ this.flavourTable[this.flavours[i].contentType] = this.flavours[i];
+}
+
+FlavourSet.prototype = {
+ appendFlavour: function (aFlavour, aFlavourIIDKey)
+ {
+ var flavour = new Flavour (aFlavour, aFlavourIIDKey);
+ this.flavours.push(flavour);
+ this.flavourTable[flavour.contentType] = flavour;
+ }
+};
+
+/**
+ * A Flavour is a simple type that represents a data type that can be handled.
+ * It takes a content type (MIME type) which is used when storing data on the
+ * system clipboard/drag and drop, and an IIDKey (string interface name
+ * which is used to QI data to an appropriate form. The default interface is
+ * assumed to be wide-string.
+ */
+function Flavour(aContentType, aDataIIDKey)
+{
+ this.contentType = aContentType;
+ this.dataIIDKey = aDataIIDKey || "nsISupportsString";
+
+ this._XferID = "Flavour";
+}
+
+function TransferDataBase() {}
+TransferDataBase.prototype = {
+ push: function (aItems)
+ {
+ this.dataList.push(aItems);
+ },
+
+ get first ()
+ {
+ return "dataList" in this && this.dataList.length ? this.dataList[0] : null;
+ }
+};
+
+/**
+ * TransferDataSet is a list (array) of TransferData objects, which represents
+ * data dragged from one or more elements.
+ */
+function TransferDataSet(aTransferDataList)
+{
+ this.dataList = aTransferDataList || [];
+
+ this._XferID = "TransferDataSet";
+}
+TransferDataSet.prototype = TransferDataBase.prototype;
+
+/**
+ * TransferData is a list (array) of FlavourData for all the applicable content
+ * types associated with a drag from a single item.
+ */
+function TransferData(aFlavourDataList)
+{
+ this.dataList = aFlavourDataList || [];
+
+ this._XferID = "TransferData";
+}
+TransferData.prototype = {
+ __proto__: TransferDataBase.prototype,
+
+ addDataForFlavour: function (aFlavourString, aData, aLength, aDataIIDKey)
+ {
+ this.dataList.push(new FlavourData(aData, aLength,
+ new Flavour(aFlavourString, aDataIIDKey)));
+ }
+};
+
+/**
+ * FlavourData is a type that represents data retrieved from the system
+ * clipboard or drag and drop. It is constructed internally by the Transferable
+ * using the raw (nsISupports) data from the clipboard, the length of the data,
+ * and an object of type Flavour representing the type. Clients implementing
+ * IDragDropObserver receive an object of this type in their implementation of
+ * onDrop. They access the 'data' property to retrieve data, which is either data
+ * QI'ed to a usable form, or unicode string.
+ */
+function FlavourData(aData, aLength, aFlavour)
+{
+ this.supports = aData;
+ this.contentLength = aLength;
+ this.flavour = aFlavour || null;
+
+ this._XferID = "FlavourData";
+}
+
+FlavourData.prototype = {
+ get data ()
+ {
+ if (this.flavour &&
+ this.flavour.dataIIDKey != "nsISupportsString")
+ return this.supports.QueryInterface(Ci[this.flavour.dataIIDKey]);
+
+ var supports = this.supports;
+ if (supports instanceof Ci.nsISupportsString)
+ return supports.data.substring(0, this.contentLength/2);
+
+ return supports;
+ }
+}
+
+/**
+ * Create a TransferData object with a single FlavourData entry. Used when
+ * unwrapping data of a specific flavour from the drag service.
+ */
+function FlavourToXfer(aData, aLength, aFlavour)
+{
+ return new TransferData([new FlavourData(aData, aLength, aFlavour)]);
+}
+
+var transferUtils = {
+
+ retrieveURLFromData: function (aData, flavour)
+ {
+ switch (flavour) {
+ case "text/unicode":
+ case "text/plain":
+ case "text/x-moz-text-internal":
+ return aData.replace(/^\s+|\s+$/g, "");
+ case "text/x-moz-url":
+ return ((aData instanceof Ci.nsISupportsString) ? aData.toString() : aData).split("\n")[0];
+ case "application/x-moz-file":
+ var fileHandler = Services.io.getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ return fileHandler.getURLSpecFromFile(aData);
+ }
+ return null;
+ }
+
+}
+
+/**
+ * nsDragAndDrop - a convenience wrapper for nsTransferable, nsITransferable
+ * and nsIDragService/nsIDragSession.
+ *
+ * Use: map the handler functions to the 'ondraggesture', 'ondragover' and
+ * 'ondragdrop' event handlers on your XML element, e.g.
+ * <xmlelement ondraggesture="nsDragAndDrop.startDrag(event, observer);"
+ * ondragover="nsDragAndDrop.dragOver(event, observer);"
+ * ondragdrop="nsDragAndDrop.drop(event, observer);"/>
+ *
+ * You need to create an observer js object with the following member
+ * functions:
+ * Object onDragStart (event) // called when drag initiated,
+ * // returns flavour list with data
+ * // to stuff into transferable
+ * void onDragOver (Object flavour) // called when element is dragged
+ * // over, so that it can perform
+ * // any drag-over feedback for provided
+ * // flavour
+ * void onDrop (Object data) // formatted data object dropped.
+ * Object getSupportedFlavours () // returns a flavour list so that
+ * // nsTransferable can determine
+ * // whether or not to accept drop.
+ **/
+
+var nsDragAndDrop = {
+
+ _mDS: null,
+ get mDragService()
+ {
+ if (!this._mDS)
+ {
+ const kDSContractID = "@mozilla.org/widget/dragservice;1";
+ const kDSIID = Ci.nsIDragService;
+ this._mDS = Cc[kDSContractID].getService(kDSIID);
+ }
+ return this._mDS;
+ },
+
+ /**
+ * void startDrag (DOMEvent aEvent, Object aDragDropObserver) ;
+ *
+ * called when a drag on an element is started.
+ *
+ * @param DOMEvent aEvent
+ * the DOM event fired by the drag init
+ * @param Object aDragDropObserver
+ * javascript object of format described above that specifies
+ * the way in which the element responds to drag events.
+ **/
+ startDrag: function (aEvent, aDragDropObserver)
+ {
+ if (!("onDragStart" in aDragDropObserver))
+ return;
+
+ const kDSIID = Ci.nsIDragService;
+ var dragAction = { action: kDSIID.DRAGDROP_ACTION_COPY + kDSIID.DRAGDROP_ACTION_MOVE + kDSIID.DRAGDROP_ACTION_LINK };
+
+ var transferData = { data: null };
+ try
+ {
+ aDragDropObserver.onDragStart(aEvent, transferData, dragAction);
+ }
+ catch (e)
+ {
+ return; // not a draggable item, bail!
+ }
+
+ if (!transferData.data) return;
+ transferData = transferData.data;
+
+ var dt = aEvent.dataTransfer;
+ var count = 0;
+ do {
+ var tds = transferData._XferID == "TransferData"
+ ? transferData
+ : transferData.dataList[count]
+ for (var i = 0; i < tds.dataList.length; ++i)
+ {
+ var currData = tds.dataList[i];
+ var currFlavour = currData.flavour.contentType;
+ var value = currData.supports;
+ if (value instanceof Ci.nsISupportsString)
+ value = value.toString();
+ dt.mozSetDataAt(currFlavour, value, count);
+ }
+
+ count++;
+ }
+ while (transferData._XferID == "TransferDataSet" &&
+ count < transferData.dataList.length);
+
+ dt.effectAllowed = "all";
+ // a drag targeted at a tree should instead use the treechildren so that
+ // the current selection is used as the drag feedback
+ dt.addElement(aEvent.originalTarget.localName == "treechildren" ?
+ aEvent.originalTarget : aEvent.target);
+ aEvent.stopPropagation();
+ },
+
+ /**
+ * void dragOver (DOMEvent aEvent, Object aDragDropObserver) ;
+ *
+ * called when a drag passes over this element
+ *
+ * @param DOMEvent aEvent
+ * the DOM event fired by passing over the element
+ * @param Object aDragDropObserver
+ * javascript object of format described above that specifies
+ * the way in which the element responds to drag events.
+ **/
+ dragOver: function (aEvent, aDragDropObserver)
+ {
+ if (!("onDragOver" in aDragDropObserver))
+ return;
+ if (!this.checkCanDrop(aEvent, aDragDropObserver))
+ return;
+ var flavourSet = aDragDropObserver.getSupportedFlavours();
+ for (var flavour in flavourSet.flavourTable)
+ {
+ if (this.mDragSession.isDataFlavorSupported(flavour))
+ {
+ aDragDropObserver.onDragOver(aEvent,
+ flavourSet.flavourTable[flavour],
+ this.mDragSession);
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ }
+ }
+ },
+
+ mDragSession: null,
+
+ /**
+ * void drop (DOMEvent aEvent, Object aDragDropObserver) ;
+ *
+ * called when the user drops on the element
+ *
+ * @param DOMEvent aEvent
+ * the DOM event fired by the drop
+ * @param Object aDragDropObserver
+ * javascript object of format described above that specifies
+ * the way in which the element responds to drag events.
+ **/
+ drop: function (aEvent, aDragDropObserver)
+ {
+ if (!("onDrop" in aDragDropObserver))
+ return;
+ if (!this.checkCanDrop(aEvent, aDragDropObserver))
+ return;
+
+ var flavourSet = aDragDropObserver.getSupportedFlavours();
+
+ var dt = aEvent.dataTransfer;
+ var dataArray = [];
+ var count = dt.mozItemCount;
+ for (var i = 0; i < count; ++i) {
+ var types = dt.mozTypesAt(i);
+ for (var j = 0; j < flavourSet.flavours.length; j++) {
+ var type = flavourSet.flavours[j].contentType;
+ // dataTransfer uses text/plain but older code used text/unicode, so
+ // switch this for compatibility
+ var modtype = (type == "text/unicode") ? "text/plain" : type;
+ if (Array.from(types).includes(modtype)) {
+ var data = dt.mozGetDataAt(modtype, i);
+ if (data) {
+ // Non-strings need some non-zero value used for their data length.
+ const kNonStringDataLength = 4;
+
+ var length = (typeof data == "string") ? data.length : kNonStringDataLength;
+ dataArray[i] = FlavourToXfer(data, length, flavourSet.flavourTable[type]);
+ break;
+ }
+ }
+ }
+ }
+
+ var transferData = new TransferDataSet(dataArray)
+
+ // hand over to the client to respond to dropped data
+ var multiple = "canHandleMultipleItems" in aDragDropObserver && aDragDropObserver.canHandleMultipleItems;
+ var dropData = multiple ? transferData : transferData.first.first;
+ aDragDropObserver.onDrop(aEvent, dropData, this.mDragSession);
+ aEvent.stopPropagation();
+ },
+
+ /**
+ * void dragExit (DOMEvent aEvent, Object aDragDropObserver) ;
+ *
+ * called when a drag leaves this element
+ *
+ * @param DOMEvent aEvent
+ * the DOM event fired by leaving the element
+ * @param Object aDragDropObserver
+ * javascript object of format described above that specifies
+ * the way in which the element responds to drag events.
+ **/
+ dragExit: function (aEvent, aDragDropObserver)
+ {
+ if (!this.checkCanDrop(aEvent, aDragDropObserver))
+ return;
+ if ("onDragExit" in aDragDropObserver)
+ aDragDropObserver.onDragExit(aEvent, this.mDragSession);
+ },
+
+ /**
+ * void dragEnter (DOMEvent aEvent, Object aDragDropObserver) ;
+ *
+ * called when a drag enters in this element
+ *
+ * @param DOMEvent aEvent
+ * the DOM event fired by entering in the element
+ * @param Object aDragDropObserver
+ * javascript object of format described above that specifies
+ * the way in which the element responds to drag events.
+ **/
+ dragEnter: function (aEvent, aDragDropObserver)
+ {
+ if (!this.checkCanDrop(aEvent, aDragDropObserver))
+ return;
+ if ("onDragEnter" in aDragDropObserver)
+ aDragDropObserver.onDragEnter(aEvent, this.mDragSession);
+ },
+
+ /**
+ * Boolean checkCanDrop (DOMEvent aEvent, Object aDragDropObserver) ;
+ *
+ * Sets the canDrop attribute for the drag session.
+ * returns false if there is no current drag session.
+ *
+ * @param DOMEvent aEvent
+ * the DOM event fired by the drop
+ * @param Object aDragDropObserver
+ * javascript object of format described above that specifies
+ * the way in which the element responds to drag events.
+ **/
+ checkCanDrop: function (aEvent, aDragDropObserver)
+ {
+ if (!this.mDragSession)
+ this.mDragSession = this.mDragService.getCurrentSession();
+ if (!this.mDragSession)
+ return false;
+ this.mDragSession.canDrop = this.mDragSession.sourceNode != aEvent.target;
+ if ("canDrop" in aDragDropObserver)
+ this.mDragSession.canDrop &= aDragDropObserver.canDrop(aEvent, this.mDragSession);
+ return true;
+ },
+
+ /**
+ * Do a security check for drag n' drop. Make sure the source document
+ * can load the dragged link.
+ *
+ * @param DOMEvent aEvent
+ * the DOM event fired by leaving the element
+ * @param Object aDragDropObserver
+ * javascript object of format described above that specifies
+ * the way in which the element responds to drag events.
+ * @param String aDraggedText
+ * the text being dragged
+ **/
+ dragDropSecurityCheck: function (aEvent, aDragSession, aDraggedText)
+ {
+ // Strip leading and trailing whitespace, then try to create a
+ // URI from the dropped string. If that succeeds, we're
+ // dropping a URI and we need to do a security check to make
+ // sure the source document can load the dropped URI. We don't
+ // so much care about creating the real URI here
+ // (i.e. encoding differences etc don't matter), we just want
+ // to know if aDraggedText really is a URI.
+
+ aDraggedText = aDraggedText.replace(/^\s*|\s*$/g, '');
+
+ var uri;
+ try {
+ uri = Services.io.newURI(aDraggedText);
+ } catch (e) {
+ }
+
+ if (!uri)
+ return;
+
+ // aDraggedText is a URI, do the security check.
+ let secMan = Services.scriptSecurityManager;
+
+ if (!aDragSession)
+ aDragSession = this.mDragService.getCurrentSession();
+
+ var sourceDoc = aDragSession.sourceDocument;
+ // Use "file:///" as the default sourceURI so that drops of file:// URIs
+ // are always allowed.
+ var principal = sourceDoc ? sourceDoc.nodePrincipal
+ : secMan.createCodebasePrincipal(Services.io.newURI("file:///"), {});
+
+ try {
+ secMan.checkLoadURIStrWithPrincipal(principal, aDraggedText,
+ Ci.nsIScriptSecurityManager.STANDARD);
+ } catch (e) {
+ // Stop event propagation right here.
+ aEvent.stopPropagation();
+
+ throw "Drop of " + aDraggedText + " denied.";
+ }
+ }
+};
+
diff --git a/comm/suite/mailnews/content/phishingDetector.js b/comm/suite/mailnews/content/phishingDetector.js
new file mode 100644
index 0000000000..04d2910753
--- /dev/null
+++ b/comm/suite/mailnews/content/phishingDetector.js
@@ -0,0 +1,173 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// Dependencies:
+// gBrandBundle, gMessengerBundle should already be defined
+// gatherTextUnder from utilityOverlay.js
+
+ChromeUtils.import("resource:///modules/hostnameUtils.jsm");
+
+const kPhishingNotSuspicious = 0;
+const kPhishingWithIPAddress = 1;
+const kPhishingWithMismatchedHosts = 2;
+
+//////////////////////////////////////////////////////////////////////////////
+// isEmailScam --> examines the message currently loaded in the message pane
+// and returns true if we think that message is an e-mail scam.
+// Assumes the message has been completely loaded in the message pane (i.e. OnMsgParsed has fired)
+// aUrl: nsIURI object for the msg we want to examine...
+//////////////////////////////////////////////////////////////////////////////
+function isMsgEmailScam(aUrl)
+{
+ var isEmailScam = false;
+ if (!aUrl || !Services.prefs.getBoolPref("mail.phishing.detection.enabled"))
+ return isEmailScam;
+
+ try {
+ // nsIMsgMailNewsUrl.folder can throw an NS_ERROR_FAILURE, especially if
+ // we are opening an .eml file.
+ var folder = aUrl.folder;
+
+ // Ignore NNTP and RSS messages.
+ if (folder.server.type == 'nntp' || folder.server.type == 'rss')
+ return isEmailScam;
+
+ // Also ignore messages in Sent/Drafts/Templates/Outbox.
+ let outgoingFlags = Ci.nsMsgFolderFlags.SentMail |
+ Ci.nsMsgFolderFlags.Drafts |
+ Ci.nsMsgFolderFlags.Templates |
+ Ci.nsMsgFolderFlags.Queue;
+ if (folder.isSpecialFolder(outgoingFlags, true))
+ return isEmailScam;
+
+ } catch (ex) {
+ if (ex.result != Cr.NS_ERROR_FAILURE)
+ throw ex;
+ }
+
+ // loop through all of the link nodes in the message's DOM, looking for phishing URLs...
+ var msgDocument = document.getElementById('messagepane').contentDocument;
+ var index;
+
+ // examine all links...
+ var linkNodes = msgDocument.links;
+ for (index = 0; index < linkNodes.length && !isEmailScam; index++)
+ isEmailScam = isPhishingURL(linkNodes[index], true);
+
+ // if an e-mail contains a non-addressbook form element, then assume the message is
+ // a phishing attack. Legitimate sites should not be using forms inside of e-mail
+ if (!isEmailScam)
+ {
+ var forms = msgDocument.getElementsByTagName("form");
+ for (index = 0; index < forms.length && !isEmailScam; index++)
+ isEmailScam = forms[index].action != "" && !/^addbook:/.test(forms[index].action);
+ }
+
+ // we'll add more checks here as our detector matures....
+ return isEmailScam;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// isPhishingURL --> examines the passed in linkNode and returns true if we think
+// the URL is an email scam.
+// aLinkNode: the link node to examine
+// aSilentMode: don't prompt the user to confirm
+// aHref: optional href for XLinks
+//////////////////////////////////////////////////////////////////////////////
+
+function isPhishingURL(aLinkNode, aSilentMode, aHref)
+{
+ if (!Services.prefs.getBoolPref("mail.phishing.detection.enabled"))
+ return false;
+
+ var phishingType = kPhishingNotSuspicious;
+ var aLinkText = gatherTextUnder(aLinkNode);
+ var href = aHref || aLinkNode.href;
+ if (!href)
+ return false;
+
+ var linkTextURL = {};
+ var isPhishingURL = false;
+
+ var hrefURL;
+ // Make sure relative link urls don't make us bail out.
+ try {
+ hrefURL = Services.io.newURI(href);
+ } catch(ex) { return false; }
+
+ // only check for phishing urls if the url is an http or https link.
+ // this prevents us from flagging imap and other internally handled urls
+ if (hrefURL.schemeIs('http') || hrefURL.schemeIs('https'))
+ {
+
+ if (aLinkText)
+ aLinkText = aLinkText.replace(/^<(.+)>$|^"(.+)"$/, "$1$2");
+ if (aLinkText != aLinkNode.href &&
+ aLinkText.replace(/\/+$/, "") != aLinkNode.href.replace(/\/+$/, ""))
+ {
+ let ipAddress = isLegalIPAddress(hrefURL.host, true);
+ if (ipAddress && !isLegalLocalIPAddress(ipAddress))
+ phishingType = kPhishingWithIPAddress;
+ else if (misMatchedHostWithLinkText(aLinkNode, hrefURL))
+ phishingType = kPhishingWithMismatchedHosts;
+
+ isPhishingURL = phishingType != kPhishingNotSuspicious;
+
+ if (!aSilentMode && isPhishingURL) // allow the user to override the decision
+ isPhishingURL = confirmSuspiciousURL(phishingType, hrefURL.host);
+ }
+ }
+
+ return isPhishingURL;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// helper methods in support of isPhishingURL
+//////////////////////////////////////////////////////////////////////////////
+
+function misMatchedHostWithLinkText(aLinkNode, aHrefURL)
+{
+ var linkNodeText = gatherTextUnder(aLinkNode);
+
+ // gatherTextUnder puts a space between each piece of text it gathers,
+ // so strip the spaces out (see bug 326082 for details).
+ linkNodeText = linkNodeText.replace(/ /g, "");
+
+ // only worry about http and https urls
+ if (linkNodeText)
+ {
+ // does the link text look like a http url?
+ if (linkNodeText.search(/(^http:|^https:)/) != -1)
+ {
+ var linkURI = Services.io.newURI(linkNodeText);
+ // compare hosts, but ignore possible www. prefix
+ return !(aHrefURL.host.replace(/^www\./, "") == linkURI.host.replace(/^www\./, ""));
+ }
+ }
+
+ return false;
+}
+
+// returns true if the user confirms the URL is a scam
+function confirmSuspiciousURL(aPhishingType, aSuspiciousHostName)
+{
+ var brandShortName = gBrandBundle.getString("brandShortName");
+ var titleMsg = gMessengerBundle.getString("confirmPhishingTitle");
+ var dialogMsg;
+
+ switch (aPhishingType)
+ {
+ case kPhishingWithIPAddress:
+ case kPhishingWithMismatchedHosts:
+ dialogMsg = gMessengerBundle.getFormattedString("confirmPhishingUrl" + aPhishingType, [brandShortName, aSuspiciousHostName], 2);
+ break;
+ default:
+ return false;
+ }
+
+ var buttons = Services.prompt.STD_YES_NO_BUTTONS +
+ Services.prompt.BUTTON_POS_1_DEFAULT;
+ return Services.prompt.confirmEx(window, titleMsg, dialogMsg, buttons, "", "", "", "", {}); /* the yes button is in position 0 */
+} \ No newline at end of file
diff --git a/comm/suite/mailnews/content/searchBar.js b/comm/suite/mailnews/content/searchBar.js
new file mode 100644
index 0000000000..2c23b395a3
--- /dev/null
+++ b/comm/suite/mailnews/content/searchBar.js
@@ -0,0 +1,432 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm");
+
+var gSearchSession = null;
+var gPreQuickSearchView = null;
+var gSearchTimer = null;
+var gViewSearchListener;
+var gSearchBundle;
+var gProgressMeter = null;
+var gSearchInProgress = false;
+var gClearButton = null;
+var gDefaultSearchViewTerms = null;
+var gQSViewIsDirty = false;
+var gNumTotalMessages;
+var gNumUnreadMessages;
+
+function SetQSStatusText(aNumHits)
+{
+ var statusMsg;
+ // if there are no hits, it means no matches were found in the search.
+ if (aNumHits == 0)
+ {
+ statusMsg = gSearchBundle.getString("noMatchesFound");
+ }
+ else
+ {
+ statusMsg = PluralForm.get(aNumHits,
+ gSearchBundle.getString("matchesFound"));
+ statusMsg = statusMsg.replace("#1", aNumHits);
+ }
+ statusFeedback.showStatusString(statusMsg);
+}
+
+// nsIMsgSearchNotify object
+var gSearchNotificationListener =
+{
+ onSearchHit: function(header, folder)
+ {
+ gNumTotalMessages++;
+ if (!header.isRead)
+ gNumUnreadMessages++;
+ // XXX todo
+ // update status text?
+ },
+
+ onSearchDone: function(status)
+ {
+ SetQSStatusText(gDBView.QueryInterface(Ci.nsITreeView).rowCount)
+ statusFeedback.showProgress(0);
+ gProgressMeter.setAttribute("mode", "normal");
+ gSearchInProgress = false;
+
+ // ### TODO need to find out if there's quick search within a virtual folder.
+ if (gCurrentVirtualFolderUri &&
+ (!gSearchInput || gSearchInput.value == ""))
+ {
+ var vFolder = MailUtils.getFolderForURI(gCurrentVirtualFolderUri, false);
+ var dbFolderInfo = vFolder.msgDatabase.dBFolderInfo;
+ dbFolderInfo.numUnreadMessages = gNumUnreadMessages;
+ dbFolderInfo.numMessages = gNumTotalMessages;
+ vFolder.updateSummaryTotals(true); // force update from db.
+ var msgdb = vFolder.msgDatabase;
+ msgdb.Commit(Ci.nsMsgDBCommitType.kLargeCommit);
+ // now that we have finished loading a virtual folder,
+ // scroll to the correct message if there is at least one.
+ if (vFolder.getTotalMessages(false) > 0)
+ ScrollToMessageAfterFolderLoad(vFolder);
+ }
+ },
+
+ onNewSearch: function()
+ {
+ statusFeedback.showProgress(0);
+ statusFeedback.showStatusString(gSearchBundle.getString("searchingMessage"));
+ gProgressMeter.setAttribute("mode", "undetermined");
+ gSearchInProgress = true;
+ gNumTotalMessages = 0;
+ gNumUnreadMessages = 0;
+ }
+}
+
+function getDocumentElements()
+{
+ gSearchBundle = document.getElementById("bundle_search");
+ gProgressMeter = document.getElementById('statusbar-icon');
+ gClearButton = document.getElementById('clearButton');
+ GetSearchInput();
+}
+
+function addListeners()
+{
+ gViewSearchListener = gDBView.QueryInterface(Ci.nsIMsgSearchNotify);
+ gSearchSession.registerListener(gViewSearchListener);
+}
+
+function removeListeners()
+{
+ gSearchSession.unregisterListener(gViewSearchListener);
+}
+
+function removeGlobalListeners()
+{
+ removeListeners();
+ gSearchSession.unregisterListener(gSearchNotificationListener);
+}
+
+function initializeGlobalListeners()
+{
+ // Setup the javascript object as a listener on the search results
+ gSearchSession.registerListener(gSearchNotificationListener);
+}
+
+function createQuickSearchView()
+{
+ //if not already in quick search view
+ if (gDBView.viewType != nsMsgViewType.eShowQuickSearchResults)
+ {
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView); //clear selection
+ if (treeView && treeView.selection)
+ treeView.selection.clearSelection();
+ gPreQuickSearchView = gDBView;
+ if (gDBView.viewType == nsMsgViewType.eShowVirtualFolderResults)
+ {
+ // remove the view as a listener on the search results
+ var saveViewSearchListener = gDBView.QueryInterface(Ci.nsIMsgSearchNotify);
+ gSearchSession.unregisterListener(saveViewSearchListener);
+ }
+ CreateDBView(gDBView.msgFolder, (gXFVirtualFolderTerms) ? nsMsgViewType.eShowVirtualFolderResults : nsMsgViewType.eShowQuickSearchResults, gDBView.viewFlags, gDBView.sortType, gDBView.sortOrder);
+ }
+}
+
+function initializeSearchBar()
+{
+ createQuickSearchView();
+ if (!gSearchSession)
+ {
+ var searchSessionContractID = "@mozilla.org/messenger/searchSession;1";
+ gSearchSession = Cc[searchSessionContractID].createInstance(Ci.nsIMsgSearchSession);
+ initializeGlobalListeners();
+ }
+ else
+ {
+ if (gSearchInProgress)
+ {
+ onSearchStop();
+ gSearchInProgress = false;
+ }
+ removeListeners();
+ }
+ addListeners();
+}
+
+function onEnterInSearchBar()
+{
+ if (!gSearchBundle)
+ getDocumentElements();
+ if (gSearchInput.value == "")
+ {
+ let viewType = gDBView && gDBView.viewType;
+ if (viewType == nsMsgViewType.eShowQuickSearchResults ||
+ viewType == nsMsgViewType.eShowVirtualFolderResults)
+ {
+ statusFeedback.showStatusString("");
+ disableQuickSearchClearButton();
+
+ viewDebug ("onEnterInSearchBar gDefaultSearchViewTerms = " + gDefaultSearchViewTerms + "gVirtualFolderTerms = "
+ + gVirtualFolderTerms + "gXFVirtualFolderTerms = " + gXFVirtualFolderTerms + "\n");
+ var addTerms = gDefaultSearchViewTerms || gVirtualFolderTerms || gXFVirtualFolderTerms;
+ if (addTerms)
+ {
+ viewDebug ("addTerms = " + addTerms + " count = " + addTerms.length + "\n");
+ initializeSearchBar();
+ onSearch(addTerms);
+ }
+ else
+ restorePreSearchView();
+ }
+ else if (gPreQuickSearchView && !gDefaultSearchViewTerms)// may be a quick search from a cross-folder virtual folder
+ restorePreSearchView();
+
+ gQSViewIsDirty = false;
+ return;
+ }
+
+ initializeSearchBar();
+
+ if (gClearButton)
+ gClearButton.setAttribute("disabled", false); //coming into search enable clear button
+
+ ClearThreadPaneSelection();
+ ClearMessagePane();
+
+ onSearch(null);
+ gQSViewIsDirty = false;
+}
+
+function restorePreSearchView()
+{
+ var selectedHdr = null;
+ //save selection
+ try
+ {
+ selectedHdr = gDBView.hdrForFirstSelectedMessage;
+ }
+ catch (ex)
+ {}
+
+ //we might have to sort the view coming out of quick search
+ var sortType = gDBView.sortType;
+ var sortOrder = gDBView.sortOrder;
+ var viewFlags = gDBView.viewFlags;
+ var folder = gDBView.msgFolder;
+
+ gDBView.close();
+ gDBView = null;
+
+ if (gPreQuickSearchView)
+ {
+ gDBView = gPreQuickSearchView;
+ if (gDBView.viewType == nsMsgViewType.eShowVirtualFolderResults)
+ {
+ // readd the view as a listener on the search results
+ var saveViewSearchListener = gDBView.QueryInterface(Ci.nsIMsgSearchNotify);
+ if (gSearchSession)
+ gSearchSession.registerListener(saveViewSearchListener);
+ }
+// dump ("view type = " + gDBView.viewType + "\n");
+
+ if (sortType != gDBView.sortType || sortOrder != gDBView.sortOrder)
+ {
+ gDBView.sort(sortType, sortOrder);
+ }
+ UpdateSortIndicators(sortType, sortOrder);
+
+ gPreQuickSearchView = null;
+ }
+ else //create default view type
+ CreateDBView(folder, nsMsgViewType.eShowAllThreads, viewFlags, sortType, sortOrder);
+
+ RerootThreadPane();
+
+ var scrolled = false;
+
+ // now restore selection
+ if (selectedHdr)
+ {
+ gDBView.selectMsgByKey(selectedHdr.messageKey);
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ var selectedIndex = treeView.selection.currentIndex;
+ if (selectedIndex >= 0)
+ {
+ // scroll
+ EnsureRowInThreadTreeIsVisible(selectedIndex);
+ scrolled = true;
+ }
+ else
+ ClearMessagePane();
+ }
+ if (!scrolled)
+ ScrollToMessageAfterFolderLoad(null);
+}
+
+function onSearch(aSearchTerms)
+{
+ viewDebug("in OnSearch, searchTerms = " + aSearchTerms + "\n");
+ RerootThreadPane();
+
+ if (aSearchTerms)
+ createSearchTermsWithList(aSearchTerms);
+ else
+ createSearchTerms();
+
+ gDBView.searchSession = gSearchSession;
+ try
+ {
+ gSearchSession.search(msgWindow);
+ }
+ catch(ex)
+ {
+ dump("Search Exception\n");
+ }
+}
+
+function createSearchTermsWithList(aTermsArray)
+{
+ var nsMsgSearchScope = Ci.nsMsgSearchScope;
+ var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ var nsMsgSearchOp = Ci.nsMsgSearchOp;
+
+ gSearchSession.searchTerms.clear();
+ gSearchSession.clearScopes();
+
+ var i;
+ var selectedFolder = GetThreadPaneFolder();
+ if (gXFVirtualFolderTerms)
+ {
+ var msgDatabase = selectedFolder.msgDatabase;
+ if (msgDatabase)
+ {
+ var dbFolderInfo = msgDatabase.dBFolderInfo;
+ var srchFolderUri = dbFolderInfo.getCharProperty("searchFolderUri");
+ viewDebug("createSearchTermsWithList xf vf scope = " + srchFolderUri + "\n");
+ var srchFolderUriArray = srchFolderUri.split('|');
+ for (i in srchFolderUriArray)
+ {
+ let realFolder = MailUtils.getFolderForURI(srchFolderUriArray[i]);
+ if (!realFolder.isServer)
+ gSearchSession.addScopeTerm(nsMsgSearchScope.offlineMail, realFolder);
+ }
+ }
+ }
+ else
+ {
+ viewDebug ("in createSearchTermsWithList, adding scope term for selected folder\n");
+ gSearchSession.addScopeTerm(nsMsgSearchScope.offlineMail, selectedFolder);
+ }
+
+ // Add each item in aTermsArray to the search session.
+ for (let term of aTermsArray) {
+ gSearchSession.appendTerm(term);
+ }
+}
+
+function createSearchTerms()
+{
+ var nsMsgSearchScope = Ci.nsMsgSearchScope;
+ var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
+ var nsMsgSearchOp = Ci.nsMsgSearchOp;
+
+ // create an nsIMutableArray to store our search terms
+ var searchTermsArray = Cc["@mozilla.org/array;1"]
+ .createInstance(Ci.nsIMutableArray);
+ var selectedFolder = GetThreadPaneFolder();
+
+ // implement | for QS
+ // does this break if the user types "foo|bar" expecting to see subjects with that string?
+ // I claim no, since "foo|bar" will be a hit for "foo" || "bar"
+ // they just might get more false positives
+ var termList = gSearchInput.value.split("|");
+ for (var i = 0; i < termList.length; i ++)
+ {
+ // if the term is empty, skip it
+ if (termList[i] == "")
+ continue;
+
+ // create, fill, and append the subject term
+ var term = gSearchSession.createTerm();
+ var value = term.value;
+ value.str = termList[i];
+ term.value = value;
+ term.attrib = nsMsgSearchAttrib.Subject;
+ term.op = nsMsgSearchOp.Contains;
+ term.booleanAnd = false;
+ searchTermsArray.appendElement(term);
+
+ // create, fill, and append the AllAddresses term
+ term = gSearchSession.createTerm();
+ value = term.value;
+ value.str = termList[i];
+ term.value = value;
+ term.attrib = nsMsgSearchAttrib.AllAddresses;
+ term.op = nsMsgSearchOp.Contains;
+ term.booleanAnd = false;
+ searchTermsArray.appendElement(term);
+ }
+
+ // now append the default view or virtual folder criteria to the quick search
+ // so we don't lose any default view information
+ viewDebug("gDefaultSearchViewTerms = " + gDefaultSearchViewTerms + "gVirtualFolderTerms = " + gVirtualFolderTerms +
+ "gXFVirtualFolderTerms = " + gXFVirtualFolderTerms + "\n");
+ var defaultSearchTerms = (gDefaultSearchViewTerms || gVirtualFolderTerms || gXFVirtualFolderTerms);
+ if (defaultSearchTerms)
+ {
+ for (let searchTerm of defaultSearchTerms)
+ {
+ searchTermsArray.appendElement(searchTerm);
+ }
+ }
+
+ createSearchTermsWithList(searchTermsArray);
+
+ // now that we've added the terms, clear out our input array
+ searchTermsArray.clear();
+}
+
+function onSearchStop()
+{
+ gSearchSession.interruptSearch();
+}
+
+function onClearSearch()
+{
+ // Use the last focused element so that focus can be restored
+ // if it does not exist, try and get the thread tree instead
+ var focusedElement = gLastFocusedElement || GetThreadTree();
+ Search("");
+ focusedElement.focus();
+}
+
+function disableQuickSearchClearButton()
+{
+ if (gClearButton)
+ gClearButton.setAttribute("disabled", true); //going out of search disable clear button
+}
+
+function ClearQSIfNecessary()
+{
+ GetSearchInput();
+
+ if (gSearchInput.value == "")
+ return;
+
+ Search("");
+}
+
+function Search(str)
+{
+ GetSearchInput();
+
+ if (str != gSearchInput.value)
+ {
+ gQSViewIsDirty = true;
+ viewDebug("in Search(), setting gQSViewIsDirty true\n");
+ }
+
+ gSearchInput.value = str; //on input does not get fired for some reason
+ onEnterInSearchBar();
+}
diff --git a/comm/suite/mailnews/content/searchTermOverlay.xul b/comm/suite/mailnews/content/searchTermOverlay.xul
new file mode 100644
index 0000000000..cd3b1df635
--- /dev/null
+++ b/comm/suite/mailnews/content/searchTermOverlay.xul
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://messenger/content/searchTerm.js"/>
+ <script src="chrome://messenger/content/dateFormat.js"/>
+
+ <vbox id="searchTermListBox">
+
+ <radiogroup id="booleanAndGroup" orient="horizontal" value="and"
+ oncommand="booleanChanged(event);">
+ <radio value="and" label="&matchAll.label;"
+ accesskey="&matchAll.accesskey;"/>
+ <radio value="or" label="&matchAny.label;"
+ accesskey="&matchAny.accesskey;"/>
+ <radio value="matchAll" id="matchAllItem" label="&matchAllMsgs.label;"
+ accesskey="&matchAllMsgs.accesskey;"/>
+ </radiogroup>
+
+ <hbox flex="1">
+ <hbox id="searchterms"/>
+ <listbox flex="1" id="searchTermList" rows="4" minheight="35%">
+ <listcols>
+ <listcol flex="&searchTermListAttributesFlexValue;"/>
+ <listcol flex="&searchTermListOperatorsFlexValue;"/>
+ <listcol flex="&searchTermListValueFlexValue;"/>
+ <listcol class="filler"/>
+ </listcols>
+
+ <!-- this is what the listitems will look like:
+ <listitem id="searchListItem">
+ <listcell allowevents="true">
+ <searchattribute id="searchAttr1" for="searchOp1,searchValue1" flex="1"/>
+ </listcell>
+ <listcell allowevents="true">
+ <searchoperator id="searchOp1" opfor="searchValue1" flex="1"/>
+ </listcell>
+ <listcell allowevents="true" >
+ <searchvalue id="searchValue1" flex="1"/>
+ </listcell>
+ <listcell>
+ <button label="add"/>
+ <button label="remove"/>
+ </listcell>
+ </listitem>
+ <listitem>
+ <listcell label="the.."/>
+ <listcell label="contains.."/>
+ <listcell label="text here"/>
+ <listcell label="+/-"/>
+ </listitem>
+ -->
+ </listbox>
+
+ </hbox>
+ </vbox>
+
+</overlay>
diff --git a/comm/suite/mailnews/content/start.xhtml b/comm/suite/mailnews/content/start.xhtml
new file mode 100644
index 0000000000..d9aaf6b790
--- /dev/null
+++ b/comm/suite/mailnews/content/start.xhtml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/start.css" type="text/css"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+ "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % startDTD SYSTEM "chrome://messenger/locale/start.dtd" >
+%startDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&startpage.title;</title>
+</head>
+
+<body>
+<h1>&headline.label;</h1>
+
+<div id="main">
+<p>&description.label;</p>
+<h2>&features.title;</h2>
+<ul>
+ <li>&feat_multiacc.label;</li>
+ <li>&feat_junk.label;</li>
+ <li>&feat_feeds.label;</li>
+ <li>&feat_filters.label;</li>
+ <li>&feat_htmlmsg.label;</li>
+ <li>&feat_abook.label;</li>
+ <li>&feat_tags.label;</li>
+ <li>&feat_integration.label;</li>
+</ul>
+<h2>&dict.title;</h2>
+<p>&dict_intro.label;</p>
+<p>&dict_info.label2;</p>
+<h2>&info.title;</h2>
+<p>&info_bugs.label2;</p>
+</div>
+
+<script>
+ const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+ // get vendor, dictionaries and release notes URLs from prefs
+ let formatter = Services.urlFormatter;
+ var vendorURL = formatter.formatURLPref("app.vendorURL");
+
+ if (vendorURL != "about:blank") {
+ var vendor = document.getElementById("vendorURL");
+ if (vendor)
+ vendor.setAttribute("href", vendorURL);
+ }
+
+ var dictURL = formatter.formatURLPref("spellchecker.dictionaries.download.url");
+ var dictionaries = document.getElementById("dictURL");
+ if (dictionaries)
+ dictionaries.setAttribute("href", dictURL);
+
+ var releaseNotesURL = formatter.formatURLPref("app.releaseNotesURL");
+ var relnotes = document.getElementById("releaseNotesURL");
+ if (relnotes)
+ relnotes.setAttribute("href", releaseNotesURL);
+</script>
+
+</body>
+</html>
diff --git a/comm/suite/mailnews/content/tabmail.js b/comm/suite/mailnews/content/tabmail.js
new file mode 100644
index 0000000000..694941ce92
--- /dev/null
+++ b/comm/suite/mailnews/content/tabmail.js
@@ -0,0 +1,969 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+// Traditionally, mailnews tabs come in two flavours: "folder" and
+// "message" tabs. But these modes are just mere default settings on tab
+// creation, defining layout, URI to load, etc.
+// The user can turn a "message" tab into a "folder" tab just by unhiding
+// the folder pane (F9) and hiding the message pane (F8), and vice versa.
+// Tab title and icon will change accordingly.
+// Both flavours are just instances of the basic "3pane" mode, triggered by
+// a bitwise or combination of these possible pane values:
+const kTabShowNoPane = 0;
+const kTabShowFolderPane = 1 << 0;
+const kTabShowMessagePane = 1 << 1;
+const kTabShowThreadPane = 1 << 2;
+const kTabShowAcctCentral = 1 << 3;
+// predefined mode masks
+const kTabMaskDisplayDeck = kTabShowThreadPane | kTabShowAcctCentral;
+// predefined traditional flavours
+const kTabModeFolder = kTabShowFolderPane | kTabShowThreadPane | kTabShowMessagePane;
+const kTabModeMessage = kTabShowMessagePane; // message tab
+
+
+// global mailnews tab definition object
+var gMailNewsTabsType =
+{
+ name: "mailnews",
+ panelId: "mailContent",
+
+ modes:
+ {
+ "3pane":
+ {
+ isDefault: true,
+ type: "3pane",
+
+ // aTabInfo belongs to the newly created tab,
+ // aArgs can contain:
+ // * modeBits is a combination of kTabShow* layout bits (or null),
+ // * folderURI designates the folder to select (or null)
+ // * msgHdr designates the message to select (or null)
+ openTab: function(aTabInfo, {modeBits: aModeBits, folderURI: aFolderURI,
+ msgHdr: aMsgHdr}) {
+ // clone the current 3pane state before overriding parts of it
+ this.saveTabState(aTabInfo);
+
+ // aModeBits must have at least one bit set
+ // if not, we just copy the current state
+ let cloneMode = !aModeBits;
+ if (cloneMode)
+ aModeBits = this.getCurrentModeBits() || kTabModeFolder;
+ aTabInfo.modeBits = aModeBits;
+ // Currently, we only check for kTabModeMessage vs. kTabModeFolder,
+ // but in theory we could distinguish in much more detail!
+ let messageId = null;
+ if (aModeBits == kTabModeMessage || cloneMode)
+ {
+ if (!aMsgHdr && gDBView)
+ {
+ try
+ {
+ // duplicate current message tab if nothing else is specified
+ aMsgHdr = gDBView.hdrForFirstSelectedMessage;
+ // Use the header's folder - this will open a msg in a virtual folder view
+ // in its real folder, which is needed if the msg wouldn't be in a new
+ // view with the same terms - e.g., it's read and the view is unread only.
+ // If we cloned the view, we wouldn't have to do this.
+ if (aTabInfo.switchToNewTab)
+ {
+ // Fix it so we won't try to load the previously loaded message.
+ aMsgHdr.folder.lastMessageLoaded = nsMsgKey_None;
+ }
+ aFolderURI = aMsgHdr.folder.URI;
+ }
+ catch (ex) {}
+ }
+ if (aMsgHdr)
+ messageId = aMsgHdr.messageId;
+ aTabInfo.clearSplitter = true;
+ }
+
+ if (!messageId)
+ {
+ // only sanitize the URL, if possible
+ let clearSplitter = aModeBits == kTabModeFolder;
+ if (!aFolderURI)
+ {
+ // Use GetSelectedMsgFolders() to find out which folder to open
+ // instead of GetLoadedMsgFolder().URI. This is required because on a
+ // right-click, the currentIndex value will be different from the
+ // actual row that is highlighted. GetSelectedMsgFolders() will
+ // return the message that is highlighted.
+ let msgFolder = GetSelectedMsgFolders()[0];
+ aFolderURI = msgFolder.URI;
+ // don't kill the splitter settings for account central
+ clearSplitter &= !msgFolder.isServer;
+ }
+ aMsgHdr = null;
+ aTabInfo.clearSplitter = clearSplitter;
+ }
+ aTabInfo.uriToOpen = aFolderURI;
+ aTabInfo.hdr = aMsgHdr;
+ aTabInfo.selectedMsgId = messageId;
+
+ // call superclass logic
+ this.openTab(aTabInfo);
+ },
+
+ // We can close all mailnews tabs - but one.
+ // Closing the last mailnews tab would destroy our mailnews functionality.
+ canCloseTab: function(aTabInfo)
+ {
+ return aTabInfo.mode.tabs.length > 1;
+ }
+ }
+ },
+
+ // combines the current pane visibility states into a mode bit mask
+ getCurrentModeBits: function()
+ {
+ let modeBits = kTabShowNoPane;
+ if (!IsFolderPaneCollapsed())
+ modeBits |= kTabShowFolderPane;
+ if (!IsDisplayDeckCollapsed())
+ {
+ // currently, the display deck has only two panes
+ if (gAccountCentralLoaded)
+ modeBits |= kTabShowAcctCentral;
+ else
+ modeBits |= kTabShowThreadPane;
+ }
+ if (!IsMessagePaneCollapsed())
+ modeBits |= kTabShowMessagePane;
+ return modeBits;
+ },
+
+ _updatePaneLayout: function(aTabInfo)
+ {
+ // first show all needed panes, then hide all unwanted ones
+ // (we have to keep this order to avoid hiding all panes!)
+ let showFolderPane = aTabInfo.modeBits & kTabShowFolderPane;
+ let showMessagePane = aTabInfo.modeBits & kTabShowMessagePane;
+ let showDisplayDeck = aTabInfo.modeBits & (kTabShowThreadPane | kTabShowAcctCentral);
+ if (showMessagePane && IsMessagePaneCollapsed())
+ MsgToggleMessagePane(true); // show message pane
+ if (showDisplayDeck && IsDisplayDeckCollapsed())
+ MsgToggleThreadPane(); // show thread pane
+ if (showFolderPane && IsFolderPaneCollapsed())
+ MsgToggleFolderPane(true); // show folder pane
+ if (!showMessagePane && !IsMessagePaneCollapsed())
+ MsgToggleMessagePane(true); // hide message pane
+ if (!showDisplayDeck && !IsDisplayDeckCollapsed())
+ MsgToggleThreadPane(); // hide thread pane
+ if (!showFolderPane && !IsFolderPaneCollapsed())
+ MsgToggleFolderPane(true); // hide folder pane
+ UpdateLayoutVisibility();
+ },
+
+ /**
+ * Create the new tab's state, which engenders some side effects.
+ * Part of our contract is that we leave the tab in the selected state.
+ */
+ openTab: function(aTabInfo)
+ {
+ // each tab gets its own messenger instance
+ // for undo/redo, backwards/forwards, etc.
+ messenger = Cc["@mozilla.org/messenger;1"]
+ .createInstance(Ci.nsIMessenger);
+ messenger.setWindow(window, msgWindow);
+ aTabInfo.messenger = messenger;
+
+ // remember the currently selected folder
+ aTabInfo.msgSelectedFolder = gMsgFolderSelected;
+
+ // show tab if permitted
+ if (aTabInfo.switchToNewTab)
+ this.showTab(aTabInfo);
+ },
+
+ showTab: function(aTabInfo)
+ {
+ // don't allow saveTabState while restoring a tab
+ aTabInfo.lock = true;
+ // set the messagepane as the primary browser for content
+ var messageBrowser = getMessageBrowser();
+ messageBrowser.setAttribute("type", "content");
+ messageBrowser.setAttribute("primary", "true");
+
+ if (aTabInfo.uriToOpen)
+ {
+ // HACK: Since we've switched away from the tab, we need to bring
+ // back the real selection before selecting the folder, so do that
+ RestoreSelectionWithoutContentLoad(document.getElementById("folderTree"));
+
+ // Clear selection, because context clicking on a folder and opening in a
+ // new tab needs to have selectFolder think the selection has changed.
+ gFolderTreeView.selection.clearSelection();
+ gFolderTreeView.selection.currentIndex = -1;
+ gMsgFolderSelected = null;
+ msgWindow.openFolder = null;
+
+ // clear gDBView so we won't try to close it
+ gDBView = null;
+
+ // reroot the message sink (we might have switched layout)
+ messenger.setWindow(null, null);
+ messenger.setWindow(window, msgWindow);
+
+ // Clear thread pane selection - otherwise, the tree tries to impose the
+ // the current selection on the new view.
+ let msgHdr = aTabInfo.hdr;
+ let msgId = aTabInfo.selectedMsgId;
+ aTabInfo.hdr = null;
+ aTabInfo.selectedMsgId = null;
+ aTabInfo.dbView = null;
+ let folder = MailUtils.getFolderForURI(aTabInfo.uriToOpen);
+ gFolderTreeView.selectFolder(folder);
+ gCurrentFolderToReroot = null;
+ delete aTabInfo.uriToOpen; // destroy after use!
+ // Store the folder that is being opened.
+ aTabInfo.msgSelectedFolder = folder;
+
+ // restore our message data
+ aTabInfo.hdr = msgHdr;
+ aTabInfo.selectedMsgId = msgId;
+
+ aTabInfo.dbView = gDBView;
+ UpdateMailToolbar("new tab");
+ }
+
+ // Do not bother with Thread and Message panes if at server level.
+ if (!aTabInfo.msgSelectedFolder.isServer) {
+ // Restore the layout if present.
+ ShowThreadPane();
+ // Some modes (e.g. new message tabs) need to initially hide the
+ // splitters, this is marked by aTabInfo.clearSplitter=true.
+ let clearSplitter = "clearSplitter" in aTabInfo && aTabInfo.clearSplitter;
+ if (clearSplitter) {
+ aTabInfo.messageSplitter.collapsible = true;
+ aTabInfo.folderSplitter.collapsible = true;
+ delete aTabInfo.clearSplitter;
+ }
+ SetSplitterState(GetThreadAndMessagePaneSplitter(),
+ aTabInfo.messageSplitter);
+ SetSplitterState(GetFolderPaneSplitter(),
+ aTabInfo.folderSplitter);
+ this._updatePaneLayout(aTabInfo);
+ ClearMessagePane();
+ // Force the header pane twisty state restoration by toggling from the
+ // opposite.
+ if (gCollapsedHeaderViewMode != aTabInfo.headerViewMode)
+ ToggleHeaderView();
+ }
+
+ // restore globals
+ messenger = aTabInfo.messenger;
+ gDBView = aTabInfo.dbView;
+ gSearchSession = aTabInfo.searchSession;
+ let folderToSelect = aTabInfo.msgSelectedFolder || gDBView && gDBView.msgFolder;
+
+ // restore view state if we had one
+ let row = gFolderTreeView.getIndexOfFolder(folderToSelect);
+ let treeBoxObj = document.getElementById("folderTree").treeBoxObject;
+ let folderTreeSelection = gFolderTreeView.selection;
+
+ // make sure that row.value is valid so that it doesn't mess up
+ // the call to ensureRowIsVisible()
+ if ((row >= 0) && !folderTreeSelection.isSelected(row))
+ {
+ gMsgFolderSelected = folderToSelect;
+ msgWindow.openFolder = folderToSelect;
+ folderTreeSelection.select(row);
+ treeBoxObj.ensureRowIsVisible(row);
+ }
+
+ if (gDBView)
+ {
+ // This sets the thread pane tree's view to the gDBView view.
+ UpdateSortIndicators(gDBView.sortType, gDBView.sortOrder);
+ RerootThreadPane();
+
+ // We don't want to reapply the mailview (threadpane changes by switching
+ // tabs alone would be rather surprising), just update the viewpicker
+ // and resave the new view.
+ UpdateViewPickerByValue(aTabInfo.mailView);
+ SetMailViewForFolder(folderToSelect, aTabInfo.mailView);
+
+ // restore quick search
+ GetSearchInput().value = aTabInfo.searchInput;
+
+ // We need to restore the selection to what it was when we switched away
+ // from this tab. We need to remember the selected keys, instead of the
+ // selected indices, since the view might have changed. But maybe the
+ // selectedIndices adjust as items are added/removed from the (hidden)
+ // view.
+ try
+ {
+ if (aTabInfo.selectedMsgId && aTabInfo.msgSelectedFolder)
+ {
+ // We clear the selection in order to generate an event when we
+ // re-select our message. This destroys aTabInfo.selectedMsgId.
+ let selectedMsgId = aTabInfo.selectedMsgId;
+ ClearThreadPaneSelection();
+ aTabInfo.selectedMsgId = selectedMsgId;
+ let msgDB = aTabInfo.msgSelectedFolder.msgDatabase;
+ let msgHdr = msgDB.getMsgHdrForMessageID(aTabInfo.selectedMsgId);
+ setTimeout(gDBView.selectFolderMsgByKey,
+ 0,
+ aTabInfo.msgSelectedFolder,
+ msgHdr.messageKey);
+ }
+ // We do not clear the selection if there was more than one message
+ // displayed. this leaves our selection intact. there was originally
+ // some claim that the selection might lose synchronization with the
+ // view, but this is unsubstantiated. said comment came from the
+ // original code that stored information on the selected rows, but
+ // then failed to do anything with it, probably because there is no
+ // existing API call that accomplishes it.
+ }
+ catch (ex)
+ {
+ dump(ex);
+ }
+ GetThreadTree().treeBoxObject.scrollToRow(aTabInfo.firstVisibleRow);
+ }
+ else if (gMsgFolderSelected.isServer)
+ {
+ // Load AccountCentral page here.
+ ShowAccountCentral(gMsgFolderSelected);
+ }
+ SetUpToolbarButtons(gMsgFolderSelected.URI);
+ UpdateMailToolbar("tab changed");
+ delete aTabInfo.lock;
+ },
+
+ closeTab: function(aTabInfo)
+ {
+ // If the tab has never been opened, we must not clean up the view,
+ // because it still belongs to a different tab.
+ if (aTabInfo.uriToOpen)
+ return;
+
+ if (aTabInfo.dbView)
+ aTabInfo.dbView.close();
+ if (aTabInfo.messenger)
+ aTabInfo.messenger.setWindow(null, null);
+ },
+
+ // called when switching away from aTabInfo
+ saveTabState: function(aTabInfo)
+ {
+ if (aTabInfo.lock)
+ return;
+
+ // save message db data and view filters
+ aTabInfo.messenger = messenger;
+ aTabInfo.dbView = gDBView;
+ aTabInfo.searchSession = gSearchSession;
+ aTabInfo.msgSelectedFolder = gMsgFolderSelected;
+ aTabInfo.selectedMsgId = null;
+ if (gDBView)
+ {
+ // save thread pane scroll position
+ aTabInfo.firstVisibleRow = GetThreadTree().treeBoxObject.getFirstVisibleRow();
+
+ let curMsgViewIndex = gDBView.currentlyDisplayedMessage;
+ if (curMsgViewIndex != nsMsgViewIndex_None)
+ {
+ try // there may not be a selected message.
+ {
+ // the currentlyDisplayedMessage is not always the first selected
+ // message, e.g. on a right click for the context menu
+ let curMsgHdr = gDBView.getMsgHdrAt(curMsgViewIndex);
+ aTabInfo.selectedMsgId = curMsgHdr.messageId;
+ }
+ catch (ex) {}
+ }
+ if (!aTabInfo.selectedMsgId)
+ aTabInfo.msgSelectedFolder = gDBView.msgFolder;
+ }
+ aTabInfo.mailView = GetMailViewForFolder(aTabInfo.msgSelectedFolder);
+
+ // remember layout
+ aTabInfo.modeBits = this.getCurrentModeBits();
+ aTabInfo.messageSplitter = GetSplitterState(GetThreadAndMessagePaneSplitter());
+ aTabInfo.folderSplitter = GetSplitterState(GetFolderPaneSplitter());
+
+ // header pane twisty state
+ aTabInfo.headerViewMode = gCollapsedHeaderViewMode;
+
+ // quick search
+ aTabInfo.searchInput = GetSearchInput().value;
+ },
+
+ onTitleChanged: function(aTabInfo, aTabNode)
+ {
+ // If we have an account, we also always have a "Local Folders" account,
+ let multipleRealAccounts = MailServices.accounts.accounts.length > 2;
+
+ // clear out specific tab data now, because we might need to return early
+ aTabNode.removeAttribute("SpecialFolder");
+ aTabNode.removeAttribute("ServerType");
+ aTabNode.removeAttribute("IsServer");
+ aTabNode.removeAttribute("IsSecure");
+ aTabNode.removeAttribute("NewMessages");
+ aTabNode.removeAttribute("ImapShared");
+ aTabNode.removeAttribute("BiffState");
+ aTabNode.removeAttribute("MessageType");
+ aTabNode.removeAttribute("Offline");
+ aTabNode.removeAttribute("Attachment");
+ aTabNode.removeAttribute("IMAPDeleted");
+
+ // aTabInfo.msgSelectedFolder may contain the base folder of saved search
+ let folder = null;
+ if (aTabInfo.uriToOpen)
+ {
+ // select folder for the backgound tab without changing the current one
+ // (stolen from SelectFolder)
+ folder = MailUtils.getFolderForURI(aTabInfo.uriToOpen);
+ }
+ else
+ {
+ folder = (aTabInfo.dbView && aTabInfo.dbView.viewFolder) ||
+ (aTabInfo.dbView && aTabInfo.dbView.msgFolder) ||
+ aTabInfo.msgSelectedFolder || gMsgFolderSelected;
+ }
+
+ // update the message header only if we're the current tab
+ if (aTabNode.selected)
+ {
+ try
+ {
+ aTabInfo.hdr = aTabInfo.dbView && aTabInfo.dbView.hdrForFirstSelectedMessage;
+ }
+ catch (e)
+ {
+ aTabInfo.hdr = null;
+ }
+ }
+
+ // update tab title and icon state
+ aTabInfo.title = "";
+ if (IsMessagePaneCollapsed() || !aTabInfo.hdr)
+ {
+ // Folder Tab
+ aTabNode.setAttribute("type", "folder"); // override "3pane"
+ if (!folder)
+ {
+ // nothing to do
+ return;
+ }
+ else
+ {
+ aTabInfo.title = folder.prettyName;
+ if (!folder.isServer && multipleRealAccounts)
+ aTabInfo.title += " - " + folder.server.prettyName;
+ }
+
+ // The user may have changed folders, triggering our onTitleChanged callback.
+ // Update the appropriate attributes on the tab.
+ aTabNode.setAttribute("SpecialFolder", FolderUtils.getSpecialFolderString(folder));
+ aTabNode.setAttribute("ServerType", folder.server.type);
+ aTabNode.setAttribute("IsServer", folder.isServer);
+ aTabNode.setAttribute("IsSecure", folder.server.isSecure);
+ aTabNode.setAttribute("NewMessages", folder.hasNewMessages);
+ aTabNode.setAttribute("ImapShared", folder.imapShared);
+
+ let biffState = "UnknownMail";
+ switch (folder.biffState)
+ {
+ case Ci.nsIMsgFolder.nsMsgBiffState_NewMail:
+ biffState = "NewMail";
+ break;
+ case Ci.nsIMsgFolder.nsMsgBiffState_NoMail:
+ biffState = "NoMail";
+ break;
+ }
+ aTabNode.setAttribute("BiffState", biffState);
+ }
+ else
+ {
+ // Message Tab
+ aTabNode.setAttribute("type", "message"); // override "3pane"
+ if (aTabInfo.hdr.flags & Ci.nsMsgMessageFlags.HasRe)
+ aTabInfo.title = "Re: ";
+ if (aTabInfo.hdr.mime2DecodedSubject)
+ aTabInfo.title += aTabInfo.hdr.mime2DecodedSubject;
+ aTabInfo.title += " - " + aTabInfo.hdr.folder.prettyName;
+ if (multipleRealAccounts)
+ aTabInfo.title += " - " + aTabInfo.hdr.folder.server.prettyName;
+
+ // message specific tab data
+ let flags = aTabInfo.hdr.flags;
+ aTabNode.setAttribute("MessageType", folder.server.type);
+ aTabNode.setAttribute("Offline",
+ Boolean(flags & Ci.nsMsgMessageFlags.Offline));
+ aTabNode.setAttribute("Attachment",
+ Boolean(flags & Ci.nsMsgMessageFlags.Attachment));
+ aTabNode.setAttribute("IMAPDeleted",
+ Boolean(flags & Ci.nsMsgMessageFlags.IMAPDeleted));
+ }
+ },
+
+ getBrowser: function(aTabInfo)
+ {
+ // we currently use the messagepane element for all 3pane tab types
+ return getMessageBrowser();
+ },
+
+ //
+ // nsIController implementation
+ //
+ // We ignore the aTabInfo parameter sent by tabmail when calling nsIController
+ // stuff and just delegate the call to the DefaultController by using it as
+ // our proto chain.
+ // XXX remove the MessageWindowController stuff once we kill messageWindow.xul
+ __proto__: "DefaultController" in window && window.DefaultController ||
+ "MessageWindowController" in window && window.MessageWindowController
+};
+
+
+
+//
+// tabmail support methods
+//
+
+function GetTabMail()
+{
+ return document.getElementById("tabmail");
+}
+
+function MsgOpenNewTab(aType, aModeBits, aBackground) {
+ // duplicate the current tab
+ var tabmail = GetTabMail();
+ if (tabmail)
+ tabmail.openTab(aType, {modeBits: aModeBits, background: aBackground});
+}
+
+function MsgOpenNewTabForFolder(aBackground) {
+ // open current folder in full 3pane tab
+ MsgOpenNewTab("3pane", kTabModeFolder, aBackground);
+}
+
+function MsgOpenNewTabForMessage(aBackground) {
+ // open current message in message tab
+ MsgOpenNewTab("3pane", kTabModeMessage, aBackground);
+}
+
+// A Thunderbird compatibility function called from e.g. newsblog.
+// We ignore aHandlerRegExp as it is not needed by SeaMonkey.
+function openContentTab(aUrl, aWhere, aHandlerRegExp)
+{
+ openUILinkIn(aUrl, aWhere);
+}
+
+function AllowOpenTabOnMiddleClick()
+{
+ return Services.prefs.getBoolPref("mail.tabs.opentabfor.middleclick");
+}
+
+function AllowOpenTabOnDoubleClick()
+{
+ return Services.prefs.getBoolPref("mail.tabs.opentabfor.doubleclick");
+}
+
+//
+// pane management
+// (maybe we should cache these items in a global object?)
+//
+
+function GetFolderPane()
+{
+ return document.getElementById("folderPaneBox");
+}
+
+function GetThreadPane()
+{
+ return document.getElementById("threadPaneBox");
+}
+
+function GetDisplayDeck()
+{
+ return document.getElementById("displayDeck");
+}
+
+function GetMessagePane()
+{
+ return document.getElementById("messagepanebox");
+}
+
+function GetHeaderPane()
+{
+ return document.getElementById("msgHeaderView");
+}
+
+function GetFolderPaneSplitter()
+{
+ return document.getElementById("folderpane-splitter");
+}
+
+function GetThreadAndMessagePaneSplitter()
+{
+ return document.getElementById("threadpane-splitter");
+}
+
+
+
+//
+// pane visibility management
+//
+// - collapsing the folderpane by clicking its splitter doesn't need
+// additional processing
+// - collapsing the messagepane by clicking its splitter needs some special
+// treatment of attachments, gDBView, etc.
+// - the threadpane has no splitter assigned to it
+// - collapsing the messagepane, threadpane or folderpane by <key> needs to
+// pay attention to the other panes' (and splitters') visibility
+
+function IsMessagePaneCollapsed()
+{
+ return GetMessagePane().collapsed;
+}
+
+function IsDisplayDeckCollapsed()
+{
+ // regard display deck as collapsed in the standalone message window
+ var displayDeck = GetDisplayDeck();
+ return !displayDeck || displayDeck.collapsed;
+}
+
+function IsFolderPaneCollapsed()
+{
+ // regard folderpane as collapsed in the standalone message window
+ var folderPane = GetFolderPane();
+ return !folderPane || folderPane.collapsed;
+}
+
+// Which state is the splitter in? Is it collapsed?
+// How wide/high is the associated pane?
+function GetSplitterState(aSplitter)
+{
+ var next = aSplitter.getAttribute("collapse") == "after";
+ var pane = next ? aSplitter.nextSibling : aSplitter.previousSibling;
+ var vertical = aSplitter.orient == "vertical";
+ var rv =
+ {
+ state: aSplitter.getAttribute("state"),
+ collapsed: aSplitter.collapsed,
+ // <splitter>s are <hbox>es,
+ // thus the "orient" attribute is usually either unset or "vertical"
+ size: vertical ? pane.height : pane.width,
+ collapsible: "collapsible" in aSplitter && aSplitter.collapsible
+ };
+ return rv;
+}
+
+function SetSplitterState(aSplitter, aState)
+{
+ // all settings in aState are optional
+ if (!aState)
+ return;
+ if ("state" in aState)
+ aSplitter.setAttribute("state", aState.state);
+ if ("collapsed" in aState)
+ aSplitter.collapsed = aState.collapsed;
+ if ("size" in aState)
+ {
+ let next = aSplitter.getAttribute("collapse") == "after";
+ let pane = next ? aSplitter.nextSibling : aSplitter.previousSibling;
+ let vertical = aSplitter.orient == "vertical";
+ if (vertical)
+ {
+ // vertical splitter orientation
+ pane.height = aState.size;
+ }
+ else
+ {
+ // horizontal splitter orientation
+ pane.width = aState.size;
+ }
+ }
+ if ("collapsible" in aState)
+ aSplitter.collapsible = aState.collapsible;
+}
+
+// If we hit one of the pane splitter <key>s or choose the respective menuitem,
+// we show/hide both the pane *and* the splitter, just like we do for the
+// browser sidebar. Clicking a splitter's grippy, though, will hide the pane
+// but not the splitter.
+function MsgToggleSplitter(aSplitter)
+{
+ var state = aSplitter.getAttribute("state");
+ if (state == "collapsed")
+ {
+ // removing the attribute would hurt persistency
+ aSplitter.setAttribute("state", "open");
+ aSplitter.collapsed = false; // always show splitter when open
+ }
+ else
+ {
+ aSplitter.setAttribute("state", "collapsed");
+ aSplitter.collapsed = true; // hide splitter
+ }
+}
+
+function MsgCollapseSplitter(aSplitter, aCollapse)
+{
+ if (!("collapsible" in aSplitter))
+ aSplitter.collapsible = true;
+ aSplitter.collapsed = aCollapse && aSplitter.collapsible;
+}
+
+// helper function for UpdateLayoutVisibility
+function UpdateFolderPaneFlex(aTuneLayout)
+{
+ var folderBox = GetFolderPane();
+ var messagesBox = document.getElementById("messagesBox");
+ if (aTuneLayout)
+ {
+ // tune folderpane layout
+ folderBox.setAttribute("flex", "1");
+ messagesBox.removeAttribute("flex");
+ }
+ else
+ {
+ // restore old layout
+ folderBox.removeAttribute("flex");
+ messagesBox.setAttribute("flex", "1");
+ }
+}
+
+// we need to finetune the pane and splitter layout in certain circumstances
+function UpdateLayoutVisibility()
+{
+ var modeBits = gMailNewsTabsType.getCurrentModeBits();
+ var folderPaneVisible = modeBits & kTabShowFolderPane;
+ var messagePaneVisible = modeBits & kTabShowMessagePane;
+ var threadPaneVisible = modeBits & kTabShowThreadPane;
+ var displayDeckVisible = modeBits & kTabMaskDisplayDeck;
+ var onlyFolderPane = modeBits == kTabShowFolderPane;
+ var onlyMessagePane = modeBits == kTabShowMessagePane;
+ var onlyDisplayDeck = modeBits == kTabShowThreadPane ||
+ modeBits == kTabShowAcctCentral;
+ var onlyOnePane = onlyFolderPane || onlyMessagePane || onlyDisplayDeck;
+ var showFolderSplitter = false;
+ var showMessageSplitter = false;
+ switch (Services.prefs.getIntPref("mail.pane_config.dynamic"))
+ {
+ case kClassicMailLayout:
+ // if only the folderpane is visible it has to flex,
+ // while the messagesbox must not
+ UpdateFolderPaneFlex(onlyFolderPane);
+ if (!onlyOnePane)
+ {
+ showFolderSplitter = folderPaneVisible;
+ showMessageSplitter = threadPaneVisible && messagePaneVisible;
+ }
+ break;
+
+ case kWideMailLayout:
+ // if only the messagepane is visible, collapse the rest
+ let messengerBox = document.getElementById("messengerBox");
+ messengerBox.collapsed = onlyMessagePane;
+ // a hidden displaydeck must not flex, while the folderpane has to
+ if (!onlyMessagePane)
+ UpdateFolderPaneFlex(!displayDeckVisible);
+ if (!onlyOnePane)
+ {
+ showFolderSplitter = folderPaneVisible && displayDeckVisible;
+ showMessageSplitter = messagePaneVisible;
+ }
+ break;
+
+ case kVerticalMailLayout:
+ // if the threadpane is hidden, we need to hide its outer box as well
+ let messagesBox = document.getElementById("messagesBox");
+ messagesBox.collapsed = !displayDeckVisible;
+ // if only the folderpane is visible, it needs to flex
+ UpdateFolderPaneFlex(onlyFolderPane);
+ if (!onlyOnePane)
+ {
+ showFolderSplitter = folderPaneVisible;
+ showMessageSplitter = messagePaneVisible;
+ }
+ break;
+ }
+
+ // set splitter visibility
+ // if the pane was hidden by clicking the splitter grippy,
+ // the splitter must not hide
+ MsgCollapseSplitter(GetFolderPaneSplitter(), !showFolderSplitter);
+ MsgCollapseSplitter(GetThreadAndMessagePaneSplitter(), !showMessageSplitter);
+
+ // disable location bar if only message pane is visible
+ document.getElementById("locationFolders").disabled = onlyMessagePane;
+ // disable mailviews and search if threadpane is invisible
+ if (!threadPaneVisible)
+ gDisableViewsSearch.setAttribute("disabled", true);
+ else
+ gDisableViewsSearch.removeAttribute("disabled");
+}
+
+function ChangeMessagePaneVisibility()
+{
+ var hidden = IsMessagePaneCollapsed();
+ // We also have to disable the Message/Attachments menuitem.
+ // It will be enabled when loading a message with attachments
+ // (see messageHeaderSink.handleAttachment).
+ if (hidden)
+ {
+ let node = document.getElementById("msgAttachmentMenu");
+ if (node)
+ node.setAttribute("disabled", "true");
+ }
+
+ if (gDBView)
+ {
+ // clear the subject, collapsing won't automatically do this
+ setTitleFromFolder(GetThreadPaneFolder(), null);
+ // the collapsed state is the state after we released the mouse
+ // so we take it as it is
+ gDBView.suppressMsgDisplay = hidden;
+ // set the subject, uncollapsing won't automatically do this
+ gDBView.loadMessageByUrl("about:blank");
+ gDBView.selectionChanged();
+ }
+
+ var event = new Event( "messagepane-"+ (hidden ? "hide" : "unhide"),
+ { bubbles: false, cancelable: true });
+ document.getElementById("messengerWindow").dispatchEvent(event);
+}
+
+function MsgToggleMessagePane(aToggleManually)
+{
+ // don't hide all three panes at once
+ if (IsDisplayDeckCollapsed() && IsFolderPaneCollapsed())
+ return;
+ // toggle the splitter manually if it wasn't clicked and remember that
+ var splitter = GetThreadAndMessagePaneSplitter();
+ if (aToggleManually)
+ MsgToggleSplitter(splitter);
+ splitter.collapsible = aToggleManually;
+ ChangeMessagePaneVisibility();
+ UpdateLayoutVisibility();
+}
+
+function MsgToggleFolderPane(aToggleManually)
+{
+ // don't hide all three panes at once
+ if (IsDisplayDeckCollapsed() && IsMessagePaneCollapsed())
+ return;
+ // toggle the splitter manually if it wasn't clicked and remember that
+ var splitter = GetFolderPaneSplitter();
+ if (aToggleManually)
+ MsgToggleSplitter(splitter);
+ splitter.collapsible = aToggleManually;
+ UpdateLayoutVisibility();
+}
+
+function MsgToggleThreadPane()
+{
+ // don't hide all three panes at once
+ if (IsFolderPaneCollapsed() && IsMessagePaneCollapsed())
+ return;
+ var threadPane = GetDisplayDeck();
+ threadPane.collapsed = !threadPane.collapsed;
+ // we only get here by hitting a key, so always hide border splitters
+ UpdateLayoutVisibility();
+}
+
+// When the ThreadPane is hidden via the displayDeck, we should collapse the
+// elements that are only meaningful to the thread pane. When AccountCentral is
+// shown via the displayDeck, we need to switch the displayDeck to show the
+// accountCentralBox and load the iframe in the AccountCentral box with the
+// corresponding page.
+function ShowAccountCentral(displayedFolder)
+{
+ GetDisplayDeck().selectedPanel = accountCentralBox;
+ let acctCentralPage = GetLocalizedStringPref("mailnews.account_central_page.url");
+ if (acctCentralPage) {
+ let loadURL =
+ acctCentralPage +
+ (displayedFolder ? "?folderURI=" + displayedFolder : "");
+ if (window.frames["accountCentralPane"].location.href != loadURL) {
+ window.frames["accountCentralPane"].location.href = loadURL;
+ }
+ } else {
+ dump("Error loading AccountCentral page\n");
+ }
+}
+
+function ShowThreadPane()
+{
+ GetDisplayDeck().selectedPanel = GetThreadPane();
+}
+
+function ShowingThreadPane()
+{
+ gDisableViewsSearch.removeAttribute("disabled");
+ var threadPaneSplitter = GetThreadAndMessagePaneSplitter();
+ threadPaneSplitter.collapsed = false;
+ if (!threadPaneSplitter.hidden && threadPaneSplitter.getAttribute("state") != "collapsed")
+ {
+ GetMessagePane().collapsed = false;
+ // XXX We need to force the tree to refresh its new height
+ // so that it will correctly scroll to the newest message
+ GetThreadTree().boxObject.height;
+ }
+ document.getElementById("key_toggleThreadPane").removeAttribute("disabled");
+ document.getElementById("key_toggleMessagePane").removeAttribute("disabled");
+}
+
+function HidingThreadPane()
+{
+ ClearThreadPane();
+ GetUnreadCountElement().hidden = true;
+ GetTotalCountElement().hidden = true;
+ GetMessagePane().collapsed = true;
+ GetThreadAndMessagePaneSplitter().collapsed = true;
+ gDisableViewsSearch.setAttribute("disabled", true);
+ document.getElementById("key_toggleThreadPane").setAttribute("disabled", "true");
+ document.getElementById("key_toggleMessagePane").setAttribute("disabled", "true");
+}
+
+var gCurrentDisplayDeckId = "";
+function ObserveDisplayDeckChange(aEvent)
+{
+ var selectedPanel = GetDisplayDeck().selectedPanel;
+ var nowSelected = selectedPanel ? selectedPanel.id : "";
+ // onselect fires for every mouse click inside the deck, so ObserveDisplayDeckChange
+ // is getting called every time we click on a message in the thread pane.
+ // Only show/hide elements if the selected deck is actually changing.
+ if (nowSelected != gCurrentDisplayDeckId)
+ {
+ if (nowSelected == "threadPaneBox")
+ ShowingThreadPane();
+ else
+ HidingThreadPane();
+
+ if (nowSelected == "accountCentralBox") {
+ if (!document.getElementById("folderPaneBox").collapsed)
+ document.getElementById("folderTree").focus();
+ gAccountCentralLoaded = true;
+ } else {
+ gAccountCentralLoaded = false;
+ }
+ gCurrentDisplayDeckId = nowSelected;
+ }
+}
+
+function InvalidateTabDBs()
+{
+ // enforce reloading the tab's dbView
+ var tabInfos = GetTabMail().tabInfo;
+ for (let i = 0; i < tabInfos.length; ++i)
+ {
+ let tabInfo = tabInfos[i];
+ // only reroot 3pane tabs
+ if (tabInfo.mode.type == "3pane")
+ {
+ // don't change URI if already set -
+ // we might try to read from an invalid msgSelectedFolder
+ if (!("uriToOpen" in tabInfo))
+ tabInfo.uriToOpen = tabInfo.msgSelectedFolder.URI;
+ }
+ }
+}
diff --git a/comm/suite/mailnews/content/tabmail.xml b/comm/suite/mailnews/content/tabmail.xml
new file mode 100644
index 0000000000..613bb3a418
--- /dev/null
+++ b/comm/suite/mailnews/content/tabmail.xml
@@ -0,0 +1,1583 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<!DOCTYPE bindings [
+ <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
+ %messengerDTD;
+ <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
+ %globalDTD;
+]>
+
+<bindings id="tabmailBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <!-- SeaMonkey's clone of Thunderbird's tab UI mechanism.
+ -
+ - We expect to be instantiated with the following children:
+ - * One "tabpanels" child element whose id must be placed in the
+ - "panelcontainer" attribute on the element we are being bound to. We do
+ - this because it is important to allow overlays to contribute panels.
+ - When we attempted to have the immediate children of the bound element
+ - be propagated through use of the "children" tag, we found that children
+ - contributed by overlays did not propagate.
+ - * Any children you want added to the right side of the tab bar. This is
+ - primarily intended to allow for "open a BLANK tab" buttons, namely
+ - calendar and tasks. For reasons similar to the tabpanels case, we
+ - expect the instantiating element to provide a child hbox for overlays
+ - to contribute buttons to.
+ -
+ - From a javascript perspective, there are three types of code that we
+ - expect to interact with:
+ - 1) Code that wants to open new tabs.
+ - 2) Code that wants to contribute one or more varieties of tabs.
+ - 3) Code that wants to monitor to know when the active tab changes.
+ -
+ - Consumer code should use the following methods:
+ - * openTab(aTabModeName, aArgs): Open a tab of the given "mode",
+ - passing the provided arguments as an object. The tab type author
+ - should tell you the modes they implement and the required/optional
+ - arguments.
+ - One of the arguments you can pass is "background": if this is true,
+ - the tab will be loaded in the background.
+ - * setTabTitle([aOptionalTabInfo]): Tells us that the title of the current
+ - tab (if no argument is provided) or provided tab needs to be updated.
+ - This will result in a call to the tab mode's logic to update the title.
+ - In the event this is not for the current tab, the caller is responsible
+ - for ensuring that the underlying tab mode is capable of providing a tab
+ - title when it is in the background.
+ - * removeCurrentTab(): Close the current tab.
+ - * removeTab(aTabElement): Close the tab whose tabmail-tab bound
+ - element is passed in.
+ - Changing the currently displayed tab is accomplished by changing
+ - tabmail.tabContainer's selectedIndex or selectedItem property.
+ -
+ - Tab contributing code should define a tab type object and register it
+ - with us by calling registerTabType. Each tab type can provide multiple
+ - tab modes. The rationale behind this organization is that Thunderbird
+ - historically/currently uses a single 3-pane view to display both
+ - three-pane folder browsing and single message browsing across multiple
+ - tabs. Each tab type has the ability to use a single tab panel for all
+ - of its display needs. So Thunderbird's "mail" tab type covers both the
+ - "folder" (3-pane folder-based browsing) and "message" (just a single
+ - message) tab modes, while SeaMonkey integrates both flavours into just
+ - one "3pane" mode. Likewise, calendar/lightning currently displays
+ - both its calendar and tasks in the same panel. A tab type can also
+ - create a new tabpanel for each tab as it is created. In that case, the
+ - tab type should probably only have a single mode unless there are a
+ - number of similar modes that can gain from code sharing.
+ - The tab type definition should include the following attributes:
+ - * name: The name of the tab-type, mainly to aid in debugging.
+ - * panelId or perTabPanel: If using a single tab panel, the id of the
+ - panel must be provided in panelId. If using one tab panel per tab,
+ - perTabPanel should be either the XUL element name that should be
+ - created for each tab, or a helper function to create and return the
+ - element.
+ - * modes: An object whose attributes are mode names (which are
+ - automatically propagated to a 'name' attribute for debugging) and
+ - values are objects with the following attributes...
+ - * any of the openTab/closeTab/saveTabState/showTab/onTitleChanged
+ - functions as described on the mode definitions. These will only be
+ - called if the mode does not provide the functions. Note that because
+ - the 'this' variable passed to the functions will always reference the
+ - tab type definition (rather than the mode definition), the mode
+ - functions can defer to the tab type functions by calling
+ - this.functionName(). (This should prove convenient.)
+ - Mode definition attributes:
+ - * type: The "type" attribute to set on the displayed tab for CSS purposes.
+ - Generally, this would be the same as the mode name, but you can do as
+ - you please.
+ - * isDefault: This should only be present and should be true for the tab
+ - mode that is the tab displayed automatically on startup.
+ - * maxTabs: The maximum number of this mode that can be opened at a time.
+ - If this limit is reached, any additional calls to openTab for this
+ - mode will simply result in the first existing tab of this mode being
+ - displayed.
+ - * shouldSwitchTo(aArgs): Optional function. Called when openTab is called
+ - on the top-level tabmail binding. It is used to decide if the openTab
+ - function should switch to an existing tab or actually open a new tab.
+ - If the openTab function should switch to an existing tab, return the
+ - index of that tab; otherwise return -1.
+ - aArgs is a set of named parameters (the ones that are later passed to
+ - openTab).
+ - * openTab(aTabInfo, aArgs): Called when a tab of the given mode is in the
+ - process of being opened. aTabInfo will have its "mode" attribute
+ - set to the mode definition of the tab mode being opened. You should
+ - set the "title" attribute on it, and may set any other attributes
+ - you wish for your own use in subsequent functions. Note that 'this'
+ - points to the tab type definition, not the mode definition as you
+ - might expect. This allows you to place common logic code on the
+ - tab type for use by multiple modes and to defer to it. Any arguments
+ - provided to the caller of tabmail.openTab will be passed to your
+ - function as well, including background.
+ - * canCloseTab(aTabInfo): Optional function.
+ - Return true (false) if the tab is (not) allowed to close.
+ - A tab's default permission is stored in aTabInfo.canClose.
+ - * closeTab(aTabInfo): Called when aTabInfo is being closed. The tab need
+ - not be currently displayed. You are responsible for properly cleaning
+ - up any state you preserved in aTabInfo.
+ - * saveTabState(aTabInfo): Called when aTabInfo is being switched away from
+ - so that you can preserve its state on aTabInfo. This is primarily for
+ - single tab panel implementations; you may not have much state to save
+ - if your tab has its own tab panel.
+ - * showTab(aTabInfo): Called when aTabInfo is being displayed and you
+ - should restore its state (if required).
+ - * onTitleChanged(aTabInfo): Called when someone calls
+ - tabmail.setTabTitle() to hint that the tab's title needs to be
+ - updated. This function should update aTabInfo.title if it can.
+ - * getBrowser(aTabInfo): This function should return the browser element
+ - for your tab if there is one (return null or don't define this
+ - function otherwise). It is used for some toolkit functions that
+ - require a global "getBrowser" function, e.g. ZoomManager.
+ -
+ - Mode definition functions for menu/toolbar commands (see nsIController):
+ - * supportsCommand(aCommand, aTabInfo): Called when a menu or toolbar needs
+ - to be updated. Return true if you support that command in
+ - isCommandEnabled and doCommand, return false otherwise.
+ - * isCommandEnabled(aCommand, aTabInfo): Called when a menu or toolbar
+ - needs to be updated. Return true if the command can be executed at the
+ - current time, false otherwise.
+ - * doCommand(aCommand, aTabInfo): Called when a menu or toolbar command is
+ - to be executed. Perform the action appropriate to the command.
+ - * onEvent(aEvent, aTabInfo): This can be used to handle different events
+ - on the window.
+ -
+ - Tab monitoring code is expected to be used for widgets on the screen
+ - outside of the tab box that need to update themselves as the active tab
+ - changes. This is primarily intended to be used for the ThunderBar; if
+ - you are not the ThunderBar and this sounds appealing to you, please
+ - solicit discussion on your needs on the mozilla.dev.apps.thunderbird
+ - newsgroup.
+ - Tab monitoring code (un)registers itself via (un)registerTabMonitor.
+ - The following functions should be provided on the monitor object:
+ - * onTabTitleChanged(aTabInfo): Called when the tab's title changes.
+ - * onTabSwitched(aTabInfo, aOldTabInfo): Called when a new tab is made
+ - active. If this is the first tab ever, aOldTabInfo will be null,
+ - otherwise aOldTabInfo will be the previously active tab.
+ -->
+ <binding id="tabmail"
+ extends="chrome://navigator/content/tabbrowser.xml#tabbrowser">
+ <resources>
+ <stylesheet src="chrome://navigator/skin/tabbrowser.css"/>
+ </resources>
+ <content>
+ <xul:stringbundle anonid="tmstringbundle" src="chrome://messenger/locale/tabmail.properties"/>
+ <xul:tabbox anonid="tabbox"
+ flex="1"
+ eventnode="document"
+ onselect="if (event.target.localName == 'tabs' &amp;&amp;
+ 'updateCurrentTab' in this.parentNode)
+ this.parentNode.updateCurrentTab();">
+ <xul:hbox class="tab-drop-indicator-bar" collapsed="true">
+ <xul:hbox class="tab-drop-indicator" mousethrough="always"/>
+ </xul:hbox>
+ <xul:hbox class="tabbrowser-strip tabmail-strip"
+ tooltip="_child"
+ context="_child"
+ anonid="strip"
+ ondragstart="nsDragAndDrop.startDrag(event, this.parentNode.parentNode); event.stopPropagation();"
+ ondragover="nsDragAndDrop.dragOver(event, this.parentNode.parentNode); event.stopPropagation();"
+ ondrop="nsDragAndDrop.drop(event, this.parentNode.parentNode); event.stopPropagation();"
+ ondragexit="nsDragAndDrop.dragExit(event, this.parentNode.parentNode); event.stopPropagation();">
+ <xul:tooltip onpopupshowing="var tabmail = this.parentNode.parentNode.parentNode;
+ return tabmail.FillTabmailTooltip(document, event);"/>
+ <xul:menupopup anonid="tabContextMenu"
+ onpopupshowing="return document.getBindingParent(this)
+ .onTabContextMenuShowing();">
+ <xul:menuitem label="&closeTabCmd.label;"
+ accesskey="&closeTabCmd.accesskey;"
+ oncommand="var tabmail = document.getBindingParent(this);
+ tabmail.removeTab(tabmail.mContextTab);"/>
+ </xul:menupopup>
+ <xul:tabs class="tabbrowser-tabs tabmail-tabs"
+ flex="1"
+ anonid="tabcontainer"
+ setfocus="false"
+ onclick="this.parentNode.parentNode.parentNode.onTabClick(event);">
+ <xul:tab selected="true"
+ validate="never"
+ type="3pane"
+ maxwidth="250"
+ width="0"
+ minwidth="100"
+ flex="100"
+ class="tabbrowser-tab tabmail-tab icon-holder"
+ crop="end"/>
+ </xul:tabs>
+ <children/>
+ </xul:hbox>
+ <!-- Remember, user of this binding, you need to provide tabpanels! -->
+ <children includes="tabpanels"/>
+ </xul:tabbox>
+ </content>
+
+ <implementation implements="nsIController, nsIObserver">
+ <constructor>
+ <![CDATA[
+ window.controllers.insertControllerAt(0, this);
+ const kAutoHide = "mail.tabs.autoHide";
+ this.mAutoHide = Services.prefs.getBoolPref(kAutoHide);
+ Services.prefs.addObserver(kAutoHide, this);
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ Services.prefs.removeObserver("mail.tabs.autoHide", this);
+ window.controllers.removeController(this);
+ ]]>
+ </destructor>
+
+ <field name="currentTabInfo">
+ null
+ </field>
+
+ <field name="tabTypes" readonly="true">
+ new Object()
+ </field>
+
+ <field name="tabModes" readonly="true">
+ new Object()
+ </field>
+
+ <field name="defaultTabMode">
+ null
+ </field>
+
+ <field name="tabInfo" readonly="true">
+ new Array()
+ </field>
+
+ <field name="tabStrip" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "strip");
+ </field>
+
+ <field name="tabContainer" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "tabcontainer");
+ </field>
+
+ <field name="panelContainer" readonly="true">
+ document.getElementById(this.getAttribute("panelcontainer"));
+ </field>
+ <field name="tabs" readonly="true">
+ this.tabContainer.childNodes
+ </field>
+ <field name="mStringBundle">
+ document.getAnonymousElementByAttribute(this, "anonid", "tmstringbundle");
+ </field>
+ <field name="mContextTab">
+ null
+ </field>
+
+ <!-- _mAutoHide/mAutoHide reflect the current autoHide pref value -->
+ <field name="_mAutoHide">false</field>
+ <property name="mAutoHide" onget="return this._mAutoHide;">
+ <setter>
+ <![CDATA[
+ if (val != this._mAutoHide)
+ {
+ if (this.tabContainer.childNodes.length == 1)
+ this.mStripVisible = !val;
+ this._mAutoHide = val;
+ }
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <!-- mStripVisible reflects the actual XUL autoHide state -->
+ <property name="mStripVisible"
+ onget="return !this.tabStrip.collapsed;"
+ onset="return this.tabStrip.collapsed = !val;"/>
+
+ <method name="registerTabType">
+ <parameter name="aTabType"/>
+ <body>
+ <![CDATA[
+ if (aTabType.name in this.tabTypes)
+ return;
+ this.tabTypes[aTabType.name] = aTabType;
+ for (let [modeName, modeDetails] of Object.entries(aTabType.modes))
+ {
+ modeDetails.name = modeName;
+ modeDetails.tabType = aTabType;
+ modeDetails.tabs = [];
+ this.tabModes[modeName] = modeDetails;
+ if (modeDetails.isDefault)
+ this.defaultTabMode = modeDetails;
+ }
+ aTabType.panel = document.getElementById(aTabType.panelId);
+ ]]>
+ </body>
+ </method>
+
+ <field name="tabMonitors" readonly="true">
+ new Array()
+ </field>
+
+ <method name="registerTabMonitor">
+ <parameter name="aTabMonitor"/>
+ <body>
+ <![CDATA[
+ if (!this.tabMonitors.includes(aTabMonitor))
+ this.tabMonitors.push(aTabMonitor);
+ ]]>
+ </body>
+ </method>
+
+ <method name="unregisterTabMonitor">
+ <parameter name="aTabMonitor"/>
+ <body>
+ <![CDATA[
+ let index = this.tabMonitors.indexOf(aTabMonitor);
+ if (index >= 0)
+ this.tabMonitors.splice(index, 1);
+ ]]>
+ </body>
+ </method>
+
+ <method name="openFirstTab">
+ <body>
+ <![CDATA[
+ // From the moment of creation, our XBL binding already has a
+ // visible tab. We need to create a tab information structure for
+ // this tab. In the process we also generate a synthetic "tab title
+ // changed" event to ensure we have an accurate title.
+ // Note: for mail tabs, the title gets only set later when the
+ // folder or message is loaded, as we don't have a gDBView yet!
+ // We assume the tab contents will set themselves up correctly.
+ if (!this.tabInfo.length)
+ {
+ let firstTabInfo = {mode: this.defaultTabMode, canClose: true};
+ let firstTabNode = this.tabContainer.firstChild;
+ firstTabInfo.mode.tabs.push(firstTabInfo);
+ this.tabInfo[0] = this.currentTabInfo = firstTabInfo;
+ this.setTabTitle(firstTabInfo);
+ if (this.tabMonitors.length)
+ {
+ for (let tabMonitor of this.tabMonitors)
+ tabMonitor.onTabSwitched(firstTabInfo, null);
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="openTab">
+ <parameter name="aTabModeName"/>
+ <parameter name="aArgs"/>
+ <body>
+ <![CDATA[
+ if (!aTabModeName)
+ aTabModeName = this.currentTabInfo.mode.type;
+
+ let tabMode = this.tabModes[aTabModeName];
+ // if we are already at our limit for this mode, show an existing one
+ if (tabMode.tabs.length == tabMode.maxTabs)
+ {
+ // show the first tab of this mode
+ this.tabContainer.selectedIndex = this.tabInfo.indexOf(tabMode.tabs[0]);
+ return;
+ }
+
+ // Do this so that we don't generate strict warnings.
+ let background = ("background" in aArgs) && aArgs.background;
+
+ // If the mode wants us to, we should switch to an existing tab
+ // rather than open a new one. We shouldn't switch to the tab if
+ // we're opening it in the background, though.
+ let shouldSwitchToFunc = tabMode.shouldSwitchTo ||
+ tabMode.tabType.shouldSwitchTo;
+
+ if (shouldSwitchToFunc)
+ {
+ let tabIndex = shouldSwitchToFunc.apply(tabMode.tabType, [aArgs]);
+ if (tabIndex >= 0)
+ {
+ if (!background)
+ this.selectTabByIndex(tabIndex);
+ return;
+ }
+ }
+
+ if (!background)
+ // we need to save the state before it gets corrupted
+ this.saveCurrentTabState();
+
+ let tabInfo = {mode: tabMode, canClose: true};
+ tabMode.tabs.push(tabInfo);
+
+ let t = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "tab");
+ t.setAttribute("crop", "end");
+ t.maxWidth = this.tabContainer.mTabMaxWidth;
+ t.minWidth = this.tabContainer.mTabMinWidth;
+ t.width = 0;
+ t.setAttribute("flex", "100");
+ t.setAttribute("validate", "never");
+ t.className = "tabbrowser-tab tabmail-tab icon-holder";
+ // for styling purposes, apply the type to the tab
+ // (this attribute may be overwritten by mode functions)
+ t.setAttribute("type", tabInfo.mode.type);
+ this.tabContainer.appendChild(t);
+ if (!this.mStripVisible)
+ {
+ this.mStripVisible = true;
+ this.tabContainer._updateCloseButtons();
+ }
+
+ let oldPanel = this.panelContainer.selectedPanel;
+
+ // Open new tabs in the background?
+ tabInfo.switchToNewTab = !background;
+
+ // the order of the following statements is important
+ let oldTabInfo = this.currentTabInfo;
+ this.tabInfo[this.tabContainer.childNodes.length - 1] = tabInfo;
+
+ if (!background) {
+ this.currentTabInfo = tabInfo;
+ // this has a side effect of calling updateCurrentTab, but our
+ // setting currentTabInfo above will cause it to take no action.
+ this.tabContainer.selectedIndex =
+ this.tabContainer.childNodes.length - 1;
+ }
+ // make sure we are on the right panel
+ let selectedPanel;
+ if (tabInfo.mode.tabType.perTabPanel)
+ {
+ // should we create the element for them, or will they do it?
+ if (typeof(tabInfo.mode.tabType.perTabPanel) == "string")
+ {
+ tabInfo.panel = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ tabInfo.mode.tabType.perTabPanel);
+ }
+ else
+ {
+ tabInfo.panel = tabInfo.mode.tabType.perTabPanel(tabInfo);
+ }
+ this.panelContainer.appendChild(tabInfo.panel);
+ selectedPanel = tabInfo.panel;
+ }
+ else
+ {
+ selectedPanel = tabInfo.mode.tabType.panel;
+ }
+ if (!background)
+ this.panelContainer.selectedPanel = selectedPanel;
+
+ oldPanel.removeAttribute("selected");
+ this.panelContainer.selectedPanel.setAttribute("selected", "true");
+
+ let tabOpenFunc = tabInfo.mode.openTab ||
+ tabInfo.mode.tabType.openTab;
+ if (tabOpenFunc)
+ tabOpenFunc.apply(tabInfo.mode.tabType, [tabInfo, aArgs]);
+
+ if (background) {
+ // if the new tab isn't made current,
+ // its title won't change automatically
+ this.setTabTitle(tabInfo);
+ }
+
+ if (!background && this.tabMonitors.length) {
+ for (let tabMonitor of this.tabMonitors)
+ tabMonitor.onTabSwitched(tabInfo, oldTabInfo);
+ }
+
+ t.setAttribute("label", tabInfo.title);
+
+ if (!background) {
+ let docTitle = tabInfo.title;
+ if (AppConstants.platform != "macosx") {
+ docTitle += " - " + gBrandBundle.getString("brandFullName");
+ }
+ document.title = docTitle;
+
+ // Update the toolbar status - we don't need to do menus as they
+ // do themselves when we open them.
+ UpdateMailToolbar("tabmail");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="selectTabByMode">
+ <parameter name="aTabModeName"/>
+ <body>
+ <![CDATA[
+ let tabMode = this.tabModes[aTabModeName];
+ if (tabMode.tabs.length)
+ {
+ let desiredTab = tabMode.tabs[0];
+ this.tabContainer.selectedIndex = this.tabInfo.indexOf(desiredTab);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="selectTabByIndex">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ // count backwards for aIndex < 0
+ if (aIndex < 0)
+ aIndex += this.tabInfo.length;
+ if (aIndex >= 0 &&
+ aIndex < this.tabInfo.length &&
+ aIndex != this.tabContainer.selectedIndex)
+ {
+ this.tabContainer.selectedIndex = aIndex;
+ }
+
+ if (aEvent)
+ {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="closeTabs">
+ <body>
+ <![CDATA[
+ for (let i = 0; i < this.tabInfo.length; i++)
+ {
+ let tabInfo = this.tabInfo[i];
+ let tabCloseFunc = tabInfo.mode.closeTab ||
+ tabInfo.mode.tabType.closeTab;
+ if (tabCloseFunc)
+ tabCloseFunc.call(tabInfo.mode.tabType, tabInfo);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeTab">
+ <parameter name="aTabNode"/>
+ <!-- parameter name="aMoreParameters..."/-->
+ <body>
+ <![CDATA[
+ // Find and locate the tab in our list.
+ let iTab, numTabs = this.tabContainer.childNodes.length;
+ for (iTab = 0; iTab < numTabs; iTab++)
+ if (this.tabContainer.childNodes[iTab] == aTabNode)
+ break;
+ let tabInfo = this.tabInfo[iTab];
+
+ // ask the tab type implementation if we're allowed to close the tab
+ let canClose = tabInfo.canClose;
+ let canCloseFunc = tabInfo.mode.canCloseTab ||
+ tabInfo.mode.tabType.canCloseTab;
+ if (canCloseFunc)
+ canClose = canCloseFunc.call(tabInfo.mode.tabType, tabInfo);
+ if (!canClose)
+ return;
+
+ let closeFunc = tabInfo.mode.closeTab ||
+ tabInfo.mode.tabType.closeTab;
+ if (closeFunc)
+ closeFunc.call(tabInfo.mode.tabType, tabInfo);
+
+ this.tabInfo.splice(iTab, 1);
+ tabInfo.mode.tabs.splice(tabInfo.mode.tabs.indexOf(tabInfo), 1);
+ aTabNode.remove();
+ --numTabs;
+ if (this.tabContainer.selectedIndex == -1)
+ this.tabContainer.selectedIndex = (iTab == numTabs) ? iTab - 1 : iTab;
+ if (this.currentTabInfo == tabInfo)
+ this.updateCurrentTab();
+
+ if (tabInfo.panel)
+ {
+ tabInfo.panel.remove();
+ delete tabInfo.panel;
+ }
+ if (numTabs == 1 && this.mAutoHide)
+ this.mStripVisible = false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeCurrentTab">
+ <body>
+ <![CDATA[
+ this.removeTab(this.tabContainer.selectedItem);
+ ]]>
+ </body>
+ </method>
+
+ <!-- UpdateCurrentTab - called in response to changing the current tab -->
+ <method name="updateCurrentTab">
+ <body>
+ <![CDATA[
+ if (this.currentTabInfo != this.tabInfo[this.tabContainer.selectedIndex])
+ {
+ if (this.currentTabInfo)
+ this.saveCurrentTabState();
+ let oldTabInfo = this.currentTabInfo;
+ let oldPanel = this.panelContainer.selectedPanel;
+ let tabInfo = this.currentTabInfo = this.tabInfo[this.tabContainer.selectedIndex];
+ this.panelContainer.selectedPanel = tabInfo.panel ||
+ tabInfo.mode.tabType.panel;
+
+ // Update the selected attribute on the current and old tab panel.
+ oldPanel.removeAttribute("selected");
+ this.panelContainer.selectedPanel.setAttribute("selected", "true");
+
+ let showTabFunc = tabInfo.mode.showTab ||
+ tabInfo.mode.tabType.showTab;
+ if (showTabFunc)
+ showTabFunc.call(tabInfo.mode.tabType, tabInfo);
+ if (this.tabMonitors.length)
+ {
+ for (let tabMonitor of this.tabMonitors)
+ tabMonitor.onTabSwitched(tabInfo, oldTabInfo);
+ }
+
+ let docTitle = tabInfo.title;
+ if (AppConstants.platform != "macosx") {
+ docTitle += " - " + gBrandBundle.getString("brandFullName");
+ }
+ document.title = docTitle;
+
+ // Update the toolbar status - we don't need to do menus as they
+ // do themselves when we open them.
+ UpdateMailToolbar("tabmail");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="saveTabState">
+ <parameter name="aTabInfo"/>
+ <body>
+ <![CDATA[
+ if (!aTabInfo)
+ return;
+ let saveTabFunc = aTabInfo.mode.saveTabState ||
+ aTabInfo.mode.tabType.saveTabState;
+ if (saveTabFunc)
+ saveTabFunc.call(aTabInfo.mode.tabType, aTabInfo);
+ ]]>
+ </body>
+ </method>
+
+ <method name="saveCurrentTabState">
+ <body>
+ <![CDATA[
+ if (!this.currentTabInfo)
+ this.currentTabInfo = this.tabInfo[0];
+ // save the old tab state before we change the current tab
+ this.saveTabState(this.currentTabInfo);
+ ]]>
+ </body>
+ </method>
+
+ <method name="setTabTitle">
+ <parameter name="aTabInfo"/>
+ <body>
+ <![CDATA[
+ // First find the tab and its index.
+ let tabInfo;
+ let index;
+ if (aTabInfo)
+ {
+ tabInfo = aTabInfo;
+ for (index = 0; index < this.tabInfo.length; ++index)
+ {
+ if (tabInfo == this.tabInfo[index])
+ break;
+ }
+ }
+ else
+ {
+ index = this.tabContainer.selectedIndex;
+ tabInfo = this.tabInfo[index];
+ }
+
+ if (tabInfo)
+ {
+ let tabNode = this.tabContainer.childNodes[index];
+ let titleChangeFunc = tabInfo.mode.onTitleChanged ||
+ tabInfo.mode.tabType.onTitleChanged;
+ if (titleChangeFunc)
+ titleChangeFunc.call(tabInfo.mode.tabType, tabInfo, tabNode);
+ if (this.tabMonitors.length)
+ {
+ for (let tabMonitor of this.tabMonitors)
+ tabMonitor.onTabTitleChanged(tabInfo);
+ }
+ tabNode.setAttribute("label", tabInfo.title);
+
+ // Update the window title if we're the displayed tab.
+ if (index == this.tabContainer.selectedIndex)
+ {
+ let docTitle = tabInfo.title;
+ if (AppConstants.platform != "macosx") {
+ docTitle += " - " + gBrandBundle.getString("brandFullName");
+ }
+ document.title = docTitle;
+
+ // Update the toolbar status - we don't need to do menus as they
+ // do themselves when we open them.
+ UpdateMailToolbar("tabmail");
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="FillTabmailTooltip">
+ <parameter name="aDocument"/>
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ aEvent.stopPropagation();
+ let tn = aDocument.tooltipNode;
+ if (tn.localName != "tab")
+ return false; // Not a tab, so cancel the tooltip.
+ if (tn.hasAttribute("label"))
+ {
+ aEvent.target.setAttribute("label", tn.getAttribute("label"));
+ return true;
+ }
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="onTabContextMenuShowing">
+ <body>
+ <![CDATA[
+ // The user might right-click on a non-tab area of the tab strip.
+ this.mContextTab = document.popupNode;
+ return this.mContextTab.localName == "tab";
+ ]]>
+ </body>
+ </method>
+
+ <!-- getBrowserForSelectedTab is required as some toolkit functions
+ require a getBrowser() function. -->
+ <method name="getBrowserForSelectedTab">
+ <body>
+ <![CDATA[
+ if (!this.currentTabInfo)
+ this.currentTabInfo = this.tabInfo[0];
+ let tabInfo = this.currentTabInfo;
+ let browserFunc = tabInfo.mode.getBrowser ||
+ tabInfo.mode.tabType.getBrowser;
+ if (!browserFunc)
+ return null;
+ return browserFunc.call(tabInfo.mode.tabType, tabInfo);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getTabForContentWindow">
+ <parameter name="aWindow"/>
+ <body>
+ <![CDATA[
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserIndexForDocument">
+ <parameter name="aDocument"/>
+ <body>
+ <![CDATA[
+ return -1;
+ ]]>
+ </body>
+ </method>
+
+ <!-- nsIObserver implementation -->
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body>
+ <![CDATA[
+ const kAutoHide = "mail.tabs.autoHide";
+ if (aTopic == "nsPref:changed" && aData == kAutoHide)
+ this.mAutoHide = Services.prefs.getBoolPref(kAutoHide);
+ ]]>
+ </body>
+ </method>
+
+ <!-- nsIController implementation -->
+
+ <method name="supportsCommand">
+ <parameter name="aCommand"/>
+ <body>
+ <![CDATA[
+ // return early on startup when we haven't got a tab loaded yet
+ let tabInfo = this.currentTabInfo;
+ if (!tabInfo)
+ return false;
+
+ let supportsCommandFunc = tabInfo.mode.supportsCommand ||
+ tabInfo.mode.tabType.supportsCommand;
+ if (!supportsCommandFunc)
+ return false;
+ return supportsCommandFunc.call(tabInfo.mode.tabType,
+ aCommand,
+ tabInfo);
+ ]]>
+ </body>
+ </method>
+
+ <method name="isCommandEnabled">
+ <parameter name="aCommand"/>
+ <body>
+ <![CDATA[
+ // return early on startup when we haven't got a tab loaded yet
+ let tabInfo = this.currentTabInfo;
+ if (!tabInfo)
+ return false;
+
+ let isCommandEnabledFunc = tabInfo.mode.isCommandEnabled ||
+ tabInfo.mode.tabType.isCommandEnabled;
+ if (!isCommandEnabledFunc)
+ return false;
+ return isCommandEnabledFunc.call(tabInfo.mode.tabType,
+ aCommand,
+ tabInfo);
+ ]]>
+ </body>
+ </method>
+
+ <method name="doCommand">
+ <parameter name="aCommand"/>
+ <body>
+ <![CDATA[
+ // return early on startup when we haven't got a tab loaded yet
+ let tabInfo = this.currentTabInfo;
+ if (!tabInfo)
+ return;
+
+ let doCommandFunc = tabInfo.mode.doCommand ||
+ tabInfo.mode.tabType.doCommand;
+ if (!doCommandFunc)
+ return;
+ doCommandFunc.call(tabInfo.mode.tabType,
+ aCommand,
+ tabInfo);
+ ]]>
+ </body>
+ </method>
+
+ <method name="onEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ // return early on startup when we haven't got a tab loaded yet
+ let tabInfo = this.currentTabInfo;
+ if (!tabInfo)
+ return;
+
+ let onEventFunc = tabInfo.mode.onEvent ||
+ tabInfo.mode.tabType.onEvent;
+ if (!onEventFunc)
+ return;
+
+ onEventFunc.call(tabInfo.mode.tabType, aCommand, tabInfo);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="tabmail-tab"
+ display="xul:box"
+ extends="chrome://global/content/bindings/tabbox.xml#tab">
+ <content closetabtext="&tabmailClose.label;">
+ <xul:hbox class="tab-middle box-inherit"
+ xbl:inherits="align,dir,pack,orient,selected"
+ flex="1">
+ <xul:image class="tab-icon tab-icon-image" xbl:inherits="validate,src=image"/>
+ <xul:label class="tab-text"
+ xbl:inherits="value=label,accesskey,crop,disabled"
+ flex="1"/>
+ </xul:hbox>
+ <xul:toolbarbutton anonid="close-button"
+ tooltiptext="&tabmailClose.tooltip;"
+ tabindex="-1"
+ class="tabs-closebutton tab-close-button"/>
+ </content>
+
+ <implementation>
+ <field name="mCorrespondingMenuitem">null</field>
+ </implementation>
+ </binding>
+
+ <binding id="tabmail-arrowscrollbox"
+ extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll">
+ <content>
+ <xul:toolbarbutton class="scrollbutton-up tab-scrollbutton-up"
+ collapsed="true"
+ xbl:inherits="orient"
+ anonid="scrollbutton-up"
+ onmousedown="_startScroll(-1);"
+ onmouseup="_stopScroll();"
+ onmouseout="_stopScroll();"/>
+ <xul:scrollbox xbl:inherits="orient,align,pack,dir"
+ flex="1"
+ anonid="scrollbox">
+ <children/>
+ </xul:scrollbox>
+ <xul:stack align="center" pack="end" class="scrollbutton-down-stack">
+ <xul:hbox flex="1"
+ class="scrollbutton-down-box"
+ collapsed="true"
+ anonid="down-box"/>
+ <xul:hbox flex="1"
+ class="scrollbutton-down-box-animate"
+ collapsed="true"
+ anonid="down-box-animate"/>
+ <xul:toolbarbutton class="scrollbutton-down tab-scrollbutton-down"
+ collapsed="true"
+ xbl:inherits="orient"
+ anonid="scrollbutton-down"
+ onmousedown="_startScroll(1);"
+ onmouseup="_stopScroll();"
+ onmouseout="_stopScroll();"/>
+ </xul:stack>
+ </content>
+
+ <implementation>
+ <field name="_scrollButtonDownBox">
+ document.getAnonymousElementByAttribute(this, "anonid", "down-box");
+ </field>
+ <field name="_scrollButtonDownBoxAnimate">
+ document.getAnonymousElementByAttribute(this, "anonid", "down-box-animate");
+ </field>
+ </implementation>
+
+ <handlers>
+ <handler event="underflow" phase="target">
+ <![CDATA[
+ // Ignore vertical events.
+ if (event.detail == 0)
+ return;
+ this._scrollButtonDownBox.collapsed = true;
+ this._scrollButtonDownBoxAnimate.collapsed = true;
+ ]]>
+ </handler>
+
+ <handler event="overflow" phase="target">
+ <![CDATA[
+ // Ignore vertical events.
+ if (event.detail == 0)
+ return;
+ this._scrollButtonDownBox.collapsed = false;
+ this._scrollButtonDownBoxAnimate.collapsed = false;
+ ]]>
+ </handler>
+
+ <handler event="UpdatedScrollButtonsDisabledState">
+ <![CDATA[
+ // filter underflow events which were dispatched on nested scrollboxes
+ if (event.target != this)
+ return;
+
+ // fix for bug #352353
+ // unlike the scrollup button on the tab strip (which is a
+ // simple toolbarbutton) the scrolldown button is
+ // a more complicated stack of boxes and a toolbarbutton
+ // so that we can animate when a tab is opened offscreen.
+ // in order to style the box with the actual background image
+ // we need to manually set the disable state to match the
+ // disable state of the toolbarbutton.
+ this._scrollButtonDownBox
+ .setAttribute("disabled", this._scrollButtonDown.disabled);
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabmail-tabs"
+ extends="chrome://global/content/bindings/tabbox.xml#tabs">
+ <content>
+ <xul:stack flex="1" class="tabs-stack">
+ <xul:vbox>
+ <xul:spacer flex="1"/>
+ <xul:hbox class="tabs-bottom" align="center"/>
+ </xul:vbox>
+ <xul:stack>
+ <xul:spacer class="tabs-left tabs-right"/>
+ <xul:hbox>
+ <xul:hbox class="tabs-newbutton-box"
+ pack="start"
+ anonid="tabstrip-newbutton">
+ <xul:toolbarbutton class="new-button tabs-newbutton"
+ tooltiptext="&tabmailNewButton.tooltip;"/>
+ </xul:hbox>
+ <xul:arrowscrollbox anonid="arrowscrollbox"
+ class="tabbrowser-arrowscrollbox tabmail-arrowscrollbox"
+ flex="1"
+ xbl:inherits="smoothscroll"
+ orient="horizontal"
+ style="min-width: 1px;">
+ <children includes="tab"/>
+ </xul:arrowscrollbox>
+ <children/>
+ <xul:hbox class="tabs-closebutton-box"
+ align="center"
+ pack="end"
+ anonid="tabstrip-closebutton">
+ <xul:toolbarbutton class="close-button tabs-closebutton"
+ tooltiptext="&tabmailCloseButton.tooltip;"/>
+ </xul:hbox>
+ <xul:stack align="center" pack="end" class="tabs-alltabs-stack">
+ <xul:hbox flex="1" class="tabs-alltabs-box" anonid="alltabs-box"/>
+ <xul:hbox flex="1"
+ class="tabs-alltabs-box-animate"
+ anonid="alltabs-box-animate"/>
+ <xul:toolbarbutton class="tabs-alltabs-button"
+ type="menu"
+ anonid="alltabs-button"
+ tooltipstring="&tabmailAllTabs.tooltip;">
+ <xul:menupopup class="tabs-alltabs-popup"
+ anonid="alltabs-popup"
+ position="after_end"/>
+ </xul:toolbarbutton>
+ </xul:stack>
+ </xul:hbox>
+ </xul:stack>
+ </xul:stack>
+ </content>
+
+ <implementation implements="nsITimerCallback, nsIDOMEventListener, nsIObserver">
+ <constructor>
+ <![CDATA[
+ this.mTabMinWidth = Services.prefs.getIntPref ("browser.tabs.tabMinWidth");
+ this.mTabMaxWidth = Services.prefs.getIntPref ("browser.tabs.tabMaxWidth");
+ this.mTabClipWidth = Services.prefs.getIntPref ("browser.tabs.tabClipWidth");
+ this.mCloseButtons = Services.prefs.getIntPref ("browser.tabs.closeButtons");
+ this.firstChild.minWidth = this.mTabMinWidth;
+ this.firstChild.maxWidth = this.mTabMaxWidth;
+ this._updateCloseButtons();
+ Services.prefs.addObserver("browser.tabs.", this);
+ window.addEventListener("resize", this);
+
+ // Listen to overflow/underflow events on the tabstrip,
+ // we cannot put these as xbl handlers on the entire binding because
+ // they would also get called for the all-tabs popup scrollbox.
+ // Also, we can't rely on event.target because these are all
+ // anonymous nodes.
+ this.arrowScrollbox.addEventListener("overflow", this);
+ this.arrowScrollbox.addEventListener("underflow", this);
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ Services.prefs.removeObserver("browser.tabs.", this);
+
+ // Release timer to avoid reference cycles.
+ if (this._animateTimer)
+ {
+ this._animateTimer.cancel();
+ this._animateTimer = null;
+ }
+ this.arrowScrollbox.removeEventListener("overflow", this);
+ this.arrowScrollbox.removeEventListener("underflow", this);
+ ]]>
+ </destructor>
+
+ <field name="arrowScrollboxWidth">0</field>
+
+ <field name="arrowScrollbox">
+ document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox");
+ </field>
+
+ <field name="arrowScrollboxClosebutton">
+ document.getAnonymousElementByAttribute(this, "anonid", "tabstrip-closebutton");
+ </field>
+
+ <field name="mTabMinWidth">100</field>
+ <field name="mTabMaxWidth">250</field>
+ <field name="mTabClipWidth">140</field>
+ <field name="mCloseButtons">3</field>
+ <method name="_updateCloseButtons">
+ <body>
+ <![CDATA[
+ // modes for tabstrip
+ // 0 - activetab = close button on active tab only
+ // 1 - alltabs = close buttons on all tabs
+ // 2 - noclose = no close buttons at all
+ // 3 - closeatend = close button at the end of the tabstrip
+ switch (this.mCloseButtons)
+ {
+ case 0:
+ this.setAttribute("closebuttons", "activetab");
+ break;
+ case 1:
+ let width = this.firstChild.boxObject.width;
+ // 0 width is an invalid value and indicates
+ // an item without display, so ignore.
+ if (width > this.mTabClipWidth || width == 0)
+ this.setAttribute("closebuttons", "alltabs");
+ else
+ this.setAttribute("closebuttons", "activetab");
+ break;
+ case 2:
+ this.setAttribute("closebuttons", "noclose");
+ break;
+ case 3:
+ this.setAttribute("closebuttons", "closeatend");
+ break;
+ }
+ this.arrowScrollboxClosebutton.collapsed = this.mCloseButtons != 3;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_handleTabSelect">
+ <body>
+ <![CDATA[
+ this.arrowScrollbox.ensureElementIsVisible(this.selectedItem);
+ ]]>
+ </body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ switch (aEvent.type)
+ {
+ case "overflow":
+ this.setAttribute("overflow", "true");
+ this.arrowScrollbox.scrollBoxObject
+ .ensureElementIsVisible(this.selectedItem);
+ break;
+ case "underflow":
+ this.removeAttribute("overflow");
+ break;
+ case "resize":
+ let width = this.arrowScrollbox.boxObject.width;
+ if (width != this.arrowScrollboxWidth)
+ {
+ this._updateCloseButtons();
+ // XXX without this line the tab bar won't budge
+ this.arrowScrollbox.scrollByPixels(1);
+ this._handleTabSelect();
+ this.arrowScrollboxWidth = width;
+ }
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <field name="mAllTabsPopup">
+ document.getAnonymousElementByAttribute(this, "anonid", "alltabs-popup");
+ </field>
+
+ <field name="mAllTabsBoxAnimate">
+ document.getAnonymousElementByAttribute(this, "anonid", "alltabs-box-animate");
+ </field>
+
+ <field name="mDownBoxAnimate">
+ this.arrowScrollbox._scrollButtonDownBoxAnimate;
+ </field>
+
+ <field name="mAllTabsButton">
+ document.getAnonymousElementByAttribute(this, "anonid", "alltabs-button");
+ </field>
+
+ <field name="_animateTimer">null</field>
+ <field name="_animateStep">-1</field>
+ <field name="_animateDelay">25</field>
+ <field name="_animatePercents">
+ [1.00, 0.85, 0.80, 0.75, 0.71, 0.68, 0.65, 0.62, 0.59, 0.57,
+ 0.54, 0.52, 0.50, 0.47, 0.45, 0.44, 0.42, 0.40, 0.38, 0.37,
+ 0.35, 0.34, 0.32, 0.31, 0.30, 0.29, 0.28, 0.27, 0.26, 0.25,
+ 0.24, 0.23, 0.23, 0.22, 0.22, 0.21, 0.21, 0.21, 0.20, 0.20,
+ 0.20, 0.20, 0.20, 0.20, 0.20, 0.20, 0.19, 0.19, 0.19, 0.18,
+ 0.18, 0.17, 0.17, 0.16, 0.15, 0.14, 0.13, 0.11, 0.09, 0.06]
+ </field>
+
+ <method name="_stopAnimation">
+ <body>
+ <![CDATA[
+ if (this._animateStep != -1)
+ {
+ if (this._animateTimer)
+ this._animateTimer.cancel();
+
+ this._animateStep = -1;
+ this.mAllTabsBoxAnimate.style.opacity = 0.0;
+ this.mDownBoxAnimate.style.opacity = 0.0;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_notifyBackgroundTab">
+ <parameter name="aTabNode"/>
+ <body>
+ <![CDATA[
+ let tsbo = this.arrowScrollbox.scrollBoxObject;
+ let tsboStart = tsbo.screenX;
+ let tsboEnd = tsboStart + tsbo.width;
+ let ctbo = aTabNode.boxObject;
+ let ctboStart = ctbo.screenX;
+ let ctboEnd = ctboStart + ctbo.width;
+
+ // only start the flash timer if the new tab (which was loaded in
+ // the background) is not completely visible
+ if (tsboStart > ctboStart || ctboEnd > tsboEnd)
+ {
+ this._animateStep = 0;
+
+ if (!this._animateTimer)
+
+ this._animateTimer =
+ Cc["@mozilla.org/timer;1"]
+ .createInstance(Ci.nsITimer);
+ else
+ this._animateTimer.cancel();
+
+ this._animateTimer.initWithCallback(this,
+ this._animateDelay,
+ Ci.nsITimer.TYPE_REPEATING_SLACK);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="notify">
+ <parameter name="aTimer"/>
+ <body>
+ <![CDATA[
+ if (!document)
+ aTimer.cancel();
+
+ let percent = this._animatePercents[this._animateStep];
+ this.mAllTabsBoxAnimate.style.opacity = percent;
+ this.mDownBoxAnimate.style.opacity = percent;
+
+ if (this._animateStep < (this._animatePercents.length - 1))
+ this._animateStep++;
+ else
+ this._stopAnimation();
+ ]]>
+ </body>
+ </method>
+
+ <!-- nsIObserver implementation -->
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body>
+ <![CDATA[
+ const kCloseButtons = "browser.tabs.closeButtons";
+ if (aTopic == "nsPref:changed" && aData == kCloseButtons)
+ {
+ this.mCloseButtons = Services.prefs.getIntPref(kCloseButtons);
+ this._updateCloseButtons();
+ }
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="TabSelect" action="this._handleTabSelect();"/>
+
+ <handler event="mouseover">
+ <![CDATA[
+ if (event.originalTarget == this.mAllTabsButton)
+ {
+ this.mAllTabsButton
+ .setAttribute("tooltiptext",
+ this.mAllTabsButton.getAttribute("tooltipstring"));
+ }
+ else
+ {
+ this.mAllTabsButton.removeAttribute("tooltiptext");
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <!-- alltabs-popup binding
+ This binding relies on the structure of the tabbrowser binding.
+ Therefore it should only be used as a child of the tabs element.
+ This binding is exposed as a pseudo-public-API so themes can customize
+ the tabbar appearance without having to be scriptable
+ (see globalBindings.xml in osx for example).
+ -->
+ <binding id="tabmail-alltabs-popup"
+ extends="chrome://global/content/bindings/popup.xml#popup">
+ <implementation implements="nsIDOMEventListener">
+ <method name="_tabOnTabClose">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ let menuItem = aEvent.target.mCorrespondingMenuitem;
+ if (menuItem)
+ menuItem.remove();
+ ]]>
+ </body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ switch (aEvent.type)
+ {
+ case "TabClose":
+ this._tabOnTabClose(aEvent);
+ break;
+ case "TabOpen":
+ this._createTabMenuItem(aEvent.originalTarget);
+ break;
+ case "scroll":
+ this._updateTabsVisibilityStatus();
+ break;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_updateTabsVisibilityStatus">
+ <body>
+ <![CDATA[
+ let tabContainer = document.getBindingParent(this);
+ let tabstripBO = tabContainer.arrowScrollbox.scrollBoxObject;
+
+ for (let i = 0; i < this.childNodes.length; i++)
+ {
+ let curTabBO = this.childNodes[i].tab.boxObject;
+ if (curTabBO.screenX >= tabstripBO.screenX &&
+ curTabBO.screenX + curTabBO.width <= tabstripBO.screenX + tabstripBO.width)
+ this.childNodes[i].removeAttribute("tabIsScrolled");
+ else
+ this.childNodes[i].setAttribute("tabIsScrolled", "true");
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_createTabMenuItem">
+ <parameter name="aTabNode"/>
+ <body>
+ <![CDATA[
+ let menuItem = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "menuitem");
+ menuItem.setAttribute("class", "menuitem-iconic alltabs-item icon-holder");
+ menuItem.setAttribute("label", aTabNode.label);
+ menuItem.setAttribute("crop", aTabNode.getAttribute("crop"));
+ menuItem.setAttribute("image", aTabNode.getAttribute("image"));
+
+ let attributes = ["busy", "selected", "type", "NewMessages", "ServerType",
+ "SpecialFolder", "ImapShared", "BiffState", "IsServer",
+ "IsSecure", "Attachment", "IMAPDeleted", "Offline",
+ "MessageType"];
+
+ attributes.forEach(
+ function(attribute)
+ {
+ if (aTabNode.hasAttribute(attribute))
+ {
+ menuItem.setAttribute(attribute, aTabNode.getAttribute(attribute));
+ }
+ }
+ );
+
+ // Keep some attributes of the menuitem in sync with its
+ // corresponding tab (e.g. the tab label)
+ aTabNode.mCorrespondingMenuitem = menuItem;
+ document.addBroadcastListenerFor(aTabNode, menuItem, "label");
+ document.addBroadcastListenerFor(aTabNode, menuItem, "crop");
+ document.addBroadcastListenerFor(aTabNode, menuItem, "image");
+ document.addBroadcastListenerFor(aTabNode, menuItem, "busy");
+ document.addBroadcastListenerFor(aTabNode, menuItem, "selected");
+ document.addBroadcastListenerFor(aTabNode, menuItem, "NewMessages");
+ document.addBroadcastListenerFor(aTabNode, menuItem, "BiffState");
+ aTabNode.addEventListener("TabClose", this);
+ menuItem.tab = aTabNode;
+ menuItem.addEventListener("command", this);
+ this.appendChild(menuItem);
+ return menuItem;
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing">
+ <![CDATA[
+ // set up the menu popup
+ let tabcontainer = document.getBindingParent(this);
+ let tabs = tabcontainer.childNodes;
+
+ // Listen for changes in the tab bar.
+ let tabbrowser = document.getBindingParent(tabcontainer);
+ tabbrowser.addEventListener("TabOpen", this);
+ tabcontainer.arrowScrollbox.addEventListener("scroll", this);
+
+ // if an animation is in progress and the user
+ // clicks on the "all tabs" button, stop the animation
+ tabcontainer._stopAnimation();
+
+ for (let i = 0; i < tabs.length; i++)
+ this._createTabMenuItem(tabs[i]);
+ this._updateTabsVisibilityStatus();
+ ]]>
+ </handler>
+
+ <handler event="popuphiding">
+ <![CDATA[
+ // clear out the menu popup and remove the listeners
+ while (this.hasChildNodes())
+ {
+ let menuItem = this.lastChild;
+ document.removeBroadcastListenerFor(menuItem.tab, menuItem, "label");
+ document.removeBroadcastListenerFor(menuItem.tab, menuItem, "crop");
+ document.removeBroadcastListenerFor(menuItem.tab, menuItem, "image");
+ document.removeBroadcastListenerFor(menuItem.tab, menuItem, "busy");
+ document.removeBroadcastListenerFor(menuItem.tab, menuItem, "selected");
+ document.removeBroadcastListenerFor(menuItem.tab, menuItem, "NewMessages");
+ document.removeBroadcastListenerFor(menuItem.tab, menuItem, "BiffState");
+ menuItem.removeEventListener("command", this);
+ menuItem.tab.removeEventListener("TabClose", this);
+ menuItem.tab.mCorrespondingMenuitem = null;
+ menuItem.remove();
+ }
+ let tabcontainer = document.getBindingParent(this);
+ tabcontainer.arrowScrollbox.removeEventListener("scroll", this);
+ document.getBindingParent(tabcontainer).removeEventListener("TabOpen", this);
+ ]]>
+ </handler>
+
+ <handler event="command">
+ <![CDATA[
+ let tabcontainer = document.getBindingParent(this);
+ tabcontainer.selectedItem = event.target.tab;
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <!-- new-tab-button/close-tab-button binding
+ These bindings rely on the structure of the tabbrowser binding.
+ Therefore they should only be used as a child of the tab or the tabs
+ element (in both cases, when they are anonymous nodes of <tabbrowser>).
+ These bindings are exposed as pseudo-public-APIs, so themes can customize
+ the tabbar appearance without having to be scriptable
+ (see globalBindings.xml in osx for example).
+ -->
+ <binding id="tabmail-new-tab-button"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <handlers>
+ <handler event="command">
+ <![CDATA[
+ let bindingParent = document.getBindingParent(this);
+ if (bindingParent)
+ {
+ let tabmail = document.getBindingParent(bindingParent);
+ if (bindingParent.localName == "tabs")
+ {
+ // new-tab-button only appears in the tabstrip
+ // duplicate the current tab
+ tabmail.openTab("", {});
+ }
+ }
+ ]]>
+ </handler>
+ <handler event="dblclick" button="0" phase="capturing">
+ <![CDATA[
+ event.stopPropagation();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabmail-close-tab-button"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
+ <handlers>
+ <handler event="command">
+ <![CDATA[
+ let bindingParent = document.getBindingParent(this);
+ if (bindingParent)
+ {
+ let tabmail = document.getBindingParent(bindingParent);
+ if (bindingParent.localName == "tab")
+ {
+ /* The only sequence in which a second click event (i.e. dblclik)
+ * can be dispatched on an in-tab close button is when it is shown
+ * after the first click (i.e. the first click event was dispatched
+ * on the tab). This happens when we show the close button only on
+ * the active tab. (bug 352021)
+ * The only sequence in which a third click event can be dispatched
+ * on an in-tab close button is when the tab was opened with a
+ * double click on the tabbar. (bug 378344)
+ * In both cases, it is most likely that the close button area has
+ * been accidentally clicked, therefore we do not close the tab.
+ */
+ if (event.detail > 1)
+ return;
+
+ tabmail.removeTab(bindingParent);
+ tabmail._blockDblClick = true;
+
+ /* XXXmano hack (see bug 343628):
+ * Since we're removing the event target, if the user
+ * double-clicks this button, the dblclick event will be dispatched
+ * with the tabbar as its event target (and explicit/originalTarget),
+ * which treats that as a mouse gesture for opening a new tab.
+ * In this context, we're manually blocking the dblclick event
+ * (see onTabBarDblClick).
+ */
+ let clickedOnce = false;
+ function enableDblClick(event)
+ {
+ var target = event.originalTarget;
+ if (target.className == "tab-close-button")
+ target._ignoredClick = true;
+ if (!clickedOnce)
+ {
+ clickedOnce = true;
+ return;
+ }
+ tabContainer._blockDblClick = false;
+ tabContainer.removeEventListener("click", enableDblClick, true);
+ }
+ tabContainer.addEventListener("click", enableDblClick, true);
+ }
+ else
+ {
+ // "tabs"
+ tabmail.removeCurrentTab();
+ }
+ }
+ ]]>
+ </handler>
+ <handler event="dblclick" button="0" phase="capturing">
+ <![CDATA[
+ // for the one-close-button case
+ event.stopPropagation();
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+</bindings>
diff --git a/comm/suite/mailnews/content/threadPane.js b/comm/suite/mailnews/content/threadPane.js
new file mode 100644
index 0000000000..ac4943d91f
--- /dev/null
+++ b/comm/suite/mailnews/content/threadPane.js
@@ -0,0 +1,598 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var { AppConstants } =
+ ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+var gLastMessageUriToLoad = null;
+var gThreadPaneCommandUpdater = null;
+
+function ThreadPaneOnClick(event)
+{
+ // usually, we're only interested in tree content clicks, not scrollbars etc.
+ let t = event.originalTarget;
+
+ // we may want to open the message in a new tab on middle click
+ if (event.button == kMouseButtonMiddle)
+ {
+ if (t.localName == "treechildren" && AllowOpenTabOnMiddleClick())
+ {
+ // we don't allow new tabs in the search dialog
+ if (document.documentElement.id != "searchMailWindow")
+ {
+ OpenMessageInNewTab(event);
+ RestoreSelectionWithoutContentLoad(GetThreadTree());
+ }
+ return;
+ }
+ }
+
+ // otherwise, we only care about left click events
+ if (event.button != kMouseButtonLeft)
+ return;
+
+ // We are already handling marking as read and flagging in nsMsgDBView.cpp,
+ // so all we need to worry about here is double clicks and column header.
+ // We also get in here for clicks on the "treecol" (headers) and the
+ // "scrollbarbutton" (scrollbar buttons), but we don't want those events to
+ // cause a "double click".
+ if (t.localName == "treecol")
+ {
+ HandleColumnClick(t.id);
+ }
+ else if (t.localName == "treechildren")
+ {
+ let tree = GetThreadTree();
+ // figure out what cell the click was in
+ var cell = tree.treeBoxObject.getCellAt(event.clientX, event.clientY);
+ if (cell.row == -1)
+ return;
+
+ // If the cell is in a "cycler" column or if the user double clicked on the
+ // twisty, don't open the message in a new window.
+ if (event.detail == 2 && !cell.col.cycler && (cell.childElt != "twisty"))
+ {
+ ThreadPaneDoubleClick(event);
+ // Double clicking should not toggle the open/close state of the thread.
+ // This will happen if we don't prevent the event from bubbling to the
+ // default handler in tree.xml.
+ event.stopPropagation();
+ }
+ else if (cell.col.id == "junkStatusCol")
+ {
+ MsgJunkMailInfo(true);
+ }
+ else if (cell.col.id == "threadCol" && !event.shiftKey && (event.ctrlKey || event.metaKey))
+ {
+ gDBView.ExpandAndSelectThreadByIndex(cell.row, true);
+ event.stopPropagation();
+ }
+ }
+}
+
+function nsMsgDBViewCommandUpdater()
+{}
+
+nsMsgDBViewCommandUpdater.prototype =
+{
+ updateCommandStatus : function()
+ {
+ // the back end is smart and is only telling us to update command status
+ // when the # of items in the selection has actually changed.
+ UpdateMailToolbar("dbview driven, thread pane");
+ },
+
+ displayMessageChanged : function(aFolder, aSubject, aKeywords)
+ {
+ if (!gDBView.suppressMsgDisplay)
+ setTitleFromFolder(aFolder, aSubject);
+ ClearPendingReadTimer(); // we are loading / selecting a new message so kill the mark as read timer for the currently viewed message
+ gHaveLoadedMessage = true;
+ goUpdateCommand("button_delete");
+ goUpdateCommand("button_junk");
+ },
+
+ updateNextMessageAfterDelete : function()
+ {
+ SetNextMessageAfterDelete();
+ },
+
+ summarizeSelection: function() {return false},
+
+ QueryInterface : function(iid)
+ {
+ if (iid.equals(Ci.nsIMsgDBViewCommandUpdater) ||
+ iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_NOINTERFACE;
+ }
+}
+
+function HandleColumnClick(columnID)
+{
+ const columnMap = {dateCol: 'byDate',
+ receivedCol: 'byReceived',
+ senderCol: 'byAuthor',
+ recipientCol: 'byRecipient',
+ subjectCol: 'bySubject',
+ locationCol: 'byLocation',
+ accountCol: 'byAccount',
+ unreadButtonColHeader: 'byUnread',
+ statusCol: 'byStatus',
+ sizeCol: 'bySize',
+ priorityCol: 'byPriority',
+ flaggedCol: 'byFlagged',
+ threadCol: 'byThread',
+ tagsCol: 'byTags',
+ junkStatusCol: 'byJunkStatus',
+ idCol: 'byId',
+ attachmentCol: 'byAttachments'};
+
+
+ var sortType;
+ if (columnID in columnMap) {
+ sortType = columnMap[columnID];
+ } else {
+ // If the column isn't in the map, check and see if it's a custom column
+ try {
+ // try to grab the columnHandler (an error is thrown if it does not exist)
+ columnHandler = gDBView.getColumnHandler(columnID);
+
+ // it exists - save this column ID in the customSortCol property of
+ // dbFolderInfo for later use (see nsIMsgDBView.cpp)
+ gDBView.db.dBFolderInfo.setProperty('customSortCol', columnID);
+
+ sortType = "byCustom";
+ } catch(err) {
+ dump("unsupported sort column: " + columnID + " - no custom handler installed. (Error was: " + err + ")\n");
+ return; // bail out
+ }
+ }
+
+ var dbview = GetDBView();
+ var simpleColumns = false;
+ try {
+ simpleColumns = !Services.prefs.getBoolPref("mailnews.thread_pane_column_unthreads");
+ }
+ catch (ex) {
+ }
+ if (sortType == "byThread") {
+ if (simpleColumns)
+ MsgToggleThreaded();
+ else if (dbview.viewFlags & nsMsgViewFlagsType.kThreadedDisplay)
+ MsgReverseSortThreadPane();
+ else
+ MsgSortByThread();
+ }
+ else {
+ if (!simpleColumns && (dbview.viewFlags & nsMsgViewFlagsType.kThreadedDisplay)) {
+ dbview.viewFlags &= ~nsMsgViewFlagsType.kThreadedDisplay;
+ MsgSortThreadPane(sortType);
+ }
+ else if (dbview.sortType == nsMsgViewSortType[sortType]) {
+ MsgReverseSortThreadPane();
+ }
+ else {
+ MsgSortThreadPane(sortType);
+ }
+ }
+}
+
+function ThreadPaneDoubleClick(event) {
+ if (IsSpecialFolderSelected(Ci.nsMsgFolderFlags.Drafts, true))
+ {
+ MsgComposeDraftMessage();
+ }
+ else if (IsSpecialFolderSelected(Ci.nsMsgFolderFlags.Templates, true))
+ {
+ ComposeMsgByType(Ci.nsIMsgCompType.Template, null,
+ Ci.nsIMsgCompFormat.Default);
+ }
+ else if (AllowOpenTabOnDoubleClick() &&
+ document.documentElement.id != "searchMailWindow")
+ { // we don't allow new tabs in the search dialog
+ // open the message in a new tab on double click
+ OpenMessageInNewTab(event);
+ RestoreSelectionWithoutContentLoad(GetThreadTree());
+ }
+ else
+ {
+ MsgOpenSelectedMessages();
+ }
+}
+
+function ThreadPaneKeyPress(event)
+{
+ if (event.keyCode == KeyEvent.DOM_VK_RETURN) {
+ if ((AppConstants.platform == "macosx" ? event.metaKey : event.ctrlKey) &&
+ AllowOpenTabOnMiddleClick()) {
+ OpenMessageInNewTab(event);
+ } else {
+ ThreadPaneDoubleClick(event);
+ }
+ }
+}
+
+function MsgSortByThread()
+{
+ var dbview = GetDBView();
+ dbview.viewFlags |= nsMsgViewFlagsType.kThreadedDisplay;
+ dbview.viewFlags &= ~nsMsgViewFlagsType.kGroupBySort;
+ MsgSortThreadPane('byDate');
+}
+
+function MsgSortThreadPane(sortName)
+{
+ var sortType = nsMsgViewSortType[sortName];
+ var dbview = GetDBView();
+
+ // turn off grouping
+ dbview.viewFlags &= ~nsMsgViewFlagsType.kGroupBySort;
+
+ dbview.sort(sortType, nsMsgViewSortOrder.ascending);
+ UpdateSortIndicators(sortType, nsMsgViewSortOrder.ascending);
+}
+
+function MsgReverseSortThreadPane()
+{
+ var dbview = GetDBView();
+ if (dbview.sortOrder == nsMsgViewSortOrder.ascending) {
+ MsgSortDescending();
+ }
+ else {
+ MsgSortAscending();
+ }
+}
+
+function MsgToggleThreaded()
+{
+ var dbview = GetDBView();
+ var newViewFlags = dbview.viewFlags ^ nsMsgViewFlagsType.kThreadedDisplay;
+ newViewFlags &= ~nsMsgViewFlagsType.kGroupBySort;
+ dbview.viewFlags = newViewFlags;
+
+ dbview.sort(dbview.sortType, dbview.sortOrder);
+ UpdateSortIndicators(dbview.sortType, dbview.sortOrder);
+}
+
+function MsgSortThreaded()
+{
+ var dbview = GetDBView();
+ var viewFlags = dbview.viewFlags;
+ let wasGrouped = viewFlags & nsMsgViewFlagsType.kGroupBySort;
+ dbview.viewFlags &= ~nsMsgViewFlagsType.kGroupBySort;
+ // if we were grouped, and not a saved search, just rebuild the view
+ if (wasGrouped && !(gMsgFolderSelected.flags &
+ Ci.nsMsgFolderFlags.Virtual))
+ SwitchView("cmd_viewAllMsgs");
+ // Toggle if not already threaded.
+ else if ((viewFlags & nsMsgViewFlagsType.kThreadedDisplay) == 0)
+ MsgToggleThreaded();
+}
+
+function MsgGroupBySort()
+{
+ var dbview = GetDBView();
+ var viewFlags = dbview.viewFlags;
+ var sortOrder = dbview.sortOrder;
+ var sortType = dbview.sortType;
+ var count = new Object;
+ var msgFolder = dbview.msgFolder;
+
+ var sortTypeSupportsGrouping = (sortType == nsMsgViewSortType.byAuthor
+ || sortType == nsMsgViewSortType.byDate || sortType == nsMsgViewSortType.byReceived || sortType == nsMsgViewSortType.byPriority
+ || sortType == nsMsgViewSortType.bySubject || sortType == nsMsgViewSortType.byTags
+ || sortType == nsMsgViewSortType.byStatus || sortType == nsMsgViewSortType.byRecipient
+ || sortType == nsMsgViewSortType.byAccount || sortType == nsMsgViewSortType.byFlagged
+ || sortType == nsMsgViewSortType.byAttachments);
+
+ if (!sortTypeSupportsGrouping)
+ return; // we shouldn't be trying to group something we don't support grouping for...
+
+ viewFlags |= nsMsgViewFlagsType.kThreadedDisplay | nsMsgViewFlagsType.kGroupBySort;
+ if (gDBView &&
+ gMsgFolderSelected.flags & Ci.nsMsgFolderFlags.Virtual)
+ {
+ gDBView.viewFlags = viewFlags;
+ UpdateSortIndicators(sortType, nsMsgViewSortOrder.ascending);
+ return;
+ }
+ // null this out, so we don't try sort.
+ if (gDBView) {
+ gDBView.close();
+ gDBView = null;
+ }
+ gDBView = Cc["@mozilla.org/messenger/msgdbview;1?type=group"]
+ .createInstance(Ci.nsIMsgDBView);
+
+ if (!gThreadPaneCommandUpdater)
+ gThreadPaneCommandUpdater = new nsMsgDBViewCommandUpdater();
+
+
+ gDBView.init(messenger, msgWindow, gThreadPaneCommandUpdater);
+ gDBView.open(msgFolder, sortType, sortOrder, viewFlags, count);
+ RerootThreadPane();
+ UpdateSortIndicators(sortType, nsMsgViewSortOrder.ascending);
+ Services.obs.notifyObservers(msgFolder, "MsgCreateDBView",
+ Ci.nsMsgViewType.eShowAllThreads + ":" + viewFlags);
+}
+
+function MsgSortUnthreaded()
+{
+ // Toggle if not already unthreaded.
+ if ((GetDBView().viewFlags & nsMsgViewFlagsType.kThreadedDisplay) != 0)
+ MsgToggleThreaded();
+}
+
+function MsgSortAscending()
+{
+ var dbview = GetDBView();
+ dbview.sort(dbview.sortType, nsMsgViewSortOrder.ascending);
+ UpdateSortIndicators(dbview.sortType, nsMsgViewSortOrder.ascending);
+}
+
+function MsgSortDescending()
+{
+ var dbview = GetDBView();
+ dbview.sort(dbview.sortType, nsMsgViewSortOrder.descending);
+ UpdateSortIndicators(dbview.sortType, nsMsgViewSortOrder.descending);
+}
+
+function groupedBySortUsingDummyRow()
+{
+ return (gDBView.viewFlags & nsMsgViewFlagsType.kGroupBySort) &&
+ (gDBView.sortType != nsMsgViewSortType.bySubject);
+}
+
+function UpdateSortIndicators(sortType, sortOrder)
+{
+ // Remove the sort indicator from all the columns
+ var treeColumns = document.getElementById('threadCols').childNodes;
+ for (var i = 0; i < treeColumns.length; i++)
+ treeColumns[i].removeAttribute('sortDirection');
+
+ // show the twisties if the view is threaded
+ var threadCol = document.getElementById("threadCol");
+ var sortedColumn;
+ // set the sort indicator on the column we are sorted by
+ var colID = ConvertSortTypeToColumnID(sortType);
+ if (colID)
+ sortedColumn = document.getElementById(colID);
+
+ var dbview = GetDBView();
+ var currCol = dbview.viewFlags & nsMsgViewFlagsType.kGroupBySort
+ ? sortedColumn : document.getElementById("subjectCol");
+
+ if (dbview.viewFlags & nsMsgViewFlagsType.kGroupBySort)
+ {
+ var threadTree = document.getElementById("threadTree");
+ var subjectCol = document.getElementById("subjectCol");
+
+ if (groupedBySortUsingDummyRow())
+ {
+ currCol.removeAttribute("primary");
+ subjectCol.setAttribute("primary", "true");
+ }
+
+ // hide the threaded column when in grouped view since you can't do
+ // threads inside of a group.
+ document.getElementById("threadCol").collapsed = true;
+ }
+
+ // clear primary attribute from group column if going to a non-grouped view.
+ if (!(dbview.viewFlags & nsMsgViewFlagsType.kGroupBySort))
+ document.getElementById("threadCol").collapsed = false;
+
+ if ((dbview.viewFlags & nsMsgViewFlagsType.kThreadedDisplay) && !groupedBySortUsingDummyRow()) {
+ threadCol.setAttribute("sortDirection", "ascending");
+ currCol.setAttribute("primary", "true");
+ }
+ else {
+ threadCol.removeAttribute("sortDirection");
+ currCol.removeAttribute("primary");
+ }
+
+ if (sortedColumn) {
+ if (sortOrder == nsMsgViewSortOrder.ascending) {
+ sortedColumn.setAttribute("sortDirection","ascending");
+ }
+ else {
+ sortedColumn.setAttribute("sortDirection","descending");
+ }
+ }
+}
+
+function IsSpecialFolderSelected(flags, checkAncestors)
+{
+ var folder = GetThreadPaneFolder();
+ return folder && folder.isSpecialFolder(flags, checkAncestors);
+}
+
+function GetThreadTree()
+{
+ return document.getElementById("threadTree")
+}
+
+function GetThreadPaneFolder()
+{
+ try {
+ return gDBView.msgFolder;
+ }
+ catch (ex) {
+ return null;
+ }
+}
+
+function EnsureRowInThreadTreeIsVisible(index)
+{
+ if (index < 0)
+ return;
+
+ var tree = GetThreadTree();
+ tree.treeBoxObject.ensureRowIsVisible(index);
+}
+
+function RerootThreadPane()
+{
+ SetNewsFolderColumns();
+
+ var treeView = gDBView.QueryInterface(Ci.nsITreeView);
+ if (treeView)
+ {
+ var tree = GetThreadTree();
+ tree.view = treeView;
+ }
+}
+
+function ThreadPaneOnLoad()
+{
+ var tree = GetThreadTree();
+ // We won't have the tree if we're in a message window, so exit silently
+ if (!tree)
+ return;
+
+ tree.addEventListener("click",ThreadPaneOnClick,true);
+
+ // The mousedown event listener below should only be added in the thread
+ // pane of the mailnews 3pane window, not in the advanced search window.
+ if(tree.parentNode.id == "searchResultListBox")
+ return;
+
+ tree.addEventListener("mousedown",TreeOnMouseDown,true);
+ var delay = Services.prefs.getIntPref("mailnews.threadpane_select_delay");
+ document.getElementById("threadTree")._selectDelay = delay;
+}
+
+function ThreadPaneSelectionChanged()
+{
+ UpdateStatusMessageCounts(gMsgFolderSelected);
+ if (!gRightMouseButtonDown)
+ GetThreadTree().view.selectionChanged();
+}
+
+var ThreadPaneDND = {
+ onDragStart(aEvent) {
+ if (aEvent.originalTarget.localName != "treechildren")
+ return;
+
+ let messageUris = gFolderDisplay.selectedMessageUris;
+ if (!messageUris)
+ return;
+
+ // A message can be dragged from one window and dropped on another window.
+ // Therefore we setNextMessageAfterDelete() here since there is no major
+ // disadvantage, even if it is a copy operation.
+ SetNextMessageAfterDelete();
+ let messengerBundle = document.getElementById("bundle_messenger");
+ let noSubject = messengerBundle.getString("defaultSaveMessageAsFileName");
+ if (noSubject.endsWith(".eml")) {
+ noSubject = noSubject.slice(0, -4);
+ }
+ let fileNames = [];
+ let dataTransfer = aEvent.dataTransfer;
+
+ for (let [index, msgUri] of messageUris.entries()) {
+ let msgService = messenger.messageServiceFromURI(msgUri);
+ let msgHdr = msgService.messageURIToMsgHdr(msgUri);
+ let subject = msgHdr.mime2DecodedSubject || noSubject;
+ if (msgHdr.flags & Ci.nsMsgMessageFlags.HasRe) {
+ subject = "Re: " + subject;
+ }
+ let uniqueFileName = suggestUniqueFileName(subject.substr(0, 120), ".eml",
+ fileNames);
+ fileNames[index] = uniqueFileName;
+ let msgUrl = {};
+ msgService.GetUrlForUri(msgUri, msgUrl, null);
+ dataTransfer.mozSetDataAt("text/x-moz-message", msgUri, index);
+ dataTransfer.mozSetDataAt("text/x-moz-url", msgUrl.value.spec, index);
+ dataTransfer.mozSetDataAt("application/x-moz-file-promise-url",
+ msgUrl.value.spec + "?fileName=" +
+ encodeURIComponent(uniqueFileName),
+ index);
+ dataTransfer.mozSetDataAt("application/x-moz-file-promise",
+ new messageFlavorDataProvider(), index);
+ }
+ dataTransfer.effectAllowed = "copyMove";
+ dataTransfer.addElement(aEvent.originalTarget);
+ },
+
+ onDragOver(aEvent) {
+ if (!gMsgFolderSelected.canFileMessages ||
+ gMsgFolderSelected.server.type == "rss")
+ return;
+ let dt = aEvent.dataTransfer;
+ dt.effectAllowed = "copy";
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ if (Array.from(dt.mozTypesAt(i)).includes("application/x-moz-file")) {
+ let extFile = dt.mozGetDataAt("application/x-moz-file", i);
+ if (!extFile) {
+ return;
+ }
+
+ extFile = extFile.QueryInterface(Ci.nsIFile);
+ if (extFile.isFile() && /\.eml$/i.test(extFile.leafName)) {
+ aEvent.preventDefault();
+ return;
+ }
+ }
+ }
+ },
+
+ onDrop(aEvent) {
+ let dt = aEvent.dataTransfer;
+ for (let i = 0; i < dt.mozItemCount; i++) {
+ let extFile = dt.mozGetDataAt("application/x-moz-file", i);
+ if (!extFile) {
+ continue;
+ }
+
+ extFile = extFile.QueryInterface(Ci.nsIFile);
+ if (extFile.isFile() && /\.eml$/i.test(extFile.leafName))
+ MailServices.copy.CopyFileMessage(extFile, gMsgFolderSelected, null,
+ false, 1, "", null, msgWindow);
+ }
+ },
+}
+
+function messageFlavorDataProvider() {}
+
+messageFlavorDataProvider.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFlavorDataProvider"]),
+
+ getFlavorData(aTransferable, aFlavor, aData, aDataLen) {
+ if (aFlavor !== "application/x-moz-file-promise") {
+ return;
+ }
+ let fileUriPrimitive = {};
+ let dataSize = {};
+ aTransferable.getTransferData("application/x-moz-file-promise-url",
+ fileUriPrimitive, dataSize);
+
+ let fileUriStr = fileUriPrimitive.value
+ .QueryInterface(Ci.nsISupportsString);
+ let fileUri = Services.io.newURI(fileUriStr.data);
+ let fileUrl = fileUri.QueryInterface(Ci.nsIURL);
+ let fileName = fileUrl.fileName;
+
+ let destDirPrimitive = {};
+ aTransferable.getTransferData("application/x-moz-file-promise-dir",
+ destDirPrimitive, dataSize);
+ let destDirectory = destDirPrimitive.value.QueryInterface(Ci.nsIFile);
+ let file = destDirectory.clone();
+ file.append(fileName);
+
+ let messageUriPrimitive = {};
+ aTransferable.getTransferData("text/x-moz-message", messageUriPrimitive,
+ dataSize);
+ let messageUri = messageUriPrimitive.value
+ .QueryInterface(Ci.nsISupportsString);
+
+ messenger.saveAs(messageUri.data, true, null, decodeURIComponent(file.path),
+ true);
+ },
+};
+
+addEventListener("load",ThreadPaneOnLoad,true);
diff --git a/comm/suite/mailnews/content/threadPane.xul b/comm/suite/mailnews/content/threadPane.xul
new file mode 100644
index 0000000000..c012852967
--- /dev/null
+++ b/comm/suite/mailnews/content/threadPane.xul
@@ -0,0 +1,91 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/threadPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/threadPaneExtras.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/threadPaneLabels.css" type="text/css"?>
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/threadpane.dtd">
+
+<overlay
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+<script src="chrome://messenger/content/threadPane.js"/>
+
+<tree id="threadTree"
+ persist="width lastfoldersent"
+ flex="1"
+ enableColumnDrag="true"
+ _selectDelay="250"
+ class="plain focusring"
+ disableKeyNavigation="true"
+ lastfoldersent="false"
+ noattachcol="true"
+ onkeypress="ThreadPaneKeyPress(event);"
+ onselect="ThreadPaneSelectionChanged();">
+ <treecols id="threadCols" pickertooltiptext="&columnChooser2.tooltip;">
+ <treecol id="threadCol" persist="hidden ordinal" fixed="true" cycler="true" class="treecol-image threadColumnHeader" currentView="unthreaded"
+ label="&threadColumn.label;" tooltiptext="&threadColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="attachmentCol" persist="hidden ordinal" fixed="true" class="treecol-image attachmentColumnHeader" hidden="true"
+ label="&attachmentColumn.label;" tooltiptext="&attachmentColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="subjectCol" persist="hidden ordinal width" flex="7" ignoreincolumnpicker="true"
+ label="&subjectColumn.label;" tooltiptext="&subjectColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="junkStatusCol" persist="hidden ordinal width" fixed="true" cycler="true" class="treecol-image junkStatusHeader"
+ label="&junkStatusColumn.label;" tooltiptext="&junkStatusColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="senderCol" persist="ordinal width hidden swappedhidden" flex="4" hidden="false" swappedhidden="true"
+ label="&fromColumn.label;" tooltiptext="&fromColumn2.tooltip;"/>
+ <treecol id="recipientCol" persist="ordinal width hidden swappedhidden" flex="4" hidden="true" swappedhidden="false"
+ label="&recipientColumn.label;" tooltiptext="&recipientColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="unreadButtonColHeader" persist="hidden ordinal" fixed="true" cycler="true" class="treecol-image readColumnHeader"
+ label="&readColumn.label;" tooltiptext="&readColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="receivedCol" persist="hidden ordinal width temphidden" flex="2" hidden="true" temphidden="false"
+ label="&receivedColumn.label;" tooltiptext="&receivedColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="dateCol" persist="hidden ordinal width" flex="2"
+ label="&dateColumn.label;" tooltiptext="&dateColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="statusCol" persist="hidden ordinal width" flex="1" hidden="true"
+ label="&statusColumn.label;" tooltiptext="&statusColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="sizeCol" persist="hidden ordinal width" flex="1" hidden="true"
+ label="&sizeColumn.label;" tooltiptext="&sizeColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="flaggedCol" persist="hidden ordinal" fixed="true" cycler="true" hidden="true" class="treecol-image flagColumnHeader"
+ label="&flagColumn.label;" tooltiptext="&flagColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="tagsCol" persist="hidden ordinal width" flex="1" hidden="true"
+ label="&tagsColumn.label;" tooltiptext="&tagsColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="accountCol" persist="hidden ordinal width" flex="1" hidden="true"
+ label="&accountColumn.label;" tooltiptext="&accountColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="priorityCol" persist="hidden ordinal width" flex="1"
+ label="&priorityColumn.label;" tooltiptext="&priorityColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="unreadCol" persist="hidden ordinal width" flex="1" hidden="true"
+ label="&unreadColumn.label;" tooltiptext="&unreadColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="totalCol" persist="hidden ordinal width" flex="1" hidden="true"
+ label="&totalColumn.label;" tooltiptext="&totalColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="locationCol" persist="width" flex="1" hidden="true" ignoreincolumnpicker="true"
+ label="&locationColumn.label;" tooltiptext="&locationColumn2.tooltip;"/>
+ <splitter class="tree-splitter"/>
+ <treecol id="idCol" persist="hidden ordinal width" flex="1" hidden="true"
+ label="&idColumn.label;" tooltiptext="&idColumn2.tooltip;"/>
+ </treecols>
+ <treechildren ondragstart="ThreadPaneDND.onDragStart(event);"
+ ondragover="ThreadPaneDND.onDragOver(event);"
+ ondrop="ThreadPaneDND.onDrop(event);"/>
+</tree>
+
+</overlay>
diff --git a/comm/suite/mailnews/jar.mn b/comm/suite/mailnews/jar.mn
new file mode 100644
index 0000000000..5529995b02
--- /dev/null
+++ b/comm/suite/mailnews/jar.mn
@@ -0,0 +1,70 @@
+# 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/.
+
+messenger.jar:
+% content messagebody %content/messagebody/ contentaccessible=yes
+% content messenger %content/messenger/
+% override chrome://global/content/nsDragAndDrop.js chrome://messenger/content/nsDragAndDrop.js
+# provide the nsTransferable in nsDragAndDrop.js to extensions that have to
+# work with Geckos from before 1.9, when there was a separate file
+% override chrome://global/content/nsTransferable.js chrome://messenger/content/nsDragAndDrop.js
+% override chrome://messagebody/skin/messageBody.css chrome://messenger/skin/messageBody.css
+% content messenger-region %content/messenger-region/
+% overlay chrome://communicator/content/pref/preferences.xul chrome://messenger/content/mailPrefsOverlay.xul
+% overlay chrome://communicator/content/pref/pref-appearance.xul chrome://messenger/content/mailPrefsOverlay.xul
+% overlay chrome://communicator/content/pref/pref-cookies.xul chrome://messenger/content/mailPrefsOverlay.xul
+% overlay chrome://editor/content/editorTasksOverlay.xul chrome://messenger/content/mailTasksOverlay.xul
+% overlay chrome://messenger/content/addressbook/abSelectAddressesDialog.xul chrome://messenger/content/mailOverlay.xul
+% overlay chrome://editor/content/composerOverlay.xul chrome://messenger/content/mailEditorOverlay.xul
+ content/messenger/browserRequest.js (content/browserRequest.js)
+ content/messenger/browserRequest.xul (content/browserRequest.xul)
+ content/messenger/commandglue.js (content/commandglue.js)
+ content/messenger/folderDisplay.js (content/folderDisplay.js)
+ content/messenger/folderPane.js (content/folderPane.js)
+ content/messenger/folderPane.xul (content/folderPane.xul)
+ content/messenger/mail-offline.js (content/mail-offline.js)
+ content/messenger/mail3PaneWindowCommands.js (content/mail3PaneWindowCommands.js)
+ content/messenger/mailCommands.js (content/mailCommands.js)
+ content/messenger/mailContextMenus.js (content/mailContextMenus.js)
+ content/messenger/mailEditorOverlay.xul (content/mailEditorOverlay.xul)
+* content/messenger/mailKeysOverlay.xul (content/mailKeysOverlay.xul)
+ content/messenger/mailOverlay.js (content/mailOverlay.js)
+* content/messenger/mailOverlay.xul (content/mailOverlay.xul)
+ content/messenger/mailTasksOverlay.js (content/mailTasksOverlay.js)
+ content/messenger/mailTasksOverlay.xul (content/mailTasksOverlay.xul)
+ content/messenger/mailViewList.js (content/mailViewList.js)
+ content/messenger/mailViewList.xul (content/mailViewList.xul)
+ content/messenger/mailViewSetup.js (content/mailViewSetup.js)
+ content/messenger/mailViewSetup.xul (content/mailViewSetup.xul)
+ content/messenger/mailWidgets.xml (content/mailWidgets.xml)
+ content/messenger/mailWindow.js (content/mailWindow.js)
+ content/messenger/mailWindowOverlay.js (content/mailWindowOverlay.js)
+* content/messenger/mailWindowOverlay.xul (content/mailWindowOverlay.xul)
+ content/messenger/messageWindow.js (content/messageWindow.js)
+ content/messenger/messageWindow.xul (content/messageWindow.xul)
+ content/messenger/messenger.css (content/messenger.css)
+ content/messenger/messenger.xul (content/messenger.xul)
+ content/messenger/msgFolderPickerOverlay.js (content/msgFolderPickerOverlay.js)
+ content/messenger/msgHdrViewOverlay.js (content/msgHdrViewOverlay.js)
+ content/messenger/msgHdrViewOverlay.xul (content/msgHdrViewOverlay.xul)
+ content/messenger/msgMail3PaneWindow.js (content/msgMail3PaneWindow.js)
+ content/messenger/msgViewNavigation.js (content/msgViewNavigation.js)
+ content/messenger/msgViewPickerOverlay.js (content/msgViewPickerOverlay.js)
+ content/messenger/nsDragAndDrop.js (content/nsDragAndDrop.js)
+ content/messenger/phishingDetector.js (content/phishingDetector.js)
+ content/messenger/searchBar.js (content/searchBar.js)
+ content/messenger/start.xhtml (content/start.xhtml)
+ content/messenger/tabmail.js (content/tabmail.js)
+ content/messenger/tabmail.xml (content/tabmail.xml)
+ content/messenger/threadPane.js (content/threadPane.js)
+ content/messenger/threadPane.xul (content/threadPane.xul)
+
+ content/messenger/SearchDialog.xul (content/SearchDialog.xul)
+ content/messenger/SearchDialog.js (content/SearchDialog.js)
+ content/messenger/ABSearchDialog.xul (content/ABSearchDialog.xul)
+ content/messenger/ABSearchDialog.js (content/ABSearchDialog.js)
+ content/messenger/FilterListDialog.xul (content/FilterListDialog.xul)
+ content/messenger/FilterListDialog.js (content/FilterListDialog.js)
+
+ content/messenger/searchTermOverlay.xul (content/searchTermOverlay.xul)
diff --git a/comm/suite/mailnews/modules/MailUtils.js b/comm/suite/mailnews/modules/MailUtils.js
new file mode 100644
index 0000000000..3bb844718f
--- /dev/null
+++ b/comm/suite/mailnews/modules/MailUtils.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */
+
+var EXPORTED_SYMBOLS = ["MailUtils"];
+
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * 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()
+ {
+ for (let server of MailServices.accounts.allServers) {
+ // 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 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;
+ },
+
+ /**
+ * Displays this message in a new window.
+ *
+ * @param aMsgHdr the message header to display
+ */
+ displayMessage: function MailUtils_displayMessage(aMsgHdr)
+ {
+ this.openMessageInNewWindow(aMsgHdr);
+ },
+
+ /**
+ * Open a new standalone message window with this header.
+ *
+ * @param aMsgHdr the message header to display
+ */
+ openMessageInNewWindow: function MailUtils_openMessageInNewWindow(aMsgHdr)
+ {
+ // Pass in the message URI as messageWindow.js doesn't handle message headers
+ let messageURI = Cc["@mozilla.org/supports-string;1"]
+ .createInstance(Ci.nsISupportsString);
+ messageURI.data = aMsgHdr.folder.getUriForMsg(aMsgHdr);
+
+ Services.ww.openWindow(null,
+ "chrome://messenger/content/messageWindow.xul",
+ "_blank",
+ "all,chrome,dialog=no,status,toolbar",
+ messageURI);
+ }
+};
diff --git a/comm/suite/mailnews/modules/moz.build b/comm/suite/mailnews/modules/moz.build
new file mode 100644
index 0000000000..4a6802690b
--- /dev/null
+++ b/comm/suite/mailnews/modules/moz.build
@@ -0,0 +1,8 @@
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "MailUtils.js",
+]
diff --git a/comm/suite/mailnews/moz.build b/comm/suite/mailnews/moz.build
new file mode 100644
index 0000000000..00f4213662
--- /dev/null
+++ b/comm/suite/mailnews/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+DIRS += [
+ "components",
+ "modules",
+]