diff options
Diffstat (limited to 'comm/suite/mailnews/components')
60 files changed, 15613 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"] |