summaryrefslogtreecommitdiffstats
path: root/comm/suite/mailnews/components/addrbook/content/abCommon.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/suite/mailnews/components/addrbook/content/abCommon.js')
-rw-r--r--comm/suite/mailnews/components/addrbook/content/abCommon.js1185
1 files changed, 1185 insertions, 0 deletions
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;
+}