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 | |
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')
113 files changed, 43101 insertions, 0 deletions
diff --git a/comm/suite/mailnews/components/addrbook/content/abCardOverlay.js b/comm/suite/mailnews/components/addrbook/content/abCardOverlay.js new file mode 100644 index 0000000000..06167ca240 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abCardOverlay.js @@ -0,0 +1,1397 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const kNonVcardFields = + ["NickNameContainer", "SecondaryEmailContainer", "ScreenNameContainer", + "customFields", "preferDisplayName"]; + +const kPhoneticFields = + ["PhoneticLastName", "PhoneticLabel1", "PhoneticSpacer1", + "PhoneticFirstName", "PhoneticLabel2", "PhoneticSpacer2"]; + +// Item is |[dialogField, cardProperty]|. +const kVcardFields = + [ // Contact > Name + ["FirstName", "FirstName"], + ["LastName", "LastName"], + ["DisplayName", "DisplayName"], + ["NickName", "NickName"], + // Contact > Internet + ["PrimaryEmail", "PrimaryEmail"], + ["SecondEmail", "SecondEmail"], + // Contact > Phones + ["WorkPhone", "WorkPhone"], + ["HomePhone", "HomePhone"], + ["FaxNumber", "FaxNumber"], + ["PagerNumber", "PagerNumber"], + ["CellularNumber", "CellularNumber"], + // Address > Home + ["HomeAddress", "HomeAddress"], + ["HomeAddress2", "HomeAddress2"], + ["HomeCity", "HomeCity"], + ["HomeState", "HomeState"], + ["HomeZipCode", "HomeZipCode"], + ["HomeCountry", "HomeCountry"], + ["WebPage2", "WebPage2"], + // Address > Work + ["JobTitle", "JobTitle"], + ["Department", "Department"], + ["Company", "Company"], + ["WorkAddress", "WorkAddress"], + ["WorkAddress2", "WorkAddress2"], + ["WorkCity", "WorkCity"], + ["WorkState", "WorkState"], + ["WorkZipCode", "WorkZipCode"], + ["WorkCountry", "WorkCountry"], + ["WebPage1", "WebPage1"], + // Other > (custom) + ["Custom1", "Custom1"], + ["Custom2", "Custom2"], + ["Custom3", "Custom3"], + ["Custom4", "Custom4"], + // Other > Notes + ["Notes", "Notes"], + // Chat + ["Yahoo", "_Yahoo"], + ["Skype", "_Skype"], + ["QQ", "_QQ"], + ["MSN", "_MSN"], + ["ICQ", "_ICQ"], + ["XMPP", "_JabberId"], + ["IRC", "_IRC"] + ]; + +var gEditCard; +var gOnSaveListeners = []; +var gOnLoadListeners = []; +var gOkCallback = null; +var gHideABPicker = false; +var gPhotoHandlers = {}; +// If any new photos were added to the card, this stores the name of the original +// and any temporary new filenames used to store photos of the card. +// 'null' is a valid value when there was no photo (e.g. the generic photo). +var gOldPhotos = []; +// If a new photo was added, the name is stored here. +var gNewPhoto = null; + +function OnLoadNewCard() +{ + InitEditCard(); + + gEditCard.card = + (("arguments" in window) && (window.arguments.length > 0) && + (window.arguments[0] instanceof Ci.nsIAbCard)) + ? window.arguments[0] + : Cc["@mozilla.org/addressbook/cardproperty;1"] + .createInstance(Ci.nsIAbCard); + gEditCard.titleProperty = "newContactTitle"; + gEditCard.selectedAB = ""; + + if ("arguments" in window && window.arguments[0]) + { + gEditCard.selectedAB = kPersonalAddressbookURI; + + if ("selectedAB" in window.arguments[0] && + (window.arguments[0].selectedAB != kAllDirectoryRoot + "?")) { + // check if selected ab is a mailing list + var abURI = window.arguments[0].selectedAB; + + var directory = GetDirectoryFromURI(abURI); + if (directory.isMailList) { + var parentURI = GetParentDirectoryFromMailingListURI(abURI); + if (parentURI) + gEditCard.selectedAB = parentURI; + } + else if (!directory.readOnly) + gEditCard.selectedAB = window.arguments[0].selectedAB; + } + + // we may have been given properties to pre-initialize the window with.... + // we'll fill these in here... + if ("primaryEmail" in window.arguments[0]) + gEditCard.card.primaryEmail = window.arguments[0].primaryEmail; + if ("displayName" in window.arguments[0]) { + gEditCard.card.displayName = window.arguments[0].displayName; + // if we've got a display name, don't generate + // a display name (and stomp on the existing display name) + // when the user types a first or last name + if (gEditCard.card.displayName) + gEditCard.generateDisplayName = false; + } + if ("okCallback" in window.arguments[0]) + gOkCallback = window.arguments[0].okCallback; + + if ("escapedVCardStr" in window.arguments[0]) { + // hide non vcard values + HideNonVcardFields(); + gEditCard.card = Cc["@mozilla.org/addressbook/msgvcardservice;1"] + .getService(Ci.nsIMsgVCardService) + .escapedVCardToAbCard(window.arguments[0].escapedVCardStr); + } + + if ("titleProperty" in window.arguments[0]) + gEditCard.titleProperty = window.arguments[0].titleProperty; + + if ("hideABPicker" in window.arguments[0]) + gHideABPicker = window.arguments[0].hideABPicker; + } + + // set popup with address book names + var abPopup = document.getElementById('abPopup'); + abPopup.value = gEditCard.selectedAB || kPersonalAddressbookURI; + + if (gHideABPicker && abPopup) { + abPopup.hidden = true; + document.getElementById("abPopupLabel").hidden = true; + } + + SetCardDialogTitle(gEditCard.card.displayName); + + GetCardValues(gEditCard.card, document); + + // FIX ME - looks like we need to focus on both the text field and the tab widget + // probably need to do the same in the addressing widget + + // focus on first or last name based on the pref + var focus = document.getElementById(gEditCard.displayLastNameFirst + ? "LastName" : "FirstName"); + if (focus) { + // XXX Using the setTimeout hack until bug 103197 is fixed + setTimeout( function(firstTextBox) { firstTextBox.focus(); }, 0, focus ); + } +} + +/** + * Get the source directory containing the card we are editing. + */ +function getContainingDirectory() { + let directory = GetDirectoryFromURI(gEditCard.abURI); + // If the source directory is "All Address Books", find the parent + // address book of the card being edited and reflect the changes in it. + if (directory.URI == kAllDirectoryRoot + "?") { + let dirId = + gEditCard.card.directoryId + .substring(0, gEditCard.card.directoryId.indexOf("&")); + directory = MailServices.ab.getDirectoryFromId(dirId); + } + return directory; +} + +function EditCardOKButton() +{ + if (!CheckCardRequiredDataPresence(document)) + return false; // don't close window + + // See if this card is in any mailing list + // if so then we need to update the addresslists of those mailing lists + let directory = getContainingDirectory(); + + // if the directory is a mailing list we need to search all the mailing lists + // in the parent directory if the card exists. + if (directory.isMailList) { + var parentURI = GetParentDirectoryFromMailingListURI(gEditCard.abURI); + directory = GetDirectoryFromURI(parentURI); + } + + var listDirectoriesCount = directory.addressLists.length; + var foundDirectories = []; + + // create a list of mailing lists and the index where the card is at. + for (let i = 0; i < listDirectoriesCount; i++) + { + var subdirectory = directory.addressLists.queryElementAt(i, Ci.nsIAbDirectory); + if (subdirectory.isMailList) + { + // See if any card in this list is the one we edited. + // Must compare card contents using .equals() instead of .indexOf() + // because gEditCard is not really a member of the .addressLists array. + let listCardsCount = subdirectory.addressLists.length; + for (let index = 0; index < listCardsCount; index++) + { + let card = subdirectory.addressLists.queryElementAt(index, Ci.nsIAbCard); + if (card.equals(gEditCard.card)) + foundDirectories.push({directory:subdirectory, cardIndex:index}); + } + } + } + + CheckAndSetCardValues(gEditCard.card, document, false); + + directory.modifyCard(gEditCard.card); + + while (foundDirectories.length) + { + // Update the addressLists item for this card + let foundItem = foundDirectories.pop(); + foundItem.directory.addressLists.replaceElementAt(gEditCard.card, foundItem.cardIndex); + } + + NotifySaveListeners(directory); + + // callback to allow caller to update + if (gOkCallback) + gOkCallback(); + + return true; // close the window +} + +function EditCardCancelButton() +{ + // If a new photo was created, remove it now as it won't be used. + purgeOldPhotos(false); +} + +function OnLoadEditCard() +{ + InitEditCard(); + + gEditCard.titleProperty = "editContactTitle"; + + if (window.arguments && window.arguments[0]) + { + if ( window.arguments[0].card ) + gEditCard.card = window.arguments[0].card; + if ( window.arguments[0].okCallback ) + gOkCallback = window.arguments[0].okCallback; + if ( window.arguments[0].abURI ) + gEditCard.abURI = window.arguments[0].abURI; + } + + // set global state variables + // if first or last name entered, disable generateDisplayName + if (gEditCard.generateDisplayName && + (gEditCard.card.firstName.length + + gEditCard.card.lastName.length + + gEditCard.card.displayName.length > 0)) + { + gEditCard.generateDisplayName = false; + } + + GetCardValues(gEditCard.card, document); + + SetCardDialogTitle(gEditCard.card.displayName); + + // check if selectedAB is a writeable + // if not disable all the fields + if ("arguments" in window && window.arguments[0]) + { + if ("abURI" in window.arguments[0]) { + var abURI = window.arguments[0].abURI; + var directory = GetDirectoryFromURI(abURI); + + if (directory.readOnly) + { + // Set all the editable vcard fields to read only + for (var i = kVcardFields.length; i-- > 0; ) + document.getElementById(kVcardFields[i][0]).readOnly = true; + + // the birthday fields + document.getElementById("Birthday").readOnly = true; + document.getElementById("BirthYear").readOnly = true; + document.getElementById("Age").readOnly = true; + + // the photo field and buttons + document.getElementById("PhotoType").disabled = true; + document.getElementById("GenericPhotoList").disabled = true; + document.getElementById("PhotoURI").disabled = true; + document.getElementById("PhotoURI").placeholder = ""; + document.getElementById("BrowsePhoto").disabled = true; + document.getElementById("UpdatePhoto").disabled = true; + + // And the phonetic fields + document.getElementById(kPhoneticFields[0]).readOnly = true; + document.getElementById(kPhoneticFields[3]).readOnly = true; + + // Also disable the mail format popup. + document.getElementById("PreferMailFormatPopup").disabled = true; + + // And the "prefer display name" checkbox. + document.getElementById("preferDisplayName").disabled = true; + + document.documentElement.buttons = "accept"; + document.documentElement.removeAttribute("ondialogaccept"); + } + } + } +} + +/* Registers functions that are called when loading the card + * values into the contact editor dialog. This is useful if + * extensions have added extra fields to the nsIAbCard, and + * need to display them in the contact editor. + */ +function RegisterLoadListener(aFunc) +{ + gOnLoadListeners.push(aFunc); +} + +function UnregisterLoadListener(aFunc) +{ + var fIndex = gOnLoadListeners.indexOf(aFunc); + if (fIndex != -1) + gOnLoadListeners.splice(fIndex, 1); +} + +// Notifies load listeners that an nsIAbCard is being loaded. +function NotifyLoadListeners(aCard, aDoc) +{ + if (!gOnLoadListeners.length) + return; + + for (let listener of gOnLoadListeners) + listener(aCard, aDoc); +} + +/* Registers functions that are called when saving the card + * values. This is useful if extensions have added extra + * fields to the user interface, and need to set those values + * in their nsIAbCard. + */ +function RegisterSaveListener(aFunc) +{ + gOnSaveListeners.push(aFunc); +} + +function UnregisterSaveListener(aFunc) +{ + var fIndex = gOnSaveListeners.indexOf(aFunc); + if (fIndex != -1) + gOnSaveListeners.splice(fIndex, 1); +} + +// Notifies save listeners that an nsIAbCard is being saved. +function NotifySaveListeners(directory) +{ + if (!gOnSaveListeners.length) + return; + + for (let listener of gOnSaveListeners) + listener(gEditCard.card, document); + + // the save listeners might have tweaked the card + // in which case we need to commit it. + directory.modifyCard(gEditCard.card); +} + +function InitPhoneticFields() +{ + var showPhoneticFields = + Services.prefs.getComplexValue("mail.addr_book.show_phonetic_fields", + Ci.nsIPrefLocalizedString).data; + + // show phonetic fields if indicated by the pref + if (showPhoneticFields == "true") + { + for (var i = kPhoneticFields.length; i-- > 0; ) + document.getElementById(kPhoneticFields[i]).hidden = false; + } +} + +function InitEditCard() +{ + InitPhoneticFields(); + + InitCommonJS(); + // Create gEditCard object that contains global variables for the current js + // file. + gEditCard = new Object(); + + // get specific prefs that gEditCard will need + try { + var displayLastNameFirst = + Services.prefs.getComplexValue("mail.addr_book.displayName.lastnamefirst", + Ci.nsIPrefLocalizedString).data; + gEditCard.displayLastNameFirst = (displayLastNameFirst == "true"); + gEditCard.generateDisplayName = + Services.prefs.getBoolPref("mail.addr_book.displayName.autoGeneration"); + } + catch (ex) { + dump("ex: failed to get pref" + ex + "\n"); + } +} + +function NewCardOKButton() +{ + if (gOkCallback) + { + if (!CheckAndSetCardValues(gEditCard.card, document, true)) + return false; // don't close window + + gOkCallback(gEditCard.card.translateTo("vcard")); + return true; // close the window + } + + var popup = document.getElementById('abPopup'); + if ( popup ) + { + var uri = popup.value; + + // FIX ME - hack to avoid crashing if no ab selected because of blank option bug from template + // should be able to just remove this if we are not seeing blank lines in the ab popup + if ( !uri ) + return false; // don't close window + // ----- + + if (gEditCard.card) + { + if (!CheckAndSetCardValues(gEditCard.card, document, true)) + return false; // don't close window + + // replace gEditCard.card with the card we added + // so that save listeners can get / set attributes on + // the card that got created. + var directory = GetDirectoryFromURI(uri); + gEditCard.card = directory.addCard(gEditCard.card); + NotifySaveListeners(directory); + } + } + + return true; // close the window +} + +function NewCardCancelButton() +{ + // If a new photo was created, remove it now as it won't be used. + purgeOldPhotos(false); +} + +// Move the data from the cardproperty to the dialog +function GetCardValues(cardproperty, doc) +{ + if (!cardproperty) + return; + + // Pass the nsIAbCard and the Document through the listeners + // to give extensions a chance to populate custom fields. + NotifyLoadListeners(cardproperty, doc); + + for (var i = kVcardFields.length; i-- > 0; ) { + doc.getElementById(kVcardFields[i][0]).value = + cardproperty.getProperty(kVcardFields[i][1], ""); + } + + var birthday = doc.getElementById("Birthday"); + modifyDatepicker(birthday); + + // Get the year first, so that the following month/day + // calculations can take leap years into account. + var year = cardproperty.getProperty("BirthYear", null); + var birthYear = doc.getElementById("BirthYear"); + // set the year in the datepicker to the stored year + // if the year isn't present, default to 2000 (a leap year) + birthday.year = saneBirthYear(year); + birthYear.value = year; + + // get the month of the year (1 - 12) + var month = cardproperty.getProperty("BirthMonth", null); + if (month > 0 && month < 13) + birthday.month = month - 1; + else + birthday.monthField.value = null; + + // get the date of the month (1 - 31) + var date = cardproperty.getProperty("BirthDay", null); + if (date > 0 && date < 32) + birthday.date = date; + else + birthday.dateField.value = null; + + // get the current age + calculateAge(null, birthYear); + // when the birth year changes, update the datepicker's year to the new value + // or to kDefaultYear if the value is null + birthYear.onchange = calculateAge; + birthday.onchange = calculateAge; + var age = doc.getElementById("Age"); + age.onchange = calculateYear; + + var popup = document.getElementById("PreferMailFormatPopup"); + if (popup) + popup.value = cardproperty.getProperty("PreferMailFormat", ""); + + var preferDisplayNameEl = document.getElementById("preferDisplayName"); + if (preferDisplayNameEl) + // getProperty may return a "1" or "0" string, we want a boolean + preferDisplayNameEl.checked = cardproperty.getProperty("PreferDisplayName", true) != false; + + // get phonetic fields if exist + try { + doc.getElementById("PhoneticFirstName").value = cardproperty.getProperty("PhoneticFirstName", ""); + doc.getElementById("PhoneticLastName").value = cardproperty.getProperty("PhoneticLastName", ""); + } + catch (ex) {} + + // Select the type if there is a valid value stored for that type, otherwise + // select the generic photo + loadPhoto(cardproperty); + + updateChatName(); +} + +// when the ab card dialog is being loaded to show a vCard, +// hide the fields which aren't supported +// by vCard so the user does not try to edit them. +function HideNonVcardFields() +{ + document.getElementById("homeTabButton").hidden = true; + document.getElementById("photoTabButton").hidden = true; + var i; + for (i = kNonVcardFields.length; i-- > 0; ) + document.getElementById(kNonVcardFields[i]).collapsed = true; + for (i = kPhoneticFields.length; i-- > 0; ) + document.getElementById(kPhoneticFields[i]).collapsed = true; +} + +// Move the data from the dialog to the cardproperty to be stored in the database +// @Returns false - Some required data are missing (card values were not set); +// true - Card values were set, or there is no card to set values on. +function CheckAndSetCardValues(cardproperty, doc, check) +{ + // If requested, check the required data presence. + if (check && !CheckCardRequiredDataPresence(document)) + return false; + + if (!cardproperty) + return true; + + for (var i = kVcardFields.length; i-- > 0; ) + cardproperty.setProperty(kVcardFields[i][1], + doc.getElementById(kVcardFields[i][0]).value); + + // get the birthday information from the dialog + var birthdayElem = doc.getElementById("Birthday"); + var birthMonth = birthdayElem.monthField.value; + var birthDay = birthdayElem.dateField.value; + var birthYear = doc.getElementById("BirthYear").value; + + // set the birth day, month, and year properties + cardproperty.setProperty("BirthDay", birthDay); + cardproperty.setProperty("BirthMonth", birthMonth); + cardproperty.setProperty("BirthYear", birthYear); + + var popup = document.getElementById("PreferMailFormatPopup"); + if (popup) + cardproperty.setProperty("PreferMailFormat", popup.value); + + var preferDisplayNameEl = document.getElementById("preferDisplayName"); + if (preferDisplayNameEl) + cardproperty.setProperty("PreferDisplayName", preferDisplayNameEl.checked); + + // set phonetic fields if exist + try { + cardproperty.setProperty("PhoneticFirstName", doc.getElementById("PhoneticFirstName").value); + cardproperty.setProperty("PhoneticLastName", doc.getElementById("PhoneticLastName").value); + } + catch (ex) {} + + let photoType = doc.getElementById("PhotoType").value; + if (gPhotoHandlers[photoType]) { + if (!gPhotoHandlers[photoType].onSave(cardproperty, doc)) { + photoType = "generic"; + onSwitchPhotoType("generic"); + gPhotoHandlers[photoType].onSave(cardproperty, doc); + } + } + cardproperty.setProperty("PhotoType", photoType); + purgeOldPhotos(true); + + // Remove obsolete chat names. + try { + cardproperty.setProperty("_GoogleTalk", ""); + } + catch (ex) {} + try { + cardproperty.setProperty("_AimScreenName", ""); + } + catch (ex) {} + + return true; +} + +function CleanUpWebPage(webPage) +{ + // no :// yet so we should add something + if ( webPage.length && webPage.search("://") == -1 ) + { + // check for missing / on http:// + if ( webPage.substr(0, 6) == "http:/" ) + return( "http://" + webPage.substr(6) ); + else + return( "http://" + webPage ); + } + else + return(webPage); +} + +// @Returns false - Some required data are missing; +// true - All required data are present. +function CheckCardRequiredDataPresence(doc) +{ + // Bug 314995 - We require at least one of the following fields to be + // filled in: email address, first name, last name, display name, + // organization (company name). + var primaryEmail = doc.getElementById("PrimaryEmail"); + if (primaryEmail.textLength == 0 && + doc.getElementById("FirstName").textLength == 0 && + doc.getElementById("LastName").textLength == 0 && + doc.getElementById("DisplayName").textLength == 0 && + doc.getElementById("Company").textLength == 0) + { + Services.prompt.alert(window, + gAddressBookBundle.getString("cardRequiredDataMissingTitle"), + gAddressBookBundle.getString("cardRequiredDataMissingMessage")); + + return false; + } + + // Simple checks that the primary email should be of the form |user@host|. + // Note: if the length of the primary email is 0 then we skip the check + // as some other field must have something as per the check above. + if (primaryEmail.textLength != 0 && !/.@./.test(primaryEmail.value)) + { + Services.prompt.alert(window, + gAddressBookBundle.getString("incorrectEmailAddressFormatTitle"), + gAddressBookBundle.getString("incorrectEmailAddressFormatMessage")); + + // Focus the dialog field, to help the user. + document.getElementById("abTabs").selectedIndex = 0; + primaryEmail.focus(); + + return false; + } + + return true; +} + +function GenerateDisplayName() +{ + if (!gEditCard.generateDisplayName) + return; + + var displayName; + + var firstNameValue = document.getElementById("FirstName").value; + var lastNameValue = document.getElementById("LastName").value; + if (lastNameValue && firstNameValue) { + displayName = (gEditCard.displayLastNameFirst) + ? gAddressBookBundle.getFormattedString("lastFirstFormat", [lastNameValue, firstNameValue]) + : gAddressBookBundle.getFormattedString("firstLastFormat", [firstNameValue, lastNameValue]); + } + else { + // one (or both) of these is empty, so this works. + displayName = firstNameValue + lastNameValue; + } + + document.getElementById("DisplayName").value = displayName; + + SetCardDialogTitle(displayName); +} + +function DisplayNameChanged() +{ + // turn off generateDisplayName if the user changes the display name + gEditCard.generateDisplayName = false; + + SetCardDialogTitle(document.getElementById("DisplayName").value); +} + +function SetCardDialogTitle(displayName) +{ + document.title = displayName + ? gAddressBookBundle.getFormattedString(gEditCard.titleProperty + "WithDisplayName", [displayName]) + : gAddressBookBundle.getString(gEditCard.titleProperty); +} + +/** + * Calculates the duration of time between an event and now and updates the year + * of whichever element did not call this function. + * @param aEvent The event calling this method. + * @param aElement Optional, but required if this function is not called from an + * element's event listener. The element that would call this. + */ +function calculateAge(aEvent, aElement) { + var datepicker, yearElem, ageElem; + if (aEvent) + aElement = this; + if (aElement.id == "BirthYear" || aElement.id == "Birthday") { + datepicker = document.getElementById("Birthday"); + yearElem = document.getElementById("BirthYear"); + ageElem = document.getElementById("Age"); + } + if (!datepicker || !yearElem || !ageElem) + return; + + // if the datepicker was updated, update the year element + if (aElement == datepicker && !(datepicker.year == kDefaultYear && !yearElem.value)) + yearElem.value = datepicker.year; + var year = yearElem.value; + // if the year element's value is invalid set the year and age elements to null + if (isNaN(year) || year < kMinYear || year > kMaxYear) { + yearElem.value = null; + ageElem.value = null; + datepicker.year = kDefaultYear; + return; + } + else if (aElement == yearElem) + datepicker.year = year; + // calculate the length of time between the event and now + try { + var event = new Date(datepicker.year, datepicker.month, datepicker.date); + // if the year is only 2 digits, then the year won't be set correctly + // using setFullYear fixes this issue + event.setFullYear(datepicker.year); + // get the difference between today and the event + var age = new Date(new Date() - event); + // get the number of years of the difference and subtract 1970 (epoch) + ageElem.value = age.getFullYear() - 1970; + } + catch(e) { + datepicker.year = kDefaultYear; + // if there was an error (like invalid year) set the year and age to null + yearElem.value = null; + ageElem.value = null; + } +} + +/** + * Calculates the year an event ocurred based on the number of years, months, + * and days since the event and updates the relevant element. + * @param aEvent The event calling this method. + * @param aElement Optional, but required if this function is not called from an + * element's event listener. The element that would call this. + */ +function calculateYear(aEvent, aElement) { + var yearElem, datepicker; + if (aEvent) + aElement = this; + if (aElement.id == "Age") { + datepicker = document.getElementById("Birthday"); + yearElem = document.getElementById("BirthYear"); + } + if (!datepicker || !yearElem) + return; + + // if the age is null, remove the year from the year element, and set the + // datepicker to the default year + if (!aElement.value) { + datepicker.year = kDefaultYear; + yearElem.value = null; + return; + } + var today = new Date(); + try { + var date = new Date(aElement.value, datepicker.month, datepicker.date); + date.setFullYear(aElement.value); + // get the difference between today and the age (the year is offset by 1970) + var difference = new Date(today - date); + datepicker.year = yearElem.value = difference.getFullYear() - 1970; + } + // the above code may throw an invalid year exception. If that happens, set + // the year to kDefaultYear and set the year element's value to 0 + catch (e) { + datepicker.year = kDefaultYear; + // if there was an error (like invalid year) set the year and age to null + yearElem.value = null; + let ageElem = document.getElementById("Age"); + if (ageElem) + ageElem.value = null; + } +} + +/** + * Modifies a datepicker in the following ways: + * - Removes the scroll arrows + * - Hides the year + * - Allows the day and month to be blank + * NOTE: + * The datepicker's date, month, year, and dateValue properties are not always + * what appear physically to the user in the datepicker fields. + * If any field is blank, the corresponding property is either the previous + * value if there was one since the card was opened or the relevant portion of + * the current date. + * + * To get the displayed values, get the value of the individual field, such as + * datepicker.yyyyField.value where yyyy is "year", "month", or "date" for the + * year, month, and day, respectively. + * If the value is null, then the field is blank and vice versa. + * @param aDatepicker The datepicker to modify. + */ +function modifyDatepicker(aDatepicker) { + // collapse the year field and separator + aDatepicker.yearField.parentNode.collapsed = true; + if (aDatepicker.yearField == aDatepicker._fieldThree || + aDatepicker.yearField == aDatepicker._fieldTwo) + aDatepicker._separatorSecond.collapsed = true; + else + aDatepicker._separatorFirst.collapsed = true; + // collapse the spinner element + document.getAnonymousElementByAttribute(aDatepicker, "anonid", "buttons") + .collapsed = true; + // this modified constrain value function ignores values less than the minimum + // to let the value be blank (null) + // from: mozilla/toolkit/content/widgets/datetimepicker.xml#759 + aDatepicker._constrainValue = function newConstrainValue(aField, aValue, aNoWrap) { + // if the value is less than one, make the field's value null + if (aValue < 1) { + aField.value = null; + return null; + } + if (aNoWrap && aField == this.monthField) + aValue--; + // make sure the date is valid for the given month + if (aField == this.dateField) { + var currentMonth = this.month; + var dt = new Date(this.year, currentMonth, aValue); + return dt.getMonth() != currentMonth ? 1 : aValue; + } + var min = (aField == this.monthField) ? 0 : 1; + var max = (aField == this.monthField) ? 11 : kMaxYear; + // make sure the value isn't too high + if (aValue > max) + return aNoWrap ? max : min; + return aValue; + } + // sets the specified field to the given value, but allows blank fields + // from: mozilla/toolkit/content/widgets/datetimepicker.xml#698 + aDatepicker._setFieldValue = function setValue(aField, aValue) { + if (aField == this.yearField && aValue >= kMinYear && aValue <= kMaxYear) { + var oldDate = this._dateValue; + this._dateValue.setFullYear(aValue); + if (oldDate != this._dateValue) { + this._dateValue.setDate(0); + this._updateUI(this.dateField, this.date); + } + } + // update the month if the value isn't null + else if (aField == this.monthField && aValue != null) { + var oldDate = this.date; + this._dateValue.setMonth(aValue); + if (oldDate != this.date) + this._dateValue.setDate(0); + this._updateUI(this.dateField, this.date); + var date = this._dateValue.getDate(); + this.dateField.value = date < 10 && this.dateLeadingZero ? "0" + date : date; + var month = this._dateValue.getMonth() + 1; + this.monthField.value = month < 10 && this.monthLeadingZero ? "0" + month : month; + } + // update the date if the value isn't null + else if (aField == this.dateField && aValue != null) { + this._dateValue.setDate(aValue); + this._updateUI(this.dateField, this.date); + var date = this._dateValue.getDate(); + this.dateField.value = date < 10 && this.dateLeadingZero ? "0" + date : date; + var month = this._dateValue.getMonth() + 1; + this.monthField.value = month < 10 && this.monthLeadingZero ? "0" + month : month; + } + this.setAttribute("value", this.value); + + if (this.attachedControl) + this.attachedControl._setValueNoSync(this._dateValue); + // if the aField's value is null or 0, set both field's values to null + if (!aField.value && aField != this.yearField) { + this.dateField.value = null; + this.monthField.value = null; + } + // make the field's value null if aValue is null and the field's value isn't + if (aValue == null && aField.value != null) + aField.value = null; + } +} + +var chatNameFieldIds = + ["Yahoo", "Skype", "QQ", "MSN", "ICQ", "XMPP", "IRC"]; + +/** + * Show the 'Chat' tab and focus the first field that has a value, or + * the first field if none of them has a value. + */ +function showChat() +{ + document.getElementById('abTabPanels').parentNode.selectedTab = + document.getElementById('chatTabButton'); + for (let id of chatNameFieldIds) { + let elt = document.getElementById(id); + if (elt.value) { + elt.focus(); + return; + } + } + document.getElementById(chatNameFieldIds[0]).focus(); +} + +/** + * Fill in the value of the ChatName readonly field with the first + * value of the fields in the Chat tab. + */ +function updateChatName() +{ + let value = ""; + for (let id of chatNameFieldIds) { + let val = document.getElementById(id).value; + if (val) { + value = val; + break; + } + } + document.getElementById("ChatName").value = value; +} + +/** + * Extract the photo information from an nsIAbCard, and populate + * the appropriate input fields in the contact editor. If the + * nsIAbCard returns an unrecognized PhotoType, the generic + * display photo is switched to. + * + * @param aCard The nsIAbCard to extract the information from. + * + */ +function loadPhoto(aCard) { + var type = aCard.getProperty("PhotoType", ""); + + if (!gPhotoHandlers[type] || !gPhotoHandlers[type].onLoad(aCard, document)) { + type = "generic"; + gPhotoHandlers[type].onLoad(aCard, document); + } + + document.getElementById("PhotoType").value = type; + gPhotoHandlers[type].onShow(aCard, document, "photo"); +} + +/** + * Event handler for when the user switches the type of + * photo for the nsIAbCard being edited. Tries to initiate a + * photo download. + * + * @param aPhotoType {string} The type to switch to + * @param aEvent {Event} The event object if used as an event handler + */ +function onSwitchPhotoType(aPhotoType, aEvent) { + if (!gEditCard) + return; + + // Stop event propagation to the radiogroup command event in case that the + // child button is pressed. Otherwise, the download is started twice in a row. + if (aEvent) { + aEvent.stopPropagation(); + } + + if (aPhotoType) { + if (aPhotoType != document.getElementById("PhotoType").value) { + document.getElementById("PhotoType").value = aPhotoType; + } + } else { + aPhotoType = document.getElementById("PhotoType").value; + } + + if (gPhotoHandlers[aPhotoType]) { + if (!gPhotoHandlers[aPhotoType].onRead(gEditCard.card, document)) { + onSwitchPhotoType("generic"); + } + } +} + +/** + * Removes the photo file at the given path, if present. + * + * @param aName The name of the photo to remove from the Photos directory. + * 'null' value is allowed and means to remove no file. + * + * @return {boolean} True if the file was deleted, false otherwise. + */ +function removePhoto(aName) { + if (!aName) + return false; + // Get the directory with all the photos + var file = getPhotosDir(); + // Get the photo (throws an exception for invalid names) + try { + file.append(aName); + file.remove(false); + return true; + } + catch (e) {} + return false; +} + +/** + * Remove previous and temporary photo files from the Photos directory. + * + * @param aSaved {boolean} Whether the new card is going to be saved/committed. + */ +function purgeOldPhotos(aSaved = true) { + // If photo was changed, the array contains at least one member, the original photo. + while (gOldPhotos.length > 0) { + let photoName = gOldPhotos.pop(); + if (!aSaved && (gOldPhotos.length == 0)) { + // If the saving was cancelled, we want to keep the original photo of the card. + break; + } + removePhoto(photoName); + } + + if (aSaved) { + // The new photo should stay so we clear the reference to it. + gNewPhoto = null; + } else { + // Changes to card not saved, we don't need the new photo. + // It may be null when there was no change of it. + removePhoto(gNewPhoto); + } +} + +/** + * Opens a file picker with image filters to look for a contact photo. + * If the user selects a file and clicks OK then the PhotoURI textbox is set + * with a file URI pointing to that file and updatePhoto is called. + * + * @param aEvent {Event} The event object if used as an event handler. + */ +function browsePhoto(aEvent) { + // Stop event propagation to the radiogroup command event in case that the + // child button is pressed. Otherwise, the download is started twice in a row. + if (aEvent) + aEvent.stopPropagation(); + + let fp = Cc["@mozilla.org/filepicker;1"] + .createInstance(Ci.nsIFilePicker); + fp.init(window, gAddressBookBundle.getString("browsePhoto"), + Ci.nsIFilePicker.modeOpen); + + // Open the directory of the currently chosen photo (if any) + let currentPhotoFile = document.getElementById("PhotoFile").file + if (currentPhotoFile) { + fp.displayDirectory = currentPhotoFile.parent; + } + + // Add All Files & Image Files filters and select the latter + fp.appendFilters(Ci.nsIFilePicker.filterImages); + fp.appendFilters(Ci.nsIFilePicker.filterAll); + + fp.open(rv => { + if (rv != Ci.nsIFilePicker.returnOK) { + return; + } + document.getElementById("PhotoFile").file = fp.file; + onSwitchPhotoType("file"); + }); +} + +/** + * Handlers to add drag and drop support. + */ +function checkDropPhoto(aEvent) { + // Just allow anything to be dropped. Different types of data are handled + // in doDropPhoto() below. + aEvent.preventDefault(); +} + +function doDropPhoto(aEvent) { + aEvent.preventDefault(); + + let photoType = ""; + + // Check if a file has been dropped. + let file = aEvent.dataTransfer.mozGetDataAt("application/x-moz-file", 0); + if (file instanceof Ci.nsIFile) { + photoType = "file"; + document.getElementById("PhotoFile").file = file; + } else { + // Check if a URL has been dropped. + let link = aEvent.dataTransfer.getData("URL"); + if (link) { + photoType = "web"; + document.getElementById("PhotoURI").value = link; + } else { + // Check if dropped text is a URL. + link = aEvent.dataTransfer.getData("text/plain"); + if (/^(ftps?|https?):\/\//i.test(link)) { + photoType = "web"; + document.getElementById("PhotoURI").value = link; + } + } + } + + onSwitchPhotoType(photoType); +} + +/** + * Self-contained object to manage the user interface used for downloading + * and storing contact photo images. + */ +var gPhotoDownloadUI = (function() { + // UI DOM elements + let elProgressbar; + let elProgressLabel; + let elPhotoType; + let elProgressContainer; + + window.addEventListener("load", function load(event) { + if (!elProgressbar) + elProgressbar = document.getElementById("PhotoDownloadProgress"); + if (!elProgressLabel) + elProgressLabel = document.getElementById("PhotoStatus"); + if (!elPhotoType) + elPhotoType = document.getElementById("PhotoType"); + if (!elProgressContainer) + elProgressContainer = document.getElementById("ProgressContainer"); + }, false); + + function onStart() { + elProgressContainer.setAttribute("class", "expanded"); + elProgressLabel.value = ""; + elProgressbar.hidden = false; + elProgressbar.value = 3; // Start with a tiny visible progress + } + + function onSuccess() { + elProgressLabel.value = ""; + elProgressContainer.setAttribute("class", ""); + } + + function onError(state) { + let msg; + switch (state) { + case gImageDownloader.ERROR_INVALID_URI: + msg = gAddressBookBundle.getString("errorInvalidUri"); + break; + case gImageDownloader.ERROR_UNAVAILABLE: + msg = gAddressBookBundle.getString("errorNotAvailable"); + break; + case gImageDownloader.ERROR_INVALID_IMG: + msg = gAddressBookBundle.getString("errorInvalidImage"); + break; + case gImageDownloader.ERROR_SAVE: + msg = gAddressBookBundle.getString("errorSaveOperation"); + break; + } + if (msg) { + elProgressLabel.value = msg; + elProgressbar.hidden = true; + onSwitchPhotoType("generic"); + } + } + + function onProgress(state, percent) { + elProgressbar.value = percent; + elProgressLabel.value = gAddressBookBundle.getString("stateImageSave"); + } + + return { + onStart: onStart, + onSuccess: onSuccess, + onError: onError, + onProgress: onProgress + } +})(); + +/* A photo handler defines the behaviour of the contact editor + * for a particular photo type. Each photo handler must implement + * the following interface: + * + * onLoad: function(aCard, aDocument): + * Called when the editor wants to populate the contact editor + * input fields with information about aCard's photo. Note that + * this does NOT make aCard's photo appear in the contact editor - + * this is left to the onShow function. Returns true on success. + * If the function returns false, the generic photo handler onLoad + * function will be called. + * + * onShow: function(aCard, aDocument, aTargetID): + * Called when the editor wants to show this photo type. + * The onShow method should take the input fields in the document, + * and render the requested photo in the IMG tag with id + * aTargetID. Note that onShow does NOT save the photo for aCard - + * this job is left to the onSave function. Returns true on success. + * If the function returns false, the generic photo handler onShow + * function will be called. + * + * onRead: function(aCard, aDocument) + * Called when the editor wants to read the user supplied new photo. + * The onRead method is responsible for analyzing the photo of this + * type requested by the user, and storing it, as well as the + * other fields required by onLoad/onShow to retrieve and display + * the photo again. Returns true on success. If the function + * returns false, the generic photo handler onRead function will + * be called. + * + * onSave: function(aCard, aDocument) + * Called when the editor wants to save this photo type to the card. + * Returns true on success. + */ + +var gGenericPhotoHandler = { + onLoad: function(aCard, aDocument) { + return true; + }, + + onShow: function(aCard, aDocument, aTargetID) { + // XXX TODO: this ignores any other value from the generic photos + // menulist than "default". + aDocument.getElementById(aTargetID) + .setAttribute("src", defaultPhotoURI); + return true; + }, + + onRead: function(aCard, aDocument) { + gPhotoDownloadUI.onSuccess(); + + newPhotoAdded("", aCard); + + gGenericPhotoHandler.onShow(aCard, aDocument, "photo"); + return true; + }, + + onSave: function(aCard, aDocument) { + // XXX TODO: this ignores any other value from the generic photos + // menulist than "default". + + // Update contact + aCard.setProperty("PhotoName", ""); + aCard.setProperty("PhotoURI", ""); + return true; + } +}; + +var gFilePhotoHandler = { + + onLoad: function(aCard, aDocument) { + let photoURI = aCard.getProperty("PhotoURI", ""); + let file; + try { + // The original file may not exist anymore, but we still display it. + file = Services.io.newURI(photoURI) + .QueryInterface(Ci.nsIFileURL) + .file; + } catch (e) {} + + if (!file) + return false; + + aDocument.getElementById("PhotoFile").file = file; + return true; + }, + + onShow: function(aCard, aDocument, aTargetID) { + let photoName = gNewPhoto || aCard.getProperty("PhotoName", null); + let photoURI = getPhotoURI(photoName); + aDocument.getElementById(aTargetID).setAttribute("src", photoURI); + return true; + }, + + onRead: function(aCard, aDocument) { + let file = aDocument.getElementById("PhotoFile").file; + if (!file) + return false; + + // If the local file has been removed/renamed, keep the current photo as is. + if (!file.exists() || !file.isFile()) + return false; + + let photoURI = Services.io.newFileURI(file).spec; + + gPhotoDownloadUI.onStart(); + + let cbSuccess = function(newPhotoName) { + gPhotoDownloadUI.onSuccess(); + + newPhotoAdded(newPhotoName, aCard); + aDocument.getElementById("PhotoFile").setAttribute("PhotoURI", photoURI); + + gFilePhotoHandler.onShow(aCard, aDocument, "photo"); + }; + + gImageDownloader.savePhoto(photoURI, cbSuccess, + gPhotoDownloadUI.onError, + gPhotoDownloadUI.onProgress); + return true; + }, + + onSave: function(aCard, aDocument) { + // Update contact + if (gNewPhoto) { + // The file may not be valid unless the photo has changed. + let photoURI = aDocument.getElementById("PhotoFile").getAttribute("PhotoURI"); + aCard.setProperty("PhotoName", gNewPhoto); + aCard.setProperty("PhotoURI", photoURI); + } + return true; + } +}; + +var gWebPhotoHandler = { + onLoad: function(aCard, aDocument) { + let photoURI = aCard.getProperty("PhotoURI", null); + + if (!photoURI) + return false; + + aDocument.getElementById("PhotoURI").value = photoURI; + return true; + }, + + onShow: function(aCard, aDocument, aTargetID) { + let photoName = gNewPhoto || aCard.getProperty("PhotoName", null); + if (!photoName) + return false; + + let photoURI = getPhotoURI(photoName); + + aDocument.getElementById(aTargetID).setAttribute("src", photoURI); + return true; + }, + + onRead: function(aCard, aDocument) { + let photoURI = aDocument.getElementById("PhotoURI").value; + if (!photoURI) + return false; + + gPhotoDownloadUI.onStart(); + + let cbSuccess = function(newPhotoName) { + gPhotoDownloadUI.onSuccess(); + + newPhotoAdded(newPhotoName, aCard); + + gWebPhotoHandler.onShow(aCard, aDocument, "photo"); + + } + + gImageDownloader.savePhoto(photoURI, cbSuccess, + gPhotoDownloadUI.onError, + gPhotoDownloadUI.onProgress); + return true; + }, + + onSave: function(aCard, aDocument) { + // Update contact + if (gNewPhoto) { + let photoURI = aDocument.getElementById("PhotoURI").value; + aCard.setProperty("PhotoName", gNewPhoto); + aCard.setProperty("PhotoURI", photoURI); + } + return true; + } +}; + +function newPhotoAdded(aPhotoName, aCard) { + // If we had the photo saved locally, shedule it for removal if card is saved. + gOldPhotos.push(gNewPhoto !== null ? gNewPhoto : aCard.getProperty("PhotoName", null)); + gNewPhoto = aPhotoName; +} + +/* In order for other photo handlers to be recognized for + * a particular type, they must be registered through this + * function. + * @param aType the type of photo to handle + * @param aPhotoHandler the photo handler to register + */ +function registerPhotoHandler(aType, aPhotoHandler) +{ + gPhotoHandlers[aType] = aPhotoHandler; +} + +registerPhotoHandler("generic", gGenericPhotoHandler); +registerPhotoHandler("web", gWebPhotoHandler); +registerPhotoHandler("file", gFilePhotoHandler); diff --git a/comm/suite/mailnews/components/addrbook/content/abCardOverlay.xul b/comm/suite/mailnews/components/addrbook/content/abCardOverlay.xul new file mode 100644 index 0000000000..40287e8411 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abCardOverlay.xul @@ -0,0 +1,515 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/addressbook/cardDialog.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/addressbook/abCardOverlay.dtd"> + +<overlay id="editcardOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<stringbundleset id="stringbundleset"> + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> +</stringbundleset> + +<script src="chrome://messenger/content/addressbook/abCommon.js"/> +<script src="chrome://messenger/content/addressbook/abCardOverlay.js"/> + +<vbox id="editcard"> + <tabbox> + <tabs id="abTabs"> + <tab id="contactTabButton" label="&Contact.tab;" + accesskey="&Contact.accesskey;"/> + <tab id="homeTabButton" label="&Home.tab;" accesskey="&Home.accesskey;"/> + <tab id="workTabButton" label="&Work.tab;" accesskey="&Work.accesskey;"/> + <tab id="otherTabButton" label="&Other.tab;" accesskey="&Other.accesskey;"/> + <tab id="chatTabButton" label="&Chat.tab;" accesskey="&Chat.accesskey;"/> + <tab id="photoTabButton" label="&Photo.tab;" accesskey="&Photo.accesskey;"/> + </tabs> + + <tabpanels id="abTabPanels" flex="1"> + <!-- ** Name Tab ** --> + <!-- The following vbox contains two hboxes + top: Name/Email/Phonenumber bottom: Email prefs. --> + <vbox id="abNameTab" > + <!-- This hbox contains two vboxes + left: Name/Email, right: Phonenumbers --> + <hbox> + <vbox id="namesAndEmailAddresses"> + <!-- This box contains the Names and Emailnames --> + + <!-- LOCALIZATION NOTE: + NameField1, NameField2, PhoneticField1, PhoneticField2 + those fields are either LN or FN depends on the target country. + They are configurable in the .dtd file. + --> + + <hbox id="NameField1Container" align="center"> + <spacer flex="1"/> + <label control="&NameField1.id;" value="&NameField1.label;" + accesskey="&NameField1.accesskey;"/> + <hbox class="CardEditWidth" align="center"> + <textbox id="&NameField1.id;" flex="1" + oninput="GenerateDisplayName()"/> + + <!-- LOCALIZATION NOTE: + Fields for phonetic are disabled as default and can be + enabled by^editing "mail.addr_book.show_phonetic_fields" + --> + + <spacer id="PhoneticSpacer1" flex="1" hidden="true"/> + <label id="PhoneticLabel1" control="&PhoneticField1.id;" + value="&PhoneticField1.label;" hidden="true"/> + <textbox id="&PhoneticField1.id;" flex="1" hidden="true"/> + </hbox> + </hbox> + <hbox id="NameField2Container" align="center"> + <spacer flex="1"/> + <label control="&NameField2.id;" value="&NameField2.label;" + accesskey="&NameField2.accesskey;"/> + <hbox class="CardEditWidth" align="center"> + <textbox id="&NameField2.id;" flex="1" + oninput="GenerateDisplayName()"/> + + <!-- LOCALIZATION NOTE: + Fields for phonetic are disabled as default and can be + enabled by editing "mail.addr_book.show_phonetic_fields" + --> + + <spacer id="PhoneticSpacer2" flex="1" hidden="true"/> + <label id="PhoneticLabel2" control="&PhoneticField2.id;" + value="&PhoneticField2.label;" hidden="true"/> + <textbox id="&PhoneticField2.id;" flex="1" hidden="true"/> + </hbox> + </hbox> + <hbox id="DisplayNameContainer" align="center"> + <spacer flex="1"/> + <label control="DisplayName" value="&DisplayName.label;" + accesskey="&DisplayName.accesskey;" /> + <hbox class="CardEditWidth"> + <textbox id="DisplayName" flex="1" + oninput="DisplayNameChanged()"/> + </hbox> + </hbox> + <hbox id="PreferDisplayNameContainer" align="center"> + <spacer flex="1"/> + <hbox class="CardEditWidth"> + <checkbox id="preferDisplayName" + label="&preferDisplayName.label;" + accesskey="&preferDisplayName2.accesskey;"/> + </hbox> + </hbox> + + <hbox id="NickNameContainer" align="center"> + <spacer flex="1"/> + <label control="NickName" value="&NickName.label;" + accesskey="&NickName.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="NickName" flex="1"/> + </hbox> + </hbox> + <hbox id="PrimaryEmailContainer" align="center"> + <spacer flex="1"/> + <label control="PrimaryEmail" value="&PrimaryEmail.label;" + accesskey="&PrimaryEmail.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="PrimaryEmail" flex="1" class="uri-element"/> + </hbox> + </hbox> + <hbox id="SecondaryEmailContainer" align="center"> + <spacer flex="1"/> + <label control="SecondEmail" value="&SecondEmail.label;" + accesskey="&SecondEmail.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="SecondEmail" flex="1" class="uri-element"/> + </hbox> + </hbox> + <hbox id="ScreenNameContainer" align="center"> + <spacer flex="1"/> + <label class="text-link" value="&chatName.label;" + onclick="showChat();"/> + <hbox class="CardEditWidth"> + <textbox id="ChatName" readonly="true" flex="1" + onclick="showChat();"/> + </hbox> + </hbox> + </vbox> <!-- End of Name/Email --> + <!-- Phone Number section --> + <vbox id="PhoneNumbers"> + <hbox id="WorkPhoneContainer" align="center"> + <spacer flex="1"/> + <label control="WorkPhone" value="&WorkPhone.label;" + accesskey="&WorkPhone.accesskey;" /> + <textbox id="WorkPhone" class="PhoneEditWidth"/> + </hbox> + <hbox id="HomePhoneContainer" align="center"> + <spacer flex="1"/> + <label control="HomePhone" value="&HomePhone.label;" + accesskey="&HomePhone.accesskey;"/> + <textbox id="HomePhone" class="PhoneEditWidth"/> + </hbox> + <hbox id="FaxNumberContainer" align="center"> + <spacer flex="1"/> + <label control="FaxNumber" value="&FaxNumber.label;" + accesskey="&FaxNumber.accesskey;"/> + <textbox id="FaxNumber" class="PhoneEditWidth"/> + </hbox> + <hbox id="PagerNumberContainer" align="center"> + <spacer flex="1"/> + <label control="PagerNumber" value="&PagerNumber.label;" + accesskey="&PagerNumber.accesskey;"/> + <textbox id="PagerNumber" class="PhoneEditWidth"/> + </hbox> + <hbox id="CellularNumberContainer" align="center"> + <spacer flex="1"/> + <label control="CellularNumber" value="&CellularNumber.label;" + accesskey="&CellularNumber.accesskey;"/> + <textbox id="CellularNumber" class="PhoneEditWidth"/> + </hbox> + </vbox> <!-- End of Phonenumbers --> + </hbox> <!-- End of Name/Email/Phonenumbers --> + <!-- Email Preferences --> + <hbox> + <vbox valign="middle"> + <label control="PreferMailFormatPopup" + value="&PreferMailFormat.label;" + accesskey="&PreferMailFormat.accesskey;"/> + </vbox> + <menulist id="PreferMailFormatPopup"> + <menupopup> + <!-- 0,1,2 come from nsIAbPreferMailFormat in nsIAbCard.idl --> + <menuitem value="0" label="&Unknown.label;"/> + <menuitem value="1" label="&PlainText.label;"/> + <menuitem value="2" label="&HTML.label;"/> + </menupopup> + </menulist> + </hbox> + </vbox> <!-- End of Name Tab --> + + <!-- ** Home Address Tab ** --> + <vbox id="abHomeTab" > + <hbox align="center"> + <spacer flex="1"/> + <label control="HomeAddress" value="&HomeAddress.label;" + accesskey="&HomeAddress.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="HomeAddress" flex="1"/> + </hbox> + </hbox> + <hbox align="center"> + <spacer flex="1"/> + <label control="HomeAddress2" value="&HomeAddress2.label;" + accesskey="&HomeAddress2.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="HomeAddress2" flex="1"/> + </hbox> + </hbox> + <hbox id="HomeCityContainer" align="center"> + <spacer flex="1"/> + <label control="HomeCity" value="&HomeCity.label;" + accesskey="&HomeCity.accesskey;"/> + <hbox id="HomeCityFieldContainer" + class="AddressCardEditWidth" + align="center"> + <textbox id="HomeCity" flex="1"/> + </hbox> + </hbox> + <hbox align="center"> + <spacer flex="1"/> + <label control="HomeState" value="&HomeState.label;" + accesskey="&HomeState.accesskey;"/> + <hbox align="center" class="AddressCardEditWidth"> + <textbox id="HomeState" flex="1"/> + <spacer class="stateZipSpacer"/> + <label control="HomeZipCode" value="&HomeZipCode.label;" + accesskey="&HomeZipCode.accesskey;"/> + <textbox id="HomeZipCode" class="ZipWidth"/> + </hbox> + </hbox> + <hbox align="center"> + <spacer flex="1"/> + <label control="HomeCountry" value="&HomeCountry.label;" + accesskey="&HomeCountry.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="HomeCountry" flex="1"/> + </hbox> + </hbox> + <hbox id="WebPage2Container" align="center"> + <spacer flex="1"/> + <label control="WebPage2" value="&HomeWebPage.label;" + accesskey="&HomeWebPage.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="WebPage2" flex="1" class="uri-element"/> + </hbox> + </hbox> + <hbox id="birthdayField" align="center"> + <spacer flex="1"/> + <label control="Birthday" value="&Birthday.label;" + accesskey="&Birthday.accesskey;"/> + <hbox class="AddressCardEditWidth" align="center"> + <!-- NOTE: This datepicker is modified. + See abCardOverlay.js for details--> + <datepicker id="Birthday" type="popup"/> + <label value="&In.label;"/> + <textbox id="BirthYear" maxlength="4" + placeholder="&Year.placeholder;" class="YearWidth" /> + <label control="Age" value="&Or.value;"/> + <textbox id="Age" maxlength="4" + placeholder="&Age.placeholder;" class="YearWidth" /> + <label value="&YearsOld.label;"/> + <spacer flex="1"/> + </hbox> + </hbox> + </vbox> + + <!-- ** Business Address Tab ** --> + <vbox id="abBusinessTab" > + <hbox id="JobTitleDepartmentContainer" align="center"> + <spacer flex="1"/> + <label control="JobTitle" value="&JobTitle.label;" + accesskey="&JobTitle.accesskey;"/> + <hbox class="AddressCardEditWidth" align="center"> + <textbox id="JobTitle" flex="1"/> + <label control="Department" value="&Department.label;" + accesskey="&Department.accesskey;"/> + <textbox id="Department" flex="1"/> + </hbox> + </hbox> + <hbox id="CompanyContainer" align="center"> + <spacer flex="1"/> + <label control="Company" value="&Company.label;" + accesskey="&Company.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="Company" flex="1"/> + </hbox> + </hbox> + <hbox id="WorkAddressContainer" align="center"> + <spacer flex="1"/> + <label control="WorkAddress" value="&WorkAddress.label;" + accesskey="&WorkAddress.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="WorkAddress" flex="1"/> + </hbox> + </hbox> + <hbox id="WorkAddress2Container" align="center"> + <spacer flex="1"/> + <label control="WorkAddress2" value="&WorkAddress2.label;" + accesskey="&WorkAddress2.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="WorkAddress2" flex="1"/> + </hbox> + </hbox> + <hbox id="WorkCityContainer" align="center"> + <spacer flex="1"/> + <label control="WorkCity" value="&WorkCity.label;" + accesskey="&WorkCity.accesskey;"/> + <hbox id="WorkCityFieldContainer" + class="AddressCardEditWidth" + align="center"> + <textbox id="WorkCity" flex="1"/> + </hbox> + </hbox> + <hbox id="WorkStateZipContainer" align="center"> + <spacer flex="1"/> + <label control="WorkState" value="&WorkState.label;" + accesskey="&WorkState.accesskey;"/> + <hbox class="AddressCardEditWidth" align="center"> + <textbox id="WorkState" flex="1"/> + <spacer class="stateZipSpacer"/> + <label control="WorkZipCode" value="&WorkZipCode.label;" + accesskey="&WorkZipCode.accesskey;"/> + <textbox id="WorkZipCode" class="ZipWidth"/> + </hbox> + </hbox> + <hbox id="WorkCountryContainer" align="center"> + <spacer flex="1"/> + <label control="WorkCountry" value="&WorkCountry.label;" + accesskey="&WorkCountry.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="WorkCountry" flex="1"/> + </hbox> + </hbox> + <hbox id="WebPage1Container" align="center"> + <spacer flex="1"/> + <label control="WebPage1" value="&WorkWebPage.label;" + accesskey="&WorkWebPage.accesskey;"/> + <hbox class="AddressCardEditWidth"> + <textbox id="WebPage1" flex="1" class="uri-element"/> + </hbox> + </hbox> + </vbox> + + <!-- ** Other Tab ** --> + <vbox id="abOtherTab" > + <vbox id="customFields"> + <hbox flex="1" align="center"> + <label control="Custom1" value="&Custom1.label;" + accesskey="&Custom1.accesskey;"/> + <textbox id="Custom1" flex="1"/> + </hbox> + <hbox flex="1" align="center"> + <label control="Custom2" value="&Custom2.label;" + accesskey="&Custom2.accesskey;"/> + <textbox id="Custom2" flex="1"/> + </hbox> + <hbox flex="1" align="center"> + <label control="Custom3" value="&Custom3.label;" + accesskey="&Custom3.accesskey;"/> + <textbox id="Custom3" flex="1"/> + </hbox> + <hbox flex="1" align="center"> + <label control="Custom4" value="&Custom4.label;" + accesskey="&Custom4.accesskey;"/> + <textbox id="Custom4" flex="1"/> + </hbox> + </vbox> + <label control="Notes" value="&Notes.label;" + accesskey="&Notes.accesskey;"/> + <textbox id="Notes" multiline="true" wrap="virtual" flex="1"/> + </vbox> + + <!-- ** Chat Tab ** --> + <hbox id="abChatTab"> + <vbox> + <hbox id="YahooContainer" align="center"> + <spacer flex="1"/> + <label control="Yahoo" value="&Yahoo.label;" + accesskey="&Yahoo.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="Yahoo" flex="1" onchange="updateChatName();"/> + </hbox> + </hbox> + <hbox id="SkypeContainer" align="center"> + <spacer flex="1"/> + <label control="Skype" value="&Skype.label;" + accesskey="&Skype.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="Skype" flex="1" onchange="updateChatName();"/> + </hbox> + </hbox> + <hbox id="QQContainer" align="center"> + <spacer flex="1"/> + <label control="QQ" value="&QQ.label;" + accesskey="&QQ.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="QQ" flex="1" onchange="updateChatName();"/> + </hbox> + </hbox> + <hbox id="MSNContainer" align="center"> + <spacer flex="1"/> + <label control="MSN" value="&MSN.label;" + accesskey="&MSN.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="MSN" flex="1" onchange="updateChatName();"/> + </hbox> + </hbox> + <hbox id="ICQContainer" align="center"> + <spacer flex="1"/> + <label control="ICQ" value="&ICQ.label;" + accesskey="&ICQ.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="ICQ" flex="1" onchange="updateChatName();"/> + </hbox> + </hbox> + <hbox id="XMPPContainer" align="center"> + <spacer flex="1"/> + <label control="XMPP" value="&XMPP.label;" + accesskey="&XMPP.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="XMPP" flex="1" onchange="updateChatName();"/> + </hbox> + </hbox> + <hbox id="IRCContainer" align="center"> + <spacer flex="1"/> + <label control="IRC" value="&IRC.label;" + accesskey="&IRC.accesskey;"/> + <hbox class="CardEditWidth"> + <textbox id="IRC" flex="1" onchange="updateChatName();"/> + </hbox> + </hbox> + </vbox> + </hbox> + + <!-- ** Photo Tab ** --> + <hbox id="abPhotoTab"> + <vbox align="center" id="PhotoContainer" + style="height: 25ch; width: 25ch;" + ondrop="doDropPhoto(event);" + ondragenter="checkDropPhoto(event);" + ondragover="checkDropPhoto(event);"> + <image id="photo" style="max-height: 25ch; max-width: 25ch;"/> + <hbox id="PhotoDropTarget" flex="1" pack="center" align="center"> + <description>&PhotoDropTarget.label;</description> + </hbox> + </vbox> + + <vbox flex="1"> + <radiogroup id="PhotoType" oncommand="onSwitchPhotoType();"> + <vbox id="GenericPhotoContainer"> + <radio id="GenericPhotoType" + value="generic" + label="&GenericPhoto.label;" + accesskey="&GenericPhoto.accesskey;"/> + <menulist id="GenericPhotoList" + class="indent" + flex="1" + oncommand="onSwitchPhotoType('generic', event);"> + <menupopup> + <menuitem label="&DefaultPhoto.label;" + selected="true" + value="default"/> + </menupopup> + </menulist> + </vbox> + + <vbox id="FilePhotoContainer"> + <radio id="FilePhotoType" + value="file" + label="&PhotoFile.label;" + accesskey="&PhotoFile.accesskey;"/> + <hbox class="indent"> + <filefield id="PhotoFile" + readonly="true" + maxlength="255" + flex="1"/> + <button id="BrowsePhoto" + label="&BrowsePhoto.label;" + accesskey="&BrowsePhoto.accesskey;" + oncommand="browsePhoto(event);"/> + </hbox> + </vbox> + + <vbox id="WebPhotoContainer"> + <radio id="WebPhotoType" + value="web" + label="&PhotoURL.label;" + accesskey="&PhotoURL.accesskey;"/> + <hbox class="indent"> + <textbox id="PhotoURI" + maxlength="255" + flex="1" + placeholder="&PhotoURL.placeholder;"/> + <button id="UpdatePhoto" + label="&UpdatePhoto.label;" + accesskey="&UpdatePhoto.accesskey;" + oncommand="onSwitchPhotoType('web', event);"/> + </hbox> + </vbox> + </radiogroup> + <hbox id="ProgressContainer" align="begin"> + <label id="PhotoStatus"/> + <spacer flex="2"/> + <progressmeter id="PhotoDownloadProgress" + value="0" + mode="determined" + hidden="true" + flex="1"/> + </hbox> + </vbox> + </hbox> + </tabpanels> + </tabbox> +</vbox> +</overlay> diff --git a/comm/suite/mailnews/components/addrbook/content/abCardViewOverlay.js b/comm/suite/mailnews/components/addrbook/content/abCardViewOverlay.js new file mode 100644 index 0000000000..e9edfbfbe4 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abCardViewOverlay.js @@ -0,0 +1,527 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//NOTE: gAddressBookBundle must be defined and set or this Overlay won't work + +var gMapItURLFormat; + +var gPhotoDisplayHandlers = {}; + +var zListName; +var zPrimaryEmail; +var zSecondaryEmail; +var zNickname; +var zDisplayName; +var zWork; +var zHome; +var zFax; +var zCellular; +var zPager; +var zBirthday; +var zCustom1; +var zCustom2; +var zCustom3; +var zCustom4; +var zYahoo; +var zSkype; +var zQQ; +var zMSN; +var zICQ; +var zXMPP; +var zIRC; + +var cvData; + +function OnLoadCardView() +{ + gMapItURLFormat = GetLocalizedStringPref("mail.addr_book.mapit_url.format"); + + zPrimaryEmail = gAddressBookBundle.getString("propertyPrimaryEmail"); + zSecondaryEmail = gAddressBookBundle.getString("propertySecondaryEmail"); + zNickname = gAddressBookBundle.getString("propertyNickname"); + zDisplayName = gAddressBookBundle.getString("propertyDisplayName"); + zListName = gAddressBookBundle.getString("propertyListName"); + zWork = gAddressBookBundle.getString("propertyWork"); + zHome = gAddressBookBundle.getString("propertyHome"); + zFax = gAddressBookBundle.getString("propertyFax"); + zCellular = gAddressBookBundle.getString("propertyCellular"); + zPager = gAddressBookBundle.getString("propertyPager"); + zBirthday = gAddressBookBundle.getString("propertyBirthday"); + zCustom1 = gAddressBookBundle.getString("propertyCustom1"); + zCustom2 = gAddressBookBundle.getString("propertyCustom2"); + zCustom3 = gAddressBookBundle.getString("propertyCustom3"); + zCustom4 = gAddressBookBundle.getString("propertyCustom4"); + zYahoo = gAddressBookBundle.getString("propertyYahoo"); + zSkype = gAddressBookBundle.getString("propertySkype"); + zQQ = gAddressBookBundle.getString("propertyQQ"); + zMSN = gAddressBookBundle.getString("propertyMSN"); + zICQ = gAddressBookBundle.getString("propertyICQ"); + zXMPP = gAddressBookBundle.getString("propertyXMPP"); + zIRC = gAddressBookBundle.getString("propertyIRC"); + + var doc = document; + + /* data for address book, prefixes: "cvb" = card view box + "cvh" = crad view header + "cv" = card view (normal fields) */ + cvData = new Object; + + // Card View Box + cvData.CardViewBox = doc.getElementById("CardViewInnerBox"); + // Title + cvData.CardTitle = doc.getElementById("CardTitle"); + // Name section + cvData.cvbContact = doc.getElementById("cvbContact"); + cvData.cvhContact = doc.getElementById("cvhContact"); + cvData.cvNickname = doc.getElementById("cvNickname"); + cvData.cvDisplayName = doc.getElementById("cvDisplayName"); + cvData.cvEmail1Box = doc.getElementById("cvEmail1Box"); + cvData.cvEmail1 = doc.getElementById("cvEmail1"); + cvData.cvBuddyIcon = doc.getElementById("cvBuddyIcon"); + cvData.cvListNameBox = doc.getElementById("cvListNameBox"); + cvData.cvListName = doc.getElementById("cvListName"); + cvData.cvEmail2Box = doc.getElementById("cvEmail2Box"); + cvData.cvEmail2 = doc.getElementById("cvEmail2"); + // Home section + cvData.cvbHome = doc.getElementById("cvbHome"); + cvData.cvhHome = doc.getElementById("cvhHome"); + cvData.cvHomeAddress = doc.getElementById("cvHomeAddress"); + cvData.cvHomeAddress2 = doc.getElementById("cvHomeAddress2"); + cvData.cvHomeCityStZip = doc.getElementById("cvHomeCityStZip"); + cvData.cvHomeCountry = doc.getElementById("cvHomeCountry"); + cvData.cvbHomeMapItBox = doc.getElementById("cvbHomeMapItBox"); + cvData.cvHomeMapIt = doc.getElementById("cvHomeMapIt"); + cvData.cvHomeWebPageBox = doc.getElementById("cvHomeWebPageBox"); + cvData.cvHomeWebPage = doc.getElementById("cvHomeWebPage"); + // Other section + cvData.cvbOther = doc.getElementById("cvbOther"); + cvData.cvBirthday = doc.getElementById("cvBirthday"); + cvData.cvhOther = doc.getElementById("cvhOther"); + cvData.cvCustom1 = doc.getElementById("cvCustom1"); + cvData.cvCustom2 = doc.getElementById("cvCustom2"); + cvData.cvCustom3 = doc.getElementById("cvCustom3"); + cvData.cvCustom4 = doc.getElementById("cvCustom4"); + cvData.cvNotes = doc.getElementById("cvNotes"); + // Description section (mailing lists only) + cvData.cvbDescription = doc.getElementById("cvbDescription"); + cvData.cvhDescription = doc.getElementById("cvhDescription"); + cvData.cvDescription = doc.getElementById("cvDescription"); + // Addresses section (mailing lists only) + cvData.cvbAddresses = doc.getElementById("cvbAddresses"); + cvData.cvhAddresses = doc.getElementById("cvhAddresses"); + cvData.cvAddresses = doc.getElementById("cvAddresses"); + // Phone section + cvData.cvbPhone = doc.getElementById("cvbPhone"); + cvData.cvhPhone = doc.getElementById("cvhPhone"); + cvData.cvPhWork = doc.getElementById("cvPhWork"); + cvData.cvPhHome = doc.getElementById("cvPhHome"); + cvData.cvPhFax = doc.getElementById("cvPhFax"); + cvData.cvPhCellular = doc.getElementById("cvPhCellular"); + cvData.cvPhPager = doc.getElementById("cvPhPager"); + // Work section + cvData.cvbWork = doc.getElementById("cvbWork"); + cvData.cvhWork = doc.getElementById("cvhWork"); + cvData.cvJobTitle = doc.getElementById("cvJobTitle"); + cvData.cvDepartment = doc.getElementById("cvDepartment"); + cvData.cvCompany = doc.getElementById("cvCompany"); + cvData.cvWorkAddress = doc.getElementById("cvWorkAddress"); + cvData.cvWorkAddress2 = doc.getElementById("cvWorkAddress2"); + cvData.cvWorkCityStZip = doc.getElementById("cvWorkCityStZip"); + cvData.cvWorkCountry = doc.getElementById("cvWorkCountry"); + cvData.cvbWorkMapItBox = doc.getElementById("cvbWorkMapItBox"); + cvData.cvWorkMapIt = doc.getElementById("cvWorkMapIt"); + cvData.cvWorkWebPageBox = doc.getElementById("cvWorkWebPageBox"); + cvData.cvWorkWebPage = doc.getElementById("cvWorkWebPage"); + cvData.cvbPhoto = doc.getElementById("cvbPhoto"); + cvData.cvPhoto = doc.getElementById("cvPhoto"); + // Chat section + cvData.cvbChat = doc.getElementById("cvbChat"); + cvData.cvhChat = doc.getElementById("cvhChat"); + cvData.cvYahoo = doc.getElementById("cvYahoo"); + cvData.cvSkype = doc.getElementById("cvSkype"); + cvData.cvQQ = doc.getElementById("cvQQ"); + cvData.cvMSN = doc.getElementById("cvMSN"); + cvData.cvICQ = doc.getElementById("cvICQ"); + cvData.cvXMPP = doc.getElementById("cvXMPP"); + cvData.cvIRC = doc.getElementById("cvIRC"); +} + +// XXX todo +// some similar code (in spirit) already exists, see OnLoadEditList() +// perhaps we could combine and put in abCommon.js? +function GetAddressesFromURI(uri) +{ + var addresses = ""; + + var editList = GetDirectoryFromURI(uri); + var addressList = editList.addressLists; + if (addressList) { + var total = addressList.length; + if (total > 0) + addresses = addressList.queryElementAt(0, Ci.nsIAbCard).primaryEmail; + for (var i = 1; i < total; i++ ) { + addresses += ", " + addressList.queryElementAt(i, Ci.nsIAbCard).primaryEmail; + } + } + return addresses; +} + +function DisplayCardViewPane(realCard) +{ + var generatedName = realCard.generateName(Services.prefs.getIntPref("mail.addr_book.lastnamefirst")); + + let data = top.cvData; + let visible = false; + + let card = { getProperty : function (prop) { + return realCard.getProperty(prop, ""); + }, + primaryEmail : realCard.primaryEmail, + displayName : realCard.displayName, + isMailList : realCard.isMailList, + mailListURI : realCard.mailListURI + }; + + // Contact photo + displayPhoto(card, cvData.cvPhoto); + + let titleString; + if (generatedName == "") + titleString = card.primaryEmail; // if no generatedName, use email + else + titleString = generatedName; + + // set fields in card view pane + if (card.isMailList) + cvSetNode(data.CardTitle, gAddressBookBundle.getFormattedString("viewListTitle", [generatedName])); + else + cvSetNode(data.CardTitle, titleString); + + // Contact section + cvSetNodeWithLabel(data.cvNickname, zNickname, card.getProperty("NickName")); + + if (card.isMailList) { + // email1 and display name always hidden when a mailing list. + cvSetVisible(data.cvDisplayName, false); + cvSetVisible(data.cvEmail1Box, false); + + visible = HandleLink(data.cvListName, zListName, card.displayName, data.cvListNameBox, "mailto:" + encodeURIComponent(GenerateAddressFromCard(card))); + } + else { + // listname always hidden if not a mailing list + cvSetVisible(data.cvListNameBox, false); + + cvSetNodeWithLabel(data.cvDisplayName, zDisplayName, card.displayName); + + visible = HandleLink(data.cvEmail1, zPrimaryEmail, card.primaryEmail, data.cvEmail1Box, "mailto:" + card.primaryEmail); + } + + visible = HandleLink(data.cvEmail2, zSecondaryEmail, card.getProperty("SecondEmail"), data.cvEmail2Box, "mailto:" + card.getProperty("SecondEmail")) || visible; + + // Home section + visible = cvSetNode(data.cvHomeAddress, card.getProperty("HomeAddress")); + visible = cvSetNode(data.cvHomeAddress2, card.getProperty("HomeAddress2")) || visible; + visible = cvSetCityStateZip(data.cvHomeCityStZip, card.getProperty("HomeCity"), card.getProperty("HomeState"), card.getProperty("HomeZipCode")) || visible; + visible = cvSetNode(data.cvHomeCountry, card.getProperty("HomeCountry")) || visible; + + let mapURLList = data.cvHomeMapIt.firstChild; + if (visible) + mapURLList.initMapAddressFromCard(card, "Home"); + + cvSetVisible(data.cvbHomeMapItBox, visible && !!mapURLList.mapURL); + + visible = HandleLink(data.cvHomeWebPage, "", card.getProperty("WebPage2"), data.cvHomeWebPageBox, card.getProperty("WebPage2")) || visible; + + cvSetVisible(data.cvhHome, visible); + cvSetVisible(data.cvbHome, visible); + if (card.isMailList) { + // Description section + visible = cvSetNode(data.cvDescription, card.getProperty("Notes")) + cvSetVisible(data.cvbDescription, visible); + + // Addresses section + visible = cvAddAddressNodes(data.cvAddresses, card.mailListURI); + cvSetVisible(data.cvbAddresses, visible); + + // Other and Chat sections, not shown for mailing lists. + cvSetVisible(data.cvbOther, false); + cvSetVisible(data.cvbChat, false); + } + else { + // Other section + /// setup the birthday information + let day = card.getProperty("BirthDay", null); + let month = card.getProperty("BirthMonth", null); + let year = card.getProperty("BirthYear", null); + let dateStr; + if (day > 0 && day < 32 && month > 0 && month < 13) { + let date; + let formatter; + if (year) { + // use UTC-based calculations to avoid off-by-one day + // due to time zone/dst discontinuity + date = new Date(Date.UTC(year, month - 1, day)); + date.setUTCFullYear(year); // to handle two-digit years properly + formatter = new Services.intl.DateTimeFormat(undefined, + { dateStyle: "long", timeZone: "UTC" }); + } + // if the year doesn't exist, display Month DD (ex. January 1) + else { + date = new Date(Date.UTC(saneBirthYear(year), month - 1, day)); + formatter = new Services.intl.DateTimeFormat(undefined, + { month: "long", day: "numeric", timeZone: "UTC" }); + } + dateStr = formatter.format(date); + } + else if (year) { + dateStr = year; + } + + visible = cvSetNodeWithLabel(data.cvBirthday, zBirthday, dateStr); + + visible = cvSetNodeWithLabel(data.cvCustom1, zCustom1, card.getProperty("Custom1")) || visible; + visible = cvSetNodeWithLabel(data.cvCustom2, zCustom2, card.getProperty("Custom2")) || visible; + visible = cvSetNodeWithLabel(data.cvCustom3, zCustom3, card.getProperty("Custom3")) || visible; + visible = cvSetNodeWithLabel(data.cvCustom4, zCustom4, card.getProperty("Custom4")) || visible; + visible = cvSetNode(data.cvNotes, card.getProperty("Notes")) || visible; + + cvSetVisible(data.cvhOther, visible); + cvSetVisible(data.cvbOther, visible); + + // Chat section + visible = cvSetNodeWithLabel(data.cvYahoo, zYahoo, + card.getProperty("_Yahoo")); + visible = cvSetNodeWithLabel(data.cvSkype, zSkype, + card.getProperty("_Skype")) || visible; + visible = cvSetNodeWithLabel(data.cvQQ, zQQ, + card.getProperty("_QQ")) || visible; + visible = cvSetNodeWithLabel(data.cvMSN, zMSN, + card.getProperty("_MSN")) || visible; + visible = cvSetNodeWithLabel(data.cvICQ, zICQ, + card.getProperty("_ICQ")) || visible; + visible = cvSetNodeWithLabel(data.cvXMPP, zXMPP, + card.getProperty("_JabberId")) || visible; + visible = cvSetNodeWithLabel(data.cvIRC, zIRC, + card.getProperty("_IRC")) || visible; + cvSetVisible(data.cvhChat, visible); + cvSetVisible(data.cvbChat, visible); + + // hide description section, not show for non-mailing lists + cvSetVisible(data.cvbDescription, false); + + // hide addresses section, not show for non-mailing lists + cvSetVisible(data.cvbAddresses, false); + } + + // Phone section + visible = cvSetNodeWithLabel(data.cvPhWork, zWork, card.getProperty("WorkPhone")); + visible = cvSetNodeWithLabel(data.cvPhHome, zHome, card.getProperty("HomePhone")) || visible; + visible = cvSetNodeWithLabel(data.cvPhFax, zFax, card.getProperty("FaxNumber")) || visible; + visible = cvSetNodeWithLabel(data.cvPhCellular, zCellular, card.getProperty("CellularNumber")) || visible; + visible = cvSetNodeWithLabel(data.cvPhPager, zPager, card.getProperty("PagerNumber")) || visible; + cvSetVisible(data.cvhPhone, visible); + cvSetVisible(data.cvbPhone, visible); + + // Work section + visible = cvSetNode(data.cvJobTitle, card.getProperty("JobTitle")); + visible = cvSetNode(data.cvDepartment, card.getProperty("Department")) || visible; + visible = cvSetNode(data.cvCompany, card.getProperty("Company")) || visible; + + let addressVisible = cvSetNode(data.cvWorkAddress, card.getProperty("WorkAddress")); + addressVisible = cvSetNode(data.cvWorkAddress2, card.getProperty("WorkAddress2")) || addressVisible; + addressVisible = cvSetCityStateZip(data.cvWorkCityStZip, card.getProperty("WorkCity"), card.getProperty("WorkState"), card.getProperty("WorkZipCode")) || addressVisible; + addressVisible = cvSetNode(data.cvWorkCountry, card.getProperty("WorkCountry")) || addressVisible; + + mapURLList = data.cvWorkMapIt.firstChild; + if (addressVisible) + mapURLList.initMapAddressFromCard(card, "Work"); + + cvSetVisible(data.cvbWorkMapItBox, addressVisible && !!mapURLList.mapURL); + + visible = HandleLink(data.cvWorkWebPage, "", card.getProperty("WebPage1"), data.cvWorkWebPageBox, card.getProperty("WebPage1")) || addressVisible || visible; + + cvSetVisible(data.cvhWork, visible); + cvSetVisible(data.cvbWork, visible); + + // make the card view box visible + cvSetVisible(top.cvData.CardViewBox, true); +} + +function ClearCardViewPane() +{ + cvSetVisible(top.cvData.CardViewBox, false); +} + +function cvSetNodeWithLabel(node, label, text) +{ + if (text) { + if (label) + return cvSetNode(node, label + ": " + text); + else + return cvSetNode(node, text); + } + else + return cvSetNode(node, ""); +} + +function cvSetCityStateZip(node, city, state, zip) +{ + let text = ""; + + if (city && state && zip) + text = gAddressBookBundle.getFormattedString("cityAndStateAndZip", + [city, state, zip]); + else if (city && state && !zip) + text = gAddressBookBundle.getFormattedString("cityAndStateNoZip", + [city, state]); + else if (zip && ((!city && state) || (city && !state))) + text = gAddressBookBundle.getFormattedString("cityOrStateAndZip", + [city + state, zip]); + else { + // Only one of the strings is non-empty so contatenating them produces that string. + text = city + state + zip; + } + + return cvSetNode(node, text); +} + +function cvSetNode(node, text) +{ + if (!node) + return false; + + node.textContent = text; + let visible = !!text; + cvSetVisible(node, visible); + + return visible; +} + +function cvAddAddressNodes(node, uri) +{ + var visible = false; + + if (node) { + var editList = GetDirectoryFromURI(uri); + var addressList = editList.addressLists; + + if (addressList) { + var total = addressList.length; + if (total > 0) { + while (node.hasChildNodes()) { + node.lastChild.remove(); + } + for (i = 0; i < total; i++ ) { + var descNode = document.createElement("description"); + var card = addressList.queryElementAt(i, Ci.nsIAbCard); + + descNode.setAttribute("class", "CardViewLink"); + node.appendChild(descNode); + + var linkNode = document.createElementNS("http://www.w3.org/1999/xhtml", "a"); + linkNode.setAttribute("id", "addr#" + i); + linkNode.setAttribute("href", "mailto:" + card.primaryEmail); + descNode.appendChild(linkNode); + + var textNode = document.createTextNode(card.displayName + " <" + card.primaryEmail + ">"); + linkNode.appendChild(textNode); + } + visible = true; + } + } + cvSetVisible(node, visible); + } + return visible; +} + +function cvSetVisible(node, visible) +{ + if ( visible ) + node.removeAttribute("collapsed"); + else + node.setAttribute("collapsed", "true"); +} + +function HandleLink(node, label, value, box, link) +{ + var visible = cvSetNodeWithLabel(node, label, value); + if (visible) + node.setAttribute('href', link); + cvSetVisible(box, visible); + + return visible; +} + +function openLink(aEvent) +{ + openAsExternal(aEvent.target.getAttribute("href")); + // return false, so we don't load the href in the addressbook window + return false; +} + +function openLinkWithUrl(aUrl) +{ + if (aUrl) + openAsExternal(aUrl); + // return false, so we don't load the href in the addressbook window + return false; +} + +/* Display the contact photo from the nsIAbCard in the IMG element. + * If the photo cannot be displayed, show the generic contact + * photo. + */ +function displayPhoto(aCard, aImg) +{ + var type = aCard.getProperty("PhotoType", ""); + if (!gPhotoDisplayHandlers[type] || + !gPhotoDisplayHandlers[type](aCard, aImg)) + gPhotoDisplayHandlers["generic"](aCard, aImg); +} + +/* In order to display the contact photos in the card view, there + * must be a registered photo display handler for the card photo + * type. The generic, file, and web photo types are handled + * by default. + * + * A photo display handler is a function that behaves as follows: + * + * function(aCard, aImg): + * The function is responsible for determining how to retrieve + * the photo from nsIAbCard aCard, and for displaying it in img + * img element aImg. Returns true if successful. If it returns + * false, the generic photo display handler will be called. + * + * The following display handlers are for the generic, file and + * web photo types. + */ + +var gGenericPhotoDisplayHandler = function(aCard, aImg) +{ + aImg.setAttribute("src", defaultPhotoURI); + return true; +}; + +var gPhotoNameDisplayHandler = function(aCard, aImg) +{ + var photoSrc = getPhotoURI(aCard.getProperty("PhotoName")); + aImg.setAttribute("src", photoSrc); + return true; +}; + +/* In order for a photo display handler to be registered for + * a particular photo type, it must be registered here. + */ +function registerPhotoDisplayHandler(aType, aPhotoDisplayHandler) +{ + if (!gPhotoDisplayHandlers[aType]) + gPhotoDisplayHandlers[aType] = aPhotoDisplayHandler; +} + +registerPhotoDisplayHandler("generic", gGenericPhotoDisplayHandler); +// File and Web are treated the same, and therefore use the +// same handler. +registerPhotoDisplayHandler("file", gPhotoNameDisplayHandler); +registerPhotoDisplayHandler("web", gPhotoNameDisplayHandler); diff --git a/comm/suite/mailnews/components/addrbook/content/abCommon.js b/comm/suite/mailnews/components/addrbook/content/abCommon.js new file mode 100644 index 0000000000..a8e9d37c21 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abCommon.js @@ -0,0 +1,1185 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { IOUtils } = ChromeUtils.import("resource:///modules/IOUtils.js"); +const { MailServices } = + ChromeUtils.import("resource:///modules/MailServices.jsm"); +const { FileUtils } = + ChromeUtils.import("resource://gre/modules/FileUtils.jsm"); +const { PrivateBrowsingUtils } = + ChromeUtils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + +var gDirTree = null; +var abList = null; +var gAbResultsTree = null; +var gAbView = null; +var gAddressBookBundle; +// A boolean variable determining whether AB column should be shown in AB +// sidebar in compose window. +var gShowAbColumnInComposeSidebar = false; + +const kDefaultSortColumn = "GeneratedName"; +const kDefaultAscending = "ascending"; +const kDefaultDescending = "descending"; +// kDefaultYear will be used in birthday calculations when no year is given; +// this is a leap year so that Feb 29th works. +const kDefaultYear = nearestLeap(new Date().getFullYear()); +const kMaxYear = 9999; +const kMinYear = 1; +const kAllDirectoryRoot = "moz-abdirectory://"; +const kLdapUrlPrefix = "moz-abldapdirectory://"; +const kPersonalAddressbookURI = "moz-abmdbdirectory://abook.mab"; +const kCollectedAddressbookURI = "moz-abmdbdirectory://history.mab"; +// The default, generic contact image is displayed via CSS when the photoURI is +// blank. +var defaultPhotoURI = ""; + +// Controller object for Dir Pane +var DirPaneController = +{ + supportsCommand: function(command) + { + switch (command) { + case "cmd_selectAll": + case "cmd_delete": + case "button_delete": + case "cmd_properties": + case "cmd_printcard": + case "cmd_printcardpreview": + case "cmd_print": + case "cmd_printpreview": + case "cmd_newlist": + case "cmd_newCard": + return true; + default: + return false; + } + }, + + isCommandEnabled: function(command) + { + switch (command) { + case "cmd_selectAll": + // The gDirTree pane only handles single selection, but normally we + // enable cmd_selectAll as it will get forwarded to the results pane. + // But if there is no gAbView, disable as we can't forward to anywhere. + return (gAbView != null); + case "cmd_delete": + case "button_delete": { + let selectedDir = getSelectedDirectory(); + if (!selectedDir) + return false; + let selectedDirURI = selectedDir.URI; + + // Context-sensitive labels for Edit > Delete menuitem. + if (command == "cmd_delete") { + goSetMenuValue(command, selectedDir.isMailList ? + "valueList" : "valueAddressBook"); + } + + // If it's one of these special ABs, return false to disable deletion. + if (selectedDirURI == kPersonalAddressbookURI || + selectedDirURI == kCollectedAddressbookURI || + selectedDirURI == (kAllDirectoryRoot + "?")) + return false; + + // If the directory is a mailing list, and it is read-only, + // return false to disable deletion. + if (selectedDir.isMailList && selectedDir.readOnly) + return false; + + // If the selected directory is an ldap directory, + // and if the prefs for this directory are locked, + // return false to disable deletion. + if (selectedDirURI.startsWith(kLdapUrlPrefix)) { + let disable = false; + try { + let prefName = selectedDirURI.substr(kLdapUrlPrefix.length); + disable = Services.prefs.getBoolPref(prefName + ".disable_delete"); + } + catch(ex) { + // If this preference is not set, that's ok. + } + if (disable) + return false; + } + + // Else return true to enable deletion (default). + return true; + } + case "cmd_printcard": + case "cmd_printcardpreview": + return (GetSelectedCardIndex() != -1); + case "cmd_print": + case "cmd_printpreview": + document.querySelectorAll("[command=cmd_print]").forEach(e => { + e.disabled = false; + }); + return true; + case "cmd_properties": + return (getSelectedDirectoryURI() != null); + case "cmd_newlist": + case "cmd_newCard": + return true; + default: + return false; + } + }, + + doCommand: function(command) + { + switch (command) { + case "cmd_printcard": + case "cmd_printcardpreview": + case "cmd_selectAll": + SendCommandToResultsPane(command); + break; + case "cmd_print": + AbPrintAddressBook(); + break; + case "cmd_printpreview": + AbPrintPreviewAddressBook(); + break; + case "cmd_delete": + case "button_delete": + if (gDirTree) + AbDeleteSelectedDirectory(); + break; + case "cmd_properties": + AbEditSelectedDirectory(); + break; + case "cmd_newlist": + AbNewList(); + break; + case "cmd_newCard": + AbNewCard(); + break; + } + }, + + onEvent: function(event) + { + // on blur events set the menu item texts back to the normal values + if (event == "blur") + goSetMenuValue("cmd_delete", "valueDefault"); + } +}; + +function SendCommandToResultsPane(command) +{ + ResultsPaneController.doCommand(command); + + // if we are sending the command so the results pane + // we should focus the results pane + gAbResultsTree.focus(); +} + +function AbNewLDAPDirectory() +{ + window.openDialog("chrome://messenger/content/addressbook/pref-directory-add.xul", + "", + "chrome,modal,resizable=no,centerscreen", + null); +} + +function AbNewAddressBook() +{ + window.openDialog("chrome://messenger/content/addressbook/abAddressBookNameDialog.xul", + "", + "chrome,modal,resizable=no,centerscreen", + null); +} + +function AbEditSelectedDirectory() +{ + let selectedDir = getSelectedDirectory(); + if (!selectedDir) + return; + + if (selectedDir.isMailList) { + goEditListDialog(null, selectedDir.URI); + } else { + window.openDialog(selectedDir.propertiesChromeURI, + "", + "chrome,modal,resizable=no,centerscreen", + {selectedDirectory: selectedDir}); + } +} + +function AbDeleteSelectedDirectory() +{ + let selectedDirURI = getSelectedDirectoryURI(); + if (!selectedDirURI) + return; + + AbDeleteDirectory(selectedDirURI); +} + +function AbDeleteDirectory(aURI) +{ + // Determine strings for smart and context-sensitive user prompts + // for confirming deletion. + let directory = GetDirectoryFromURI(aURI); + let confirmDeleteTitleID; + let confirmDeleteTitle; + let confirmDeleteMessageID; + let confirmDeleteMessage; + let brandShortName; + let clearCollectionPrefs = false; + + if (directory.isMailList) { + // It's a mailing list. + confirmDeleteMessageID = "confirmDeleteThisMailingList"; + confirmDeleteTitleID = "confirmDeleteThisMailingListTitle"; + } else { + // It's an address book: check which type. + if (Services.prefs.getCharPref("mail.collect_addressbook") == aURI && + (Services.prefs.getBoolPref("mail.collect_email_address_outgoing") || + Services.prefs.getBoolPref("mail.collect_email_address_incoming") || + Services.prefs.getBoolPref("mail.collect_email_address_newsgroup"))) { + // It's a collection address book: let's be clear about the consequences. + brandShortName = document.getElementById("bundle_brand").getString("brandShortName"); + confirmDeleteMessageID = "confirmDeleteThisCollectionAddressbook"; + confirmDeleteTitleID = "confirmDeleteThisCollectionAddressbookTitle"; + clearCollectionPrefs = true; + } else if (directory.URI.startsWith(kLdapUrlPrefix)) { + // It's an LDAP directory, so we only delete our offline copy. + confirmDeleteMessageID = "confirmDeleteThisLDAPDir"; + confirmDeleteTitleID = "confirmDeleteThisLDAPDirTitle"; + } else { + // It's a normal personal address book: we'll delete its contacts, too. + confirmDeleteMessageID = "confirmDeleteThisAddressbook"; + confirmDeleteTitleID = "confirmDeleteThisAddressbookTitle"; + } + } + + // Get the raw strings with placeholders. + confirmDeleteTitle = gAddressBookBundle.getString(confirmDeleteTitleID); + confirmDeleteMessage = gAddressBookBundle.getString(confirmDeleteMessageID); + + // Substitute placeholders as required. + // Replace #1 with the name of the selected address book or mailing list. + confirmDeleteMessage = confirmDeleteMessage.replace("#1", directory.dirName); + if (brandShortName) { + // For a collection address book, replace #2 with the brandShortName. + confirmDeleteMessage = confirmDeleteMessage.replace("#2", brandShortName); + } + + // Ask for confirmation before deleting + if (!Services.prompt.confirm(window, confirmDeleteTitle, + confirmDeleteMessage)) { + // Deletion cancelled by user. + return; + } + + // If we're about to delete the collection AB, update the respective prefs. + if (clearCollectionPrefs) { + Services.prefs.setBoolPref("mail.collect_email_address_outgoing", false); + Services.prefs.setBoolPref("mail.collect_email_address_incoming", false); + Services.prefs.setBoolPref("mail.collect_email_address_newsgroup", false); + + // Change the collection AB pref to "Personal Address Book" so that we + // don't get a blank item in prefs dialog when collection is re-enabled. + Services.prefs.setCharPref("mail.collect_addressbook", + kPersonalAddressbookURI); + } + + MailServices.ab.deleteAddressBook(aURI); +} + +function InitCommonJS() +{ + gDirTree = document.getElementById("dirTree"); + abList = document.getElementById("addressbookList"); + gAddressBookBundle = document.getElementById("bundle_addressBook"); + + // Make an entry for "All Address Books". + if (abList) { + abList.insertItemAt(0, gAddressBookBundle.getString("allAddressBooks"), + kAllDirectoryRoot + "?"); + } +} + +function UpgradeAddressBookResultsPaneUI(prefName) +{ + // placeholder in case any new columns get added to the address book + // var resultsPaneUIVersion = Services.prefs.getIntPref(prefName); +} + +function AbDelete() +{ + let types = GetSelectedCardTypes(); + if (types == kNothingSelected) + return; + + // Determine strings for smart and context-sensitive user prompts + // for confirming deletion. + let confirmDeleteTitleID; + let confirmDeleteTitle; + let confirmDeleteMessageID; + let confirmDeleteMessage; + let itemName; + let containingListName; + let selectedDir = getSelectedDirectory(); + let numSelectedItems = gAbView.selection.count; + + switch(types) { + case kListsAndCards: + confirmDeleteMessageID = "confirmDelete2orMoreContactsAndLists"; + confirmDeleteTitleID = "confirmDelete2orMoreContactsAndListsTitle"; + break; + case kSingleListOnly: + // Set item name for single mailing list. + let theCard = GetSelectedAbCards()[0]; + itemName = theCard.displayName; + confirmDeleteMessageID = "confirmDeleteThisMailingList"; + confirmDeleteTitleID = "confirmDeleteThisMailingListTitle"; + break; + case kMultipleListsOnly: + confirmDeleteMessageID = "confirmDelete2orMoreMailingLists"; + confirmDeleteTitleID = "confirmDelete2orMoreMailingListsTitle"; + break; + case kCardsOnly: + if (selectedDir.isMailList) { + // Contact(s) in mailing lists will be removed from the list, not deleted. + if (numSelectedItems == 1) { + confirmDeleteMessageID = "confirmRemoveThisContact"; + confirmDeleteTitleID = "confirmRemoveThisContactTitle"; + } else { + confirmDeleteMessageID = "confirmRemove2orMoreContacts"; + confirmDeleteTitleID = "confirmRemove2orMoreContactsTitle"; + } + // For removing contacts from mailing list, set placeholder value + containingListName = selectedDir.dirName; + } else { + // Contact(s) in address books will be deleted. + if (numSelectedItems == 1) { + confirmDeleteMessageID = "confirmDeleteThisContact"; + confirmDeleteTitleID = "confirmDeleteThisContactTitle"; + } else { + confirmDeleteMessageID = "confirmDelete2orMoreContacts"; + confirmDeleteTitleID = "confirmDelete2orMoreContactsTitle"; + } + } + if (numSelectedItems == 1) { + // Set item name for single contact. + let theCard = GetSelectedAbCards()[0]; + let nameFormatFromPref = Services.prefs.getIntPref("mail.addr_book.lastnamefirst"); + itemName = theCard.generateName(nameFormatFromPref); + } + break; + } + + // Get the raw model strings. + // For numSelectedItems == 1, it's simple strings. + // For messages with numSelectedItems > 1, it's multi-pluralform string sets. + // confirmDeleteMessage has placeholders for some forms. + confirmDeleteTitle = gAddressBookBundle.getString(confirmDeleteTitleID); + confirmDeleteMessage = gAddressBookBundle.getString(confirmDeleteMessageID); + + // Get plural form where applicable; substitute placeholders as required. + if (numSelectedItems == 1) { + // If single selected item, substitute itemName. + confirmDeleteMessage = confirmDeleteMessage.replace("#1", itemName); + } else { + // If multiple selected items, get the right plural string from the + // localized set, then substitute numSelectedItems. + confirmDeleteMessage = PluralForm.get(numSelectedItems, confirmDeleteMessage); + confirmDeleteMessage = confirmDeleteMessage.replace("#1", numSelectedItems); + } + // If contact(s) in a mailing list, substitute containingListName. + if (containingListName) + confirmDeleteMessage = confirmDeleteMessage.replace("#2", containingListName); + + // Finally, show our smart confirmation message, and act upon it! + if (!Services.prompt.confirm(window, confirmDeleteTitle, + confirmDeleteMessage)) { + // Deletion cancelled by user. + return; + } + + if (selectedDir.URI == (kAllDirectoryRoot + "?")) { + // Delete cards from "All Address Books" view. + let cards = GetSelectedAbCards(); + for (let i = 0; i < cards.length; i++) { + let dirId = cards[i].directoryId + .substring(0, cards[i].directoryId.indexOf("&")); + let directory = MailServices.ab.getDirectoryFromId(dirId); + + let cardArray = + Cc["@mozilla.org/array;1"] + .createInstance(Ci.nsIMutableArray); + cardArray.appendElement(cards[i], false); + if (directory) + directory.deleteCards(cardArray); + } + SetAbView(kAllDirectoryRoot + "?"); + } else { + // Delete cards from address books or mailing lists. + gAbView.deleteSelectedCards(); + } +} + +function AbNewCard() +{ + goNewCardDialog(getSelectedDirectoryURI()); +} + +function AbEditCard(card) +{ + // Need a card, + if (!card) + return; + + if (card.isMailList) { + goEditListDialog(card, card.mailListURI); + } else { + goEditCardDialog(getSelectedDirectoryURI(), card); + } +} + +function AbNewMessage() +{ + let params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(Ci.nsIMsgComposeParams); + if (params) { + let composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(Ci.nsIMsgCompFields); + if (composeFields) { + params.type = Ci.nsIMsgCompType.New; + params.format = Ci.nsIMsgCompFormat.Default; + if (DirPaneHasFocus()) { + let selectedDir = getSelectedDirectory(); + let hidesRecipients = false; + try { + // This is a bit of hackery so that extensions can have mailing lists + // where recipients are sent messages via BCC. + hidesRecipients = selectedDir.getBoolValue("HidesRecipients", false); + } catch(e) { + // Standard Thunderbird mailing lists do not have preferences + // associated with them, so we'll silently eat the error. + } + + if (selectedDir && selectedDir.isMailList && hidesRecipients) + // Bug 669301 (https://bugzilla.mozilla.org/show_bug.cgi?id=669301) + // We're using BCC right now to hide recipients from one another. + // We should probably use group syntax, but that's broken + // right now, so this will have to do. + composeFields.bcc = GetSelectedAddressesFromDirTree(); + else + composeFields.to = GetSelectedAddressesFromDirTree(); + } else { + composeFields.to = GetSelectedAddresses(); + } + params.composeFields = composeFields; + MailServices.compose.OpenComposeWindowWithParams(null, params); + } + } +} + +function AbCopyAddress() +{ + var cards = GetSelectedAbCards(); + if (!cards) + return; + + var count = cards.length; + if (!count) + return; + + var addresses = cards[0].primaryEmail; + for (var i = 1; i < count; i++) + addresses += "," + cards[i].primaryEmail; + + Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(addresses); +} + +/** + * Set up items in the View > Layout menupopup. This function is responsible + * for updating the menu items' state to reflect reality. + * + * @param aEvent the event that caused the View > Layout menupopup to be shown + */ +function InitViewLayoutMenuPopup(aEvent) +{ + let dirTreeVisible = document.getElementById("dirTree-splitter") + .getAttribute("state") != "collapsed"; + document.getElementById("menu_showDirectoryPane") + .setAttribute("checked", dirTreeVisible); + + let cardPaneVisible = document.getElementById("results-splitter") + .getAttribute("state") != "collapsed"; + document.getElementById("menu_showCardPane") + .setAttribute("checked", cardPaneVisible); +} + +// Generate a list of cards from the selected mailing list +// and get a comma separated list of card addresses. If the +// item selected in the directory pane is not a mailing list, +// an empty string is returned. +function GetSelectedAddressesFromDirTree() +{ + let selectedDir = getSelectedDirectory(); + + if (!selectedDir || !selectedDir.isMailList) + return ""; + + let listCardsCount = selectedDir.addressLists.length; + let cards = new Array(listCardsCount); + for (let i = 0; i < listCardsCount; ++i) + cards[i] = selectedDir.addressLists + .queryElementAt(i, Ci.nsIAbCard); + return GetAddressesForCards(cards); +} + +// Generate a comma separated list of addresses from a given +// set of cards. +function GetAddressesForCards(cards) +{ + var addresses = ""; + + if (!cards) + return addresses; + + var count = cards.length; + for (var i = 0; i < count; ++i) { + var generatedAddress = GenerateAddressFromCard(cards[i]); + if (generatedAddress) { + // If it's not the first address in the list, add a comma separator. + if (addresses) + addresses += ","; + addresses += generatedAddress; + } + } + + return addresses; +} + + +function SelectFirstAddressBook() +{ + if (gDirectoryTreeView.selection.currentIndex != 0) { + gDirectoryTreeView.selection.select(0); + ChangeDirectoryByURI(getSelectedDirectoryURI()); + } + gAbResultsTree.focus(); +} + +function DirPaneClick(event) +{ + // we only care about left button events + if (event.button != 0) + return; + + // if the user clicks on the header / trecol, do nothing + if (event.originalTarget.localName == "treecol") { + event.stopPropagation(); + return; + } +} + +function DirPaneDoubleClick(event) +{ + // We only care about left button events. + if (event.button != 0) + return; + + // Ignore double clicking on invalid rows. + let row = gDirTree.treeBoxObject.getRowAt(event.clientX, event.clientY); + if (row == -1 || row >= gDirectoryTreeView.rowCount) + return; + + // Default action for double click is expand/collapse which ships with the tree. + // For convenience, allow double-click to edit the properties of mailing + // lists in directory tree. + if (gDirTree && gDirTree.view.selection && + gDirTree.view.selection.count == 1 && + getSelectedDirectory().isMailList) { + AbEditSelectedDirectory(); + } +} + +function DirPaneSelectionChange() +{ + let uri = getSelectedDirectoryURI(); + // clear out the search box when changing folders... + onAbClearSearch(false); + if (gDirectoryTreeView.selection && + gDirectoryTreeView.selection.count == 1) { + ChangeDirectoryByURI(uri); + document.getElementById("localResultsOnlyMessage") + .setAttribute("hidden", + !gDirectoryTreeView.hasRemoteAB || + uri != kAllDirectoryRoot + "?"); + } +} + +function ChangeDirectoryByURI(uri = kPersonalAddressbookURI) +{ + SetAbView(uri); + + // Actively de-selecting if there are any pre-existing selections + // in the results list. + if (gAbView && gAbView.getCardFromRow(0)) + gAbView.selection.clearSelection(); + else + // the selection changes if we were switching directories. + ResultsPaneSelectionChanged() +} + +function AbNewList() +{ + goNewListDialog(getSelectedDirectoryURI()); +} + +function goNewListDialog(selectedAB) +{ + window.openDialog("chrome://messenger/content/addressbook/abMailListDialog.xul", + "", + "chrome,modal,resizable,centerscreen", + {selectedAB:selectedAB}); +} + +function goEditListDialog(abCard, listURI) +{ + let params = { + abCard: abCard, + listURI: listURI, + refresh: false, // This is an out param, true if OK in dialog is clicked. + }; + + window.openDialog("chrome://messenger/content/addressbook/abEditListDialog.xul", + "", + "chrome,modal,resizable,centerscreen", + params); + + if (params.refresh) { + ChangeDirectoryByURI(listURI); // force refresh + } +} + +function goNewCardDialog(selectedAB) +{ + window.openDialog("chrome://messenger/content/addressbook/abNewCardDialog.xul", + "", + "chrome,modal,resizable=no,centerscreen", + {selectedAB:selectedAB}); +} + +function goEditCardDialog(abURI, card) +{ + window.openDialog("chrome://messenger/content/addressbook/abEditCardDialog.xul", + "", + "chrome,modal,resizable=no,centerscreen", + {abURI:abURI, card:card}); +} + +function setSortByMenuItemCheckState(id, value) +{ + var menuitem = document.getElementById(id); + if (menuitem) { + menuitem.setAttribute("checked", value); + } +} + +function InitViewSortByMenu() +{ + var sortColumn = kDefaultSortColumn; + var sortDirection = kDefaultAscending; + + if (gAbView) { + sortColumn = gAbView.sortColumn; + sortDirection = gAbView.sortDirection; + } + + // this approach is necessary to support generic columns that get overlayed. + let elements = document.querySelectorAll('[name="sortas"]'); + for (let i = 0; i < elements.length; i++) { + let cmd = elements[i].id; + let columnForCmd = cmd.substr(10); // everything right of cmd_SortBy + setSortByMenuItemCheckState(cmd, (sortColumn == columnForCmd)); + } + + setSortByMenuItemCheckState("sortAscending", (sortDirection == kDefaultAscending)); + setSortByMenuItemCheckState("sortDescending", (sortDirection == kDefaultDescending)); +} + +function GenerateAddressFromCard(card) +{ + if (!card) + return ""; + + var email; + + if (card.isMailList) + { + var directory = GetDirectoryFromURI(card.mailListURI); + email = directory.description || card.displayName; + } + else + email = card.primaryEmail; + + return MailServices.headerParser.makeMimeAddress(card.displayName, email); +} + +function GetDirectoryFromURI(uri) +{ + return MailServices.ab.getDirectory(uri); +} + +// returns null if abURI is not a mailing list URI +function GetParentDirectoryFromMailingListURI(abURI) +{ + var abURIArr = abURI.split("/"); + /* + turn turn "moz-abmdbdirectory://abook.mab/MailList6" + into ["moz-abmdbdirectory:","","abook.mab","MailList6"] + then, turn ["moz-abmdbdirectory:","","abook.mab","MailList6"] + into "moz-abmdbdirectory://abook.mab" + */ + if (abURIArr.length == 4 && abURIArr[0] == "moz-abmdbdirectory:" && abURIArr[3] != "") { + return abURIArr[0] + "/" + abURIArr[1] + "/" + abURIArr[2]; + } + + return null; +} + +/** + * Return true if the directory pane has focus, otherwise false. + */ +function DirPaneHasFocus() +{ + return (top.document.commandDispatcher.focusedElement == gDirTree); +} + +/** + * Get the selected directory object. + * + * @return The object of the currently selected directory + */ +function getSelectedDirectory() +{ + // Select Addresses Dialog + if (abList) + return MailServices.ab.getDirectory(abList.value); + + // Main Address Book + if (gDirTree.currentIndex < 0) + return null; + return gDirectoryTreeView.getDirectoryAtIndex(gDirTree.currentIndex); +} + +/** + * Get the URI of the selected directory. + * + * @return The URI of the currently selected directory + */ +function getSelectedDirectoryURI() +{ + // Select Addresses Dialog + if (abList) + return abList.value; + + // Main Address Book + if (gDirTree.currentIndex < 0) + return null; + return gDirectoryTreeView.getDirectoryAtIndex(gDirTree.currentIndex).URI; +} + +/** + * DEPRECATED legacy function wrapper for addon compatibility; + * use getSelectedDirectoryURI() instead! + * Return the URI of the selected directory. + */ +function GetSelectedDirectory() +{ + return getSelectedDirectoryURI(); +} + +/** + * Clears the contents of the search input field, + * possibly causing refresh of results. + * + * @param aRefresh Set to false if the refresh isn't needed, + * e.g. window/AB is going away so user will not see anything. + */ +function onAbClearSearch(aRefresh = true) +{ + let searchInput = document.getElementById("searchInput"); + if (!searchInput || !searchInput.value) + return; + + searchInput.value = ""; + if (aRefresh) + onEnterInSearchBar(); +} + +/** + * Returns an nsIFile of the directory in which contact photos are stored. + * This will create the directory if it does not yet exist. + */ +function getPhotosDir() { + var file = Services.dirsvc.get("ProfD", Ci.nsIFile); + // Get the Photos directory + file.append("Photos"); + if (!file.exists() || !file.isDirectory()) + file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0777", 8)); + return file; +} + +/** + * Returns a URI specifying the location of a photo based on its name. + * If the name is blank, or if the photo with that name is not in the Photos + * directory then the default photo URI is returned. + * + * @param aPhotoName The name of the photo from the Photos folder, if any. + * + * @return A URI pointing to a photo. + */ +function getPhotoURI(aPhotoName) { + if (!aPhotoName) + return defaultPhotoURI; + var file = getPhotosDir(); + try { + file.append(aPhotoName); + } + catch (e) { + return defaultPhotoURI; + } + if (!file.exists()) + return defaultPhotoURI; + return Services.io.newFileURI(file).spec; +} + +/** + * Generates a unique filename to be used for a local copy of a contact's photo. + * + * @param aPath The path to the folder in which the photo will be saved. + * @param aExtension The file extension of the photo. + * + * @return A unique filename in the given path. + */ +function makePhotoFile(aDir, aExtension) { + var filename, newFile; + // Find a random filename for the photo that doesn't exist yet + do { + filename = new String(Math.random()).replace("0.", "") + "." + aExtension; + newFile = aDir.clone(); + newFile.append(filename); + } while (newFile.exists()); + return newFile; +} + +/** + * Public self-contained object for image transfers. + * Responsible for file transfer, validating the image and downscaling. + * Attention: It is the responsibility of the caller to remove the old photo + * and update the card! + */ +var gImageDownloader = (function() { + let downloadInProgress = false; + + // Current instance of nsIWebBrowserPersist. It is used two times, during + // the actual download and for saving canvas data. + let downloader; + + // Temporary nsIFile used for download. + let tempFile; + + // Images are downsized to this size while keeping the aspect ratio. + const maxSize = 300; + + // Callback function for various states + let callbackSuccess; + let callbackError; + let callbackProgress; + + // Start at 4% to show a slight progress initially. + const initProgress = 4; + + // Constants indicating error and file transfer status + const STATE_TRANSFERRING = 0; + const STATE_RESIZING = 1; + const STATE_OK = 2; + // The URI does not have a valid format. + const ERROR_INVALID_URI = 0; + // In case of HTTP transfers: server did not answer with a 200 status code. + const ERROR_UNAVAILABLE = 1; + // The file type is not supported. Only jpeg, png and gif are. + const ERROR_INVALID_IMG = 2; + // An error occurred while saving the image to the hard drive. + const ERROR_SAVE = 4; + + + /** + * Saves a target photo in the profile's photo directory. Only one concurrent + * file transfer is supported. Starting a new transfer while another is still + * in progress will cancel the former file transfer. + * + * @param aURI {string} URI pointing to the photo. + * @param cbSuccess(photoName) {function} A callback function which is called + * on success. + * The photo file name is passed in. + * @param cbError(state) {function} A callback function which is called + * in case of an error. The error + * state is passed in. + * @param cbProgress(errcode, percent) {function} A callback function which + * provides progress report. + * An error code (see above) + * and the progress percentage + * (0-100) is passed in. + * State transitions: STATE_TRANSFERRING -> STATE_RESIZING -> STATE_OK (100%) + */ + function savePhoto(aURI, aCBSuccess, aCBError, aCBProgress) { + callbackSuccess = typeof aCBSuccess == "function" ? aCBSuccess : null; + callbackError = typeof aCBError == "function" ? aCBError : null; + callbackProgress = typeof aCBProgress == "function" ? aCBProgress : null; + + // Make sure that there is no running download. + cancelSave(); + downloadInProgress = true; + + if (callbackProgress) { + callbackProgress(STATE_TRANSFERRING, initProgress); + } + + downloader = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] + .createInstance(Ci.nsIWebBrowserPersist); + downloader.persistFlags = + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE | + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_CLEANUP_ON_FAILURE; + downloader.progressListener = { + onProgressChange(aWebProgress, aRequest, aCurSelfProgress, + aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { + if (aMaxTotalProgress > -1 && callbackProgress) { + // Download progress is 0-90%, 90-100% is verifying and scaling the + // image. + let percent = + Math.round(initProgress + + (aCurTotalProgress / aMaxTotalProgress) * + (90 - initProgress)); + callbackProgress(STATE_TRANSFERRING, percent); + } + }, + onStateChange(aWebProgress, aRequest, aStateFlag, aStatus) { + // Check if the download successfully finished. + if ((aStateFlag & Ci.nsIWebProgressListener.STATE_STOP) && + !(aStateFlag & Ci.nsIWebProgressListener.STATE_IS_REQUEST)) { + try { + // Check the response code in case of an HTTP request to catch 4xx + // errors. + let http = aRequest.QueryInterface(Ci.nsIHttpChannel); + if (http.responseStatus == 200) { + verifyImage(); + } else if (callbackError) { + callbackError(ERROR_UNAVAILABLE); + } + } catch (err) { + // The nsIHttpChannel interface is not available - just proceed. + verifyImage(); + } + } + }, + }; + + let source; + try { + source = Services.io.newURI(aURI); + } catch (err) { + if (callbackError) { + callbackError(ERROR_INVALID_URI); + } + return; + } + + // Start the transfer to a temporary file. + tempFile = FileUtils.getFile("TmpD", + ["tb-photo-" + new Date().getTime() + ".tmp"]); + tempFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE); + try { + // Obtain the privacy context of the browser window that the URL + // we are downloading comes from. If, and only if, the URL is not + // related to a window, null should be used instead. + let privacy = PrivateBrowsingUtils.privacyContextFromWindow(window); + downloader.saveURI(source, null, null, null, null, null, tempFile, + privacy); + } catch (err) { + cleanup(); + if (callbackError) { + callbackError(ERROR_SAVE); + } + } + } + + /** + * Verifies the downloaded file to be an image. + * Scales the image and starts the saving operation. + */ + function verifyImage() { + let img = new Image(); + img.onerror = function() { + cleanup(); + if (callbackError) { + callbackError(ERROR_INVALID_IMG); + } + }; + img.onload = function() { + if (callbackProgress) { + callbackProgress(STATE_RESIZING, 95); + } + + // Images are scaled down in two steps to improve quality. Resizing + // ratios larger than 2 use a different interpolation algorithm than + // small ratios. Resize three times (instead of just two steps) to + // improve the final quality. + let canvas = downscale(img, 3.8 * maxSize); + canvas = downscale(canvas, 1.9 * maxSize); + canvas = downscale(canvas, maxSize); + + saveCanvas(canvas); + + if (callbackProgress) { + callbackProgress(STATE_OK, 100); + } + + // Remove the temporary file. + cleanup(); + }; + + if (callbackProgress) { + callbackProgress(STATE_RESIZING, 92); + } + + img.src = Services.io.newFileURI(tempFile).spec; + } + + /** + * Scale a graphics object down to a specified maximum dimension while + * preserving the aspect ratio. Does not upscale an image. + * + * @param aGraphicsObject {image | canvas} Image or canvas object + * @param aMaxDimension {integer} The maximal allowed width or + * height + * + * @return A canvas object. + */ + function downscale(aGraphicsObject, aMaxDimension) { + let w = aGraphicsObject.width; + let h = aGraphicsObject.height; + + if (w > h && w > aMaxDimension) { + h = Math.round(aMaxDimension * h / w); + w = aMaxDimension; + } else if (h > aMaxDimension) { + w = Math.round(aMaxDimension * w / h); + h = aMaxDimension; + } + + let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", + "canvas"); + canvas.width = w; + canvas.height = h; + + let ctx = canvas.getContext("2d"); + ctx.drawImage(aGraphicsObject, 0, 0, w, h); + return canvas; + } + + /** + * Cancel a running download (if any). + */ + function cancelSave() { + if (!downloadInProgress) { + return; + } + + // Cancel the nsIWebBrowserPersist file transfer. + if (downloader) { + downloader.cancelSave(); + } + cleanup(); + } + + /** + * Remove the temporary file and reset internal status. + */ + function cleanup() { + if (tempFile) { + try { + if (tempFile.exists()) { + tempFile.remove(false); + } + } catch (err) {} + tempFile = null; + } + + downloadInProgress = false; + } + + /** + * Save the contents of a canvas to the photos directory of the profile. + */ + function saveCanvas(aCanvas) { + // Get the photos directory and check that it exists. + let file = getPhotosDir(); + file = makePhotoFile(file, "png"); + + // Create a data url from the canvas and then create URIs of the source and + // targets. + let source = Services.io.newURI(aCanvas.toDataURL("image/png", ""), "UTF8"); + let target = Services.io.newFileURI(file); + + downloader = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] + .createInstance(Ci.nsIWebBrowserPersist); + downloader.persistFlags = + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE | + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_CLEANUP_ON_FAILURE; + downloader.progressListener = { + onStateChange(aWebProgress, aRequest, aFlag, aStatus) { + if ((aFlag & Ci.nsIWebProgressListener.STATE_STOP) && + !(aFlag & Ci.nsIWebProgressListener.STATE_IS_REQUEST)) { + if (callbackSuccess) { + callbackSuccess(file.leafName); + } + } + }, + }; + + // Obtain the privacy context of the browser window that the URL + // we are downloading comes from. If, and only if, the URL is not + // related to a window, null should be used instead. + let privacy = PrivateBrowsingUtils.privacyContextFromWindow(window); + downloader.saveURI(source, null, null, null, null, null, target, privacy); + } + + // Publicly accessible methods. + return { cancelSave, savePhoto, STATE_TRANSFERRING, STATE_RESIZING, STATE_OK, + ERROR_UNAVAILABLE, ERROR_INVALID_URI, ERROR_INVALID_IMG, + ERROR_SAVE, }; +})(); + +/** + * Validates the given year and returns it, if it looks sane. + * Returns kDefaultYear (a leap year), if no valid date is given. + * This ensures that month/day calculations still work. + */ +function saneBirthYear(aYear) { + return aYear && (aYear <= kMaxYear) && (aYear >= kMinYear) ? aYear : kDefaultYear; +} + +/** + * Returns the nearest leap year before aYear. + */ +function nearestLeap(aYear) { + for (let year = aYear; year > 0; year--) { + if (new Date(year, 1, 29).getMonth() == 1) + return year; + } + return 2000; +} diff --git a/comm/suite/mailnews/components/addrbook/content/abEditCardDialog.xul b/comm/suite/mailnews/components/addrbook/content/abEditCardDialog.xul new file mode 100644 index 0000000000..752934becb --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abEditCardDialog.xul @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<?xul-overlay href="chrome://messenger/content/addressbook/abCardOverlay.xul"?> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="abcardWindow" + onload="OnLoadEditCard()" + ondialogaccept="return EditCardOKButton();" + ondialogcancel="return EditCardCancelButton();"> + + <stringbundleset id="stringbundleset"/> + <vbox id="editcard"/> +</dialog> diff --git a/comm/suite/mailnews/components/addrbook/content/abEditListDialog.xul b/comm/suite/mailnews/components/addrbook/content/abEditListDialog.xul new file mode 100644 index 0000000000..3162664e99 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abEditListDialog.xul @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/" type="text/css"?> + +<?xul-overlay href="chrome://messenger/content/addressbook/abListOverlay.xul"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/addressbook/abMailListDialog.dtd"> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + id="ablistWindow" + onload="OnLoadEditList()" + ondialogaccept="return EditListOKButton();" + ondragover="DragOverAddressListTree(event);" + ondrop="DropOnAddressListTree(event);"> + + <stringbundleset id="stringbundleset"/> + <vbox id="editlist" flex="1"/> +</dialog> diff --git a/comm/suite/mailnews/components/addrbook/content/abListOverlay.xul b/comm/suite/mailnews/components/addrbook/content/abListOverlay.xul new file mode 100644 index 0000000000..9f8c07f010 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abListOverlay.xul @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/addressingWidget.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/addressbook/cardDialog.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/addressbook/abMailListDialog.dtd"> + +<overlay id="editListOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> + </stringbundleset> + + <script src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"/> + <script src="chrome://messenger/content/addressbook/abCommon.js"/> + <script src="chrome://messenger/content/addressbook/abMailListDialog.js"/> + +<vbox id="editlist"> + <grid> + <columns> + <column/> + <column class="CardEditWidth" flex="1"/> + </columns> + + <rows> + <row id="ListNameContainer" align="center"> + <label control="ListName" + class="CardEditLabel" + value="&ListName.label;" + accesskey="&ListName.accesskey;"/> + <textbox id="ListName"/> + </row> + + <row id="ListNickNameContainer" align="center"> + <label control="ListNickName" + class="CardEditLabel" + value="&ListNickName.label;" + accesskey="&ListNickName.accesskey;"/> + <textbox id="ListNickName"/> + </row> + + <row id="ListDescriptionContainer" align="center"> + <label control="ListDescription" + class="CardEditLabel" + value="&ListDescription.label;" + accesskey="&ListDescription.accesskey;"/> + <textbox id="ListDescription"/> + </row> + </rows> + </grid> + + <spacer style="height:1em"/> + <label control="addressCol1#1" + value="&AddressTitle.label;" + accesskey="&AddressTitle.accesskey;"/> + + <spacer style="height:0.1em"/> + + <listbox id="addressingWidget" style="height: 15em;" + onclick="awClickEmptySpace(event.target, true)"> + <listitem class="addressingWidgetItem" allowevents="true"> + <listcell class="addressingWidgetCell"> + <textbox id="addressCol1#1" + class="plain textbox-addressingWidget uri-element" + type="autocomplete" + flex="1" + autocompletesearch="addrbook ldap" + autocompletesearchparam="{}" timeout="300" maxrows="4" + completedefaultindex="true" forcecomplete="true" + minresultsforpopup="3" + ontextentered="awRecipientTextCommand(eventParam, this)" + onkeydown="awRecipientKeyDown(event, this);" + onclick="awNotAnEmptyArea(event);"> + <image onclick="awNotAnEmptyArea(event)" class="person-icon"/> + </textbox> + </listcell> + </listitem> + </listbox> +</vbox> + +</overlay> + diff --git a/comm/suite/mailnews/components/addrbook/content/abMailListDialog.xul b/comm/suite/mailnews/components/addrbook/content/abMailListDialog.xul new file mode 100644 index 0000000000..f335bfdce3 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abMailListDialog.xul @@ -0,0 +1,34 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/" type="text/css"?> + +<?xul-overlay href="chrome://messenger/content/addressbook/abListOverlay.xul"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/addressbook/abMailListDialog.dtd"> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="ablistWindow" + title="&mailListWindow.title;" + ondialogaccept="return MailListOKButton();" + onload="OnLoadNewMailList()" + ondragover="DragOverAddressListTree(event);" + ondrop="DropOnAddressListTree(event);"> + + <stringbundleset id="stringbundleset"/> + + <hbox align="center" valign="center"> + <label control="abPopup" value="&addToAddressBook.label;" accesskey="&addToAddressBook.accesskey;"/> + <menulist id="abPopup"> + <menupopup id="abPopup-menupopup" class="addrbooksPopup" writable="true" + supportsmaillists="true"/> + </menulist> + </hbox> + + <spacer style="height:1em"/> + + <vbox id="editlist"/> + +</dialog> diff --git a/comm/suite/mailnews/components/addrbook/content/abNewCardDialog.xul b/comm/suite/mailnews/components/addrbook/content/abNewCardDialog.xul new file mode 100644 index 0000000000..80f4e578e3 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abNewCardDialog.xul @@ -0,0 +1,35 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> + +<?xul-overlay href="chrome://messenger/content/addressbook/abCardOverlay.xul"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/addressbook/abNewCardDialog.dtd"> + +<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="abcardWindow" + windowtype="mailnews:newcarddialog" + onload="OnLoadNewCard()" + ondialogaccept="return NewCardOKButton();" + ondialogcancel="return NewCardCancelButton();"> + + <stringbundleset id="stringbundleset"/> + + <hbox align="center"> + + <label id="abPopupLabel" control="abPopup" value="&chooseAddressBook.label;" accesskey="&chooseAddressBook.accesskey;"/> + + <menulist id="abPopup"> + <menupopup id="abPopup-menupopup" class="addrbooksPopup" writable="true"/> + </menulist> + + </hbox> + + <spacer style="height:1em"/> + + <vbox id="editcard"/> + +</dialog> diff --git a/comm/suite/mailnews/components/addrbook/content/abResultsPaneOverlay.xul b/comm/suite/mailnews/components/addrbook/content/abResultsPaneOverlay.xul new file mode 100644 index 0000000000..08af2de3fc --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abResultsPaneOverlay.xul @@ -0,0 +1,94 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/addressbook/abResultsPane.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/addressbook/abResultsPaneOverlay.dtd"> + +<overlay + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + +<script src="chrome://messenger/content/addressbook/abResultsPane.js"/> +<script src="chrome://global/content/nsDragAndDrop.js"/> +<script src="chrome://messenger/content/addressbook/abDragDrop.js"/> + +<tree id="abResultsTree" flex="1" enableColumnDrag="true" class="plain focusring" + onclick="AbResultsPaneOnClick(event);" + onselect="this.view.selectionChanged(); document.commandDispatcher.updateCommands('addrbook-select');" + sortCol="GeneratedName" + persist="sortCol height"> + + <treecols id="abResultsTreeCols"> + <!-- these column ids must match up to the mork column names, except for GeneratedName and ChatName, see nsIAddrDatabase.idl --> + <treecol id="GeneratedName" + persist="hidden ordinal width sortDirection" flex="1" + label="&GeneratedName.label;" primary="true"/> + <splitter class="tree-splitter"/> + <treecol id="PrimaryEmail" + persist="hidden ordinal width sortDirection" flex="1" + label="&PrimaryEmail.label;"/> + <splitter class="tree-splitter"/> + <treecol id="ChatName" + persist="hidden ordinal width sortDirection" flex="1" + label="&ChatName.label;"/> + <splitter class="tree-splitter"/> + <treecol id="Company" + persist="hidden ordinal width sortDirection" flex="1" + label="&Company.label;"/> + <splitter class="tree-splitter"/> + <treecol id="NickName" + persist="hidden ordinal width sortDirection" flex="1" + label="&NickName.label;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol id="SecondEmail" + persist="hidden ordinal width sortDirection" flex="1" + label="&SecondEmail.label;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol id="Department" + persist="hidden ordinal width sortDirection" flex="1" + label="&Department.label;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol id="JobTitle" + persist="hidden ordinal width sortDirection" flex="1" + label="&JobTitle.label;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol id="CellularNumber" + persist="hidden ordinal width sortDirection" flex="1" + label="&CellularNumber.label;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol id="PagerNumber" + persist="hidden ordinal width sortDirection" flex="1" + label="&PagerNumber.label;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol id="FaxNumber" + persist="hidden ordinal width sortDirection" flex="1" + label="&FaxNumber.label;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol id="HomePhone" + persist="hidden ordinal width sortDirection" flex="1" + label="&HomePhone.label;" hidden="true"/> + <splitter class="tree-splitter"/> + <treecol id="WorkPhone" + persist="hidden ordinal width sortDirection" flex="1" + label="&WorkPhone.label;"/> + <splitter class="tree-splitter"/> + <treecol id="addrbook" + persist="hidden ordinal width sortDirection" flex="1" + hidden="true" + label="&Addrbook.label;"/> + <!-- LOCALIZATION NOTE: _PhoneticName may be enabled for Japanese builds. --> + <!-- + <splitter class="tree-splitter"/> + <treecol id="_PhoneticName" + persist="hidden ordinal width sortDirection" flex="1" + label="&_PhoneticName.label;" hidden="true"/> + --> + + </treecols> + <treechildren ondragstart="nsDragAndDrop.startDrag(event, abResultsPaneObserver);"/> +</tree> + +</overlay> + diff --git a/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.js b/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.js new file mode 100644 index 0000000000..7e23a0b89d --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.js @@ -0,0 +1,399 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const {encodeABTermValue, getModelQuery} = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm"); + +var addressbook = 0; +var composeWindow = 0; +var msgCompFields = 0; +var editCardCallback = 0; + +var gSearchInput; +var gSearchTimer = null; +var gQueryURIFormat = null; + +// localization strings +var prefixTo; +var prefixCc; +var prefixBcc; + +var gToButton; +var gCcButton; +var gBccButton; + +var gActivatedButton; + +var gDragService = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService); + +var gSelectAddressesAbViewListener = { + onSelectionChanged: function() { + ResultsPaneSelectionChanged(); + }, + onCountChanged: function(total) { + // do nothing + } +}; + +function GetAbViewListener() +{ + return gSelectAddressesAbViewListener; +} + +function OnLoadSelectAddress() +{ + InitCommonJS(); + + prefixTo = gAddressBookBundle.getString("prefixTo") + ": "; + prefixCc = gAddressBookBundle.getString("prefixCc") + ": "; + prefixBcc = gAddressBookBundle.getString("prefixBcc") + ": "; + + UpgradeAddressBookResultsPaneUI("mailnews.ui.select_addresses_results.version"); + + var toAddress="", ccAddress="", bccAddress=""; + + // look in arguments[0] for parameters + if (window.arguments && window.arguments[0]) + { + // keep parameters in global for later + if ( window.arguments[0].composeWindow ) + top.composeWindow = window.arguments[0].composeWindow; + if ( window.arguments[0].msgCompFields ) + top.msgCompFields = window.arguments[0].msgCompFields; + if ( window.arguments[0].toAddress ) + toAddress = window.arguments[0].toAddress; + if ( window.arguments[0].ccAddress ) + ccAddress = window.arguments[0].ccAddress; + if ( window.arguments[0].bccAddress ) + bccAddress = window.arguments[0].bccAddress; + + // put the addresses into the bucket + AddAddressFromComposeWindow(toAddress, prefixTo); + AddAddressFromComposeWindow(ccAddress, prefixCc); + AddAddressFromComposeWindow(bccAddress, prefixBcc); + } + + gSearchInput = document.getElementById("searchInput"); + + // Reselect the persisted address book if possible, if not just select the + // first in the list. + var temp = abList.value; + abList.selectedItem = null; + abList.value = temp; + if (!abList.selectedItem) + abList.selectedIndex = 0; + + ChangeDirectoryByURI(abList.value); + + DialogBucketPaneSelectionChanged(); + + var workPhoneCol = document.getElementById("WorkPhone"); + workPhoneCol.setAttribute("hidden", "true"); + + var companyCol = document.getElementById("Company"); + companyCol.setAttribute("hidden", "true"); + + gToButton = document.getElementById("toButton"); + gCcButton = document.getElementById("ccButton"); + gBccButton = document.getElementById("bccButton"); + + gAbResultsTree.focus(); + + gActivatedButton = gToButton; + + document.documentElement.addEventListener("keypress", OnReturnHit, true); +} + +function OnUnloadSelectAddress() +{ + CloseAbView(); +} + +function AddAddressFromComposeWindow(addresses, prefix) +{ + if ( addresses ) + { + var emails = {}; + var names = {}; + var fullNames = {}; + var numAddresses = MailServices.headerParser.parseHeadersWithArray(addresses, emails, names, fullNames); + + for ( var index = 0; index < numAddresses; index++ ) + { + AddAddressIntoBucket(prefix, fullNames.value[index], emails.value[index]); + } + } +} + +function SelectAddressOKButton() +{ + // Empty email checks are now done in AddAddressIntoBucket below. + var body = document.getElementById('bucketBody'); + var item, row, cell, prefix, address, email; + var toAddress="", ccAddress="", bccAddress="", emptyEmail=""; + + for ( var index = 0; index < body.childNodes.length; index++ ) + { + item = body.childNodes[index]; + if ( item.childNodes && item.childNodes.length ) + { + row = item.childNodes[0]; + if ( row.childNodes && row.childNodes.length ) + { + cell = row.childNodes[0]; + prefix = cell.getAttribute('prefix'); + address = cell.getAttribute('address'); + email = cell.getAttribute('email'); + if ( prefix ) + { + switch ( prefix ) + { + case prefixTo: + if ( toAddress ) + toAddress += ", "; + toAddress += address; + break; + case prefixCc: + if ( ccAddress ) + ccAddress += ", "; + ccAddress += address; + break; + case prefixBcc: + if ( bccAddress ) + bccAddress += ", "; + bccAddress += address; + break; + } + } + } + } + } + // reset the UI in compose window + msgCompFields.to = toAddress; + msgCompFields.cc = ccAddress; + msgCompFields.bcc = bccAddress; + top.composeWindow.CompFields2Recipients(top.msgCompFields); + + return true; +} + +function SelectAddressToButton() +{ + AddSelectedAddressesIntoBucket(prefixTo); + gActivatedButton = gToButton; +} + +function SelectAddressCcButton() +{ + AddSelectedAddressesIntoBucket(prefixCc); + gActivatedButton = gCcButton; +} + +function SelectAddressBccButton() +{ + AddSelectedAddressesIntoBucket(prefixBcc); + gActivatedButton = gBccButton; +} + +function AddSelectedAddressesIntoBucket(prefix) +{ + var cards = GetSelectedAbCards(); + var count = cards.length; + + for (var i = 0; i < count; i++) { + AddCardIntoBucket(prefix, cards[i]); + } +} + +function AddCardIntoBucket(prefix, card) +{ + var address = GenerateAddressFromCard(card); + if (card.isMailList) { + AddAddressIntoBucket(prefix, address, card.displayName); + } + else { + AddAddressIntoBucket(prefix, address, card.primaryEmail); + } +} + +function AddAddressIntoBucket(prefix, address, email) +{ + if (!email) + { + Services.prompt.alert(window, + gAddressBookBundle.getString("emptyEmailAddCardTitle"), + gAddressBookBundle.getString("emptyEmailAddCard")); + } + else + { + var body = document.getElementById("bucketBody"); + + var item = document.createElement('treeitem'); + var row = document.createElement('treerow'); + var cell = document.createElement('treecell'); + cell.setAttribute('label', prefix + address); + cell.setAttribute('prefix', prefix); + cell.setAttribute('address', address); + cell.setAttribute('email', email); + + row.appendChild(cell); + item.appendChild(row); + body.appendChild(item); + } +} + +function RemoveSelectedFromBucket() +{ + var bucketTree = document.getElementById("addressBucket"); + if ( bucketTree ) + { + var body = document.getElementById("bucketBody"); + var selection = bucketTree.view.selection; + var rangeCount = selection.getRangeCount(); + + for (var i = rangeCount-1; i >= 0; --i) + { + var start = {}, end = {}; + selection.getRangeAt(i,start,end); + for (var j = end.value; j >= start.value; --j) + { + bucketTree.contentView.getItemAtIndex(j).remove(); + } + } + } +} + +/* Function: ResultsPaneSelectionChanged() + * Callers : OnLoadSelectAddress(), abCommon.js:ResultsPaneSelectionChanged() + * ------------------------------------------------------------------------- + * This function is used to grab the selection state of the results tree to maintain + * the appropriate enabled/disabled states of the "Edit", "To:", "CC:", and "Bcc:" buttons. + * If an entry is selected in the results Tree, then the "disabled" attribute is removed. + * Otherwise, if nothing is selected, "disabled" is set to true. + */ + +function ResultsPaneSelectionChanged() +{; + var editButton = document.getElementById("edit"); + var toButton = document.getElementById("toButton"); + var ccButton = document.getElementById("ccButton"); + var bccButton = document.getElementById("bccButton"); + + var numSelected = GetNumSelectedCards(); + if (numSelected > 0) + { + if (numSelected == 1) + editButton.removeAttribute("disabled"); + else + editButton.setAttribute("disabled", "true"); + + toButton.removeAttribute("disabled"); + ccButton.removeAttribute("disabled"); + bccButton.removeAttribute("disabled"); + } + else + { + editButton.setAttribute("disabled", "true"); + toButton.setAttribute("disabled", "true"); + ccButton.setAttribute("disabled", "true"); + bccButton.setAttribute("disabled", "true"); + } +} + +/* Function: DialogBucketPaneSelectionChanged() + * Callers : OnLoadSelectAddress(), abSelectAddressesDialog.xul:id="addressBucket" + * ------------------------------------------------------------------------------- + * This function is used to grab the selection state of the bucket tree to maintain + * the appropriate enabled/disabled states of the "Remove" button. + * If an entry is selected in the bucket Tree, then the "disabled" attribute is removed. + * Otherwise, if nothing is selected, "disabled" is set to true. + */ + +function DialogBucketPaneSelectionChanged() +{ + var bucketTree = document.getElementById("addressBucket"); + var removeButton = document.getElementById("remove"); + + removeButton.disabled = bucketTree.view.selection.count == 0; +} + +function AbResultsPaneDoubleClick(card) +{ + AddCardIntoBucket(prefixTo, card); +} + +function OnClickedCard(card) +{ + // in the select address dialog, do nothing on click +} + +function UpdateCardView() +{ + // in the select address dialog, do nothing +} + +function DropRecipient(address) +{ + AddAddressFromComposeWindow(address, prefixTo); +} + +function OnReturnHit(event) +{ + if (event.keyCode == 13) { + var focusedElement = document.commandDispatcher.focusedElement; + if (focusedElement && (focusedElement.id == "addressBucket")) + return; + event.stopPropagation(); + if (focusedElement && (focusedElement.id == "abResultsTree")) + gActivatedButton.doCommand(); + } +} + + +function onEnterInSearchBar() +{ + var selectedNode = abList.selectedItem; + + if (!selectedNode) + return; + + if (!gQueryURIFormat) { + // Get model query from pref. We don't want the query starting with "?" + // as we have to prefix "?and" to this format. + gQueryURIFormat = getModelQuery("mail.addr_book.quicksearchquery.format"); + } + + var searchURI = selectedNode.value; + + // Use helper method to split up search query to multi-word search + // query against multiple fields. + let searchWords = getSearchTokens(gSearchInput.value); + searchURI += generateQueryURI(gQueryURIFormat, searchWords); + + SetAbView(searchURI); + + SelectFirstCard(); +} + +function DirPaneSelectionChangeMenulist() +{ + if (abList && abList.selectedItem) { + if (gSearchInput.value && (gSearchInput.value != "")) + onEnterInSearchBar(); + else + ChangeDirectoryByURI(abList.value); + } + + // Hide the addressbook column if the selected addressbook isn't + // "All address books". Since the column is redundant in all other cases. + let addrbookColumn = document.getElementById("addrbook"); + if (abList.value.startsWith(kAllDirectoryRoot + "?")) { + addrbookColumn.hidden = !gShowAbColumnInComposeSidebar; + addrbookColumn.removeAttribute("ignoreincolumnpicker"); + } else { + addrbookColumn.hidden = true; + addrbookColumn.setAttribute("ignoreincolumnpicker", "true"); + } +} diff --git a/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.xul b/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.xul new file mode 100644 index 0000000000..067139e7ec --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abSelectAddressesDialog.xul @@ -0,0 +1,92 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/addressbook/selectAddressesDialog.css" type="text/css"?> + +<?xul-overlay href="chrome://messenger/content/addressbook/abResultsPaneOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> + +<!DOCTYPE dialog [ +<!ENTITY % abSelectAddressesDialogDTD SYSTEM "chrome://messenger/locale/addressbook/abSelectAddressesDialog.dtd" > +%abSelectAddressesDialogDTD; +<!ENTITY % abResultsPaneOverlayDTD SYSTEM "chrome://messenger/locale/addressbook/abResultsPaneOverlay.dtd" > +%abResultsPaneOverlayDTD; +]> + +<dialog id="selectAddressesWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="&selectAddressWindow.title;" + style="width: 50em; height: 35em;" + persist="width height screenX screenY" + buttons="accept,cancel" + ondialogaccept="return SelectAddressOKButton();" + onload="OnLoadSelectAddress();" + onunload="OnUnloadSelectAddress();"> + + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> + <stringbundle id="bundle_composeMsgs" src="chrome://messenger/locale/messengercompose/composeMsgs.properties"/> + + <script src="chrome://messenger/content/addressbook/abCommon.js"/> + <script src="chrome://messenger/content/addressbook/abSelectAddressesDialog.js"/> + <script src="chrome://messenger/content/addressbook/abDragDrop.js"/> + <script src="chrome://messenger/content/messengercompose/MsgComposeCommands.js"/> + <script src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"/> + <script src="chrome://global/content/globalOverlay.js"/> + + <vbox flex="1"> + + <hbox id="topBox" align="center"> + <label value="&lookIn.label;" accesskey="&lookIn.accesskey;" control="addressbookList"/> + <menulist id="addressbookList" + oncommand="DirPaneSelectionChangeMenulist(); document.commandDispatcher.updateCommands('addrbook-select');"> + <menupopup id="addressbookList-menupopup" class="addrbooksPopup"/> + </menulist> + <label value="&for.label;" accesskey="&for.accesskey;" control="searchInput"/> + <textbox id="searchInput" flex="1" type="search" + aria-controls="abResultsTree" + placeholder="&for.placeholder;" + oncommand="onEnterInSearchBar();" clickSelectsAll="true"/> + </hbox> + + <hbox flex="1"> + + <vbox id="resultsBox" flex="4"> + <tree id="abResultsTree" flex="1" persist="height"/> + </vbox> + + <vbox id="addToBucketButtonBox"> + <spacer flex="1"/> + <button id="toButton" label="&toButton.label;" accesskey="&toButton.accesskey;" oncommand="SelectAddressToButton()"/> + <spacer class="middle-button-spacer"/> + <button id="ccButton" label="&ccButton.label;" accesskey="&ccButton.accesskey;" oncommand="SelectAddressCcButton()"/> + <spacer class="middle-button-spacer"/> + <button id="bccButton" label="&bccButton.label;" accesskey="&bccButton.accesskey;" oncommand="SelectAddressBccButton()"/> + <spacer class="above-remove-spacer"/> + <button id="remove" label="&removeButton.label;" accesskey="&removeButton.accesskey;" class="dialog" oncommand="RemoveSelectedFromBucket()"/> + <spacer flex="1"/> + </vbox> + + <vbox id="bucketBox" flex="1"> + <label value="&addressMessageTo.label;" control="addressBucket"/> + <tree id="addressBucket" flex="1" hidecolumnpicker="true" + ondragover="DragAddressOverTargetControl(event);" + ondrop="DropOnAddressingTarget(event, false);" + onselect="DialogBucketPaneSelectionChanged();"> + <treecols> + <treecol id="addressCol" flex="1" hideheader="true"/> + </treecols> + <treechildren id="bucketBody" flex="1"/> + </tree> + </vbox> + + </hbox> + + <hbox id="newEditButtonBox"> + <button id="new" label="&newButton.label;" accesskey="&newButton.accesskey;" tooltiptext="&addressPickerNewButton.tooltip;" oncommand="AbNewCard();"/> + <button id="edit" label="&editButton.label;" accesskey="&editButton.accesskey;" tooltiptext="&addressPickerEditButton.tooltip;" oncommand="AbEditSelectedCard()"/> + </hbox> + </vbox> + +</dialog> diff --git a/comm/suite/mailnews/components/addrbook/content/abTrees.js b/comm/suite/mailnews/components/addrbook/content/abTrees.js new file mode 100644 index 0000000000..ac52d3464d --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/abTrees.js @@ -0,0 +1,332 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file contains our implementation for various addressbook trees. It + * depends on jsTreeView.js being loaded before this script is loaded. + */ + +const {IOUtils} = ChromeUtils.import("resource:///modules/IOUtils.js"); + +// Tree Sort helper methods. +var AB_ORDER = ["aab", "pab", "mork", "ldap", "mapi+other", "anyab", "cab"]; + +function getDirectoryValue(aDir, aKey) { + if (aKey == "ab_type") { + if (aDir._directory.URI == kAllDirectoryRoot + "?") + return "aab"; + if (aDir._directory.URI == kPersonalAddressbookURI) + return "pab"; + if (aDir._directory.URI == kCollectedAddressbookURI) + return "cab"; + if (aDir._directory instanceof Ci.nsIAbMDBDirectory) + return "mork"; + if (aDir._directory instanceof Ci.nsIAbLDAPDirectory) + return "ldap"; + + // If there is any other AB type. + return "mapi+other"; + } else if (aKey == "ab_name") { + return aDir._directory.dirName; + } + + // This should never happen. + return null; +} + +function abNameCompare(a, b) { + return a.localeCompare(b); +} + +function abTypeCompare(a, b) { + return (AB_ORDER.indexOf(a) - AB_ORDER.indexOf(b)); +} + +var SORT_PRIORITY = ["ab_type", "ab_name"]; +var SORT_FUNCS = [abTypeCompare, abNameCompare]; + +function abSort(a, b) { + for (let i = 0; i < SORT_FUNCS.length; i++) { + let sortBy = SORT_PRIORITY[i]; + let aValue = getDirectoryValue(a, sortBy); + let bValue = getDirectoryValue(b, sortBy); + + if (!aValue && !bValue) + return 0; + if (!aValue) + return -1; + if (!bValue) + return 1; + if (aValue != bValue) { + let result = SORT_FUNCS[i](aValue, bValue); + + if (result != 0) + return result; + } + } + return 0; +} + +/** + * Each abDirTreeItem corresponds to one row in the tree view. + */ +function abDirTreeItem(aDirectory) +{ + this._directory = aDirectory; +} + +abDirTreeItem.prototype = +{ + getText: function atv_getText() + { + return this._directory.dirName; + }, + + get id() + { + return this._directory.URI; + }, + + _open: false, + get open() + { + return this._open; + }, + + _level: 0, + get level() + { + return this._level; + }, + + _children: null, + get children() + { + if (!this._children) + { + this._children = []; + let myEnum; + if (this._directory.URI == (kAllDirectoryRoot + "?")) + myEnum = MailServices.ab.directories; + else + myEnum = this._directory.childNodes; + + while (myEnum.hasMoreElements()) + { + var abItem = new abDirTreeItem( + myEnum.getNext().QueryInterface(Ci.nsIAbDirectory)); + if (gDirectoryTreeView&& + this.id == kAllDirectoryRoot + "?" && + getDirectoryValue(abItem, "ab_type") == "ldap") + gDirectoryTreeView.hasRemoteAB = true; + + abItem._level = this._level + 1; + abItem._parent = this; + this._children.push(abItem); + } + + this._children.sort(abSort); + } + return this._children; + }, + + getProperties: function atv_getProps() + { + var properties = [] + if (this._directory.isMailList) + properties.push("IsMailList-true"); + if (this._directory.isRemote) + properties.push("IsRemote-true"); + if (this._directory.isSecure) + properties.push("IsSecure-true"); + return properties.join(" "); + } +}; + +/** + * Our actual implementation of nsITreeView. + */ +function directoryTreeView() {} +directoryTreeView.prototype = +{ + __proto__: new PROTO_TREE_VIEW(), + + hasRemoteAB: false, + + init: function dtv_init(aTree, aJSONFile) + { + if (aJSONFile) { + // Parse our persistent-open-state json file + let data = IOUtils.loadFileToString(aJSONFile); + if (data) { + this._persistOpenMap = JSON.parse(data); + } + } + + this._rebuild(); + aTree.view = this; + }, + + shutdown: function dtv_shutdown(aJSONFile) + { + // Write out the persistOpenMap to our JSON file. + if (aJSONFile) + { + // Write out our json file... + let data = JSON.stringify(this._persistOpenMap); + IOUtils.saveStringToFile(aJSONFile, data); + } + }, + + // Override the dnd methods for those functions in abDragDrop.js + canDrop: function dtv_canDrop(aIndex, aOrientation, dataTransfer) + { + return abDirTreeObserver.canDrop(aIndex, aOrientation, dataTransfer); + }, + + drop: function dtv_drop(aRow, aOrientation, dataTransfer) + { + abDirTreeObserver.onDrop(aRow, aOrientation, dataTransfer); + }, + + getDirectoryAtIndex: function dtv_getDirForIndex(aIndex) + { + return this._rowMap[aIndex]._directory; + }, + + getIndexOfDirectory: function dtv_getIndexOfDir(aItem) + { + for (var i = 0; i < this._rowMap.length; i++) + if (this._rowMap[i]._directory == aItem) + return i; + + return -1; + }, + + // Override jsTreeView's isContainer, since we want to be able + // to react to drag-drop events for all items in the directory + // tree. + isContainer: function dtv_isContainer(aIndex) + { + return true; + }, + + /** + * NOTE: This function will result in indeterminate rows being selected. + * Callers should take care to re-select a desired row after calling + * this function. + */ + _rebuild: function dtv__rebuild() { + this._rowMap = []; + + // Make an entry for All Address Books. + let rootAB = MailServices.ab.getDirectory(kAllDirectoryRoot + "?"); + rootAB.dirName = gAddressBookBundle.getString("allAddressBooks"); + this._rowMap.push(new abDirTreeItem(rootAB)); + + // Sort our addressbooks now. + this._rowMap.sort(abSort); + + this._restoreOpenStates(); + }, + + getIndexForId: function(aId) { + for (let i = 0; i < this._rowMap.length; i++) { + if (this._rowMap[i].id == aId) + return i; + } + + return -1; + }, + + // nsIAbListener interfaces + onItemAdded: function dtv_onItemAdded(aParent, aItem) + { + if (!(aItem instanceof Ci.nsIAbDirectory)) + return; + + var oldCount = this._rowMap.length; + var tree = this._tree; + this._tree = null; + this._rebuild(); + if (!tree) + return; + + this._tree = tree; + var itemIndex = this.getIndexOfDirectory(aItem); + tree.rowCountChanged(itemIndex, this._rowMap.length - oldCount); + var parentIndex = this.getIndexOfDirectory(aParent); + if (parentIndex > -1) + tree.invalidateRow(parentIndex); + }, + + onItemRemoved: function dtv_onItemRemoved(aParent, aItem) + { + if (!(aItem instanceof Ci.nsIAbDirectory)) + return; + + var itemIndex = this.getIndexOfDirectory(aItem); + var oldCount = this._rowMap.length; + var tree = this._tree; + this._tree = null; + this._rebuild(); + if (!tree) + return; + + this._tree = tree; + tree.rowCountChanged(itemIndex, this._rowMap.length - oldCount); + + // This does not currently work, see Bug 1323563. + // If we're deleting a top-level address-book, just select the first book. + // if (aParent.URI == kAllDirectoryRoot || + // aParent.URI == kAllDirectoryRoot + "?") { + // this.selection.select(0); + // return; + // } + + var parentIndex = this.getIndexOfDirectory(aParent); + if (parentIndex > -1) + tree.invalidateRow(parentIndex); + + if (!this.selection.count) + { + // The previously selected item was a member of the deleted subtree. + // Select the parent of the subtree. + // If there is no parent, select the next item. + // If there is no next item, select the first item. + var newIndex = parentIndex; + if (newIndex < 0) + newIndex = itemIndex; + if (newIndex >= this._rowMap.length) + newIndex = 0; + + this.selection.select(newIndex); + } + }, + + onItemPropertyChanged: function dtv_onItemProp(aItem, aProp, aOld, aNew) + { + if (!(aItem instanceof Ci.nsIAbDirectory)) + return; + + var index = this.getIndexOfDirectory(aItem); + var current = this.getDirectoryAtIndex(this.selection.currentIndex); + var tree = this._tree; + this._tree = null; + this._rebuild(); + this._tree = tree; + this.selection.select(this.getIndexOfDirectory(current)); + + if (index > -1) { + var newIndex = this.getIndexOfDirectory(aItem); + if (newIndex >= index) + this._tree.invalidateRange(index, newIndex); + else + this._tree.invalidateRange(newIndex, index); + } + } +}; + +var gDirectoryTreeView = new directoryTreeView(); diff --git a/comm/suite/mailnews/components/addrbook/content/addressbook-panel.js b/comm/suite/mailnews/components/addrbook/content/addressbook-panel.js new file mode 100644 index 0000000000..9fa0a125e0 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/addressbook-panel.js @@ -0,0 +1,119 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gIsMsgCompose = false; + +function GetAbViewListener() +{ + // the ab panel doesn't care if the total changes, or if the selection changes + return null; +} + +var mutationObs = null; + +function AbPanelLoad() +{ + InitCommonJS(); + + UpgradeAddressBookResultsPaneUI("mailnews.ui.addressbook_panel_results.version"); + + var abPopup = document.getElementById('addressbookList'); + + // Reselect the persisted address book if possible, if not just select the + // first in the list. + var temp = abPopup.value; + abPopup.selectedItem = null; + abPopup.value = temp; + if (!abPopup.selectedItem) + abPopup.selectedIndex = 0; + + ChangeDirectoryByURI(abPopup.value); + + mutationObs = new MutationObserver(function(aMutations) { + aMutations.forEach(function(mutation) { + if (getSelectedDirectoryURI() == (kAllDirectoryRoot + "?") && + mutation.type == "attributes" && + mutation.attributeName == "hidden") { + let curState = document.getElementById("addrbook").hidden; + gShowAbColumnInComposeSidebar = !curState; + } + }); + }); + + document.getElementById("addrbook").hidden = !gShowAbColumnInComposeSidebar; + + mutationObs.observe(document.getElementById("addrbook"), + { attributes: true, childList: true }); + + gSearchInput = document.getElementById("searchInput"); + + // for the compose window we want to show To, Cc, Bcc and a separator + // for all other windows we want to show Compose Mail To + var popup = document.getElementById("composeMail"); + gIsMsgCompose = parent.document + .documentElement + .getAttribute("windowtype") == "msgcompose"; + for (var i = 0; i < 4; i++) + popup.childNodes[i].hidden = !gIsMsgCompose; + popup.childNodes[4].hidden = gIsMsgCompose; +} + +function AbPanelUnload() +{ + mutationObs.disconnect(); + + CloseAbView(); +} + +function AbPanelAdd(addrtype) +{ + var cards = GetSelectedAbCards(); + var count = cards.length; + + for (var i = 0; i < count; i++) { + // turn each card into a properly formatted address + var address = GenerateAddressFromCard(cards[i]); + if (address) + top.awAddRecipient(addrtype, address); + else + Services.prompt.alert(window, + gAddressBookBundle.getString("emptyEmailAddCardTitle"), + gAddressBookBundle.getString("emptyEmailAddCard")); + } +} + +function AbPanelNewCard() +{ + goNewCardDialog(abList.value); +} + +function AbPanelNewList() +{ + goNewListDialog(abList.value); +} + +function ResultsPaneSelectionChanged() +{ + // do nothing for ab panel +} + +function OnClickedCard() +{ + // do nothing for ab panel +} + +function AbResultsPaneDoubleClick(card) +{ + // double click for ab panel means "send mail to this person / list" + if (gIsMsgCompose) + AbPanelAdd('addr_to'); + else + AbNewMessage(); +} + +function UpdateCardView() +{ + // do nothing for ab panel +} diff --git a/comm/suite/mailnews/components/addrbook/content/addressbook-panel.xul b/comm/suite/mailnews/components/addrbook/content/addressbook-panel.xul new file mode 100644 index 0000000000..72f650ffdb --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/addressbook-panel.xul @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<!-- -*- Mode: xml; indent-tabs-mode: nil; -*- + - This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/addressbook/sidebarPanel.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/addressbook/addressPanes.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/addressbook/abResultsPane.css" type="text/css"?> + +<!DOCTYPE page [ +<!ENTITY % abSelectAddressesDialogDTD SYSTEM "chrome://messenger/locale/addressbook/abSelectAddressesDialog.dtd" > +%abSelectAddressesDialogDTD; +<!ENTITY % abResultsPaneOverlayDTD SYSTEM "chrome://messenger/locale/addressbook/abResultsPaneOverlay.dtd" > +%abResultsPaneOverlayDTD; +]> + +<page id="addressbook-panel" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="AbPanelLoad();" + onunload="AbPanelUnload();" + title="&selectAddressWindow.title;" + selectedaddresses="true"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> + </stringbundleset> + + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://global/content/nsDragAndDrop.js"/> + <script src="chrome://messenger/content/addressbook/addressbook.js"/> + <script src="chrome://messenger/content/addressbook/abCommon.js"/> + <script src="chrome://messenger/content/addressbook/abDragDrop.js"/> + <script src="chrome://messenger/content/addressbook/abResultsPane.js"/> + <script src="chrome://messenger/content/addressbook/abSelectAddressesDialog.js"/> + <script src="chrome://messenger/content/addressbook/addressbook-panel.js"/> + <script src="chrome://communicator/content/utilityOverlay.js"/> + + <commandset id="addressbook-panel-commandset"> + <command id="cmd_newlist" oncommand="AbNewList();"/> + <command id="cmd_properties" oncommand="goDoCommand('cmd_properties');"/> + </commandset> + + <menupopup id="composeMail" onpopupshowing="CommandUpdate_AddressBook();"> + <menuitem label="&toButton.label;" accesskey="&toButton.accesskey;" oncommand="AbPanelAdd('addr_to');" default="true"/> + <menuitem label="&ccButton.label;" accesskey="&ccButton.accesskey;" oncommand="AbPanelAdd('addr_cc');"/> + <menuitem label="&bccButton.label;" accesskey="&bccButton.accesskey;" oncommand="AbPanelAdd('addr_bcc');"/> + <menuseparator/> + <menuitem label="&composeEmail.label;" accesskey="&composeEmail.accesskey;" oncommand="AbNewMessage();" default="true"/> + <menuitem label="©Address.label;" accesskey="©Address.accesskey;" oncommand="AbCopyAddress();"/> + <menuitem label="&deleteAddrBookCard.label;" accesskey="&deleteAddrBookCard.accesskey;" oncommand="AbDelete();"/> + <menuseparator/> + <menuitem label="&newAddrBookCard.label;" accesskey="&newAddrBookCard.accesskey;" oncommand="AbPanelNewCard();"/> + <menuitem label="&newAddrBookMailingList.label;" accesskey="&newAddrBookMailingList.accesskey;" command="cmd_newlist"/> + <menuseparator/> + <menuitem label="&addrBookCardProperties.label;" accesskey="&addrBookCardProperties.accesskey;" command="cmd_properties"/> + </menupopup> + <vbox id="results_box" flex="1"> + <hbox id="panel-bar" class="toolbar" align="center"> + <label value="&lookIn.label;" control="addressbookList" id="lookInLabel"/> + <menulist id="addressbookList" + oncommand="DirPaneSelectionChangeMenulist();" flex="1" + persist="value"> + <menupopup id="addressbookList-menupopup" class="addrbooksPopup"/> + </menulist> + </hbox> + <hbox align="center"> + <label value="&for.label;" id="forLabel" control="searchInput"/> + <textbox id="searchInput" flex="1" type="search" + aria-labelledby="lookInLabel addressbookList forLabel" + aria-controls="abResultsTree" + placeholder="&for.placeholder;" + oncommand="onEnterInSearchBar();" clickSelectsAll="true"/> + </hbox> + + <tree id="abResultsTree" flex="1" context="composeMail" onclick="AbResultsPaneOnClick(event);" class="plain" + sortCol="GeneratedName" persist="sortCol"> + <treecols> + <!-- these column ids must match up to the mork column names, see nsIAddrDatabase.idl --> + <treecol id="GeneratedName" + persist="hidden ordinal width sortDirection" flex="1" label="&GeneratedName.label;" primary="true"/> + <splitter class="tree-splitter"/> + <treecol id="addrbook" + persist="hidden ordinal width sortDirection" hidden="true" + flex="1" label="&Addrbook.label;"/> + <splitter class="tree-splitter"/> + <treecol id="PrimaryEmail" + persist="hidden ordinal width sortDirection" + hiddenbydefault="true" + flex="1" label="&PrimaryEmail.label;"/> + </treecols> + <treechildren ondragstart="nsDragAndDrop.startDrag(event, abResultsPaneObserver);"/> +</tree> + + </vbox> +</page> diff --git a/comm/suite/mailnews/components/addrbook/content/addressbook.js b/comm/suite/mailnews/components/addrbook/content/addressbook.js new file mode 100644 index 0000000000..e1bce6a868 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/addressbook.js @@ -0,0 +1,581 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const {encodeABTermValue, getModelQuery} = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm"); +const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm"); +const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm"); +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +const nsIAbListener = Ci.nsIAbListener; +const kPrefMailAddrBookLastNameFirst = "mail.addr_book.lastnamefirst"; +const kPersistCollapseMapStorage = "directoryTree.json"; + +var gSearchTimer = null; +var gStatusText = null; +var gQueryURIFormat = null; +var gSearchInput; +var gSearchBox; +var gCardViewBox; +var gCardViewBoxEmail1; + +var msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"] + .createInstance(Ci.nsIMsgWindow); + +// Constants that correspond to choices +// in Address Book->View -->Show Name as +const kDisplayName = 0; +const kLastNameFirst = 1; +const kFirstNameFirst = 2; +const kLDAPDirectory = 0; // defined in nsDirPrefs.h +const kPABDirectory = 2; // defined in nsDirPrefs.h + +function OnUnloadAddressBook() +{ + MailServices.ab.removeAddressBookListener(gDirectoryTreeView); + + // Shutdown the tree view - this will also save the open/collapsed + // state of the tree view to a JSON file. + gDirectoryTreeView.shutdown(kPersistCollapseMapStorage); + + MailServices.mailSession.RemoveMsgWindow(msgWindow); + + CloseAbView(); +} + +var gAddressBookAbViewListener = { + onSelectionChanged: function() { + ResultsPaneSelectionChanged(); + }, + onCountChanged: function(total) { + SetStatusText(total); + } +}; + +function GetAbViewListener() +{ + return gAddressBookAbViewListener; +} + +function OnLoadAddressBook() +{ + gSearchInput = document.getElementById("searchInput"); + + verifyAccounts(null, false); // this will do migration, if we need to. + + InitCommonJS(); + + UpgradeAddressBookResultsPaneUI("mailnews.ui.addressbook_results.version"); + + GetCurrentPrefs(); + + // FIX ME - later we will be able to use onload from the overlay + OnLoadCardView(); + + // Before and after callbacks for the customizeToolbar code + var abToolbox = getAbToolbox(); + abToolbox.customizeInit = AbToolboxCustomizeInit; + abToolbox.customizeDone = AbToolboxCustomizeDone; + abToolbox.customizeChange = AbToolboxCustomizeChange; + + // Initialize the Address Book tree view + gDirectoryTreeView.init(gDirTree, kPersistCollapseMapStorage); + + SelectFirstAddressBook(); + + // if the pref is locked disable the menuitem New->LDAP directory + if (Services.prefs.prefIsLocked("ldap_2.disable_button_add")) + document.getElementById("addLDAP").setAttribute("disabled", "true"); + + // Add a listener, so we can switch directories if the current directory is + // deleted. This listener cares when a directory (= address book), or a + // directory item is/are removed. In the case of directory items, we are + // only really interested in mailing list changes and not cards but we have + // to have both. + MailServices.ab.addAddressBookListener(gDirectoryTreeView, nsIAbListener.all); + + gDirTree.controllers.appendController(DirPaneController); + + // Ensure we don't load xul error pages into the main window + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .useErrorPages = false; + + MailServices.mailSession.AddMsgWindow(msgWindow); +} + +function GetCurrentPrefs() +{ + // check "Show Name As" menu item based on pref + var menuitemID; + switch (Services.prefs.getIntPref(kPrefMailAddrBookLastNameFirst)) + { + case kFirstNameFirst: + menuitemID = 'firstLastCmd'; + break; + case kLastNameFirst: + menuitemID = 'lastFirstCmd'; + break; + case kDisplayName: + default: + menuitemID = 'displayNameCmd'; + break; + } + + var menuitem = top.document.getElementById(menuitemID); + if ( menuitem ) + menuitem.setAttribute('checked', 'true'); + + // show phonetic fields if indicated by the pref + if (GetLocalizedStringPref("mail.addr_book.show_phonetic_fields") == "true") + document.getElementById("cmd_SortBy_PhoneticName") + .setAttribute("hidden", "false"); +} + +function SetNameColumn(cmd) +{ + var prefValue; + + switch (cmd) + { + case 'firstLastCmd': + prefValue = kFirstNameFirst; + break; + case 'lastFirstCmd': + prefValue = kLastNameFirst; + break; + case 'displayNameCmd': + prefValue = kDisplayName; + break; + } + + Services.prefs.setIntPref(kPrefMailAddrBookLastNameFirst, prefValue); +} + +function CommandUpdate_AddressBook() +{ + goUpdateCommand('cmd_delete'); + goUpdateCommand('button_delete'); + goUpdateCommand('cmd_printcardpreview'); + goUpdateCommand('cmd_printcard'); + goUpdateCommand('cmd_print'); + goUpdateCommand('cmd_properties'); + goUpdateCommand('cmd_newlist'); +} + +function ResultsPaneSelectionChanged() +{ + UpdateCardView(); +} + +function UpdateCardView() +{ + var cards = GetSelectedAbCards(); + + // display the selected card, if exactly one card is selected. + // either no cards, or more than one card is selected, clear the pane. + if (cards.length == 1) + OnClickedCard(cards[0]) + else + ClearCardViewPane(); +} + +function OnClickedCard(card) +{ + if (card) + DisplayCardViewPane(card); + else + ClearCardViewPane(); +} + +function AbClose() +{ + top.close(); +} + +function AbPrintCardInternal(doPrintPreview, msgType) +{ + var selectedItems = GetSelectedAbCards(); + var numSelected = selectedItems.length; + + if (!numSelected) + return; + + let statusFeedback; + statusFeedback = Cc["@mozilla.org/messenger/statusfeedback;1"].createInstance(); + statusFeedback = statusFeedback.QueryInterface(Ci.nsIMsgStatusFeedback); + + let selectionArray = []; + + for (let i = 0; i < numSelected; i++) { + let card = selectedItems[i]; + let printCardUrl = CreatePrintCardUrl(card); + if (printCardUrl) { + selectionArray.push(printCardUrl); + } + } + + printEngineWindow = window.openDialog("chrome://messenger/content/msgPrintEngine.xul", + "", + "chrome,dialog=no,all", + selectionArray.length, selectionArray, + statusFeedback, doPrintPreview, msgType); + + return; +} + +function AbPrintCard() +{ + AbPrintCardInternal(false, Ci.nsIMsgPrintEngine.MNAB_PRINT_AB_CARD); +} + +function AbPrintPreviewCard() +{ + AbPrintCardInternal(true, Ci.nsIMsgPrintEngine.MNAB_PRINTPREVIEW_AB_CARD); +} + +function CreatePrintCardUrl(card) +{ + return "data:application/xml;base64," + card.translateTo("base64xml"); +} + +function AbPrintAddressBookInternal(doPrintPreview, msgType) +{ + let uri = getSelectedDirectoryURI(); + if (!uri) + return; + + var statusFeedback; + statusFeedback = Cc["@mozilla.org/messenger/statusfeedback;1"].createInstance(); + statusFeedback = statusFeedback.QueryInterface(Ci.nsIMsgStatusFeedback); + + /* + turn "moz-abmdbdirectory://abook.mab" into + "addbook://moz-abmdbdirectory/abook.mab?action=print" + */ + + var abURIArr = uri.split("://"); + var printUrl = "addbook://" + abURIArr[0] + "/" + abURIArr[1] + "?action=print" + + printEngineWindow = window.openDialog("chrome://messenger/content/msgPrintEngine.xul", + "", + "chrome,dialog=no,all", + 1, [printUrl], + statusFeedback, doPrintPreview, msgType); + + return; +} + +function AbPrintAddressBook() +{ + AbPrintAddressBookInternal(false, Ci.nsIMsgPrintEngine.MNAB_PRINT_ADDRBOOK); +} + +function AbPrintPreviewAddressBook() +{ + AbPrintAddressBookInternal(true, Ci.nsIMsgPrintEngine.MNAB_PRINTPREVIEW_ADDRBOOK); +} + +/** + * Export the currently selected addressbook. + */ +function AbExportSelection() { + let selectedDirURI = getSelectedDirectoryURI(); + if (!selectedDirURI) + return; + + if (selectedDirURI == (kAllDirectoryRoot + "?")) + AbExportAll(); + else + AbExport(selectedDirURI); +} + +/** + * Export all found addressbooks, each in a separate file. + */ +function AbExportAll() +{ + let directories = MailServices.ab.directories; + + while (directories.hasMoreElements()) { + let directory = directories.getNext(); + // Do not export LDAP ABs. + if (directory.URI.startsWith(kLdapUrlPrefix)) + continue; + + AbExport(directory.URI); + } +} + +/** + * Export the specified addressbook to a file. + * + * @param aSelectedDirURI The URI of the addressbook to export. + */ +function AbExport(aSelectedDirURI) +{ + if (!aSelectedDirURI) + return; + + try { + let directory = GetDirectoryFromURI(aSelectedDirURI); + MailServices.ab.exportAddressBook(window, directory); + } + catch (ex) { + var message; + switch (ex.result) { + case Cr.NS_ERROR_FILE_ACCESS_DENIED: + message = gAddressBookBundle.getString("failedToExportMessageFileAccessDenied"); + break; + case Cr.NS_ERROR_FILE_NO_DEVICE_SPACE: + message = gAddressBookBundle.getString("failedToExportMessageNoDeviceSpace"); + break; + default: + message = ex.message; + break; + } + + Services.prompt.alert(window, + gAddressBookBundle.getString("failedToExportTitle"), + message); + } +} + +function SetStatusText(total) +{ + if (!gStatusText) + gStatusText = document.getElementById('statusText'); + + try { + let statusText; + + if (gSearchInput.value) { + if (total == 0) { + statusText = gAddressBookBundle.getString("noMatchFound"); + } else { + statusText = PluralForm + .get(total, gAddressBookBundle.getString("matchesFound1")) + .replace("#1", total); + } + } + else + statusText = + gAddressBookBundle.getFormattedString( + "totalContactStatus", + [getSelectedDirectory().dirName, total]); + + gStatusText.setAttribute("label", statusText); + } + catch(ex) { + dump("failed to set status text: " + ex + "\n"); + } +} + +function AbResultsPaneDoubleClick(card) +{ + AbEditCard(card); +} + +function onAdvancedAbSearch() +{ + let selectedDirURI = getSelectedDirectoryURI(); + if (!selectedDirURI) + return; + + var existingSearchWindow = Services.wm.getMostRecentWindow("mailnews:absearch"); + if (existingSearchWindow) + existingSearchWindow.focus(); + else + window.openDialog("chrome://messenger/content/ABSearchDialog.xul", "", + "chrome,resizable,status,centerscreen,dialog=no", + {directory: selectedDirURI}); +} + +function onEnterInSearchBar() +{ + ClearCardViewPane(); + if (!gQueryURIFormat) { + // Get model query from pref. We don't want the query starting with "?" + // as we have to prefix "?and" to this format. + gQueryURIFormat = getModelQuery("mail.addr_book.quicksearchquery.format"); + } + + let searchURI = getSelectedDirectoryURI(); + if (!searchURI) return; + + /* + XXX todo, handle the case where the LDAP url + already has a query, like + moz-abldapdirectory://nsdirectory.netscape.com:389/ou=People,dc=netscape,dc=com?(or(Department,=,Applications)) + */ + // Use helper method to split up search query to multi-word search + // query against multiple fields. + let searchWords = getSearchTokens(gSearchInput.value); + searchURI += generateQueryURI(gQueryURIFormat, searchWords); + + if (searchURI == kAllDirectoryRoot) + searchURI += "?"; + + document.getElementById("localResultsOnlyMessage") + .setAttribute("hidden", + !gDirectoryTreeView.hasRemoteAB || + searchURI != kAllDirectoryRoot + "?"); + + SetAbView(searchURI); + + // XXX todo + // this works for synchronous searches of local addressbooks, + // but not for LDAP searches + SelectFirstCard(); +} + +function SwitchPaneFocus(event) +{ + var focusedElement = WhichPaneHasFocus(); + var cardViewBox = GetCardViewBox(); + var cardViewBoxEmail1 = GetCardViewBoxEmail1(); + var searchBox = GetSearchBox(); + var dirTree = GetDirTree(); + + if (event && event.shiftKey) + { + if (focusedElement == gAbResultsTree && searchBox.getAttribute('hidden') != 'true') + searchInput.focus(); + else if ((focusedElement == gAbResultsTree || focusedElement == searchBox) && !IsDirPaneCollapsed()) + dirTree.focus(); + else if (focusedElement != cardViewBox && !IsCardViewAndAbResultsPaneSplitterCollapsed()) + { + if(cardViewBoxEmail1) + cardViewBoxEmail1.focus(); + else + cardViewBox.focus(); + } + else + gAbResultsTree.focus(); + } + else + { + if (focusedElement == searchBox) + gAbResultsTree.focus(); + else if (focusedElement == gAbResultsTree && !IsCardViewAndAbResultsPaneSplitterCollapsed()) + { + if(cardViewBoxEmail1) + cardViewBoxEmail1.focus(); + else + cardViewBox.focus(); + } + else if (focusedElement != dirTree && !IsDirPaneCollapsed()) + dirTree.focus(); + else if (searchBox.getAttribute('hidden') != 'true') + gSearchInput.focus(); + else + gAbResultsTree.focus(); + } +} + +function WhichPaneHasFocus() +{ + var cardViewBox = GetCardViewBox(); + var searchBox = GetSearchBox(); + var dirTree = GetDirTree(); + + var currentNode = top.document.commandDispatcher.focusedElement; + while (currentNode) + { + var nodeId = currentNode.getAttribute('id'); + + if(currentNode == gAbResultsTree || + currentNode == cardViewBox || + currentNode == searchBox || + currentNode == dirTree) + return currentNode; + + currentNode = currentNode.parentNode; + } + + return null; +} + +function GetDirTree() +{ + if (!gDirTree) + gDirTree = document.getElementById('dirTree'); + return gDirTree; +} + +function GetSearchBox() +{ + if (!gSearchBox) + gSearchBox = document.getElementById('searchBox'); + return gSearchBox; +} + +function GetCardViewBox() +{ + if (!gCardViewBox) + gCardViewBox = document.getElementById('CardViewBox'); + return gCardViewBox; +} + +function GetCardViewBoxEmail1() +{ + if (!gCardViewBoxEmail1) + { + try { + gCardViewBoxEmail1 = document.getElementById('cvEmail1'); + } + catch (ex) { + gCardViewBoxEmail1 = null; + } + } + return gCardViewBoxEmail1; +} + +function IsDirPaneCollapsed() +{ + var dirPaneBox = GetDirTree().parentNode; + return dirPaneBox.getAttribute("collapsed") == "true" || + dirPaneBox.getAttribute("hidden") == "true"; +} + +function IsCardViewAndAbResultsPaneSplitterCollapsed() +{ + var cardViewBox = document.getElementById('CardViewOuterBox'); + try { + return (cardViewBox.getAttribute("collapsed") == "true"); + } + catch (ex) { + return false; + } +} + +function LaunchUrl(url) +{ + // Doesn't matter if this bit fails, window.location contains its own prompts + try { + window.location = url; + } + catch (ex) {} +} + +function getAbToolbox() +{ + return document.getElementById("ab-toolbox"); +} + +function AbToolboxCustomizeInit() +{ + toolboxCustomizeInit("ab-menubar"); +} + +function AbToolboxCustomizeDone(aToolboxChanged) +{ + toolboxCustomizeDone("ab-menubar", getAbToolbox(), aToolboxChanged); +} + +function AbToolboxCustomizeChange(event) +{ + toolboxCustomizeChange(getAbToolbox(), event); +} diff --git a/comm/suite/mailnews/components/addrbook/content/addressbook.xul b/comm/suite/mailnews/components/addrbook/content/addressbook.xul new file mode 100644 index 0000000000..698fbe62c5 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/addressbook.xul @@ -0,0 +1,722 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/addressbook/addressbook.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/addressbook/addressPanes.css" + type="text/css"?> + +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/contentAreaContextOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/addressbook/abResultsPaneOverlay.xul"?> + +<!DOCTYPE window [ +<!ENTITY % abMainWindowDTD SYSTEM "chrome://messenger/locale/addressbook/abMainWindow.dtd" > +%abMainWindowDTD; +<!ENTITY % abResultsPaneOverlayDTD SYSTEM "chrome://messenger/locale/addressbook/abResultsPaneOverlay.dtd" > +%abResultsPaneOverlayDTD; +<!ENTITY % mailOverlayDTD SYSTEM "chrome://messenger/locale/mailOverlay.dtd"> +%mailOverlayDTD; +<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" > +%messengerDTD; +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +]> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:html="http://www.w3.org/1999/xhtml" + id="addressbookWindow" + height="450" + width="750" + title="&addressbookWindow.title;" + lightweightthemes="true" + lightweightthemesfooter="status-bar" + windowtype="mail:addressbook" + macanimationtype="document" + drawtitle="true" + persist="width height screenX screenY sizemode" + toggletoolbar="true" + onload="OnLoadAddressBook()" + onunload="OnUnloadAddressBook()"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/> + </stringbundleset> + +<script src="chrome://messenger/content/jsTreeView.js"/> +<script src="chrome://messenger/content/addressbook/abTrees.js"/> +<script src="chrome://messenger/content/accountUtils.js"/> +<script src="chrome://messenger/content/addressbook/addressbook.js"/> +<script src="chrome://messenger/content/addressbook/abCommon.js"/> +<script src="chrome://communicator/content/contentAreaClick.js"/> +<script src="chrome://global/content/printUtils.js"/> +<script src="chrome://messenger/content/msgPrintEngine.js"/> +<script src="chrome://messenger/content/addressbook/abCardViewOverlay.js"/> + + <commandset id="addressBook"> + <commandset id="CommandUpdate_AddressBook" + commandupdater="true" + events="focus,addrbook-select" + oncommandupdate="CommandUpdate_AddressBook()"/> + <commandset id="selectEditMenuItems"/> + <commandset id="undoEditMenuItems"/> + <commandset id="globalEditMenuItems"/> + <command id="cmd_newNavigator"/> + <command id="cmd_newPrivateWindow"/> + <command id="cmd_newEditor"/> + <command id="cmd_newlist" oncommand="AbNewList();"/> + <command id="cmd_newCard" oncommand="goDoCommand('cmd_newCard');"/> + <command id="cmd_newMessage" oncommand="AbNewMessage();"/> + <command id="cmd_printSetup" oncommand="PrintUtils.showPageSetup()"/> + <command id="cmd_print" oncommand="AbPrintCard();"/> + <command id="cmd_printpreview" oncommand="AbPrintPreviewCard();"/> + <command id="cmd_printcard" oncommand="AbPrintCard()"/> + <command id="cmd_printcardpreview" oncommand="AbPrintPreviewCard()"/> + <command id="cmd_printAddressBook" oncommand="AbPrintAddressBook()"/> + <command id="cmd_printPreviewAddressBook" oncommand="AbPrintPreviewAddressBook()"/> + <command id="cmd_close" oncommand="AbClose()"/> + <command id="cmd_properties" oncommand="goDoCommand('cmd_properties');"/> + <command id="cmd_undo"/> + <command id="cmd_redo"/> + <command id="cmd_copy"/> + <command id="cmd_paste"/> + <command id="cmd_cut"/> + <command id="cmd_delete" + valueAddressBook="&deleteAbCmd.label;" + valueCard="&deleteContactCmd.label;" + valueCards="&deleteContactsCmd.label;" + valueList="&deleteListCmd.label;" + valueLists="&deleteListsCmd.label;" + valueItems="&deleteItemsCmd.label;"/> + <command id="cmd_selectAll"/> + <command id="button_delete" oncommand="goDoCommand('button_delete');"/> + <command id="cmd_swapFirstNameLastName" oncommand="AbSwapFirstNameLastName()"/> + <commandset id="tasksCommands"/> + </commandset> + +<broadcasterset id="abBroadcasters"> + <broadcaster id="Communicator:WorkMode"/> +</broadcasterset> + +<broadcasterset id="mainBroadcasterSet"/> + +<keyset id="tasksKeys"> + <!-- File Menu --> + <key id="key_newNavigator"/> + <key id="key_newPrivateWindow"/> + <key id="key_newBlankPage"/> +#ifdef XP_MACOSX + <key id="key_newMessage" key="&newMessageCmd.key;" + modifiers="accel,shift" command="cmd_newMessage"/> +#else + <key id="key_newMessage" key="&newMessageCmd.key;" + modifiers="accel" command="cmd_newMessage"/> +#endif + <key id="key_newCard" key="&newContact.key;" modifiers="accel" + command="cmd_newCard"/> + <key id="key_printCard" key="&printContactViewCmd.key;" + command="cmd_printcard" modifiers="accel"/> + <key id="key_close"/> + <!-- Edit Menu --> + <key id="key_delete"/> + <key id="key_delete2"/> <!-- secondary delete key --> + <key id="key_undo"/> + <key id="key_redo"/> + <key id="key_cut"/> + <key id="key_copy"/> + <key id="key_paste"/> + <key id="key_selectAll"/> + <key id="key_properties" key="&propertiesCmd.key;" command="cmd_properties" modifiers="accel"/> + + <!-- View Menu --> +#ifndef XP_MACOSX + <key id="key_toggleDirectoryPane" + keycode="VK_F9" + oncommand="togglePaneSplitter('dirTree-splitter');"/> +#else + <key id="key_toggleDirectoryPane" + key="&toggleDirectoryPaneCmd.key;" + modifiers="accel,alt" + oncommand="togglePaneSplitter('dirTree-splitter');"/> +#endif + + <!-- Tab/F6 Keys --> + <key keycode="VK_TAB" oncommand="SwitchPaneFocus(event);" modifiers="control,shift"/> + <key keycode="VK_TAB" oncommand="SwitchPaneFocus(event);" modifiers="control"/> + <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="control,shift"/> + <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="control"/> + <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);" modifiers="shift"/> + <key keycode="VK_F6" oncommand="SwitchPaneFocus(event);"/> + + <!-- Search field --> + <key key="&focusSearchInput.key;" + modifiers="accel" + oncommand="focusElement(document.getElementById('searchInput'));"/> + +</keyset> + +<menupopup id="dirTreeContext"> + <menuitem id="dirTreeContext-properties" + label="&editItemButton.label;" + accesskey="&editItemButton.accesskey;" + command="cmd_properties"/> + <menuseparator/> + <menuitem id="dirTreeContext-newcard" + label="&newContactButton.label;" + accesskey="&newContactButton.accesskey;" + command="cmd_newCard"/> + <menuitem id="dirTreeContext-newlist" + label="&newlistButton.label;" + accesskey="&newlistButton.accesskey;" + command="cmd_newlist"/> + <menuseparator/> + <menuitem id="dirTreeContext-print" + label="&printButton.label;" + accesskey="&printButton.accesskey;" + command="cmd_printAddressBook"/> + <menuitem id="dirTreeContext-delete" + label="&deleteItemButton.label;" + accesskey="&deleteItemButton.accesskey;" + command="button_delete"/> +</menupopup> + +<menupopup id="abResultsTreeContext"> + <menuitem id="abResultsTreeContext-properties" + label="&editItemButton.label;" + accesskey="&editItemButton.accesskey;" + command="cmd_properties"/> + <menuseparator/> + <menuitem id="abResultsTreeContext-newmessage" + label="&newmsgButton.label;" + accesskey="&newmsgButton.accesskey;" + command="cmd_newMessage"/> + <menuseparator/> + <menuitem id="abResultsTreeContext-print" + label="&printButton.label;" + accesskey="&printButton.accesskey;" + command="cmd_printcard"/> + <menuitem id="abResultsTreeContext-delete" + label="&deleteItemButton.label;" + accesskey="&deleteItemButton.accesskey;" + command="button_delete"/> +</menupopup> + +<menupopup id="toolbar-context-menu"/> + +<vbox id="titlebar"/> + +<toolbox id="ab-toolbox" + mode="full" + defaultmode="full" + class="toolbox-top"> + <toolbar type="menubar" + id="addrbook-toolbar-menubar2" + class="chromeclass-menubar" + persist="collapsed" + grippytooltiptext="&menuBar.tooltip;" + customizable="true" + defaultset="menubar-items" + mode="icons" + iconsize="small" + defaultmode="icons" + defaulticonsize="small" + context="toolbar-context-menu"> + <toolbaritem id="menubar-items" + class="menubar-items" + align="center"> + <menubar id="ab-menubar"> + <menu id="menu_File"> + <menupopup id="menu_FilePopup"> + <menu id="menu_New"> + <menupopup id="menu_NewPopup"> + <menuitem id="menu_newContact" + label="&newContact.label;" + accesskey="&newContact.accesskey;" + key="key_newCard" + command="cmd_newCard"/> + <menuitem id="menu_newList" + label="&newListCmd.label;" + accesskey="&newListCmd.accesskey;" + command="cmd_newlist"/> + <menuitem id="menu_newAddrbook" + label="&newAddressBookCmd.label;" + accesskey="&newAddressBookCmd.accesskey;" + oncommand="AbNewAddressBook();"/> + <menuitem id="addLDAP" + label="&newLDAPDirectoryCmd.label;" + accesskey="&newLDAPDirectoryCmd.accesskey;" + oncommand="AbNewLDAPDirectory();"/> + <menuseparator/> + <menuitem id="menu_newNavigator"/> + <menuitem id="menu_newPrivateWindow"/> + <menuitem id="menu_newMessage" + label="&newMessageCmd.label;" + accesskey="&newMessageCmd.accesskey;" + key="key_newMessage" + command="cmd_newMessage"/> + <menuitem id="menu_newEditor"/> + </menupopup> + </menu> + <menuitem id="menu_close"/> + <menuseparator/> +#ifndef XP_MACOSX + <menuitem id="printPreviewMenuItem" + label="&printPreviewContactViewCmd.label;" + accesskey="&printPreviewContactViewCmd.accesskey;" + command="cmd_printcardpreview"/> +#endif + <menuitem id="printMenuItem" label="&printContactViewCmd.label;" + accesskey="&printContactViewCmd.accesskey;" + key="key_printCard" + command="cmd_printcard"/> + <menuseparator id="menu_PageSetupSeparator"/> + <menuitem id="menu_printSetup"/> + <menuseparator id="menu_PrintAddrbookSeparator"/> +#ifndef XP_MACOSX + <menuitem id="printPreviewAddressBook" + label="&printPreviewAddressBook.label;" + accesskey="&printPreviewAddressBook.accesskey;" + command="cmd_printPreviewAddressBook"/> +#endif + <menuitem id="printAddressBook" + label="&printAddressBook.label;" + accesskey="&printAddressBook.accesskey;" + command="cmd_printAddressBook"/> + </menupopup> + </menu> + + <menu id="menu_Edit"> + <menupopup id="menu_EditPopup"> + <menuitem id="menu_undo"/> + <menuitem id="menu_redo"/> + <menuseparator/> + <menuitem id="menu_cut"/> + <menuitem id="menu_copy"/> + <menuitem id="menu_paste"/> + <menuitem id="menu_delete"/> + <menuseparator/> + <menuitem id="menu_selectAll"/> + <menuseparator/> + <!-- LOCALIZATION NOTE: set "hideSwapFnLnUI" to false in .dtd to enable the UI --> + <menuitem label="&swapFirstNameLastNameCmd.label;" + accesskey="&swapFirstNameLastNameCmd.accesskey;" + hidden="&hideSwapFnLnUI;" + command="cmd_swapFirstNameLastName"/> + <menuitem label="&propertiesCmd.label;" + accesskey="&propertiesCmd.accesskey;" + key="key_properties" + command="cmd_properties"/> + <menuitem id="menu_preferences" + oncommand="goPreferences('addressing_pane')"/> + </menupopup> + </menu> + <menu id="menu_View"> + <menupopup id="menu_View_Popup"> + <menu id="menu_Toolbars"> + <menupopup id="view_toolbars_popup" + onpopupshowing="onViewToolbarsPopupShowing(event);" + oncommand="onViewToolbarCommand(event);"> + <menuitem id="menu_showTaskbar"/> + </menupopup> + </menu> + <menu id="menu_Layout" + label="&layoutMenu.label;" + accesskey="&layoutMenu.accesskey;"> + <menupopup id="view_layout_popup" + onpopupshowing="InitViewLayoutMenuPopup(event);"> + <menuitem id="menu_showDirectoryPane" + label="&showDirectoryPane.label;" + key="key_toggleDirectoryPane" + accesskey="&showDirectoryPane.accesskey;" + oncommand="togglePaneSplitter('dirTree-splitter');" + checked="true" + type="checkbox"/> + <menuitem id="menu_showCardPane" + label="&showContactPane2.label;" + accesskey="&showContactPane2.accesskey;" + oncommand="togglePaneSplitter('results-splitter');" + checked="true" + type="checkbox"/> + </menupopup> + </menu> + <menuseparator id="viewMenuAfterLayoutSeparator"/> + <menu id="menu_ShowNameAs" label="&menu_ShowNameAs.label;" accesskey="&menu_ShowNameAs.accesskey;"> + <menupopup id="menuShowNameAsPopup"> + <menuitem type="radio" name="shownameas" + id="firstLastCmd" + label="&firstLastCmd.label;" + accesskey="&firstLastCmd.accesskey;" + oncommand="SetNameColumn('firstLastCmd')"/> + <menuitem type="radio" name="shownameas" + id="lastFirstCmd" + label="&lastFirstCmd.label;" + accesskey="&lastFirstCmd.accesskey;" + oncommand="SetNameColumn('lastFirstCmd')"/> + <menuitem type="radio" name="shownameas" + id="displayNameCmd" + label="&displayNameCmd.label;" + accesskey="&displayNameCmd.accesskey;" + oncommand="SetNameColumn('displayNameCmd')"/> + </menupopup> + </menu> + <menu id="sortMenu" label="&sortMenu.label;" accesskey="&sortMenu.accesskey;"> + <menupopup id="sortMenuPopup" onpopupshowing="InitViewSortByMenu()"> + <menuitem label="&GeneratedName.label;" + id="cmd_SortByGeneratedName" + accesskey="&GeneratedName.accesskey;" + oncommand="SortResultPane('GeneratedName');" name="sortas" type="radio" checked="true"/> + <menuitem label="&PrimaryEmail.label;" + id="cmd_SortByPrimaryEmail" + accesskey="&PrimaryEmail.accesskey;" + oncommand="SortResultPane('PrimaryEmail');" name="sortas" type="radio" checked="true"/> + <menuitem label="&ChatName.label;" + id="cmd_SortByChatName" + accesskey="&ChatName.accesskey;" + oncommand="SortResultPane('ChatName');" + name="sortas" + type="radio" + checked="true"/> + <menuitem label="&Company.label;" + id="cmd_SortByCompany" + accesskey="&Company.accesskey;" + oncommand="SortResultPane('Company');" name="sortas" type="radio" checked="true"/> + <!-- LOCALIZATION NOTE: + Fields for phonetic are disabled as default and can be enabled by + editing "mail.addr_book.show_phonetic_fields" + --> + <menuitem label="&_PhoneticName.label;" + id="cmd_SortBy_PhoneticName" + hidden="true" + accesskey="&_PhoneticName.accesskey;" + oncommand="SortResultPane('_PhoneticName');" name="sortas" type="radio" checked="true"/> + <menuitem label="&NickName.label;" + id="cmd_SortByNickName" + accesskey="&NickName.accesskey;" + oncommand="SortResultPane('NickName');" name="sortas" type="radio" checked="true"/> + <menuitem label="&SecondEmail.label;" + id="cmd_SortBySecondEmail" + accesskey="&SecondEmail.accesskey;" + oncommand="SortResultPane('SecondEmail');" name="sortas" type="radio" checked="true"/> + <menuitem label="&Department.label;" + id="cmd_SortByDepartment" + accesskey="&Department.accesskey;" + oncommand="SortResultPane('Department');" name="sortas" type="radio" checked="true"/> + <menuitem label="&JobTitle.label;" + id="cmd_SortByJobTitle" + accesskey="&JobTitle.accesskey;" + oncommand="SortResultPane('JobTitle');" name="sortas" type="radio" checked="true"/> + <menuitem label="&CellularNumber.label;" + id="cmd_SortByCellularNumber" + accesskey="&CellularNumber.accesskey;" + oncommand="SortResultPane('CellularNumber');" name="sortas" type="radio" checked="true"/> + <menuitem label="&PagerNumber.label;" + id="cmd_SortByPagerNumber" + accesskey="&PagerNumber.accesskey;" + oncommand="SortResultPane('PagerNumber');" name="sortas" type="radio" checked="true"/> + <menuitem label="&FaxNumber.label;" + id="cmd_SortByFaxNumber" + accesskey="&FaxNumber.accesskey;" + oncommand="SortResultPane('FaxNumber');" name="sortas" type="radio" checked="true"/> + <menuitem label="&HomePhone.label;" + id="cmd_SortByHomePhone" + accesskey="&HomePhone.accesskey;" + oncommand="SortResultPane('HomePhone');" name="sortas" type="radio" checked="true"/> + <menuitem label="&WorkPhone.label;" + id="cmd_SortByWorkPhone" + accesskey="&WorkPhone.accesskey;" + oncommand="SortResultPane('WorkPhone');" name="sortas" type="radio" checked="true"/> + <menuitem label="&Addrbook.label;" + id="cmd_SortByaddrbook" + accesskey="&Addrbook.accesskey;" + oncommand="SortResultPane('addrbook');" + name="sortas" + type="radio" + checked="true"/> + <menuseparator/> + <menuitem id="sortAscending" type="radio" name="sortdirection" label="&sortAscending.label;" accesskey="&sortAscending.accesskey;" oncommand="AbSortAscending()"/> + <menuitem id="sortDescending" type="radio" name="sortdirection" label="&sortDescending.label;" accesskey="&sortDescending.accesskey;" oncommand="AbSortDescending()"/> + </menupopup> + </menu> + </menupopup> + </menu> + <menu id="tasksMenu"> + <menupopup id="taskPopup"> + <menuitem label="&searchAddressesCmd.label;" + accesskey="&searchAddressesCmd.accesskey;" + id="menu_search_addresses" + oncommand="onAdvancedAbSearch();"/> + <menuitem label="&importCmd.label;" accesskey="&importCmd.accesskey;" oncommand="toImport()"/> + <menuitem label="&exportCmd.label;" + accesskey="&exportCmd.accesskey;" + oncommand="AbExportSelection();"/> + <menuseparator/> + </menupopup> + </menu> + + <menu id="windowMenu"/> + <menu id="menu_Help"/> + <spacer flex="1"/> + </menubar> + </toolbaritem> + </toolbar> + <toolbar class="chromeclass-toolbar toolbar-primary" + id="ab-bar2" + persist="collapsed" + customizable="true" + grippytooltiptext="&addressbookToolbar.tooltip;" + toolbarname="&showAbToolbarCmd.label;" + accesskey="&showAbToolbarCmd.accesskey;" + defaultset="button-newcard,button-newlist,separator,button-editcard,button-newmessage,button-abdelete,spring,searchBox,throbber-box" + context="toolbar-context-menu"> + <toolbarbutton id="button-newcard" + class="toolbarbutton-1" + label="&newContactButton.label;" + tooltiptext="&newContactButton.tooltip;" + removable="true" + command="cmd_newCard"/> + <toolbarbutton id="button-newlist" + class="toolbarbutton-1" + label="&newlistButton.label;" + tooltiptext="&newlistButton.tooltip;" + removable="true" + command="cmd_newlist"/> + <toolbarbutton id="button-editcard" + class="toolbarbutton-1" + label="&editItemButton.label;" + tooltiptext="&editItemButton.tooltip;" + removable="true" + command="cmd_properties"/> + <toolbarbutton id="button-newmessage" + class="toolbarbutton-1" + label="&newmsgButton.label;" + tooltiptext="&newmsgButton.tooltip;" + removable="true" + oncommand="AbNewMessage();"/> + <toolbarbutton id="button-abdelete" + class="toolbarbutton-1" + label="&deleteItemButton.label;" + tooltiptext="&deleteItemButton.tooltip;" + removable="true" + oncommand="goDoCommand('button_delete');"/> + <toolbarbutton id="print-button" + label="&printButton.label;" + tooltiptext="&printButton.tooltip;" + removable="true"/> + <toolbaritem id="searchBox" + flex="1" + title="&searchBox.title;" + removable="true" + align="center" + class="toolbaritem-noline chromeclass-toolbar-additional"> + <textbox id="searchInput" + flex="1" + type="search" + aria-controls="abResultsTree" + placeholder="&searchNameAndEmail.placeholder;" + clickSelectsAll="true" + oncommand="onEnterInSearchBar();" + onkeypress="if (event.keyCode == KeyEvent.DOM_VK_RETURN) this.select();"/> + </toolbaritem> + + <toolbaritem id="throbber-box"/> + </toolbar> + <toolbarpalette id="AbToolbarPalette"/> + <toolbarset id="customToolbars" context="toolbar-context-menu"/> +</toolbox> + + <!-- The main address book three pane --> + <hbox flex="1"> + <vbox id="dirTreeBox" persist="width collapsed"> + <tree id="dirTree" + class="abDirectory plain" + seltype="single" + style="min-width: 150px;" + flex="1" + persist="width" + hidecolumnpicker="true" + context="dirTreeContext" + onselect="DirPaneSelectionChange(); document.commandDispatcher.updateCommands('addrbook-select');" + ondblclick="DirPaneDoubleClick(event);" + onclick="DirPaneClick(event);" + onblur="goOnEvent(this, 'blur');"> + <treecols> + <treecol id="DirCol" + flex="1" + primary="true" + label="&dirTreeHeader.label;" + crop="center" + persist="width"/> + </treecols> + <treechildren/> + </tree> + </vbox> + + <splitter id="dirTree-splitter" collapse="before" persist="state"> + <grippy/> + </splitter> + + <vbox flex="1" style="min-width:100px"> + <description id="localResultsOnlyMessage" + value="&localResultsOnlyMessage.label;"/> + <vbox id="blankResultsPaneMessageBox" + flex="1" + pack="center" + align="center"> + <description id="blankResultsPaneMessage" + value="&blankResultsPaneMessage.label;"/> + </vbox> + <!-- results pane --> + <tree id="abResultsTree" context="abResultsTreeContext" flex="1" /> + + <splitter id="results-splitter" collapse="after" persist="state"> + <grippy/> + </splitter> + + <!-- card view --> + <hbox id="CardViewOuterBox" flex="1" persist="height"> + <vbox id="CardViewBox" + flex="1" + style="height:170px; min-height:1px; min-width:1px"> + <vbox id="CardViewInnerBox" collapsed="true" flex="1"> + <description id="CardTitle"/> + <hbox style="width:100%" flex="1"> + <vbox id="cvbPhoto" + class="cardViewGroup" + align="center" + style="min-width: 10ch; max-width: 10ch;"> + <image id="cvPhoto" style="max-width: 10ch; max-height: 10ch;"/> + </vbox> + <hbox flex="1" equalsize="always"> + <vbox flex="1" class="cardViewColumn"> + <vbox id="cvbContact" class="cardViewGroup"> + <description class="CardViewHeading" id="cvhContact">&contact.heading;</description> + <description class="CardViewLink" id="cvListNameBox"> + <html:p><html:a href="" id="cvListName"/></html:p> + </description> + <description class="CardViewText" id="cvDisplayName"/> + <description class="CardViewText" id="cvNickname"/> + <description class="CardViewLink" id="cvEmail1Box"> + <html:a href="" id="cvEmail1"/> + </description> + <description class="CardViewLink" id="cvEmail2Box"> + <html:a href="" id="cvEmail2"/> + </description> + </vbox> + <vbox id="cvbHome" class="cardViewGroup"> + <description class="CardViewHeading" id="cvhHome">&home.heading;</description> + <hbox> + <vbox flex="1"> + <description class="CardViewText" id="cvHomeAddress"/> + <description class="CardViewText" id="cvHomeAddress2"/> + <description class="CardViewText" id="cvHomeCityStZip"/> + <description class="CardViewText" id="cvHomeCountry"/> + </vbox> + <vbox id="cvbHomeMapItBox" pack="end"> + <button id="cvHomeMapIt" + label="&mapItButton.label;" + type="menu-button" + oncommand="openLinkWithUrl(this.firstChild.mapURL);" + tooltiptext="&mapIt.tooltip;"> + <menupopup class="map-list"/> + </button> + </vbox> + </hbox> + <description class="CardViewLink" id="cvHomeWebPageBox"> + <html:a onclick="return openLink(event);" + href="" + id="cvHomeWebPage"/> + </description> + </vbox> + <vbox id="cvbOther" class="cardViewGroup"> + <description class="CardViewHeading" id="cvhOther">&other.heading;</description> + <description class="CardViewText" id="cvBirthday"/> + <description class="CardViewText" id="cvCustom1"/> + <description class="CardViewText" id="cvCustom2"/> + <description class="CardViewText" id="cvCustom3"/> + <description class="CardViewText" id="cvCustom4"/> + <description class="CardViewText" id="cvNotes" + style="white-space: pre-wrap;"/> + <hbox> + <image id="cvBuddyIcon"/> + </hbox> + </vbox> + <vbox id="cvbChat" class="cardViewGroup"> + <description class="CardViewHeading" id="cvhChat">&chat.heading;</description> + <description class="CardViewText" id="cvYahoo"/> + <description class="CardViewText" id="cvSkype"/> + <description class="CardViewText" id="cvQQ"/> + <description class="CardViewText" id="cvMSN"/> + <description class="CardViewText" id="cvICQ"/> + <description class="CardViewText" id="cvXMPP"/> + <description class="CardViewText" id="cvIRC"/> + </vbox> + <!-- the description and addresses groups are only for + mailing lists --> + <vbox id="cvbDescription" class="cardViewGroup"> + <description class="CardViewHeading" id="cvhDescription">&description.heading;</description> + <description class="CardViewText" id="cvDescription"/> + </vbox> + <vbox id="cvbAddresses" class="cardViewGroup"> + <description class="CardViewHeading" id="cvhAddresses">&addresses.heading;</description> + <vbox id="cvAddresses"/> + </vbox> + </vbox> + + <vbox flex="1" class="cardViewColumn"> + <vbox id="cvbPhone" class="cardViewGroup"> + <description class="CardViewHeading" id="cvhPhone">&phone.heading;</description> + <description class="CardViewText" id="cvPhWork"/> + <description class="CardViewText" id="cvPhHome"/> + <description class="CardViewText" id="cvPhFax"/> + <description class="CardViewText" id="cvPhCellular"/> + <description class="CardViewText" id="cvPhPager"/> + </vbox> + <vbox id="cvbWork" class="cardViewGroup"> + <description class="CardViewHeading" id="cvhWork">&work.heading;</description> + <description class="CardViewText" id="cvJobTitle"/> + <description class="CardViewText" id="cvDepartment"/> + <description class="CardViewText" id="cvCompany"/> + <hbox> + <vbox flex="1"> + <description class="CardViewText" id="cvWorkAddress"/> + <description class="CardViewText" id="cvWorkAddress2"/> + <description class="CardViewText" id="cvWorkCityStZip"/> + <description class="CardViewText" id="cvWorkCountry"/> + </vbox> + <vbox id="cvbWorkMapItBox" pack="end"> + <button id="cvWorkMapIt" + label="&mapItButton.label;" + type="menu-button" + oncommand="openLinkWithUrl(this.firstChild.mapURL);" + tooltiptext="&mapIt.tooltip;"> + <menupopup class="map-list"/> + </button> + </vbox> + </hbox> + <description class="CardViewLink" id="cvWorkWebPageBox"> + <html:a onclick="return openLink(event);" + href="" + id="cvWorkWebPage"/> + </description> + </vbox> + </vbox> + </hbox> + </hbox> + </vbox> + </vbox> + </hbox> + </vbox> + </hbox> + + <panel id="customizeToolbarSheetPopup"/> + <statusbar id="status-bar" class="chromeclass-status"> + <statusbarpanel id="component-bar"/> + <statusbarpanel id="statusText" flex="1" value="&statusText.label;"/> + <statusbarpanel id="offline-status" class="statusbarpanel-iconic"/> + </statusbar> +</window> diff --git a/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.js b/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.js new file mode 100644 index 0000000000..bfc741c97f --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.js @@ -0,0 +1,24 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function Startup() +{ + enableAutocomplete(); +} + +function onEditDirectories() +{ + window.openDialog("chrome://messenger/content/addressbook/pref-editdirectories.xul", + "editDirectories", "chrome,modal=yes,resizable=no", null); +} + +function enableAutocomplete() +{ + var acLDAPValue = document.getElementById("ldap_2.autoComplete.useDirectory") + .value; + + EnableElementById("directoriesList", acLDAPValue, false); + EnableElementById("editButton", acLDAPValue, false); +} diff --git a/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.xul b/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.xul new file mode 100644 index 0000000000..af4bed750b --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/content/prefs/pref-addressing.xul @@ -0,0 +1,92 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/addressbook/pref-addressing.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="addressing_pane" label="&pref.addressing.title;" + script="chrome://messenger/content/addressbook/pref-addressing.js"> + + <stringbundle id="bundle_addressBook" + src="chrome://messenger/locale/addressbook/addressBook.properties"/> + + <preferences id="addressing_preferences"> + <preference id="mail.collect_email_address_outgoing" + name="mail.collect_email_address_outgoing" + type="bool"/> + <preference id="mail.collect_addressbook" + name="mail.collect_addressbook" + type="string"/> + <preference id="mail.autoComplete.highlightNonMatches" + name="mail.autoComplete.highlightNonMatches" + type="bool"/> + <preference id="mail.enable_autocomplete" + name="mail.enable_autocomplete" + type="bool"/> + <preference id="ldap_2.autoComplete.useDirectory" + name="ldap_2.autoComplete.useDirectory" + onchange="enableAutocomplete();" type="bool"/> + <preference id="ldap_2.autoComplete.directoryServer" + name="ldap_2.autoComplete.directoryServer" + type="string"/> + <preference id="pref.ldap.disable_button.edit_directories" + name="pref.ldap.disable_button.edit_directories" + type="bool"/> + </preferences> + + <groupbox> + <caption label="&emailCollectiontitle.label;"/> + <description>&emailCollectiontext.label;</description> + <hbox align="center"> + <checkbox id="emailCollectionOutgoing" + label="&emailCollectionPicker.label;" + accesskey="&emailCollectionPicker.accesskey;" + preference="mail.collect_email_address_outgoing"/> + <menulist id="localDirectoriesList" flex="1" + aria-labelledby="emailCollectionOutgoing" + preference="mail.collect_addressbook"> + <menupopup id="localDirectoriesPopup" class="addrbooksPopup" + localonly="true" writable="true"/> + </menulist> + </hbox> + </groupbox> + <groupbox id="addressAutocompletion"> + <caption label="&addressingTitle.label;"/> + <hbox align="center"> + <checkbox id="highlightNonMatches" label="&highlightNonMatches.label;" + preference="mail.autoComplete.highlightNonMatches" + accesskey="&highlightNonMatches.accesskey;"/> + </hbox> + + <separator class="thin"/> + + <description>&autocompleteText.label;</description> + <hbox align="center"> + <checkbox id="addressingAutocomplete" label="&addressingEnable.label;" + preference="mail.enable_autocomplete" + accesskey="&addressingEnable.accesskey;"/> + </hbox> + <hbox align="center"> + <checkbox id="autocompleteLDAP" label="&directories.label;" + preference="ldap_2.autoComplete.useDirectory" + accesskey="&directories.accesskey;"/> + <menulist id="directoriesList" flex="1" + aria-labelledby="autocompleteLDAP" + preference="ldap_2.autoComplete.directoryServer"> + <menupopup id="directoriesListPopup" class="addrbooksPopup" + none="&directoriesNone.label;" + remoteonly="true" value="dirPrefId"/> + </menulist> + <button id="editButton" label="&editDirectories.label;" + oncommand="onEditDirectories();" + accesskey="&editDirectories.accesskey;" + preference="pref.ldap.disable_button.edit_directories"/> + </hbox> + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/addrbook/jar.mn b/comm/suite/mailnews/components/addrbook/jar.mn new file mode 100644 index 0000000000..cc2cec4e13 --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/jar.mn @@ -0,0 +1,24 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +messenger.jar: + content/messenger/addressbook/addressbook.js (content/addressbook.js) +* content/messenger/addressbook/addressbook.xul (content/addressbook.xul) + content/messenger/addressbook/abCommon.js (content/abCommon.js) + content/messenger/addressbook/abCardOverlay.js (content/abCardOverlay.js) + content/messenger/addressbook/abCardOverlay.xul (content/abCardOverlay.xul) + content/messenger/addressbook/abCardViewOverlay.js (content/abCardViewOverlay.js) + content/messenger/addressbook/abEditCardDialog.xul (content/abEditCardDialog.xul) + content/messenger/addressbook/abNewCardDialog.xul (content/abNewCardDialog.xul) + content/messenger/addressbook/abResultsPaneOverlay.xul (content/abResultsPaneOverlay.xul) + content/messenger/addressbook/abMailListDialog.xul (content/abMailListDialog.xul) + content/messenger/addressbook/abEditListDialog.xul (content/abEditListDialog.xul) + content/messenger/addressbook/abListOverlay.xul (content/abListOverlay.xul) + content/messenger/addressbook/abSelectAddressesDialog.js (content/abSelectAddressesDialog.js) + content/messenger/addressbook/abSelectAddressesDialog.xul (content/abSelectAddressesDialog.xul) + content/messenger/addressbook/abTrees.js (content/abTrees.js) + content/messenger/addressbook/addressbook-panel.xul (content/addressbook-panel.xul) + content/messenger/addressbook/addressbook-panel.js (content/addressbook-panel.js) + content/messenger/addressbook/pref-addressing.js (content/prefs/pref-addressing.js) + content/messenger/addressbook/pref-addressing.xul (content/prefs/pref-addressing.xul) diff --git a/comm/suite/mailnews/components/addrbook/moz.build b/comm/suite/mailnews/components/addrbook/moz.build new file mode 100644 index 0000000000..77788c7f3e --- /dev/null +++ b/comm/suite/mailnews/components/addrbook/moz.build @@ -0,0 +1,8 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ["jar.mn"] + +DEFINES["TOOLKIT_DIR"] = "%s/toolkit" % (CONFIG["topsrcdir"],) diff --git a/comm/suite/mailnews/components/calendar/content/suite-overlay-addons.xhtml b/comm/suite/mailnews/components/calendar/content/suite-overlay-addons.xhtml new file mode 100644 index 0000000000..c1ccec4917 --- /dev/null +++ b/comm/suite/mailnews/components/calendar/content/suite-overlay-addons.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<overlay id="suiteAddonsOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"> + + <script><![CDATA[ + var lightningPrefs = { + guid: "{e2fda1a4-762b-4020-b5ad-a41df1933103}", + handleEvent: function(aEvent) { + var item = gListView.getListItemForID(this.guid); + if (!item) + return; + + item.showPreferences = this.showPreferences; + }, + showPreferences: function() { + var win = Services.wm.getMostRecentWindow("mozilla:preferences"); + if (win) { + win.focus(); + var doc = win.document; + var pane = doc.getElementById("paneLightning"); + doc.querySelector("dialog").syncTreeWithPane(pane, true); + } else { + openDialog("chrome://communicator/content/pref/preferences.xhtml", + "PrefWindow", + "non-private,chrome,titlebar,dialog=no,resizable", + "paneLightning"); + } + }, + }; + + window.addEventListener("ViewChanged", lightningPrefs, false); + ]]></script> + +</overlay> diff --git a/comm/suite/mailnews/components/calendar/content/suite-overlay-preferences.xhtml b/comm/suite/mailnews/components/calendar/content/suite-overlay-preferences.xhtml new file mode 100644 index 0000000000..0e7aad33df --- /dev/null +++ b/comm/suite/mailnews/components/calendar/content/suite-overlay-preferences.xhtml @@ -0,0 +1,66 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://lightning/skin/lightning.css"?> + +<?xul-overlay href="chrome://calendar/content/preferences/general.xhtml"?> +<?xul-overlay href="chrome://calendar/content/preferences/alarms.xhtml"?> +<?xul-overlay href="chrome://calendar/content/preferences/categories.xhtml"?> +<?xul-overlay href="chrome://calendar/content/preferences/views.xhtml"?> + +<!DOCTYPE overlay [ + <!ENTITY % lightningDTD SYSTEM "chrome://lightning/locale/lightning.dtd"> + %lightningDTD; + <!ENTITY % preferencesDTD SYSTEM "chrome://calendar/locale/preferences/preferences.dtd"> + %preferencesDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"> + + <treechildren id="prefsPanelChildren"> + <treeitem container="true" + id="lightningItem" + insertafter="mailnewsItem,navigatorItem" + label="&lightning.preferencesLabel;" + prefpane="paneLightning"> + <treechildren id="lightningChildren"> + <treeitem id="lightningAlarms" + label="&paneAlarms.title;" + prefpane="paneLightningAlarms"/> + <treeitem id="lightningCategories" + label="&paneCategories.title;" + prefpane="paneLightningCategories"/> + <treeitem id="lightningViews" + label="&paneViews.title;" + prefpane="paneLightningViews"/> + </treechildren> + </treeitem> + </treechildren> + + <prefwindow id="prefDialog"> + <prefpane id="paneLightning" + label="&lightning.preferencesLabel;" + onpaneload="gCalendarGeneralPane.init();"> + <vbox id="calPreferencesBoxGeneral"/> + </prefpane> + <prefpane id="paneLightningAlarms" + label="&paneAlarms.title;" + onpaneload="gAlarmsPane.init();"> + <vbox id="calPreferencesBoxAlarms"/> + </prefpane> + <prefpane id="paneLightningCategories" + label="&paneCategories.title;" + onpaneload="gCategoriesPane.init();"> + <vbox id="calPreferencesBoxCategories"/> + </prefpane> + <prefpane id="paneLightningViews" + label="&paneViews.title;" + onpaneload="gViewsPane.init();"> + <vbox id="calPreferencesBoxViews"/> + </prefpane> + </prefwindow> + +</overlay> diff --git a/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.js b/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.js new file mode 100644 index 0000000000..6c27c9c385 --- /dev/null +++ b/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.js @@ -0,0 +1,47 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../../suite/base/content/utilityOverlay.js */ + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var ltnSuiteUtils = { + addStartupObserver: function() { + Services.obs.addObserver(this.startupObserver, "lightning-startup-done"); + Services.obs.addObserver(this.startupObserver, "calendar-taskview-startup-done"); + }, + + startupObserver: { + observe: function(subject, topic, state) { + if (topic != "lightning-startup-done" && topic != "calendar-taskview-startup-done") { + return; + } + + const ids = [ + ["CustomizeTaskActionsToolbar", "task-actions-toolbox"], + ["CustomizeCalendarToolbar", "calendar-toolbox"], + ["CustomizeTaskToolbar", "task-toolbox"], + ]; + + ids.forEach(([itemID, toolboxID]) => { + let item = document.getElementById(itemID); + let toolbox = document.getElementById(toolboxID); + toolbox.customizeInit = function() { + item.setAttribute("disabled", "true"); + toolboxCustomizeInit("mail-menubar"); + }; + toolbox.customizeDone = function(aToolboxChanged) { + item.removeAttribute("disabled"); + toolboxCustomizeDone("mail-menubar", toolbox, aToolboxChanged); + }; + toolbox.customizeChange = function(aEvent) { + toolboxCustomizeChange(toolbox, aEvent); + }; + }); + }, + }, +}; + +ltnSuiteUtils.addStartupObserver(); diff --git a/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.xhtml b/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.xhtml new file mode 100644 index 0000000000..79ae5d662a --- /dev/null +++ b/comm/suite/mailnews/components/calendar/content/suite-overlay-sidebar.xhtml @@ -0,0 +1,39 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this file, + - You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<overlay id="suiteSidebarOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:html="http://www.w3.org/1999/xhtml"> + + <script src="chrome://lightning/content/suite-overlay-sidebar.js"/> + + <key id="openLightningKey" removeelement="true"/> + <key id="openTasksKey" removeelement="true"/> + <key id="calendar-new-event-key" removeelement="true"/> + <key id="calendar-new-todo-key" removeelement="true"/> + + <menuitem id="CustomizeTaskActionsToolbar" + oncommand="goCustomizeToolbar(document.getElementById('task-actions-toolbox'))"/> + + <toolbox id="calendar-toolbox" + defaultlabelalign="end" + xpfe="false"/> + <toolbox id="task-toolbox" + defaultlabelalign="end" + xpfe="false"/> + <toolbox id="task-actions-toolbox" + defaultlabelalign="end" + xpfe="false"/> + + <toolbar id="calendar-toolbar2" + defaultlabelalign="end" + context="toolbar-context-menu"/> + <toolbar id="task-toolbar2" + defaultlabelalign="end" + context="toolbar-context-menu"/> + <toolbar id="task-actions-toolbar" + context="toolbar-context-menu"/> + +</overlay> diff --git a/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js b/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js new file mode 100644 index 0000000000..48f7f31b1a --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/MsgComposeCommands.js @@ -0,0 +1,3936 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm"); +const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm"); +ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm"); +const {FolderUtils} = ChromeUtils.import("resource:///modules/FolderUtils.jsm"); +const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm"); +const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.js"); +const { MimeParser } = ChromeUtils.import("resource:///modules/mimeParser.jsm"); + +ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); + +/** + * interfaces + */ +var nsIMsgCompDeliverMode = Ci.nsIMsgCompDeliverMode; +var nsIMsgCompSendFormat = Ci.nsIMsgCompSendFormat; +var nsIMsgCompConvertible = Ci.nsIMsgCompConvertible; +var nsIMsgCompType = Ci.nsIMsgCompType; +var nsIMsgCompFormat = Ci.nsIMsgCompFormat; +var nsIAbPreferMailFormat = Ci.nsIAbPreferMailFormat; +var mozISpellCheckingEngine = Ci.mozISpellCheckingEngine; + +/** + * In order to distinguish clearly globals that are initialized once when js load (static globals) and those that need to be + * initialize every time a compose window open (globals), I (ducarroz) have decided to prefix by s... the static one and + * by g... the other one. Please try to continue and repect this rule in the future. Thanks. + */ +/** + * static globals, need to be initialized only once + */ +var sComposeMsgsBundle; +var sBrandBundle; + +var sRDF = null; +var sNameProperty = null; +var sDictCount = 0; + +/** + * Global message window object. This is used by mail-offline.js and therefore + * should not be renamed. We need to avoid doing this kind of cross file global + * stuff in the future and instead pass this object as parameter when needed by + * functions in the other js file. + */ +var msgWindow; + +var gMessenger; + +/** + * Global variables, need to be re-initialized every time mostly because + * we need to release them when the window closes. + */ +var gHideMenus; +var gMsgCompose; +var gOriginalMsgURI; +var gWindowLocked; +var gSendLocked; +var gContentChanged; +var gAutoSaving; +var gCurrentIdentity; +var defaultSaveOperation; +var gSendOrSaveOperationInProgress; +var gCloseWindowAfterSave; +var gSavedSendNowKey; +var gSendFormat; +var gLogComposePerformance; + +var gMsgIdentityElement; +var gMsgAddressingWidgetElement; +var gMsgSubjectElement; +var gMsgAttachmentElement; +var gMsgHeadersToolbarElement; +var gComposeType; +var gFormatToolbarHidden = false; +var gBodyFromArgs; + +// i18n globals +var gCharsetConvertManager; + +var gLastWindowToHaveFocus; +var gReceiptOptionChanged; +var gDSNOptionChanged; +var gAttachVCardOptionChanged; + +var gAutoSaveInterval; +var gAutoSaveTimeout; +var gAutoSaveKickedIn; +var gEditingDraft; + +var kComposeAttachDirPrefName = "mail.compose.attach.dir"; + +function InitializeGlobalVariables() +{ + gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger); + + gMsgCompose = null; + gOriginalMsgURI = null; + gWindowLocked = false; + gContentChanged = false; + gCurrentIdentity = null; + defaultSaveOperation = "draft"; + gSendOrSaveOperationInProgress = false; + gAutoSaving = false; + gCloseWindowAfterSave = false; + gSavedSendNowKey = null; + gSendFormat = nsIMsgCompSendFormat.AskUser; + gCharsetConvertManager = Cc['@mozilla.org/charset-converter-manager;1'].getService(Ci.nsICharsetConverterManager); + gHideMenus = false; + // We are storing the value of the bool logComposePerformance inorder to + // avoid logging unnecessarily. + gLogComposePerformance = MailServices.compose.logComposePerformance; + + gLastWindowToHaveFocus = null; + gReceiptOptionChanged = false; + gDSNOptionChanged = false; + gAttachVCardOptionChanged = false; + msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"] + .createInstance(Ci.nsIMsgWindow); + MailServices.mailSession.AddMsgWindow(msgWindow); +} +InitializeGlobalVariables(); + +function ReleaseGlobalVariables() +{ + gCurrentIdentity = null; + gCharsetConvertManager = null; + gMsgCompose = null; + gOriginalMsgURI = null; + gMessenger = null; + sComposeMsgsBundle = null; + sBrandBundle = null; + MailServices.mailSession.RemoveMsgWindow(msgWindow); + msgWindow = null; +} + +function disableEditableFields() +{ + gMsgCompose.editor.flags |= Ci.nsIEditor.eEditorReadonlyMask; + var disableElements = document.getElementsByAttribute("disableonsend", "true"); + for (let i = 0; i < disableElements.length; i++) + disableElements[i].setAttribute('disabled', 'true'); + +} + +function enableEditableFields() +{ + gMsgCompose.editor.flags &= ~Ci.nsIEditor.eEditorReadonlyMask; + var enableElements = document.getElementsByAttribute("disableonsend", "true"); + for (let i = 0; i < enableElements.length; i++) + enableElements[i].removeAttribute('disabled'); + +} + +/** + * Small helper function to check whether the node passed in is a signature. + * Note that a text node is not a DOM element, hence .localName can't be used. + */ +function isSignature(aNode) { + return ["DIV","PRE"].includes(aNode.nodeName) && + aNode.classList.contains("moz-signature"); +} + +var stateListener = { + NotifyComposeFieldsReady: function() { + ComposeFieldsReady(); + updateSendCommands(true); + }, + + NotifyComposeBodyReady: function() { + this.useParagraph = gMsgCompose.composeHTML && + Services.prefs.getBoolPref("mail.compose.default_to_paragraph"); + this.editor = GetCurrentEditor(); + this.paragraphState = document.getElementById("cmd_paragraphState"); + + // Look at the compose types which require action (nsIMsgComposeParams.idl): + switch (gComposeType) { + + case Ci.nsIMsgCompType.MailToUrl: + gBodyFromArgs = true; + case Ci.nsIMsgCompType.New: + case Ci.nsIMsgCompType.NewsPost: + case Ci.nsIMsgCompType.ForwardAsAttachment: + this.NotifyComposeBodyReadyNew(); + break; + + case Ci.nsIMsgCompType.Reply: + case Ci.nsIMsgCompType.ReplyAll: + case Ci.nsIMsgCompType.ReplyToSender: + case Ci.nsIMsgCompType.ReplyToGroup: + case Ci.nsIMsgCompType.ReplyToSenderAndGroup: + case Ci.nsIMsgCompType.ReplyWithTemplate: + case Ci.nsIMsgCompType.ReplyToList: + this.NotifyComposeBodyReadyReply(); + break; + + case Ci.nsIMsgCompType.ForwardInline: + this.NotifyComposeBodyReadyForwardInline(); + break; + + case Ci.nsIMsgCompType.EditTemplate: + defaultSaveOperation = "template"; + case Ci.nsIMsgCompType.Draft: + case Ci.nsIMsgCompType.Template: + case Ci.nsIMsgCompType.Redirect: + case Ci.nsIMsgCompType.EditAsNew: + break; + + default: + dump("Unexpected nsIMsgCompType in NotifyComposeBodyReady (" + + gComposeType + ")\n"); + } + + // Set the selected item in the identity list as needed, which will cause + // an identity/signature switch. This can only be done once the message + // body has already been assembled with the signature we need to switch. + if (gMsgCompose.identity != gCurrentIdentity) { + // Since switching the signature loses the caret position, we record it + // and restore it later. + let selection = this.editor.selection; + let range = selection.getRangeAt(0); + let start = range.startOffset; + let startNode = range.startContainer; + + this.editor.enableUndo(false); + let identityList = GetMsgIdentityElement(); + identityList.selectedItem = identityList.getElementsByAttribute( + "identitykey", gMsgCompose.identity.key)[0]; + LoadIdentity(false); + + this.editor.enableUndo(true); + this.editor.resetModificationCount(); + selection.collapse(startNode, start); + } + + if (gMsgCompose.composeHTML) + loadHTMLMsgPrefs(); + AdjustFocus(); + }, + + NotifyComposeBodyReadyNew: function() { + let insertParagraph = this.useParagraph; + + let mailDoc = document.getElementById("content-frame").contentDocument; + let mailBody = mailDoc.querySelector("body"); + if (insertParagraph && gBodyFromArgs) { + // Check for "empty" body before allowing paragraph to be inserted. + // Non-empty bodies in a new message can occur when clicking on a + // mailto link or when using the command line option -compose. + // An "empty" body can be one of these two cases: + // 1) <br> and nothing follows (no next sibling) + // 2) <div/pre class="moz-signature"> + // Note that <br><div/pre class="moz-signature"> doesn't happen in + // paragraph mode. + let firstChild = mailBody.firstChild; + if ((firstChild.nodeName != "BR" || firstChild.nextSibling) && + !isSignature(firstChild)) + insertParagraph = false; + } + + // Control insertion of line breaks. + if (insertParagraph) { + this.editor.enableUndo(false); + + this.editor.selection.collapse(mailBody, 0); + let pElement = this.editor.createElementWithDefaults("p"); + let brElement = this.editor.createElementWithDefaults("br"); + pElement.appendChild(brElement); + this.editor.insertElementAtSelection(pElement, false); + + this.paragraphState.setAttribute("state", "p"); + + this.editor.beginningOfDocument(); + this.editor.enableUndo(true); + this.editor.resetModificationCount(); + } else { + this.paragraphState.setAttribute("state", ""); + } + }, + + NotifyComposeBodyReadyReply: function() { + // Control insertion of line breaks. + if (this.useParagraph) { + let mailDoc = document.getElementById("content-frame").contentDocument; + let mailBody = mailDoc.querySelector("body"); + let selection = this.editor.selection; + + // Make sure the selection isn't inside the signature. + if (isSignature(mailBody.firstChild)) + selection.collapse(mailBody, 0); + + let range = selection.getRangeAt(0); + let start = range.startOffset; + + if (start != range.endOffset) { + // The selection is not collapsed, most likely due to the + // "select the quote" option. In this case we do nothing. + return; + } + + if (range.startContainer != mailBody) { + dump("Unexpected selection in NotifyComposeBodyReadyReply\n"); + return; + } + + this.editor.enableUndo(false); + + let pElement = this.editor.createElementWithDefaults("p"); + let brElement = this.editor.createElementWithDefaults("br"); + pElement.appendChild(brElement); + this.editor.insertElementAtSelection(pElement, false); + + // Position into the paragraph. + selection.collapse(pElement, 0); + + this.paragraphState.setAttribute("state", "p"); + + this.editor.enableUndo(true); + this.editor.resetModificationCount(); + } else { + this.paragraphState.setAttribute("state", ""); + } + }, + + NotifyComposeBodyReadyForwardInline: function() { + let mailDoc = document.getElementById("content-frame").contentDocument; + let mailBody = mailDoc.querySelector("body"); + let selection = this.editor.selection; + + this.editor.enableUndo(false); + + // Control insertion of line breaks. + selection.collapse(mailBody, 0); + if (this.useParagraph) { + let pElement = this.editor.createElementWithDefaults("p"); + let brElement = this.editor.createElementWithDefaults("br"); + pElement.appendChild(brElement); + this.editor.insertElementAtSelection(pElement, false); + this.paragraphState.setAttribute("state", "p"); + } else { + // insertLineBreak() has been observed to insert two <br> elements + // instead of one before a <div>, so we'll do it ourselves here. + let brElement = this.editor.createElementWithDefaults("br"); + this.editor.insertElementAtSelection(brElement, false); + this.paragraphState.setAttribute("state", ""); + } + + this.editor.beginningOfDocument(); + this.editor.enableUndo(true); + this.editor.resetModificationCount(); + }, + + ComposeProcessDone: function(aResult) { + gWindowLocked = false; + enableEditableFields(); + updateComposeItems(); + + if (aResult== Cr.NS_OK) + { + if (!gAutoSaving) + SetContentAndBodyAsUnmodified(); + + if (gCloseWindowAfterSave) + { + // Notify the SendListener that Send has been aborted and Stopped + if (gMsgCompose) + gMsgCompose.onSendNotPerformed(null, Cr.NS_ERROR_ABORT); + + MsgComposeCloseWindow(); + } + } + // else if we failed to save, and we're autosaving, need to re-mark the editor + // as changed, so that we won't lose the changes. + else if (gAutoSaving) + { + gMsgCompose.bodyModified = true; + gContentChanged = true; + } + + gAutoSaving = false; + gCloseWindowAfterSave = false; + }, + + SaveInFolderDone: function(folderURI) { + DisplaySaveFolderDlg(folderURI); + } +}; + +// all progress notifications are done through the nsIWebProgressListener implementation... +var progressListener = { + onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) + { + document.getElementById('navigator-throbber').setAttribute("busy", "true"); + document.getElementById('compose-progressmeter').setAttribute( "mode", "undetermined" ); + document.getElementById("statusbar-progresspanel").collapsed = false; + } + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) + { + gSendOrSaveOperationInProgress = false; + document.getElementById('navigator-throbber').removeAttribute("busy"); + document.getElementById('compose-progressmeter').setAttribute( "mode", "normal" ); + document.getElementById('compose-progressmeter').setAttribute( "value", 0 ); + document.getElementById("statusbar-progresspanel").collapsed = true; + document.getElementById('statusText').setAttribute('label', ''); + } + }, + + onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) + { + // Calculate percentage. + var percent; + if ( aMaxTotalProgress > 0 ) + { + percent = Math.round( (aCurTotalProgress*100)/aMaxTotalProgress ); + if ( percent > 100 ) + percent = 100; + + document.getElementById('compose-progressmeter').removeAttribute("mode"); + + // Advance progress meter. + document.getElementById('compose-progressmeter').setAttribute( "value", percent ); + } + else + { + // Progress meter should be barber-pole in this case. + document.getElementById('compose-progressmeter').setAttribute( "mode", "undetermined" ); + } + }, + + onLocationChange: function(aWebProgress, aRequest, aLocation, aFlags) + { + // we can ignore this notification + }, + + onStatusChange: function(aWebProgress, aRequest, aStatus, aMessage) + { + // Looks like it's possible that we get call while the document has been already delete! + // therefore we need to protect ourself by using try/catch + try { + let statusText = document.getElementById("statusText"); + if (statusText) + statusText.setAttribute("label", aMessage); + } catch (ex) {} + }, + + onSecurityChange: function(aWebProgress, aRequest, state) + { + // we can ignore this notification + }, + + QueryInterface : function(iid) + { + if (iid.equals(Ci.nsIWebProgressListener) || + iid.equals(Ci.nsISupportsWeakReference) || + iid.equals(Ci.nsISupports)) + return this; + + throw Cr.NS_NOINTERFACE; + } +}; + +var defaultController = +{ + supportsCommand: function(command) + { + switch (command) + { + //File Menu + case "cmd_attachFile": + case "cmd_attachPage": + case "cmd_close": + case "cmd_save": + case "cmd_saveAsFile": + case "cmd_saveAsDraft": + case "cmd_saveAsTemplate": + case "cmd_sendButton": + case "cmd_sendNow": + case "cmd_sendWithCheck": + case "cmd_sendLater": + case "cmd_printSetup": + case "cmd_printpreview": + case "cmd_print": + + //Edit Menu + case "cmd_account": + case "cmd_preferences": + + //Options Menu + case "cmd_selectAddress": + case "cmd_outputFormat": + case "cmd_quoteMessage": + return true; + + default: + return false; + } + }, + isCommandEnabled: function(command) + { + var composeHTML = gMsgCompose && gMsgCompose.composeHTML; + + switch (command) + { + //File Menu + case "cmd_attachFile": + case "cmd_attachPage": + case "cmd_close": + case "cmd_save": + case "cmd_saveAsFile": + case "cmd_saveAsDraft": + case "cmd_saveAsTemplate": + case "cmd_printSetup": + case "cmd_printpreview": + case "cmd_print": + return !gWindowLocked; + case "cmd_sendButton": + case "cmd_sendLater": + case "cmd_sendWithCheck": + case "cmd_sendButton": + return !gWindowLocked && !gSendLocked; + case "cmd_sendNow": + return !gWindowLocked && !Services.io.offline && !gSendLocked; + + //Edit Menu + case "cmd_account": + case "cmd_preferences": + return true; + + //Options Menu + case "cmd_selectAddress": + return !gWindowLocked; + case "cmd_outputFormat": + return composeHTML; + case "cmd_quoteMessage": + var selectedURIs = GetSelectedMessages(); + if (selectedURIs && selectedURIs.length > 0) + return true; + return false; + + default: + return false; + } + }, + + doCommand: function(command) + { + switch (command) + { + //File Menu + case "cmd_attachFile" : if (defaultController.isCommandEnabled(command)) AttachFile(); break; + case "cmd_attachPage" : AttachPage(); break; + case "cmd_close" : DoCommandClose(); break; + case "cmd_save" : Save(); break; + case "cmd_saveAsFile" : SaveAsFile(true); break; + case "cmd_saveAsDraft" : SaveAsDraft(); break; + case "cmd_saveAsTemplate" : SaveAsTemplate(); break; + case "cmd_sendButton" : + if (defaultController.isCommandEnabled(command)) + { + if (Services.io.offline) + SendMessageLater(); + else + SendMessage(); + } + break; + case "cmd_sendNow" : if (defaultController.isCommandEnabled(command)) SendMessage(); break; + case "cmd_sendWithCheck" : if (defaultController.isCommandEnabled(command)) SendMessageWithCheck(); break; + case "cmd_sendLater" : if (defaultController.isCommandEnabled(command)) SendMessageLater(); break; + case "cmd_printSetup" : PrintUtils.showPageSetup(); break; + case "cmd_printpreview" : PrintUtils.printPreview(PrintPreviewListener); break; + case "cmd_print" : + let browser = GetCurrentEditorElement(); + PrintUtils.printWindow(browser.outerWindowID, browser); + break; + + //Edit Menu + case "cmd_account" : + let currentAccountKey = getCurrentAccountKey(); + let account = MailServices.accounts.getAccount(currentAccountKey); + MsgAccountManager(null, account.incomingServer); + break; + case "cmd_preferences" : DoCommandPreferences(); break; + + //Options Menu + case "cmd_selectAddress" : if (defaultController.isCommandEnabled(command)) SelectAddress(); break; + case "cmd_quoteMessage" : if (defaultController.isCommandEnabled(command)) QuoteSelectedMessage(); break; + default: + return; + } + }, + + onEvent: function(event) + { + } +}; + +var gAttachmentBucketController = +{ + supportsCommand: function(aCommand) + { + switch (aCommand) + { + case "cmd_delete": + case "cmd_renameAttachment": + case "cmd_selectAll": + case "cmd_openAttachment": + return true; + default: + return false; + } + }, + + isCommandEnabled: function(aCommand) + { + switch (aCommand) + { + case "cmd_delete": + return MessageGetNumSelectedAttachments() > 0; + case "cmd_renameAttachment": + return MessageGetNumSelectedAttachments() == 1; + case "cmd_selectAll": + return MessageHasAttachments(); + case "cmd_openAttachment": + return MessageGetNumSelectedAttachments() == 1; + default: + return false; + } + }, + + doCommand: function(aCommand) + { + switch (aCommand) + { + case "cmd_delete": + if (MessageGetNumSelectedAttachments() > 0) + RemoveSelectedAttachment(); + break; + case "cmd_renameAttachment": + if (MessageGetNumSelectedAttachments() == 1) + RenameSelectedAttachment(); + break; + case "cmd_selectAll": + if (MessageHasAttachments()) + SelectAllAttachments(); + break; + case "cmd_openAttachment": + if (MessageGetNumSelectedAttachments() == 1) + OpenSelectedAttachment(); + break; + default: + return; + } + }, + + onEvent: function(event) + { + } +}; + +function QuoteSelectedMessage() +{ + var selectedURIs = GetSelectedMessages(); + if (selectedURIs) + for (let i = 0; i < selectedURIs.length; i++) + gMsgCompose.quoteMessage(selectedURIs[i]); +} + +function GetSelectedMessages() +{ + var mailWindow = gMsgCompose && Services.wm.getMostRecentWindow("mail:3pane"); + return mailWindow && mailWindow.gFolderDisplay.selectedMessageUris; +} + +function SetupCommandUpdateHandlers() +{ + top.controllers.appendController(defaultController); + + let attachmentBucket = document.getElementById("attachmentBucket"); + attachmentBucket.controllers.appendController(gAttachmentBucketController); + + document.getElementById("optionsMenuPopup") + .addEventListener("popupshowing", updateOptionItems, true); +} + +function UnloadCommandUpdateHandlers() +{ + document.getElementById("optionsMenuPopup") + .removeEventListener("popupshowing", updateOptionItems, true); + + top.controllers.removeController(defaultController); + + let attachmentBucket = document.getElementById("attachmentBucket"); + attachmentBucket.controllers.removeController(gAttachmentBucketController); +} + +function CommandUpdate_MsgCompose() +{ + var focusedWindow = top.document.commandDispatcher.focusedWindow; + + // we're just setting focus to where it was before + if (focusedWindow == gLastWindowToHaveFocus) { + return; + } + + gLastWindowToHaveFocus = focusedWindow; + + updateComposeItems(); +} + +function updateComposeItems() +{ + try { + // Edit Menu + goUpdateCommand("cmd_rewrap"); + + // Insert Menu + if (gMsgCompose && gMsgCompose.composeHTML) + { + goUpdateCommand("cmd_renderedHTMLEnabler"); + goUpdateCommand("cmd_decreaseFontStep"); + goUpdateCommand("cmd_increaseFontStep"); + goUpdateCommand("cmd_bold"); + goUpdateCommand("cmd_italic"); + goUpdateCommand("cmd_underline"); + goUpdateCommand("cmd_ul"); + goUpdateCommand("cmd_ol"); + goUpdateCommand("cmd_indent"); + goUpdateCommand("cmd_outdent"); + goUpdateCommand("cmd_align"); + goUpdateCommand("cmd_smiley"); + } + + // Options Menu + goUpdateCommand("cmd_spelling"); + } catch(e) {} +} + +function openEditorContextMenu(popup) +{ + gContextMenu = new nsContextMenu(popup); + if (gContextMenu.shouldDisplay) + { + // If message body context menu then focused element should be content. + var showPasteExtra = + top.document.commandDispatcher.focusedWindow == content; + gContextMenu.showItem("context-pasteNoFormatting", showPasteExtra); + gContextMenu.showItem("context-pasteQuote", showPasteExtra); + if (showPasteExtra) + { + goUpdateCommand("cmd_pasteNoFormatting"); + goUpdateCommand("cmd_pasteQuote"); + } + return true; + } + return false; +} + +function updateEditItems() +{ + goUpdateCommand("cmd_pasteNoFormatting"); + goUpdateCommand("cmd_pasteQuote"); + goUpdateCommand("cmd_delete"); + goUpdateCommand("cmd_renameAttachment"); + goUpdateCommand("cmd_selectAll"); + goUpdateCommand("cmd_openAttachment"); + goUpdateCommand("cmd_findReplace"); + goUpdateCommand("cmd_find"); + goUpdateCommand("cmd_findNext"); + goUpdateCommand("cmd_findPrev"); +} + +function updateOptionItems() +{ + goUpdateCommand("cmd_quoteMessage"); +} + +/** + * Update all the commands for sending a message to reflect their current state. + */ +function updateSendCommands(aHaveController) { + updateSendLock(); + if (aHaveController) { + goUpdateCommand("cmd_sendButton"); + goUpdateCommand("cmd_sendNow"); + goUpdateCommand("cmd_sendLater"); + goUpdateCommand("cmd_sendWithCheck"); + } else { + goSetCommandEnabled("cmd_sendButton", + defaultController.isCommandEnabled("cmd_sendButton")); + goSetCommandEnabled("cmd_sendNow", + defaultController.isCommandEnabled("cmd_sendNow")); + goSetCommandEnabled("cmd_sendLater", + defaultController.isCommandEnabled("cmd_sendLater")); + goSetCommandEnabled("cmd_sendWithCheck", + defaultController.isCommandEnabled("cmd_sendWithCheck")); + } +} + +var messageComposeOfflineQuitObserver = { + observe: function(aSubject, aTopic, aState) { + // sanity checks + if (aTopic == "network:offline-status-changed") + { + MessageComposeOfflineStateChanged(aState == "offline"); + } + // check whether to veto the quit request (unless another observer already + // did) + else if (aTopic == "quit-application-requested" && + aSubject instanceof Ci.nsISupportsPRBool && + !aSubject.data) + aSubject.data = !ComposeCanClose(); + } +} + +function AddMessageComposeOfflineQuitObserver() +{ + Services.obs.addObserver(messageComposeOfflineQuitObserver, + "network:offline-status-changed"); + Services.obs.addObserver(messageComposeOfflineQuitObserver, + "quit-application-requested"); + + // set the initial state of the send button + MessageComposeOfflineStateChanged(Services.io.offline); +} + +function RemoveMessageComposeOfflineQuitObserver() +{ + Services.obs.removeObserver(messageComposeOfflineQuitObserver, + "network:offline-status-changed"); + Services.obs.removeObserver(messageComposeOfflineQuitObserver, + "quit-application-requested"); +} + +function MessageComposeOfflineStateChanged(goingOffline) +{ + try { + var sendButton = document.getElementById("button-send"); + var sendNowMenuItem = document.getElementById("menu_sendNow"); + + if (!gSavedSendNowKey) { + gSavedSendNowKey = sendNowMenuItem.getAttribute('key'); + } + + // don't use goUpdateCommand here ... the defaultController might not be installed yet + updateSendCommands(false); + + if (goingOffline) + { + sendButton.label = sendButton.getAttribute('later_label'); + sendButton.setAttribute('tooltiptext', sendButton.getAttribute('later_tooltiptext')); + sendNowMenuItem.removeAttribute('key'); + } + else + { + sendButton.label = sendButton.getAttribute('now_label'); + sendButton.setAttribute('tooltiptext', sendButton.getAttribute('now_tooltiptext')); + if (gSavedSendNowKey) { + sendNowMenuItem.setAttribute('key', gSavedSendNowKey); + } + } + + } catch(e) {} +} + +function DoCommandClose() +{ + if (ComposeCanClose()) { + // Notify the SendListener that Send has been aborted and Stopped + if (gMsgCompose) + gMsgCompose.onSendNotPerformed(null, Cr.NS_ERROR_ABORT); + + // note: if we're not caching this window, this destroys it for us + MsgComposeCloseWindow(); + } + + return false; +} + +function DoCommandPreferences() +{ + goPreferences('composing_messages_pane'); +} + +function toggleAffectedChrome(aHide) +{ + // chrome to toggle includes: + // (*) menubar + // (*) toolbox + // (*) sidebar + // (*) statusbar + + if (!gChromeState) + gChromeState = {}; + + var statusbar = document.getElementById("status-bar"); + + // sidebar states map as follows: + // hidden => hide/show nothing + // collapsed => hide/show only the splitter + // shown => hide/show the splitter and the box + if (aHide) + { + // going into print preview mode + gChromeState.sidebar = SidebarGetState(); + SidebarSetState("hidden"); + + // deal with the Status Bar + gChromeState.statusbarWasHidden = statusbar.hidden; + statusbar.hidden = true; + } + else + { + // restoring normal mode (i.e., leaving print preview mode) + SidebarSetState(gChromeState.sidebar); + + // restore the Status Bar + statusbar.hidden = gChromeState.statusbarWasHidden; + } + + // if we are unhiding and sidebar used to be there rebuild it + if (!aHide && gChromeState.sidebar == "visible") + SidebarRebuild(); + + getMailToolbox().hidden = aHide; + document.getElementById("appcontent").collapsed = aHide; +} + +var PrintPreviewListener = { + getPrintPreviewBrowser() + { + var browser = document.getElementById("ppBrowser"); + if (!browser) + { + browser = document.createElement("browser"); + browser.setAttribute("id", "ppBrowser"); + browser.setAttribute("flex", "1"); + browser.setAttribute("disablehistory", "true"); + browser.setAttribute("disablesecurity", "true"); + browser.setAttribute("type", "content"); + document.getElementById("sidebar-parent") + .insertBefore(browser, document.getElementById("appcontent")); + } + return browser; + }, + getSourceBrowser() + { + return GetCurrentEditorElement(); + }, + getNavToolbox() + { + return getMailToolbox(); + }, + onEnter() + { + toggleAffectedChrome(true); + }, + onExit() + { + document.getElementById("ppBrowser").collapsed = true; + toggleAffectedChrome(false); + } +} + +function ToggleWindowLock() +{ + gWindowLocked = !gWindowLocked; + updateComposeItems(); +} + +/* This function will go away soon as now arguments are passed to the window using a object of type nsMsgComposeParams instead of a string */ +function GetArgs(originalData) +{ + var args = new Object(); + + if (originalData == "") + return null; + + var data = ""; + var separator = String.fromCharCode(1); + + var quoteChar = ""; + var prevChar = ""; + var nextChar = ""; + for (let i = 0; i < originalData.length; i++, prevChar = aChar) + { + var aChar = originalData.charAt(i) + var aCharCode = originalData.charCodeAt(i) + if ( i < originalData.length - 1) + nextChar = originalData.charAt(i + 1); + else + nextChar = ""; + + if (aChar == quoteChar && (nextChar == "," || nextChar == "")) + { + quoteChar = ""; + data += aChar; + } + else if ((aCharCode == 39 || aCharCode == 34) && prevChar == "=") //quote or double quote + { + if (quoteChar == "") + quoteChar = aChar; + data += aChar; + } + else if (aChar == ",") + { + if (quoteChar == "") + data += separator; + else + data += aChar + } + else + data += aChar + } + + var pairs = data.split(separator); + + for (let i = pairs.length - 1; i >= 0; i--) + { + var pos = pairs[i].indexOf('='); + if (pos == -1) + continue; + var argname = pairs[i].substring(0, pos); + var argvalue = pairs[i].substring(pos + 1); + if (argvalue.charAt(0) == "'" && argvalue.charAt(argvalue.length - 1) == "'") + args[argname] = argvalue.substring(1, argvalue.length - 1); + else + try { + args[argname] = decodeURIComponent(argvalue); + } catch (e) {args[argname] = argvalue;} + // dump("[" + argname + "=" + args[argname] + "]\n"); + } + return args; +} + +function ComposeFieldsReady() +{ + //If we are in plain text, we need to set the wrap column + if (! gMsgCompose.composeHTML) { + try { + gMsgCompose.editor.wrapWidth = gMsgCompose.wrapLength; + } + catch (e) { + dump("### textEditor.wrapWidth exception text: " + e + " - failed\n"); + } + } + CompFields2Recipients(gMsgCompose.compFields); + SetComposeWindowTitle(); + enableEditableFields(); +} + +// checks if the passed in string is a mailto url, if it is, generates nsIMsgComposeParams +// for the url and returns them. +function handleMailtoArgs(mailtoUrl) +{ + // see if the string is a mailto url....do this by checking the first 7 characters of the string + if (/^mailto:/i.test(mailtoUrl)) + { + // if it is a mailto url, turn the mailto url into a MsgComposeParams object.... + var uri = Services.io.newURI(mailtoUrl); + + if (uri) + return MailServices.compose.getParamsForMailto(uri); + } + + return null; +} +/** + * Handle ESC keypress from composition window for + * notifications with close button in the + * attachmentNotificationBox. + */ +function handleEsc() +{ + let activeElement = document.activeElement; + + // If findbar is visible and the focus is in the message body, + // hide it. (Focus on the findbar is handled by findbar itself). + let findbar = document.getElementById("FindToolbar"); + if (findbar && !findbar.hidden && activeElement.id == "content-frame") { + findbar.close(); + return; + } + + // If there is a notification in the attachmentNotificationBox + // AND focus is in message body, subject field or on the notification, + // hide it. + let notification = document.getElementById("attachmentNotificationBox") + .currentNotification; + if (notification && (activeElement.id == "content-frame" || + activeElement.parentNode.parentNode.id == "msgSubject" || + notification.contains(activeElement) || + activeElement.classList.contains("messageCloseButton"))) { + notification.close(); + } +} + +/** + * On paste or drop, we may want to modify the content before inserting it into + * the editor, replacing file URLs with data URLs when appropriate. + */ +function onPasteOrDrop(e) { + // For paste use e.clipboardData, for drop use e.dataTransfer. + let dataTransfer = ("clipboardData" in e) ? e.clipboardData : e.dataTransfer; + + if (!dataTransfer.types.includes("text/html")) { + return; + } + + if (!gMsgCompose.composeHTML) { + // We're in the plain text editor. Nothing to do here. + return; + } + + let html = dataTransfer.getData("text/html"); + let doc = (new DOMParser()).parseFromString(html, "text/html"); + let tmpD = Services.dirsvc.get("TmpD", Ci.nsIFile); + let pendingConversions = 0; + let needToPreventDefault = true; + for (let img of doc.images) { + if (!/^file:/i.test(img.src)) { + // Doesn't start with file:. Nothing to do here. + continue; + } + + // This may throw if the URL is invalid for the OS. + let nsFile; + try { + nsFile = Services.io.getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler) + .getFileFromURLSpec(img.src); + } catch (ex) { + continue; + } + + if (!nsFile.exists()) { + continue; + } + + if (!tmpD.contains(nsFile)) { + // Not anywhere under the temp dir. + continue; + } + + let contentType = Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromFile(nsFile); + if (!contentType.startsWith("image/")) { + continue; + } + + // If we ever get here, we need to prevent the default paste or drop since + // the code below will do its own insertion. + if (needToPreventDefault) { + e.preventDefault(); + needToPreventDefault = false; + } + + File.createFromNsIFile(nsFile).then(function(file) { + if (file.lastModified < (Date.now() - 60000)) { + // Not put in temp in the last minute. May be something other than + // a copy-paste. Let's not allow that. + return; + } + + let doTheInsert = function() { + // Now run it through sanitation to make sure there wasn't any + // unwanted things in the content. + let ParserUtils = Cc["@mozilla.org/parserutils;1"] + .getService(Ci.nsIParserUtils); + let html2 = ParserUtils.sanitize(doc.documentElement.innerHTML, + ParserUtils.SanitizerAllowStyle); + getBrowser().contentDocument.execCommand("insertHTML", false, html2); + } + + // Everything checks out. Convert file to data URL. + let reader = new FileReader(); + reader.addEventListener("load", function() { + let dataURL = reader.result; + pendingConversions--; + img.src = dataURL; + if (pendingConversions == 0) { + doTheInsert(); + } + }); + + reader.addEventListener("error", function() { + pendingConversions--; + if (pendingConversions == 0) { + doTheInsert(); + } + }); + + pendingConversions++; + reader.readAsDataURL(file); + }); + } +} + +function ComposeStartup(aParams) +{ + var params = null; // New way to pass parameters to the compose window as a nsIMsgComposeParameters object + var args = null; // old way, parameters are passed as a string + gBodyFromArgs = false; + + if (aParams) + params = aParams; + else if (window.arguments && window.arguments[0]) { + try { + if (window.arguments[0] instanceof Ci.nsIMsgComposeParams) + params = window.arguments[0]; + else + params = handleMailtoArgs(window.arguments[0]); + } + catch(ex) { dump("ERROR with parameters: " + ex + "\n"); } + + // if still no dice, try and see if the params is an old fashioned list of string attributes + // XXX can we get rid of this yet? + if (!params) + { + args = GetArgs(window.arguments[0]); + } + } + + // Set the document language to the preference as early as possible. + document.documentElement + .setAttribute("lang", Services.prefs.getCharPref("spellchecker.dictionary")); + + var identityList = GetMsgIdentityElement(); + + document.addEventListener("paste", onPasteOrDrop); + document.addEventListener("drop", onPasteOrDrop); + + if (identityList) + FillIdentityList(identityList); + + if (!params) { + // This code will go away soon as now arguments are passed to the window + // using a object of type nsMsgComposeParams instead of a string. + params = Cc["@mozilla.org/messengercompose/composeparams;1"] + .createInstance(Ci.nsIMsgComposeParams); + params.composeFields = Cc["@mozilla.org/messengercompose/composefields;1"] + .createInstance(Ci.nsIMsgCompFields); + + if (args) { //Convert old fashion arguments into params + var composeFields = params.composeFields; + if (args.bodyislink && args.bodyislink == "true") + params.bodyIsLink = true; + if (args.type) + params.type = args.type; + if (args.format) { + // Only use valid values. + if (args.format == Ci.nsIMsgCompFormat.PlainText || + args.format == Ci.nsIMsgCompFormat.HTML || + args.format == Ci.nsIMsgCompFormat.OppositeOfDefault) + params.format = args.format; + else if (args.format.toLowerCase().trim() == "html") + params.format = Ci.nsIMsgCompFormat.HTML; + else if (args.format.toLowerCase().trim() == "text") + params.format = Ci.nsIMsgCompFormat.PlainText; + } + if (args.originalMsgURI) + params.originalMsgURI = args.originalMsgURI; + if (args.preselectid) + params.identity = getIdentityForKey(args.preselectid); + if (args.from) + composeFields.from = args.from; + if (args.to) + composeFields.to = args.to; + if (args.cc) + composeFields.cc = args.cc; + if (args.bcc) + composeFields.bcc = args.bcc; + if (args.newsgroups) + composeFields.newsgroups = args.newsgroups; + if (args.subject) + composeFields.subject = args.subject; + if (args.attachment) + { + var attachmentList = args.attachment.split(","); + var commandLine = Cc["@mozilla.org/toolkit/command-line;1"] + .createInstance(); + for (let i = 0; i < attachmentList.length; i++) + { + let attachmentStr = attachmentList[i]; + let uri = commandLine.resolveURI(attachmentStr); + let attachment = Cc["@mozilla.org/messengercompose/attachment;1"] + .createInstance(Ci.nsIMsgAttachment); + + if (uri instanceof Ci.nsIFileURL) + { + if (uri.file.exists()) + attachment.size = uri.file.fileSize; + else + attachment = null; + } + + // Only want to attach if a file that exists or it is not a file. + if (attachment) + { + attachment.url = uri.spec; + composeFields.addAttachment(attachment); + } + else + { + let title = sComposeMsgsBundle.getString("errorFileAttachTitle"); + let msg = sComposeMsgsBundle.getFormattedString("errorFileAttachMessage", + [attachmentStr]); + Services.prompt.alert(null, title, msg); + } + } + } + if (args.newshost) + composeFields.newshost = args.newshost; + if (args.message) { + let msgFile = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsIFile); + if (OS.Path.dirname(args.message) == ".") { + let workingDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + args.message = OS.Path.join(workingDir.path, OS.Path.basename(args.message)); + } + msgFile.initWithPath(args.message); + + if (!msgFile.exists()) { + let title = sComposeMsgsBundle.getString("errorFileMessageTitle"); + let msg = sComposeMsgsBundle.getFormattedString("errorFileMessageMessage", + [args.message]); + Services.prompt.alert(null, title, msg); + } else { + let data = ""; + let fstream = null; + let cstream = null; + + try { + fstream = Cc["@mozilla.org/network/file-input-stream;1"] + .createInstance(Ci.nsIFileInputStream); + cstream = Cc["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Ci.nsIConverterInputStream); + fstream.init(msgFile, -1, 0, 0); // Open file in default/read-only mode. + cstream.init(fstream, "UTF-8", 0, 0); + + let str = {}; + let read = 0; + + do { + // Read as much as we can and put it in str.value. + read = cstream.readString(0xffffffff, str); + data += str.value; + } while (read != 0); + } catch (e) { + let title = sComposeMsgsBundle.getString("errorFileMessageTitle"); + let msg = sComposeMsgsBundle.getFormattedString("errorLoadFileMessageMessage", + [args.message]); + Services.prompt.alert(null, title, msg); + + } finally { + if (cstream) + cstream.close(); + if (fstream) + fstream.close(); + } + + if (data) { + let pos = data.search(/\S/); // Find first non-whitespace character. + + if (params.format != Ci.nsIMsgCompFormat.PlainText && + (args.message.endsWith(".htm") || + args.message.endsWith(".html") || + data.substr(pos, 14).toLowerCase() == "<!doctype html" || + data.substr(pos, 5).toLowerCase() == "<html")) { + // We replace line breaks because otherwise they'll be converted + // to <br> in nsMsgCompose::BuildBodyMessageAndSignature(). + // Don't do the conversion if the user asked explicitly for plain + // text. + data = data.replace(/\r?\n/g, " "); + } + gBodyFromArgs = true; + composeFields.body = data; + } + } + } else if (args.body) { + gBodyFromArgs = true; + composeFields.body = args.body; + } + } + } + + gComposeType = params.type; + + // Detect correct identity when missing or mismatched. + // An identity with no email is likely not valid. + // When editing a draft, 'params.identity' is pre-populated with the identity + // that created the draft or the identity owning the draft folder for a + // "foreign", draft, see ComposeMessage() in mailCommands.js. We don't want + // the latter, so use the creator identity which could be null. + if (gComposeType == Ci.nsIMsgCompType.Draft) { + let creatorKey = params.composeFields.creatorIdentityKey; + params.identity = creatorKey ? getIdentityForKey(creatorKey) : null; + } + let from = []; + if (params.composeFields.from) + from = MailServices.headerParser + .parseEncodedHeader(params.composeFields.from, null); + from = (from.length && from[0] && from[0].email) ? + from[0].email.toLowerCase().trim() : null; + if (!params.identity || !params.identity.email || + (from && !emailSimilar(from, params.identity.email))) { + let identities = MailServices.accounts.allIdentities; + let suitableCount = 0; + + // Search for a matching identity. + if (from) { + for (let ident of identities) { + if (ident.email && from == ident.email.toLowerCase()) { + if (suitableCount == 0) + params.identity = ident; + suitableCount++; + if (suitableCount > 1) + break; // No need to find more, it's already not unique. + } + } + } + + if (!params.identity || !params.identity.email) { + let identity = null; + // No preset identity and no match, so use the default account. + let defaultAccount = MailServices.accounts.defaultAccount; + if (defaultAccount) { + identity = defaultAccount.defaultIdentity; + } + if (!identity) { + // Get the first identity we have in the list. + let identitykey = identityList.getItemAtIndex(0).getAttribute("identitykey"); + identity = MailServices.accounts.getIdentity(identitykey); + } + params.identity = identity; + } + + // Warn if no or more than one match was found. + // But don't warn for +suffix additions (a+b@c.com). + if (from && (suitableCount > 1 || + (suitableCount == 0 && !emailSimilar(from, params.identity.email)))) + gComposeNotificationBar.setIdentityWarning(params.identity.identityName); + } + + identityList.selectedItem = + identityList.getElementsByAttribute("identitykey", params.identity.key)[0]; + if (params.composeFields.from) + identityList.value = MailServices.headerParser.parseDecodedHeader(params.composeFields.from)[0].toString(); + LoadIdentity(true); + + // Get the <editor> element to startup an editor + var editorElement = GetCurrentEditorElement(); + + // Remember the original message URI. When editing a draft which is a reply + // or forwarded message, this gets overwritten by the ancestor's message URI + // so the disposition flags ("replied" or "forwarded") can be set on the + // ancestor. + // For our purposes we need the URI of the message being processed, not its + // original ancestor. + gOriginalMsgURI = params.originalMsgURI; + gMsgCompose = MailServices.compose.initCompose(params, window, + editorElement.docShell); + + document.getElementById("returnReceiptMenu") + .setAttribute("checked", gMsgCompose.compFields.returnReceipt); + document.getElementById("dsnMenu") + .setAttribute('checked', gMsgCompose.compFields.DSN); + document.getElementById("cmd_attachVCard") + .setAttribute("checked", gMsgCompose.compFields.attachVCard); + document.getElementById("menu_inlineSpellCheck") + .setAttribute("checked", + Services.prefs.getBoolPref("mail.spellcheck.inline")); + + let editortype = gMsgCompose.composeHTML ? "htmlmail" : "textmail"; + editorElement.makeEditable(editortype, true); + + // setEditorType MUST be call before setContentWindow + if (gMsgCompose.composeHTML) { + initLocalFontFaceMenu(document.getElementById("FontFacePopup")); + } else { + //Remove HTML toolbar, format and insert menus as we are editing in plain + //text mode. + let toolbar = document.getElementById("FormatToolbar"); + toolbar.hidden = true; + toolbar.setAttribute("hideinmenu", "true"); + document.getElementById("outputFormatMenu").setAttribute("hidden", true); + document.getElementById("formatMenu").setAttribute("hidden", true); + document.getElementById("insertMenu").setAttribute("hidden", true); + } + + // Do setup common to Message Composer and Web Composer. + EditorSharedStartup(); + + if (params.bodyIsLink) { + let body = gMsgCompose.compFields.body; + if (gMsgCompose.composeHTML) { + let cleanBody; + try { + cleanBody = decodeURI(body); + } catch(e) { + cleanBody = body; + } + + body = body.replace(/&/g, "&"); + gMsgCompose.compFields.body = + "<br /><a href=\"" + body + "\">" + cleanBody + "</a><br />"; + } else { + gMsgCompose.compFields.body = "\n<" + body + ">\n"; + } + } + + GetMsgSubjectElement().value = gMsgCompose.compFields.subject; + + var attachments = gMsgCompose.compFields.attachments; + while (attachments.hasMoreElements()) { + AddAttachment(attachments.getNext().QueryInterface(Ci.nsIMsgAttachment)); + } + + var event = document.createEvent('Events'); + event.initEvent('compose-window-init', false, true); + document.getElementById("msgcomposeWindow").dispatchEvent(event); + + gMsgCompose.RegisterStateListener(stateListener); + + // Add an observer to be called when document is done loading, + // which creates the editor. + try { + GetCurrentCommandManager().addCommandObserver(gMsgEditorCreationObserver, + "obs_documentCreated"); + + // Load empty page to create the editor + editorElement.webNavigation.loadURI("about:blank", + Ci.nsIWebNavigation.LOAD_FLAGS_NONE, + null, // referrer + null, // post-data stream + null, // HTTP headers + Services.scriptSecurityManager.getSystemPrincipal()); + } catch (e) { + dump(" Failed to startup editor: "+e+"\n"); + } + + // create URI of the folder from draftId + var draftId = gMsgCompose.compFields.draftId; + var folderURI = draftId.substring(0, draftId.indexOf("#")).replace("-message", ""); + + try { + var folder = sRDF.GetResource(folderURI); + + gEditingDraft = (folder instanceof Ci.nsIMsgFolder) && + (folder.flags & Ci.nsMsgFolderFlags.Drafts); + } + catch (ex) { + gEditingDraft = false; + } + + gAutoSaveKickedIn = false; + + gAutoSaveInterval = Services.prefs.getBoolPref("mail.compose.autosave") + ? Services.prefs.getIntPref("mail.compose.autosaveinterval") * 60000 + : 0; + + if (gAutoSaveInterval) + gAutoSaveTimeout = setTimeout(AutoSave, gAutoSaveInterval); +} + +function splitEmailAddress(aEmail) { + let at = aEmail.lastIndexOf("@"); + return (at != -1) ? [aEmail.slice(0, at), aEmail.slice(at + 1)] + : [aEmail, ""]; +} + +// Emails are equal ignoring +suffixes (email+suffix@example.com). +function emailSimilar(a, b) { + if (!a || !b) + return a == b; + a = splitEmailAddress(a.toLowerCase()); + b = splitEmailAddress(b.toLowerCase()); + return a[1] == b[1] && a[0].split("+", 1)[0] == b[0].split("+", 1)[0]; +} + +// The new, nice, simple way of getting notified when a new editor has been created +var gMsgEditorCreationObserver = +{ + observe: function(aSubject, aTopic, aData) + { + if (aTopic == "obs_documentCreated") + { + var editor = GetCurrentEditor(); + var commandManager = GetCurrentCommandManager(); + if (editor && commandManager == aSubject) { + let editorStyle = editor.QueryInterface(Ci.nsIEditorStyleSheets); + // We use addOverrideStyleSheet rather than addStyleSheet so that we get + // a synchronous load, rather than having a late-finishing async load + // mark our editor as modified when the user hasn't typed anything yet, + // but that means the sheet must not @import slow things, especially + // not over the network. + editorStyle.addOverrideStyleSheet("chrome://messenger/skin/messageQuotes.css"); + InitEditor(editor); + } + // Now that we know this document is an editor, update commands now if + // the document has focus, or next time it receives focus via + // CommandUpdate_MsgCompose() + if (gLastWindowToHaveFocus == document.commandDispatcher.focusedWindow) + updateComposeItems(); + else + gLastWindowToHaveFocus = null; + } + } +} + +function WizCallback(state) +{ + if (state){ + ComposeStartup(null); + } + else + { + // The account wizard is still closing so we can't close just yet + setTimeout(MsgComposeCloseWindow, 0); + } +} + +function ComposeLoad() +{ + sComposeMsgsBundle = document.getElementById("bundle_composeMsgs"); + sBrandBundle = document.getElementById("brandBundle"); + + var otherHeaders = Services.prefs.getCharPref("mail.compose.other.header"); + + sRDF = Cc['@mozilla.org/rdf/rdf-service;1'] + .getService(Ci.nsIRDFService); + sNameProperty = sRDF.GetResource("http://home.netscape.com/NC-rdf#Name?sort=true"); + + AddMessageComposeOfflineQuitObserver(); + + if (gLogComposePerformance) + MailServices.compose.TimeStamp("Start initializing the compose window (ComposeLoad)", false); + + msgWindow.notificationCallbacks = new nsMsgBadCertHandler(); + + try { + SetupCommandUpdateHandlers(); + // This will do migration, or create a new account if we need to. + // We also want to open the account wizard if no identities are found + var state = verifyAccounts(WizCallback, true); + + if (otherHeaders) { + var selectNode = document.getElementById('addressCol1#1'); + var otherHeaders_Array = otherHeaders.split(","); + for (let i = 0; i < otherHeaders_Array.length; i++) + selectNode.appendItem(otherHeaders_Array[i] + ":", "addr_other"); + } + if (state) + ComposeStartup(null); + } + catch (ex) { + Cu.reportError(ex); + var errorTitle = sComposeMsgsBundle.getString("initErrorDlogTitle"); + var errorMsg = sComposeMsgsBundle.getString("initErrorDlgMessage"); + Services.prompt.alert(window, errorTitle, errorMsg); + + MsgComposeCloseWindow(); + return; + } + if (gLogComposePerformance) + MailServices.compose.TimeStamp("Done with the initialization (ComposeLoad). Waiting on editor to load about:blank", false); + + // Before and after callbacks for the customizeToolbar code + var mailToolbox = getMailToolbox(); + mailToolbox.customizeInit = MailToolboxCustomizeInit; + mailToolbox.customizeDone = MailToolboxCustomizeDone; + mailToolbox.customizeChange = MailToolboxCustomizeChange; +} + +function ComposeUnload() +{ + // Send notification that the window is going away completely. + document.getElementById("msgcomposeWindow").dispatchEvent( + new Event("compose-window-unload", { bubbles: false, cancelable: false })); + + GetCurrentCommandManager().removeCommandObserver(gMsgEditorCreationObserver, + "obs_documentCreated"); + UnloadCommandUpdateHandlers(); + + // Stop InlineSpellCheckerUI so personal dictionary is saved + EnableInlineSpellCheck(false); + + EditorCleanup(); + + RemoveMessageComposeOfflineQuitObserver(); + + if (gMsgCompose) + gMsgCompose.UnregisterStateListener(stateListener); + if (gAutoSaveTimeout) + clearTimeout(gAutoSaveTimeout); + if (msgWindow) { + msgWindow.closeWindow(); + msgWindow.notificationCallbacks = null; + } + + ReleaseGlobalVariables(); +} + +function ComposeSetCharacterSet(aEvent) +{ + if (gMsgCompose) + SetDocumentCharacterSet(aEvent.target.getAttribute("charset")); + else + dump("Compose has not been created!\n"); +} + +function SetDocumentCharacterSet(aCharset) +{ + // Replace generic Japanese with ISO-2022-JP. + if (aCharset == "Japanese") { + aCharset = "ISO-2022-JP"; + } + gMsgCompose.SetDocumentCharset(aCharset); + SetComposeWindowTitle(); +} + +function GetCharsetUIString() +{ + // The charset here is already the canonical charset (not an alias). + let charset = gMsgCompose.compFields.characterSet; + if (!charset) + return ""; + + if (charset.toLowerCase() != gMsgCompose.compFields.defaultCharacterSet.toLowerCase()) { + try { + return " - " + gCharsetConvertManager.getCharsetTitle(charset); + } + catch(e) { // Not a canonical charset after all... + Cu.reportError("Not charset title for charset=" + charset); + return " - " + charset; + } + } + return ""; +} + +// Add-ons can override this to customize the behavior. +function DoSpellCheckBeforeSend() +{ + return Services.prefs.getBoolPref("mail.SpellCheckBeforeSend"); +} + +/** + * Handles message sending operations. + * @param msgType nsIMsgCompDeliverMode of the operation. + */ +function GenericSendMessage(msgType) { + var msgCompFields = gMsgCompose.compFields; + + Recipients2CompFields(msgCompFields); + var address = GetMsgIdentityElement().value; + address = MailServices.headerParser.makeFromDisplayAddress(address); + msgCompFields.from = MailServices.headerParser.makeMimeHeader([address[0]]); + var subject = GetMsgSubjectElement().value; + msgCompFields.subject = subject; + Attachments2CompFields(msgCompFields); + + if (msgType == Ci.nsIMsgCompDeliverMode.Now || + msgType == Ci.nsIMsgCompDeliverMode.Later || + msgType == Ci.nsIMsgCompDeliverMode.Background) { + //Do we need to check the spelling? + if (DoSpellCheckBeforeSend()) { + // We disable spellcheck for the following -subject line, attachment + // pane, identity and addressing widget therefore we need to explicitly + // focus on the mail body when we have to do a spellcheck. + SetMsgBodyFrameFocus(); + window.cancelSendMessage = false; + window.openDialog("chrome://editor/content/EdSpellCheck.xul", "_blank", + "dialog,close,titlebar,modal,resizable", + true, true, false); + if (window.cancelSendMessage) + return; + } + + // Strip trailing spaces and long consecutive WSP sequences from the + // subject line to prevent getting only WSP chars on a folded line. + var fixedSubject = subject.replace(/\s{74,}/g, " ") + .replace(/\s*$/, ""); + if (fixedSubject != subject) { + subject = fixedSubject; + msgCompFields.subject = fixedSubject; + GetMsgSubjectElement().value = fixedSubject; + } + + // Remind the person if there isn't a subject. + if (subject == "") { + if (Services.prompt.confirmEx( + window, + sComposeMsgsBundle.getString("subjectEmptyTitle"), + sComposeMsgsBundle.getString("subjectEmptyMessage"), + (Services.prompt.BUTTON_TITLE_IS_STRING * + Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_IS_STRING * + Services.prompt.BUTTON_POS_1), + sComposeMsgsBundle.getString("sendWithEmptySubjectButton"), + sComposeMsgsBundle.getString("cancelSendingButton"), + null, null, {value:0}) == 1) { + GetMsgSubjectElement().focus(); + return; + } + } + + // Check if the user tries to send a message to a newsgroup through a mail + // account. + var currentAccountKey = getCurrentAccountKey(); + var account = MailServices.accounts.getAccount(currentAccountKey); + if (!account) { + throw "UNEXPECTED: currentAccountKey '" + currentAccountKey + + "' has no matching account!"; + } + + if (account.incomingServer.type != "nntp" && + msgCompFields.newsgroups != "") { + const kDontAskAgainPref = "mail.compose.dontWarnMail2Newsgroup"; + // Default to ask user if the pref is not set. + var dontAskAgain = Services.prefs.getBoolPref(kDontAskAgainPref); + if (!dontAskAgain) { + var checkbox = {value:false}; + var okToProceed = Services.prompt.confirmCheck( + window, + sComposeMsgsBundle.getString("noNewsgroupSupportTitle"), + sComposeMsgsBundle.getString("recipientDlogMessage"), + sComposeMsgsBundle.getString("CheckMsg"), + checkbox); + + if (!okToProceed) + return; + } + if (checkbox.value) + Services.prefs.setBoolPref(kDontAskAgainPref, true); + + // Remove newsgroups to prevent news_p to be set + // in nsMsgComposeAndSend::DeliverMessage() + msgCompFields.newsgroups = ""; + } + + // Before sending the message, check what to do with HTML message, + // eventually abort. + var convert = DetermineConvertibility(); + var action = DetermineHTMLAction(convert); + // Check if e-mail addresses are complete, in case user has turned off + // autocomplete to local domain. + if (!CheckValidEmailAddress(msgCompFields.to, msgCompFields.cc, msgCompFields.bcc)) + return; + + if (action == Ci.nsIMsgCompSendFormat.AskUser) { + var recommAction = (convert == Ci.nsIMsgCompConvertible.No) + ? Ci.nsIMsgCompSendFormat.AskUser + : Ci.nsIMsgCompSendFormat.PlainText; + var result2 = {action:recommAction, convertible:convert, abort:false}; + window.openDialog("chrome://messenger/content/messengercompose/askSendFormat.xul", + "askSendFormatDialog", "chrome,modal,titlebar,centerscreen", + result2); + if (result2.abort) + return; + action = result2.action; + } + + // We will remember the users "send format" decision in the address + // collector code (see nsAbAddressCollector::CollectAddress()) + // by using msgCompFields.forcePlainText and + // msgCompFields.useMultipartAlternative to determine the + // nsIAbPreferMailFormat (unknown, plaintext, or html). + // If the user sends both, we remember html. + switch (action) { + case Ci.nsIMsgCompSendFormat.PlainText: + msgCompFields.forcePlainText = true; + msgCompFields.useMultipartAlternative = false; + break; + case Ci.nsIMsgCompSendFormat.HTML: + msgCompFields.forcePlainText = false; + msgCompFields.useMultipartAlternative = false; + break; + case Ci.nsIMsgCompSendFormat.Both: + msgCompFields.forcePlainText = false; + msgCompFields.useMultipartAlternative = true; + break; + default: + throw new Error("Invalid nsIMsgCompSendFormat action; action=" + action); + } + } + + // Hook for extra compose pre-processing. + Services.obs.notifyObservers(window, "mail:composeOnSend"); + + var originalCharset = gMsgCompose.compFields.characterSet; + // Check if the headers of composing mail can be converted to a mail charset. + if (msgType == Ci.nsIMsgCompDeliverMode.Now || + msgType == Ci.nsIMsgCompDeliverMode.Later || + msgType == Ci.nsIMsgCompDeliverMode.Background || + msgType == Ci.nsIMsgCompDeliverMode.Save || + msgType == Ci.nsIMsgCompDeliverMode.SaveAsDraft || + msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft || + msgType == Ci.nsIMsgCompDeliverMode.SaveAsTemplate) { + var fallbackCharset = new Object; + // Check encoding, switch to UTF-8 if the default encoding doesn't fit + // and disable_fallback_to_utf8 isn't set for this encoding. + if (!gMsgCompose.checkCharsetConversion(getCurrentIdentity(), + fallbackCharset)) { + let disableFallback = Services.prefs + .getBoolPref("mailnews.disable_fallback_to_utf8." + originalCharset, false); + if (disableFallback) + msgCompFields.needToCheckCharset = false; + else + fallbackCharset.value = "UTF-8"; + } + + if (fallbackCharset && + fallbackCharset.value && fallbackCharset.value != "") + gMsgCompose.SetDocumentCharset(fallbackCharset.value); + } + try { + // Just before we try to send the message, fire off the + // compose-send-message event for listeners such as smime so they can do + // any pre-security work such as fetching certificates before sending. + var event = document.createEvent('UIEvents'); + event.initEvent('compose-send-message', false, true); + var msgcomposeWindow = document.getElementById("msgcomposeWindow"); + msgcomposeWindow.setAttribute("msgtype", msgType); + msgcomposeWindow.dispatchEvent(event); + if (event.defaultPrevented) + throw Cr.NS_ERROR_ABORT; + + gAutoSaving = (msgType == Ci.nsIMsgCompDeliverMode.AutoSaveAsDraft); + if (!gAutoSaving) { + // Disable the ui if we're not auto-saving. + gWindowLocked = true; + disableEditableFields(); + updateComposeItems(); + } else { + // If we're auto saving, mark the body as not changed here, and not + // when the save is done, because the user might change it between now + // and when the save is done. + SetContentAndBodyAsUnmodified(); + } + + var progress = Cc["@mozilla.org/messenger/progress;1"] + .createInstance(Ci.nsIMsgProgress); + if (progress) { + progress.registerListener(progressListener); + gSendOrSaveOperationInProgress = true; + } + msgWindow.domWindow = window; + msgWindow.rootDocShell.allowAuth = true; + gMsgCompose.SendMsg(msgType, getCurrentIdentity(), getCurrentAccountKey(), + msgWindow, progress); + } + catch (ex) { + Cu.reportError("GenericSendMessage FAILED: " + ex); + gWindowLocked = false; + enableEditableFields(); + updateComposeItems(); + } + if (gMsgCompose && originalCharset != gMsgCompose.compFields.characterSet) + SetDocumentCharacterSet(gMsgCompose.compFields.characterSet); +} + +/** + * Check if the given address is valid (contains a @). + * + * @param aAddress The address string to check. + */ +function isValidAddress(aAddress) { + return (aAddress.includes("@", 1) && !aAddress.endsWith("@")); +} + +/** + * Keep the Send buttons disabled until any recipient is entered. + */ +function updateSendLock() { + gSendLocked = true; + if (!gMsgCompose) + return; + + // Helper function to check for a valid list name. + function isValidListName(aInput) { + let listNames = MimeParser.parseHeaderField(aInput, + MimeParser.HEADER_ADDRESS); + return listNames.length > 0 && + MailServices.ab.mailListNameExists(listNames[0].name); + } + + const mailTypes = [ "addr_to", "addr_cc", "addr_bcc" ]; + + // Enable the send buttons if anything usable was entered into at least one + // recipient field. + for (let row = 1; row <= top.MAX_RECIPIENTS; row ++) { + let popupValue = awGetPopupElement(row).value; + let inputValue = awGetInputElement(row).value.trim(); + // Check for a valid looking email address or a valid mailing list name + // from one of our addressbooks. + if ((mailTypes.includes(popupValue) && + (isValidAddress(inputValue) || isValidListName(inputValue))) || + ((popupValue == "addr_newsgroups") && (inputValue != ""))) { + gSendLocked = false; + break; + } + } +} + +function CheckValidEmailAddress(aTo, aCC, aBCC) +{ + var invalidStr = null; + // crude check that the to, cc, and bcc fields contain at least one '@'. + // We could parse each address, but that might be overkill. + if (aTo.length > 0 && (aTo.indexOf("@") <= 0 && aTo.toLowerCase() != "postmaster" || aTo.indexOf("@") == aTo.length - 1)) + invalidStr = aTo; + else if (aCC.length > 0 && (aCC.indexOf("@") <= 0 && aCC.toLowerCase() != "postmaster" || aCC.indexOf("@") == aCC.length - 1)) + invalidStr = aCC; + else if (aBCC.length > 0 && (aBCC.indexOf("@") <= 0 && aBCC.toLowerCase() != "postmaster" || aBCC.indexOf("@") == aBCC.length - 1)) + invalidStr = aBCC; + if (invalidStr) + { + var errorTitle = sComposeMsgsBundle.getString("addressInvalidTitle"); + var errorMsg = sComposeMsgsBundle.getFormattedString("addressInvalid", [invalidStr], 1); + Services.prompt.alert(window, errorTitle, errorMsg); + return false; + } + return true; +} + +function SendMessage() +{ + let sendInBackground = Services.prefs.getBoolPref("mailnews.sendInBackground"); + if (sendInBackground && AppConstants.platform != "macosx") { + let enumerator = Services.wm.getEnumerator(null); + let count = 0; + while (enumerator.hasMoreElements() && count < 2) + { + enumerator.getNext(); + count++; + } + if (count == 1) + sendInBackground = false; + } + GenericSendMessage(sendInBackground ? nsIMsgCompDeliverMode.Background + : nsIMsgCompDeliverMode.Now); +} + +function SendMessageWithCheck() +{ + var warn = Services.prefs.getBoolPref("mail.warn_on_send_accel_key"); + + if (warn) { + var checkValue = {value:false}; + var buttonPressed = Services.prompt.confirmEx(window, + sComposeMsgsBundle.getString('sendMessageCheckWindowTitle'), + sComposeMsgsBundle.getString('sendMessageCheckLabel'), + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1), + sComposeMsgsBundle.getString('sendMessageCheckSendButtonLabel'), + null, null, + sComposeMsgsBundle.getString('CheckMsg'), + checkValue); + if (buttonPressed != 0) { + return; + } + if (checkValue.value) { + Services.prefs.setBoolPref("mail.warn_on_send_accel_key", false); + } + } + + if (Services.io.offline) + SendMessageLater(); + else + SendMessage(); +} + +function SendMessageLater() +{ + GenericSendMessage(nsIMsgCompDeliverMode.Later); +} + +function Save() +{ + switch (defaultSaveOperation) + { + case "file" : SaveAsFile(false); break; + case "template" : SaveAsTemplate(false); break; + default : SaveAsDraft(false); break; + } +} + +function SaveAsFile(saveAs) +{ + var subject = GetMsgSubjectElement().value; + GetCurrentEditorElement().contentDocument.title = subject; + + if (gMsgCompose.bodyConvertible() == nsIMsgCompConvertible.Plain) + SaveDocument(saveAs, false, "text/plain"); + else + SaveDocument(saveAs, false, "text/html"); + defaultSaveOperation = "file"; +} + +function SaveAsDraft() +{ + GenericSendMessage(nsIMsgCompDeliverMode.SaveAsDraft); + defaultSaveOperation = "draft"; + + gAutoSaveKickedIn = false; + gEditingDraft = true; +} + +function SaveAsTemplate() +{ + let savedReferences = null; + if (gMsgCompose && gMsgCompose.compFields) { + // Clear References header. When we use the template, we don't want that + // header, yet, "edit as new message" maintains it. So we need to clear + // it when saving the template. + // Note: The In-Reply-To header is the last entry in the references header, + // so it will get cleared as well. + savedReferences = gMsgCompose.compFields.references; + gMsgCompose.compFields.references = null; + } + + GenericSendMessage(nsIMsgCompDeliverMode.SaveAsTemplate); + defaultSaveOperation = "template"; + + if (savedReferences) + gMsgCompose.compFields.references = savedReferences; + + gAutoSaveKickedIn = false; + gEditingDraft = false; +} + +// Sets the additional FCC, in addition to the default FCC. +function MessageFcc(aFolder) { + if (!gMsgCompose) + return; + + var msgCompFields = gMsgCompose.compFields; + if (!msgCompFields) + return; + + // Get the uri for the folder to FCC into. + var fccURI = aFolder.URI; + msgCompFields.fcc2 = (msgCompFields.fcc2 == fccURI) ? "nocopy://" : fccURI; +} + +function updatePriorityMenu(priorityMenu) +{ + var priority = (gMsgCompose && gMsgCompose.compFields && gMsgCompose.compFields.priority) || "Normal"; + priorityMenu.getElementsByAttribute("value", priority)[0].setAttribute("checked", "true"); +} + +function PriorityMenuSelect(target) +{ + if (gMsgCompose) + { + var msgCompFields = gMsgCompose.compFields; + if (msgCompFields) + msgCompFields.priority = target.getAttribute("value"); + } +} + +function OutputFormatMenuSelect(target) +{ + if (gMsgCompose) + { + var msgCompFields = gMsgCompose.compFields; + var toolbar = document.getElementById("FormatToolbar"); + var format_menubar = document.getElementById("formatMenu"); + var insert_menubar = document.getElementById("insertMenu"); + + if (msgCompFields) + switch (target.getAttribute('id')) + { + case "format_auto": gSendFormat = nsIMsgCompSendFormat.AskUser; break; + case "format_plain": gSendFormat = nsIMsgCompSendFormat.PlainText; break; + case "format_html": gSendFormat = nsIMsgCompSendFormat.HTML; break; + case "format_both": gSendFormat = nsIMsgCompSendFormat.Both; break; + } + gHideMenus = (gSendFormat == nsIMsgCompSendFormat.PlainText); + format_menubar.hidden = gHideMenus; + insert_menubar.hidden = gHideMenus; + if (gHideMenus) { + gFormatToolbarHidden = toolbar.hidden; + toolbar.hidden = true; + toolbar.setAttribute("hideinmenu", "true"); + } else { + toolbar.hidden = gFormatToolbarHidden; + toolbar.removeAttribute("hideinmenu"); + } + } +} + +function SelectAddress() +{ + var msgCompFields = gMsgCompose.compFields; + + Recipients2CompFields(msgCompFields); + + var toAddress = msgCompFields.to; + var ccAddress = msgCompFields.cc; + var bccAddress = msgCompFields.bcc; + + dump("toAddress: " + toAddress + "\n"); + window.openDialog("chrome://messenger/content/addressbook/abSelectAddressesDialog.xul", + "", + "chrome,resizable,titlebar,modal", + {composeWindow:top.window, + msgCompFields:msgCompFields, + toAddress:toAddress, + ccAddress:ccAddress, + bccAddress:bccAddress}); + // We have to set focus to the addressingwidget because we seem to loose focus often + // after opening the SelectAddresses Dialog- bug # 89950 + AdjustFocus(); +} + +// walk through the recipients list and add them to the inline spell checker ignore list +function addRecipientsToIgnoreList(aAddressesToAdd) +{ + if (InlineSpellCheckerUI.enabled) + { + // break the list of potentially many recipients back into individual names + var emailAddresses = {}; + var names = {}; + var fullNames = {}; + var numAddresses = + MailServices.headerParser.parseHeadersWithArray(aAddressesToAdd, + emailAddresses, names, + fullNames); + var tokenizedNames = []; + + // each name could consist of multiple words delimited by commas and/or spaces. + // i.e. Green Lantern or Lantern,Green. + for (let i = 0; i < names.value.length; i++) + { + if (!names.value[i]) + continue; + var splitNames = names.value[i].match(/[^\s,]+/g); + if (splitNames) + tokenizedNames = tokenizedNames.concat(splitNames); + } + + if (InlineSpellCheckerUI.mInlineSpellChecker.spellCheckPending) + { + // spellchecker is enabled, but we must wait for its init to complete + Services.obs.addObserver(function observe(subject, topic, data) { + if (subject == gMsgCompose.editor) + { + Services.obs.removeObserver(observe, topic); + InlineSpellCheckerUI.mInlineSpellChecker.ignoreWords(tokenizedNames); + } + }, "inlineSpellChecker-spellCheck-ended"); + } + else + { + InlineSpellCheckerUI.mInlineSpellChecker.ignoreWords(tokenizedNames); + } + } +} + +function onAddressColCommand(aWidgetId) { + gContentChanged = true; + awSetAutoComplete(aWidgetId.slice(aWidgetId.lastIndexOf('#') + 1)); + updateSendCommands(true); +} + +/** + * Called if the list of recipients changed in any way. + * + * @param aAutomatic Set to true if the change of recipients was invoked + * programatically and should not be considered a change + * of message content. + */ +function onRecipientsChanged(aAutomatic) { + if (!aAutomatic) { + gContentChanged = true; + setupAutocomplete(); + } + updateSendCommands(true); +} + +function InitLanguageMenu() +{ + var languageMenuList = document.getElementById("languageMenuList"); + if (!languageMenuList) + return; + + var spellChecker = Cc["@mozilla.org/spellchecker/engine;1"] + .getService(mozISpellCheckingEngine); + // Get the list of dictionaries from the spellchecker. + var dictList = spellChecker.getDictionaryList(); + var count = dictList.length; + + // If dictionary count hasn't changed then no need to update the menu. + if (sDictCount == count) + return; + + // Store current dictionary count. + sDictCount = count; + + // Load the language string bundle that will help us map + // RFC 1766 strings to UI strings. + var languageBundle = document.getElementById("languageBundle"); + var isoStrArray; + var langId; + var langLabel; + + for (let i = 0; i < count; i++) + { + try + { + langId = dictList[i]; + isoStrArray = dictList[i].split(/[-_]/); + + if (languageBundle && isoStrArray[0]) + langLabel = languageBundle.getString(isoStrArray[0].toLowerCase()); + + // the user needs to be able to distinguish between the UK English dictionary + // and say the United States English Dictionary. If we have a isoStr value then + // wrap it in parentheses and append it to the menu item string. i.e. + // English (US) and English (UK) + if (!langLabel) + langLabel = langId; + // if we have a language ID like US or UK, append it to the menu item, and any sub-variety + else if (isoStrArray.length > 1 && isoStrArray[1]) { + langLabel += ' (' + isoStrArray[1]; + if (isoStrArray.length > 2 && isoStrArray[2]) + langLabel += '-' + isoStrArray[2]; + langLabel += ')'; + } + } + catch (ex) + { + // getString throws an exception when a key is not found in the + // bundle. In that case, just use the original dictList string. + langLabel = langId; + } + dictList[i] = [langLabel, langId]; + } + + // sort by locale-aware collation + dictList.sort( + function compareFn(a, b) + { + return a[0].localeCompare(b[0]); + } + ); + + // Remove any languages from the list. + while (languageMenuList.hasChildNodes()) + languageMenuList.lastChild.remove(); + + for (let i = 0; i < count; i++) + { + var item = document.createElement("menuitem"); + item.setAttribute("label", dictList[i][0]); + item.setAttribute("value", dictList[i][1]); + item.setAttribute("type", "radio"); + languageMenuList.appendChild(item); + } +} + +function OnShowDictionaryMenu(aTarget) +{ + InitLanguageMenu(); + var spellChecker = InlineSpellCheckerUI.mInlineSpellChecker.spellChecker; + var curLang = spellChecker.GetCurrentDictionary(); + var languages = aTarget.getElementsByAttribute("value", curLang); + if (languages.length > 0) + languages[0].setAttribute("checked", true); +} + +function ChangeLanguage(event) +{ + // We need to change the dictionary language and if we are using inline spell check, + // recheck the message + var spellChecker = InlineSpellCheckerUI.mInlineSpellChecker.spellChecker; + if (spellChecker.GetCurrentDictionary() != event.target.value) + { + spellChecker.SetCurrentDictionary(event.target.value); + + ComposeChangeLanguage(event.target.value) + } + event.stopPropagation(); +} + +function ComposeChangeLanguage(aLang) +{ + if (document.documentElement.getAttribute("lang") != aLang) { + + // Update the document language as well. + // This is needed to synchronize the subject. + document.documentElement.setAttribute("lang", aLang); + + // Update spellchecker pref + Services.prefs.setCharPref("spellchecker.dictionary", aLang); + + // Now check the document and the subject over again with the new + // dictionary. + if (InlineSpellCheckerUI.enabled) { + InlineSpellCheckerUI.mInlineSpellChecker.spellCheckRange(null); + + // Also force a recheck of the subject. The spell checker for the subject + // isn't always ready yet. Usually throws unless the subject was selected + // at least once. So don't auto-create it, hence pass 'false'. + let inlineSpellChecker = + GetMsgSubjectElement().editor.getInlineSpellChecker(false); + if (inlineSpellChecker) { + inlineSpellChecker.spellCheckRange(null); + } + } + } +} + +function ToggleReturnReceipt(target) +{ + var msgCompFields = gMsgCompose.compFields; + if (msgCompFields) + { + msgCompFields.returnReceipt = ! msgCompFields.returnReceipt; + target.setAttribute('checked', msgCompFields.returnReceipt); + gReceiptOptionChanged = true; + } +} + +function ToggleDSN(target) +{ + var msgCompFields = gMsgCompose.compFields; + + if (msgCompFields) + { + msgCompFields.DSN = !msgCompFields.DSN; + target.setAttribute('checked', msgCompFields.DSN); + gDSNOptionChanged = true; + } +} + +function ToggleAttachVCard(target) +{ + var msgCompFields = gMsgCompose.compFields; + if (msgCompFields) + { + msgCompFields.attachVCard = ! msgCompFields.attachVCard; + target.setAttribute('checked', msgCompFields.attachVCard); + gAttachVCardOptionChanged = true; + } +} + +function FillIdentityList(menulist) +{ + var accounts = FolderUtils.allAccountsSorted(true); + + for (let acc = 0; acc < accounts.length; acc++) + { + let account = accounts[acc]; + let identities = account.identities; + + if (identities.length == 0) + continue; + + for (let i = 0; i < identities.length; i++) + { + let identity = identities[i]; + let item = menulist.appendItem(identity.identityName, + identity.fullAddress, + account.incomingServer.prettyName); + item.setAttribute("identitykey", identity.key); + item.setAttribute("accountkey", account.key); + if (i == 0) + { + // Mark the first identity as default. + item.setAttribute("default", "true"); + } + } + } +} + +function getCurrentAccountKey() +{ + // get the accounts key + var identityList = GetMsgIdentityElement(); + return identityList.selectedItem.getAttribute("accountkey"); +} + +function getCurrentIdentityKey() +{ + // get the identity key + var identityList = GetMsgIdentityElement(); + return identityList.selectedItem.getAttribute("identitykey"); +} + +function getIdentityForKey(key) +{ + return MailServices.accounts.getIdentity(key); +} + +function getCurrentIdentity() +{ + return getIdentityForKey(getCurrentIdentityKey()); +} + +function AdjustFocus() +{ + let element = awGetInputElement(awGetNumberOfRecipients()); + if (element.value == "") { + awSetFocusTo(element); + } + else + { + element = GetMsgSubjectElement(); + if (element.value == "") { + element.focus(); + } + else { + SetMsgBodyFrameFocus(); + } + } +} + +function SetComposeWindowTitle() +{ + var newTitle = GetMsgSubjectElement().value; + + if (newTitle == "" ) + newTitle = sComposeMsgsBundle.getString("defaultSubject"); + + newTitle += GetCharsetUIString(); + document.title = sComposeMsgsBundle.getString("windowTitlePrefix") + " " + newTitle; +} + +// Check for changes to document and allow saving before closing +// This is hooked up to the OS's window close widget (e.g., "X" for Windows) +function ComposeCanClose() +{ + if (gSendOrSaveOperationInProgress) + { + var brandShortName = sBrandBundle.getString("brandShortName"); + + var promptTitle = sComposeMsgsBundle.getString("quitComposeWindowTitle"); + var promptMsg = sComposeMsgsBundle.getFormattedString("quitComposeWindowMessage2", + [brandShortName], 1); + var quitButtonLabel = sComposeMsgsBundle.getString("quitComposeWindowQuitButtonLabel2"); + var waitButtonLabel = sComposeMsgsBundle.getString("quitComposeWindowWaitButtonLabel2"); + + if (Services.prompt.confirmEx(window, promptTitle, promptMsg, + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1), + waitButtonLabel, quitButtonLabel, null, null, {value:0}) == 1) + { + gMsgCompose.abort(); + return true; + } + return false; + } + + // Returns FALSE only if user cancels save action + if (gContentChanged || gMsgCompose.bodyModified || (gAutoSaveKickedIn && !gEditingDraft)) + { + // call window.focus, since we need to pop up a dialog + // and therefore need to be visible (to prevent user confusion) + window.focus(); + let draftFolderURI = gCurrentIdentity.draftFolder; + let draftFolderName = MailUtils.getFolderForURI(draftFolderURI).prettyName; + switch (Services.prompt.confirmEx(window, + sComposeMsgsBundle.getString("saveDlogTitle"), + sComposeMsgsBundle.getFormattedString("saveDlogMessages3", [draftFolderName]), + (Services.prompt.BUTTON_TITLE_SAVE * Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1) + + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_2), + null, + null, + sComposeMsgsBundle.getString("discardButtonLabel"), + null, {value:0})) + { + case 0: //Save + // we can close immediately if we already autosaved the draft + if (!gContentChanged && !gMsgCompose.bodyModified) + break; + gCloseWindowAfterSave = true; + GenericSendMessage(nsIMsgCompDeliverMode.AutoSaveAsDraft); + return false; + case 1: //Cancel + return false; + case 2: //Don't Save + // only delete the draft if we didn't start off editing a draft + if (!gEditingDraft && gAutoSaveKickedIn) + RemoveDraft(); + break; + } + } + + return true; +} + +function RemoveDraft() +{ + try + { + var draftId = gMsgCompose.compFields.draftId; + var msgKey = draftId.substr(draftId.indexOf('#') + 1); + var folder = sRDF.GetResource(gMsgCompose.savedFolderURI); + try { + if (folder instanceof Ci.nsIMsgFolder) + { + let msg = folder.GetMessageHeader(msgKey); + folder.deleteMessages([msg], null, true, false, null, false); + } + } + catch (ex) // couldn't find header - perhaps an imap folder. + { + if (folder instanceof Ci.nsIMsgImapMailFolder) + { + const kImapMsgDeletedFlag = 0x0008; + folder.storeImapFlags(kImapMsgDeletedFlag, true, [msgKey], null); + } + } + } catch (ex) {} +} + +function SetContentAndBodyAsUnmodified() +{ + gMsgCompose.bodyModified = false; + gContentChanged = false; +} + +function MsgComposeCloseWindow() +{ + if (gMsgCompose) + gMsgCompose.CloseWindow(); + else + window.close(); +} + +// attachedLocalFile must be a nsIFile +function SetLastAttachDirectory(attachedLocalFile) +{ + try { + var file = attachedLocalFile.QueryInterface(Ci.nsIFile); + var parent = file.parent.QueryInterface(Ci.nsIFile); + + Services.prefs.setComplexValue(kComposeAttachDirPrefName, + Ci.nsIFile, parent); + } + catch (ex) { + dump("error: SetLastAttachDirectory failed: " + ex + "\n"); + } +} + +function AttachFile() +{ + //Get file using nsIFilePicker and convert to URL + const nsIFilePicker = Ci.nsIFilePicker; + let fp = Cc["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + fp.init(window, sComposeMsgsBundle.getString("chooseFileToAttach"), + nsIFilePicker.modeOpenMultiple); + let lastDirectory = GetLocalFilePref(kComposeAttachDirPrefName); + if (lastDirectory) + fp.displayDirectory = lastDirectory; + + fp.appendFilters(nsIFilePicker.filterAll); + fp.open(rv => { + if (rv != nsIFilePicker.returnOK || !fp.files) { + return; + } + try { + let firstAttachedFile = AttachFiles(fp.files); + if (firstAttachedFile) { + SetLastAttachDirectory(firstAttachedFile); + } + } + catch (ex) { + dump("failed to get attachments: " + ex + "\n"); + } + }); +} + +function AttachFiles(attachments) +{ + if (!attachments || !attachments.hasMoreElements()) + return null; + + var firstAttachedFile = null; + + while (attachments.hasMoreElements()) { + var currentFile = attachments.getNext().QueryInterface(Ci.nsIFile); + + if (!firstAttachedFile) { + firstAttachedFile = currentFile; + } + + var fileHandler = Services.io.getProtocolHandler("file").QueryInterface(Ci.nsIFileProtocolHandler); + var currentAttachment = fileHandler.getURLSpecFromFile(currentFile); + + if (!DuplicateFileCheck(currentAttachment)) { + var attachment = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment); + attachment.url = currentAttachment; + attachment.size = currentFile.fileSize; + AddAttachment(attachment); + gContentChanged = true; + } + } + return firstAttachedFile; +} + +function AddAttachment(attachment) +{ + if (attachment && attachment.url) + { + var bucket = GetMsgAttachmentElement(); + var item = document.createElement("listitem"); + + if (!attachment.name) + attachment.name = gMsgCompose.AttachmentPrettyName(attachment.url, attachment.urlCharset); + + // for security reasons, don't allow *-message:// uris to leak out + // we don't want to reveal the .slt path (for mailbox://), or the username or hostname + var messagePrefix = /^mailbox-message:|^imap-message:|^news-message:/i; + if (messagePrefix.test(attachment.name)) + attachment.name = sComposeMsgsBundle.getString("messageAttachmentSafeName"); + else { + // for security reasons, don't allow mail protocol uris to leak out + // we don't want to reveal the .slt path (for mailbox://), or the username or hostname + var mailProtocol = /^file:|^mailbox:|^imap:|^s?news:/i; + if (mailProtocol.test(attachment.name)) + attachment.name = sComposeMsgsBundle.getString("partAttachmentSafeName"); + } + + var nameAndSize = attachment.name; + if (attachment.size != -1) + nameAndSize += " (" + gMessenger.formatFileSize(attachment.size) + ")"; + item.setAttribute("label", nameAndSize); //use for display only + item.attachment = attachment; //full attachment object stored here + try { + item.setAttribute("tooltiptext", decodeURI(attachment.url)); + } catch(e) { + item.setAttribute("tooltiptext", attachment.url); + } + item.setAttribute("class", "listitem-iconic"); + item.setAttribute("image", "moz-icon:" + attachment.url); + item.setAttribute("crop", "center"); + bucket.appendChild(item); + } +} + +function SelectAllAttachments() +{ + var bucketList = GetMsgAttachmentElement(); + if (bucketList) + bucketList.selectAll(); +} + +function MessageHasAttachments() +{ + var bucketList = GetMsgAttachmentElement(); + if (bucketList) { + return (bucketList && bucketList.hasChildNodes() && (bucketList == top.document.commandDispatcher.focusedElement)); + } + return false; +} + +function MessageGetNumSelectedAttachments() +{ + var bucketList = GetMsgAttachmentElement(); + return (bucketList) ? bucketList.selectedItems.length : 0; +} + +function AttachPage() +{ + var params = { action: "5", url: null }; + window.openDialog("chrome://communicator/content/openLocation.xul", + "_blank", "chrome,close,titlebar,modal", params); + if (params.url) + { + var attachment = + Cc["@mozilla.org/messengercompose/attachment;1"] + .createInstance(Ci.nsIMsgAttachment); + attachment.url = params.url; + AddAttachment(attachment); + } +} + +function DuplicateFileCheck(FileUrl) +{ + var bucket = GetMsgAttachmentElement(); + for (let i = 0; i < bucket.childNodes.length; i++) + { + let attachment = bucket.childNodes[i].attachment; + if (attachment) + { + if (FileUrl == attachment.url) + return true; + } + } + + return false; +} + +function Attachments2CompFields(compFields) +{ + var bucket = GetMsgAttachmentElement(); + + //First, we need to clear all attachment in the compose fields + compFields.removeAttachments(); + + for (let i = 0; i < bucket.childNodes.length; i++) + { + let attachment = bucket.childNodes[i].attachment; + if (attachment) + compFields.addAttachment(attachment); + } +} + +function RemoveAllAttachments() +{ + var child; + var bucket = GetMsgAttachmentElement(); + while (bucket.hasChildNodes()) + { + child = bucket.removeChild(bucket.lastChild); + // Let's release the attachment object hold by the node else it won't go away until the window is destroyed + child.attachment = null; + } +} + +function RemoveSelectedAttachment() +{ + var child; + var bucket = GetMsgAttachmentElement(); + if (bucket.selectedItems.length > 0) { + for (let i = bucket.selectedItems.length - 1; i >= 0; i--) + { + child = bucket.removeChild(bucket.selectedItems[i]); + // Let's release the attachment object hold by the node else it won't go away until the window is destroyed + child.attachment = null; + } + gContentChanged = true; + } +} + +function RenameSelectedAttachment() +{ + var bucket = GetMsgAttachmentElement(); + if (bucket.selectedItems.length != 1) + return; // not one attachment selected + + var item = bucket.getSelectedItem(0); + var attachmentName = {value: item.attachment.name}; + if (Services.prompt.prompt( + window, + sComposeMsgsBundle.getString("renameAttachmentTitle"), + sComposeMsgsBundle.getString("renameAttachmentMessage"), + attachmentName, + null, + {value: 0})) + { + var modifiedAttachmentName = attachmentName.value; + if (modifiedAttachmentName == "") + return; // name was not filled, bail out + + var nameAndSize = modifiedAttachmentName; + if (item.attachment.size != -1) + nameAndSize += " (" + gMessenger.formatFileSize(item.attachment.size) + ")"; + item.label = nameAndSize; + item.attachment.name = modifiedAttachmentName; + gContentChanged = true; + } +} + +function FocusOnFirstAttachment() +{ + var bucketList = GetMsgAttachmentElement(); + + if (bucketList && bucketList.hasChildNodes()) + bucketList.selectItem(bucketList.firstChild); +} + +function AttachmentElementHasItems() +{ + var element = GetMsgAttachmentElement(); + return element ? element.childNodes.length : 0; +} + +function OpenSelectedAttachment() +{ + let bucket = document.getElementById("attachmentBucket"); + if (bucket.selectedItems.length == 1) { + let attachmentUrl = bucket.getSelectedItem(0).attachment.url; + + let messagePrefix = /^mailbox-message:|^imap-message:|^news-message:/i; + if (messagePrefix.test(attachmentUrl)) { + // We must be dealing with a forwarded attachment, treat this special. + let msgHdr = gMessenger.msgHdrFromURI(attachmentUrl); + if (msgHdr) { + MailUtils.openMessageInNewWindow(msgHdr); + } + } else { + // Turn the URL into a nsIURI object then open it. + let uri = Services.io.newURI(attachmentUrl); + if (uri) { + let channel = Services.io.newChannelFromURI(uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + if (channel) { + let uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader); + uriLoader.openURI(channel, true, new nsAttachmentOpener()); + } + } + } + } // if one attachment selected +} + +function nsAttachmentOpener() +{ +} + +nsAttachmentOpener.prototype = +{ + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIURIContentListener) || + iid.equals(Ci.nsIInterfaceRequestor) || + iid.equals(Ci.nsISupports)) { + return this; + } + throw Cr.NS_NOINTERFACE; + }, + + doContent: function(contentType, isContentPreferred, request, contentHandler) + { + return false; + }, + + isPreferred: function(contentType, desiredContentType) + { + return false; + }, + + canHandleContent: function(contentType, isContentPreferred, desiredContentType) + { + return false; + }, + + getInterface: function(iid) + { + if (iid.equals(Ci.nsIDOMWindow)) { + return window; + } + + if (iid.equals(Ci.nsIDocShell)) { + return window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + } + + return this.QueryInterface(iid); + }, + + loadCookie: null, + parentContentListener: null +} + +function DetermineHTMLAction(convertible) +{ + try { + gMsgCompose.expandMailingLists(); + } catch(ex) { + dump("gMsgCompose.expandMailingLists failed: " + ex + "\n"); + } + + if (!gMsgCompose.composeHTML) + { + return nsIMsgCompSendFormat.PlainText; + } + + if (gSendFormat == nsIMsgCompSendFormat.AskUser) + { + return gMsgCompose.determineHTMLAction(convertible); + } + + return gSendFormat; +} + +function DetermineConvertibility() +{ + if (!gMsgCompose.composeHTML) + return nsIMsgCompConvertible.Plain; + + try { + return gMsgCompose.bodyConvertible(); + } catch(ex) {} + return nsIMsgCompConvertible.No; +} + +function LoadIdentity(startup) +{ + var identityElement = GetMsgIdentityElement(); + var prevIdentity = gCurrentIdentity; + + if (identityElement) { + identityElement.value = identityElement.selectedItem.value; + + var idKey = identityElement.selectedItem.getAttribute("identitykey"); + gCurrentIdentity = MailServices.accounts.getIdentity(idKey); + + let accountKey = null; + if (identityElement.selectedItem) + accountKey = identityElement.selectedItem.getAttribute("accountkey"); + + let maxRecipients = awGetMaxRecipients(); + for (let i = 1; i <= maxRecipients; i++) + { + let params = JSON.parse(awGetInputElement(i).searchParam); + params.idKey = idKey; + params.accountKey = accountKey; + awGetInputElement(i).searchParam = JSON.stringify(params); + } + + if (!startup && prevIdentity && idKey != prevIdentity.key) + { + var prevReplyTo = prevIdentity.replyTo; + var prevCc = ""; + var prevBcc = ""; + var prevReceipt = prevIdentity.requestReturnReceipt; + var prevDSN = prevIdentity.requestDSN; + var prevAttachVCard = prevIdentity.attachVCard; + + if (prevIdentity.doCc) + prevCc += prevIdentity.doCcList; + + if (prevIdentity.doBcc) + prevBcc += prevIdentity.doBccList; + + var newReplyTo = gCurrentIdentity.replyTo; + var newCc = ""; + var newBcc = ""; + var newReceipt = gCurrentIdentity.requestReturnReceipt; + var newDSN = gCurrentIdentity.requestDSN; + var newAttachVCard = gCurrentIdentity.attachVCard; + + if (gCurrentIdentity.doCc) + newCc += gCurrentIdentity.doCcList; + + if (gCurrentIdentity.doBcc) + newBcc += gCurrentIdentity.doBccList; + + var needToCleanUp = false; + var msgCompFields = gMsgCompose.compFields; + + if (!gReceiptOptionChanged && + prevReceipt == msgCompFields.returnReceipt && + prevReceipt != newReceipt) + { + msgCompFields.returnReceipt = newReceipt; + document.getElementById("returnReceiptMenu").setAttribute('checked',msgCompFields.returnReceipt); + } + + if (!gDSNOptionChanged && + prevDSN == msgCompFields.DSN && + prevDSN != newDSN) + { + msgCompFields.DSN = newDSN; + document.getElementById("dsnMenu").setAttribute('checked',msgCompFields.DSN); + } + + if (!gAttachVCardOptionChanged && + prevAttachVCard == msgCompFields.attachVCard && + prevAttachVCard != newAttachVCard) + { + msgCompFields.attachVCard = newAttachVCard; + document.getElementById("cmd_attachVCard").setAttribute('checked',msgCompFields.attachVCard); + } + + if (newReplyTo != prevReplyTo) + { + needToCleanUp = true; + if (prevReplyTo != "") + awRemoveRecipients(msgCompFields, "addr_reply", prevReplyTo); + if (newReplyTo != "") + awAddRecipients(msgCompFields, "addr_reply", newReplyTo); + } + + let toAddrs = new Set(msgCompFields.splitRecipients(msgCompFields.to, true)); + let ccAddrs = new Set(msgCompFields.splitRecipients(msgCompFields.cc, true)); + + if (newCc != prevCc) + { + needToCleanUp = true; + if (prevCc) + awRemoveRecipients(msgCompFields, "addr_cc", prevCc); + if (newCc) { + // Ensure none of the Ccs are already in To. + let cc2 = msgCompFields.splitRecipients(newCc, true); + newCc = cc2.filter(x => !toAddrs.has(x)).join(", "); + awAddRecipients(msgCompFields, "addr_cc", newCc); + } + } + + if (newBcc != prevBcc) + { + needToCleanUp = true; + if (prevBcc) + awRemoveRecipients(msgCompFields, "addr_bcc", prevBcc); + if (newBcc) { + // Ensure none of the Bccs are already in To or Cc. + let bcc2 = msgCompFields.splitRecipients(newBcc, true); + let toCcAddrs = new Set([...toAddrs, ...ccAddrs]); + newBcc = bcc2.filter(x => !toCcAddrs.has(x)).join(", "); + awAddRecipients(msgCompFields, "addr_bcc", newBcc); + } + } + + if (needToCleanUp) + awCleanupRows(); + + try { + gMsgCompose.identity = gCurrentIdentity; + } catch (ex) { dump("### Cannot change the identity: " + ex + "\n");} + + var event = document.createEvent('Events'); + event.initEvent('compose-from-changed', false, true); + document.getElementById("msgcomposeWindow").dispatchEvent(event); + + gComposeNotificationBar.clearIdentityWarning(); + } + + if (!startup) { + if (Services.prefs.getBoolPref("mail.autoComplete.highlightNonMatches")) + document.getElementById('addressCol2#1').highlightNonMatches = true; + + // Only do this if we aren't starting up... + // It gets done as part of startup already. + addRecipientsToIgnoreList(gCurrentIdentity.fullAddress); + } + } +} + +function setupAutocomplete() +{ + var autoCompleteWidget = document.getElementById("addressCol2#1"); + + // if the pref is set to turn on the comment column, honor it here. + // this element then gets cloned for subsequent rows, so they should + // honor it as well + // + if (Services.prefs.getBoolPref("mail.autoComplete.highlightNonMatches")) + autoCompleteWidget.highlightNonMatches = true; + + if (Services.prefs.getIntPref("mail.autoComplete.commentColumn", 0) != 0) + autoCompleteWidget.showCommentColumn = true; +} + +function subjectKeyPress(event) +{ + switch(event.keyCode) { + case KeyEvent.DOM_VK_TAB: + if (!event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) { + SetMsgBodyFrameFocus(); + event.preventDefault(); + } + break; + case KeyEvent.DOM_VK_RETURN: + SetMsgBodyFrameFocus(); + break; + } +} + +function AttachmentBucketClicked(event) +{ + if (event.button != 0) + return; + + if (event.originalTarget.localName == "listboxbody") + goDoCommand('cmd_attachFile'); + else if (event.originalTarget.localName == "listitem" && event.detail == 2) + OpenSelectedAttachment(); +} + +// Content types supported in the attachmentBucketObserver. +let flavours = [ "text/x-moz-message", "application/x-moz-file", + "text/x-moz-url", ]; + +var attachmentBucketObserver = { + onDrop(aEvent) { + let dt = aEvent.dataTransfer; + let dataList = []; + for (let i = 0; i < dt.mozItemCount; i++) { + let types = Array.from(dt.mozTypesAt(i)); + for (let flavour of flavours) { + if (types.includes(flavour)) { + let data = dt.mozGetDataAt(flavour, i); + if (data) { + dataList.push({ data, flavour }); + } + break; + } + } + } + + for (let { data, flavour } of dataList) { + let isValidAttachment = false; + let prettyName; + let size; + + // We could be dropping an attachment of various flavours; + // check and do the right thing. + switch (flavour) { + case "application/x-moz-file": { + if (data instanceof Ci.nsIFile) { + size = data.fileSize; + } + + try { + data = Services.io.getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler) + .getURLSpecFromFile(data); + isValidAttachment = true; + } catch (e) { + Cu.reportError("Couldn't process the dragged file " + + data.leafName + ":" + e); + } + break; + } + + case "text/x-moz-message": { + isValidAttachment = true; + let msgHdr = gMessenger.messageServiceFromURI(data) + .messageURIToMsgHdr(data); + prettyName = msgHdr.mime2DecodedSubject + ".eml"; + size = msgHdr.messageSize; + break; + } + + case "text/x-moz-url": { + let pieces = data.split("\n"); + data = pieces[0]; + if (pieces.length > 1) { + prettyName = pieces[1]; + } + if (pieces.length > 2) { + size = parseInt(pieces[2]); + } + + // If this is a URL (or selected text), check if it's a valid URL + // by checking if we can extract a scheme using Services.io. + // Don't attach invalid or mailto: URLs. + try { + let scheme = Services.io.extractScheme(data); + if (scheme != "mailto") { + isValidAttachment = true; + } + } catch (ex) {} + break; + } + } + + if (isValidAttachment && !DuplicateFileCheck(data)) { + let attachment = Cc["@mozilla.org/messengercompose/attachment;1"] + .createInstance(Ci.nsIMsgAttachment); + attachment.url = data; + attachment.name = prettyName; + + if (size !== undefined) { + attachment.size = size; + } + + AddAttachment(attachment); + } + } + + aEvent.stopPropagation(); + }, + + onDragOver(aEvent) { + let dragSession = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService).getCurrentSession(); + for (let flavour of flavours) { + if (dragSession.isDataFlavorSupported(flavour)) { + let attachmentBucket = GetMsgAttachmentElement(); + attachmentBucket.setAttribute("dragover", "true"); + aEvent.stopPropagation(); + aEvent.preventDefault(); + break; + } + } + }, + + onDragExit(aEvent) { + let attachmentBucket = GetMsgAttachmentElement(); + attachmentBucket.removeAttribute("dragover"); + }, +}; + +function DisplaySaveFolderDlg(folderURI) +{ + try + { + var showDialog = gCurrentIdentity.showSaveMsgDlg; + } + catch (e) + { + return; + } + + if (showDialog){ + let msgfolder = MailUtils.getFolderForURI(folderURI, true); + if (!msgfolder) + return; + var checkbox = {value:0}; + var SaveDlgTitle = sComposeMsgsBundle.getString("SaveDialogTitle"); + var dlgMsg = sComposeMsgsBundle.getFormattedString("SaveDialogMsg", + [msgfolder.name, + msgfolder.server.prettyName]); + + var CheckMsg = sComposeMsgsBundle.getString("CheckMsg"); + Services.prompt.alertCheck(window, SaveDlgTitle, dlgMsg, CheckMsg, checkbox); + try { + gCurrentIdentity.showSaveMsgDlg = !checkbox.value; + }//try + catch (e) { + return; + }//catch + + }//if + return; +} + +function SetMsgAddressingWidgetElementFocus() +{ + awSetFocusTo(awGetInputElement(awGetNumberOfRecipients())); +} + +function SetMsgIdentityElementFocus() +{ + GetMsgIdentityElement().focus(); +} + +function SetMsgSubjectElementFocus() +{ + GetMsgSubjectElement().focus(); +} + +function SetMsgAttachmentElementFocus() +{ + GetMsgAttachmentElement().focus(); + FocusOnFirstAttachment(); +} + +function SetMsgBodyFrameFocus() +{ + //window.content.focus(); fails to blur the currently focused element + document.commandDispatcher + .advanceFocusIntoSubtree(document.getElementById("appcontent")); +} + +function GetMsgAddressingWidgetElement() +{ + if (!gMsgAddressingWidgetElement) + gMsgAddressingWidgetElement = document.getElementById("addressingWidget"); + + return gMsgAddressingWidgetElement; +} + +function GetMsgIdentityElement() +{ + if (!gMsgIdentityElement) + gMsgIdentityElement = document.getElementById("msgIdentity"); + + return gMsgIdentityElement; +} + +function GetMsgSubjectElement() +{ + if (!gMsgSubjectElement) + gMsgSubjectElement = document.getElementById("msgSubject"); + + return gMsgSubjectElement; +} + +function GetMsgAttachmentElement() +{ + if (!gMsgAttachmentElement) + gMsgAttachmentElement = document.getElementById("attachmentBucket"); + + return gMsgAttachmentElement; +} + +function GetMsgHeadersToolbarElement() +{ + if (!gMsgHeadersToolbarElement) + gMsgHeadersToolbarElement = document.getElementById("MsgHeadersToolbar"); + + return gMsgHeadersToolbarElement; +} + +function IsMsgHeadersToolbarCollapsed() +{ + var element = GetMsgHeadersToolbarElement(); + return element && element.collapsed; +} + +function WhichElementHasFocus() +{ + var msgIdentityElement = GetMsgIdentityElement(); + var msgAddressingWidgetElement = GetMsgAddressingWidgetElement(); + var msgSubjectElement = GetMsgSubjectElement(); + var msgAttachmentElement = GetMsgAttachmentElement(); + + if (top.document.commandDispatcher.focusedWindow == content) + return content; + + var currentNode = top.document.commandDispatcher.focusedElement; + while (currentNode) + { + if (currentNode == msgIdentityElement || + currentNode == msgAddressingWidgetElement || + currentNode == msgSubjectElement || + currentNode == msgAttachmentElement) + return currentNode; + + currentNode = currentNode.parentNode; + } + + return null; +} + +// Function that performs the logic of switching focus from +// one element to another in the mail compose window. +// The default element to switch to when going in either +// direction (shift or no shift key pressed), is the +// AddressingWidgetElement. +// +// The only exception is when the MsgHeadersToolbar is +// collapsed, then the focus will always be on the body of +// the message. +function SwitchElementFocus(event) +{ + var focusedElement = WhichElementHasFocus(); + + if (event && event.shiftKey) + { + if (IsMsgHeadersToolbarCollapsed()) + SetMsgBodyFrameFocus(); + else if (focusedElement == gMsgAddressingWidgetElement) + SetMsgIdentityElementFocus(); + else if (focusedElement == gMsgIdentityElement) + SetMsgBodyFrameFocus(); + else if (focusedElement == content) + { + // only set focus to the attachment element if there + // are any attachments. + if (AttachmentElementHasItems()) + SetMsgAttachmentElementFocus(); + else + SetMsgSubjectElementFocus(); + } + else if (focusedElement == gMsgAttachmentElement) + SetMsgSubjectElementFocus(); + else + SetMsgAddressingWidgetElementFocus(); + } + else + { + if (IsMsgHeadersToolbarCollapsed()) + SetMsgBodyFrameFocus(); + else if (focusedElement == gMsgAddressingWidgetElement) + SetMsgSubjectElementFocus(); + else if (focusedElement == gMsgSubjectElement) + { + // only set focus to the attachment element if there + // are any attachments. + if (AttachmentElementHasItems()) + SetMsgAttachmentElementFocus(); + else + SetMsgBodyFrameFocus(); + } + else if (focusedElement == gMsgAttachmentElement) + SetMsgBodyFrameFocus(); + else if (focusedElement == content) + SetMsgIdentityElementFocus(); + else + SetMsgAddressingWidgetElementFocus(); + } +} + +function loadHTMLMsgPrefs() +{ + var fontFace = Services.prefs.getStringPref("msgcompose.font_face", ""); + doStatefulCommand("cmd_fontFace", fontFace); + + var fontSize = Services.prefs.getCharPref("msgcompose.font_size", ""); + if (fontSize) + EditorSetFontSize(fontSize); + + var bodyElement = GetBodyElement(); + + var textColor = Services.prefs.getCharPref("msgcompose.text_color", ""); + if (!bodyElement.hasAttribute("text") && textColor) + { + bodyElement.setAttribute("text", textColor); + gDefaultTextColor = textColor; + document.getElementById("cmd_fontColor").setAttribute("state", textColor); + onFontColorChange(); + } + + var bgColor = Services.prefs.getCharPref("msgcompose.background_color", ""); + if (!bodyElement.hasAttribute("bgcolor") && bgColor) + { + bodyElement.setAttribute("bgcolor", bgColor); + gDefaultBackgroundColor = bgColor; + document.getElementById("cmd_backgroundColor").setAttribute("state", bgColor); + onBackgroundColorChange(); + } +} + +function AutoSave() +{ + if (gMsgCompose.editor && (gContentChanged || gMsgCompose.bodyModified) && + !gSendOrSaveOperationInProgress) + { + GenericSendMessage(nsIMsgCompDeliverMode.AutoSaveAsDraft); + gAutoSaveKickedIn = true; + } + gAutoSaveTimeout = setTimeout(AutoSave, gAutoSaveInterval); +} + +/** + * Helper function to remove a query part from a URL, so for example: + * ...?remove=xx&other=yy becomes ...?other=yy. + * + * @param aURL the URL from which to remove the query part + * @param aQuery the query part to remove + * @return the URL with the query part removed + */ +function removeQueryPart(aURL, aQuery) +{ + // Quick pre-check. + if (!aURL.includes(aQuery)) + return aURL; + + let indexQM = aURL.indexOf("?"); + if (indexQM < 0) + return aURL; + + let queryParts = aURL.substr(indexQM + 1).split("&"); + let indexPart = queryParts.indexOf(aQuery); + if (indexPart < 0) + return aURL; + queryParts.splice(indexPart, 1); + return aURL.substr(0, indexQM + 1) + queryParts.join("&"); +} + +function InitEditor(editor) +{ + // Set the eEditorMailMask flag to avoid using content prefs for the spell + // checker, otherwise the dictionary setting in preferences is ignored and + // the dictionary is inconsistent between the subject and message body. + var eEditorMailMask = Ci.nsIEditor.eEditorMailMask; + editor.flags |= eEditorMailMask; + GetMsgSubjectElement().editor.flags |= eEditorMailMask; + + // Control insertion of line breaks. + editor.returnInParagraphCreatesNewParagraph = + Services.prefs.getBoolPref("mail.compose.default_to_paragraph") || + Services.prefs.getBoolPref("editor.CR_creates_new_p"); + editor.document.execCommand("defaultparagraphseparator", false, + gMsgCompose.composeHTML && + Services.prefs.getBoolPref("mail.compose.default_to_paragraph") ? + "p" : "br"); + + gMsgCompose.initEditor(editor, window.content); + InlineSpellCheckerUI.init(editor); + EnableInlineSpellCheck(Services.prefs.getBoolPref("mail.spellcheck.inline")); + document.getElementById("menu_inlineSpellCheck").setAttribute("disabled", !InlineSpellCheckerUI.canSpellCheck); + + // Listen for spellchecker changes, set the document language to the + // dictionary picked by the user via the right-click menu in the editor. + document.addEventListener("spellcheck-changed", updateDocumentLanguage); + + // XXX: the error event fires twice for each load. Why?? + editor.document.body.addEventListener("error", function(event) { + if (event.target.localName != "img") { + return; + } + + if (event.target.getAttribute("moz-do-not-send") == "true") { + return; + } + + let src = event.target.src; + + if (!src) { + return; + } + + if (!/^file:/i.test(src)) { + // Check if this is a protocol that can fetch parts. + let protocol = src.substr(0, src.indexOf(":")).toLowerCase(); + if (!(Services.io.getProtocolHandler(protocol) instanceof + Ci.nsIMsgMessageFetchPartService)) { + // Can't fetch parts, don't try to load. + return; + } + } + + if (event.target.classList.contains("loading-internal")) { + // We're already loading this, or tried so unsuccesfully. + return; + } + + if (gOriginalMsgURI) { + let msgSvc = Cc["@mozilla.org/messenger;1"] + .createInstance(Ci.nsIMessenger) + .messageServiceFromURI(gOriginalMsgURI); + let originalMsgNeckoURI = msgSvc.getUrlForUri(gOriginalMsgURI); + + if (src.startsWith(removeQueryPart(originalMsgNeckoURI.spec, + "type=application/x-message-display"))) { + // Reply/Forward/Edit Draft/Edit as New can contain references to + // images in the original message. Load those and make them data: URLs + // now. + event.target.classList.add("loading-internal"); + try { + loadBlockedImage(src); + } catch (e) { + // Couldn't load the referenced image. + Cu.reportError(e); + } + } + else { + // Appears to reference a random message. Notify and keep blocking. + gComposeNotificationBar.setBlockedContent(src); + } + } + else { + // For file:, and references to parts of random messages, show the + // blocked content notification. + gComposeNotificationBar.setBlockedContent(src); + } + }, true); + + // Convert mailnews URL back to data: URL. + let background = editor.document.body.background; + if (background && gOriginalMsgURI) { + // Check that background has the same URL as the message itself. + let msgSvc = Cc["@mozilla.org/messenger;1"] + .createInstance(Ci.nsIMessenger) + .messageServiceFromURI(gOriginalMsgURI); + let originalMsgNeckoURI = msgSvc.getUrlForUri(gOriginalMsgURI); + + if (background.startsWith( + removeQueryPart(originalMsgNeckoURI.spec, + "type=application/x-message-display"))) { + try { + editor.document.body.background = loadBlockedImage(background, true); + } catch (e) { + // Couldn't load the referenced image. + Cu.reportError(e); + } + } + } +} + +/** + * The event listener for the "spellcheck-changed" event updates + * the document language. + */ +function updateDocumentLanguage(event) +{ + document.documentElement.setAttribute("lang", event.detail.dictionary); +} + +function EnableInlineSpellCheck(aEnableInlineSpellCheck) +{ + InlineSpellCheckerUI.enabled = aEnableInlineSpellCheck; + GetMsgSubjectElement().setAttribute("spellcheck", aEnableInlineSpellCheck); +} + +function getMailToolbox() +{ + return document.getElementById("compose-toolbox"); +} + +function MailToolboxCustomizeInit() +{ + if (document.commandDispatcher.focusedWindow == content) + window.focus(); + disableEditableFields(); + GetMsgHeadersToolbarElement().setAttribute("moz-collapsed", true); + document.getElementById("compose-toolbar-sizer").setAttribute("moz-collapsed", true); + document.getElementById("content-frame").setAttribute("moz-collapsed", true); + toolboxCustomizeInit("mail-menubar"); +} + +function MailToolboxCustomizeDone(aToolboxChanged) +{ + toolboxCustomizeDone("mail-menubar", getMailToolbox(), aToolboxChanged); + GetMsgHeadersToolbarElement().removeAttribute("moz-collapsed"); + document.getElementById("compose-toolbar-sizer").removeAttribute("moz-collapsed"); + document.getElementById("content-frame").removeAttribute("moz-collapsed"); + enableEditableFields(); + SetMsgBodyFrameFocus(); +} + +function MailToolboxCustomizeChange(aEvent) +{ + toolboxCustomizeChange(getMailToolbox(), aEvent); +} + +/** + * Object to handle message related notifications that are showing in a + * notificationbox below the composed message content. + */ +var gComposeNotificationBar = { + + get notificationBar() { + delete this.notificationBar; + return this.notificationBar = document.getElementById("attachmentNotificationBox"); + }, + + setBlockedContent: function(aBlockedURI) { + let brandName = sBrandBundle.getString("brandShortName"); + let buttonLabel = sComposeMsgsBundle.getString("blockedContentPrefLabel"); + let buttonAccesskey = sComposeMsgsBundle.getString("blockedContentPrefAccesskey"); + + let buttons = [{ + label: buttonLabel, + accessKey: buttonAccesskey, + popup: "blockedContentOptions", + callback: function(aNotification, aButton) { + return true; // keep notification open + } + }]; + + // The popup value is a space separated list of all the blocked urls. + let popup = document.getElementById("blockedContentOptions"); + let urls = popup.value ? popup.value.split(" ") : []; + if (!urls.includes(aBlockedURI)) { + urls.push(aBlockedURI); + } + popup.value = urls.join(" "); + + let msg = sComposeMsgsBundle.getFormattedString("blockedContentMessage", + [brandName, brandName]); + msg = PluralForm.get(urls.length, msg); + + if (!this.isShowingBlockedContentNotification()) { + this.notificationBar + .appendNotification(msg, "blockedContent", null, + this.notificationBar.PRIORITY_WARNING_MEDIUM, + buttons); + } + else { + this.notificationBar.getNotificationWithValue("blockedContent") + .setAttribute("label", msg); + } + }, + + isShowingBlockedContentNotification: function() { + return !!this.notificationBar.getNotificationWithValue("blockedContent"); + }, + + clearBlockedContentNotification: function() { + this.notificationBar.removeNotification( + this.notificationBar.getNotificationWithValue("blockedContent")); + }, + + clearNotifications: function(aValue) { + this.notificationBar.removeAllNotifications(true); + }, + + setIdentityWarning: function(aIdentityName) { + if (!this.notificationBar.getNotificationWithValue("identityWarning")) { + let text = sComposeMsgsBundle.getString("identityWarning").split("%S"); + let label = new DocumentFragment(); + label.appendChild(document.createTextNode(text[0])); + label.appendChild(document.createElement("b")); + label.lastChild.appendChild(document.createTextNode(aIdentityName)); + label.appendChild(document.createTextNode(text[1])); + this.notificationBar.appendNotification(label, "identityWarning", null, + this.notificationBar.PRIORITY_WARNING_HIGH, null); + } + }, + + clearIdentityWarning: function() { + let idWarning = this.notificationBar.getNotificationWithValue("identityWarning"); + if (idWarning) + this.notificationBar.removeNotification(idWarning); + } +}; + +/** + * Populate the menuitems of what blocked content to unblock. + */ +function onBlockedContentOptionsShowing(aEvent) { + let urls = aEvent.target.value ? aEvent.target.value.split(" ") : []; + + // Out with the old... + let childNodes = aEvent.target.childNodes; + for (let i = childNodes.length - 1; i >= 0; i--) { + childNodes[i].remove(); + } + + // ... and in with the new. + for (let url of urls) { + let menuitem = document.createElement("menuitem"); + let fString = sComposeMsgsBundle.getFormattedString("blockedAllowResource", + [url]); + menuitem.setAttribute("label", fString); + menuitem.setAttribute("crop", "center"); + menuitem.setAttribute("value", url); + menuitem.setAttribute("oncommand", + "onUnblockResource(this.value, this.parentNode);"); + aEvent.target.appendChild(menuitem); + } +} + +/** + * Handle clicking the "Load <url>" in the blocked content notification bar. + * @param {String} aURL - the URL that was unblocked + * @param {Node} aNode - the node holding as value the URLs of the blocked + * resources in the message (space separated). + */ +function onUnblockResource(aURL, aNode) { + try { + loadBlockedImage(aURL); + } catch (e) { + // Couldn't load the referenced image. + Cu.reportError(e); + } finally { + // Remove it from the list on success and failure. + let urls = aNode.value.split(" "); + for (let i = 0; i < urls.length; i++) { + if (urls[i] == aURL) { + urls.splice(i, 1); + aNode.value = urls.join(" "); + if (urls.length == 0) { + gComposeNotificationBar.clearBlockedContentNotification(); + } + break; + } + } + } +} + +/** + * Convert the blocked content to a data URL and swap the src to that for the + * elements that were using it. + * + * @param {String} aURL - (necko) URL to unblock + * @param {Bool} aReturnDataURL - return data: URL instead of processing image + * @return {String} the image as data: URL. + * @throw Error() if reading the data failed + */ +function loadBlockedImage(aURL, aReturnDataURL = false) { + let filename; + if (/^(file|chrome):/i.test(aURL)) { + filename = aURL.substr(aURL.lastIndexOf("/") + 1); + } + else { + let fnMatch = /[?&;]filename=([^?&]+)/.exec(aURL); + filename = (fnMatch && fnMatch[1]) || ""; + } + + filename = decodeURIComponent(filename); + let uri = Services.io.newURI(aURL); + let contentType; + if (filename) { + try { + contentType = Cc["@mozilla.org/mime;1"] + .getService(Ci.nsIMIMEService) + .getTypeFromURI(uri); + } catch (ex) { + contentType = "image/png"; + } + + if (!contentType.startsWith("image/")) { + // Unsafe to unblock this. It would just be garbage either way. + throw new Error("Won't unblock; URL=" + aURL + + ", contentType=" + contentType); + } + } + else { + // Assuming image/png is the best we can do. + contentType = "image/png"; + } + + let channel = + Services.io.newChannelFromURI(uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER); + + let inputStream = channel.open(); + let stream = Cc["@mozilla.org/binaryinputstream;1"] + .createInstance(Ci.nsIBinaryInputStream); + stream.setInputStream(inputStream); + let streamData = ""; + try { + while (stream.available() > 0) { + streamData += stream.readBytes(stream.available()); + } + } catch(e) { + stream.close(); + throw new Error("Couln't read all data from URL=" + aURL + " (" + e +")"); + } + stream.close(); + + let encoded = btoa(streamData); + let dataURL = "data:" + contentType + + (filename ? ";filename=" + encodeURIComponent(filename) : "") + + ";base64," + encoded; + + if (aReturnDataURL) { + return dataURL; + } + + let editor = GetCurrentEditor(); + for (let img of editor.document.images) { + if (img.src == aURL) { + img.src = dataURL; // Swap to data URL. + img.classList.remove("loading-internal"); + } + } +} diff --git a/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js b/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js new file mode 100644 index 0000000000..38cd1f5ecc --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/addressingWidgetOverlay.js @@ -0,0 +1,1167 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +top.MAX_RECIPIENTS = 1; /* for the initial listitem created in the XUL */ + +var inputElementType = ""; +var selectElementType = ""; +var selectElementIndexTable = null; + +var gNumberOfCols = 0; + +var gDragService = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService); + +/** + * global variable inherited from MsgComposeCommands.js + * + var gMsgCompose; + */ + +function awGetMaxRecipients() +{ + return top.MAX_RECIPIENTS; +} + +function awGetNumberOfCols() +{ + if (gNumberOfCols == 0) + { + var listbox = document.getElementById('addressingWidget'); + var listCols = listbox.getElementsByTagName('listcol'); + gNumberOfCols = listCols.length; + if (!gNumberOfCols) + gNumberOfCols = 1; /* if no cols defined, that means we have only one! */ + } + + return gNumberOfCols; +} + +function awInputElementName() +{ + if (inputElementType == "") + inputElementType = document.getElementById("addressCol2#1").localName; + return inputElementType; +} + +function awSelectElementName() +{ + if (selectElementType == "") + selectElementType = document.getElementById("addressCol1#1").localName; + return selectElementType; +} + +// TODO: replace awGetSelectItemIndex with recipient type index constants + +function awGetSelectItemIndex(itemData) +{ + if (selectElementIndexTable == null) + { + selectElementIndexTable = new Object(); + var selectElem = document.getElementById("addressCol1#1"); + for (var i = 0; i < selectElem.childNodes[0].childNodes.length; i ++) + { + var aData = selectElem.childNodes[0].childNodes[i].getAttribute("value"); + selectElementIndexTable[aData] = i; + } + } + return selectElementIndexTable[itemData]; +} + +function Recipients2CompFields(msgCompFields) +{ + if (!msgCompFields) { + throw new Error("Message Compose Error: msgCompFields is null (ExtractRecipients)"); + return; + } + + var i = 1; + var addrTo = ""; + var addrCc = ""; + var addrBcc = ""; + var addrReply = ""; + var addrNg = ""; + var addrFollow = ""; + var to_Sep = ""; + var cc_Sep = ""; + var bcc_Sep = ""; + var reply_Sep = ""; + var ng_Sep = ""; + var follow_Sep = ""; + + var recipientType; + var inputField; + var fieldValue; + var recipient; + while ((inputField = awGetInputElement(i))) + { + fieldValue = inputField.value; + + if (fieldValue != "") + { + recipientType = awGetPopupElement(i).value; + recipient = null; + + switch (recipientType) + { + case "addr_to" : + case "addr_cc" : + case "addr_bcc" : + case "addr_reply" : + try { + let headerParser = MailServices.headerParser; + recipient = + headerParser.makeFromDisplayAddress(fieldValue) + .map(fullValue => headerParser.makeMimeAddress( + fullValue.name, + fullValue.email)) + .join(", "); + } catch (ex) { + recipient = fieldValue; + } + break; + } + + switch (recipientType) + { + case "addr_to" : addrTo += to_Sep + recipient; to_Sep = ","; break; + case "addr_cc" : addrCc += cc_Sep + recipient; cc_Sep = ","; break; + case "addr_bcc" : addrBcc += bcc_Sep + recipient; bcc_Sep = ","; break; + case "addr_reply" : addrReply += reply_Sep + recipient; reply_Sep = ","; break; + case "addr_newsgroups" : addrNg += ng_Sep + fieldValue; ng_Sep = ","; break; + case "addr_followup" : addrFollow += follow_Sep + fieldValue; follow_Sep = ","; break; + case "addr_other": + let headerName = awGetPopupElement(i).label; + headerName = headerName.substring(0, headerName.indexOf(':')); + msgCompFields.setRawHeader(headerName, fieldValue, null); + break; + } + } + i ++; + } + + msgCompFields.to = addrTo; + msgCompFields.cc = addrCc; + msgCompFields.bcc = addrBcc; + msgCompFields.replyTo = addrReply; + msgCompFields.newsgroups = addrNg; + msgCompFields.followupTo = addrFollow; +} + +function CompFields2Recipients(msgCompFields) +{ + if (msgCompFields) { + var listbox = document.getElementById('addressingWidget'); + var newListBoxNode = listbox.cloneNode(false); + var listBoxColsClone = listbox.firstChild.cloneNode(true); + newListBoxNode.appendChild(listBoxColsClone); + let templateNode = listbox.querySelector("listitem"); + // dump("replacing child in comp fields 2 recips \n"); + listbox.parentNode.replaceChild(newListBoxNode, listbox); + + top.MAX_RECIPIENTS = 0; + var msgReplyTo = msgCompFields.replyTo; + var msgTo = msgCompFields.to; + var msgCC = msgCompFields.cc; + var msgBCC = msgCompFields.bcc; + var msgNewsgroups = msgCompFields.newsgroups; + var msgFollowupTo = msgCompFields.followupTo; + var havePrimaryRecipient = false; + if (msgReplyTo) + awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgReplyTo, false), + "addr_reply", newListBoxNode, templateNode); + if (msgTo) { + var rcp = msgCompFields.splitRecipients(msgTo, false); + if (rcp.length) + { + awSetInputAndPopupFromArray(rcp, "addr_to", newListBoxNode, templateNode); + havePrimaryRecipient = true; + } + } + if (msgCC) + awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgCC, false), + "addr_cc", newListBoxNode, templateNode); + if (msgBCC) + awSetInputAndPopupFromArray(msgCompFields.splitRecipients(msgBCC, false), + "addr_bcc", newListBoxNode, templateNode); + if (msgNewsgroups) { + awSetInputAndPopup(msgNewsgroups, "addr_newsgroups", newListBoxNode, templateNode); + havePrimaryRecipient = true; + } + if(msgFollowupTo) + awSetInputAndPopup(msgFollowupTo, "addr_followup", newListBoxNode, templateNode); + + // If it's a new message, we need to add an extra empty recipient. + if (!havePrimaryRecipient) + _awSetInputAndPopup("", "addr_to", newListBoxNode, templateNode); + awFitDummyRows(2); + + // CompFields2Recipients is called whenever a user replies or edits an existing message. + // We want to add all of the recipients for this message to the ignore list for spell check + let currentAddress = gCurrentIdentity ? gCurrentIdentity.fullAddress : ""; + addRecipientsToIgnoreList([currentAddress,msgTo,msgCC,msgBCC].filter(adr => adr).join(", ")); + } +} + +function awSetInputAndPopupId(inputElem, popupElem, rowNumber) +{ + popupElem.id = "addressCol1#" + rowNumber; + inputElem.id = "addressCol2#" + rowNumber; + inputElem.setAttribute("aria-labelledby", popupElem.id); +} + +function awSetInputAndPopupValue(inputElem, inputValue, popupElem, popupValue, rowNumber) +{ + inputElem.value = inputValue.trimLeft(); + + popupElem.selectedItem = popupElem.childNodes[0].childNodes[awGetSelectItemIndex(popupValue)]; + + if (rowNumber >= 0) + awSetInputAndPopupId(inputElem, popupElem, rowNumber); + + _awSetAutoComplete(popupElem, inputElem); + + onRecipientsChanged(true); +} + +function _awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode) +{ + top.MAX_RECIPIENTS++; + + var newNode = templateNode.cloneNode(true); + parentNode.appendChild(newNode); // we need to insert the new node before we set the value of the select element! + + var input = newNode.getElementsByTagName(awInputElementName()); + var select = newNode.getElementsByTagName(awSelectElementName()); + + if (input && input.length == 1 && select && select.length == 1) + awSetInputAndPopupValue(input[0], inputValue, select[0], popupValue, top.MAX_RECIPIENTS) +} + +function awSetInputAndPopup(inputValue, popupValue, parentNode, templateNode) +{ + if ( inputValue && popupValue ) + { + var addressArray = inputValue.split(","); + + for ( var index = 0; index < addressArray.length; index++ ) + _awSetInputAndPopup(addressArray[index], popupValue, parentNode, templateNode); + } +} + +function awSetInputAndPopupFromArray(inputArray, popupValue, parentNode, templateNode) +{ + if (popupValue) + { + for (let recipient of inputArray) + _awSetInputAndPopup(recipient, popupValue, parentNode, templateNode); + } +} + +function awRemoveRecipients(msgCompFields, recipientType, recipientsList) +{ + if (!msgCompFields) + return; + + var recipientArray = msgCompFields.splitRecipients(recipientsList, false); + + for (var index = 0; index < recipientArray.length; index++) + for (var row = 1; row <= top.MAX_RECIPIENTS; row ++) + { + var popup = awGetPopupElement(row); + if (popup.value == recipientType) { + var input = awGetInputElement(row); + if (input.value == recipientArray[index]) + { + awSetInputAndPopupValue(input, "", popup, "addr_to", -1); + break; + } + } + } +} + +function awAddRecipients(msgCompFields, recipientType, recipientsList) +{ + if (!msgCompFields) + return; + + var recipientArray = msgCompFields.splitRecipients(recipientsList, false); + + for (var index = 0; index < recipientArray.length; index++) + awAddRecipient(recipientType, recipientArray[index]); +} + +// this was broken out of awAddRecipients so it can be re-used...adds a new row matching recipientType and +// drops in the single address. +function awAddRecipient(recipientType, address) +{ + for (var row = 1; row <= top.MAX_RECIPIENTS; row ++) + { + if (awGetInputElement(row).value == "") + break; + } + + if (row > top.MAX_RECIPIENTS) + awAppendNewRow(false); + + awSetInputAndPopupValue(awGetInputElement(row), address, awGetPopupElement(row), recipientType, row); + + /* be sure we still have an empty row left at the end */ + if (row == top.MAX_RECIPIENTS) + { + awAppendNewRow(true); + awSetInputAndPopupValue(awGetInputElement(top.MAX_RECIPIENTS), "", awGetPopupElement(top.MAX_RECIPIENTS), recipientType, top.MAX_RECIPIENTS); + } + + // add the recipient to our spell check ignore list + addRecipientsToIgnoreList(address); +} + +function awTestRowSequence() +{ + /* + This function is for debug and testing purpose only, normal users should not run it! + + Everytime we insert or delete a row, we must be sure we didn't break the ID sequence of + the addressing widget rows. This function will run a quick test to see if the sequence still ok + + You need to define the pref mail.debug.test_addresses_sequence to true in order to activate it + */ + + var test_sequence; + if (Services.prefs.getPrefType("mail.debug.test_addresses_sequence") == Ci.nsIPrefBranch.PREF_BOOL) + test_sequence = Services.prefs.getBoolPref("mail.debug.test_addresses_sequence"); + if (!test_sequence) + return true; + + /* debug code to verify the sequence still good */ + + var listbox = document.getElementById('addressingWidget'); + var listitems = listbox.getElementsByTagName('listitem'); + if (listitems.length >= top.MAX_RECIPIENTS ) + { + for (var i = 1; i <= listitems.length; i ++) + { + var item = listitems [i - 1]; + let inputID = item.querySelector(awInputElementName()).id.split("#")[1]; + let popupID = item.querySelector(awSelectElementName()).id.split("#")[1]; + if (inputID != i || popupID != i) + { + dump("#ERROR: sequence broken at row " + i + ", inputID=" + inputID + ", popupID=" + popupID + "\n"); + return false; + } + dump("---SEQUENCE OK---\n"); + return true; + } + } + else + dump("#ERROR: listitems.length(" + listitems.length + ") < top.MAX_RECIPIENTS(" + top.MAX_RECIPIENTS + ")\n"); + + return false; +} + +function awCleanupRows() +{ + var maxRecipients = top.MAX_RECIPIENTS; + var rowID = 1; + + for (var row = 1; row <= maxRecipients; row ++) + { + var inputElem = awGetInputElement(row); + if (inputElem.value == "" && row < maxRecipients) + awRemoveRow(awGetRowByInputElement(inputElem)); + else + { + awSetInputAndPopupId(inputElem, awGetPopupElement(row), rowID); + rowID ++; + } + } + + awTestRowSequence(); +} + +function awDeleteRow(rowToDelete) +{ + /* When we delete a row, we must reset the id of others row in order to not break the sequence */ + var maxRecipients = top.MAX_RECIPIENTS; + awRemoveRow(rowToDelete); + + // assume 2 column update (input and popup) + for (var row = rowToDelete + 1; row <= maxRecipients; row ++) + awSetInputAndPopupId(awGetInputElement(row), awGetPopupElement(row), (row-1)); + + awTestRowSequence(); +} + +function awClickEmptySpace(target, setFocus) +{ + if (target == null || + (target.localName != "listboxbody" && + target.localName != "listcell" && + target.localName != "listitem")) + return; + + let lastInput = awGetInputElement(top.MAX_RECIPIENTS); + + if ( lastInput && lastInput.value ) + awAppendNewRow(setFocus); + else if (setFocus) + awSetFocusTo(lastInput); +} + +function awReturnHit(inputElement) +{ + let row = awGetRowByInputElement(inputElement); + let nextInput = awGetInputElement(row+1); + + if ( !nextInput ) + { + if ( inputElement.value ) + awAppendNewRow(true); + else // No address entered, switch to Subject field + { + var subjectField = document.getElementById( 'msgSubject' ); + subjectField.select(); + subjectField.focus(); + } + } + else + { + nextInput.select(); + awSetFocusTo(nextInput); + } + + // be sure to add the recipient to our ignore list + // when the user hits enter in an autocomplete widget... + addRecipientsToIgnoreList(inputElement.value); +} + +function awDeleteHit(inputElement) +{ + let row = awGetRowByInputElement(inputElement); + + /* 1. don't delete the row if it's the last one remaining, just reset it! */ + if (top.MAX_RECIPIENTS <= 1) + { + inputElement.value = ""; + return; + } + + /* 2. Set the focus to the previous field if possible */ + // Note: awSetFocusTo() is asynchronous, i.e. we'll focus after row removal. + if (row > 1) + awSetFocusTo(awGetInputElement(row - 1)) + else + awSetFocusTo(awGetInputElement(2)) + + /* 3. Delete the row */ + awDeleteRow(row); +} + +function awAppendNewRow(setFocus) +{ + var listbox = document.getElementById('addressingWidget'); + var listitem1 = awGetListItem(1); + + if ( listbox && listitem1 ) + { + var lastRecipientType = awGetPopupElement(top.MAX_RECIPIENTS).value; + + var nextDummy = awGetNextDummyRow(); + var newNode = listitem1.cloneNode(true); + if (nextDummy) + listbox.replaceChild(newNode, nextDummy); + else + listbox.appendChild(newNode); + + top.MAX_RECIPIENTS++; + + var input = newNode.getElementsByTagName(awInputElementName()); + if ( input && input.length == 1 ) + { + input[0].value = ""; + + // We always clone the first row. The problem is that the first row + // could be focused. When we clone that row, we end up with a cloned + // XUL textbox that has a focused attribute set. Therefore we think + // we're focused and don't properly refocus. The best solution to this + // would be to clone a template row that didn't really have any presentation, + // rather than using the real visible first row of the listbox. + // + // For now we'll just put in a hack that ensures the focused attribute + // is never copied when the node is cloned. + if (input[0].getAttribute('focused') != '') + input[0].removeAttribute('focused'); + } + var select = newNode.getElementsByTagName(awSelectElementName()); + if ( select && select.length == 1 ) + { + // It only makes sense to clone some field types; others + // should not be cloned, since it just makes the user have + // to go to the trouble of selecting something else. In such + // cases let's default to 'To' (a reasonable default since + // we already default to 'To' on the first dummy field of + // a new message). + switch (lastRecipientType) + { + case "addr_reply": + case "addr_other": + select[0].selectedIndex = awGetSelectItemIndex("addr_to"); + break; + case "addr_followup": + select[0].selectedIndex = awGetSelectItemIndex("addr_newsgroups"); + break; + default: + // e.g. "addr_to","addr_cc","addr_bcc","addr_newsgroups": + select[0].selectedIndex = awGetSelectItemIndex(lastRecipientType); + } + + awSetInputAndPopupId(input[0], select[0], top.MAX_RECIPIENTS); + + if (input) + _awSetAutoComplete(select[0], input[0]); + } + + // Focus the new input widget. + if (setFocus && input[0] ) + awSetFocusTo(input[0]); + } +} + +// functions for accessing the elements in the addressing widget + +/** + * Returns the recipient type popup for a row. + * + * @param row Index of the recipient row to return. Starts at 1. + * @return This returns the menulist (not its child menupopup), despite the + * function name. + */ +function awGetPopupElement(row) +{ + return document.getElementById("addressCol1#" + row); +} + +/** + * Returns the recipient inputbox for a row. + * + * @param row Index of the recipient row to return. Starts at 1. + * @return This returns the textbox element. + */ +function awGetInputElement(row) +{ + return document.getElementById("addressCol2#" + row); +} + +function awGetElementByCol(row, col) +{ + var colID = "addressCol" + col + "#" + row; + return document.getElementById(colID); +} + +function awGetListItem(row) +{ + var listbox = document.getElementById('addressingWidget'); + + if ( listbox && row > 0) + { + var listitems = listbox.getElementsByTagName('listitem'); + if ( listitems && listitems.length >= row ) + return listitems[row-1]; + } + return 0; +} + +function awGetRowByInputElement(inputElement) +{ + var row = 0; + if (inputElement) { + var listitem = inputElement.parentNode.parentNode; + while (listitem) { + if (listitem.localName == "listitem") + ++row; + listitem = listitem.previousSibling; + } + } + return row; +} + + +// Copy Node - copy this node and insert ahead of the (before) node. Append to end if before=0 +function awCopyNode(node, parentNode, beforeNode) +{ + var newNode = node.cloneNode(true); + + if ( beforeNode ) + parentNode.insertBefore(newNode, beforeNode); + else + parentNode.appendChild(newNode); + + return newNode; +} + +// remove row + +function awRemoveRow(row) +{ + awGetListItem(row).remove(); + awFitDummyRows(); + + top.MAX_RECIPIENTS --; +} + +/** + * Set focus to the specified element, typically a recipient input element. + * We do this asynchronusly to allow other processes like adding or removing rows + * to complete before shifting focus. + * + * @param element the element to receive focus asynchronously + */ +function awSetFocusTo(element) { + // Remember the (input) element to focus for asynchronous focusing, so that we + // play safe if this gets called again and the original element gets removed + // before we can focus it. + top.awInputToFocus = element; + setTimeout(_awSetFocusTo, 0); +} + +function _awSetFocusTo() { + top.awInputToFocus.focus(); +} + +// Deprecated - use awSetFocusTo() instead. +// ### TODO: This function should be removed if we're sure addons aren't using it. +function awSetFocus(row, inputElement) { + awSetFocusTo(inputElement); +} + +function awTabFromRecipient(element, event) { + var row = awGetRowByInputElement(element); + if (!event.shiftKey && row < top.MAX_RECIPIENTS) { + var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based. + var listBox = document.getElementById("addressingWidget"); + listBox.listBoxObject.ensureIndexIsVisible(listBoxRow + 1); + } + + // be sure to add the recipient to our ignore list + // when the user tabs out of an autocomplete line... + addRecipientsToIgnoreList(element.value); +} + +function awTabFromMenulist(element, event) +{ + var row = awGetRowByInputElement(element); + if (event.shiftKey && row > 1) { + var listBoxRow = row - 1; // listbox row indices are 0-based, ours are 1-based. + var listBox = document.getElementById("addressingWidget"); + listBox.listBoxObject.ensureIndexIsVisible(listBoxRow - 1); + } +} + +function awGetNumberOfRecipients() +{ + return top.MAX_RECIPIENTS; +} + +function DropOnAddressingTarget(event, onWidget) { + let dragSession = gDragService.getCurrentSession(); + + let trans = Cc["@mozilla.org/widget/transferable;1"] + .createInstance(Ci.nsITransferable); + trans.init(getLoadContext()); + trans.addDataFlavor("text/x-moz-address"); + + let added = false; + for (let i = 0; i < dragSession.numDropItems; ++i) { + dragSession.getData(trans, i); + let dataObj = {}; + let bestFlavor = {}; + let len = {}; + + // Ensure we catch any empty data that may have slipped through. + try { + trans.getAnyTransferData(bestFlavor, dataObj, len); + } catch(ex) { + continue; + } + if (dataObj) { + dataObj = dataObj.value.QueryInterface(Ci.nsISupportsString); + } + if (!dataObj) { + continue; + } + + // Pull the address out of the data object. + let address = dataObj.data.substring(0, len.value); + if (!address) { + continue; + } + + if (onWidget) { + // Break down and add each address. + parseAndAddAddresses(address, + awGetPopupElement(top.MAX_RECIPIENTS).value); + } else { + // Add address into the bucket. + DropRecipient(address); + } + added = true; + } + + // We added at least one address during the drop. + // Disable the default handler and stop propagating the event + // to avoid data being dropped twice. + if (added) { + event.preventDefault(); + event.stopPropagation(); + } +} + +function _awSetAutoComplete(selectElem, inputElem) +{ + let params = JSON.parse(inputElem.getAttribute('autocompletesearchparam')); + params.type = selectElem.value; + inputElem.setAttribute('autocompletesearchparam', JSON.stringify(params)); +} + +function awSetAutoComplete(rowNumber) +{ + var inputElem = awGetInputElement(rowNumber); + var selectElem = awGetPopupElement(rowNumber); + _awSetAutoComplete(selectElem, inputElem) +} + +function awRecipientTextCommand(userAction, element) +{ + if (userAction == "typing" || userAction == "scrolling") + awReturnHit(element); +} + +// Called when an autocomplete session item is selected and the status of +// the session it was selected from is nsIAutoCompleteStatus::failureItems. +// +// As of this writing, the only way that can happen is when an LDAP +// autocomplete session returns an error to be displayed to the user. +// +// There are hardcoded messages in here, but these are just fallbacks for +// when string bundles have already failed us. +// +function awRecipientErrorCommand(errItem, element) +{ + // remove the angle brackets from the general error message to construct + // the title for the alert. someday we'll pass this info using a real + // exception object, and then this code can go away. + // + var generalErrString; + if (errItem.value != "") { + generalErrString = errItem.value.slice(1, errItem.value.length-1); + } else { + generalErrString = "Unknown LDAP server problem encountered"; + } + + // try and get the string of the specific error to contruct the complete + // err msg, otherwise fall back to something generic. This message is + // handed to us as an nsISupportsString in the param slot of the + // autocomplete error item, by agreement documented in + // nsILDAPAutoCompFormatter.idl + // + var specificErrString = ""; + try { + var specificError = errItem.param.QueryInterface(Ci.nsISupportsString); + specificErrString = specificError.data; + } catch (ex) { + } + if (specificErrString == "") { + specificErrString = "Internal error"; + } + + Services.prompt.alert(window, generalErrString, specificErrString); +} + +function awRecipientKeyPress(event, element) +{ + switch(event.key) { + case "ArrowUp": + awArrowHit(element, -1); + break; + case "ArrowDown": + awArrowHit(element, 1); + break; + case "Enter": + case "Tab": + // if the user text contains a comma or a line return, ignore + if (element.value.includes(',')) { + var addresses = element.value; + element.value = ""; // clear out the current line so we don't try to autocomplete it.. + parseAndAddAddresses(addresses, awGetPopupElement(awGetRowByInputElement(element)).value); + } + else if (event.key == "Tab") + awTabFromRecipient(element, event); + + break; + } +} + +function awArrowHit(inputElement, direction) +{ + var row = awGetRowByInputElement(inputElement) + direction; + if (row) { + var nextInput = awGetInputElement(row); + + if (nextInput) + awSetFocusTo(nextInput); + else if (inputElement.value) + awAppendNewRow(true); + } +} + +function awRecipientKeyDown(event, element) +{ + switch(event.key) { + case "Delete": + case "Backspace": + /* do not query directly the value of the text field else the autocomplete widget could potentially + alter it value while doing some internal cleanup, instead, query the value through the first child + */ + if (!element.value) + awDeleteHit(element); + + //We need to stop the event else the listbox will receive it and the function + //awKeyDown will be executed! + event.stopPropagation(); + break; + } +} + +function awKeyDown(event, listboxElement) +{ + switch(event.key) { + case "Delete": + case "Backspace": + /* Warning, the listboxElement.selectedItems will change everytime we delete a row */ + var length = listboxElement.selectedCount; + for (var i = 1; i <= length; i++) { + var inputs = listboxElement.selectedItem.getElementsByTagName(awInputElementName()); + if (inputs && inputs.length == 1) + awDeleteHit(inputs[0]); + } + break; + } +} + +function awMenulistKeyPress(event, element) +{ + switch(event.key) { + case "Tab": + awTabFromMenulist(element, event); + break; + } +} + +/* ::::::::::: addressing widget dummy rows ::::::::::::::::: */ + +var gAWContentHeight = 0; +var gAWRowHeight = 0; + +function awFitDummyRows() +{ + awCalcContentHeight(); + awCreateOrRemoveDummyRows(); +} + +function awCreateOrRemoveDummyRows() +{ + var listbox = document.getElementById("addressingWidget"); + var listboxHeight = listbox.boxObject.height; + + // remove rows to remove scrollbar + let kids = listbox.querySelectorAll('[_isDummyRow]'); + for (let i = kids.length - 1; gAWContentHeight > listboxHeight && i >= 0; --i) { + gAWContentHeight -= gAWRowHeight; + kids[i].remove(); + } + + // add rows to fill space + if (gAWRowHeight) { + while (gAWContentHeight + gAWRowHeight < listboxHeight) { + awCreateDummyItem(listbox); + gAWContentHeight += gAWRowHeight; + } + } +} + +function awCalcContentHeight() +{ + var listbox = document.getElementById("addressingWidget"); + var items = listbox.getElementsByTagName("listitem"); + + gAWContentHeight = 0; + if (items.length > 0) { + // all rows are forced to a uniform height in xul listboxes, so + // find the first listitem with a boxObject and use it as precedent + var i = 0; + do { + gAWRowHeight = items[i].boxObject.height; + ++i; + } while (i < items.length && !gAWRowHeight); + gAWContentHeight = gAWRowHeight*items.length; + } +} + +function awCreateDummyItem(aParent) +{ + var titem = document.createElement("listitem"); + titem.setAttribute("_isDummyRow", "true"); + titem.setAttribute("class", "dummy-row"); + + for (var i = awGetNumberOfCols(); i > 0; i--) + awCreateDummyCell(titem); + + if (aParent) + aParent.appendChild(titem); + + return titem; +} + +function awCreateDummyCell(aParent) +{ + var cell = document.createElement("listcell"); + cell.setAttribute("class", "addressingWidgetCell dummy-row-cell"); + if (aParent) + aParent.appendChild(cell); + + return cell; +} + +function awGetNextDummyRow() +{ + // gets the next row from the top down + return document.querySelector('#addressingWidget > [_isDummyRow]'); +} + +function awSizerListen() +{ + // when splitter is clicked, fill in necessary dummy rows each time the mouse is moved + awCalcContentHeight(); // precalculate + document.addEventListener("mousemove", awSizerMouseMove, true); + document.addEventListener("mouseup", awSizerMouseUp); +} + +function awSizerMouseMove() +{ + awCreateOrRemoveDummyRows(2); +} + +function awSizerMouseUp() +{ + document.removeEventListener("mousemove", awSizerMouseMove); + document.removeEventListener("mouseup", awSizerMouseUp); +} + +function awSizerResized(aSplitter) +{ + // set the height on the listbox rather than on the toolbox + var listbox = document.getElementById("addressingWidget"); + listbox.height = listbox.boxObject.height; + // remove all the heights set on the splitter's previous siblings + for (let sib = aSplitter.previousSibling; sib; sib = sib.previousSibling) + sib.removeAttribute("height"); +} + +function awDocumentKeyPress(event) +{ + try { + var id = event.target.id; + if (id.startsWith('addressCol1')) + awRecipientKeyPress(event, event.target); + } catch (e) { } +} + +function awRecipientInputCommand(event, inputElement) +{ + gContentChanged=true; + setupAutocomplete(); +} + +// Given an arbitrary block of text like a comma delimited list of names or a names separated by spaces, +// we will try to autocomplete each of the names and then take the FIRST match for each name, adding it the +// addressing widget on the compose window. + +var gAutomatedAutoCompleteListener = null; + +function parseAndAddAddresses(addressText, recipientType) +{ + // strip any leading >> characters inserted by the autocomplete widget + var strippedAddresses = addressText.replace(/.* >> /, ""); + + var addresses = MailServices.headerParser + .makeFromDisplayAddress(strippedAddresses); + + if (addresses.length) + { + // we need to set up our own autocomplete session and search for results + + setupAutocomplete(); // be safe, make sure we are setup + if (!gAutomatedAutoCompleteListener) + gAutomatedAutoCompleteListener = new AutomatedAutoCompleteHandler(); + + gAutomatedAutoCompleteListener.init(addresses.map(addr => addr.toString()), + addresses.length, recipientType); + } +} + +function AutomatedAutoCompleteHandler() +{ +} + +// state driven self contained object which will autocomplete a block of addresses without any UI. +// force picks the first match and adds it to the addressing widget, then goes on to the next +// name to complete. + +AutomatedAutoCompleteHandler.prototype = +{ + param: this, + sessionName: null, + namesToComplete: {}, + numNamesToComplete: 0, + indexIntoNames: 0, + + numSessionsToSearch: 0, + numSessionsSearched: 0, + recipientType: null, + searchResults: null, + + init:function(namesToComplete, numNamesToComplete, recipientType) + { + this.indexIntoNames = 0; + this.numNamesToComplete = numNamesToComplete; + this.namesToComplete = namesToComplete; + + this.recipientType = recipientType; + + // set up the auto complete sessions to use + setupAutocomplete(); + this.autoCompleteNextAddress(); + }, + + autoCompleteNextAddress:function() + { + this.numSessionsToSearch = 0; + this.numSessionsSearched = 0; + this.searchResults = new Array; + + if (this.indexIntoNames < this.numNamesToComplete && this.namesToComplete[this.indexIntoNames]) + { + /* XXX This is used to work, until switching to the new toolkit broke it + We should fix it see bug 456550. + if (!this.namesToComplete[this.indexIntoNames].includes('@')) // don't autocomplete if address has an @ sign in it + { + // make sure total session count is updated before we kick off ANY actual searches + if (gAutocompleteSession) + this.numSessionsToSearch++; + + if (gLDAPSession && gCurrentAutocompleteDirectory) + this.numSessionsToSearch++; + + if (gAutocompleteSession) + { + gAutocompleteSession.onAutoComplete(this.namesToComplete[this.indexIntoNames], null, this); + // AB searches are actually synchronous. So by the time we get here we have already looked up results. + + // if we WERE going to also do an LDAP lookup, then check to see if we have a valid match in the AB, if we do + // don't bother with the LDAP search too just return + + if (gLDAPSession && gCurrentAutocompleteDirectory && this.searchResults[0] && this.searchResults[0].defaultItemIndex != -1) + { + this.processAllResults(); + return; + } + } + + if (gLDAPSession && gCurrentAutocompleteDirectory) + gLDAPSession.onStartLookup(this.namesToComplete[this.indexIntoNames], null, this); + } + */ + + if (!this.numSessionsToSearch) + this.processAllResults(); // ldap and ab are turned off, so leave text alone + } + }, + + onStatus:function(aStatus) + { + return; + }, + + onAutoComplete: function(aResults, aStatus) + { + // store the results until all sessions are done and have reported in + if (aResults) + this.searchResults[this.numSessionsSearched] = aResults; + + this.numSessionsSearched++; // bump our counter + + if (this.numSessionsToSearch <= this.numSessionsSearched) + setTimeout('gAutomatedAutoCompleteListener.processAllResults()', 0); // we are all done + }, + + processAllResults: function() + { + // Take the first result and add it to the compose window + var addressToAdd; + + // loop through the results looking for the non default case (default case is the address book with only one match, the default domain) + var sessionIndex; + + var searchResultsForSession; + + for (sessionIndex in this.searchResults) + { + searchResultsForSession = this.searchResults[sessionIndex]; + if (searchResultsForSession && searchResultsForSession.defaultItemIndex > -1) + { + addressToAdd = searchResultsForSession.items + .queryElementAt(searchResultsForSession.defaultItemIndex, + Ci.nsIAutoCompleteItem).value; + break; + } + } + + // still no match? loop through looking for the -1 default index + if (!addressToAdd) + { + for (sessionIndex in this.searchResults) + { + searchResultsForSession = this.searchResults[sessionIndex]; + if (searchResultsForSession && searchResultsForSession.defaultItemIndex == -1) + { + addressToAdd = searchResultsForSession.items + .queryElementAt(0, Ci.nsIAutoCompleteItem).value; + break; + } + } + } + + // no matches anywhere...just use what we were given + if (!addressToAdd) + addressToAdd = this.namesToComplete[this.indexIntoNames]; + + // that will automatically set the focus on a new available row, and make sure it is visible + awAddRecipient(this.recipientType ? this.recipientType : "addr_to", addressToAdd); + + this.indexIntoNames++; + this.autoCompleteNextAddress(); + }, + + QueryInterface : function(iid) + { + if (iid.equals(Ci.nsIAutoCompleteListener) || + iid.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + } +} diff --git a/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul b/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul new file mode 100644 index 0000000000..efd1c6007a --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/mailComposeOverlay.xul @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<overlay id="mailComposeOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <menupopup id="menu_EditPopup" onpopupshowing="updateEditItems();"> + <menuitem id="menu_inlineSpellCheck" + oncommand="EnableInlineSpellCheck(!InlineSpellCheckerUI.enabled);"/> + <menuitem id="menu_accountmgr" + insertafter="sep_preferences" + command="cmd_account"/> + </menupopup> +</overlay> diff --git a/comm/suite/mailnews/components/compose/content/messengercompose.xul b/comm/suite/mailnews/components/compose/content/messengercompose.xul new file mode 100644 index 0000000000..89126d814d --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/messengercompose.xul @@ -0,0 +1,720 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/messengercompose/messengercompose.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?> +<?xml-stylesheet href="chrome://editor/skin/editorFormatToolbar.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/addressingWidget.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/smime/msgCompSMIMEOverlay.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/content/bindings.css" type="text/css"?> + +<?xul-overlay href="chrome://communicator/content/charsetOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/tasksOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/sidebar/sidebarOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/contentAreaContextOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/messengercompose/msgComposeContextOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> +<?xul-overlay href="chrome://editor/content/editorOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/messengercompose/mailComposeOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/mailOverlay.xul"?> + +<!DOCTYPE window [ +<!ENTITY % messengercomposeDTD SYSTEM "chrome://messenger/locale/messengercompose/messengercompose.dtd" > +%messengercomposeDTD; +<!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" > +%messengerDTD; +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" > +%brandDTD; +<!ENTITY % utilityDTD SYSTEM "chrome://communicator/locale/utilityOverlay.dtd"> +%utilityDTD; +<!ENTITY % msgCompSMIMEDTD SYSTEM "chrome://messenger-smime/locale/msgCompSMIMEOverlay.dtd"> +%msgCompSMIMEDTD; +]> + +<window id="msgcomposeWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:nc="http://home.netscape.com/NC-rdf#" + onunload="ComposeUnload()" + onload="ComposeLoad()" + onclose="return DoCommandClose()" + onfocus="EditorOnFocus()" + title="&msgComposeWindow.title;" + toggletoolbar="true" + lightweightthemes="true" + lightweightthemesfooter="status-bar" + windowtype="msgcompose" + macanimationtype="document" + drawtitle="true" + width="640" height="480" + persist="screenX screenY width height sizemode"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_composeMsgs" src="chrome://messenger/locale/messengercompose/composeMsgs.properties"/> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + <stringbundle id="bundle_offlinePrompts" src="chrome://messenger/locale/offline.properties"/> + <stringbundle id="languageBundle" src="chrome://global/locale/languageNames.properties"/> + <stringbundle id="brandBundle" src="chrome://branding/locale/brand.properties"/> + <stringbundle id="bundle_comp_smime" src="chrome://messenger-smime/locale/msgCompSMIMEOverlay.properties"/> + </stringbundleset> + + <script src="chrome://communicator/content/contentAreaClick.js"/> + <script src="chrome://global/content/printUtils.js"/> + <script src="chrome://messenger/content/accountUtils.js"/> + <script src="chrome://messenger/content/mail-offline.js"/> + <script src="chrome://editor/content/editor.js"/> + <script src="chrome://messenger/content/messengercompose/MsgComposeCommands.js"/> + <script src="chrome://messenger/content/messengercompose/addressingWidgetOverlay.js"/> + <script src="chrome://messenger/content/addressbook/abDragDrop.js"/> + <script src="chrome://messenger-smime/content/msgCompSMIMEOverlay.js"/> + + <commandset id="composeCommands"> + <commandset id="msgComposeCommandUpdate" + commandupdater="true" + events="focus" + oncommandupdate="CommandUpdate_MsgCompose()"/> + + <commandset id="editorCommands"/> + <commandset id="commonEditorMenuItems"/> + <commandset id="composerMenuItems"/> + <commandset id="composerEditMenuItems"/> + <commandset id="composerStyleMenuItems"/> + <commandset id="composerTableMenuItems"/> + <commandset id="composerListMenuItems"/> + <commandset id="tasksCommands"/> + <!-- File Menu --> + <command id="cmd_attachFile" oncommand="goDoCommand('cmd_attachFile')"/> + <command id="cmd_attachPage" oncommand="goDoCommand('cmd_attachPage')"/> + <command id="cmd_attachVCard" checked="false" oncommand="ToggleAttachVCard(event.target)"/> + <command id="cmd_save" oncommand="goDoCommand('cmd_save')"/> + <command id="cmd_saveAsFile" oncommand="goDoCommand('cmd_saveAsFile')"/> + <command id="cmd_saveAsDraft" oncommand="goDoCommand('cmd_saveAsDraft')"/> + <command id="cmd_saveAsTemplate" oncommand="goDoCommand('cmd_saveAsTemplate')"/> + <command id="cmd_sendButton" oncommand="goDoCommand('cmd_sendButton')"/> + <command id="cmd_sendNow" oncommand="goDoCommand('cmd_sendNow')"/> + <command id="cmd_sendWithCheck" oncommand="goDoCommand('cmd_sendWithCheck')"/> + <command id="cmd_sendLater" oncommand="goDoCommand('cmd_sendLater')"/> + + <!-- Edit Menu --> + <!--command id="cmd_pasteQuote"/ DO NOT INCLUDE THOSE COMMANDS ELSE THE EDIT MENU WILL BE BROKEN! --> + <!--command id="cmd_find"/--> + <!--command id="cmd_findNext"/--> + <!--command id="cmd_findReplace"/--> + <command id="cmd_renameAttachment" oncommand="goDoCommand('cmd_renameAttachment')" disabled="true"/> + <command id="cmd_openAttachment" oncommand="goDoCommand('cmd_openAttachment')"/> + <command id="cmd_account" + label="&accountManagerCmd.label;" + accesskey="&accountManagerCmd.accesskey;" + oncommand="goDoCommand('cmd_account');"/> + + <!-- Options Menu --> + <command id="cmd_selectAddress" oncommand="goDoCommand('cmd_selectAddress')"/> + <command id="cmd_outputFormat" oncommand="OutputFormatMenuSelect(event.target)"/> + <command id="cmd_quoteMessage" oncommand="goDoCommand('cmd_quoteMessage')"/> + <command id="cmd_viewSecurityStatus" oncommand="showMessageComposeSecurityStatus();"/> + </commandset> + + <broadcasterset id="composeBroadcasters"> + <broadcaster id="Communicator:WorkMode"/> + <broadcaster id="securityStatus" crypto="" signing=""/> + </broadcasterset> + + <observes element="securityStatus" attribute="crypto"/> + <observes element="securityStatus" attribute="signing"/> + + <broadcasterset id="mainBroadcasterSet"/> + + <keyset id="tasksKeys"> + <!-- File Menu --> + <key id="key_send" keycode="&sendCmd.keycode;" observes="cmd_sendWithCheck" modifiers="accel"/> + <key id="key_sendLater" keycode="&sendLaterCmd.keycode;" observes="cmd_sendLater" modifiers="accel, shift"/> + + <!-- Options Menu --> + <!-- key id="key_selectAddresses" key="&selectAddressCmd.key;" command="cmd_selectAddress"/ --> + + <key id="showHideSidebar"/> + <!-- Tab/F6 Keys --> + <key keycode="VK_TAB" oncommand="SwitchElementFocus(event);" modifiers="control"/> + <key keycode="VK_TAB" oncommand="SwitchElementFocus(event);" modifiers="control,shift"/> + <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="control"/> + <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="control,shift"/> + <key keycode="VK_F6" oncommand="SwitchElementFocus(event);" modifiers="shift"/> + <key keycode="VK_F6" oncommand="SwitchElementFocus(event);"/> + <key keycode="VK_ESCAPE" oncommand="handleEsc();"/> + </keyset> + <keyset id="editorKeys"/> + <keyset id="composeKeys"> +#ifndef XP_MACOSX + <key id="key_renameAttachment" keycode="VK_F2" + oncommand="goDoCommand('cmd_renameAttachment');"/> +#endif + </keyset> + + <popupset id="contentAreaContextSet"/> + + <popupset id="editorPopupSet"> + <menupopup id="sidebarPopup"/> + + <menupopup id="msgComposeAttachmentContext" + onpopupshowing="updateEditItems();"> + <menuitem label="&openAttachment.label;" + accesskey="&openAttachment.accesskey;" + command="cmd_openAttachment"/> + <menuitem accesskey="&deleteAttachment.accesskey;" + command="cmd_delete"/> + <menuitem label="&renameAttachment.label;" + accesskey="&renameAttachment.accesskey;" + command="cmd_renameAttachment"/> + <menuitem label="&selectAllCmd.label;" + accesskey="&selectAllAttachments.accesskey;" + command="cmd_selectAll"/> + <menuseparator/> + <menuitem label="&attachFile.label;" + accesskey="&attachFile.accesskey;" + command="cmd_attachFile"/> + <menuitem label="&attachPage.label;" + accesskey="&attachPage.accesskey;" + command="cmd_attachPage"/> + </menupopup> + </popupset> + + <menupopup id="blockedContentOptions" value="" + onpopupshowing="onBlockedContentOptionsShowing(event);"> + </menupopup> + + <vbox id="titlebar"/> + + <toolbox id="compose-toolbox" + class="toolbox-top" + mode="full" + defaultmode="full"> + <toolbar id="compose-toolbar-menubar2" + type="menubar" + class="chromeclass-menubar" + persist="collapsed" + grippytooltiptext="&menuBar.tooltip;" + customizable="true" + defaultset="menubar-items" + mode="icons" + iconsize="small" + defaultmode="icons" + defaulticonsize="small" + context="toolbar-context-menu"> + <toolbaritem id="menubar-items" + class="menubar-items" + align="center"> + <menubar id="mail-menubar"> + <menu id="menu_File"> + <menupopup id="menu_FilePopup"> + <menu id="menu_New"> + <menupopup id="menu_NewPopup"> + <menuitem id="menu_newMessage"/> + <menuseparator id="menuNewPopupSeparator"/> + <menuitem id="menu_newCard"/> + <menuitem id="menu_newNavigator"/> + <menuitem id="menu_newPrivateWindow"/> + <menuitem id="menu_newEditor"/> + </menupopup> + </menu> + <menu id="menu_Attach" + label="&attachMenu.label;" + accesskey="&attachMenu.accesskey;"> + <menupopup id="menu_AttachPopup"> + <menuitem id="menu_AttachFile" + label="&attachFileCmd.label;" + accesskey="&attachFileCmd.accesskey;" + command="cmd_attachFile"/> + <menuitem id="menu_AttachPage" + label="&attachPageCmd.label;" + accesskey="&attachPageCmd.accesskey;" + command="cmd_attachPage"/> + <menuseparator id="menuAttachPageSeparator"/> + <menuitem id="menu_AttachPageVCard" + type="checkbox" + label="&attachVCardCmd.label;" + accesskey="&attachVCardCmd.accesskey;" + command="cmd_attachVCard"/> + </menupopup> + </menu> + <menuitem id="menu_close"/> + <menuseparator id="menuFileAfterCloseSeparator"/> + <menuitem id="menu_SaveCmd" + label="&saveCmd.label;" + accesskey="&saveCmd.accesskey;" + key="key_save" + command="cmd_save"/> + <menu id="menu_SaveAsCmd" + label="&saveAsCmd.label;" + accesskey="&saveAsCmd.accesskey;"> + <menupopup id="menu_SaveAsCmdPopup"> + <menuitem id="menu_SaveAsFileCmd" + label="&saveAsFileCmd.label;" + accesskey="&saveAsFileCmd.accesskey;" + command="cmd_saveAsFile"/> + <menuseparator id="menuSaveAfterSaveAsSeparator"/> + <menuitem id="menu_SaveAsDraftCmd" + label="&saveAsDraftCmd.label;" + accesskey="&saveAsDraftCmd.accesskey;" + command="cmd_saveAsDraft"/> + <menuitem id="menu_SaveAsTemplateCmd" + label="&saveAsTemplateCmd.label;" + accesskey="&saveAsTemplateCmd.accesskey;" + command="cmd_saveAsTemplate"/> + </menupopup> + </menu> + <menuseparator id="menuFileAfterSaveAsSeparator"/> + <menuitem id="menu_sendNow" + label="&sendNowCmd.label;" + accesskey="&sendNowCmd.accesskey;" + key="key_send" command="cmd_sendNow"/> + <menuitem id="menu_sendLater" + label="&sendLaterCmd.label;" + accesskey="&sendLaterCmd.accesskey;" + key="key_sendLater" + command="cmd_sendLater"/> + <menuseparator id="menuFileAfterSendLaterSeparator"/> + <menuitem id="menu_printSetup"/> + <menuitem id="menu_printPreview"/> + <menuitem id="menu_print"/> + </menupopup> + </menu> + <menu id="menu_Edit"/> + <menu id="menu_View"> + <menupopup id="menu_View_Popup"> + <menu id="menu_Toolbars"> + <menupopup id="view_toolbars_popup" + onpopupshowing="onViewToolbarsPopupShowing(event)" + oncommand="onViewToolbarCommand(event);"> + <menuitem id="menu_showTaskbar"/> + </menupopup> + </menu> + <menuseparator id="viewMenuBeforeSecurityStatusSeparator"/> + <menuitem id="menu_viewSecurityStatus" + label="&menu_viewSecurityStatus.label;" + accesskey="&menu_viewSecurityStatus.accesskey;" + command="cmd_viewSecurityStatus"/> + </menupopup> + </menu> + + <menu id="insertMenu" + command="cmd_renderedHTMLEnabler"/> + + <menu id="formatMenu" + label="&formatMenu.label;" + accesskey="&formatMenu.accesskey;" + command="cmd_renderedHTMLEnabler"> + <menupopup id="formatMenuPopup"> + <menu id="tableMenu"/> + <menuseparator id="menuFormatAfterTableSeparator"/> + <menuitem id="objectProperties"/> + <menuitem id="colorsAndBackground"/> + </menupopup> + </menu> + + <menu id="optionsMenu" + label="&optionsMenu.label;" + accesskey="&optionsMenu.accesskey;"> + <menupopup id="optionsMenuPopup" + onpopupshowing="setSecuritySettings(1);"> + <menuitem id="menu_selectAddress" + label="&selectAddressCmd.label;" + accesskey="&selectAddressCmd.accesskey;" + command="cmd_selectAddress"/> + <menuitem id="menu_quoteMessage" + label=""eCmd.label;" + accesskey=""eCmd.accesskey;" + command="cmd_quoteMessage"/> + <menuseparator id="menuOptionsAfterQuoteSeparator"/> + <menuitem id="returnReceiptMenu" + type="checkbox" + label="&returnReceiptMenu.label;" + accesskey="&returnReceiptMenu.accesskey;" + checked="false" + oncommand="ToggleReturnReceipt(event.target)"/> + <menuitem id="dsnMenu" + type="checkbox" + label="&dsnMenu.label;" + accesskey="&dsnMenu.accesskey;" + checked="false" + oncommand="ToggleDSN(event.target);"/> + <menu id="outputFormatMenu" + label="&outputFormatMenu.label;" + accesskey="&outputFormatMenu.accesskey;" + command="cmd_outputFormat"> + <menupopup id="outputFormatMenuPopup"> + <menuitem id="format_auto" type="radio" name="output_format" label="&autoFormatCmd.label;" accesskey="&autoFormatCmd.accesskey;" checked="true"/> + <menuitem id="format_plain" type="radio" name="output_format" label="&plainTextFormatCmd.label;" accesskey="&plainTextFormatCmd.accesskey;"/> + <menuitem id="format_html" type="radio" name="output_format" label="&htmlFormatCmd.label;" accesskey="&htmlFormatCmd.accesskey;"/> + <menuitem id="format_both" type="radio" name="output_format" label="&bothFormatCmd.label;" accesskey="&bothFormatCmd.accesskey;"/> + </menupopup> + </menu> + <menu id="priorityMenu" + label="&priorityMenu.label;" + accesskey="&priorityMenu.accesskey;" + oncommand="PriorityMenuSelect(event.target);"> + <menupopup id="priorityMenuPopup" + onpopupshowing="updatePriorityMenu(this);"> + <menuitem id="priority_highest" type="radio" name="priority" label="&highestPriorityCmd.label;" accesskey="&highestPriorityCmd.accesskey;" value="Highest"/> + <menuitem id="priority_high" type="radio" name="priority" label="&highPriorityCmd.label;" accesskey="&highPriorityCmd.accesskey;" value="High"/> + <menuitem id="priority_normal" type="radio" name="priority" label="&normalPriorityCmd.label;" accesskey="&normalPriorityCmd.accesskey;" value="Normal"/> + <menuitem id="priority_low" type="radio" name="priority" label="&lowPriorityCmd.label;" accesskey="&lowPriorityCmd.accesskey;" value="Low"/> + <menuitem id="priority_lowest" type="radio" name="priority" label="&lowestPriorityCmd.label;" accesskey="&lowestPriorityCmd.accesskey;" value="Lowest"/> + </menupopup> + </menu> + <menu id="charsetMenu" + onpopupshowing="UpdateCharsetMenu(gMsgCompose.compFields.characterSet, this);" + oncommand="ComposeSetCharacterSet(event);"> + <menupopup id="charsetPopup" detectors="false"/> + </menu> + <menu id="fccMenu" + label="&fileCarbonCopyCmd.label;" + accesskey="&fileCarbonCopyCmd.accesskey;" + oncommand="MessageFcc(event.target._folder);"> + <menupopup id="fccMenuPopup" + type="folder" + mode="filing" + showFileHereLabel="true" + fileHereLabel="&fileHereMenu.label;"/> + </menu> + <menuseparator id="smimeOptionsSeparator"/> + <menuitem id="menu_securityEncryptRequire1" + type="checkbox" + label="&menu_securityEncryptRequire.label;" + accesskey="&menu_securityEncryptRequire.accesskey;" + oncommand="toggleEncryptMessage();"/> + <menuitem id="menu_securitySign1" + type="checkbox" + label="&menu_securitySign.label;" + accesskey="&menu_securitySign.accesskey;" + oncommand="toggleSignMessage();"/> + </menupopup> + </menu> + <menu id="tasksMenu"/> + <menu id="windowMenu"/> + <menu id="menu_Help"/> + </menubar> + </toolbaritem> + </toolbar> + + <toolbar id="composeToolbar" + class="toolbar-primary chromeclass-toolbar" + persist="collapsed" + grippytooltiptext="&mailToolbar.tooltip;" + toolbarname="&showComposeToolbarCmd.label;" + accesskey="&showComposeToolbarCmd.accesskey;" + customizable="true" + defaultset="button-send,separator,button-address,button-attach,spellingButton,button-security,separator,button-save,spring,throbber-box" + context="toolbar-context-menu"> + <toolbarbutton id="button-send" + class="toolbarbutton-1" + label="&sendButton.label;" + tooltiptext="&sendButton.tooltip;" + now_label="&sendButton.label;" + now_tooltiptext="&sendButton.tooltip;" + later_label="&sendLaterCmd.label;" + later_tooltiptext="&sendlaterButton.tooltip;" + removable="true" + command="cmd_sendButton"> + <observes element="Communicator:WorkMode" + attribute="offline"/> + </toolbarbutton> + + <toolbarbutton id="button-address" + class="toolbarbutton-1" + label="&addressButton.label;" + tooltiptext="&addressButton.tooltip;" + removable="true" + command="cmd_selectAddress"/> + + <toolbarbutton id="button-attach" + type="menu-button" + class="toolbarbutton-1" + label="&attachButton.label;" + tooltiptext="&attachButton.tooltip;" + removable="true" + command="cmd_attachFile"> + <menupopup id="button-attachPopup"> + <menuitem id="button-attachFile" + label="&attachFileCmd.label;" + accesskey="&attachFileCmd.accesskey;" + command="cmd_attachFile"/> + <menuitem id="button-attachPage" + label="&attachPageCmd.label;" + accesskey="&attachPageCmd.accesskey;" + command="cmd_attachPage"/> + <menuseparator id="buttonAttachAfterPageSeparator"/> + <menuitem id="button-attachVCard" + type="checkbox" + label="&attachVCardCmd.label;" + accesskey="&attachVCardCmd.accesskey;" + command="cmd_attachVCard"/> + </menupopup> + </toolbarbutton> + + <toolbarbutton id="spellingButton" + type="menu-button" + class="toolbarbutton-1" + label="&spellingButton.label;" + removable="true" + command="cmd_spelling"> + <!-- this popup gets dynamically generated --> + <menupopup id="languageMenuList" + oncommand="ChangeLanguage(event);" + onpopupshowing="OnShowDictionaryMenu(event.target);"/> + </toolbarbutton> + + <toolbarbutton id="button-save" + type="menu-button" + class="toolbarbutton-1" + label="&saveButton.label;" + tooltiptext="&saveButton.tooltip;" + removable="true" + command="cmd_save"> + <menupopup id="button-savePopup"> + <menuitem id="button-saveAsFile" + label="&saveAsFileCmd.label;" + accesskey="&saveAsFileCmd.accesskey;" + command="cmd_saveAsFile"/> + <menuseparator id="buttonSaveAfterFileSeparator"/> + <menuitem id="button-saveAsDraft" + label="&saveAsDraftCmd.label;" + accesskey="&saveAsDraftCmd.accesskey;" + command="cmd_saveAsDraft"/> + <menuitem id="button-saveAsTemplate" + label="&saveAsTemplateCmd.label;" + accesskey="&saveAsTemplateCmd.accesskey;" + command="cmd_saveAsTemplate"/> + </menupopup> + </toolbarbutton> + + <toolbaritem id="throbber-box"/> + </toolbar> + + <toolbarset id="customToolbars" context="toolbar-context-menu"/> + + <toolbar id="MsgHeadersToolbar" + persist="collapsed" + flex="1" + grippytooltiptext="&addressBar.tooltip;" + nowindowdrag="true"> + <hbox id="msgheaderstoolbar-box" flex="1"> + <vbox id="addresses-box" flex="1"> + <hbox align="center"> + <label value="&fromAddr.label;" accesskey="&fromAddr.accesskey;" control="msgIdentity"/> + <menulist id="msgIdentity" + editable="true" + disableautoselect="true" + flex="1" + oncommand="LoadIdentity(false);"> + <menupopup id="msgIdentityPopup"/> + </menulist> + </hbox> + <!-- Addressing Widget --> + <listbox id="addressingWidget" flex="1" + seltype="multiple" rows="4" + onkeydown="awKeyDown(event, this);" + onclick="awClickEmptySpace(event.originalTarget, true);" + ondragover="DragAddressOverTargetControl(event);" + ondrop="DropOnAddressingTarget(event, true);"> + + <listcols> + <listcol id="typecol-addressingWidget"/> + <listcol id="textcol-addressingWidget" flex="1"/> + </listcols> + + <listitem class="addressingWidgetItem" allowevents="true"> + <listcell class="addressingWidgetCell" align="stretch"> + <menulist id="addressCol1#1" disableonsend="true" + class="aw-menulist menulist-compact" flex="1" + onkeypress="awMenulistKeyPress(event, this);" + oncommand="onAddressColCommand(this.id);"> + <menupopup> + <menuitem value="addr_to" label="&toAddr.label;"/> + <menuitem value="addr_cc" label="&ccAddr.label;"/> + <menuitem value="addr_bcc" label="&bccAddr.label;"/> + <menuitem value="addr_reply" label="&replyAddr.label;"/> + <menuitem value="addr_newsgroups" + label="&newsgroupsAddr.label;"/> + <menuitem value="addr_followup" + label="&followupAddr.label;"/> + </menupopup> + </menulist> + </listcell> + + <listcell class="addressingWidgetCell"> + <textbox id="addressCol2#1" + class="plain textbox-addressingWidget uri-element" + aria-labelledby="addressCol1#1" + type="autocomplete" flex="1" maxrows="4" + newlines="replacewithcommas" + autocompletesearch="mydomain addrbook ldap news" + timeout="300" autocompletesearchparam="{}" + completedefaultindex="true" forcecomplete="true" + minresultsforpopup="2" ignoreblurwhilesearching="true" + ontextentered="awRecipientTextCommand(eventParam, this);" + onerrorcommand="awRecipientErrorCommand(eventParam, this);" + onchange="onRecipientsChanged();" + oninput="onRecipientsChanged();" + onkeypress="awRecipientKeyPress(event, this);" + onkeydown="awRecipientKeyDown(event, this);" + disableonsend="true"> + <image class="person-icon" + onclick="this.parentNode.select();"/> + </textbox> + </listcell> + </listitem> + </listbox> + <hbox align="center"> + <label value="&subject.label;" accesskey="&subject.accesskey;" control="msgSubject"/> + <textbox id="msgSubject" flex="1" class="toolbar" disableonsend="true" spellcheck="true" + oninput="gContentChanged=true;SetComposeWindowTitle();" + onkeypress="subjectKeyPress(event);" /> + </hbox> + </vbox> + <splitter id="attachmentbucket-sizer" collapse="after"/> + <vbox id="attachments-box"> + <label id="attachmentBucketText" value="&attachments.label;" crop="right" + accesskey="&attachments.accesskey;" control="attachmentBucket"/> + <listbox id="attachmentBucket" + seltype="multiple" + flex="1" + rows="4" + tabindex="-1" + context="msgComposeAttachmentContext" + disableoncustomize="true" + onkeypress="if (event.keyCode == 8 || event.keyCode == 46) RemoveSelectedAttachment();" + onclick="AttachmentBucketClicked(event);" + ondragover="attachmentBucketObserver.onDragOver(event);" + ondrop="attachmentBucketObserver.onDrop(event);" + ondragexit="attachmentBucketObserver.onDragExit(event);"/> + </vbox> + </hbox> + </toolbar> + + <!-- These toolbar items get filled out from the editorOverlay --> + <toolbar id="FormatToolbar" + class="chromeclass-toolbar" + persist="collapsed" + grippytooltiptext="&formatToolbar.tooltip;" + toolbarname="&showFormatToolbarCmd.label;" + accesskey="&showFormatToolbarCmd.accesskey;" + customizable="true" + defaultset="paragraph-select-container,font-face-select-container,color-buttons-container,DecreaseFontSizeButton,IncreaseFontSizeButton,separator,boldButton,italicButton,underlineButton,separator,ulButton,olButton,outdentButton,indentButton,separator,AlignPopupButton,InsertPopupButton,smileButtonMenu" + mode="icons" + iconsize="small" + defaultmode="icons" + defaulticonsize="small" + context="toolbar-context-menu" + nowindowdrag="true"> + <toolbaritem id="paragraph-select-container"/> + <toolbaritem id="font-face-select-container"/> + <toolbaritem id="color-buttons-container" + disableoncustomize="true"/> + <toolbarbutton id="DecreaseFontSizeButton"/> + <toolbarbutton id="IncreaseFontSizeButton"/> + <toolbarbutton id="boldButton"/> + <toolbarbutton id="italicButton"/> + <toolbarbutton id="underlineButton"/> + <toolbarbutton id="ulButton"/> + <toolbarbutton id="olButton"/> + <toolbarbutton id="outdentButton"/> + <toolbarbutton id="indentButton"/> + <toolbarbutton id="AlignPopupButton"/> + <toolbarbutton id="InsertPopupButton"/> + <toolbarbutton id="smileButtonMenu"/> + </toolbar> + + <toolbarpalette id="MsgComposeToolbarPalette"> + <toolbarbutton id="print-button" + label="&printButton.label;" + tooltiptext="&printButton.tooltip;"/> + <toolbarbutton id="button-security" + type="menu-button" + class="toolbarbutton-1" + label="&securityButton.label;" + tooltiptext="&securityButton.tooltip;" + oncommand="doSecurityButton();"> + <menupopup onpopupshowing="setSecuritySettings(2);"> + <menuitem id="menu_securityEncryptRequire2" + type="checkbox" + label="&menu_securityEncryptRequire.label;" + accesskey="&menu_securityEncryptRequire.accesskey;" + oncommand="setNextCommand('encryptMessage');"/> + <menuitem id="menu_securitySign2" + type="checkbox" + label="&menu_securitySign.label;" + accesskey="&menu_securitySign.accesskey;" + oncommand="setNextCommand('signMessage');"/> + <menuseparator id="smimeToolbarButtonSeparator"/> + <menuitem id="menu_securityStatus2" + label="&menu_securityStatus.label;" + accesskey="&menu_securityStatus.accesskey;" + oncommand="setNextCommand('show');"/> + </menupopup> + </toolbarbutton> + </toolbarpalette> + + </toolbox> + + <splitter id="compose-toolbar-sizer" + resizeafter="grow" + onmousedown="awSizerListen();" + oncommand="awSizerResized(this);"> + <observes element="MsgHeadersToolbar" attribute="collapsed"/> + </splitter> + + <!-- sidebar/toolbar/content/status --> + <hbox id="sidebar-parent" flex="1"> + <!-- From sidebarOverlay.xul --> + <vbox id="sidebar-box" class="chromeclass-extrachrome" hidden="true"/> + <splitter id="sidebar-splitter" class="chromeclass-extrachrome" hidden="true"/> + + <!-- The mail message body frame --> + <vbox id="appcontent" flex="1"> + <findbar id="FindToolbar" browserid="content-frame"/> + <editor id="content-frame" + type="content" + primary="true" + src="about:blank" + name="browser.message.body" + minheight="100" + flex="1" + ondblclick="EditorDblClick(event);" + context="contentAreaContextMenu"/> + </vbox> + </hbox> + + <hbox> + <notificationbox id="attachmentNotificationBox" + flex="1" + notificationside="bottom"/> + </hbox> + + <statusbar id="status-bar" + class="chromeclass-status"> + <statusbarpanel id="component-bar"/> + <statusbarpanel id="statusText" + flex="1"/> + <statusbarpanel id="statusbar-progresspanel" + class="statusbarpanel-progress" + collapsed="true"> + <progressmeter id="compose-progressmeter" + class="progressmeter-statusbar" + mode="normal" + value="0"/> + </statusbarpanel> + <statusbarpanel id="signing-status" + class="statusbarpanel-iconic" + collapsed="true" + oncommand="showMessageComposeSecurityStatus();"/> + <statusbarpanel id="encryption-status" + class="statusbarpanel-iconic" + collapsed="true" + oncommand="showMessageComposeSecurityStatus();"/> + <statusbarpanel id="offline-status" + class="statusbarpanel-iconic" + checkfunc="MailCheckBeforeOfflineChange();"/> + </statusbar> + +</window> diff --git a/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul b/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul new file mode 100644 index 0000000000..f3558873d2 --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/msgComposeContextOverlay.xul @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<overlay id="msgComposeContextOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <menupopup id="contentAreaContextMenu" + onpopupshowing="return event.target != this || + openEditorContextMenu(this);"> + <!-- Hide the menuitems by default so they do not to show up + in the sidebar context menu. --> + <menuitem id="context-pasteNoFormatting" + insertafter="context-paste" + hidden="true" + command="cmd_pasteNoFormatting"/> + <menuitem id="context-pasteQuote" + insertafter="context-pasteNoFormatting" + hidden="true" + command="cmd_pasteQuote"/> + </menupopup> +</overlay> diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js new file mode 100644 index 0000000000..f67d919f63 --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.js @@ -0,0 +1,30 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function Startup() { + let value = document.getElementById("mail.compose.autosave").value; + EnableElementById("autoSaveInterval", value, false); +} + +function EnableMailComposeAutosaveInterval(aValue) { + let focus = (document.getElementById("autoSave") == document.commandDispatcher.focusedElement); + EnableElementById("autoSaveInterval", aValue, focus); +} + +function PopulateFonts() { + var fontsList = document.getElementById("fontSelect"); + try { + var enumerator = Cc["@mozilla.org/gfx/fontenumerator;1"] + .getService(Ci.nsIFontEnumerator); + var localFonts = enumerator.EnumerateAllFonts(); + for (let font of localFonts) + if (font != "serif" && font != "sans-serif" && font != "monospace") + fontsList.appendItem(font, font); + } catch (ex) { } + + // Select the item after the list is completely generated. + document.getElementById(fontsList.getAttribute("preference")) + .setElementValue(fontsList); +} diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul new file mode 100644 index 0000000000..c6f3b4fac8 --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/prefs/pref-composing_messages.xul @@ -0,0 +1,212 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % pref-composing_messagesDTD SYSTEM "chrome://messenger/locale/messengercompose/pref-composing_messages.dtd"> +%pref-composing_messagesDTD; +<!ENTITY % editorOverlayDTD SYSTEM "chrome://editor/locale/editorOverlay.dtd"> +%editorOverlayDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="composing_messages_pane" + label="&pref.composing.messages.title;" + script="chrome://messenger/content/messengercompose/pref-composing_messages.js" + onpaneload="this.PopulateFonts();"> + + <preferences id="composing_messages_preferences"> + <preference id="mail.forward_message_mode" + name="mail.forward_message_mode" + type="int"/> + <preference id="mail.reply_quote_inline" + name="mail.reply_quote_inline" + type="bool"/> + <preference id="mail.compose.autosave" + name="mail.compose.autosave" + type="bool" + onchange="EnableMailComposeAutosaveInterval(this.value);"/> + <preference id="mail.compose.autosaveinterval" + name="mail.compose.autosaveinterval" + type="int"/> + <preference id="mail.warn_on_send_accel_key" + name="mail.warn_on_send_accel_key" + type="bool"/> + <preference id="mailnews.wraplength" + name="mailnews.wraplength" + type="int"/> + <preference id="msgcompose.font_face" + name="msgcompose.font_face" + type="string"/> + <preference id="msgcompose.font_size" + name="msgcompose.font_size" + type="string"/> + <preference id="msgcompose.text_color" + name="msgcompose.text_color" + type="string"/> + <preference id="msgcompose.background_color" + name="msgcompose.background_color" + type="string"/> + <preference id="mailnews.reply_header_type" + name="mailnews.reply_header_type" + type="int"/> + <preference id="mail.compose.default_to_paragraph" + name="mail.compose.default_to_paragraph" + type="bool"/> + </preferences> + + <groupbox> + <caption label="&generalComposing.label;"/> + + <radiogroup id="forwardMessageMode" + orient="horizontal" + align="center" + preference="mail.forward_message_mode"> + <label value="&forwardMsg.label;" control="forwardMessageMode"/> + <radio value="2" + label="&inline.label;" + accesskey="&inline.accesskey;"/> + <radio value="0" + label="&asAttachment.label;" + accesskey="&asAttachment.accesskey;"/> + </radiogroup> + + <checkbox id="replyQuoteInline" label="&replyQuoteInline.label;" + preference="mail.reply_quote_inline" + accesskey="&replyQuoteInline.accesskey;"/> + + <hbox align="center"> + <checkbox id="autoSave" label="&autoSave.label;" + preference="mail.compose.autosave" + accesskey="&autoSave.accesskey;" + aria-labelledby="autoSave autoSaveInterval autoSaveEnd"/> + <textbox id="autoSaveInterval" + type="number" + min="1" + max="99" + size="2" + preference="mail.compose.autosaveinterval" + aria-labelledby="autoSave autoSaveInterval autoSaveEnd"/> + <label id="autoSaveEnd" value="&autoSaveEnd.label;"/> + </hbox> + + <checkbox id="mailWarnOnSendAccelKey" + label="&warnOnSendAccelKey.label;" + accesskey="&warnOnSendAccelKey.accesskey;" + preference="mail.warn_on_send_accel_key"/> + + <hbox align="center"> + <label id="wrapOutLabel" + value="&wrapOutMsg.label;" + accesskey="&wrapOutMsg.accesskey;" + control="wrapLength"/> + <textbox id="wrapLength" + type="number" + min="0" + max="999" + size="3" + preference="mailnews.wraplength" + aria-labelledby="wrapOutLabel wrapLength wrapOutEnd"/> + <label id="wrapOutEnd" value="&char.label;"/> + </hbox> + <hbox align="center"> + <label id="selectHeaderType" + value="&selectHeaderType.label;" + accesskey="&selectHeaderType.accesskey;" + control="mailNewsReplyList"/> + <menulist id="mailNewsReplyList" + preference="mailnews.reply_header_type"> + <menupopup> + <menuitem value="0" + label="&noReplyOption.label;"/> + <menuitem value="1" + label="&authorWroteOption.label;"/> + <menuitem value="2" + label="&onDateAuthorWroteOption.label;"/> + <menuitem value="3" + label="&authorWroteOnDateOption.label;"/> + </menupopup> + </menulist> + </hbox> + </groupbox> + + <!-- Composing Mail --> + + <groupbox align="start"> + <caption label="&defaultMessagesHeader.label;"/> + <grid> + <columns> + <column/> + <column/> + </columns> + + <rows> + <row align="center"> + <label value="&font.label;" + accesskey="&font.accesskey;" + control="fontSelect"/> + <menulist id="fontSelect" preference="msgcompose.font_face"> + <menupopup> + <menuitem value="" + label="&fontVarWidth.label;"/> + <menuitem value="tt" + label="&fontFixedWidth.label;"/> + <menuseparator/> + <menuitem value="Helvetica, Arial, sans-serif" + label="&fontHelvetica.label;"/> + <menuitem value="Times New Roman, Times, serif" + label="&fontTimes.label;"/> + <menuitem value="Courier New, Courier, monospace" + label="&fontCourier.label;"/> + <menuseparator/> + </menupopup> + </menulist> + </row> + <row align="center"> + <label value="&size.label;" + accesskey="&size.accesskey;" + control="fontSizeSelect"/> + <hbox align="center"> + <menulist id="fontSizeSelect" preference="msgcompose.font_size"> + <menupopup> + <menuitem value="x-small" label="&size-tinyCmd.label;"/> + <menuitem value="small" label="&size-smallCmd.label;"/> + <menuitem value="medium" label="&size-mediumCmd.label;"/> + <menuitem value="large" label="&size-largeCmd.label;"/> + <menuitem value="x-large" label="&size-extraLargeCmd.label;"/> + <menuitem value="xx-large" label="&size-hugeCmd.label;"/> + </menupopup> + </menulist> + <label value="&fontColor.label;" + accesskey="&fontColor.accesskey;" + control="msgComposeTextColor"/> + <colorpicker id="msgComposeTextColor" + type="button" + preference="msgcompose.text_color"/> + <label value="&bgColor.label;" + accesskey="&bgColor.accesskey;" + control="msgComposeBackgroundColor"/> + <colorpicker id="msgComposeBackgroundColor" + type="button" + preference="msgcompose.background_color"/> + </hbox> + </row> + </rows> + </grid> + <separator class="thin"/> + <description>&defaultCompose.label;</description> + <radiogroup id="defaultCompose" + class="indent" + preference="mail.compose.default_to_paragraph"> + <radio value="false" + label="&defaultBodyText.label;" + accesskey="&defaultBodyText.accesskey;"/> + <radio value="true" + label="&defaultParagraph.label;" + accesskey="&defaultParagraph.accesskey;"/> + </radiogroup> + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js new file mode 100644 index 0000000000..b5c31d424d --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.js @@ -0,0 +1,151 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gListbox; +var gPref; +var gError; + +function Startup() +{ + // Store some useful elements in globals. + gListbox = + { + html: document.getElementById("html_domains"), + plaintext: document.getElementById("plaintext_domains") + }; + gPref = + { + html_domains: document.getElementById("mailnews.html_domains"), + plaintext_domains: document.getElementById("mailnews.plaintext_domains") + }; + gError = document.getElementById("formatting_error_msg"); + + // Make it easier to access the pref pane from onsync. + gListbox.html.pane = this; + gListbox.plaintext.pane = this; +} + +function AddDomain(aType) +{ + var domains = null; + var result = {value: null}; + if (Services.prompt.prompt(window, gListbox[aType].getAttribute("title"), + gListbox[aType].getAttribute("msg"), result, + null, {value: 0})) + domains = result.value.replace(/ /g, "").split(","); + + if (domains) + { + var added = false; + var removed = false; + var listbox = gListbox[aType]; + var other = aType == "html" ? gListbox.plaintext : gListbox.html; + for (var i = 0; i < domains.length; i++) + { + var domainName = TidyDomainName(domains[i], true); + if (domainName) + { + if (!DomainFirstMatch(listbox, domainName)) + { + var match = DomainFirstMatch(other, domainName); + if (match) + { + match.remove(); + removed = true; + } + listbox.appendItem(domainName); + added = true; + } + } + } + if (added) + listbox.doCommand(); + if (removed) + other.doCommand(); + } +} + +function TidyDomainName(aDomain, aWarn) +{ + // See if it is an email address and if so take just the domain part. + aDomain = aDomain.replace(/.*@/, ""); + + // See if it is a valid domain otherwise return null. + if (!/.\../.test(aDomain)) + { + if (aWarn) + { + var errorMsg = gError.getAttribute("inverr").replace(/@string@/, aDomain); + Services.prompt.alert(window, gError.getAttribute("title"), errorMsg); + } + return null; + } + + // Finally make sure the domain is in lowercase. + return aDomain.toLowerCase(); +} + +function DomainFirstMatch(aListbox, aDomain) +{ + return aListbox.getElementsByAttribute("label", aDomain).item(0); +} + +function RemoveDomains(aType, aEvent) +{ + if (aEvent && aEvent.keyCode != KeyEvent.DOM_VK_DELETE && + aEvent.keyCode != KeyEvent.DOM_VK_BACK_SPACE) + return; + + var nextNode = null; + var listbox = gListbox[aType]; + + while (listbox.selectedItem) + { + var selectedNode = listbox.selectedItem; + nextNode = selectedNode.nextSibling || selectedNode.previousSibling; + selectedNode.remove(); + } + + if (nextNode) + listbox.selectItem(nextNode); + + listbox.doCommand(); +} + +function ReadDomains(aListbox) +{ + var arrayOfPrefs = gPref[aListbox.id].value.replace(/ /g, "").split(","); + if (arrayOfPrefs) + { + var i; + // Check all the existing items, remove any that are not needed and + // make sure we do not duplicate any by removing from pref array. + var domains = aListbox.getElementsByAttribute("label", "*"); + if (domains) + { + for (i = domains.length; --i >= 0; ) + { + var domain = domains[i]; + var index = arrayOfPrefs.indexOf(domain.label); + if (index > -1) + arrayOfPrefs.splice(index, 1); + else + domain.remove(); + } + } + for (i = 0; i < arrayOfPrefs.length; i++) + { + var str = TidyDomainName(arrayOfPrefs[i], false); + if (str) + aListbox.appendItem(str); + } + } +} + +function WriteDomains(aListbox) +{ + var domains = aListbox.getElementsByAttribute("label", "*"); + return Array.from(domains, e => e.label).join(","); +} diff --git a/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul new file mode 100644 index 0000000000..0167a0990f --- /dev/null +++ b/comm/suite/mailnews/components/compose/content/prefs/pref-formatting.xul @@ -0,0 +1,120 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/messengercompose/pref-formatting.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="formatting_pane" + label="&pref.formatting.title;" + script="chrome://messenger/content/messengercompose/pref-formatting.js"> + <preferences id="formatting_preferences"> + <preference id="mail.default_html_action" + name="mail.default_html_action" + type="int"/> + <preference id="mailnews.html_domains" + name="mailnews.html_domains" + type="string"/> + <preference id="mailnews.plaintext_domains" + name="mailnews.plaintext_domains" + type="string"/> + <preference id="mailnews.sendformat.auto_downgrade" + name="mailnews.sendformat.auto_downgrade" + type="bool"/> + </preferences> + + <data id="formatting_error_msg" + title="&domainnameError.title;" + inverr="&invalidEntryError.label;"/> + + <description>&sendMaildesc.label;</description> + + <radiogroup id="mailDefaultHTMLAction" + preference="mail.default_html_action"> + <radio value="0" + label="&askMe.label;" + accesskey="&askMe.accesskey;"/> + <radio value="1" + label="&convertPlain2.label;" + accesskey="&convertPlain2.accesskey;"/> + <radio value="2" + label="&sendHTML2.label;" + accesskey="&sendHTML2.accesskey;"/> + <radio value="3" + label="&sendBoth2.label;" + accesskey="&sendBoth2.accesskey;"/> + </radiogroup> + + <groupbox flex="1"> + <caption label="&domain.title;"/> + + <description>&domaindesc.label;</description> + + <hbox flex="1"> + <vbox flex="1"> + <label value="&HTMLdomaintitle.label;" + accesskey="&HTMLdomaintitle.accesskey;" + control="html_domains"/> + <hbox flex="1"> + <listbox id="html_domains" + title="&add.htmltitle;" + msg="&add.htmldomain;" + flex="1" + seltype="multiple" + preference="mailnews.html_domains" + onsyncfrompreference="return this.pane.ReadDomains(this);" + onsynctopreference="return this.pane.WriteDomains(this);" + onkeypress="RemoveDomains('html', event);"/> + <vbox> + <button label="&AddButton.label;" + accesskey="&AddHtmlDomain.accesskey;" + oncommand="AddDomain('html');"> + <observes element="html_domains" attribute="disabled"/> + </button> + <button label="&DeleteButton.label;" + accesskey="&DeleteHtmlDomain.accesskey;" + oncommand="RemoveDomains('html', null);"> + <observes element="html_domains" attribute="disabled"/> + </button> + </vbox> + </hbox> + </vbox> + <vbox flex="1"> + <label value="&PlainTexttitle.label;" + accesskey="&PlainTexttitle.accesskey;" + control="plaintext_domains"/> + <hbox flex="1"> + <listbox id="plaintext_domains" + title="&add.plaintexttitle;" + msg="&add.plaintextdomain;" + flex="1" + seltype="multiple" + preference="mailnews.plaintext_domains" + onsyncfrompreference="return this.pane.ReadDomains(this);" + onsynctopreference="return this.pane.WriteDomains(this);" + onkeypress="RemoveDomains('plaintext', event);"/> + <vbox> + <button label="&AddButton.label;" + accesskey="&AddPlainText.accesskey;" + oncommand="AddDomain('plaintext');"> + <observes element="plaintext_domains" attribute="disabled"/> + </button> + <button label="&DeleteButton.label;" + accesskey="&DeletePlainText.accesskey;" + oncommand="RemoveDomains('plaintext', null);"> + <observes element="plaintext_domains" attribute="disabled"/> + </button> + </vbox> + </hbox> + </vbox> + </hbox> + </groupbox> + + <checkbox id="autoDowngrade" + label="&autoDowngrade.label;" + accesskey="&autoDowngrade.accesskey;" + preference="mailnews.sendformat.auto_downgrade"/> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/compose/jar.mn b/comm/suite/mailnews/components/compose/jar.mn new file mode 100644 index 0000000000..c9465fa8d7 --- /dev/null +++ b/comm/suite/mailnews/components/compose/jar.mn @@ -0,0 +1,14 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +messenger.jar: + content/messenger/messengercompose/pref-composing_messages.xul (content/prefs/pref-composing_messages.xul) + content/messenger/messengercompose/pref-composing_messages.js (content/prefs/pref-composing_messages.js) + content/messenger/messengercompose/pref-formatting.xul (content/prefs/pref-formatting.xul) + content/messenger/messengercompose/pref-formatting.js (content/prefs/pref-formatting.js) +* content/messenger/messengercompose/messengercompose.xul (content/messengercompose.xul) + content/messenger/messengercompose/mailComposeOverlay.xul (content/mailComposeOverlay.xul) + content/messenger/messengercompose/msgComposeContextOverlay.xul (content/msgComposeContextOverlay.xul) + content/messenger/messengercompose/MsgComposeCommands.js (content/MsgComposeCommands.js) + content/messenger/messengercompose/addressingWidgetOverlay.js (content/addressingWidgetOverlay.js) diff --git a/comm/suite/mailnews/components/compose/moz.build b/comm/suite/mailnews/components/compose/moz.build new file mode 100644 index 0000000000..de5cd1bf81 --- /dev/null +++ b/comm/suite/mailnews/components/compose/moz.build @@ -0,0 +1,6 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ["jar.mn"] diff --git a/comm/suite/mailnews/components/moz.build b/comm/suite/mailnews/components/moz.build new file mode 100644 index 0000000000..9d5b9f36ad --- /dev/null +++ b/comm/suite/mailnews/components/moz.build @@ -0,0 +1,11 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "compose", + "prefs", + "addrbook", + "smime", +] diff --git a/comm/suite/mailnews/components/prefs/content/mailPrefsOverlay.xul b/comm/suite/mailnews/components/prefs/content/mailPrefsOverlay.xul new file mode 100644 index 0000000000..2a4acef93b --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/mailPrefsOverlay.xul @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<!DOCTYPE overlay [ +<!ENTITY % mailPrefsOverlayDTD SYSTEM "chrome://messenger/locale/mailPrefsOverlay.dtd"> +%mailPrefsOverlayDTD; +]> + +<overlay id="mailPrefsOverlay" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <preferences id="appearance_preferences"> + <preference id="general.startup.mail" + name="general.startup.mail" + type="bool"/> + <preference id="general.startup.addressbook" + name="general.startup.addressbook" + type="bool"/> + </preferences> + + <!-- mail startup toggle --> + <groupbox id="generalStartupPreferences"> + <checkbox id="generalStartupMail" + insertafter="generalStartupBrowser" + label="&mail.label;" + accesskey="&mail.accesskey;" + preference="general.startup.mail"/> + <checkbox id="generalStartupAddressBook" + insertafter="generalStartupEditor,generalStartupMail" + label="&addressbook.label;" + accesskey="&addressbook.accesskey;" + preference="general.startup.addressbook"/> + </groupbox> + + <!-- category tree entries for mail/news --> + <treechildren id="prefsPanelChildren"> + <treeitem container="true" + id="mailnewsItem" + insertafter="navigatorItem" + label="&mail.label;" + prefpane="mailnews_pane" + url="chrome://messenger/content/pref-mailnews.xul" + helpTopic="mail_prefs_general"> + <treechildren id="messengerChildren"> + <treeitem id="viewingMessagesItem" + label="&viewingMessages.label;" + prefpane="viewing_messages_pane" + url="chrome://messenger/content/pref-viewing_messages.xul" + helpTopic="mail_prefs_display"/> + <treeitem id="notificationsItem" + label="¬ifications.label;" + prefpane="notifications_pane" + url="chrome://messenger/content/pref-notifications.xul" + helpTopic="mail_prefs_notifications"/> + <treeitem id="composingItem" + label="&composingMessages.label;" + prefpane="composing_messages_pane" + url="chrome://messenger/content/messengercompose/pref-composing_messages.xul" + helpTopic="mail_prefs_messages"/> + <treeitem id="formattingItem" + label="&format.label;" + prefpane="formatting_pane" + url="chrome://messenger/content/messengercompose/pref-formatting.xul" + helpTopic="mail_prefs_formatting"/> + <treeitem id="addressItem" + label="&address.label;" + prefpane="addressing_pane" + url="chrome://messenger/content/addressbook/pref-addressing.xul" + helpTopic="mail_prefs_addressing"/> + <treeitem id="junkItem" + label="&junk.label;" + prefpane="junk_pane" + url="chrome://messenger/content/pref-junk.xul" + helpTopic="mail-prefs-junk"/> + <treeitem id="tagsItem" + label="&tags.label;" + prefpane="tags_pane" + url="chrome://messenger/content/pref-tags.xul" + helpTopic="mail-prefs-tags"/> + <treeitem id="receiptsItem" + label="&return.label;" + prefpane="receipts_pane" + url="chrome://messenger/content/pref-receipts.xul" + helpTopic="mail-prefs-receipts"/> + <treeitem id="characterEncodingItem" + label="&characterEncoding2.label;" + prefpane="character_encoding_pane" + url="chrome://messenger/content/pref-character_encoding.xul" + helpTopic="mail_prefs_text_encoding"/> + <treeitem id="offlineItem" + label="&networkStorage.label;" + prefpane="offline_pane" + url="chrome://messenger/content/pref-offline.xul" + helpTopic="mail_prefs_offline"/> + </treechildren> + </treeitem> + </treechildren> + +</overlay> diff --git a/comm/suite/mailnews/components/prefs/content/pref-character_encoding.js b/comm/suite/mailnews/components/prefs/content/pref-character_encoding.js new file mode 100644 index 0000000000..0ae30e9b1f --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-character_encoding.js @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// The contents of this file will be loaded into the scope of the object +// <prefpane id="character_encoding_pane">! + +var updatingPref = false; + +function Startup () +{ + PrefChanged(document.getElementById('mailnews.view_default_charset')); + PrefChanged(document.getElementById('mailnews.send_default_charset')); +} + +function PrefChanged(aPref) +{ + if (updatingPref) + return; + + var id = aPref.id.substr(9, 4) + "DefaultCharsetList"; + var menulist = document.getElementById(id); + if (!aPref.hasUserValue) + menulist.selectedIndex = 0; + else { + var bundle = document.getElementById("charsetBundle"); + menulist.value = bundle.getString(aPref.value.toLowerCase()); + } +} + +function UpdatePref(aMenulist) +{ + updatingPref = true; + var id = "mailnews." + aMenulist.id.substr(0, 4) + "_default_charset"; + var pref = document.getElementById(id); + if (aMenulist.selectedIndex) + pref.value = aMenulist.value; + else + pref.value = undefined; // reset to default + updatingPref = false; +} diff --git a/comm/suite/mailnews/components/prefs/content/pref-character_encoding.xul b/comm/suite/mailnews/components/prefs/content/pref-character_encoding.xul new file mode 100755 index 0000000000..009f5f49de --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-character_encoding.xul @@ -0,0 +1,111 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?> + +<!DOCTYPE overlay [ + <!ENTITY % prefCharacterEncodingDTD SYSTEM "chrome://messenger/locale/pref-character_encoding.dtd"> %prefCharacterEncodingDTD; + <!ENTITY % prefUtilitiesDTD SYSTEM "chrome://communicator/locale/pref/prefutilities.dtd"> %prefUtilitiesDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="character_encoding_pane" + label="&pref.character.encoding2.title;" + script="chrome://messenger/content/pref-character_encoding.js"> + <preferences id="character_encoding_preferences"> + <preference id="mailnews.view_default_charset" + name="mailnews.view_default_charset" + type="wstring" + onchange="PrefChanged(this);"/> + <preference id="mail.strictly_mime" + name="mail.strictly_mime" + type="bool"/> + <preference id="mailnews.send_default_charset" + name="mailnews.send_default_charset" + type="wstring" + onchange="PrefChanged(this);"/> + <preference id="mailnews.reply_in_default_charset" + name="mailnews.reply_in_default_charset" + type="bool"/> + </preferences> + + <groupbox align="start"> + <caption label="&messageDisplay.caption;"/> + <hbox align="center"> + <label control="viewDefaultCharsetList" + value="&viewFallbackCharset2.label;" + accesskey="&viewFallbackCharset2.accesskey;"/> + <menulist id="viewDefaultCharsetList" + oncommand="UpdatePref(this);"> + <menupopup> + <menuitem label="&FallbackCharset.auto;" value=""/> + <menuitem label="&FallbackCharset.unicode;" value="UTF-8"/> + <menuitem label="&FallbackCharset.other;" value="windows-1252"/> + <menuseparator/> + <menuitem label="&FallbackCharset.arabic;" value="windows-1256"/> + <menuitem label="&FallbackCharset.baltic;" value="windows-1257"/> + <menuitem label="&FallbackCharset.ceiso;" value="ISO-8859-2"/> + <menuitem label="&FallbackCharset.cewindows;" value="windows-1250"/> + <menuitem label="&FallbackCharset.simplified;" value="gbk"/> + <menuitem label="&FallbackCharset.traditional;" value="Big5"/> + <menuitem label="&FallbackCharset.cyrillic;" value="windows-1251"/> + <menuitem label="&FallbackCharset.greek;" value="ISO-8859-7"/> + <menuitem label="&FallbackCharset.hebrew;" value="windows-1255"/> + <menuitem label="&FallbackCharset.japanese;" value="Shift_JIS"/> + <menuitem label="&FallbackCharset.korean;" value="EUC-KR"/> + <menuitem label="&FallbackCharset.thai;" value="windows-874"/> + <menuitem label="&FallbackCharset.turkish;" value="windows-1254"/> + <menuitem label="&FallbackCharset.vietnamese;" value="windows-1258"/> + </menupopup> + </menulist> + </hbox> + <description>&viewFallbackCharset.desc;</description> + </groupbox> + + <!-- Composing Mail --> + <groupbox align="start"> + <caption label="&composingMessages.caption;"/> + + <checkbox id="strictlyMime" + label="&useMIME.label;" + accesskey="&useMIME.accesskey;" + preference="mail.strictly_mime"/> + + <hbox align="center"> + <label value="&sendDefaultCharset2.label;" + accesskey="&sendDefaultCharset2.accesskey;" + control="sendDefaultCharsetList"/> + <menulist id="sendDefaultCharsetList" + oncommand="UpdatePref(this);"> + <menupopup> + <menuitem label="&FallbackCharset.auto;" value=""/> + <menuitem label="&FallbackCharset.unicode;" value="UTF-8"/> + <menuitem label="&FallbackCharset.other;" value="windows-1252"/> + <menuseparator/> + <menuitem label="&FallbackCharset.arabic;" value="windows-1256"/> + <menuitem label="&FallbackCharset.baltic;" value="windows-1257"/> + <menuitem label="&FallbackCharset.ceiso;" value="ISO-8859-2"/> + <menuitem label="&FallbackCharset.cewindows;" value="windows-1250"/> + <menuitem label="&FallbackCharset.simplified;" value="gbk"/> + <menuitem label="&FallbackCharset.traditional;" value="Big5"/> + <menuitem label="&FallbackCharset.cyrillic;" value="windows-1251"/> + <menuitem label="&FallbackCharset.greek;" value="ISO-8859-7"/> + <menuitem label="&FallbackCharset.hebrew;" value="windows-1255"/> + <menuitem label="&FallbackCharset.japanese;" value="Shift_JIS"/> + <menuitem label="&FallbackCharset.korean;" value="EUC-KR"/> + <menuitem label="&FallbackCharset.thai;" value="windows-874"/> + <menuitem label="&FallbackCharset.turkish;" value="windows-1254"/> + <menuitem label="&FallbackCharset.vietnamese;" value="windows-1258"/> + </menupopup> + </menulist> + </hbox> + <checkbox id="replyInDefaultCharset" + label="&replyInDefaultCharset3.label;" + accesskey="&replyInDefaultCharset3.accesskey;" + preference="mailnews.reply_in_default_charset"/> + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/prefs/content/pref-junk.js b/comm/suite/mailnews/components/prefs/content/pref-junk.js new file mode 100644 index 0000000000..9f31050c46 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-junk.js @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function Startup() +{ + UpdateDependentElement("manualMark", "manualMarkMode"); + UpdateDependentElement("enableJunkLogging", "openJunkLog"); +} + +function UpdateDependentElement(aBaseId, aDependentId) +{ + var pref = document.getElementById(aBaseId).getAttribute("preference"); + EnableElementById(aDependentId, document.getElementById(pref).value, false); +} + +function OpenJunkLog() +{ + window.openDialog("chrome://messenger/content/junkLog.xul", + "junkLog", + "chrome,modal,titlebar,resizable,centerscreen"); +} + +function ResetTrainingData() +{ + // make sure the user really wants to do this + var bundle = document.getElementById("bundleJunkPreferences"); + var title = bundle.getString("confirmResetJunkTrainingTitle"); + var text = bundle.getString("confirmResetJunkTrainingText"); + + // if the user says no, then just fall out + if (Services.prompt.confirmEx(window, title, text, + Services.prompt.STD_YES_NO_BUTTONS | + Services.prompt.BUTTON_POS_1_DEFAULT, + "", "", "", null, {})) + return; + + // otherwise go ahead and remove the training data + MailServices.junk.resetTrainingData(); +} diff --git a/comm/suite/mailnews/components/prefs/content/pref-junk.xul b/comm/suite/mailnews/components/prefs/content/pref-junk.xul new file mode 100644 index 0000000000..b6d1f4507d --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-junk.xul @@ -0,0 +1,134 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?> + +<!DOCTYPE overlay [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % prefJunkDTD SYSTEM "chrome://messenger/locale/pref-junk.dtd"> +%prefJunkDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="junk_pane" + label="&pref.junk.title;" + script="chrome://messenger/content/pref-junk.js"> + <preferences id="junk_preferences"> + <preference id="mail.spam.manualMark" + name="mail.spam.manualMark" + type="bool" + onchange="EnableElementById('manualMarkMode', this.value, false);"/> + <preference id="mail.spam.manualMarkMode" + name="mail.spam.manualMarkMode" + type="int"/> + <preference id="mail.spam.markAsReadOnSpam" + name="mail.spam.markAsReadOnSpam" + type="bool"/> + <preference id="mailnews.ui.junk.manualMarkAsJunkMarksRead" + name="mailnews.ui.junk.manualMarkAsJunkMarksRead" + type="bool"/> + <preference id="mail.spam.logging.enabled" + name="mail.spam.logging.enabled" + type="bool" + onchange="EnableElementById('openJunkLog', this.value, false);"/> + <preference id="pref.junk.disable_button.openJunkLog" + name="pref.junk.disable_button.openJunkLog" + type="string"/> + <preference id="pref.junk.disable_button.resetTrainingData" + name="pref.junk.disable_button.resetTrainingData" + type="string"/> + <preference id="mail.phishing.detection.enabled" + name="mail.phishing.detection.enabled" + type="bool"/> + <preference id="mailnews.downloadToTempFile" + name="mailnews.downloadToTempFile" + type="bool"/> + </preferences> + + <stringbundleset id="junkBundleset"> + <stringbundle id="bundleJunkPreferences" + src="chrome://messenger/locale/messenger.properties"/> + </stringbundleset> + + <groupbox> + <caption label="&junkSettings.caption;"/> + <description>&junkMail.intro;</description> + <class separator="thin"/> + + <checkbox id="manualMark" + label="&manualMark.label;" + accesskey="&manualMark.accesskey;" + preference="mail.spam.manualMark"/> + <radiogroup id="manualMarkMode" + class="indent" + aria-labelledby="manualMark" + preference="mail.spam.manualMarkMode"> + <radio id="manualMarkMode0" + label="&manualMarkModeMove.label;" + accesskey="&manualMarkModeMove.accesskey;" + value="0"/> + <radio id="manualMarkMode1" + label="&manualMarkModeDelete.label;" + accesskey="&manualMarkModeDelete.accesskey;" + value="1"/> + </radiogroup> + + <separator class="thin"/> + + <description>&markAsRead.intro;</description> + <vbox class="indent"> + <checkbox id="autoMarkAsRead" + label="&autoMarkAsRead.label;" + accesskey="&autoMarkAsRead.accesskey;" + preference="mail.spam.markAsReadOnSpam"/> + <checkbox id="manualMarkAsRead" + label="&manualMarkAsRead.label;" + accesskey="&manualMarkAsRead.accesskey;" + preference="mailnews.ui.junk.manualMarkAsJunkMarksRead"/> + </vbox> + + <separator class="thin"/> + + <hbox align="start"> + <checkbox id="enableJunkLogging" + label="&enableJunkLogging.label;" + accesskey="&enableJunkLogging.accesskey;" + preference="mail.spam.logging.enabled"/> + <spacer flex="1"/> + <button id="openJunkLog" + label="&openJunkLog.label;" + accesskey="&openJunkLog.accesskey;" + preference="pref.junk.disable_button.openJunkLog" + oncommand="OpenJunkLog();"/> + </hbox> + <hbox align="start"> + <spacer flex="1"/> + <button id="resetTrainingData" + label="&resetTrainingData.label;" + accesskey="&resetTrainingData.accesskey;" + preference="pref.junk.disable_button.resetTrainingData" + oncommand="ResetTrainingData();"/> + </hbox> + </groupbox> + + <groupbox> + <caption label="&pref.suspectMail.caption;"/> + + <checkbox id="enablePhishingDetector" + label="&enablePhishingDetector.label;" + accesskey="&enablePhishingDetector.accesskey;" + preference="mail.phishing.detection.enabled"/> + + <separator class="thin"/> + + <checkbox id="enableAntiVirusQuarantine" + label="&antiVirus.label;" + accesskey="&antiVirus.accesskey;" + preference="mailnews.downloadToTempFile"/> + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/prefs/content/pref-mailnews.js b/comm/suite/mailnews/components/prefs/content/pref-mailnews.js new file mode 100644 index 0000000000..f057fb46ae --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-mailnews.js @@ -0,0 +1,25 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function Startup() +{ + startPageCheck(); +} + +function startPageCheck() +{ + var checked = document.getElementById("mailnews.start_page.enabled").value; + var urlElement = document.getElementById("mailnewsStartPageUrl"); + var prefLocked = document.getElementById("mailnews.start_page.url").locked; + + urlElement.disabled = !checked || prefLocked; +} + +function setHomePageToDefaultPage() +{ + var startPagePref = document.getElementById("mailnews.start_page.url"); + + startPagePref.value = startPagePref.defaultValue; +} diff --git a/comm/suite/mailnews/components/prefs/content/pref-mailnews.xul b/comm/suite/mailnews/components/prefs/content/pref-mailnews.xul new file mode 100644 index 0000000000..e3fbeb12e8 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-mailnews.xul @@ -0,0 +1,141 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?> +<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?> + +<!DOCTYPE overlay [ +<!ENTITY % prefMailnewsDTD SYSTEM "chrome://messenger/locale/pref-mailnews.dtd"> +%prefMailnewsDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="mailnews_pane" + label="&pref.mailnews.title;" + script="chrome://messenger/content/pref-mailnews.js"> + + <preferences id="mailnews_preferences"> + <preference id="mailnews.confirm.moveFoldersToTrash" + name="mailnews.confirm.moveFoldersToTrash" type="bool"/> + <preference id="mailnews.remember_selected_message" + name="mailnews.remember_selected_message" type="bool"/> + <preference id="mailnews.thread_pane_column_unthreads" + name="mailnews.thread_pane_column_unthreads" + inverted="true" type="bool"/> + <preference id="mail.tabs.autoHide" + name="mail.tabs.autoHide" + type="bool"/> + <preference id="mail.tabs.loadInBackground" + name="mail.tabs.loadInBackground" + inverted="true" type="bool"/> + <preference id="mail.biff.on_new_window" + name="mail.biff.on_new_window" + type="bool" + inverted="true"/> + <preference id="mail.tabs.opentabfor.middleclick" + name="mail.tabs.opentabfor.middleclick" + type="bool"/> + <preference id="mail.tabs.opentabfor.doubleclick" + name="mail.tabs.opentabfor.doubleclick" + type="bool"/> + <preference id="mailnews.start_page.enabled" + onchange="this.parentNode.parentNode.startPageCheck();" + name="mailnews.start_page.enabled" type="bool"/> + <preference id="mailnews.start_page.url" + name="mailnews.start_page.url" type="wstring"/> + </preferences> + + <groupbox> + <caption label="&generalSettings.caption;"/> + + <hbox align="center"> + <checkbox id="mailnewsConfirmMoveFoldersToTrash" label="&confirmMove.label;" + preference="mailnews.confirm.moveFoldersToTrash" + accesskey="&confirmMove.accesskey;"/> + </hbox> + + <hbox align="center"> + <checkbox id="mailRememberLastMsg" label="&rememberLastMsg.label;" + preference="mailnews.remember_selected_message" + accesskey="&rememberLastMsg.accesskey;" /> + </hbox> + + <hbox align="center"> + <checkbox id="mailPreserveThreading" + label="&preserveThreading.label;" + accesskey="&preserveThreading.accesskey;" + preference="mailnews.thread_pane_column_unthreads"/> + </hbox> + + <hbox align="center"> + <checkbox id="mailAutoHide" + label="&mailAutoHide.label;" + accesskey="&mailAutoHide.accesskey;" + preference="mail.tabs.autoHide"/> + </hbox> + + <hbox align="center"> + <checkbox id="loadInBackground" + label="&loadInBackground.label;" + accesskey="&loadInBackground.accesskey;" + preference="mail.tabs.loadInBackground"/> + </hbox> + + <hbox align="center"> + <checkbox id="mailBiffOnNewWindow" + label="&mailBiffOnNewWindow.label;" + accesskey="&mailBiffOnNewWindow.accesskey;" + preference="mail.biff.on_new_window"/> + </hbox> + </groupbox> + + <groupbox id="mailOpenTabFor" align="start"> + <caption label="&mailOpenTabsFor.label;"/> + <hbox align="center"> + <checkbox id="mailMiddleClick" +#ifndef XP_MACOSX + label="&mailMiddleClick.label;" + accesskey="&mailMiddleClick.accesskey;" +#else + label="&mailMiddleClickMac.label;" + accesskey="&mailMiddleClickMac.accesskey;" +#endif + preference="mail.tabs.opentabfor.middleclick"/> + </hbox> + + <hbox align="center"> + <checkbox id="mailDoubleClick" + label="&mailDoubleClick.label;" + accesskey="&mailDoubleClick.accesskey;" + preference="mail.tabs.opentabfor.doubleclick"/> + </hbox> + </groupbox> + + <groupbox> + <caption label="&messengerStartPage.caption;"/> + <hbox align="center"> + <checkbox id="mailnewsStartPageEnabled" label="&enableStartPage.label;" + preference="mailnews.start_page.enabled" + accesskey="&enableStartPage.accesskey;"/> + </hbox> + + <hbox align="center"> + <label value="&location.label;" accesskey="&location.accesskey;" + control="mailnewsStartPageUrl"/> + <textbox id="mailnewsStartPageUrl" flex="1" type="autocomplete" + preference="mailnews.start_page.url" timeout="50" + autocompletesearch="history" maxrows="6" class="uri-element"/> + </hbox> + <hbox pack="end"> + <button label="&useDefault.label;" accesskey="&useDefault.accesskey;" + oncommand="setHomePageToDefaultPage();"> + <observes element="mailnewsStartPageUrl" attribute="disabled"/> + </button> + </hbox> + + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/prefs/content/pref-notifications.js b/comm/suite/mailnews/components/prefs/content/pref-notifications.js new file mode 100644 index 0000000000..89191e7cd6 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-notifications.js @@ -0,0 +1,91 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// The contents of this file will be loaded into the scope of the object +// <prefpane id="notifications_pane">! + +var {AppConstants} = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +var gSoundUrlPref = null; + +function Startup() +{ + // if we don't have the alert service, hide the pref UI for using alerts to notify on new mail + // see bug #158711 + var newMailNotificationAlertUI = document.getElementById("newMailNotificationAlertBox"); + newMailNotificationAlertUI.hidden = !("@mozilla.org/alerts-service;1" in Cc); + + // as long as the old notification code is still around, the new options + // won't apply if mail.biff.show_new_alert is false and should be hidden + document.getElementById("showAlertPreviewText").hidden = + document.getElementById("showAlertSubject").hidden = + document.getElementById("showAlertSender").hidden = + !Services.prefs.getBoolPref("mail.biff.show_new_alert"); + + // animate dock icon option currently available for macOS only + var newMailNotificationBouncePref = document.getElementById("newMailNotificationBounceBox"); + newMailNotificationBouncePref.hidden = AppConstants.platform != "macosx"; + + // show tray icon option currently available for Windows only + var newMailNotificationTrayIconPref = document.getElementById("newMailNotificationTrayIconBox"); + newMailNotificationTrayIconPref.hidden = AppConstants.platform != "win"; + + // use system alert option currently available for Linux only + var useSystemAlertPref = document.getElementById("useSystemAlertBox"); + useSystemAlertPref.hidden = AppConstants.platform != "linux"; + + EnableAlert(document.getElementById("mail.biff.show_alert").value, false); + EnableTrayIcon(document.getElementById("mail.biff.show_tray_icon").value); + + gSoundUrlPref = document.getElementById("mail.biff.play_sound.url"); + + PlaySoundCheck(document.getElementById("mail.biff.play_sound").value); +} + +function EnableAlert(aEnable, aFocus) +{ + // switch off the balloon on Windows if the user wants regular alerts + if (aEnable && AppConstants.platform == "win") { + let balloonAlert = document.getElementById("mail.biff.show_balloon"); + if (!balloonAlert.locked) + balloonAlert.value = false; + } + + EnableElementById("showAlertTime", aEnable, aFocus); + EnableElementById("showAlertPreviewText", aEnable, false); + EnableElementById("showAlertSubject", aEnable, false); + EnableElementById("showAlertSender", aEnable, false); + EnableElementById("useSystemAlertRadio", aEnable, false); +} + +function EnableTrayIcon(aEnable) +{ + EnableElementById("newMailNotificationBalloon", aEnable, false); +} + +function ClearAlert(aEnable) +{ + // switch off the regular alerts if the user wants the balloon + if (aEnable && AppConstants.platform == "win") { + let showAlert = document.getElementById("mail.biff.show_alert"); + if (!showAlert.locked) + showAlert.value = false; + } +} + +function PlaySoundCheck(aPlaySound) +{ + let playSoundType = document.getElementById("mail.biff.play_sound.type").value; + + EnableElementById("newMailNotificationType", aPlaySound, false); + EnableSoundURL(aPlaySound && (playSoundType == 1)); +} + +function EnableSoundURL(aEnable) +{ + EnableElementById("mailnewsSoundFileUrl", aEnable, false); +} diff --git a/comm/suite/mailnews/components/prefs/content/pref-notifications.xul b/comm/suite/mailnews/components/prefs/content/pref-notifications.xul new file mode 100644 index 0000000000..20ac974050 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-notifications.xul @@ -0,0 +1,187 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?> + +<!DOCTYPE overlay [ +<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % prefNotificationsDTD SYSTEM "chrome://messenger/locale/pref-notifications.dtd"> +%prefNotificationsDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="notifications_pane" + label="&pref.notifications.title;" + script="chrome://messenger/content/pref-notifications.js"> + + <preferences id="notifications_preferences"> + <preference id="mail.biff.show_alert" + name="mail.biff.show_alert" + type="bool" + onchange="EnableAlert(this.value, this.value);"/> + <preference id="alerts.totalOpenTime" + name="alerts.totalOpenTime" + type="int"/> + <preference id="mail.biff.alert.show_preview" + name="mail.biff.alert.show_preview" + type="bool"/> + <preference id="mail.biff.alert.show_subject" + name="mail.biff.alert.show_subject" + type="bool"/> + <preference id="mail.biff.alert.show_sender" + name="mail.biff.alert.show_sender" + type="bool"/> + <preference id="mail.biff.use_system_alert" + name="mail.biff.use_system_alert" + type="bool"/> + <preference id="mail.biff.show_tray_icon" + name="mail.biff.show_tray_icon" + type="bool" + onchange="EnableTrayIcon(this.value);"/> + <preference id="mail.biff.show_balloon" + name="mail.biff.show_balloon" + type="bool" + onchange="ClearAlert(this.value);"/> + <preference id="mail.biff.animate_dock_icon" + name="mail.biff.animate_dock_icon" + type="bool"/> + <preference id="mail.biff.play_sound" + name="mail.biff.play_sound" + type="bool" + onchange="PlaySoundCheck(this.value);"/> + <preference id="mail.biff.play_sound.type" + name="mail.biff.play_sound.type" + type="int" + onchange="EnableSoundURL(this.value == 1);"/> + <preference id="mail.biff.play_sound.url" + name="mail.biff.play_sound.url" + type="string"/> + </preferences> + + <groupbox id="newMessagesArrivePrefs"> + <caption label="¬ifications.caption;"/> + + <label value="&newMessagesArrive.label;"/> + <vbox id="newMailNotificationAlertBox"> + <hbox align="center"> + <checkbox id="newMailNotificationAlert" + label="&showAlertFor.label;" + accesskey="&showAlertFor.accesskey;" + preference="mail.biff.show_alert"/> + <textbox id="showAlertTime" + type="number" + size="3" + min="1" + max="3600" + preference="alerts.totalOpenTime" + onsyncfrompreference="return document.getElementById(this.getAttribute('preference')).value / 1000;" + onsynctopreference="return this.value * 1000;" + aria-labelledby="newMailNotificationAlert showAlertTime showAlertTimeEnd"/> + <label id="showAlertTimeEnd" + value="&showAlertTimeEnd.label;"> + <observes element="newMailNotificationAlert" + attribute="disabled"/> + </label> + </hbox> + <vbox id="showAlertOptionsBox" + class="indent"> + <checkbox id="showAlertPreviewText" + label="&showAlertPreviewText.label;" + accesskey="&showAlertPreviewText.accesskey;" + preference="mail.biff.alert.show_preview"/> + <checkbox id="showAlertSubject" + label="&showAlertSubject.label;" + accesskey="&showAlertSubject.accesskey;" + preference="mail.biff.alert.show_subject"/> + <checkbox id="showAlertSender" + label="&showAlertSender.label;" + accesskey="&showAlertSender.accesskey;" + preference="mail.biff.alert.show_sender"/> + <separator id="newMailNotificationAlertSeparator" + class="thin"/> + <vbox id="useSystemAlertBox"> + <radiogroup id="useSystemAlertRadio" + preference="mail.biff.use_system_alert"> + <radio id="useSystemAlert" + value="true" + label="&useSystemAlert.label;" + accesskey="&useSystemAlert.accesskey;"/> + <radio id="useBuiltInAlert" + value="false" + label="&useBuiltInAlert.label;" + accesskey="&useBuiltInAlert.accesskey;"/> + </radiogroup> + <separator id="useSystemAlertSeparator" + class="thin"/> + </vbox> + </vbox> + </vbox> + <vbox id="newMailNotificationTrayIconBox"> + <checkbox id="newMailNotificationTrayIcon" + label="&showTrayIcon.label;" + accesskey="&showTrayIcon.accesskey;" + preference="mail.biff.show_tray_icon"/> + <checkbox id="newMailNotificationBalloon" + class="indent" + label="&showBalloon.label;" + accesskey="&showBalloon.accesskey;" + preference="mail.biff.show_balloon"/> + <separator id="newMailNotificationTrayIconSeparator" + class="thin"/> + </vbox> + <vbox id="newMailNotificationBounceBox"> + <checkbox id="newMailNotificationBounce" + label="&bounceSystemDockIcon.label;" + accesskey="&bounceSystemDockIcon.accesskey;" + preference="mail.biff.animate_dock_icon"/> + <separator id="newMailNotificationBounceSeparator" + class="thin"/> + </vbox> + <checkbox id="newMailNotification" + label="&playSound.label;" + accesskey="&playSound.accesskey;" + preference="mail.biff.play_sound"/> + <radiogroup id="newMailNotificationType" + preference="mail.biff.play_sound.type" + class="indent" + aria-labelledby="newMailNotification"> + <radio id="system" + value="0" + label="&systemsound.label;" + accesskey="&systemsound.accesskey;"/> + <radio id="custom" + value="1" + label="&customsound.label;" + accesskey="&customsound.accesskey;"/> + </radiogroup> + + <hbox align="center" class="indent"> + <filefield id="mailnewsSoundFileUrl" + flex="1" + preference="mail.biff.play_sound.url" + preference-editable="true" + onsyncfrompreference="return WriteSoundField(this, document.getElementById('notifications_pane').gSoundUrlPref.value);" + aria-labelledby="custom"/> + <hbox align="center"> + <button id="browse" + label="&browse.label;" + filepickertitle="&browse.title;" + accesskey="&browse.accesskey;" + oncommand="SelectSound(gSoundUrlPref);"> + <observes element="mailnewsSoundFileUrl" attribute="disabled"/> + </button> + <button id="playButton" + label="&playButton.label;" + accesskey="&playButton.accesskey;" + oncommand="PlaySound(gSoundUrlPref.value, true);"> + <observes element="mailnewsSoundFileUrl" attribute="disabled"/> + </button> + </hbox> + </hbox> + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/prefs/content/pref-offline.js b/comm/suite/mailnews/components/prefs/content/pref-offline.js new file mode 100644 index 0000000000..d453c8f97a --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-offline.js @@ -0,0 +1,19 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// The contents of this file will be loaded into the scope of the object +// <prefpane id="offline_pane">! + +function Startup() +{ + var value = document.getElementById("mail.prompt_purge_threshhold").value; + EnableElementById("offlineCompactFolderMin", value, false); +} + +function EnableMailPurgeThreshold(aValue) +{ + var focus = (document.getElementById("offlineCompactFolder") == document.commandDispatcher.focusedElement); + EnableElementById("offlineCompactFolderMin", aValue, focus); +} diff --git a/comm/suite/mailnews/components/prefs/content/pref-offline.xul b/comm/suite/mailnews/components/prefs/content/pref-offline.xul new file mode 100644 index 0000000000..49e4288ab0 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-offline.xul @@ -0,0 +1,121 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/pref-offline.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="offline_pane" + label="&pref.network.title;" + script="chrome://messenger/content/pref-offline.js"> + + <preferences id="offline_preferences"> + <preference id="offline.startup_state" + name="offline.startup_state" + type="int"/> + <preference id="offline.send.unsent_messages" + name="offline.send.unsent_messages" + type="int"/> + <preference id="offline.download.download_messages" + name="offline.download.download_messages" + type="int"/> + <preference id="mailnews.tcptimeout" + name="mailnews.tcptimeout" + type="int"/> + <preference id="mail.prompt_purge_threshhold" + name="mail.prompt_purge_threshhold" + type="bool" + onchange="EnableMailPurgeThreshold(this.value);"/> + <preference id="mail.purge_threshhold_mb" + name="mail.purge_threshhold_mb" + type="int"/> + </preferences> + + <groupbox> + <caption label="&pref.offline.caption;"/> + + <hbox align="center"> + <label value="&textStartUp;" control="whenStartingUp" + accesskey="&textStartUp.accesskey;"/> + <menulist id="whenStartingUp" preference="offline.startup_state"> + <menupopup> + <menuitem value="0" label="&menuitemRememberPrevState;"/> + <menuitem value="1" label="&menuitemAskMe;"/> + <menuitem value="2" label="&menuitemAlwaysOnline;"/> + <menuitem value="3" label="&menuitemAlwaysOffline;"/> + <menuitem value="4" label="&menuitemAutomatic;"/> + </menupopup> + </menulist> + </hbox> + + <separator/> + + <label value="&textGoingOnline;" control="whengoingOnlinestate"/> + <radiogroup id="whengoingOnlinestate" + orient="horizontal" class="indent" + preference="offline.send.unsent_messages"> + <radio value="1" label="&radioAutoSend;" + accesskey="&radioAutoSend.accesskey;"/> + <radio value="2" label="&radioNotSend;" + accesskey="&radioNotSend.accesskey;"/> + <radio value="0" label="&radioAskUnsent;" + accesskey="&radioAskUnsent.accesskey;"/> + </radiogroup> + + <separator/> + + <label value="&textGoingOffline;" control="whengoingOfflinestate"/> + <radiogroup id="whengoingOfflinestate" + orient="horizontal" class="indent" + preference="offline.download.download_messages"> + <radio value="1" label="&radioAutoDownload;" + accesskey="&radioAutoDownload.accesskey;"/> + <radio value="2" label="&radioNotDownload;" + accesskey="&radioNotDownload.accesskey;"/> + <radio value="0" label="&radioAskDownload;" + accesskey="&radioAskDownload.accesskey;"/> + </radiogroup> + </groupbox> + + <groupbox> + <caption label="&mailConnections.caption;"/> + <hbox align="center"> + <label id="timeoutLabel" + value="&mailnewsTimeout.label;" + accesskey="&mailnewsTimeout.accesskey;" + control="mailnewsTimeoutSeconds"/> + <textbox id="mailnewsTimeoutSeconds" + type="number" + size="4" + preference="mailnews.tcptimeout" + aria-labelledby="timeoutLabel mailnewsTimeoutSeconds timeoutSeconds"/> + <label id="timeoutSeconds" value="&mailnewsTimeoutSeconds.label;"/> + </hbox> + </groupbox> + + <groupbox> + <caption label="&Diskspace;"/> + <hbox align="center"> + <checkbox id="offlineCompactFolder" + label="&offlineCompactFolders.label;" + accesskey="&offlineCompactFolders.accesskey;" + preference="mail.prompt_purge_threshhold" + aria-labelledby="offlineCompactFolder offlineCompactFolderMin offlineCompactFolderMB"/> + <textbox id="offlineCompactFolderMin" + type="number" + size="4" + min="1" + max="2048" + increment="10" + value="20" + preference="mail.purge_threshhold_mb" + aria-labelledby="offlineCompactFolder offlineCompactFolderMin offlineCompactFolderMB"/> + <label id="offlineCompactFolderMB" value="&offlineCompactFoldersMB.label;"/> + </hbox> + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/prefs/content/pref-receipts.js b/comm/suite/mailnews/components/prefs/content/pref-receipts.js new file mode 100644 index 0000000000..7a85ac1a1f --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-receipts.js @@ -0,0 +1,28 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gNotInToCcLabel; +var gOutsideDomainLabel; +var gOtherCasesLabel; + +function Startup() +{ + gNotInToCcLabel = document.getElementById("notInToCcLabel"); + gOutsideDomainLabel = document.getElementById("outsideDomainLabel"); + gOtherCasesLabel = document.getElementById("otherCasesLabel"); + + var value = document.getElementById("mail.mdn.report.enabled").value; + EnableDisableAllowedReceipts(value); +} + +function EnableDisableAllowedReceipts(aEnable) +{ + EnableElementById("notInToCcPref", aEnable, false); + EnableElementById("outsideDomainPref", aEnable, false); + EnableElementById("otherCasesPref", aEnable, false); + gNotInToCcLabel.disabled = !aEnable; + gOutsideDomainLabel.disabled = !aEnable; + gOtherCasesLabel.disabled = !aEnable; +} diff --git a/comm/suite/mailnews/components/prefs/content/pref-receipts.xul b/comm/suite/mailnews/components/prefs/content/pref-receipts.xul new file mode 100644 index 0000000000..0ca17a02ed --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-receipts.xul @@ -0,0 +1,146 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + + +<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?> + +<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/pref-receipts.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="receipts_pane" + label="&pref.receipts.title;" + script="chrome://messenger/content/pref-receipts.js"> + <preferences id="receipts_preferences"> + <preference id="mail.receipt.request_return_receipt_on" + name="mail.receipt.request_return_receipt_on" + type="bool"/> + <preference id="mail.incorporate.return_receipt" + name="mail.incorporate.return_receipt" + type="int"/> + <preference id="mail.mdn.report.enabled" + name="mail.mdn.report.enabled" + type="bool" + onchange="EnableDisableAllowedReceipts(this.value);"/> + <preference id="mail.mdn.report.not_in_to_cc" + name="mail.mdn.report.not_in_to_cc" + type="int"/> + <preference id="mail.mdn.report.outside_domain" + name="mail.mdn.report.outside_domain" + type="int"/> + <preference id="mail.mdn.report.other" + name="mail.mdn.report.other" + type="int"/> + </preferences> + + <groupbox> + <caption label="&prefReceipts.caption;"/> + + <vbox id="returnReceiptSettings" align="start"> + <checkbox id="alwaysRequest" + label="&requestReceipt.label;" + accesskey="&requestReceipt.accesskey;" + preference="mail.receipt.request_return_receipt_on"/> + + <separator/> + + <vbox id="receiptArrive"> + <label control="receiptFolder">&receiptArrive.label;</label> + <radiogroup id="receiptFolder" + class="indent" + preference="mail.incorporate.return_receipt"> + <radio value="0" + label="&leaveIt.label;" + accesskey="&leaveIt.accesskey;"/> + <radio value="1" + label="&moveToSent.label;" + accesskey="&moveToSent.accesskey;"/> + </radiogroup> + </vbox> + + <separator/> + + <vbox id="receiptRequest"> + <label control="receiptSend">&requestMDN.label;</label> + <radiogroup id="receiptSend" + class="indent" + preference="mail.mdn.report.enabled"> + <radio value="false" + label="&never.label;" + accesskey="&never.accesskey;"/> + <radio value="true" + label="&returnSome.label;" + accesskey="&returnSome.accesskey;"/> + + <hbox id="receiptSendIf" class="indent"> + <grid> + <columns> + <column/> + <column/> + </columns> + <rows> + <row align="center"> + <label id="notInToCcLabel" + accesskey="¬InToCc.accesskey;" + control="notInToCcPref" + value="¬InToCc.label;"/> + <menulist id="notInToCcPref" + preference="mail.mdn.report.not_in_to_cc"> + <menupopup> + <menuitem value="0" + label="&neverSend.label;"/> + <menuitem value="1" + label="&alwaysSend.label;"/> + <menuitem value="2" + label="&askMe.label;"/> + </menupopup> + </menulist> + </row> + <row align="center"> + <label id="outsideDomainLabel" + accesskey="&outsideDomain.accesskey;" + control="outsideDomainPref" + value="&outsideDomain.label;"/> + <menulist id="outsideDomainPref" + preference="mail.mdn.report.outside_domain"> + <menupopup> + <menuitem value="0" + label="&neverSend.label;"/> + <menuitem value="1" + label="&alwaysSend.label;"/> + <menuitem value="2" + label="&askMe.label;"/> + </menupopup> + </menulist> + </row> + <row align="center"> + <label id="otherCasesLabel" + accesskey="&otherCases.accesskey;" + control="otherCasesPref" + value="&otherCases.label;"/> + <menulist id="otherCasesPref" + preference="mail.mdn.report.other"> + <menupopup> + <menuitem value="0" + label="&neverSend.label;"/> + <menuitem value="1" + label="&alwaysSend.label;"/> + <menuitem value="2" + label="&askMe.label;"/> + </menupopup> + </menulist> + </row> + </rows> + </grid> + </hbox> + </radiogroup> + + </vbox> + + </vbox> + + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/prefs/content/pref-tags.js b/comm/suite/mailnews/components/prefs/content/pref-tags.js new file mode 100644 index 0000000000..8182fe7237 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-tags.js @@ -0,0 +1,478 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +// Each tag entry in our list looks like this: +// <listitem> +// <listcell> +// <textbox/> +// </listcell> +// <listcell> +// <colorpicker type='button'/> +// </listcell> +// </listitem> +// For ease of handling, all tag data is stored in <listitem>.tagInfo also. + +const kOrdinalCharLow = "a"; +const kOrdinalCharHigh = "z"; +const kOrdinalPadding = String.fromCharCode(kOrdinalCharLow.charCodeAt(0) - 1); + +var gInstantApply = document.documentElement.instantApply; // read only once +var gTagList = null; // tagList root element +var gAddButton = null; +var gDeleteButton = null; +var gRaiseButton = null; +var gLowerButton = null; + +var gDeletedTags = {}; // tags marked for deletion in non-instant apply mode + + +function Startup() +{ + gTagList = document.getElementById('tagList'); + gAddButton = document.getElementById('addTagButton'); + gDeleteButton = document.getElementById('deleteTagButton'); + gRaiseButton = document.getElementById('raiseTagButton'); + gLowerButton = document.getElementById('lowerTagButton'); + InitTagList(); + if (!gInstantApply) + window.addEventListener("dialogaccept", this.OnOK, true); + UpdateButtonStates(); +} + +function InitTagList() +{ + // Read the tags from preferences via the tag service. + var tagArray = MailServices.tags.getAllTags(); + for (var i = 0; i < tagArray.length; ++i) + { + var t = tagArray[i]; + var tagInfo = {tag: t.tag, + key: t.key, + color: t.color, + ordinal: t.ordinal, + new: false, // not added in this run + changed: false}; // not changed (yet) + AppendTagEntry(tagInfo, null); + } +} + +// read text and color from the listitem +function UpdateTagInfo(aTagInfo, aEntry) +{ + var tag = aEntry.firstChild.firstChild.value; + var color = aEntry.lastChild.lastChild.color; + if (tag != aTagInfo.tag || color != aTagInfo.color) + { + aTagInfo.changed = true; // never unset changed flag here! + aTagInfo.tag = tag; + aTagInfo.color = color; + } +} + +// set text and color of the listitem +function UpdateTagEntry(aTagInfo, aEntry) +{ + aEntry.firstChild.firstChild.value = aTagInfo.tag; + aEntry.lastChild.lastChild.color = aTagInfo.color || 'inherit'; +} + +function AppendTagEntry(aTagInfo, aRefChild) +{ + // Creating a colorpicker dynamically in an onload handler is really sucky. + // You MUST first set its type attribute (to select the correct binding), then + // add the element to the DOM (to bind the binding) and finally set the color + // property(!) afterwards. Try in any other order and fail... :-( + var tagCell = document.createElement('listcell'); + var textbox = document.createElement('textbox'); + textbox.setAttribute('flex', 1); + textbox.setAttribute('value', aTagInfo.tag); + tagCell.appendChild(textbox); + + var colorCell = document.createElement('listcell'); + var colorpicker = document.createElement('colorpicker'); + colorpicker.setAttribute('type', 'button'); + colorpicker.setAttribute('color', aTagInfo.color || 'inherit') + colorCell.appendChild(colorpicker); + + var entry = document.createElement('listitem'); + entry.addEventListener('focus', OnFocus, true); + entry.addEventListener('change', OnChange); + entry.setAttribute('allowevents', 'true'); // activate textbox and colorpicker + entry.tagInfo = aTagInfo; + entry.appendChild(tagCell); + entry.appendChild(colorCell); + + gTagList.insertBefore(entry, aRefChild); + return entry; +} + +function OnFocus(aEvent) +{ + gTagList.selectedItem = this; + UpdateButtonStates(); +} + +function FocusTagEntry(aEntry) +{ + // focus the entry's textbox + gTagList.ensureElementIsVisible(aEntry); + aEntry.firstChild.firstChild.focus(); +} + +function GetTagOrdinal(aTagInfo) +{ + if (aTagInfo.ordinal) + return aTagInfo.ordinal; + return aTagInfo.key; +} + +function SetTagOrdinal(aTagInfo, aOrdinal) +{ + var ordinal = aTagInfo.ordinal; + aTagInfo.ordinal = (aTagInfo.key != aOrdinal) ? aOrdinal : ''; + if (aTagInfo.ordinal != ordinal) + aTagInfo.changed = true; +} + +function BisectString(aPrev, aNext) +{ + // find a string which is lexically greater than aPrev and lesser than aNext: + // - copy leading parts common to aPrev and aNext into the result + // - find the first position where aPrev and aNext differ: + // - if we can squeeze a character in between there: fine, done! + // - if not: + // - if the rest of aNext is longer than one character, we can squeeze + // in just the first aNext rest-character and be done! + // - else we try to "increment" aPrev a bit to fit in + if ((aPrev >= aNext) || (aPrev + kOrdinalCharLow >= aNext)) + return ''; // no such string exists + + // pad the shorter string + var lenPrev = aPrev.length; + var lenNext = aNext.length; + var lenMax = Math.max(lenPrev, lenNext); + + // loop over both strings at once, padding if necessary + var constructing = false; + var result = ''; + for (var i = 0; i < lenMax; ++i) + { + var prevChar = (i < lenPrev) ? aPrev[i] : kOrdinalPadding; + var nextChar = constructing ? kOrdinalCharHigh + : (i < lenNext) ? aNext[i] + : kOrdinalPadding; + var prevCode = prevChar.charCodeAt(0); + var nextCode = nextChar.charCodeAt(0); + if (prevCode == nextCode) + { + // copy common characters + result += prevChar; + } + else if (prevCode + 1 < nextCode) + { + // found a real bisecting string + result += String.fromCharCode((prevCode + nextCode) / 2); + return result; + } + else + { + // nextCode is greater than prevCode, but there's no place in between. + // But if aNext[i+1] exists, then nextChar will suffice and we're done! + // ("x" < "xsomething") + if (i + 1 < lenNext) + { + // found a real bisecting string + return result + nextChar; + } + // just copy over prevChar and enter construction mode + result += prevChar; + constructing = true; + } + } + return ''; // nothing found +} + +function RecalculateOrdinal(aEntry) +{ + // Calculate a new ordinal for the given entry, assuming that both its + // predecessor's and successor's are correct, i.e. ord(p) < ord(s)! + var tagInfo = aEntry.tagInfo; + var ordinal = tagInfo.key; + // get neighbouring ordinals + var prevOrdinal = '', nextOrdinal = ''; + var prev = aEntry.previousSibling; + if (prev && prev.nodeName == 'listitem') // first.prev == listhead + prevOrdinal = GetTagOrdinal(prev.tagInfo); + var next = aEntry.nextSibling; + if (next) + { + nextOrdinal = GetTagOrdinal(next.tagInfo); + } + else + { + // ensure key < nextOrdinal if entry is the last/only entry + nextOrdinal = prevOrdinal || ordinal; + nextOrdinal = String.fromCharCode(nextOrdinal.charCodeAt(0) + 2); + } + + if (prevOrdinal < ordinal && ordinal < nextOrdinal) + { + // no ordinal needed, just clear it + SetTagOrdinal(tagInfo, '') + return; + } + + // so we need a new ordinal, because key <= prevOrdinal or key >= nextOrdinal + ordinal = BisectString(prevOrdinal, nextOrdinal); + if (ordinal) + { + // found a new ordinal + SetTagOrdinal(tagInfo, ordinal) + return; + } + + // couldn't find an ordinal before the nextOrdinal, so take that instead + // and recalculate a new one for the next entry + SetTagOrdinal(tagInfo, nextOrdinal); + if (next) + ApplyChange(next); +} + +function OnChange(aEvent) +{ + ApplyChange(aEvent.currentTarget); +} + +function ApplyChange(aEntry) +{ + if (!aEntry) + { + dump('ApplyChange: aEntry is null! (called by ' + ApplyChange.caller.name + ')\n'); + return; + } + + // the tag data got changed, so write it back to the system + var tagInfo = aEntry.tagInfo; + UpdateTagInfo(tagInfo, aEntry); + // ensure unique tag name + var dupeList = ReadTagListFromUI(aEntry); + var uniqueTag = DisambiguateTag(tagInfo.tag, dupeList); + if (tagInfo.tag != uniqueTag) + { + tagInfo.tag = uniqueTag; + tagInfo.changed = true; + UpdateTagEntry(tagInfo, aEntry); + } + + if (gInstantApply) + { + // If the item was newly added, we still can rename the key, + // so that it's in sync with the actual tag. + if (tagInfo.new && tagInfo.key) + { + // Do not clear the "new" flag! + // The key will only stick after closing the dialog. + MailServices.tags.deleteKey(tagInfo.key); + tagInfo.key = ''; + } + if (!tagInfo.key) + { + // create a new key, based upon the new tag + MailServices.tags.addTag(tagInfo.tag, '', ''); + tagInfo.key = MailServices.tags.getKeyForTag(tagInfo.tag); + } + + // Recalculate the sort ordinal, if necessary. + // We assume that the neighbour's ordinals are correct, + // i.e. that ordinal(pos - 1) < ordinal(pos + 1)! + RecalculateOrdinal(aEntry); + WriteTag(tagInfo); + } +} + +function WriteTag(aTagInfo) +{ +//dump('********** WriteTag: ' + aTagInfo.toSource() + '\n'); + try + { + MailServices.tags.addTagForKey(aTagInfo.key, aTagInfo.tag, aTagInfo.color, + aTagInfo.ordinal); + aTagInfo.changed = false; + } + catch (e) + { + dump('WriteTag: update exception:\n' + e); + } +} + +function UpdateButtonStates() +{ + var entry = gTagList.selectedItem; + // disable Delete if no selection + gDeleteButton.disabled = !entry; + // disable Raise if no selection or first entry + gRaiseButton.disabled = !entry || !gTagList.getPreviousItem(entry, 1); + // disable Lower if no selection or last entry + gLowerButton.disabled = !entry || !gTagList.getNextItem(entry, 1); +} + +function ReadTagListFromUI(aIgnoreEntry) +{ + // reads distinct tag names from the UI + var dupeList = {}; // indexed by tag + for (var entry = gTagList.firstChild; entry; entry = entry.nextSibling) + if ((entry != aIgnoreEntry) && (entry.localName == 'listitem')) + dupeList[entry.firstChild.firstChild.value] = true; + return dupeList; +} + +function DisambiguateTag(aTag, aTagList) +{ + if (aTag in aTagList) + { + var suffix = 2; + while (aTag + ' ' + suffix in aTagList) + ++suffix; + aTag += ' ' + suffix; + } + return aTag; +} + +function AddTag() +{ + // Add a new tag to the UI here. + // It will be be written to the preference system + // (a) directly on each change for instant apply, or + // (b) only if the dialogaccept handler is executed. + + // create new unique tag name + var dupeList = ReadTagListFromUI(); + var tag = DisambiguateTag(gAddButton.getAttribute('defaulttagname'), dupeList); + + // create new tag list entry + var tagInfo = {tag: tag, + key: '', + color: 'inherit', + ordinal: '', + new: true, + changed: true}; + var refChild = gTagList.getNextItem(gTagList.selectedItem, 1); + var newEntry = AppendTagEntry(tagInfo, refChild); + ApplyChange(newEntry); + FocusTagEntry(newEntry); +} + +function DeleteTag() +{ + // Delete the selected tag from the UI here. If it was added during this + // preference dialog session, we can drop it at once; if it was read from + // the preferences system, we may need to remember killing it in OnOK. + var entry = gTagList.selectedItem; + var key = entry.tagInfo.key; + if (key) + { + if (gInstantApply) + MailServices.tags.deleteKey(key); + else + gDeletedTags[key] = true; // dummy value + } + // after removing, move focus to next entry, if it exist, else try previous + var newFocusItem = gTagList.getNextItem(entry, 1) || + gTagList.getPreviousItem(entry, 1); + gTagList.removeItemAt(gTagList.getIndexOfItem(entry)); + if (newFocusItem) + FocusTagEntry(newFocusItem); + else + UpdateButtonStates(); +} + +function MoveTag(aMoveUp) +{ + // Move the selected tag one position up or down in the tagList's child order. + // This reordering may require changing ordinal strings. + var entry = gTagList.selectedItem; + var tagInfo = entry.tagInfo; + UpdateTagInfo(tagInfo, entry); // remember changed values + var successor = aMoveUp ? gTagList.getPreviousItem(entry, 1) + : gTagList.getNextItem(entry, 2); + entry.parentNode.insertBefore(entry, successor); + FocusTagEntry(entry); + tagInfo.changed = true; + UpdateTagEntry(tagInfo, entry); // needs to be visible + ApplyChange(entry); +} + +function Restore() +{ + // clear pref panel tag list + // Remember any known keys for deletion in the OKHandler. + while (gTagList.getRowCount()) + { + var key = gTagList.removeItemAt(0).tagInfo.key; + if (key) + { + if (gInstantApply) + MailServices.tags.deleteKey(key); + else + gDeletedTags[key] = true; // dummy value + } + } + // add default items (no ordinal strings for those) + for (var i = 1; i <= 5; ++i) + { + // create default tags from the former label defaults + var key = "$label" + i; + var tag = GetLocalizedStringPref("mailnews.labels.description." + i); + var color = Services.prefs.getDefaultBranch("mailnews.labels.color.").getCharPref(i); + var tagInfo = {tag: tag, + key: key, + color: color, + ordinal: '', + new: false, + changed: true}; + var newEntry = AppendTagEntry(tagInfo, null); + ApplyChange(newEntry); + } + FocusTagEntry(gTagList.getItemAtIndex(0)); +} + +function OnOK() +{ + // remove all deleted tags from the preferences system + for (var key in gDeletedTags) + MailServices.tags.deleteKey(key); + + // Write tags to the preferences system, creating keys and ordinal strings. + for (var entry = gTagList.firstChild; entry; entry = entry.nextSibling) + { + if (entry.localName == 'listitem') + { + // only write listitems which have changed (this includes new ones) + var tagInfo = entry.tagInfo; + if (tagInfo.changed) + { + if (!tagInfo.key) + { + // newly added tag, need to create a key and read it + MailServices.tags.addTag(tagInfo.tag, '', ''); + tagInfo.key = MailServices.tags.getKeyForTag(tagInfo.tag); + } + if (tagInfo.key) + { + // Recalculate the sort ordinal, if necessary. + // We assume that the neighbour's ordinals are correct, + // i.e. that ordinal(pos - 1) < ordinal(pos + 1)! + RecalculateOrdinal(entry); + // update the tag definition + WriteTag(tagInfo); + } + } + } + } +} diff --git a/comm/suite/mailnews/components/prefs/content/pref-tags.xul b/comm/suite/mailnews/components/prefs/content/pref-tags.xul new file mode 100644 index 0000000000..cd99824a25 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-tags.xul @@ -0,0 +1,83 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/prefPanels.css" type="text/css"?> + +<!DOCTYPE page SYSTEM "chrome://messenger/locale/pref-tags.dtd"> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="tags_pane" + label="&pref.tags.title;" + script="chrome://messenger/content/pref-tags.js"> + + <preferences id="tags_preferences"> + <preference id="pref.tags.disable_button.add" + name="pref.tags.disable_button.add" + type="bool"/> + <preference id="pref.tags.disable_button.delete" + name="pref.tags.disable_button.delete" + type="bool"/> + <preference id="pref.tags.disable_button.lower" + name="pref.tags.disable_button.lower" + type="bool"/> + <preference id="pref.tags.disable_button.raise" + name="pref.tags.disable_button.raise" + type="bool"/> + <preference id="pref.tags.disable_button.restore" + name="pref.tags.disable_button.restore" + type="bool"/> + </preferences> + + <groupbox flex="1"> + <caption label="&pref.tags.caption;"/> + <label control="tagList">&pref.tags.description;</label> + <hbox flex="1"> + <listbox id="tagList" flex="1" onselect="UpdateButtonStates();"> + <listcols> + <listcol flex="1"/> + <listcol/> + </listcols> + <listhead> + <listheader label="&tagColumn.label;"/> + <listheader label="&colorColumn.label;"/> + </listhead> + </listbox> + + <vbox> + <button id="addTagButton" + label="&addTagButton.label;" + accesskey="&addTagButton.accesskey;" + defaulttagname="&defaultTagName.label;" + preference="pref.tags.disable_button.add" + oncommand="AddTag();"/> + <button id="deleteTagButton" + label="&deleteTagButton.label;" + accesskey="&deleteTagButton.accesskey;" + preference="pref.tags.disable_button.delete" + oncommand="DeleteTag();"/> + <spacer flex="1"/> + <button id="raiseTagButton" + label="&raiseTagButton.label;" + accesskey="&raiseTagButton.accesskey;" + preference="pref.tags.disable_button.raise" + oncommand="MoveTag(true);"/> + <button id="lowerTagButton" + label="&lowerTagButton.label;" + accesskey="&lowerTagButton.accesskey;" + preference="pref.tags.disable_button.lower" + oncommand="MoveTag(false);"/> + <spacer flex="1"/> + <button id="restoreButton" + label="&restoreButton.label;" + accesskey="&restoreButton.accesskey;" + preference="pref.tags.disable_button.restore" + oncommand="Restore();"/> + </vbox> + </hbox> + </groupbox> + + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.js b/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.js new file mode 100644 index 0000000000..75b5da1b3d --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.js @@ -0,0 +1,26 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// The contents of this file will be loaded into the scope of the object +// <prefpane id="viewing_messages_pane">! + +function Startup() +{ + var autoPref = document.getElementById("mailnews.mark_message_read.auto"); + UpdateMarkAsReadOptions(autoPref.value); +} + +function UpdateMarkAsReadOptions(aEnableReadDelay) +{ + EnableElementById("markAsReadAfterPreferences", aEnableReadDelay, false); + // ... and the extras! + var delayPref = document.getElementById("mailnews.mark_message_read.delay"); + UpdateMarkAsReadTextbox(aEnableReadDelay && delayPref.value, false); +} + +function UpdateMarkAsReadTextbox(aEnable, aFocus) +{ + EnableElementById("markAsReadDelay", aEnable, aFocus); +} diff --git a/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.xul b/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.xul new file mode 100644 index 0000000000..117761c86b --- /dev/null +++ b/comm/suite/mailnews/components/prefs/content/pref-viewing_messages.xul @@ -0,0 +1,174 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE overlay [ +<!ENTITY % pref-viewing_messagesDTD SYSTEM "chrome://messenger/locale/pref-viewing_messages.dtd"> +%pref-viewing_messagesDTD; +<!ENTITY % editorOverlayDTD SYSTEM "chrome://editor/locale/editorOverlay.dtd"> +%editorOverlayDTD; +]> + +<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <prefpane id="viewing_messages_pane" + label="&pref.viewing.messages.title;" + script="chrome://messenger/content/pref-viewing_messages.js"> + + <preferences id="viewing_messages_preferences"> + <preference id="mailnews.reuse_message_window" + name="mailnews.reuse_message_window" + type="bool"/> + <preference id="mail.close_message_window.on_delete" + name="mail.close_message_window.on_delete" + type="bool"/> + <preference id="mailnews.message_display.disable_remote_image" + name="mailnews.message_display.disable_remote_image" + type="bool"/> + <preference id="mailnews.mark_message_read.auto" + name="mailnews.mark_message_read.auto" + type="bool" + onchange="UpdateMarkAsReadOptions(this.value);"/> + <preference id="mailnews.mark_message_read.delay" + name="mailnews.mark_message_read.delay" + type="bool" + onchange="UpdateMarkAsReadTextbox(this.value, this.value);"/> + <preference id="mailnews.mark_message_read.delay.interval" + name="mailnews.mark_message_read.delay.interval" + type="int"/> + <preference id="mail.fixed_width_messages" + name="mail.fixed_width_messages" + type="bool"/> + <preference id="mail.wrap_long_lines" + name="mail.wrap_long_lines" + type="bool"/> + <preference id="mail.display_glyph" + name="mail.display_glyph" + type="bool"/> + <preference id="mail.quoted_style" + name="mail.quoted_style" + type="int"/> + <preference id="mail.quoted_size" + name="mail.quoted_size" + type="int"/> + <preference id="mail.citation_color" + name="mail.citation_color" + type="string"/> + <preference id="mail.showCondensedAddresses" + name="mail.showCondensedAddresses" + type="bool"/> + </preferences> + + <groupbox align="start"> + <caption label="&generalMessageDisplay.caption;"/> + <label value="&openingMessages.label;" control="reuseMessageWindow"/> + <vbox class="indent"> + <radiogroup id="reuseMessageWindow" + orient="horizontal" + preference="mailnews.reuse_message_window"> + <radio id="new" + label="&newWindowRadio.label;" + accesskey="&newWindowRadio.accesskey;" + value="false"/> + <radio id="existing" + label="&existingWindowRadio.label;" + accesskey="&existingWindowRadio.accesskey;" + value="true"/> + </radiogroup> + <checkbox id="closeMsgWindowOnDelete" + label="&closeMsgWindowOnDelete.label;" + accesskey="&closeMsgWindowOnDelete.accesskey;" + preference="mail.close_message_window.on_delete"/> + </vbox> + + <checkbox id="disableContent" label="&disableContent.label;" + accesskey="&disableContent.accesskey;" + preference="mailnews.message_display.disable_remote_image"/> + <checkbox id="showCondensedAddresses" + label="&showCondensedAddresses.label;" + accesskey="&showCondensedAddresses.accesskey;" + preference="mail.showCondensedAddresses"/> + + <separator class="thin"/> + + <checkbox id="automaticallyMarkAsRead" + preference="mailnews.mark_message_read.auto" + label="&autoMarkAsRead.label;" + accesskey="&autoMarkAsRead.accesskey;" + oncommand="UpdateMarkAsReadOptions(this.checked);"/> + + <hbox align="center" class="indent"> + <checkbox id="markAsReadAfterPreferences" + label="&markAsReadAfter.label;" + accesskey="&markAsReadAfter.accesskey;" + preference="mailnews.mark_message_read.delay"/> + <textbox id="markAsReadDelay" + type="number" + size="2" + maximum="99" + preference="mailnews.mark_message_read.delay.interval" + aria-labelledby="markAsReadAfterPreferences markAsReadDelay secondsLabel"/> + <label id="secondsLabel" + value="&secondsLabel.label;"> + <observes element="markAsReadAfterPreferences" + attribute="disabled"/> + </label> + </hbox> + </groupbox> + + <groupbox> + <caption label="&displayPlainText.caption;"/> + <hbox align="center"> + <label value="&fontPlainText.label;" + accesskey="&fontPlainText.accesskey;" + control="mailFixedWidthMessages"/> + <radiogroup id="mailFixedWidthMessages" + orient="horizontal" + preference="mail.fixed_width_messages"> + <radio label="&fontFixedWidth.label;" + accesskey="&fontFixedWidth.accesskey;" + value="true"/> + <radio label="&fontVarWidth.label;" + accesskey="&fontVarWidth.accesskey;" + value="false"/> + </radiogroup> + </hbox> + + <checkbox id="wrapLongLines" label="&wrapInMsg.label;" + accesskey="&wrapInMsg.accesskey;" + preference="mail.wrap_long_lines"/> + <checkbox id="displayGlyph" label="&convertEmoticons.label;" + accesskey="&convertEmoticons.accesskey;" + preference="mail.display_glyph"/> + + <separator class="thin"/> + + <description>&displayQuoted.label;</description> + <hbox class="indent" align="center"> + <label value="&style.label;" accesskey="&style.accesskey;" control="mailQuotedStyle"/> + <menulist id="mailQuotedStyle" preference="mail.quoted_style"> + <menupopup> + <menuitem value="0" label="®ular.label;"/> + <menuitem value="1" label="&bold.label;"/> + <menuitem value="2" label="&italic.label;"/> + <menuitem value="3" label="&boldItalic.label;"/> + </menupopup> + </menulist> + + <label value="&size.label;" accesskey="&size.accesskey;" control="mailQuotedSize"/> + <menulist id="mailQuotedSize" preference="mail.quoted_size"> + <menupopup> + <menuitem value="0" label="®ular.label;"/> + <menuitem value="1" label="&bigger.label;"/> + <menuitem value="2" label="&smaller.label;"/> + </menupopup> + </menulist> + + <label value="&color.label;" accesskey="&color.accesskey;" control="mailCitationColor"/> + <colorpicker type="button" id="mailCitationColor" palettename="standard" + preference="mail.citation_color"/> + </hbox> + </groupbox> + </prefpane> +</overlay> diff --git a/comm/suite/mailnews/components/prefs/jar.mn b/comm/suite/mailnews/components/prefs/jar.mn new file mode 100644 index 0000000000..9baa230079 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/jar.mn @@ -0,0 +1,23 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +messenger.jar: + content/messenger/mailPrefsOverlay.xul (content/mailPrefsOverlay.xul) +* content/messenger/pref-mailnews.xul (content/pref-mailnews.xul) + content/messenger/pref-mailnews.js (content/pref-mailnews.js) + content/messenger/pref-notifications.xul (content/pref-notifications.xul) + content/messenger/pref-notifications.js (content/pref-notifications.js) + content/messenger/pref-junk.xul (content/pref-junk.xul) + content/messenger/pref-junk.js (content/pref-junk.js) + content/messenger/pref-tags.xul (content/pref-tags.xul) + content/messenger/pref-tags.js (content/pref-tags.js) + content/messenger/pref-viewing_messages.xul (content/pref-viewing_messages.xul) + content/messenger/pref-viewing_messages.js (content/pref-viewing_messages.js) + content/messenger/pref-receipts.xul (content/pref-receipts.xul) + content/messenger/pref-receipts.js (content/pref-receipts.js) + content/messenger/pref-character_encoding.xul (content/pref-character_encoding.xul) + content/messenger/pref-character_encoding.js (content/pref-character_encoding.js) + content/messenger/pref-offline.xul (content/pref-offline.xul) + content/messenger/pref-offline.js (content/pref-offline.js) + diff --git a/comm/suite/mailnews/components/prefs/moz.build b/comm/suite/mailnews/components/prefs/moz.build new file mode 100644 index 0000000000..de5cd1bf81 --- /dev/null +++ b/comm/suite/mailnews/components/prefs/moz.build @@ -0,0 +1,6 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ["jar.mn"] diff --git a/comm/suite/mailnews/components/smime/content/msgCompSMIMEOverlay.js b/comm/suite/mailnews/components/smime/content/msgCompSMIMEOverlay.js new file mode 100644 index 0000000000..e802130008 --- /dev/null +++ b/comm/suite/mailnews/components/smime/content/msgCompSMIMEOverlay.js @@ -0,0 +1,354 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +// Account encryption policy values: +// const kEncryptionPolicy_Never = 0; +// 'IfPossible' was used by ns4. +// const kEncryptionPolicy_IfPossible = 1; +var kEncryptionPolicy_Always = 2; + +var gEncryptedURIService = + Cc["@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"] + .getService(Ci.nsIEncryptedSMIMEURIsService); + +var gNextSecurityButtonCommand = ""; +var gSMFields = null; +var gEncryptOptionChanged; +var gSignOptionChanged; + +function onComposerLoad() +{ + // Are we already set up ? Or are the required fields missing ? + if (gSMFields || !gMsgCompose || !gMsgCompose.compFields) + return; + + gMsgCompose.compFields.composeSecure = null; + + gSMFields = Cc["@mozilla.org/messengercompose/composesecure;1"] + .createInstance(Ci.nsIMsgComposeSecure); + if (!gSMFields) + return; + + gMsgCompose.compFields.composeSecure = gSMFields; + + // Set up the initial security state. + gSMFields.requireEncryptMessage = + gCurrentIdentity.getIntAttribute("encryptionpolicy") == kEncryptionPolicy_Always; + if (!gSMFields.requireEncryptMessage && + gEncryptedURIService && + gEncryptedURIService.isEncrypted(gMsgCompose.originalMsgURI)) + { + // Override encryption setting if original is known as encrypted. + gSMFields.requireEncryptMessage = true; + } + if (gSMFields.requireEncryptMessage) + setEncryptionUI(); + else + setNoEncryptionUI(); + + gSMFields.signMessage = gCurrentIdentity.getBoolAttribute("sign_mail"); + if (gSMFields.signMessage) + setSignatureUI(); + else + setNoSignatureUI(); +} + +addEventListener("load", smimeComposeOnLoad, {capture: false, once: true}); + +// this function gets called multiple times +function smimeComposeOnLoad() +{ + onComposerLoad(); + + top.controllers.appendController(SecurityController); + + addEventListener("compose-from-changed", onComposerFromChanged, true); + addEventListener("compose-send-message", onComposerSendMessage, true); + + addEventListener("unload", smimeComposeOnUnload, {capture: false, once: true}); +} + +function smimeComposeOnUnload() +{ + removeEventListener("compose-from-changed", onComposerFromChanged, true); + removeEventListener("compose-send-message", onComposerSendMessage, true); + + top.controllers.removeController(SecurityController); +} + +function showNeedSetupInfo() +{ + let compSmimeBundle = document.getElementById("bundle_comp_smime"); + let brandBundle = document.getElementById("brandBundle"); + if (!compSmimeBundle || !brandBundle) + return; + + let buttonPressed = Services.prompt.confirmEx(window, + brandBundle.getString("brandShortName"), + compSmimeBundle.getString("NeedSetup"), + Services.prompt.STD_YES_NO_BUTTONS, 0, 0, 0, null, {}); + if (buttonPressed == 0) + openHelp("sign-encrypt", "chrome://communicator/locale/help/suitehelp.rdf"); +} + +function toggleEncryptMessage() +{ + if (!gSMFields) + return; + + gSMFields.requireEncryptMessage = !gSMFields.requireEncryptMessage; + + if (gSMFields.requireEncryptMessage) + { + // Make sure we have a cert. + if (!gCurrentIdentity.getUnicharAttribute("encryption_cert_name")) + { + gSMFields.requireEncryptMessage = false; + showNeedSetupInfo(); + return; + } + + setEncryptionUI(); + } + else + { + setNoEncryptionUI(); + } + + gEncryptOptionChanged = true; +} + +function toggleSignMessage() +{ + if (!gSMFields) + return; + + gSMFields.signMessage = !gSMFields.signMessage; + + if (gSMFields.signMessage) // make sure we have a cert name... + { + if (!gCurrentIdentity.getUnicharAttribute("signing_cert_name")) + { + gSMFields.signMessage = false; + showNeedSetupInfo(); + return; + } + + setSignatureUI(); + } + else + { + setNoSignatureUI(); + } + + gSignOptionChanged = true; +} + +function setSecuritySettings(menu_id) +{ + if (!gSMFields) + return; + + document.getElementById("menu_securityEncryptRequire" + menu_id) + .setAttribute("checked", gSMFields.requireEncryptMessage); + document.getElementById("menu_securitySign" + menu_id) + .setAttribute("checked", gSMFields.signMessage); +} + +function setNextCommand(what) +{ + gNextSecurityButtonCommand = what; +} + +function doSecurityButton() +{ + var what = gNextSecurityButtonCommand; + gNextSecurityButtonCommand = ""; + + switch (what) + { + case "encryptMessage": + toggleEncryptMessage(); + break; + + case "signMessage": + toggleSignMessage(); + break; + + case "show": + default: + showMessageComposeSecurityStatus(); + } +} + +function setNoSignatureUI() +{ + top.document.getElementById("securityStatus").removeAttribute("signing"); + top.document.getElementById("signing-status").collapsed = true; +} + +function setSignatureUI() +{ + top.document.getElementById("securityStatus").setAttribute("signing", "ok"); + top.document.getElementById("signing-status").collapsed = false; +} + +function setNoEncryptionUI() +{ + top.document.getElementById("securityStatus").removeAttribute("crypto"); + top.document.getElementById("encryption-status").collapsed = true; +} + +function setEncryptionUI() +{ + top.document.getElementById("securityStatus").setAttribute("crypto", "ok"); + top.document.getElementById("encryption-status").collapsed = false; +} + +function showMessageComposeSecurityStatus() +{ + Recipients2CompFields(gMsgCompose.compFields); + + window.openDialog( + "chrome://messenger-smime/content/msgCompSecurityInfo.xul", + "", + "chrome,modal,resizable,centerscreen", + { + compFields : gMsgCompose.compFields, + subject : GetMsgSubjectElement().value, + smFields : gSMFields, + isSigningCertAvailable : + gCurrentIdentity.getUnicharAttribute("signing_cert_name") != "", + isEncryptionCertAvailable : + gCurrentIdentity.getUnicharAttribute("encryption_cert_name") != "", + currentIdentity : gCurrentIdentity + } + ); +} + +var SecurityController = +{ + supportsCommand: function(command) + { + switch (command) + { + case "cmd_viewSecurityStatus": + return true; + + default: + return false; + } + }, + + isCommandEnabled: function(command) + { + switch (command) + { + case "cmd_viewSecurityStatus": + return true; + + default: + return false; + } + } +}; + +function onComposerSendMessage() +{ + let emailAddresses = []; + + try + { + if (!gMsgCompose.compFields.composeSecure.requireEncryptMessage) + return; + + emailAddresses = Cc["@mozilla.org/messenger-smime/smimejshelper;1"] + .createInstance(Ci.nsISMimeJSHelper) + .getNoCertAddresses(gMsgCompose.compFields); + } + catch (e) + { + return; + } + + if (emailAddresses.length > 0) + { + // The rules here: If the current identity has a directoryServer set, then + // use that, otherwise, try the global preference instead. + + let autocompleteDirectory; + + // Does the current identity override the global preference? + if (gCurrentIdentity.overrideGlobalPref) + { + autocompleteDirectory = gCurrentIdentity.directoryServer; + } + else + { + // Try the global one + if (Services.prefs.getBoolPref("ldap_2.autoComplete.useDirectory")) + autocompleteDirectory = + Services.prefs.getCharPref("ldap_2.autoComplete.directoryServer"); + } + + if (autocompleteDirectory) + window.openDialog("chrome://messenger-smime/content/certFetchingStatus.xul", + "", + "chrome,modal,resizable,centerscreen", + autocompleteDirectory, + emailAddresses); + } +} + +function onComposerFromChanged() +{ + if (!gSMFields) + return; + + var encryptionPolicy = gCurrentIdentity.getIntAttribute("encryptionpolicy"); + var useEncryption = false; + + if (!gEncryptOptionChanged) + { + // Encryption wasn't manually checked. + // Set up the encryption policy from the setting of the new identity. + + // 0 == never, 1 == if possible (ns4), 2 == always encrypt. + useEncryption = (encryptionPolicy == kEncryptionPolicy_Always); + } + else + { + useEncryption = !!gCurrentIdentity.getUnicharAttribute("encryption_cert_name"); + } + + gSMFields.requireEncryptMessage = useEncryption; + if (useEncryption) + setEncryptionUI(); + else + setNoEncryptionUI(); + + // - If signing is disabled, we will not turn it on automatically. + // - If signing is enabled, but the new account defaults to not sign, we will turn signing off. + var signMessage = gCurrentIdentity.getBoolAttribute("sign_mail"); + var useSigning = false; + + if (!gSignOptionChanged) + { + // Signing wasn't manually checked. + // Set up the signing policy from the setting of the new identity. + useSigning = signMessage; + } + else + { + useSigning = !!gCurrentIdentity.getUnicharAttribute("signing_cert_name"); + } + gSMFields.signMessage = useSigning; + if (useSigning) + setSignatureUI(); + else + setNoSignatureUI(); +} diff --git a/comm/suite/mailnews/components/smime/content/msgHdrViewSMIMEOverlay.js b/comm/suite/mailnews/components/smime/content/msgHdrViewSMIMEOverlay.js new file mode 100644 index 0000000000..09f665b4d0 --- /dev/null +++ b/comm/suite/mailnews/components/smime/content/msgHdrViewSMIMEOverlay.js @@ -0,0 +1,258 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var gSignedUINode = null; +var gEncryptedUINode = null; +var gSMIMEContainer = null; +var gStatusBar = null; +var gSignedStatusPanel = null; +var gEncryptedStatusPanel = null; + +var gEncryptedURIService = null; +var gMyLastEncryptedURI = null; + +var gSMIMEBundle = null; +// var gBrandBundle; -- defined in mailWindow.js + +// manipulates some globals from msgReadSMIMEOverlay.js + +var nsICMSMessageErrors = Ci.nsICMSMessageErrors; + +/// Get the necko URL for the message URI. +function neckoURLForMessageURI(aMessageURI) +{ + let msgSvc = Cc["@mozilla.org/messenger;1"] + .createInstance(Ci.nsIMessenger) + .messageServiceFromURI(aMessageURI); + let neckoURI = msgSvc.getUrlForUri(aMessageURI); + return neckoURI.spec; +} + +var smimeHeaderSink = +{ + maxWantedNesting: function() + { + return 1; + }, + + signedStatus: function(aNestingLevel, aSignatureStatus, aSignerCert) + { + if (aNestingLevel > 1) { + // we are not interested + return; + } + + gSignatureStatus = aSignatureStatus; + gSignerCert = aSignerCert; + + gSMIMEContainer.collapsed = false; + gSignedUINode.collapsed = false; + gSignedStatusPanel.collapsed = false; + + switch (aSignatureStatus) { + case nsICMSMessageErrors.SUCCESS: + gSignedUINode.setAttribute("signed", "ok"); + gStatusBar.setAttribute("signed", "ok"); + break; + + case nsICMSMessageErrors.VERIFY_NOT_YET_ATTEMPTED: + gSignedUINode.setAttribute("signed", "unknown"); + gStatusBar.setAttribute("signed", "unknown"); + break; + + case nsICMSMessageErrors.VERIFY_CERT_WITHOUT_ADDRESS: + case nsICMSMessageErrors.VERIFY_HEADER_MISMATCH: + gSignedUINode.setAttribute("signed", "mismatch"); + gStatusBar.setAttribute("signed", "mismatch"); + break; + + default: + gSignedUINode.setAttribute("signed", "notok"); + gStatusBar.setAttribute("signed", "notok"); + break; + } + }, + + encryptionStatus: function(aNestingLevel, aEncryptionStatus, aRecipientCert) + { + if (aNestingLevel > 1) { + // we are not interested + return; + } + + gEncryptionStatus = aEncryptionStatus; + gEncryptionCert = aRecipientCert; + + gSMIMEContainer.collapsed = false; + gEncryptedUINode.collapsed = false; + gEncryptedStatusPanel.collapsed = false; + + if (nsICMSMessageErrors.SUCCESS == aEncryptionStatus) + { + gEncryptedUINode.setAttribute("encrypted", "ok"); + gStatusBar.setAttribute("encrypted", "ok"); + } + else + { + gEncryptedUINode.setAttribute("encrypted", "notok"); + gStatusBar.setAttribute("encrypted", "notok"); + } + + if (gEncryptedURIService) + { + // Remember the message URI and the corresponding necko URI. + gMyLastEncryptedURI = GetLoadedMessage(); + gEncryptedURIService.rememberEncrypted(gMyLastEncryptedURI); + gEncryptedURIService.rememberEncrypted( + neckoURLForMessageURI(gMyLastEncryptedURI)); + } + + switch (aEncryptionStatus) + { + case nsICMSMessageErrors.SUCCESS: + case nsICMSMessageErrors.ENCRYPT_INCOMPLETE: + break; + default: + var brand = gBrandBundle.getString("brandShortName"); + var title = gSMIMEBundle.getString("CantDecryptTitle").replace(/%brand%/g, brand); + var body = gSMIMEBundle.getString("CantDecryptBody").replace(/%brand%/g, brand); + + // insert our message + msgWindow.displayHTMLInMessagePane(title, + "<html>\n" + + "<body bgcolor=\"#fafaee\">\n" + + "<center><br><br><br>\n" + + "<table>\n" + + "<tr><td>\n" + + "<center><strong><font size=\"+3\">\n" + + title+"</font></center><br>\n" + + body+"\n" + + "</td></tr></table></center></body></html>", false); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIMsgSMIMEHeaderSink"]), +}; + +function forgetEncryptedURI() +{ + if (gMyLastEncryptedURI && gEncryptedURIService) + { + gEncryptedURIService.forgetEncrypted(gMyLastEncryptedURI); + gEncryptedURIService.forgetEncrypted( + neckoURLForMessageURI(gMyLastEncryptedURI)); + gMyLastEncryptedURI = null; + } +} + +function onSMIMEStartHeaders() +{ + gEncryptionStatus = -1; + gSignatureStatus = -1; + + gSignerCert = null; + gEncryptionCert = null; + + gSMIMEContainer.collapsed = true; + + gSignedUINode.collapsed = true; + gSignedUINode.removeAttribute("signed"); + gSignedStatusPanel.collapsed = true; + gStatusBar.removeAttribute("signed"); + + gEncryptedUINode.collapsed = true; + gEncryptedUINode.removeAttribute("encrypted"); + gEncryptedStatusPanel.collapsed = true; + gStatusBar.removeAttribute("encrypted"); + + forgetEncryptedURI(); +} + +function onSMIMEEndHeaders() +{} + +function onSmartCardChange() +{ + // only reload encrypted windows + if (gMyLastEncryptedURI && gEncryptionStatus != -1) + ReloadMessage(); +} + +function msgHdrViewSMIMEOnLoad(event) +{ + window.crypto.enableSmartCardEvents = true; + document.addEventListener("smartcard-insert", onSmartCardChange); + document.addEventListener("smartcard-remove", onSmartCardChange); + if (!gSMIMEBundle) + gSMIMEBundle = document.getElementById("bundle_read_smime"); + + // we want to register our security header sink as an opaque nsISupports + // on the msgHdrSink used by mail..... + msgWindow.msgHeaderSink.securityInfo = smimeHeaderSink; + + gSignedUINode = document.getElementById('signedHdrIcon'); + gEncryptedUINode = document.getElementById('encryptedHdrIcon'); + gSMIMEContainer = document.getElementById('smimeBox'); + gStatusBar = document.getElementById('status-bar'); + gSignedStatusPanel = document.getElementById('signed-status'); + gEncryptedStatusPanel = document.getElementById('encrypted-status'); + + // add ourself to the list of message display listeners so we get notified when we are about to display a + // message. + var listener = {}; + listener.onStartHeaders = onSMIMEStartHeaders; + listener.onEndHeaders = onSMIMEEndHeaders; + gMessageListeners.push(listener); + + gEncryptedURIService = + Cc["@mozilla.org/messenger-smime/smime-encrypted-uris-service;1"] + .getService(Ci.nsIEncryptedSMIMEURIsService); +} + +function msgHdrViewSMIMEOnUnload(event) +{ + window.crypto.enableSmartCardEvents = false; + document.removeEventListener("smartcard-insert", onSmartCardChange); + document.removeEventListener("smartcard-remove", onSmartCardChange); + forgetEncryptedURI(); + removeEventListener("messagepane-loaded", msgHdrViewSMIMEOnLoad, true); + removeEventListener("messagepane-unloaded", msgHdrViewSMIMEOnUnload, true); + removeEventListener("messagepane-hide", msgHdrViewSMIMEOnMessagePaneHide, true); + removeEventListener("messagepane-unhide", msgHdrViewSMIMEOnMessagePaneUnhide, true); +} + +function msgHdrViewSMIMEOnMessagePaneHide() +{ + gSMIMEContainer.collapsed = true; + gSignedUINode.collapsed = true; + gSignedStatusPanel.collapsed = true; + gEncryptedUINode.collapsed = true; + gEncryptedStatusPanel.collapsed = true; +} + +function msgHdrViewSMIMEOnMessagePaneUnhide() +{ + if (gEncryptionStatus != -1 || gSignatureStatus != -1) + { + gSMIMEContainer.collapsed = false; + + if (gSignatureStatus != -1) + { + gSignedUINode.collapsed = false; + gSignedStatusPanel.collapsed = false; + } + + if (gEncryptionStatus != -1) + { + gEncryptedUINode.collapsed = false; + gEncryptedStatusPanel.collapsed = false; + } + } +} + +addEventListener('messagepane-loaded', msgHdrViewSMIMEOnLoad, true); +addEventListener('messagepane-unloaded', msgHdrViewSMIMEOnUnload, true); +addEventListener('messagepane-hide', msgHdrViewSMIMEOnMessagePaneHide, true); +addEventListener('messagepane-unhide', msgHdrViewSMIMEOnMessagePaneUnhide, true); diff --git a/comm/suite/mailnews/components/smime/jar.mn b/comm/suite/mailnews/components/smime/jar.mn new file mode 100644 index 0000000000..2f1f63cf32 --- /dev/null +++ b/comm/suite/mailnews/components/smime/jar.mn @@ -0,0 +1,15 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +messenger.jar: +% content messenger-smime %content/messenger-smime/ + content/messenger/certpicker.js (../../../../mailnews/extensions/smime/certpicker.js) + content/messenger/certpicker.xhtml (../../../../mailnews/extensions/smime/certpicker.xhtml) + content/messenger-smime/msgHdrViewSMIMEOverlay.js (content/msgHdrViewSMIMEOverlay.js) + content/messenger-smime/msgCompSMIMEOverlay.js (content/msgCompSMIMEOverlay.js) + content/messenger-smime/msgReadSMIMEOverlay.js (../../../../mailnews/extensions/smime/msgReadSMIMEOverlay.js) + content/messenger-smime/msgCompSecurityInfo.js (../../../../mailnews/extensions/smime/msgCompSecurityInfo.js) + content/messenger-smime/msgCompSecurityInfo.xhtml (../../../../mailnews/extensions/smime/msgCompSecurityInfo.xhtml) + content/messenger-smime/certFetchingStatus.js (../../../../mailnews/extensions/smime/certFetchingStatus.js) + content/messenger-smime/certFetchingStatus.xhtml (../../../../mailnews/extensions/smime/certFetchingStatus.xhtml) diff --git a/comm/suite/mailnews/components/smime/moz.build b/comm/suite/mailnews/components/smime/moz.build new file mode 100644 index 0000000000..de5cd1bf81 --- /dev/null +++ b/comm/suite/mailnews/components/smime/moz.build @@ -0,0 +1,6 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ["jar.mn"] diff --git a/comm/suite/mailnews/content/ABSearchDialog.js b/comm/suite/mailnews/content/ABSearchDialog.js new file mode 100644 index 0000000000..15d85b6234 --- /dev/null +++ b/comm/suite/mailnews/content/ABSearchDialog.js @@ -0,0 +1,327 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const {encodeABTermValue} = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm"); +const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm"); +const {PluralForm} = ChromeUtils.import("resource://gre/modules/PluralForm.jsm"); + +var searchSessionContractID = "@mozilla.org/messenger/searchSession;1"; +var gSearchSession; + +var nsMsgSearchScope = Ci.nsMsgSearchScope; +var nsMsgSearchOp = Ci.nsMsgSearchOp; +var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib; +var nsIAbDirectory = Ci.nsIAbDirectory; + +var gStatusText; +var gSearchBundle; +var gAddressBookBundle; + +var gSearchStopButton; +var gPropertiesButton; +var gComposeButton; +var gSearchPhoneticName = "false"; + +var gSearchAbViewListener = { + onSelectionChanged: function() { + UpdateCardView(); + }, + onCountChanged: function(aTotal) { + let statusText; + if (aTotal == 0) { + statusText = gAddressBookBundle.getString("noMatchFound"); + } else { + statusText = PluralForm + .get(aTotal, gAddressBookBundle.getString("matchesFound1")) + .replace("#1", aTotal); + } + + gStatusText.setAttribute("label", statusText); + } +}; + +function searchOnLoad() +{ + setHelpFileURI("chrome://communicator/locale/help/suitehelp.rdf"); + UpgradeAddressBookResultsPaneUI("mailnews.ui.advanced_directory_search_results.version"); + + initializeSearchWidgets(); + initializeSearchWindowWidgets(); + + gSearchBundle = document.getElementById("bundle_search"); + gSearchStopButton.setAttribute("label", gSearchBundle.getString("labelForSearchButton")); + gSearchStopButton.setAttribute("accesskey", gSearchBundle.getString("labelForSearchButton.accesskey")); + gAddressBookBundle = document.getElementById("bundle_addressBook"); + gSearchSession = Cc[searchSessionContractID].createInstance(Ci.nsIMsgSearchSession); + + // initialize a flag for phonetic name search + gSearchPhoneticName = + GetLocalizedStringPref("mail.addr_book.show_phonetic_fields"); + + if (window.arguments && window.arguments[0]) + SelectDirectory(window.arguments[0].directory); + else + SelectDirectory(document.getElementById("abPopup-menupopup") + .firstChild.value); + + // initialize globals, see abCommon.js, InitCommonJS() + abList = document.getElementById("abPopup"); + + onMore(null); +} + +function searchOnUnload() +{ + CloseAbView(); +} + +function initializeSearchWindowWidgets() +{ + gSearchStopButton = document.getElementById("search-button"); + gPropertiesButton = document.getElementById("propertiesButton"); + gComposeButton = document.getElementById("composeButton"); + gStatusText = document.getElementById('statusText'); + // matchAll doesn't make sense for address book search + hideMatchAllItem(); +} + +function onSearchStop() +{ +} + +function onAbSearchReset(event) +{ + gPropertiesButton.setAttribute("disabled","true"); + gComposeButton.setAttribute("disabled","true"); + + CloseAbView(); + + onReset(event); + gStatusText.setAttribute("label", ""); +} + +function SelectDirectory(aURI) +{ + var selectedAB = aURI; + + if (!selectedAB) + selectedAB = kPersonalAddressbookURI; + + // set popup with address book names + var abPopup = document.getElementById('abPopup'); + if ( abPopup ) + abPopup.value = selectedAB; + + setSearchScope(GetScopeForDirectoryURI(selectedAB)); +} + +function GetScopeForDirectoryURI(aURI) +{ + var directory = MailServices.ab.getDirectory(aURI); + var booleanAnd = gSearchBooleanRadiogroup.selectedItem.value == "and"; + + if (directory.isRemote) { + if (booleanAnd) + return nsMsgSearchScope.LDAPAnd; + else + return nsMsgSearchScope.LDAP; + } + else { + if (booleanAnd) + return nsMsgSearchScope.LocalABAnd; + else + return nsMsgSearchScope.LocalAB; + } +} + +function onEnterInSearchTerm() +{ + // on enter + // if not searching, start the search + // if searching, stop and then start again + if (gSearchStopButton.getAttribute("label") == gSearchBundle.getString("labelForSearchButton")) { + onSearch(); + } + else { + onSearchStop(); + onSearch(); + } +} + +function onSearch() +{ + gStatusText.setAttribute("label", ""); + gPropertiesButton.setAttribute("disabled","true"); + gComposeButton.setAttribute("disabled","true"); + + gSearchSession.clearScopes(); + + var currentAbURI = document.getElementById('abPopup').getAttribute('value'); + + gSearchSession.addDirectoryScopeTerm(GetScopeForDirectoryURI(currentAbURI)); + gSearchSession.searchTerms = saveSearchTerms(gSearchSession.searchTerms, gSearchSession); + + var searchUri = currentAbURI + "?("; + + for (let i = 0; i < gSearchSession.searchTerms.length; i++) { + let searchTerm = gSearchSession.searchTerms[i]; + + // get the "and" / "or" value from the first term + if (i == 0) { + if (searchTerm.booleanAnd) + searchUri += "and"; + else + searchUri += "or"; + } + + var attrs; + + switch (searchTerm.attrib) { + case nsMsgSearchAttrib.Name: + if (gSearchPhoneticName != "true") + attrs = ["DisplayName","FirstName","LastName","NickName"]; + else + attrs = ["DisplayName","FirstName","LastName","NickName","PhoneticFirstName","PhoneticLastName"]; + break; + case nsMsgSearchAttrib.DisplayName: + attrs = ["DisplayName"]; + break; + case nsMsgSearchAttrib.Email: + attrs = ["PrimaryEmail"]; + break; + case nsMsgSearchAttrib.PhoneNumber: + attrs = ["HomePhone","WorkPhone","FaxNumber","PagerNumber","CellularNumber"]; + break; + case nsMsgSearchAttrib.Organization: + attrs = ["Company"]; + break; + case nsMsgSearchAttrib.Department: + attrs = ["Department"]; + break; + case nsMsgSearchAttrib.City: + attrs = ["WorkCity"]; + break; + case nsMsgSearchAttrib.Street: + attrs = ["WorkAddress"]; + break; + case nsMsgSearchAttrib.Nickname: + attrs = ["NickName"]; + break; + case nsMsgSearchAttrib.WorkPhone: + attrs = ["WorkPhone"]; + break; + case nsMsgSearchAttrib.HomePhone: + attrs = ["HomePhone"]; + break; + case nsMsgSearchAttrib.Fax: + attrs = ["FaxNumber"]; + break; + case nsMsgSearchAttrib.Pager: + attrs = ["PagerNumber"]; + break; + case nsMsgSearchAttrib.Mobile: + attrs = ["CellularNumber"]; + break; + case nsMsgSearchAttrib.Title: + attrs = ["JobTitle"]; + break; + case nsMsgSearchAttrib.AdditionalEmail: + attrs = ["SecondEmail"]; + break; + default: + dump("XXX " + searchTerm.attrib + " not a supported search attr!\n"); + attrs = ["DisplayName"]; + break; + } + + var opStr; + + switch (searchTerm.op) { + case nsMsgSearchOp.Contains: + opStr = "c"; + break; + case nsMsgSearchOp.DoesntContain: + opStr = "!c"; + break; + case nsMsgSearchOp.Is: + opStr = "="; + break; + case nsMsgSearchOp.Isnt: + opStr = "!="; + break; + case nsMsgSearchOp.BeginsWith: + opStr = "bw"; + break; + case nsMsgSearchOp.EndsWith: + opStr = "ew"; + break; + case nsMsgSearchOp.SoundsLike: + opStr = "~="; + break; + default: + opStr = "c"; + break; + } + + // currently, we can't do "and" and "or" searches at the same time + // (it's either all "and"s or all "or"s) + var max_attrs = attrs.length; + + for (var j=0;j<max_attrs;j++) { + // append the term(s) to the searchUri + searchUri += "(" + attrs[j] + "," + opStr + "," + encodeABTermValue(searchTerm.value.str) + ")"; + } + } + + searchUri += ")"; + SetAbView(searchUri); +} + +// used to toggle functionality for Search/Stop button. +function onSearchButton(event) +{ + if (event.target.label == gSearchBundle.getString("labelForSearchButton")) + onSearch(); + else + onSearchStop(); +} + +function GetAbViewListener() +{ + return gSearchAbViewListener; +} + +function onProperties() +{ + AbEditSelectedCard(); +} + +function onCompose() +{ + AbNewMessage(); +} + +function AbResultsPaneDoubleClick(card) +{ + AbEditCard(card); +} + +function UpdateCardView() +{ + var numSelected = GetNumSelectedCards(); + + if (!numSelected) { + gPropertiesButton.setAttribute("disabled","true"); + gComposeButton.setAttribute("disabled","true"); + return; + } + + gComposeButton.removeAttribute("disabled"); + + if (numSelected == 1) + gPropertiesButton.removeAttribute("disabled"); + else + gPropertiesButton.setAttribute("disabled","true"); +} diff --git a/comm/suite/mailnews/content/ABSearchDialog.xul b/comm/suite/mailnews/content/ABSearchDialog.xul new file mode 100644 index 0000000000..a32c7dbb8b --- /dev/null +++ b/comm/suite/mailnews/content/ABSearchDialog.xul @@ -0,0 +1,99 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<?xml-stylesheet href="chrome://messenger/skin/searchDialog.css" type="text/css"?> + +<?xul-overlay href="chrome://messenger/content/addressbook/abResultsPaneOverlay.xul"?> +<?xul-overlay href="chrome://messenger/content/searchTermOverlay.xul"?> +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> + +<!DOCTYPE dialog [ +<!ENTITY % abResultsPaneOverlayDTD SYSTEM "chrome://messenger/locale/addressbook +/abResultsPaneOverlay.dtd"> +%abResultsPaneOverlayDTD; +<!ENTITY % SearchDialogDTD SYSTEM "chrome://messenger/locale/SearchDialog.dtd"> +%SearchDialogDTD; +]> + +<dialog id="searchAddressBookWindow" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:nc="http://home.netscape.com/NC-rdf#" + windowtype="mailnews:absearch" + title="&abSearchDialogTitle.label;" + style="width: 52em; height: 34em;" + persist="screenX screenY width height sizemode" + buttons="help" + ondialoghelp="return openHelp('mail_advanced_ab_search');" + onload="searchOnLoad();" + onunload="onSearchStop(); searchOnUnload();"> + + <stringbundleset id="stringbundleset"> + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> + <stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/> + <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/> + <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/> + </stringbundleset> + + <script src="chrome://messenger/content/mailWindow.js"/> + <script src="chrome://messenger/content/msgMail3PaneWindow.js"/> + <script src="chrome://global/content/globalOverlay.js"/> + <script src="chrome://messenger/content/commandglue.js"/> + <script src="chrome://messenger/content/ABSearchDialog.js"/> + <script src="chrome://messenger/content/addressbook/abCommon.js"/> + + <broadcaster id="Communicator:WorkMode"/> + + <dummy class="usesMailWidgets"/> + + <vbox id="searchTerms" flex="3" persist="height"> + <vbox> + <hbox align="center"> + <label value="&abSearchHeading.label;" accesskey="&abSearchHeading.accesskey;" control="abPopup"/> + <menulist id="abPopup" oncommand="SelectDirectory(this.value);"> + <menupopup id="abPopup-menupopup" class="addrbooksPopup"/> + </menulist> + <spacer flex="10"/> + <button id="search-button" oncommand="onSearchButton(event);" default="true"/> + </hbox> + <hbox align="center"> + <spacer flex="1"/> + <button label="&resetButton.label;" oncommand="onAbSearchReset(event);" accesskey="&resetButton.accesskey;"/> + </hbox> + </vbox> + + <hbox flex="1"> + <vbox id="searchTermListBox" flex="1"/> + </hbox> + </vbox> + + <splitter id="gray_horizontal_splitter" collapse="after" persist="state"> + <grippy/> + </splitter> + + <vbox id="searchResults" flex="4" persist="height"> + <vbox id="searchResultListBox" flex="1"> + <tree id="abResultsTree" flex="1" context="threadPaneContext"/> + </vbox> + <hbox align="center"> + <button id="propertiesButton" + label="&propertiesButton.label;" + accesskey="&propertiesButton.accesskey;" + disabled="true" + oncommand="onProperties();"/> + <button id="composeButton" + label="&composeButton.label;" + accesskey="&composeButton.accesskey;" + disabled="true" + oncommand="onCompose();"/> + <spacer flex="1"/> + <button dlgtype="help" class="dialog-button"/> + </hbox> + </vbox> + + <statusbar class="chromeclass-status" id="status-bar"> + <statusbarpanel id="statusText" crop="right" flex="1"/> + <statusbarpanel id="offline-status" class="statusbarpanel-iconic"/> + </statusbar> + +</dialog> diff --git a/comm/suite/mailnews/content/FilterListDialog.js b/comm/suite/mailnews/content/FilterListDialog.js new file mode 100644 index 0000000000..555796fc5e --- /dev/null +++ b/comm/suite/mailnews/content/FilterListDialog.js @@ -0,0 +1,1037 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { PluralForm } = ChromeUtils.import("resource://gre/modules/PluralForm.jsm"); +var {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const {MailServices} = ChromeUtils.import("resource:///modules/MailServices.jsm"); + +var gEditButton; +var gDeleteButton; +var gNewButton; +var gCopyToNewButton; +var gTopButton; +var gUpButton; +var gDownButton; +var gBottomButton; +var gSearchBox; +var gRunFiltersFolderPrefix; +var gRunFiltersFolder; +var gRunFiltersButton; +var gFilterBundle; +var gFilterListMsgWindow = null; +var gFilterListbox; +var gCurrentFilterList; +var gStatusBar; +var gStatusText; +var gServerMenu; + +var msgMoveMotion = { + Up : 0, + Down : 1, + Top : 2, + Bottom : 3, +} + +var gStatusFeedback = { + showStatusString: function(status) + { + gStatusText.setAttribute("value", status); + }, + startMeteors: function() + { + // change run button to be a stop button + gRunFiltersButton.setAttribute("label", gRunFiltersButton.getAttribute("stoplabel")); + gRunFiltersButton.setAttribute("accesskey", gRunFiltersButton.getAttribute("stopaccesskey")); + gStatusBar.setAttribute("mode", "undetermined"); + }, + stopMeteors: function() + { + try { + // change run button to be a stop button + gRunFiltersButton.setAttribute("label", gRunFiltersButton.getAttribute("runlabel")); + gRunFiltersButton.setAttribute("accesskey", gRunFiltersButton.getAttribute("runaccesskey")); + gStatusBar.setAttribute("mode", "normal"); + } + catch (ex) { + // can get here if closing window when running filters + } + }, + showProgress: function(percentage) + { + }, + closeWindow: function() + { + } +}; + +function onLoad() +{ + setHelpFileURI("chrome://communicator/locale/help/suitehelp.rdf"); + gFilterListMsgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance(Ci.nsIMsgWindow); + gFilterListMsgWindow.domWindow = window; + gFilterListMsgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL; + gFilterListMsgWindow.statusFeedback = gStatusFeedback; + + gFilterBundle = document.getElementById("bundle_filter"); + + gServerMenu = document.getElementById("serverMenu"); + gFilterListbox = document.getElementById("filterList"); + + gEditButton = document.getElementById("editButton"); + gDeleteButton = document.getElementById("deleteButton"); + gNewButton = document.getElementById("newButton"); + gCopyToNewButton = document.getElementById("copyToNewButton"); + gTopButton = document.getElementById("reorderTopButton"); + gUpButton = document.getElementById("reorderUpButton"); + gDownButton = document.getElementById("reorderDownButton"); + gBottomButton = document.getElementById("reorderBottomButton"); + gSearchBox = document.getElementById("searchBox"); + gRunFiltersFolderPrefix = document.getElementById("folderPickerPrefix"); + gRunFiltersFolder = document.getElementById("runFiltersFolder"); + gRunFiltersButton = document.getElementById("runFiltersButton"); + gStatusBar = document.getElementById("statusbar-icon"); + gStatusText = document.getElementById("statusText"); + + updateButtons(); + + processWindowArguments(window.arguments[0]); + + Services.obs.addObserver(onFilterClose, + "quit-application-requested"); + + top.controllers.insertControllerAt(0, gFilterController); +} + +/** + * Processes arguments sent to this dialog when opened or refreshed. + * + * @param aArguments An object having members representing the arguments. + * { arg1: value1, arg2: value2, ... } + */ +function processWindowArguments(aArguments) { + let wantedFolder; + if ("folder" in aArguments) + wantedFolder = aArguments.folder; + + // If a specific folder was requested, try to select it + // if we don't already show its server. + if (!gServerMenu._folder || + (wantedFolder && (wantedFolder != gServerMenu._folder) && + (wantedFolder.rootFolder != gServerMenu._folder))) { + + // Get the folder where filters should be defined, if that server + // can accept filters. + let firstItem = getFilterFolderForSelection(wantedFolder); + + // if the selected server cannot have filters, get the default server + // if the default server cannot have filters, check all accounts + // and get a server that can have filters. + if (!firstItem) { + var server = getServerThatCanHaveFilters(); + if (server) + firstItem = server.rootFolder; + } + + if (firstItem) + setFilterFolder(firstItem); + + if (wantedFolder) + setRunFolder(wantedFolder); + } else { + // If we didn't change folder still redraw the list + // to show potential new filters if we were called for refresh. + rebuildFilterList(); + } + + // If a specific filter was requested, try to select it. + if ("filter" in aArguments) + selectFilter(aArguments.filter); +} + +/** + * This is called from OpenOrFocusWindow() if the dialog is already open. + * New filters could have been created by operations outside the dialog. + * + * @param aArguments An object of arguments having the same format + * as window.arguments[0]. + */ +function refresh(aArguments) { + // As we really don't know what has changed, clear the search box + // unconditionally so that the changed/added filters are surely visible. + resetSearchBox(); + + processWindowArguments(aArguments); +} + +function CanRunFiltersAfterTheFact(aServer) +{ + // filter after the fact is implement using search + // so if you can't search, you can't filter after the fact + return aServer.canSearchMessages; +} + +/** + * Change the root server for which we are managing filters. + * + * @param msgFolder The nsIMsgFolder server containing filters + * (or a folder for NNTP server). + */ +function setFilterFolder(msgFolder) { + if (!msgFolder || msgFolder == gServerMenu._folder) + return; + + // Save the current filters to disk before switching because + // the dialog may be closed and we'll lose current filters. + if (gCurrentFilterList) + gCurrentFilterList.saveToDefaultFile(); + + // Setting this attribute should go away in bug 473009. + gServerMenu._folder = msgFolder; + // Calling this should go away in bug 802609. + gServerMenu.menupopup.selectFolder(msgFolder); + + // Calling getEditableFilterList will detect any errors in + // msgFilterRules.dat, backup the file, and alert the user. + gCurrentFilterList = msgFolder.getEditableFilterList(gFilterListMsgWindow); + rebuildFilterList(); + + // Select the first item in the list, if there is one. + if (gFilterListbox.itemCount > 0) + gFilterListbox.selectItem(gFilterListbox.getItemAtIndex(0)); + + // This will get the deferred to account root folder, if server is deferred. + // We intentionally do this after setting the current server, as we want + // that to refer to the rootFolder for the actual server, not the + // deferred-to server, as current server is really a proxy for the + // server whose filters we are editing. But below here we are managing + // where the filters will get applied, which is on the deferred-to server. + msgFolder = msgFolder.server.rootMsgFolder; + + // root the folder picker to this server + let runMenu = gRunFiltersFolder.menupopup; + runMenu._teardown(); + runMenu._parentFolder = msgFolder; + runMenu._ensureInitialized(); + + var canFilterAfterTheFact = CanRunFiltersAfterTheFact(msgFolder.server); + gRunFiltersButton.hidden = !canFilterAfterTheFact; + gRunFiltersFolder.hidden = !canFilterAfterTheFact; + gRunFiltersFolderPrefix.hidden = !canFilterAfterTheFact; + + if (canFilterAfterTheFact) { + let wantedFolder = null; + // For a given server folder, get the default run target folder or show + // "Choose Folder". + if (!msgFolder.isServer) { + wantedFolder = msgFolder; + } else { + try { + switch (msgFolder.server.type) { + case "nntp": + // For NNTP select the subscribed newsgroup. + wantedFolder = gServerMenu._folder; + break; + case "rss": + // Show "Choose Folder" for feeds. + wantedFolder = null; + break; + case "imap": + case "pop3": + case "none": + // Find Inbox for IMAP and POP or Local Folders, + // show "Choose Folder" if not found. + wantedFolder = msgFolder.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox); + break; + default: + // For other account types we don't know what's good to select, + // so show "Choose Folder". + wantedFolder = null; + } + } catch (e) { + Cu.reportError("Failed to select a suitable folder to run filters on: " + e); + wantedFolder = null; + } + } + // Select a useful first folder for the server. + setRunFolder(wantedFolder); + } +} + +/** + * Select a folder on which filters are to be run. + * + * @param aFolder nsIMsgFolder folder to select. + */ +function setRunFolder(aFolder) { + // Setting this attribute should go away in bug 473009. + gRunFiltersFolder._folder = aFolder; + // Calling this should go away in bug 802609. + gRunFiltersFolder.menupopup.selectFolder(gRunFiltersFolder._folder); + updateButtons(); +} + +/** + * Toggle enabled state of a filter, in both the filter properties and the UI. + * + * @param aFilterItem an item (row) of the filter list to be toggled + */ +function toggleFilter(aFilterItem) +{ + let filter = aFilterItem._filter; + if (filter.unparseable && !filter.enabled) + { + Services.prompt.alert(window, null, + gFilterBundle.getFormattedString("cannotEnableIncompatFilter", + [document.getElementById("bundle_brand").getString("brandShortName")])); + return; + } + filter.enabled = !filter.enabled; + + // Now update the checkbox + aFilterItem.childNodes[1].setAttribute("enabled", filter.enabled); + // For accessibility set the checked state on listitem + aFilterItem.setAttribute("aria-checked", filter.enabled); +} + +/** + * Selects a specific filter in the filter list. + * The listbox view is scrolled to the corresponding item. + * + * @param aFilter The nsIMsgFilter to select. + * + * @return true/false indicating whether the filter was found and selected. + */ +function selectFilter(aFilter) { + if (currentFilter() == aFilter) + return true; + + resetSearchBox(aFilter); + + let filterCount = gCurrentFilterList.filterCount; + for (let i = 0; i < filterCount; i++) { + if (gCurrentFilterList.getFilterAt(i) == aFilter) { + gFilterListbox.ensureIndexIsVisible(i); + gFilterListbox.selectedIndex = i; + return true; + } + } + return false; +} + +/** + * Returns the currently selected filter. If multiple filters are selected, + * returns the first one. If none are selected, returns null. + */ +function currentFilter() +{ + let currentItem = gFilterListbox.selectedItem; + return currentItem ? currentItem._filter : null; +} + +function onEditFilter() +{ + if (gEditButton.disabled) + return; + + var selectedFilter = currentFilter(); + if (!selectedFilter) + return; + + let args = {filter: selectedFilter, filterList: gCurrentFilterList}; + + window.openDialog("chrome://messenger/content/FilterEditor.xul", "FilterEditor", "chrome,modal,titlebar,resizable,centerscreen", args); + + if ("refresh" in args && args.refresh) { + // Reset search if edit was okay (name change might lead to hidden entry). + resetSearchBox(selectedFilter); + rebuildFilterList(); + } +} + +/** + * Handler function for the 'New...' buttons. + * Opens the filter dialog for creating a new filter. + */ +function onNewFilter() { + calculatePositionAndShowCreateFilterDialog({}); +} + +/** + * Handler function for the 'Copy...' button. + * Opens the filter dialog for copying the selected filter. + */ +function onCopyToNewFilter() { + if (gCopyToNewButton.disabled) + return; + + let selectedFilter = currentFilter(); + if (!selectedFilter) + return; + + calculatePositionAndShowCreateFilterDialog({copiedFilter: selectedFilter}); +} + +/** + * Calculates the position for inserting the new filter, + * and then displays the create dialog. + * + * @param args The object containing the arguments for the dialog, + * passed to the filterEditorOnLoad() function. + * It will be augmented with the insertion position + * and global filters list properties by this function. + */ +function calculatePositionAndShowCreateFilterDialog(args) { + let selectedFilter = currentFilter(); + // If no filter is selected use the first position. + let position = 0; + if (selectedFilter) { + // Get the position in the unfiltered list. + // - this is where the new filter should be inserted! + let filterCount = gCurrentFilterList.filterCount; + for (let i = 0; i < filterCount; i++) { + if (gCurrentFilterList.getFilterAt(i) == selectedFilter) { + position = i; + break; + } + } + } + args.filterPosition = position; + args.filterList = gCurrentFilterList; + args.refresh = false; + + window.openDialog("chrome://messenger/content/FilterEditor.xul", + "FilterEditor", + "chrome,modal,titlebar,resizable,centerscreen", args); + + if (args.refresh) + { + // On success: reset the search box if necessary! + resetSearchBox(args.newFilter); + rebuildFilterList(); + + // Select the new filter, it is at the position of previous selection. + gFilterListbox.selectItem(gFilterListbox.getItemAtIndex(position)); + if (currentFilter() != args.newFilter) + Cu.reportError("Filter created at an unexpected position!"); + } +} + +function onDeleteFilter() +{ + if (gDeleteButton.disabled) + return; + + let items = gFilterListbox.selectedItems; + if (!items.length) + return; + + let checkValue = {value: false}; + if (Services.prefs.getBoolPref("mailnews.filters.confirm_delete") && + Services.prompt.confirmEx(window, null, + gFilterBundle.getString("deleteFilterConfirmation"), + Services.prompt.STD_YES_NO_BUTTONS, + '', '', '', + gFilterBundle.getString('dontWarnAboutDeleteCheckbox'), + checkValue)) + return; + + if (checkValue.value) + Services.prefs.setBoolPref("mailnews.filters.confirm_delete", false); + + // Save filter position before the first selected one. + let newSelectionIndex = gFilterListbox.selectedIndex - 1; + + // Must reverse the loop, as the items list shrinks when we delete. + for (let index = items.length - 1; index >= 0; --index) { + let item = items[index]; + gCurrentFilterList.removeFilter(item._filter); + item.remove(); + } + updateCountBox(); + + // Select filter above previously selected if one existed, + // otherwise the first one. + if (newSelectionIndex == -1 && gFilterListbox.itemCount > 0) + newSelectionIndex = 0; + if (newSelectionIndex > -1) { + gFilterListbox.selectedIndex = newSelectionIndex; + updateViewPosition(-1); + } +} + +/** + * Move filter one step up in visible list. + */ +function onUp(event) { + moveFilter(msgMoveMotion.Up); +} + +/** + * Move filter one step down in visible list. + */ +function onDown(event) { + moveFilter(msgMoveMotion.Down); +} + +/** + * Move filter to bottom for long filter lists. + */ +function onTop(event) { + moveFilter(msgMoveMotion.Top); +} + +/** + * Move filter to top for long filter lists. + */ +function onBottom(event) { + moveFilter(msgMoveMotion.Bottom); +} + +/** + * Moves a singular selected filter up or down either 1 increment or to the + * top/bottom. + * + * @param motion + * msgMoveMotion.Up, msgMoveMotion.Down, msgMoveMotion.Top, msgMoveMotion.Bottom + */ +function moveFilter(motion) { + // At the moment, do not allow moving groups of filters. + let selectedFilter = currentFilter(); + if (!selectedFilter) + return; + + let relativeStep = 0; + let moveFilterNative; + + switch (motion) { + case msgMoveMotion.Top: + if (selectedFilter) { + gCurrentFilterList.removeFilter(selectedFilter); + gCurrentFilterList.insertFilterAt(0, selectedFilter); + rebuildFilterList(); + } + return; + case msgMoveMotion.Bottom: + if (selectedFilter) { + gCurrentFilterList.removeFilter(selectedFilter); + gCurrentFilterList.insertFilterAt(gCurrentFilterList.filterCount, + selectedFilter); + rebuildFilterList(); + } + return; + case msgMoveMotion.Up: + relativeStep = -1; + moveFilterNative = Ci.nsMsgFilterMotion.up; + break; + case msgMoveMotion.Down: + relativeStep = +1; + moveFilterNative = Ci.nsMsgFilterMotion.down; + break; + } + + if (!gSearchBox.value) { + // Use legacy move filter code: up, down; only if searchBox is empty. + moveCurrentFilter(moveFilterNative); + return; + } + + let nextIndex = gFilterListbox.selectedIndex + relativeStep; + let nextFilter = gFilterListbox.getItemAtIndex(nextIndex)._filter; + + gCurrentFilterList.removeFilter(selectedFilter); + + // Find the index of the filter we want to insert at. + let newIndex = -1; + let filterCount = gCurrentFilterList.filterCount; + for (let i = 0; i < filterCount; i++) { + if (gCurrentFilterList.getFilterAt(i) == nextFilter) { + newIndex = i; + break; + } + } + + if (motion == msgMoveMotion.Down) + newIndex += relativeStep; + + gCurrentFilterList.insertFilterAt(newIndex, selectedFilter); + + rebuildFilterList(); +} + +function viewLog() +{ + let args = {filterList: gCurrentFilterList}; + + window.openDialog("chrome://messenger/content/viewLog.xul", "FilterLog", "chrome,modal,titlebar,resizable,centerscreen", args); +} + +function onFilterUnload() +{ + // make sure to save the filter to disk + if (gCurrentFilterList) + gCurrentFilterList.saveToDefaultFile(); + + Services.obs.removeObserver(onFilterClose, "quit-application-requested"); + top.controllers.removeController(gFilterController); +} + +function onFilterClose(aCancelQuit, aTopic, aData) +{ + if (aTopic == "quit-application-requested" && + aCancelQuit instanceof Ci.nsISupportsPRBool && + aCancelQuit.data) + return false; + + if (gRunFiltersButton.getAttribute("label") == gRunFiltersButton.getAttribute("stoplabel")) { + var promptTitle = gFilterBundle.getString("promptTitle"); + var promptMsg = gFilterBundle.getString("promptMsg");; + var stopButtonLabel = gFilterBundle.getString("stopButtonLabel"); + var continueButtonLabel = gFilterBundle.getString("continueButtonLabel"); + + if (Services.prompt.confirmEx(window, promptTitle, promptMsg, + (Services.prompt.BUTTON_TITLE_IS_STRING * + Services.prompt.BUTTON_POS_0) + + (Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1), + continueButtonLabel, stopButtonLabel, null, null, {value:0}) == 0) { + if (aTopic == "quit-application-requested") + aCancelQuit.data = true; + return false; + } + gFilterListMsgWindow.StopUrls(); + } + + return true; +} + +function runSelectedFilters() +{ + // if run button has "stop" label, do stop. + if (gRunFiltersButton.getAttribute("label") == gRunFiltersButton.getAttribute("stoplabel")) { + gFilterListMsgWindow.StopUrls(); + return; + } + + let folder = gRunFiltersFolder._folder || + gRunFiltersFolder.selectedItem._folder; + if (!folder) + return; + + let filterList = MailServices.filters.getTempFilterList(folder); + + // make sure the tmp filter list uses the real filter list log stream + filterList.logStream = gCurrentFilterList.logStream; + filterList.loggingEnabled = gCurrentFilterList.loggingEnabled; + + let index = 0; + for (let item of gFilterListbox.selectedItems) { + filterList.insertFilterAt(index++, item._filter); + } + + MailServices.filters.applyFiltersToFolders(filterList, [folder], gFilterListMsgWindow); +} + +function moveCurrentFilter(motion) +{ + let filter = currentFilter(); + if (!filter) + return; + + gCurrentFilterList.moveFilter(filter, motion); + rebuildFilterList(); +} + +/** + * Redraws the list of filters. Takes the search box value into account. + * + * This function should perform very fast even in case of high number of filters. + * Therefore there are some optimizations (e.g. listelement.children[] instead of + * list.getItemAtIndex()), that favour speed vs. semantical perfection. + */ +function rebuildFilterList() +{ + // Get filters that match the search box. + let aTempFilterList = onFindFilter(); + + let searchBoxFocus = false; + let activeElement = document.activeElement; + + // Find if the currently focused element is a child inside the search box + // (probably html:input). Traverse up the parents until the first element + // with an ID is found. If it is not searchBox, return false. + while (activeElement != null) { + if (activeElement == gSearchBox) { + searchBoxFocus = true; + break; + } + else if (activeElement.id) { + searchBoxFocus = false; + break; + } + activeElement = activeElement.parentNode; + } + + // Make a note of which filters were previously selected + let selectedNames = []; + for (let i = 0; i < gFilterListbox.selectedItems.length; i++) + selectedNames.push(gFilterListbox.selectedItems[i]._filter.filterName); + + // Save scroll position so we can try to restore it later. + // Doesn't work when the list is rebuilt after search box condition changed. + let firstVisibleRowIndex = gFilterListbox.getIndexOfFirstVisibleRow(); + + // listbox.xml seems to cache the value of the first selected item in a + // range at _selectionStart. The old value though is now obsolete, + // since we will recreate all of the elements. We need to clear this, + // and one way to do this is with a call to clearSelection. This might be + // ugly from an accessibility perspective, since it fires an onSelect event. + gFilterListbox.clearSelection(); + + let listitem, nameCell, enabledCell, filter; + let filterCount = gCurrentFilterList.filterCount; + let listitemCount = gFilterListbox.itemCount; + let listitemIndex = 0; + let tempFilterListLength = aTempFilterList ? aTempFilterList.length - 1 : 0; + for (let i = 0; i < filterCount; i++) { + if (aTempFilterList && listitemIndex > tempFilterListLength) + break; + + filter = gCurrentFilterList.getFilterAt(i); + if (aTempFilterList && aTempFilterList[listitemIndex] != i) + continue; + + if (listitemCount > listitemIndex) { + // If there is a free existing listitem, reuse it. + // Use .children[] instead of .getItemAtIndex() as it is much faster. + listitem = gFilterListbox.children[listitemIndex + 1]; + nameCell = listitem.childNodes[0]; + enabledCell = listitem.childNodes[1]; + } + else + { + // If there are not enough listitems in the list, create a new one. + listitem = document.createElement("listitem"); + listitem.setAttribute("role", "checkbox"); + nameCell = document.createElement("listcell"); + enabledCell = document.createElement("listcell"); + enabledCell.setAttribute("class", "listcell-iconic"); + listitem.appendChild(nameCell); + listitem.appendChild(enabledCell); + gFilterListbox.appendChild(listitem); + let size = (enabledCell.clientWidth - 28) / 2; + enabledCell.style.paddingLeft = size.toString() + "px"; + // We have to attach this listener to the listitem, even though we only + // care about clicks on the enabledCell. However, attaching to that item + // doesn't result in any events actually getting received. + listitem.addEventListener("click", onFilterClick, true); + listitem.addEventListener("dblclick", onFilterDoubleClick, true); + } + // For accessibility set the label on listitem. + listitem.setAttribute("label", filter.filterName); + // Set the listitem values to represent the current filter. + nameCell.setAttribute("label", filter.filterName); + enabledCell.setAttribute("enabled", filter.enabled); + listitem.setAttribute("aria-checked", filter.enabled); + listitem._filter = filter; + + if (selectedNames.includes(filter.filterName)) + gFilterListbox.addItemToSelection(listitem); + + listitemIndex++; + } + // Remove any superfluous listitems, if the number of filters shrunk. + for (let i = listitemCount - 1; i >= listitemIndex; i--) { + gFilterListbox.lastChild.remove(); + } + + updateViewPosition(firstVisibleRowIndex); + updateCountBox(); + + // If before rebuilding the list the searchbox was focused, focus it again. + // In any other case, focus the list. + if (searchBoxFocus) + gSearchBox.focus(); + else + gFilterListbox.focus(); +} + +function updateViewPosition(firstVisibleRowIndex) +{ + if (firstVisibleRowIndex == -1) + firstVisibleRowIndex = gFilterListbox.getIndexOfFirstVisibleRow(); + + // Restore to the extent possible the scroll position. + if (firstVisibleRowIndex && gFilterListbox.itemCount) + gFilterListbox.scrollToIndex(Math.min(firstVisibleRowIndex, + gFilterListbox.itemCount - 1)); + + if (gFilterListbox.selectedCount) { + // Make sure that at least the first selected item is visible. + gFilterListbox.ensureElementIsVisible(gFilterListbox.selectedItems[0]); + + // The current item should be the first selected item, so that keyboard + // selection extension can work. + gFilterListbox.currentItem = gFilterListbox.selectedItems[0]; + } + + updateButtons(); +} + +/** + * Try to only enable buttons that make sense + * - moving filters is currently only enabled for single selection + * also movement is restricted by searchBox and current selection position + * - edit only for single filters + * - delete / run only for one or more selected filters + */ +function updateButtons() +{ + var numFiltersSelected = gFilterListbox.selectedItems.length; + var oneFilterSelected = (numFiltersSelected == 1); + + // "edit" only enabled when one filter selected + // or if we couldn't parse the filter. + let disabled = !oneFilterSelected || currentFilter().unparseable; + gEditButton.disabled = disabled; + + // "copy" is the same as "edit". + gCopyToNewButton.disabled = disabled; + + // "delete" only disabled when no filters are selected + gDeleteButton.disabled = !numFiltersSelected; + + // we can run multiple filters on a folder + // so only disable this UI if no filters are selected + gRunFiltersFolderPrefix.disabled = !numFiltersSelected; + gRunFiltersFolder.disabled = !numFiltersSelected; + gRunFiltersButton.disabled = !numFiltersSelected || + !gRunFiltersFolder._folder; + + // "up" and "top" enabled only if one filter is selected, + // and it's not the first. + // Don't use gFilterListbox.currentIndex here, it's buggy when we've just + // changed the children in the list (via rebuildFilterList) + disabled = !(oneFilterSelected && + gFilterListbox.getSelectedItem(0) != gFilterListbox.getItemAtIndex(0)); + gUpButton.disabled = disabled; + gTopButton.disabled = disabled; + + // "down" and "bottom" enabled only if one filter selected, + // and it's not the last. + disabled = !(oneFilterSelected && + gFilterListbox.selectedIndex < gFilterListbox.itemCount - 1); + gDownButton.disabled = disabled; + gBottomButton.disabled = disabled; +} + +/** + * Given a selected folder, returns the folder where filters should + * be defined (the root folder except for news) if the server can + * accept filters. + * + * @param nsIMsgFolder aFolder - selected folder, from window args + * @returns an nsIMsgFolder where the filter is defined + */ +function getFilterFolderForSelection(aFolder) { + if (!aFolder || !aFolder.server) + return null; + + let rootFolder = aFolder.server.rootFolder; + if (rootFolder && rootFolder.isServer && rootFolder.server.canHaveFilters) + return (aFolder.server.type == "nntp") ? aFolder : rootFolder; + + return null; +} + +/** + * If the selected server cannot have filters, get the default server. + * If the default server cannot have filters, check all accounts + * and get a server that can have filters. + * + * @returns an nsIMsgIncomingServer + */ +function getServerThatCanHaveFilters() +{ + let defaultAccount = MailServices.accounts.defaultAccount; + if (defaultAccount) { + let defaultIncomingServer = defaultAccount.incomingServer; + // Check to see if default server can have filters. + if (defaultIncomingServer.canHaveFilters) + return defaultIncomingServer; + } + + // if it cannot, check all accounts to find a server + // that can have filters + for (let currentServer of MailServices.accounts.allServers) + { + if (currentServer.canHaveFilters) + return currentServer; + } + + return null; +} + +function onFilterClick(event) +{ + // We only care about button 0 (left click) events. + if (event.button != 0) + return; + + // Remember, we had to attach the click-listener to the whole listitem, so + // now we need to see if the clicked the enable-column + let toggle = event.target.childNodes[1]; + if ((event.clientX < toggle.boxObject.x + toggle.boxObject.width) && + (event.clientX > toggle.boxObject.x)) { + toggleFilter(event.target); + event.stopPropagation(); + } +} + +function onFilterDoubleClick(event) +{ + // We only care about button 0 (left click) events. + if (event.button != 0) + return; + + onEditFilter(); +} + +function onFilterListKeyPress(aEvent) { + if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey || aEvent.shiftKey) + return; + + if (aEvent.keyCode) { + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_INSERT: + if (!gNewButton.disabled) + onNewFilter(); + break; + case KeyEvent.DOM_VK_DELETE: + if (!gDeleteButton.disabled) + onDeleteFilter(); + break; + case KeyEvent.DOM_VK_RETURN: + if (!gEditButton.disabled) + onEditFilter(); + break; + } + return; + } + + switch (aEvent.charCode) { + case KeyEvent.DOM_VK_SPACE: + for (let item of gFilterListbox.selectedItems) { + toggleFilter(item); + } + break; + default: + gSearchBox.focus(); + gSearchBox.value = String.fromCharCode(aEvent.charCode); + } +} + +/** + * Decides if the given filter matches the given keyword. + * + * @param aFilter nsIMsgFilter to check + * @param aKeyword the string to find in the filter name + * + * @return True if the filter name contains the searched keyword. + Otherwise false. In the future this may be extended to match + other filter attributes. + */ +function filterSearchMatch(aFilter, aKeyword) { + return (aFilter.filterName.toLocaleLowerCase().includes(aKeyword)) +} + +/** + * Called from rebuildFilterList when the list needs to be redrawn. + * @return Uses the search term in search box, to produce an array of + * row (filter) numbers (indexes) that match the search term. + */ +function onFindFilter() { + let keyWord = gSearchBox.value.toLocaleLowerCase(); + + // If searchbox is empty, just return and let rebuildFilterList + // create an unfiltered list. + if (!keyWord) + return null; + + // Rematch everything in the list, remove what doesn't match the search box. + let rows = gCurrentFilterList.filterCount; + let matchingFilterList = []; + // Use the full gCurrentFilterList, not the filterList listbox, + // which may already be filtered. + for (let i = 0; i < rows; i++) { + if (filterSearchMatch(gCurrentFilterList.getFilterAt(i), keyWord)) + matchingFilterList.push(i); + } + + return matchingFilterList; +} + +/** + * Clear the search term in the search box if needed. + * + * @param aFilter If this nsIMsgFilter matches the search term, + * do not reset the box. If this is null, + * reset unconditionally. + */ +function resetSearchBox(aFilter) { + let keyword = gSearchBox.value.toLocaleLowerCase(); + if (keyword && (!aFilter || !filterSearchMatch(aFilter, keyword))) + gSearchBox.reset(); +} + +/** + * Display "1 item", "11 items" or "4 of 10" if list is filtered via search box. + */ +function updateCountBox() { + let countBox = document.getElementById("countBox"); + let sum = gCurrentFilterList.filterCount; + let len = gFilterListbox.itemCount; + + if (len == sum) { + // "N items" + countBox.value = PluralForm.get(len, gFilterBundle.getString("filterCountItems")) + .replace("#1", len); + countBox.removeAttribute("filterActive"); + } else { + // "N of M" + countBox.value = gFilterBundle.getFormattedString("filterCountVisibleOfTotal", + [len, sum]); + if (len == 0 && sum > 0) + countBox.setAttribute("filterActive", "nomatches"); + else + countBox.setAttribute("filterActive", "matches"); + } +} + +function doHelpButton() +{ + openHelp("mail-filters"); +} + +var gFilterController = +{ + supportsCommand: function(aCommand) + { + return aCommand == "cmd_selectAll"; + }, + + isCommandEnabled: function(aCommand) + { + return aCommand == "cmd_selectAll"; + }, + + doCommand: function(aCommand) + { + if (aCommand == "cmd_selectAll") + gFilterListbox.selectAll(); + }, + + onEvent: function(aEvent) + { + } +}; diff --git a/comm/suite/mailnews/content/FilterListDialog.xul b/comm/suite/mailnews/content/FilterListDialog.xul new file mode 100644 index 0000000000..95f6d473ae --- /dev/null +++ b/comm/suite/mailnews/content/FilterListDialog.xul @@ -0,0 +1,197 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/filterDialog.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?> + +<?xul-overlay href="chrome://communicator/content/utilityOverlay.xul"?> + +<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/FilterListDialog.dtd"> + +<dialog id="filterListDialog" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + windowtype="mailnews:filterlist" + title="&filterListDialog.title;" + style="width: 45em; height: 31em;" + persist="width height screenX screenY" + buttons="help" + ondialoghelp="return openHelp('mail-filters');" + onload="onLoad();" + onunload="onFilterUnload();" + onclose="return onFilterClose();"> + + <script src="chrome://messenger/content/FilterListDialog.js"/> + + <stringbundle id="bundle_filter" + src="chrome://messenger/locale/filter.properties"/> + <stringbundle id="bundle_brand" + src="chrome://branding/locale/brand.properties"/> + + <keyset id="filterKeys"> + <key id="key_selectAll"/> + </keyset> + + <hbox align="center"> + <label value="&filtersForPrefix.label;" + accesskey="&filtersForPrefix.accesskey;" + control="serverMenu"/> + + <menulist id="serverMenu" + class="folderMenuItem" + IsServer="true" + IsSecure="false" + ServerType="none"> + <menupopup id="serverMenuPopup" + class="menulist-menupopup" + type="folder" + mode="filters" + expandFolders="nntp" + showFileHereLabel="true" + showAccountsFileHere="true" + oncommand="setFilterFolder(event.target._folder)"/> + </menulist> + <textbox id="searchBox" + class="searchBox" + flex="1" + type="search" + oncommand="rebuildFilterList();" + emptytext="&searchBox.emptyText;" + isempty="true"/> + </hbox> + + <grid flex="1"> + <columns> + <column flex="1"/> + <column/> + </columns> + <rows> + <row> + <separator class="thin"/> + </row> + + <row> + <hbox> + <label id="filterHeader" + flex="1" + control="filterTree">&filterHeader.label;</label> + <label id="countBox"/> + </hbox> + </row> + + <row flex="1"> + <vbox> + <listbox id="filterList" + flex="1" + seltype="multiple" + onselect="updateButtons();" + onkeypress="onFilterListKeyPress(event);"> + <listhead> + <listheader id="nameColumn" + label="&nameColumn.label;" + flex="1"/> + <listheader id="activeColumn" + label="&activeColumn.label;" + minwidth="40px"/> + </listhead> + </listbox> + </vbox> + + <vbox> + <button id="newButton" + label="&newButton.label;" + accesskey="&newButton.accesskey;" + oncommand="onNewFilter();"/> + <button id="copyToNewButton" + label="©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> diff --git a/comm/suite/mailnews/jar.mn b/comm/suite/mailnews/jar.mn new file mode 100644 index 0000000000..5529995b02 --- /dev/null +++ b/comm/suite/mailnews/jar.mn @@ -0,0 +1,70 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +messenger.jar: +% content messagebody %content/messagebody/ contentaccessible=yes +% content messenger %content/messenger/ +% override chrome://global/content/nsDragAndDrop.js chrome://messenger/content/nsDragAndDrop.js +# provide the nsTransferable in nsDragAndDrop.js to extensions that have to +# work with Geckos from before 1.9, when there was a separate file +% override chrome://global/content/nsTransferable.js chrome://messenger/content/nsDragAndDrop.js +% override chrome://messagebody/skin/messageBody.css chrome://messenger/skin/messageBody.css +% content messenger-region %content/messenger-region/ +% overlay chrome://communicator/content/pref/preferences.xul chrome://messenger/content/mailPrefsOverlay.xul +% overlay chrome://communicator/content/pref/pref-appearance.xul chrome://messenger/content/mailPrefsOverlay.xul +% overlay chrome://communicator/content/pref/pref-cookies.xul chrome://messenger/content/mailPrefsOverlay.xul +% overlay chrome://editor/content/editorTasksOverlay.xul chrome://messenger/content/mailTasksOverlay.xul +% overlay chrome://messenger/content/addressbook/abSelectAddressesDialog.xul chrome://messenger/content/mailOverlay.xul +% overlay chrome://editor/content/composerOverlay.xul chrome://messenger/content/mailEditorOverlay.xul + content/messenger/browserRequest.js (content/browserRequest.js) + content/messenger/browserRequest.xul (content/browserRequest.xul) + content/messenger/commandglue.js (content/commandglue.js) + content/messenger/folderDisplay.js (content/folderDisplay.js) + content/messenger/folderPane.js (content/folderPane.js) + content/messenger/folderPane.xul (content/folderPane.xul) + content/messenger/mail-offline.js (content/mail-offline.js) + content/messenger/mail3PaneWindowCommands.js (content/mail3PaneWindowCommands.js) + content/messenger/mailCommands.js (content/mailCommands.js) + content/messenger/mailContextMenus.js (content/mailContextMenus.js) + content/messenger/mailEditorOverlay.xul (content/mailEditorOverlay.xul) +* content/messenger/mailKeysOverlay.xul (content/mailKeysOverlay.xul) + content/messenger/mailOverlay.js (content/mailOverlay.js) +* content/messenger/mailOverlay.xul (content/mailOverlay.xul) + content/messenger/mailTasksOverlay.js (content/mailTasksOverlay.js) + content/messenger/mailTasksOverlay.xul (content/mailTasksOverlay.xul) + content/messenger/mailViewList.js (content/mailViewList.js) + content/messenger/mailViewList.xul (content/mailViewList.xul) + content/messenger/mailViewSetup.js (content/mailViewSetup.js) + content/messenger/mailViewSetup.xul (content/mailViewSetup.xul) + content/messenger/mailWidgets.xml (content/mailWidgets.xml) + content/messenger/mailWindow.js (content/mailWindow.js) + content/messenger/mailWindowOverlay.js (content/mailWindowOverlay.js) +* content/messenger/mailWindowOverlay.xul (content/mailWindowOverlay.xul) + content/messenger/messageWindow.js (content/messageWindow.js) + content/messenger/messageWindow.xul (content/messageWindow.xul) + content/messenger/messenger.css (content/messenger.css) + content/messenger/messenger.xul (content/messenger.xul) + content/messenger/msgFolderPickerOverlay.js (content/msgFolderPickerOverlay.js) + content/messenger/msgHdrViewOverlay.js (content/msgHdrViewOverlay.js) + content/messenger/msgHdrViewOverlay.xul (content/msgHdrViewOverlay.xul) + content/messenger/msgMail3PaneWindow.js (content/msgMail3PaneWindow.js) + content/messenger/msgViewNavigation.js (content/msgViewNavigation.js) + content/messenger/msgViewPickerOverlay.js (content/msgViewPickerOverlay.js) + content/messenger/nsDragAndDrop.js (content/nsDragAndDrop.js) + content/messenger/phishingDetector.js (content/phishingDetector.js) + content/messenger/searchBar.js (content/searchBar.js) + content/messenger/start.xhtml (content/start.xhtml) + content/messenger/tabmail.js (content/tabmail.js) + content/messenger/tabmail.xml (content/tabmail.xml) + content/messenger/threadPane.js (content/threadPane.js) + content/messenger/threadPane.xul (content/threadPane.xul) + + content/messenger/SearchDialog.xul (content/SearchDialog.xul) + content/messenger/SearchDialog.js (content/SearchDialog.js) + content/messenger/ABSearchDialog.xul (content/ABSearchDialog.xul) + content/messenger/ABSearchDialog.js (content/ABSearchDialog.js) + content/messenger/FilterListDialog.xul (content/FilterListDialog.xul) + content/messenger/FilterListDialog.js (content/FilterListDialog.js) + + content/messenger/searchTermOverlay.xul (content/searchTermOverlay.xul) diff --git a/comm/suite/mailnews/modules/MailUtils.js b/comm/suite/mailnews/modules/MailUtils.js new file mode 100644 index 0000000000..3bb844718f --- /dev/null +++ b/comm/suite/mailnews/modules/MailUtils.js @@ -0,0 +1,100 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["MailUtils"]; + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/** + * This module has several utility functions for use by both core and + * third-party code. Some functions are aimed at code that doesn't have a + * window context, while others can be used anywhere. + */ +var MailUtils = +{ + /** + * Discover all folders. This is useful during startup, when you have code + * that deals with folders and that executes before the main 3pane window is + * open (the folder tree wouldn't have been initialized yet). + */ + discoverFolders: function MailUtils_discoverFolders() + { + for (let server of MailServices.accounts.allServers) { + // Bug 466311 Sometimes this can throw file not found, we're unsure + // why, but catch it and log the fact. + try { + server.rootFolder.subFolders; + } + catch (ex) { + Services.console.logStringMessage("Discovering folders for account failed with " + + "exception: " + ex); + } + } + }, + + /** + * Get the nsIMsgFolder corresponding to this URI. This uses the RDF service + * to do the work. + * + * @param aFolderURI the URI to convert into a folder + * @param aCheckFolderAttributes whether to check that the folder either has + * a parent or isn't a server + * @returns the nsIMsgFolder corresponding to this URI, or null if + * aCheckFolderAttributes is true and the folder doesn't have a + * parent or is a server + */ + getFolderForURI: function MailUtils_getFolderForURI(aFolderURI, + aCheckFolderAttributes) + { + let folder = null; + let rdfService = Cc['@mozilla.org/rdf/rdf-service;1'] + .getService(Ci.nsIRDFService); + folder = rdfService.GetResource(aFolderURI); + // This is going to QI the folder to an nsIMsgFolder as well + if (folder && folder instanceof Ci.nsIMsgFolder) + { + if (aCheckFolderAttributes && !(folder.parent || folder.isServer)) + return null; + } + else + { + return null; + } + + return folder; + }, + + /** + * Displays this message in a new window. + * + * @param aMsgHdr the message header to display + */ + displayMessage: function MailUtils_displayMessage(aMsgHdr) + { + this.openMessageInNewWindow(aMsgHdr); + }, + + /** + * Open a new standalone message window with this header. + * + * @param aMsgHdr the message header to display + */ + openMessageInNewWindow: function MailUtils_openMessageInNewWindow(aMsgHdr) + { + // Pass in the message URI as messageWindow.js doesn't handle message headers + let messageURI = Cc["@mozilla.org/supports-string;1"] + .createInstance(Ci.nsISupportsString); + messageURI.data = aMsgHdr.folder.getUriForMsg(aMsgHdr); + + Services.ww.openWindow(null, + "chrome://messenger/content/messageWindow.xul", + "_blank", + "all,chrome,dialog=no,status,toolbar", + messageURI); + } +}; diff --git a/comm/suite/mailnews/modules/moz.build b/comm/suite/mailnews/modules/moz.build new file mode 100644 index 0000000000..4a6802690b --- /dev/null +++ b/comm/suite/mailnews/modules/moz.build @@ -0,0 +1,8 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_JS_MODULES += [ + "MailUtils.js", +] diff --git a/comm/suite/mailnews/moz.build b/comm/suite/mailnews/moz.build new file mode 100644 index 0000000000..00f4213662 --- /dev/null +++ b/comm/suite/mailnews/moz.build @@ -0,0 +1,11 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ["jar.mn"] + +DIRS += [ + "components", + "modules", +] |