diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/suite/mailnews/content | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/suite/mailnews/content')
49 files changed, 27299 insertions, 0 deletions
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="©Button.label;" + accesskey="©Button.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="©MessageLocation.label;" + accesskey="©MessageLocation.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="©LinkCmd.label;" + accesskey="©LinkCmd.accesskey;" + command="cmd_copyLink"/> + <menuitem id="context-copyimage" + label="©ImageCmd.label;" + accesskey="©ImageCmd.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="©EmailCmd.label;" + accesskey="©EmailCmd.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="©MsgToMenu.label;" + accesskey="©MsgToMenu.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="¬JunkButton.label;" + tooltiptext="¬JunkButton.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="©LinkCmd.label;" + accesskey="©LinkCmd.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' && + '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> |