diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/addrbook | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
183 files changed, 38785 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/.eslintrc.js b/comm/mailnews/addrbook/.eslintrc.js new file mode 100644 index 0000000000..5816519fbb --- /dev/null +++ b/comm/mailnews/addrbook/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/valid-jsdoc"], +}; diff --git a/comm/mailnews/addrbook/content/abAddressBookNameDialog.js b/comm/mailnews/addrbook/content/abAddressBookNameDialog.js new file mode 100644 index 0000000000..6c921d9285 --- /dev/null +++ b/comm/mailnews/addrbook/content/abAddressBookNameDialog.js @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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" +); + +var gOkButton; +var gNameInput; +var gDirectory = null; + +var kPersonalAddressbookURI = "jsaddrbook://abook.sqlite"; +var kCollectedAddressbookURI = "jsaddrbook://history.sqlite"; +var kAllDirectoryRoot = "moz-abdirectory://"; + +window.addEventListener("DOMContentLoaded", abNameOnLoad); + +function abNameOnLoad() { + // Get the document elements. + gOkButton = document.querySelector("dialog").getButton("accept"); + gNameInput = document.getElementById("name"); + + // look in arguments[0] for parameters to see if we have a directory or not + if ( + "arguments" in window && + window.arguments[0] && + "selectedDirectory" in window.arguments[0] + ) { + gDirectory = window.arguments[0].selectedDirectory; + gNameInput.value = gDirectory.dirName; + } + + // Work out the window title (if we have a directory specified, then it's a + // rename). + var bundle = document.getElementById("bundle_addressBook"); + + if (gDirectory) { + let oldListName = gDirectory.dirName; + document.title = bundle.getFormattedString("addressBookTitleEdit", [ + oldListName, + ]); + } else { + document.title = bundle.getString("addressBookTitleNew"); + } + + if ( + gDirectory && + (gDirectory.URI == kCollectedAddressbookURI || + gDirectory.URI == kPersonalAddressbookURI || + gDirectory.URI == kAllDirectoryRoot + "?") + ) { + // Address book name is not editable, therefore disable the field and + // only have an ok button that doesn't do anything. + gNameInput.readOnly = true; + document.querySelector("dialog").buttons = "accept"; + } else { + document.addEventListener("dialogaccept", abNameOKButton); + gNameInput.focus(); + abNameDoOkEnabling(); + } +} + +function abNameOKButton(event) { + let newDirName = gNameInput.value.trim(); + + // Do not allow an already existing name. + if ( + MailServices.ab.directoryNameExists(newDirName) && + (!gDirectory || newDirName != gDirectory.dirName) + ) { + const kAlertTitle = document + .getElementById("bundle_addressBook") + .getString("duplicateNameTitle"); + const kAlertText = document + .getElementById("bundle_addressBook") + .getFormattedString("duplicateNameText", [newDirName]); + Services.prompt.alert(window, kAlertTitle, kAlertText); + event.preventDefault(); + return; + } + + // Either create a new directory or update an existing one depending on what + // we were given when we started. + if (gDirectory) { + gDirectory.dirName = newDirName; + } else { + let dirPrefId = MailServices.ab.newAddressBook( + newDirName, + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + let directory = MailServices.ab.getDirectoryFromId(dirPrefId); + window.arguments[0].newDirectoryUID = directory.UID; + if ("onNewDirectory" in window.arguments[0]) { + window.arguments[0].onNewDirectory(directory); + } + } +} + +function abNameDoOkEnabling() { + gOkButton.disabled = gNameInput.value.trim() == ""; +} diff --git a/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml b/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml new file mode 100644 index 0000000000..3fc0d7d9e1 --- /dev/null +++ b/comm/mailnews/addrbook/content/abAddressBookNameDialog.xhtml @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> + +<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/abAddressBookNameDialog.dtd"> + +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + scrolling="false" + style="min-width: 540px; min-height: 210px" +> + <head> + <title><!-- addressBookTitleEdit --></title> + <script + defer="defer" + src="chrome://messenger/content/globalOverlay.js" + ></script> + <script + defer="defer" + src="chrome://global/content/editMenuOverlay.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/addressbook/abAddressBookNameDialog.js" + ></script> + </head> + <body> + <xul:dialog> + <xul:stringbundle + id="bundle_addressBook" + src="chrome://messenger/locale/addressbook/addressBook.properties" + /> + + <div class="input-container"> + <label id="nameLabel" for="name">&name.label;</label> + <input + id="name" + type="text" + class="input-inline" + oninput="abNameDoOkEnabling();" + /> + </div> + </xul:dialog> + </body> +</html> diff --git a/comm/mailnews/addrbook/content/abCardDAVDialog.js b/comm/mailnews/addrbook/content/abCardDAVDialog.js new file mode 100644 index 0000000000..8ccce89680 --- /dev/null +++ b/comm/mailnews/addrbook/content/abCardDAVDialog.js @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + CardDAVUtils: "resource:///modules/CardDAVUtils.jsm", + MailServices: "resource:///modules/MailServices.jsm", +}); + +var log = console.createInstance({ + prefix: "carddav.setup", + maxLogLevel: "Warn", + maxLogLevelPref: "carddav.setup.loglevel", +}); + +var oAuth = null; +var callbacks = null; +var uiElements = {}; +var userContextId; + +window.addEventListener( + "DOMContentLoaded", + () => { + for (let id of [ + "username", + "location", + "statusArea", + "statusImage", + "statusMessage", + "resultsArea", + "availableBooks", + ]) { + uiElements[id] = document.getElementById("carddav-" + id); + } + }, + { once: true } +); +window.addEventListener( + "DOMContentLoaded", + async () => { + await document.l10n.translateRoots(); + fillLocationPlaceholder(); + setStatus(); + }, + { once: true } +); + +/** + * Update the placeholder text for the network location field. If the username + * is a valid email address use the domain part of the username, otherwise use + * the default placeholder. + */ +function fillLocationPlaceholder() { + let parts = uiElements.username.value.split("@"); + let domain = parts.length == 2 && parts[1] ? parts[1] : null; + + if (domain) { + uiElements.location.setAttribute("placeholder", domain); + } else { + uiElements.location.setAttribute( + "placeholder", + uiElements.location.getAttribute("default-placeholder") + ); + } +} + +function handleCardDAVURLInput(event) { + changeCardDAVURL(); +} + +function changeCardDAVURL() { + uiElements.resultsArea.hidden = true; + setStatus(); +} + +function handleCardDAVURLBlur(event) { + if ( + uiElements.location.validity.typeMismatch && + !uiElements.location.value.match(/^https?:\/\//) + ) { + uiElements.location.value = `https://${uiElements.location.value}`; + } +} + +async function check() { + // We might be accepting the dialog by pressing Enter in the URL input. + handleCardDAVURLBlur(); + + let username = uiElements.username.value; + + if (!uiElements.location.validity.valid && !username.split("@")[1]) { + log.error(`Invalid URL: "${uiElements.location.value}"`); + return; + } + + let url = uiElements.location.value || username.split("@")[1]; + if (!url.match(/^https?:\/\//)) { + url = "https://" + url; + } + + setStatus("loading", "carddav-loading"); + while (uiElements.availableBooks.lastChild) { + uiElements.availableBooks.lastChild.remove(); + } + + let foundBooks; + try { + foundBooks = await CardDAVUtils.detectAddressBooks( + username, + undefined, + url, + true + ); + } catch (ex) { + if (ex.result == Cr.NS_ERROR_NOT_AVAILABLE) { + setStatus("error", "carddav-known-incompatible", { + url: new URL(url).hostname, + }); + } else { + log.error(ex); + setStatus("error", "carddav-connection-error"); + } + return; + } + + // Create a list of CardDAV directories that already exist. + let existing = []; + for (let d of MailServices.ab.directories) { + if (d.dirType == Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE) { + existing.push(d.getStringValue("carddav.url", "")); + } + } + + // Display a checkbox for each directory that doesn't already exist. + let alreadyAdded = 0; + for (let book of foundBooks) { + if (existing.includes(book.url.href)) { + alreadyAdded++; + continue; + } + let checkbox = uiElements.availableBooks.appendChild( + document.createXULElement("checkbox") + ); + checkbox.setAttribute("label", book.name); + checkbox.checked = true; + checkbox.value = book.url.href; + checkbox._book = book; + } + + if (uiElements.availableBooks.childElementCount == 0) { + if (alreadyAdded > 0) { + setStatus("error", "carddav-already-added"); + } else { + setStatus("error", "carddav-none-found"); + } + } else { + uiElements.resultsArea.hidden = false; + setStatus(); + } +} + +function setStatus(status, message, args) { + uiElements.username.disabled = status == "loading"; + uiElements.location.disabled = status == "loading"; + + switch (status) { + case "loading": + uiElements.statusImage.setAttribute( + "src", + "chrome://global/skin/icons/loading.png" + ); + uiElements.statusImage.setAttribute( + "srcset", + "chrome://global/skin/icons/loading@2x.png 2x" + ); + break; + case "error": + uiElements.statusImage.setAttribute( + "src", + "chrome://global/skin/icons/warning.svg" + ); + uiElements.statusImage.removeAttribute("srcset"); + break; + default: + uiElements.statusImage.removeAttribute("src"); + uiElements.statusImage.removeAttribute("srcset"); + break; + } + + if (status) { + uiElements.statusArea.setAttribute("status", status); + document.l10n.setAttributes(uiElements.statusMessage, message, args); + } else { + uiElements.statusArea.removeAttribute("status"); + uiElements.statusMessage.removeAttribute("data-l10n-id"); + uiElements.statusMessage.textContent = ""; + } + + // Grow to fit the list of books. Uses `resizeBy` because it has special + // handling in SubDialog.jsm that the other resize functions don't have. + window.resizeBy(0, Math.min(250, uiElements.availableBooks.scrollHeight)); + window.dispatchEvent(new CustomEvent("status-changed")); +} + +window.addEventListener("dialogaccept", event => { + if (uiElements.resultsArea.hidden) { + event.preventDefault(); + check(); + return; + } + + if (uiElements.availableBooks.childElementCount == 0) { + return; + } + + for (let checkbox of uiElements.availableBooks.children) { + if (checkbox.checked) { + let book = checkbox._book.create(); + if (window.arguments[0]) { + // Pass the UID of the book back to the opening window. + window.arguments[0].newDirectoryUID = book.UID; + } + } + } +}); diff --git a/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml b/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml new file mode 100644 index 0000000000..9e0fc5e1e4 --- /dev/null +++ b/comm/mailnews/addrbook/content/abCardDAVDialog.xhtml @@ -0,0 +1,96 @@ +<?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/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/cardDAV.css" type="text/css"?> + +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + scrolling="false" + style="min-width: 540px; min-height: 210px" +> + <head> + <title data-l10n-id="carddav-window-title"></title> + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="messenger/addressbook/abCardDAVDialog.ftl" /> + <script + defer="defer" + src="chrome://messenger/content/globalOverlay.js" + ></script> + <script + defer="defer" + src="chrome://global/content/editMenuOverlay.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/addressbook/abCardDAVDialog.js" + ></script> + </head> + <html:body + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + > + <dialog + id="carddav-dialog" + data-l10n-id="carddav-dialog" + data-l10n-attrs="buttonlabelaccept,buttonaccesskeyaccept" + > + <html:div id="carddav-properties-table"> + <html:div> + <label + control="carddav-username" + data-l10n-id="carddav-username-label" + data-l10n-attrs="value,accesskey" + /> + </html:div> + <html:div class="input-container"> + <html:input + id="carddav-username" + type="text" + oninput="fillLocationPlaceholder();" + /> + </html:div> + <html:div> + <label + control="carddav-location" + data-l10n-id="carddav-location-label" + data-l10n-attrs="value,accesskey" + /> + </html:div> + <html:div class="input-container"> + <html:input + id="carddav-location" + type="url" + data-l10n-id="carddav-location" + data-l10n-attrs="default-placeholder" + required="required" + onblur="handleCardDAVURLBlur(event);" + oninput="handleCardDAVURLInput(event);" + /> + </html:div> + </html:div> + + <html:div id="carddav-statusArea"> + <html:div id="carddav-statusContainer"> + <html:img id="carddav-statusImage" alt="" /> + <html:span id="carddav-statusMessage"> </html:span> + <!-- Include 160 = nbsp, to make the element occupy the + full height, for at least one line. With a normal space, + it does not have sufficient height. --> + </html:div> + </html:div> + + <vbox id="carddav-resultsArea" hidden="true" flex="1"> + <label + id="carddav-availableBooksHeader" + data-l10n-id="carddav-available-books" + /> + <vbox id="carddav-availableBooks" class="indent"></vbox> + </vbox> + </dialog> + </html:body> +</html> diff --git a/comm/mailnews/addrbook/content/abCardDAVProperties.js b/comm/mailnews/addrbook/content/abCardDAVProperties.js new file mode 100644 index 0000000000..3b2bddf2c4 --- /dev/null +++ b/comm/mailnews/addrbook/content/abCardDAVProperties.js @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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" +); + +var gDirectory = window.arguments[0].selectedDirectory; +var gStringBundle, + gNameInput, + gURLInput, + gRefreshActiveInput, + gRefreshMenulist, + gReadOnlyInput, + gAcceptButton; + +window.addEventListener( + "DOMContentLoaded", + () => { + gStringBundle = Services.strings.createBundle( + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + document.title = gStringBundle.formatStringFromName( + "addressBookTitleEdit", + [gDirectory.dirName] + ); + + gNameInput = document.getElementById("carddav-name"); + gNameInput.value = gDirectory.dirName; + gNameInput.addEventListener("input", () => { + gAcceptButton.disabled = gNameInput.value.trim() == ""; + }); + + gURLInput = document.getElementById("carddav-url"); + gURLInput.value = gDirectory.getStringValue("carddav.url", ""); + + gRefreshActiveInput = document.getElementById("carddav-refreshActive"); + gRefreshActiveInput.addEventListener( + "command", + () => (gRefreshMenulist.disabled = !gRefreshActiveInput.checked) + ); + + gRefreshMenulist = document.getElementById("carddav-refreshInterval"); + initRefreshInterval(); + + gReadOnlyInput = document.getElementById("carddav-readOnly"); + gReadOnlyInput.checked = gDirectory.readOnly; + + gAcceptButton = document.querySelector("dialog").getButton("accept"); + }, + { once: true } +); + +window.addEventListener("dialogaccept", event => { + let newDirName = gNameInput.value.trim(); + let newSyncInterval = gRefreshActiveInput.checked + ? gRefreshMenulist.value + : 0; + + if (newDirName != gDirectory.dirName) { + // Do not allow an already existing name. + if (MailServices.ab.directoryNameExists(newDirName)) { + let alertTitle = gStringBundle.GetStringFromName("duplicateNameTitle"); + let alertText = gStringBundle.formatStringFromName("duplicateNameText", [ + newDirName, + ]); + Services.prompt.alert(window, alertTitle, alertText); + event.preventDefault(); + return; + } + + gDirectory.dirName = newDirName; + } + + if (newSyncInterval != gDirectory.getIntValue("carddav.syncinterval", -1)) { + gDirectory.setIntValue("carddav.syncinterval", newSyncInterval); + } + + if (gReadOnlyInput.checked != gDirectory.readOnly) { + gDirectory.setBoolValue("readOnly", gReadOnlyInput.checked); + } +}); + +function initRefreshInterval() { + function createMenuItem(minutes) { + let menuitem = document.createXULElement("menuitem"); + menuitem.setAttribute("value", minutes); + menuitem.setAttribute("data-l10n-attrs", "label"); + if (minutes < 60) { + document.l10n.setAttributes( + menuitem, + "carddav-refreshinterval-minutes-value", + { + minutes, + } + ); + } else { + document.l10n.setAttributes( + menuitem, + "carddav-refreshinterval-hours-value", + { + hours: minutes / 60, + } + ); + } + + gRefreshMenulist.menupopup.appendChild(menuitem); + if (refreshInterval == minutes) { + gRefreshMenulist.value = minutes; + foundValue = true; + } + + return menuitem; + } + + let refreshInterval = gDirectory.getIntValue("carddav.syncinterval", 30); + if (refreshInterval === null) { + refreshInterval = 30; + } + + let foundValue = false; + + for (let min of [1, 5, 15, 30, 60, 120, 240, 360, 720, 1440]) { + createMenuItem(min); + } + + if (refreshInterval == 0) { + gRefreshMenulist.value = 30; // The default. + gRefreshMenulist.disabled = true; + foundValue = true; + } else { + gRefreshActiveInput.checked = true; + } + + if (!foundValue) { + // Special menuitem in case the user changed the value in the config editor. + createMenuItem(refreshInterval); + } +} diff --git a/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml b/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml new file mode 100644 index 0000000000..b611cbbf43 --- /dev/null +++ b/comm/mailnews/addrbook/content/abCardDAVProperties.xhtml @@ -0,0 +1,93 @@ +<?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/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/cardDAV.css" type="text/css"?> + +<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/abAddressBookNameDialog.dtd"> + +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + width="540" + height="210" + scrolling="false" +> + <head> + <title><!-- addressBookTitleEdit --></title> + <link + rel="localization" + href="messenger/addressbook/abCardDAVProperties.ftl" + /> + <script + defer="defer" + src="chrome://messenger/content/globalOverlay.js" + ></script> + <script + defer="defer" + src="chrome://global/content/editMenuOverlay.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/addressbook/abCardDAVProperties.js" + ></script> + </head> + <body> + <xul:dialog id="carddav-properties-dialog"> + <div id="carddav-properties-table"> + <div> + <xul:label + control="carddav-name" + value="&name.label;" + accesskey="&name.accesskey;" + /> + </div> + <div class="input-container"> + <input id="carddav-name" type="text" class="input-inline" /> + </div> + <div> + <xul:label + data-l10n-id="carddav-url-label" + data-l10n-attrs="value,accesskey" + control="carddav-url" + /> + </div> + <div class="input-container"> + <input + id="carddav-url" + type="url" + class="input-inline" + readonly="readonly" + /> + </div> + <div id="carddav-refreshActive-cell"> + <xul:checkbox + id="carddav-refreshActive" + data-l10n-id="carddav-refreshinterval-label" + data-l10n-attrs="label,accesskey" + control="carddav-refreshInterval" + /> + </div> + <div id="carddav-refreshInterval-cell"> + <xul:menulist id="carddav-refreshInterval"> + <xul:menupopup> + <!-- This will be filled programmatically to reduce the number of needed strings --> + </xul:menupopup> + </xul:menulist> + </div> + <div></div> + <div> + <xul:checkbox + id="carddav-readOnly" + data-l10n-id="carddav-readonly-label" + data-l10n-attrs="label,accesskey" + /> + </div> + </div> + </xul:dialog> + </body> +</html> diff --git a/comm/mailnews/addrbook/content/abDragDrop.js b/comm/mailnews/addrbook/content/abDragDrop.js new file mode 100644 index 0000000000..56cbee0e08 --- /dev/null +++ b/comm/mailnews/addrbook/content/abDragDrop.js @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 abResultsPane.js */ + +// Returns the load context for the current window +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +var abFlavorDataProvider = { + QueryInterface: ChromeUtils.generateQI(["nsIFlavorDataProvider"]), + + getFlavorData(aTransferable, aFlavor, aData) { + if (aFlavor == "application/x-moz-file-promise") { + var primitive = {}; + aTransferable.getTransferData("text/vcard", primitive); + var vCard = primitive.value.QueryInterface(Ci.nsISupportsString).data; + aTransferable.getTransferData( + "application/x-moz-file-promise-dest-filename", + primitive + ); + var leafName = primitive.value.QueryInterface(Ci.nsISupportsString).data; + aTransferable.getTransferData( + "application/x-moz-file-promise-dir", + primitive + ); + var localFile = primitive.value.QueryInterface(Ci.nsIFile).clone(); + localFile.append(leafName); + + var ofStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + ofStream.init(localFile, -1, -1, 0); + var converter = Cc[ + "@mozilla.org/intl/converter-output-stream;1" + ].createInstance(Ci.nsIConverterOutputStream); + converter.init(ofStream, null); + converter.writeString(vCard); + converter.close(); + + aData.value = localFile; + } + }, +}; + +let abResultsPaneObserver = { + onDragStart(event) { + let selectedRows = GetSelectedRows(); + + if (!selectedRows) { + return; + } + + let selectedAddresses = GetSelectedAddresses(); + + event.dataTransfer.setData("moz/abcard", selectedRows); + event.dataTransfer.setData("moz/abcard", selectedRows); + event.dataTransfer.setData("text/x-moz-address", selectedAddresses); + event.dataTransfer.setData("text/plain", selectedAddresses); + + let card = GetSelectedCard(); + if (card && card.displayName && !card.isMailList) { + try { + // A card implementation may throw NS_ERROR_NOT_IMPLEMENTED. + // Don't break drag-and-drop if that happens. + let vCard = card.translateTo("vcard"); + event.dataTransfer.setData("text/vcard", decodeURIComponent(vCard)); + event.dataTransfer.setData( + "application/x-moz-file-promise-dest-filename", + `${card.displayName}.vcf`.replace(/(.{74}).*(.{10})$/u, "$1...$2") + ); + event.dataTransfer.setData( + "application/x-moz-file-promise-url", + "data:text/vcard," + vCard + ); + event.dataTransfer.setData( + "application/x-moz-file-promise", + abFlavorDataProvider + ); + } catch (ex) { + console.error(ex); + } + } + + event.dataTransfer.effectAllowed = "copyMove"; + // a drag targeted at a tree should instead use the treechildren so that + // the current selection is used as the drag feedback + event.dataTransfer.addElement(event.target); + event.stopPropagation(); + }, +}; + +function DragAddressOverTargetControl(event) { + var dragSession = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService) + .getCurrentSession(); + + if (!dragSession.isDataFlavorSupported("text/x-moz-address")) { + return; + } + + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(getLoadContext()); + trans.addDataFlavor("text/x-moz-address"); + + var canDrop = true; + + for (var i = 0; i < dragSession.numDropItems; ++i) { + dragSession.getData(trans, i); + var dataObj = {}; + var bestFlavor = {}; + try { + trans.getAnyTransferData(bestFlavor, dataObj); + } catch (ex) { + canDrop = false; + break; + } + } + dragSession.canDrop = canDrop; +} diff --git a/comm/mailnews/addrbook/content/abMailListDialog.js b/comm/mailnews/addrbook/content/abMailListDialog.js new file mode 100644 index 0000000000..6d512d3014 --- /dev/null +++ b/comm/mailnews/addrbook/content/abMailListDialog.js @@ -0,0 +1,961 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 ../../../mail/components/addrbook/content/abCommon.js */ +/* import-globals-from ../../../mail/components/compose/content/addressingWidgetOverlay.js */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +top.MAX_RECIPIENTS = 1; + +var gListCard; +var gEditList; +var gOldListName = ""; + +var gAWContentHeight = 0; +var gAWRowHeight = 0; +var gNumberOfCols = 0; + +var test_addresses_sequence = false; + +if ( + Services.prefs.getPrefType("mail.debug.test_addresses_sequence") == + Ci.nsIPrefBranch.PREF_BOOL +) { + test_addresses_sequence = Services.prefs.getBoolPref( + "mail.debug.test_addresses_sequence" + ); +} + +try { + var gDragService = Cc["@mozilla.org/widget/dragservice;1"].getService( + Ci.nsIDragService + ); +} catch (e) {} + +// Returns the load context for the current window +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +function mailingListExists(listname) { + if (MailServices.ab.mailListNameExists(listname)) { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + Services.prompt.alert( + window, + bundle.GetStringFromName("mailListNameExistsTitle"), + bundle.GetStringFromName("mailListNameExistsMessage") + ); + return true; + } + return false; +} + +/** + * Get the new inputs from the create/edit mailing list dialog and use them to + * update the mailing list that was passed in as an argument. + * + * @param {nsIAbDirectory} mailList - The mailing list object to update. When + * creating a new list it will be newly created and empty. + * @param {boolean} isNewList - Whether we are populating a new list. + * @returns {boolean} - Whether the operation succeeded or not. + */ +function updateMailList(mailList, isNewList) { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + let listname = document.getElementById("ListName").value.trim(); + + if (listname.length == 0) { + alert(bundle.GetStringFromName("emptyListName")); + return false; + } + + if (listname.match(" ")) { + alert(bundle.GetStringFromName("badListNameSpaces")); + return false; + } + + for (let char of ',;"<>') { + if (listname.includes(char)) { + alert(bundle.GetStringFromName("badListNameCharacters")); + return false; + } + } + + let canonicalNewListName = listname.toLowerCase(); + let canonicalOldListName = gOldListName.toLowerCase(); + if (isNewList || canonicalOldListName != canonicalNewListName) { + if (mailingListExists(listname)) { + // After showing the "Mailing List Already Exists" error alert, + // focus ListName input field for user to choose a different name. + document.getElementById("ListName").focus(); + return false; + } + } + + mailList.isMailList = true; + mailList.dirName = listname; + mailList.listNickName = document.getElementById("ListNickName").value; + mailList.description = document.getElementById("ListDescription").value; + + return true; +} + +/** + * Updates the members of the mailing list. + * + * @param {nsIAbDirectory} mailList - The mailing list object to + * update. When creating a new list it will be newly created and empty. + * @param {nsIAbDirectory} parentDirectory - The address book containing the + * mailing list. + */ +function updateMailListMembers(mailList, parentDirectory) { + // Gather email address inputs into a single string (comma-separated). + let addresses = Array.from( + document.querySelectorAll(".textbox-addressingWidget"), + element => element.value + ) + .filter(value => value.trim()) + .join(); + + // Convert the addresses string into address objects. + let addressObjects = + MailServices.headerParser.makeFromDisplayAddress(addresses); + let existingCards = mailList.childCards; + + // Work out which addresses need to be added... + let existingCardAddresses = existingCards.map(card => card.primaryEmail); + let addressObjectsToAdd = addressObjects.filter( + aObj => !existingCardAddresses.includes(aObj.email) + ); + + // ... and which need to be removed. + let addressObjectAddresses = addressObjects.map(aObj => aObj.email); + let cardsToRemove = existingCards.filter( + card => !addressObjectAddresses.includes(card.primaryEmail) + ); + + for (let { email, name } of addressObjectsToAdd) { + let card = parentDirectory.cardForEmailAddress(email); + if (!card) { + card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + card.primaryEmail = email; + card.displayName = name || email; + } + mailList.addCard(card); + } + + if (cardsToRemove.length > 0) { + mailList.deleteCards(cardsToRemove); + } +} + +function MailListOKButton(event) { + var popup = document.getElementById("abPopup"); + if (popup) { + var uri = popup.getAttribute("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) { + event.preventDefault(); + return; // don't close window + } + // ----- + + // Add mailing list to database + var mailList = + Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance(); + mailList = mailList.QueryInterface(Ci.nsIAbDirectory); + + if (updateMailList(mailList, true)) { + var parentDirectory = GetDirectoryFromURI(uri); + mailList = parentDirectory.addMailList(mailList); + updateMailListMembers(mailList, parentDirectory); + window.arguments[0].newListUID = mailList.UID; + window.arguments[0].newListURI = mailList.URI; + } else { + event.preventDefault(); + } + } +} + +function OnLoadNewMailList() { + var selectedAB = null; + + if ("arguments" in window && window.arguments[0]) { + var abURI = window.arguments[0].selectedAB; + if (abURI && abURI != kAllDirectoryRoot + "?") { + var directory = GetDirectoryFromURI(abURI); + if (directory.isMailList) { + var parentURI = GetParentDirectoryFromMailingListURI(abURI); + if (parentURI) { + selectedAB = parentURI; + } + } else if (directory.readOnly) { + selectedAB = kPersonalAddressbookURI; + } else { + selectedAB = abURI; + } + } + + let cards = window.arguments[0].cards; + if (cards && cards.length > 0) { + let listbox = document.getElementById("addressingWidget"); + let newListBoxNode = listbox.cloneNode(false); + let templateNode = listbox.querySelector("richlistitem"); + + top.MAX_RECIPIENTS = 0; + for (let card of cards) { + let address = MailServices.headerParser + .makeMailboxObject(card.displayName, card.primaryEmail) + .toString(); + SetInputValue(address, newListBoxNode, templateNode); + } + listbox.parentNode.replaceChild(newListBoxNode, listbox); + } + } + + if (!selectedAB) { + selectedAB = kPersonalAddressbookURI; + } + + // set popup with address book names + var abPopup = document.getElementById("abPopup"); + abPopup.value = selectedAB; + + AppendNewRowAndSetFocus(); + awFitDummyRows(1); + + if (AppConstants.MOZ_APP_NAME == "seamonkey") { + /* global awDocumentKeyPress */ + document.addEventListener("keypress", awDocumentKeyPress, true); + } + + // focus on first name + var listName = document.getElementById("ListName"); + if (listName) { + setTimeout( + function (firstTextBox) { + firstTextBox.focus(); + }, + 0, + listName + ); + } + + let input = document.getElementById("addressCol1#1"); + input.popup.addEventListener("click", () => { + awReturnHit(input); + }); + + document.addEventListener("dialogaccept", MailListOKButton); +} + +function EditListOKButton(event) { + // edit mailing list in database + if (updateMailList(gEditList, false)) { + let parentURI = GetParentDirectoryFromMailingListURI(gEditList.URI); + let parentDirectory = GetDirectoryFromURI(parentURI); + updateMailListMembers(gEditList, parentDirectory); + if (gListCard) { + // modify the list card (for the results pane) from the mailing list + gListCard.displayName = gEditList.dirName; + gListCard.lastName = gEditList.dirName; + gListCard.setProperty("NickName", gEditList.listNickName); + gListCard.setProperty("Notes", gEditList.description); + } + + gEditList.editMailListToDatabase(gListCard); + + window.arguments[0].refresh = true; + return; // close the window + } + event.preventDefault(); +} + +function OnLoadEditList() { + gListCard = window.arguments[0].abCard; + var listUri = window.arguments[0].listURI; + + gEditList = GetDirectoryFromURI(listUri); + + document.getElementById("ListName").value = gEditList.dirName; + document.getElementById("ListNickName").value = gEditList.listNickName; + document.getElementById("ListDescription").value = gEditList.description; + gOldListName = gEditList.dirName; + + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + document.title = bundle.formatStringFromName("mailingListTitleEdit", [ + gOldListName, + ]); + + let cards = gEditList.childCards; + if (cards.length > 0) { + let listbox = document.getElementById("addressingWidget"); + let newListBoxNode = listbox.cloneNode(false); + let templateNode = listbox.querySelector("richlistitem"); + + top.MAX_RECIPIENTS = 0; + for (let card of cards) { + let address = MailServices.headerParser + .makeMailboxObject(card.displayName, card.primaryEmail) + .toString(); + SetInputValue(address, newListBoxNode, templateNode); + } + listbox.parentNode.replaceChild(newListBoxNode, listbox); + } + + // Is this directory read-only? If so, we now need to set all the fields to + // read-only. + if (gEditList.readOnly) { + const kMailListFields = ["ListName", "ListNickName", "ListDescription"]; + + for (let i = 0; i < kMailListFields.length; ++i) { + document.getElementById(kMailListFields[i]).readOnly = true; + } + + document.querySelector("dialog").buttons = "accept"; + + // Getting a sane read-only implementation for the addressing widget would + // basically need a separate dialog. Given I'm not sure about the future of + // the mailing list dialog in its current state, let's just disable it + // completely. + document.getElementById("addressingWidget").disabled = true; + } else { + document.addEventListener("dialogaccept", EditListOKButton); + } + + if (AppConstants.MOZ_APP_NAME == "seamonkey") { + document.addEventListener("keypress", awDocumentKeyPress, true); + } + + // workaround for bug 118337 - for mailing lists that have more rows than fits inside + // the display, the value of the textbox inside the new row isn't inherited into the input - + // the first row then appears to be duplicated at the end although it is actually empty. + // see awAppendNewRow which copies first row and clears it + setTimeout(AppendLastRow, 0); + + document.querySelectorAll(`input[is="autocomplete-input"]`).forEach(input => { + input.popup.addEventListener("click", () => { + awReturnHit(input); + }); + }); +} + +function AppendLastRow() { + AppendNewRowAndSetFocus(); + awFitDummyRows(1); + + // focus on first name + let listName = document.getElementById("ListName"); + if (listName) { + listName.focus(); + } +} + +function AppendNewRowAndSetFocus() { + let lastInput = awGetInputElement(top.MAX_RECIPIENTS); + if (lastInput && lastInput.value) { + awAppendNewRow(true); + } else { + awSetFocusTo(lastInput); + } +} + +function SetInputValue(inputValue, 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.querySelector(`input[is="autocomplete-input"]`); + let label = newNode.querySelector(`label.person-icon`); + if (input) { + input.value = inputValue; + input.setAttribute("id", "addressCol1#" + top.MAX_RECIPIENTS); + label.setAttribute("for", "addressCol1#" + top.MAX_RECIPIENTS); + input.popup.addEventListener("click", () => { + awReturnHit(input); + }); + } +} + +function awClickEmptySpace(target, setFocus) { + if (target == null || target.localName != "hbox") { + 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); + if (inputElement.value) { + let nextInput = awGetInputElement(row + 1); + if (!nextInput) { + awAppendNewRow(true); + } else { + awSetFocusTo(nextInput); + } + } +} + +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); + + var numberOfCols = awGetNumberOfCols(); + for (var row = rowToDelete + 1; row <= maxRecipients; row++) { + for (var col = 1; col <= numberOfCols; col++) { + awGetElementByCol(row, col).setAttribute( + "id", + "addressCol" + col + "#" + (row - 1) + ); + } + } + + awTestRowSequence(); +} + +/** + * Append a new row. + * + * @param {boolean} setFocus - Whether to set the focus on the new row. + * @returns {Element?} The input element from the new row. + */ +function awAppendNewRow(setFocus) { + let body = document.getElementById("addressingWidget"); + let listitem1 = awGetListItem(1); + let input; + let label; + + if (body && listitem1) { + let nextDummy = awGetNextDummyRow(); + let newNode = listitem1.cloneNode(true); + if (nextDummy) { + body.replaceChild(newNode, nextDummy); + } else { + body.appendChild(newNode); + } + + top.MAX_RECIPIENTS++; + + input = newNode.querySelector(`input[is="autocomplete-input"]`); + label = newNode.querySelector(`label.person-icon`); + if (input) { + input.value = ""; + input.setAttribute("id", "addressCol1#" + top.MAX_RECIPIENTS); + label.setAttribute("for", "addressCol1#" + top.MAX_RECIPIENTS); + input.popup.addEventListener("click", () => { + awReturnHit(input); + }); + } + // Focus the new input widget. + if (setFocus && input) { + awSetFocusTo(input); + } + } + return input; +} + +// functions for accessing the elements in the addressing widget + +/** + * Returns the recipient inputbox for a row. + * + * @param {integer} row - Index of the recipient row to return. Starts at 1. + * @returns {Element} This returns the input element. + */ +function awGetInputElement(row) { + return document.getElementById("addressCol1#" + 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) { + return listbox.getItemAtIndex(row - 1); + } + + return null; +} + +/** + * @param {Element} inputElement - The recipient input element. + * @returns {integer} The row index (starting from 1) where the input element + * is found. 0 if the element is not found. + */ +function awGetRowByInputElement(inputElement) { + if (!inputElement) { + return 0; + } + + var listitem = inputElement.parentNode.parentNode; + return ( + document.getElementById("addressingWidget").getIndexOfItem(listitem) + 1 + ); +} + +function DragOverAddressListTree(event) { + var dragSession = gDragService.getCurrentSession(); + + // XXX add support for other flavors here + if (dragSession.isDataFlavorSupported("text/x-moz-address")) { + dragSession.canDrop = true; + } +} + +function DropOnAddressListTree(event) { + let dragSession = gDragService.getCurrentSession(); + let trans; + + try { + trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(getLoadContext()); + trans.addDataFlavor("text/x-moz-address"); + } catch (ex) { + return; + } + + for (let i = 0; i < dragSession.numDropItems; ++i) { + dragSession.getData(trans, i); + let dataObj = {}; + let bestFlavor = {}; + trans.getAnyTransferData(bestFlavor, dataObj); + if (dataObj) { + dataObj = dataObj.value.QueryInterface(Ci.nsISupportsString); + } + if (!dataObj) { + continue; + } + + // pull the URL out of the data object + let address = dataObj.data.substring(0, dataObj.length); + if (!address) { + continue; + } + + DropListAddress(event.target, address); + } +} + +function DropListAddress(target, address) { + // Set focus on a new available, visible row. + awClickEmptySpace(target, true); + if (top.MAX_RECIPIENTS == 0) { + top.MAX_RECIPIENTS = 1; + } + + // Break apart the MIME-ready header address into individual addressees to + // add to the dialog. + let addresses = MailServices.headerParser.parseEncodedHeader(address); + for (let addr of addresses) { + let lastInput = awGetInputElement(top.MAX_RECIPIENTS); + lastInput.value = addr.toString(); + awAppendNewRow(true); + } +} + +/** + * Handles keypress events for the email address inputs (that auto-fill) + * in the Address Book Mailing List dialogs. When a comma-separated list of + * addresses is entered on one row, split them into one address per row. Only + * add a new blank row on "Enter" key. On "Tab" key focus moves to the "Cancel" + * button. + * + * @param {KeyboardEvent} event - The DOM keypress event. + * @param {Element} element - The element that triggered the keypress event. + */ +function awAbRecipientKeyPress(event, element) { + if (event.key != "Enter" && event.key != "Tab") { + return; + } + + if (!element.value) { + if (event.key == "Enter") { + awReturnHit(element); + } + } else { + let inputElement = element; + let originalRow = awGetRowByInputElement(element); + let row; + let addresses = MailServices.headerParser.makeFromDisplayAddress( + element.value + ); + + if (addresses.length > 1) { + // Collect any existing addresses from the following rows so we don't + // simply overwrite them. + row = originalRow + 1; + inputElement = awGetInputElement(row); + + while (inputElement) { + if (inputElement.value) { + addresses.push(inputElement.value); + inputElement.value = ""; + } + row += 1; + inputElement = awGetInputElement(row); + } + } + + // Insert the addresses, adding new rows if needed. + row = originalRow; + let needNewRows = false; + + for (let address of addresses) { + if (needNewRows) { + inputElement = awAppendNewRow(false); + } else { + inputElement = awGetInputElement(row); + if (!inputElement) { + needNewRows = true; + inputElement = awAppendNewRow(false); + } + } + + if (inputElement) { + inputElement.value = address; + } + row += 1; + } + + if (event.key == "Enter") { + // Prevent the dialog from closing. "Enter" inserted a new row instead. + event.preventDefault(); + awReturnHit(inputElement); + } else if (event.key == "Tab") { + // Focus the last row to let "Tab" move focus to the "Cancel" button. + let lastRow = row - 1; + awGetInputElement(lastRow).focus(); + } + } +} + +/** + * Handle keydown event on a recipient input. + * Enables recipient row deletion with DEL or BACKSPACE and + * recipient list navigation with cursor up/down. + * + * Note that the keydown event fires for ALL keys, so this may affect + * autocomplete as user enters a recipient text. + * + * @param {KeyboardEvent} event - The keydown event fired on a recipient input. + * @param {HTMLInputElement} inputElement - The recipient input element + * on which the event fired (textbox-addressingWidget). + */ +function awRecipientKeyDown(event, inputElement) { + switch (event.key) { + // Enable deletion of empty recipient rows. + case "Delete": + case "Backspace": + if (inputElement.value.length == 1 && event.repeat) { + // User is holding down Delete or Backspace to delete recipient text + // inline and is now deleting the last character: Set flag to + // temporarily block row deletion. + top.awRecipientInlineDelete = true; + } + if (!inputElement.value && !event.altKey) { + // When user presses DEL or BACKSPACE on an empty row, and it's not an + // ongoing inline deletion, and not ALT+BACKSPACE for input undo, + // we delete the row. + if (top.awRecipientInlineDelete && !event.repeat) { + // User has released and re-pressed Delete or Backspace key + // after holding them down to delete recipient text inline: + // unblock row deletion. + top.awRecipientInlineDelete = false; + } + if (!top.awRecipientInlineDelete) { + let deleteForward = event.key == "Delete"; + awDeleteHit(inputElement, deleteForward); + } + } + break; + + // Enable browsing the list of recipients up and down with cursor keys. + case "ArrowDown": + case "ArrowUp": + // Only browse recipients if the autocomplete popup is not open. + if (!inputElement.popupOpen) { + let row = awGetRowByInputElement(inputElement); + let down = event.key == "ArrowDown"; + let noEdgeRow = down ? row < top.MAX_RECIPIENTS : row > 1; + if (noEdgeRow) { + let targetRow = down ? row + 1 : row - 1; + awSetFocusTo(awGetInputElement(targetRow)); + } + } + break; + } +} + +/** + * Delete recipient row (addressingWidgetItem) from UI. + * + * @param {HTMLInputElement} inputElement - The recipient input element. + * textbox-addressingWidget) whose parent row (addressingWidgetItem) will be + * deleted. + * @param {boolean} deleteForward - true: focus next row after deleting the row + * false: focus previous row after deleting the row + */ +function awDeleteHit(inputElement, deleteForward = false) { + let row = awGetRowByInputElement(inputElement); + + // Don't delete the row if it's the last one remaining; just reset it. + if (top.MAX_RECIPIENTS <= 1) { + inputElement.value = ""; + return; + } + + // Set the focus to the input field of the next/previous row according to + // the direction of deleting if possible. + // Note: awSetFocusTo() is asynchronous, i.e. we'll focus after row removal. + if ( + (!deleteForward && row > 1) || + (deleteForward && row == top.MAX_RECIPIENTS) + ) { + // We're deleting backwards, but not the first row, + // or forwards on the last row: Focus previous row. + awSetFocusTo(awGetInputElement(row - 1)); + } else { + // We're deleting forwards, but not the last row, + // or backwards on the first row: Focus next row. + awSetFocusTo(awGetInputElement(row + 1)); + } + + // Delete the row. + awDeleteRow(row); +} + +function awTestRowSequence() { + /* + This function is for debug and testing purpose only, normal user should not run it! + + Every time 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 + */ + + if (!test_addresses_sequence) { + return true; + } + + // Debug code to verify the sequence is still good. + + let listbox = document.getElementById("addressingWidget"); + let listitems = listbox.itemChildren; + if (listitems.length >= top.MAX_RECIPIENTS) { + for (let i = 1; i <= listitems.length; i++) { + let item = listitems[i - 1]; + let inputID = item + .querySelector(`input[is="autocomplete-input"]`) + .id.split("#")[1]; + let menulist = item.querySelector("menulist"); + // In some places like the mailing list dialog there is no menulist, + // and so no popupID that needs to be kept in sequence. + let popupID = menulist && menulist.id.split("#")[1]; + if (inputID != i || (popupID && 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 awRemoveRow(row) { + awGetListItem(row).remove(); + awFitDummyRows(); + + top.MAX_RECIPIENTS--; +} + +function awGetNumberOfCols() { + if (gNumberOfCols == 0) { + var listbox = document.getElementById("addressingWidget"); + var listCols = listbox.getElementsByTagName("treecol"); + gNumberOfCols = listCols.length; + if (!gNumberOfCols) { + // If no cols defined, that means we have only one! + gNumberOfCols = 1; + } + } + + return gNumberOfCols; +} + +function awCreateDummyItem(aParent) { + var listbox = document.getElementById("addressingWidget"); + var item = listbox.getItemAtIndex(0); + + var titem = document.createXULElement("richlistitem"); + titem.setAttribute("_isDummyRow", "true"); + titem.setAttribute("class", "dummy-row"); + titem.style.height = item.getBoundingClientRect().height + "px"; + + for (let i = 0; i < awGetNumberOfCols(); i++) { + let cell = awCreateDummyCell(titem); + if (item.children[i].hasAttribute("style")) { + cell.setAttribute("style", item.children[i].getAttribute("style")); + } + if (item.children[i].hasAttribute("flex")) { + cell.setAttribute("flex", item.children[i].getAttribute("flex")); + } + } + + if (aParent) { + aParent.appendChild(titem); + } + + return titem; +} + +function awFitDummyRows() { + awCalcContentHeight(); + awCreateOrRemoveDummyRows(); +} + +function awCreateOrRemoveDummyRows() { + let listbox = document.getElementById("addressingWidget"); + let listboxHeight = listbox.getBoundingClientRect().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.itemChildren; + + 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].getBoundingClientRect().height; + ++i; + } while (i < items.length && !gAWRowHeight); + gAWContentHeight = gAWRowHeight * items.length; + } +} + +/* ::::::::::: addressing widget dummy rows ::::::::::::::::: */ + +function awCreateDummyCell(aParent) { + var cell = document.createXULElement("hbox"); + 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]"); +} + +/** + * Set focus to the specified element, typically a recipient input element. + * We do this asynchronously to allow other processes like adding or removing rows + * to complete before shifting focus. + * + * @param {Element} 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(); +} + +// returns null if abURI is not a mailing list URI +function GetParentDirectoryFromMailingListURI(abURI) { + var abURIArr = abURI.split("/"); + /* + Turn "jsaddrbook://abook.sqlite/MailList6" + into ["jsaddrbook:","","abook.sqlite","MailList6"], + then into "jsaddrbook://abook.sqlite". + + Turn "moz-aboutlookdirectory:///<top dir ID>/<ML dir ID>" + into ["moz-aboutlookdirectory:","","","<top dir ID>","<ML dir ID>"], + and then into: "moz-aboutlookdirectory:///<top dir ID>". + */ + if ( + abURIArr.length == 4 && + ["jsaddrbook:", "moz-abmdbdirectory:"].includes(abURIArr[0]) && + abURIArr[3] != "" + ) { + return abURIArr[0] + "//" + abURIArr[2]; + } else if ( + abURIArr.length == 5 && + abURIArr[0] == "moz-aboutlookdirectory:" && + abURIArr[4] != "" + ) { + return abURIArr[0] + "///" + abURIArr[3]; + } + + return null; +} diff --git a/comm/mailnews/addrbook/content/abResultsPane.js b/comm/mailnews/addrbook/content/abResultsPane.js new file mode 100644 index 0000000000..2a4bd99f3b --- /dev/null +++ b/comm/mailnews/addrbook/content/abResultsPane.js @@ -0,0 +1,482 @@ +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 ; js-indent-level: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 ../../../mail/base/content/utilityOverlay.js */ +/* import-globals-from ../../../mail/components/addrbook/content/abCommon.js */ +/* import-globals-from abView.js */ + +/** + * Use of items in this file require: + * + * AbResultsPaneDoubleClick(card) + * Is called when the results pane is double-clicked, with the clicked card. + * GetAbViewListener() + * Called when creating a new view + */ +/* globals AbResultsPaneDoubleClick, GetAbViewListener */ // abContactsPane.js or abSearchDialog.js + +var kDefaultSortColumn = "GeneratedName"; + +// List/card selections in the results pane. +var kNothingSelected = 0; +var kListsAndCards = 1; +var kMultipleListsOnly = 2; +var kSingleListOnly = 3; +var kCardsOnly = 4; + +// Global Variables + +// Holds a reference to the "abResultsTree" document element. Initially +// set up by SetAbView. +var gAbResultsTree = null; +// gAbView is the current value of gAbResultsTree.view, without passing +// through XPCOM, so we can access extra functions if necessary. +var gAbView = null; + +function SetAbView(aURI, aSearchQuery, aSearchString) { + // If we don't have a URI, just clear the view and leave everything else + // alone. + if (!aURI) { + if (gAbView) { + CloseAbView(); + } + return; + } + + // If we do have a URI, we want to allow updating the review even if the + // URI is the same, as the search results may be different. + + var sortColumn = kDefaultSortColumn; + var sortDirection = kDefaultAscending; + + if (!gAbResultsTree) { + gAbResultsTree = document.getElementById("abResultsTree"); + gAbResultsTree.controllers.appendController(ResultsPaneController); + } + + if (gAbView) { + sortColumn = gAbView.sortColumn; + sortDirection = gAbView.sortDirection; + } else { + if (gAbResultsTree.hasAttribute("sortCol")) { + sortColumn = gAbResultsTree.getAttribute("sortCol"); + } + var sortColumnNode = document.getElementById(sortColumn); + if (sortColumnNode && sortColumnNode.hasAttribute("sortDirection")) { + sortDirection = sortColumnNode.getAttribute("sortDirection"); + } + } + + gAbView = gAbResultsTree.view = new ABView( + GetDirectoryFromURI(aURI), + aSearchQuery, + aSearchString, + GetAbViewListener(), + sortColumn, + sortDirection + ).QueryInterface(Ci.nsITreeView); + window.dispatchEvent(new CustomEvent("viewchange")); + + UpdateSortIndicators(sortColumn, sortDirection); + + // If the selected address book is LDAP and the search box is empty, + // inform the user of the empty results pane. + let abResultsTree = document.getElementById("abResultsTree"); + let cardViewOuterBox = document.getElementById("CardViewOuterBox"); + let blankResultsPaneMessageBox = document.getElementById( + "blankResultsPaneMessageBox" + ); + if (aURI.startsWith("moz-abldapdirectory://") && !aSearchQuery) { + if (abResultsTree) { + abResultsTree.hidden = true; + } + if (cardViewOuterBox) { + cardViewOuterBox.hidden = true; + } + if (blankResultsPaneMessageBox) { + blankResultsPaneMessageBox.hidden = false; + } + } else { + if (abResultsTree) { + abResultsTree.hidden = false; + } + if (cardViewOuterBox) { + cardViewOuterBox.hidden = false; + } + if (blankResultsPaneMessageBox) { + blankResultsPaneMessageBox.hidden = true; + } + } +} + +function CloseAbView() { + gAbView = null; + if (gAbResultsTree) { + gAbResultsTree.view = null; + } +} + +function GetSelectedAddresses() { + return GetAddressesForCards(GetSelectedAbCards()); +} + +function GetNumSelectedCards() { + try { + return gAbView.selection.count; + } catch (ex) {} + + // if something went wrong, return 0 for the count. + return 0; +} + +function GetSelectedCardTypes() { + var cards = GetSelectedAbCards(); + if (!cards) { + console.error("ERROR: GetSelectedCardTypes: |cards| is null."); + return kNothingSelected; // no view + } + var count = cards.length; + if (count == 0) { + // Nothing selected. + return kNothingSelected; + } + + var mailingListCnt = 0; + var cardCnt = 0; + for (let i = 0; i < count; i++) { + // We can assume no values from GetSelectedAbCards will be null. + if (cards[i].isMailList) { + mailingListCnt++; + } else { + cardCnt++; + } + } + + if (mailingListCnt == 0) { + return kCardsOnly; + } + if (cardCnt > 0) { + return kListsAndCards; + } + if (mailingListCnt == 1) { + return kSingleListOnly; + } + return kMultipleListsOnly; +} + +// NOTE, will return -1 if more than one card selected, or no cards selected. +function GetSelectedCardIndex() { + if (!gAbView) { + return -1; + } + + var treeSelection = gAbView.selection; + if (treeSelection.getRangeCount() == 1) { + var start = {}; + var end = {}; + treeSelection.getRangeAt(0, start, end); + if (start.value == end.value) { + return start.value; + } + } + + return -1; +} + +// NOTE, returns the card if exactly one card is selected, null otherwise +function GetSelectedCard() { + var index = GetSelectedCardIndex(); + return index == -1 ? null : gAbView.getCardFromRow(index); +} + +/** + * Return a (possibly empty) list of cards + * + * It pushes only non-null/empty element, if any, into the returned list. + */ +function GetSelectedAbCards() { + var abView = gAbView; + + if (!abView?.selection) { + return []; + } + + let cards = []; + var count = abView.selection.getRangeCount(); + for (let i = 0; i < count; ++i) { + let start = {}; + let end = {}; + + abView.selection.getRangeAt(i, start, end); + + for (let j = start.value; j <= end.value; ++j) { + // avoid inserting null element into the list. GetRangeAt() may be buggy. + let tmp = abView.getCardFromRow(j); + if (tmp) { + cards.push(tmp); + } + } + } + return cards; +} + +// XXX todo +// an optimization might be to make this return +// the selected ranges, which would be faster +// when the user does large selections, but for now, let's keep it simple. +function GetSelectedRows() { + var selectedRows = ""; + + if (!gAbView) { + return selectedRows; + } + + var rangeCount = gAbView.selection.getRangeCount(); + for (let i = 0; i < rangeCount; ++i) { + var start = {}; + var end = {}; + gAbView.selection.getRangeAt(i, start, end); + for (let j = start.value; j <= end.value; ++j) { + if (selectedRows) { + selectedRows += ","; + } + selectedRows += j; + } + } + + return selectedRows; +} + +function AbResultsPaneOnClick(event) { + // we only care about button 0 (left click) events + if (event.button != 0) { + return; + } + + // all we need to worry about here is double clicks + // and column header clicks. + // + // we get in here for clicks on the "treecol" (headers) + // and the "scrollbarbutton" (scrollbar buttons) + // we don't want those events to cause a "double click" + + var t = event.target; + + if (t.localName == "treecol") { + var sortDirection; + var currentDirection = t.getAttribute("sortDirection"); + + // Revert the sort order. If none is set, use Ascending. + sortDirection = + currentDirection == kDefaultAscending + ? kDefaultDescending + : kDefaultAscending; + + SortAndUpdateIndicators(t.id, sortDirection); + } else if (t.localName == "treechildren") { + // figure out what row the click was in + var row = gAbResultsTree.getRowAt(event.clientX, event.clientY); + if (row == -1) { + return; + } + + if (event.detail == 2) { + AbResultsPaneDoubleClick(gAbView.getCardFromRow(row)); + } + } +} + +function SortAndUpdateIndicators(sortColumn, sortDirection) { + UpdateSortIndicators(sortColumn, sortDirection); + + if (gAbView) { + gAbView.sortBy(sortColumn, sortDirection); + } +} + +function UpdateSortIndicators(colID, sortDirection) { + var sortedColumn = null; + + // set the sort indicator on the column we are sorted by + if (colID) { + sortedColumn = document.getElementById(colID); + if (sortedColumn) { + sortedColumn.setAttribute("sortDirection", sortDirection); + gAbResultsTree.setAttribute("sortCol", colID); + } + } + + // remove the sort indicator from all the columns + // except the one we are sorted by + var currCol = gAbResultsTree.firstElementChild.firstElementChild; + while (currCol) { + if (currCol != sortedColumn && currCol.localName == "treecol") { + currCol.removeAttribute("sortDirection"); + } + currCol = currCol.nextElementSibling; + } +} + +// Controller object for Results Pane +var ResultsPaneController = { + supportsCommand(command) { + switch (command) { + case "cmd_selectAll": + case "cmd_delete": + case "button_delete": + case "cmd_print": + case "cmd_printcard": + return true; + default: + return false; + } + }, + + isCommandEnabled(command) { + switch (command) { + case "cmd_selectAll": + return true; + case "cmd_delete": + case "button_delete": { + let numSelected; + let enabled = false; + if (gAbView && gAbView.selection) { + if (gAbView.directory) { + enabled = !gAbView.directory.readOnly; + } else { + enabled = true; + } + numSelected = gAbView.selection.count; + } else { + numSelected = 0; + } + enabled = enabled && numSelected > 0; + + if (enabled && !gAbView?.directory) { + // Undefined gAbView.directory means "All Address Books" is selected. + // Disable the menu/button if any selected card is from a read only + // directory. + enabled = !GetSelectedAbCards().some( + card => + MailServices.ab.getDirectoryFromUID(card.directoryUID).readOnly + ); + } + + if (command == "cmd_delete") { + switch (GetSelectedCardTypes()) { + case kSingleListOnly: + updateDeleteControls("valueList"); + break; + case kMultipleListsOnly: + updateDeleteControls("valueLists"); + break; + case kListsAndCards: + updateDeleteControls("valueItems"); + break; + case kCardsOnly: + default: + updateDeleteControls( + numSelected < 2 ? "valueCard" : "valueCards" + ); + } + } + return enabled; + } + case "cmd_print": + // cmd_print is currently only used in SeaMonkey. + // Prevent printing when we don't have an opener (browserDOMWindow is + // null). + let enabled = window.browserDOMWindow && GetNumSelectedCards() > 0; + document.querySelectorAll("[command=cmd_print]").forEach(e => { + e.disabled = !enabled; + }); + return enabled; + case "cmd_printcard": + // Prevent printing when we don't have an opener (browserDOMWindow is + // null). + return window.browserDOMWindow && GetNumSelectedCards() > 0; + default: + return false; + } + }, + + doCommand(command) { + switch (command) { + case "cmd_selectAll": + if (gAbView) { + gAbView.selection.selectAll(); + } + break; + case "cmd_delete": + case "button_delete": + AbDelete(); + break; + } + }, +}; + +function updateDeleteControls( + labelAttribute, + accessKeyAttribute = "accesskeyDefault" +) { + goSetMenuValue("cmd_delete", labelAttribute); + goSetAccessKey("cmd_delete", accessKeyAttribute); + + // The toolbar button doesn't update itself from the command. Do that now. + let button = document.getElementById("button-abdelete"); + if (!button) { + return; + } + + let command = document.getElementById("cmd_delete"); + button.label = command.getAttribute("label"); + button.setAttribute( + "tooltiptext", + button.getAttribute( + labelAttribute == "valueCardDAV" ? "tooltipCardDAV" : "tooltipDefault" + ) + ); +} + +/** + * Generate a comma separated list of addresses from the given cards. + * + * @param {nsIAbCard[]} cards - The cards to get addresses for. + * @returns {string} A string of comma separated mailboxes. + */ +function GetAddressesForCards(cards) { + if (!cards) { + return ""; + } + + return cards + .map(makeMimeAddressFromCard) + .filter(addr => addr) + .join(","); +} + +/** + * Make a MIME encoded string output of the card. This will make a difference + * e.g. in scenarios where non-ASCII is used in the mailbox, or when then + * display name include special characters such as comma. + * + * @param {nsIAbCard} card - The card to use. + * @returns {string} A MIME encoded mailbox representation of the card. + */ +function makeMimeAddressFromCard(card) { + if (!card) { + return ""; + } + + let email; + if (card.isMailList) { + let directory = GetDirectoryFromURI(card.mailListURI); + email = directory.description || card.displayName; + } else { + email = card.emailAddresses[0]; + } + return MailServices.headerParser.makeMimeAddress(card.displayName, email); +} diff --git a/comm/mailnews/addrbook/content/abView.js b/comm/mailnews/addrbook/content/abView.js new file mode 100644 index 0000000000..f5acb7d11c --- /dev/null +++ b/comm/mailnews/addrbook/content/abView.js @@ -0,0 +1,539 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals MailServices, PROTO_TREE_VIEW, Services */ + +function ABView( + directory, + searchQuery, + searchString, + listener, + sortColumn, + sortDirection +) { + this.__proto__.__proto__ = new PROTO_TREE_VIEW(); + this.directory = directory; + this.listener = listener; + + let directories = directory ? [directory] : MailServices.ab.directories; + if (searchQuery) { + searchQuery = searchQuery.replace(/^\?+/, ""); + for (let dir of directories) { + dir.search(searchQuery, searchString, this); + } + } else { + for (let dir of directories) { + for (let card of dir.childCards) { + this._rowMap.push(new abViewCard(card, dir)); + } + } + if (this.listener) { + this.listener.onCountChanged(this.rowCount); + } + } + this.sortBy(sortColumn, sortDirection); +} +ABView.nameFormat = Services.prefs.getIntPref( + "mail.addr_book.lastnamefirst", + 0 +); +ABView.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsITreeView", + "nsIAbDirSearchListener", + "nsIObserver", + "nsISupportsWeakReference", + ]), + + directory: null, + listener: null, + _notifications: [ + "addrbook-directory-deleted", + "addrbook-directory-invalidated", + "addrbook-contact-created", + "addrbook-contact-updated", + "addrbook-contact-deleted", + "addrbook-list-created", + "addrbook-list-updated", + "addrbook-list-deleted", + "addrbook-list-member-added", + "addrbook-list-member-removed", + ], + + sortColumn: "", + sortDirection: "", + collator: new Intl.Collator(undefined, { numeric: true }), + + deleteSelectedCards() { + let directoryMap = new Map(); + for (let i = 0; i < this.selection.getRangeCount(); i++) { + let start = {}; + let finish = {}; + this.selection.getRangeAt(i, start, finish); + for (let j = start.value; j <= finish.value; j++) { + let card = this.getCardFromRow(j); + let cardSet = directoryMap.get(card.directoryUID); + if (!cardSet) { + cardSet = new Set(); + directoryMap.set(card.directoryUID, cardSet); + } + cardSet.add(card); + } + } + + for (let [directoryUID, cardSet] of directoryMap) { + let directory; + if (this.directory && this.directory.isMailList) { + // Removes cards from the list instead of deleting them. + directory = this.directory; + } else { + directory = MailServices.ab.getDirectoryFromUID(directoryUID); + } + + cardSet = [...cardSet]; + directory.deleteCards(cardSet.filter(card => !card.isMailList)); + for (let card of cardSet.filter(card => card.isMailList)) { + MailServices.ab.deleteAddressBook(card.mailListURI); + } + } + }, + getCardFromRow(row) { + return this._rowMap[row] ? this._rowMap[row].card : null; + }, + getDirectoryFromRow(row) { + return this._rowMap[row] ? this._rowMap[row].directory : null; + }, + sortBy(sortColumn, sortDirection, resort) { + // Remember what was selected. + let selection = this.selection; + if (selection) { + for (let i = 0; i < this._rowMap.length; i++) { + this._rowMap[i].wasSelected = selection.isSelected(i); + this._rowMap[i].wasCurrent = selection.currentIndex == i; + } + } + + // Do the sort. + if (sortColumn == this.sortColumn && !resort) { + if (sortDirection == this.sortDirection) { + return; + } + this._rowMap.reverse(); + } else { + this._rowMap.sort((a, b) => { + let aText = a.getText(sortColumn); + let bText = b.getText(sortColumn); + if (sortDirection == "descending") { + return this.collator.compare(bText, aText); + } + return this.collator.compare(aText, bText); + }); + } + + // Restore what was selected. + if (selection) { + selection.selectEventsSuppressed = true; + for (let i = 0; i < this._rowMap.length; i++) { + if (this._rowMap[i].wasSelected != selection.isSelected(i)) { + selection.toggleSelect(i); + } + } + // Can't do this until updating the selection is finished. + for (let i = 0; i < this._rowMap.length; i++) { + if (this._rowMap[i].wasCurrent) { + selection.currentIndex = i; + break; + } + } + this.selectionChanged(); + selection.selectEventsSuppressed = false; + } + + if (this.tree) { + this.tree.invalidate(); + } + this.sortColumn = sortColumn; + this.sortDirection = sortDirection; + }, + + // nsITreeView + + selectionChanged() { + if (this.listener) { + this.listener.onSelectionChanged(); + } + }, + setTree(tree) { + this.tree = tree; + for (let topic of this._notifications) { + if (tree) { + Services.obs.addObserver(this, topic, true); + } else { + try { + Services.obs.removeObserver(this, topic); + } catch (ex) { + // `this` might not be a valid observer. + } + } + } + Services.prefs.addObserver("mail.addr_book.lastnamefirst", this, true); + }, + + // nsIAbDirSearchListener + + onSearchFoundCard(card) { + // Instead of duplicating the insertion code below, just call it. + this.observe(card, "addrbook-contact-created", this.directory?.UID); + }, + onSearchFinished(status, complete, secInfo, location) { + // Special handling for Bad Cert errors. + let offerCertException = false; + try { + // If code is not an NSS error, getErrorClass() will fail. + let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService( + Ci.nsINSSErrorsService + ); + let errorClass = nssErrorsService.getErrorClass(status); + if (errorClass == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) { + offerCertException = true; + } + } catch (ex) {} + + if (offerCertException) { + // Give the user the option of adding an exception for the bad cert. + let params = { + exceptionAdded: false, + securityInfo: secInfo, + prefetchCert: true, + location, + }; + window.openDialog( + "chrome://pippki/content/exceptionDialog.xhtml", + "", + "chrome,centerscreen,modal", + params + ); + // params.exceptionAdded will be set if the user added an exception. + } + }, + + // nsIObserver + + observe(subject, topic, data) { + if (topic == "nsPref:changed") { + ABView.nameFormat = Services.prefs.getIntPref( + "mail.addr_book.lastnamefirst", + 0 + ); + for (let card of this._rowMap) { + delete card._getTextCache.GeneratedName; + } + if (this.tree) { + if (this.sortColumn == "GeneratedName") { + this.sortBy(this.sortColumn, this.sortDirection, true); + } else { + this.tree.invalidate(this.tree.columns.GeneratedName); + } + } + return; + } + + if (this.directory && data && this.directory.UID != data) { + return; + } + + // If we make it here, we're in the root directory, or the right directory. + + switch (topic) { + case "addrbook-directory-deleted": { + if (this.directory) { + break; + } + + subject.QueryInterface(Ci.nsIAbDirectory); + let scrollPosition = this.tree?.getFirstVisibleRow(); + for (let i = this._rowMap.length - 1; i >= 0; i--) { + if (this._rowMap[i].directory.UID == subject.UID) { + this._rowMap.splice(i, 1); + if (this.tree) { + this.tree.rowCountChanged(i, -1); + } + } + } + if (this.listener) { + this.listener.onCountChanged(this.rowCount); + } + if (this.tree && scrollPosition !== null) { + this.tree.scrollToRow(scrollPosition); + } + break; + } + case "addrbook-directory-invalidated": + subject.QueryInterface(Ci.nsIAbDirectory); + if (subject == this.directory) { + this._rowMap.length = 0; + for (let card of this.directory.childCards) { + this._rowMap.push(new abViewCard(card, this.directory)); + } + this.sortBy(this.sortColumn, this.sortDirection, true); + if (this.listener) { + this.listener.onCountChanged(this.rowCount); + } + } + break; + case "addrbook-list-created": { + let parentDir = MailServices.ab.getDirectoryFromUID(data); + // `subject` is an nsIAbDirectory, make it the matching card instead. + subject.QueryInterface(Ci.nsIAbDirectory); + for (let card of parentDir.childCards) { + if (card.UID == subject.UID) { + subject = card; + break; + } + } + } + // Falls through. + case "addrbook-list-member-added": + case "addrbook-contact-created": + if (topic == "addrbook-list-member-added" && !this.directory) { + break; + } + + subject.QueryInterface(Ci.nsIAbCard); + let viewCard = new abViewCard(subject); + let sortText = viewCard.getText(this.sortColumn); + let addIndex = null; + for (let i = 0; addIndex === null && i < this._rowMap.length; i++) { + let comparison = this.collator.compare( + sortText, + this._rowMap[i].getText(this.sortColumn) + ); + if ( + (comparison < 0 && this.sortDirection == "ascending") || + (comparison >= 0 && this.sortDirection == "descending") + ) { + addIndex = i; + } + } + if (addIndex === null) { + addIndex = this._rowMap.length; + } + this._rowMap.splice(addIndex, 0, viewCard); + if (this.tree) { + this.tree.rowCountChanged(addIndex, 1); + } + if (this.listener) { + this.listener.onCountChanged(this.rowCount); + } + break; + + case "addrbook-list-updated": { + let parentDir = this.directory; + if (!parentDir) { + parentDir = MailServices.ab.getDirectoryFromUID(data); + } + // `subject` is an nsIAbDirectory, make it the matching card instead. + subject.QueryInterface(Ci.nsIAbDirectory); + for (let card of parentDir.childCards) { + if (card.UID == subject.UID) { + subject = card; + break; + } + } + } + // Falls through. + case "addrbook-contact-updated": { + subject.QueryInterface(Ci.nsIAbCard); + let needsSort = false; + for (let i = this._rowMap.length - 1; i >= 0; i--) { + if ( + this._rowMap[i].card.equals(subject) && + this._rowMap[i].card.directoryUID == subject.directoryUID + ) { + this._rowMap.splice(i, 1, new abViewCard(subject)); + needsSort = true; + } + } + if (needsSort) { + this.sortBy(this.sortColumn, this.sortDirection, true); + } + break; + } + + case "addrbook-list-deleted": { + subject.QueryInterface(Ci.nsIAbDirectory); + let scrollPosition = this.tree?.getFirstVisibleRow(); + for (let i = this._rowMap.length - 1; i >= 0; i--) { + if (this._rowMap[i].card.UID == subject.UID) { + this._rowMap.splice(i, 1); + if (this.tree) { + this.tree.rowCountChanged(i, -1); + } + } + } + if (this.listener) { + this.listener.onCountChanged(this.rowCount); + } + if (this.tree && scrollPosition !== null) { + this.tree.scrollToRow(scrollPosition); + } + break; + } + case "addrbook-list-member-removed": + if (!this.directory) { + break; + } + // Falls through. + case "addrbook-contact-deleted": { + subject.QueryInterface(Ci.nsIAbCard); + let scrollPosition = this.tree?.getFirstVisibleRow(); + for (let i = this._rowMap.length - 1; i >= 0; i--) { + if ( + this._rowMap[i].card.equals(subject) && + this._rowMap[i].card.directoryUID == subject.directoryUID + ) { + this._rowMap.splice(i, 1); + if (this.tree) { + this.tree.rowCountChanged(i, -1); + } + } + } + if (this.listener) { + this.listener.onCountChanged(this.rowCount); + } + if (this.tree && scrollPosition !== null) { + this.tree.scrollToRow(scrollPosition); + } + break; + } + } + }, +}; + +/** + * Representation of a card, used as a table row in ABView. + * + * @param {nsIAbCard} card - contact or mailing list card for this row. + * @param {nsIAbDirectory} [directoryHint] - the directory containing card, + * if available (this is a performance optimization only). + */ +function abViewCard(card, directoryHint) { + this.card = card; + this._getTextCache = {}; + if (directoryHint) { + this._directory = directoryHint; + } else { + this._directory = MailServices.ab.getDirectoryFromUID( + this.card.directoryUID + ); + } +} +abViewCard.listFormatter = new Services.intl.ListFormat( + Services.appinfo.name == "xpcshell" ? "en-US" : undefined, + { type: "unit" } +); +abViewCard.prototype = { + _getText(columnID) { + try { + let { getProperty, supportsVCard, vCardProperties } = this.card; + + if (this.card.isMailList) { + if (columnID == "GeneratedName") { + return this.card.displayName; + } + if (["NickName", "Notes"].includes(columnID)) { + return getProperty(columnID, ""); + } + return ""; + } + + switch (columnID) { + case "addrbook": + case "Addrbook": + return this._directory.dirName; + case "GeneratedName": + return this.card.generateName(ABView.nameFormat); + case "_PhoneticName": + return this.card.generatePhoneticName(true); + case "ChatName": + return this.card.isMailList ? "" : this.card.generateChatName(); + case "EmailAddresses": + return abViewCard.listFormatter.format(this.card.emailAddresses); + case "PhoneNumbers": { + let phoneNumbers; + if (supportsVCard) { + phoneNumbers = vCardProperties.getAllValues("tel"); + } else { + phoneNumbers = [ + getProperty("WorkPhone", ""), + getProperty("HomePhone", ""), + getProperty("CellularNumber", ""), + getProperty("FaxNumber", ""), + getProperty("PagerNumber", ""), + ]; + } + return abViewCard.listFormatter.format(phoneNumbers.filter(Boolean)); + } + case "JobTitle": + case "Title": + if (supportsVCard) { + return vCardProperties.getFirstValue("title"); + } + return getProperty("JobTitle", ""); + case "Department": + if (supportsVCard) { + let vCardValue = vCardProperties.getFirstValue("org"); + if (Array.isArray(vCardValue)) { + return vCardValue[1] || ""; + } + return ""; + } + return getProperty(columnID, ""); + case "Company": + case "Organization": + if (supportsVCard) { + let vCardValue = vCardProperties.getFirstValue("org"); + if (Array.isArray(vCardValue)) { + return vCardValue[0] || ""; + } + return vCardValue; + } + return getProperty("Company", ""); + case "NickName": + if (supportsVCard) { + return vCardProperties.getFirstValue("nickname"); + } + return getProperty(columnID, ""); + default: + return getProperty(columnID, ""); + } + } catch (ex) { + return ""; + } + }, + getText(columnID) { + if (!(columnID in this._getTextCache)) { + this._getTextCache[columnID] = this._getText(columnID)?.trim() ?? ""; + } + return this._getTextCache[columnID]; + }, + get id() { + return this.card.UID; + }, + get open() { + return false; + }, + get level() { + return 0; + }, + get children() { + return []; + }, + getProperties() { + return this.card.isMailList ? "MailList" : ""; + }, + get directory() { + return this._directory; + }, +}; diff --git a/comm/mailnews/addrbook/content/map-list.js b/comm/mailnews/addrbook/content/map-list.js new file mode 100644 index 0000000000..102cb09522 --- /dev/null +++ b/comm/mailnews/addrbook/content/map-list.js @@ -0,0 +1,217 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* global MozElements */ + +// Wrap in a block to prevent leaking to window scope. +{ + /** + * The MozMapList widget behaves as a popup menu showing available map options + * for an address. It is a part of the card view in the addressbook. + * + * @augments {MozElements.MozMenuPopup} + */ + class MozMapList extends MozElements.MozMenuPopup { + connectedCallback() { + if (this.delayConnectedCallback() || this.hasConnected) { + return; + } + this.setAttribute("is", "map-list"); + + this.addEventListener("command", event => { + this._chooseMapService(event.target); + event.stopPropagation(); + }); + + this.addEventListener("popupshowing", event => { + this._listMapServices(); + }); + + this._setWidgetDisabled(true); + } + + get mapURL() { + return this._createMapItURL(); + } + + /** + * Initializes the necessary address data from an addressbook card. + * + * @param {nsIAbCard} card - The card to get the address data from. + * @param {string} addrPrefix - Card property prefix: "Home" or "Work", + * to make the map use either HomeAddress or WorkAddress. + */ + initMapAddressFromCard(card, addrPrefix) { + let mapItURLFormat = this._getMapURLPref(); + let doNotShowMap = !mapItURLFormat || !addrPrefix || !card; + this._setWidgetDisabled(doNotShowMap); + if (doNotShowMap) { + return; + } + + this.address1 = card.getProperty(addrPrefix + "Address"); + this.address2 = card.getProperty(addrPrefix + "Address2"); + this.city = card.getProperty(addrPrefix + "City"); + this._state = card.getProperty(addrPrefix + "State"); + this.zip = card.getProperty(addrPrefix + "ZipCode"); + this.country = card.getProperty(addrPrefix + "Country"); + } + + /** + * Sets the disabled/enabled state of the parent widget (e.g. a button). + */ + _setWidgetDisabled(disabled) { + this.parentNode.disabled = disabled; + } + + /** + * Returns the Map service URL from localized pref. Returns null if there + * is none at the given index. + * + * @param {integer} [index=0] - The index of the service to return. + * 0 is the default service. + */ + _getMapURLPref(index = 0) { + let url = null; + if (!index) { + url = Services.prefs.getComplexValue( + "mail.addr_book.mapit_url.format", + Ci.nsIPrefLocalizedString + ).data; + } else { + try { + url = Services.prefs.getComplexValue( + "mail.addr_book.mapit_url." + index + ".format", + Ci.nsIPrefLocalizedString + ).data; + } catch (e) {} + } + + return url; + } + + /** + * Builds menuitem elements representing map services defined in prefs + * and attaches them to the specified button. + */ + _listMapServices() { + let index = 1; + let itemFound = true; + let defaultFound = false; + const kUserIndex = 100; + let mapList = this; + while (mapList.hasChildNodes()) { + mapList.lastChild.remove(); + } + + let defaultUrl = this._getMapURLPref(); + + // Creates the menuitem with supplied data. + function addMapService(url, name) { + let item = document.createXULElement("menuitem"); + item.setAttribute("url", url); + item.setAttribute("label", name); + item.setAttribute("type", "radio"); + item.setAttribute("name", "mapit_service"); + if (url == defaultUrl) { + item.setAttribute("checked", "true"); + } + mapList.appendChild(item); + } + + // Generates a useful generic name by cutting out only the host address. + function generateName(url) { + return new URL(url).hostname; + } + + // Add all defined map services as menuitems. + while (itemFound) { + let urlName; + let urlTemplate = this._getMapURLPref(index); + if (!urlTemplate) { + itemFound = false; + } else { + // Name is not mandatory, generate one if not found. + try { + urlName = Services.prefs.getComplexValue( + "mail.addr_book.mapit_url." + index + ".name", + Ci.nsIPrefLocalizedString + ).data; + } catch (e) { + urlName = generateName(urlTemplate); + } + } + if (itemFound) { + addMapService(urlTemplate, urlName); + index++; + if (urlTemplate == defaultUrl) { + defaultFound = true; + } + } else if (index < kUserIndex) { + // After iterating the base region provided urls, check for user defined ones. + index = kUserIndex; + itemFound = true; + } + } + if (!defaultFound) { + // If user had put a customized map URL into mail.addr_book.mapit_url.format + // preserve it as a new map service named with the URL. + // 'index' now points to the first unused entry in prefs. + let defaultName = generateName(defaultUrl); + addMapService(defaultUrl, defaultName); + Services.prefs.setCharPref( + "mail.addr_book.mapit_url." + index + ".format", + defaultUrl + ); + Services.prefs.setCharPref( + "mail.addr_book.mapit_url." + index + ".name", + defaultName + ); + } + } + + /** + * Save user selected mapping service. + * + * @param {Element} item - The chosen menuitem with map service. + */ + _chooseMapService(item) { + // Save selected URL as the default. + let defaultUrl = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ); + defaultUrl.data = item.getAttribute("url"); + Services.prefs.setComplexValue( + "mail.addr_book.mapit_url.format", + Ci.nsIPrefLocalizedString, + defaultUrl + ); + } + + /** + * Generate the map URL used to open the link on clicking the menulist button. + * + * @returns {urlFormat} - the map url generated from the address. + */ + _createMapItURL() { + let urlFormat = this._getMapURLPref(); + if (!urlFormat) { + return null; + } + + urlFormat = urlFormat.replace("@A1", encodeURIComponent(this.address1)); + urlFormat = urlFormat.replace("@A2", encodeURIComponent(this.address2)); + urlFormat = urlFormat.replace("@CI", encodeURIComponent(this.city)); + urlFormat = urlFormat.replace("@ST", encodeURIComponent(this._state)); + urlFormat = urlFormat.replace("@ZI", encodeURIComponent(this.zip)); + urlFormat = urlFormat.replace("@CO", encodeURIComponent(this.country)); + + return urlFormat; + } + } + + customElements.define("map-list", MozMapList, { extends: "menupopup" }); +} diff --git a/comm/mailnews/addrbook/modules/AddrBookCard.jsm b/comm/mailnews/addrbook/modules/AddrBookCard.jsm new file mode 100644 index 0000000000..23f387f921 --- /dev/null +++ b/comm/mailnews/addrbook/modules/AddrBookCard.jsm @@ -0,0 +1,481 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["AddrBookCard"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + BANISHED_PROPERTIES: "resource:///modules/VCardUtils.jsm", + newUID: "resource:///modules/AddrBookUtils.jsm", + VCardProperties: "resource:///modules/VCardUtils.jsm", + VCardPropertyEntry: "resource:///modules/VCardUtils.jsm", +}); + +/** + * Prototype for nsIAbCard objects that are not mailing lists. + * + * @implements {nsIAbCard} + */ +function AddrBookCard() { + this._directoryUID = ""; + this._properties = new Map([ + ["PopularityIndex", 0], + ["LastModifiedDate", 0], + ]); + + this._hasVCard = false; + XPCOMUtils.defineLazyGetter(this, "_vCardProperties", () => { + // Lazy creation of the VCardProperties object. Change the `_properties` + // object as much as you like (e.g. loading in properties from a database) + // before running this code. After it runs, the `_vCardProperties` object + // takes over and anything in `_properties` which could be stored in the + // vCard will be ignored! + + this._hasVCard = true; + + let vCard = this.getProperty("_vCard", ""); + try { + if (vCard) { + let vCardProperties = lazy.VCardProperties.fromVCard(vCard, { + isGoogleCardDAV: this._isGoogleCardDAV, + }); + // Custom1..4 properties could still exist as nsIAbCard properties. + // Migrate them now. + for (let key of ["Custom1", "Custom2", "Custom3", "Custom4"]) { + let value = this.getProperty(key, ""); + if ( + value && + vCardProperties.getFirstEntry(`x-${key.toLowerCase()}`) === null + ) { + vCardProperties.addEntry( + new lazy.VCardPropertyEntry( + `x-${key.toLowerCase()}`, + {}, + "text", + value + ) + ); + } + this.deleteProperty(key); + } + return vCardProperties; + } + return lazy.VCardProperties.fromPropertyMap(this._properties); + } catch (error) { + console.error("Error creating vCard properties", error); + // Return an empty VCardProperties object if parsing failed + // catastrophically. + return new lazy.VCardProperties("4.0"); + } + }); +} + +AddrBookCard.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIAbCard"]), + classID: Components.ID("{1143991d-31cd-4ea6-9c97-c587d990d724}"), + + /* nsIAbCard */ + + generateName(generateFormat, bundle) { + let result = ""; + switch (generateFormat) { + case Ci.nsIAbCard.GENERATE_DISPLAY_NAME: + result = this.displayName; + break; + + case Ci.nsIAbCard.GENERATE_LAST_FIRST_ORDER: + if (this.lastName) { + let otherNames = [ + this.prefixName, + this.firstName, + this.middleName, + this.suffixName, + ] + .filter(Boolean) + .join(" "); + if (!otherNames) { + // Only use the lastName if we don't have anything to add after the + // comma, in order to avoid for the string to finish with ", ". + result = this.lastName; + } else { + result = + bundle?.formatStringFromName("lastFirstFormat", [ + this.lastName, + otherNames, + ]) ?? `${this.lastName}, ${otherNames}`; + } + } + break; + + default: + let startNames = [this.prefixName, this.firstName, this.middleName] + .filter(Boolean) + .join(" "); + let endNames = [this.lastName, this.suffixName] + .filter(Boolean) + .join(" "); + result = + bundle?.formatStringFromName("firstLastFormat", [ + startNames, + endNames, + ]) ?? `${startNames} ${endNames}`; + break; + } + + // Remove any leftover blank spaces. + result = result.trim(); + + if (result == "" || result == ",") { + result = + this.displayName || + [ + this.prefixName, + this.firstName, + this.middleName, + this.lastName, + this.suffixName, + ] + .filter(Boolean) + .join(" ") + .trim(); + + if (!result) { + // So far we don't have anything to show as a contact name. + + if (this.primaryEmail) { + // Let's use the primary email localpart. + result = this.primaryEmail.split("@", 1)[0]; + } else { + // We don't have a primary email either, let's try with the + // organization name. + result = !this._hasVCard + ? this.getProperty("Company", "") + : this._vCardProperties.getFirstValue("org"); + } + } + } + return result || ""; + }, + get directoryUID() { + return this._directoryUID; + }, + set directoryUID(value) { + this._directoryUID = value; + }, + get UID() { + if (!this._uid) { + this._uid = lazy.newUID(); + } + return this._uid; + }, + set UID(value) { + if (this._uid && value != this._uid) { + throw Components.Exception( + `Bad UID: got ${value} != ${this.uid}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + this._uid = value; + }, + get properties() { + let props = []; + for (const [name, value] of this._properties) { + props.push({ + get name() { + return name; + }, + get value() { + return value; + }, + QueryInterface: ChromeUtils.generateQI(["nsIProperty"]), + }); + } + return props; + }, + get supportsVCard() { + return true; + }, + get vCardProperties() { + return this._vCardProperties; + }, + get firstName() { + if (!this._hasVCard) { + return this.getProperty("FirstName", ""); + } + let name = this._vCardProperties.getFirstValue("n"); + if (!Array.isArray(name)) { + return ""; + } + name = name[1]; + if (Array.isArray(name)) { + name = name.join(" "); + } + return name; + }, + set firstName(value) { + let n = this._vCardProperties.getFirstEntry("n"); + if (n) { + n.value[1] = value; + } else { + this._vCardProperties.addEntry( + new lazy.VCardPropertyEntry("n", {}, "text", ["", value, "", "", ""]) + ); + } + }, + get lastName() { + if (!this._hasVCard) { + return this.getProperty("LastName", ""); + } + let name = this._vCardProperties.getFirstValue("n"); + if (!Array.isArray(name)) { + return ""; + } + name = name[0]; + if (Array.isArray(name)) { + name = name.join(" "); + } + return name; + }, + set lastName(value) { + let n = this._vCardProperties.getFirstEntry("n"); + if (n) { + n.value[0] = value; + } else { + this._vCardProperties.addEntry( + new lazy.VCardPropertyEntry("n", {}, "text", [value, "", "", "", ""]) + ); + } + }, + get displayName() { + if (!this._hasVCard) { + return this.getProperty("DisplayName", ""); + } + return this._vCardProperties.getFirstValue("fn") || ""; + }, + set displayName(value) { + let fn = this._vCardProperties.getFirstEntry("fn"); + if (fn) { + fn.value = value; + } else { + this._vCardProperties.addEntry( + new lazy.VCardPropertyEntry("fn", {}, "text", value) + ); + } + }, + get primaryEmail() { + if (!this._hasVCard) { + return this.getProperty("PrimaryEmail", ""); + } + return this._vCardProperties.getAllValuesSorted("email")[0] ?? ""; + }, + set primaryEmail(value) { + let entries = this._vCardProperties.getAllEntriesSorted("email"); + if (entries.length && entries[0].value != value) { + this._vCardProperties.removeEntry(entries[0]); + entries.shift(); + } + + if (value) { + let existing = entries.find(e => e.value == value); + if (existing) { + existing.params.pref = "1"; + } else { + this._vCardProperties.addEntry( + new lazy.VCardPropertyEntry("email", { pref: "1" }, "text", value) + ); + } + } else if (entries.length) { + entries[0].params.pref = "1"; + } + }, + get isMailList() { + return false; + }, + get mailListURI() { + return ""; + }, + get emailAddresses() { + return this._vCardProperties.getAllValuesSorted("email"); + }, + get photoURL() { + let photoEntry = this.vCardProperties.getFirstEntry("photo"); + if (photoEntry?.value) { + if (photoEntry.value?.startsWith("data:image/")) { + // This is a version 4.0 card + // OR a version 3.0 card with the URI type set (uncommon) + // OR a version 3.0 card that is lying about its type. + return photoEntry.value; + } + if (photoEntry.type == "binary" && photoEntry.value.startsWith("iVBO")) { + // This is a version 3.0 card. + // The first 3 bytes say this image is PNG. + return `data:image/png;base64,${photoEntry.value}`; + } + if (photoEntry.type == "binary" && photoEntry.value.startsWith("/9j/")) { + // This is a version 3.0 card. + // The first 3 bytes say this image is JPEG. + return `data:image/jpeg;base64,${photoEntry.value}`; + } + if (photoEntry.type == "uri" && /^https?:\/\//.test(photoEntry.value)) { + // A remote URI. + return photoEntry.value; + } + } + + let photoName = this.getProperty("PhotoName", ""); + if (photoName) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("Photos"); + file.append(photoName); + return Services.io.newFileURI(file).spec; + } + + return ""; + }, + + getProperty(name, defaultValue) { + if (this._properties.has(name)) { + return this._properties.get(name); + } + return defaultValue; + }, + getPropertyAsAString(name) { + if (!this._properties.has(name)) { + return ""; + } + return this.getProperty(name); + }, + getPropertyAsAUTF8String(name) { + if (!this._properties.has(name)) { + throw Components.Exception(`${name} N/A`, Cr.NS_ERROR_NOT_AVAILABLE); + } + return this.getProperty(name); + }, + getPropertyAsUint32(name) { + let value = this.getProperty(name); + if (!isNaN(parseInt(value, 10))) { + return parseInt(value, 10); + } + if (!isNaN(parseInt(value, 16))) { + return parseInt(value, 16); + } + throw Components.Exception( + `${name}: ${value} - not an int`, + Cr.NS_ERROR_NOT_AVAILABLE + ); + }, + getPropertyAsBool(name, defaultValue) { + let value = this.getProperty(name); + switch (value) { + case false: + case 0: + case "0": + return false; + case true: + case 1: + case "1": + return true; + case undefined: + return defaultValue; + } + throw Components.Exception( + `${name}: ${value} - not a boolean`, + Cr.NS_ERROR_NOT_AVAILABLE + ); + }, + setProperty(name, value) { + if (lazy.BANISHED_PROPERTIES.includes(name)) { + throw new Components.Exception( + `Unable to set ${name} as a property, use vCardProperties`, + Cr.NS_ERROR_UNEXPECTED + ); + } + if ([null, undefined, ""].includes(value)) { + this._properties.delete(name); + return; + } + if (typeof value == "boolean") { + value = value ? "1" : "0"; + } + this._properties.set(name, "" + value); + }, + setPropertyAsAString(name, value) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + }, + setPropertyAsAUTF8String(name, value) { + this.setProperty(name, value); + }, + setPropertyAsUint32(name, value) { + this.setProperty(name, value); + }, + setPropertyAsBool(name, value) { + this.setProperty(name, value ? "1" : "0"); + }, + deleteProperty(name) { + this._properties.delete(name); + }, + hasEmailAddress(emailAddress) { + emailAddress = emailAddress.toLowerCase(); + return this.emailAddresses.some(e => e.toLowerCase() == emailAddress); + }, + translateTo(type) { + if (type == "vcard") { + if (!this._vCardProperties.getFirstValue("uid")) { + this._vCardProperties.addValue("uid", this.UID); + } + return encodeURIComponent(this._vCardProperties.toVCard()); + } + // Get nsAbCardProperty to do the work, the code is in C++ anyway. + let cardCopy = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + cardCopy.UID = this.UID; + cardCopy.copy(this); + return cardCopy.translateTo(type); + }, + generatePhoneticName(lastNameFirst) { + if (lastNameFirst) { + return ( + this.getProperty("PhoneticLastName", "") + + this.getProperty("PhoneticFirstName", "") + ); + } + return ( + this.getProperty("PhoneticFirstName", "") + + this.getProperty("PhoneticLastName", "") + ); + }, + generateChatName() { + for (let name of [ + "_GoogleTalk", + "_AimScreenName", + "_Yahoo", + "_Skype", + "_QQ", + "_MSN", + "_ICQ", + "_JabberId", + "_IRC", + ]) { + if (this._properties.has(name)) { + return this._properties.get(name); + } + } + return ""; + }, + copy(srcCard) { + throw Components.Exception( + "nsIAbCard.copy() not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + }, + equals(card) { + return this.UID == card.UID; + }, +}; diff --git a/comm/mailnews/addrbook/modules/AddrBookDirectory.jsm b/comm/mailnews/addrbook/modules/AddrBookDirectory.jsm new file mode 100644 index 0000000000..b35f35b147 --- /dev/null +++ b/comm/mailnews/addrbook/modules/AddrBookDirectory.jsm @@ -0,0 +1,817 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["AddrBookDirectory"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const lazy = {}; +XPCOMUtils.defineLazyModuleGetters(lazy, { + AddrBookCard: "resource:///modules/AddrBookCard.jsm", + AddrBookMailingList: "resource:///modules/AddrBookMailingList.jsm", + BANISHED_PROPERTIES: "resource:///modules/VCardUtils.jsm", + compareAddressBooks: "resource:///modules/AddrBookUtils.jsm", + newUID: "resource:///modules/AddrBookUtils.jsm", + VCardProperties: "resource:///modules/VCardUtils.jsm", +}); + +/** + * Abstract base class implementing nsIAbDirectory. + * + * @abstract + * @implements {nsIAbDirectory} + */ +class AddrBookDirectory { + QueryInterface = ChromeUtils.generateQI(["nsIAbDirectory"]); + + constructor() { + this._uid = null; + this._dirName = null; + } + + _initialized = false; + init(uri) { + if (this._initialized) { + throw new Components.Exception( + `Directory already initialized: ${uri}`, + Cr.NS_ERROR_ALREADY_INITIALIZED + ); + } + + // If this._readOnly is true, the user is prevented from making changes to + // the contacts. Subclasses may override this (for example to sync with a + // server) by setting this._overrideReadOnly to true, but must clear it + // before yielding to another thread (e.g. awaiting a Promise). + + if (this._dirPrefId) { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "_readOnly", + `${this.dirPrefId}.readOnly`, + false + ); + } + + this._initialized = true; + } + async cleanUp() { + if (!this._initialized) { + throw new Components.Exception( + "Directory not initialized", + Cr.NS_ERROR_NOT_INITIALIZED + ); + } + } + + get _prefBranch() { + if (this.__prefBranch) { + return this.__prefBranch; + } + if (!this._dirPrefId) { + throw Components.Exception("No dirPrefId!", Cr.NS_ERROR_NOT_AVAILABLE); + } + return (this.__prefBranch = Services.prefs.getBranch( + `${this._dirPrefId}.` + )); + } + /** @abstract */ + get lists() { + throw new Components.Exception( + `${this.constructor.name} does not implement lists getter.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + /** @abstract */ + get cards() { + throw new Components.Exception( + `${this.constructor.name} does not implement cards getter.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + getCard(uid) { + let card = new lazy.AddrBookCard(); + card.directoryUID = this.UID; + card._uid = uid; + card._properties = this.loadCardProperties(uid); + card._isGoogleCardDAV = this._isGoogleCardDAV; + return card.QueryInterface(Ci.nsIAbCard); + } + /** @abstract */ + loadCardProperties(uid) { + throw new Components.Exception( + `${this.constructor.name} does not implement loadCardProperties.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + /** @abstract */ + saveCardProperties(uid, properties) { + throw new Components.Exception( + `${this.constructor.name} does not implement saveCardProperties.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + /** @abstract */ + deleteCard(uid) { + throw new Components.Exception( + `${this.constructor.name} does not implement deleteCard.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + /** @abstract */ + saveList(list) { + throw new Components.Exception( + `${this.constructor.name} does not implement saveList.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + /** @abstract */ + deleteList(uid) { + throw new Components.Exception( + `${this.constructor.name} does not implement deleteList.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + /** + * Create a Map of the properties to record when saving `card`, including + * any changes we want to make just before saving. + * + * @param {nsIAbCard} card + * @param {?string} uid + * @returns {Map<string, string>} + */ + prepareToSaveCard(card, uid) { + let propertyMap = new Map( + Array.from(card.properties, p => [p.name, p.value]) + ); + let newProperties = new Map(); + + // Get a VCardProperties object for the card. + let vCardProperties; + if (card.supportsVCard) { + vCardProperties = card.vCardProperties; + } else { + vCardProperties = lazy.VCardProperties.fromPropertyMap(propertyMap); + } + + if (uid) { + // Force the UID to be as passed. + vCardProperties.clearValues("uid"); + vCardProperties.addValue("uid", uid); + } else if (vCardProperties.getFirstValue("uid") != card.UID) { + vCardProperties.clearValues("uid"); + vCardProperties.addValue("uid", card.UID); + } + + // Collect only the properties we intend to keep. + for (let [name, value] of propertyMap) { + if (lazy.BANISHED_PROPERTIES.includes(name)) { + continue; + } + if (value !== null && value !== undefined && value !== "") { + newProperties.set(name, value); + } + } + + // Add the vCard and the properties from it we want to cache. + newProperties.set("_vCard", vCardProperties.toVCard()); + + let displayName = vCardProperties.getFirstValue("fn"); + newProperties.set("DisplayName", displayName || ""); + + let flatten = value => { + if (Array.isArray(value)) { + return value.join(" "); + } + return value; + }; + + let name = vCardProperties.getFirstValue("n"); + if (Array.isArray(name)) { + newProperties.set("FirstName", flatten(name[1])); + newProperties.set("LastName", flatten(name[0])); + } + + let email = vCardProperties.getAllValuesSorted("email"); + if (email[0]) { + newProperties.set("PrimaryEmail", email[0]); + } + if (email[1]) { + newProperties.set("SecondEmail", email[1]); + } + + let nickname = vCardProperties.getFirstValue("nickname"); + if (nickname) { + newProperties.set("NickName", flatten(nickname)); + } + + // Always set the last modified date. + newProperties.set("LastModifiedDate", "" + Math.floor(Date.now() / 1000)); + return newProperties; + } + + /* nsIAbDirectory */ + + get readOnly() { + return this._readOnly; + } + get isRemote() { + return false; + } + get isSecure() { + return false; + } + get propertiesChromeURI() { + return "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml"; + } + get dirPrefId() { + return this._dirPrefId; + } + get dirName() { + if (this._dirName === null) { + this._dirName = this.getLocalizedStringValue("description", ""); + } + return this._dirName; + } + set dirName(value) { + this.setLocalizedStringValue("description", value); + this._dirName = value; + Services.obs.notifyObservers(this, "addrbook-directory-updated", "DirName"); + } + get dirType() { + return Ci.nsIAbManager.JS_DIRECTORY_TYPE; + } + get fileName() { + return this._fileName; + } + get UID() { + if (!this._uid) { + if (this._prefBranch.getPrefType("uid") == Services.prefs.PREF_STRING) { + this._uid = this._prefBranch.getStringPref("uid"); + } else { + this._uid = lazy.newUID(); + this._prefBranch.setStringPref("uid", this._uid); + } + } + return this._uid; + } + get URI() { + return this._uri; + } + get position() { + return this._prefBranch.getIntPref("position", 1); + } + get childNodes() { + let lists = Array.from( + this.lists.values(), + list => + new lazy.AddrBookMailingList( + list.uid, + this, + list.name, + list.nickName, + list.description + ).asDirectory + ); + lists.sort(lazy.compareAddressBooks); + return lists; + } + /** @abstract */ + get childCardCount() { + throw new Components.Exception( + `${this.constructor.name} does not implement childCardCount getter.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + get childCards() { + let results = Array.from( + this.lists.values(), + list => + new lazy.AddrBookMailingList( + list.uid, + this, + list.name, + list.nickName, + list.description + ).asCard + ).concat(Array.from(this.cards.keys(), this.getCard, this)); + + return results; + } + get supportsMailingLists() { + return true; + } + + search(query, string, listener) { + if (!listener) { + return; + } + if (!query) { + listener.onSearchFinished(Cr.NS_ERROR_FAILURE, true, null, ""); + return; + } + if (query[0] == "?") { + query = query.substring(1); + } + + let results = Array.from( + this.lists.values(), + list => + new lazy.AddrBookMailingList( + list.uid, + this, + list.name, + list.nickName, + list.description + ).asCard + ).concat(Array.from(this.cards.keys(), this.getCard, this)); + + // Process the query string into a tree of conditions to match. + let lispRegexp = /^\((and|or|not|([^\)]*)(\)+))/; + let index = 0; + let rootQuery = { children: [], op: "or" }; + let currentQuery = rootQuery; + + while (true) { + let match = lispRegexp.exec(query.substring(index)); + if (!match) { + break; + } + index += match[0].length; + + if (["and", "or", "not"].includes(match[1])) { + // For the opening bracket, step down a level. + let child = { + parent: currentQuery, + children: [], + op: match[1], + }; + currentQuery.children.push(child); + currentQuery = child; + } else { + let [name, condition, value] = match[2].split(","); + currentQuery.children.push({ + name, + condition, + value: decodeURIComponent(value).toLowerCase(), + }); + + // For each closing bracket except the first, step up a level. + for (let i = match[3].length - 1; i > 0; i--) { + currentQuery = currentQuery.parent; + } + } + } + + results = results.filter(card => { + let properties; + if (card.isMailList) { + properties = new Map([ + ["DisplayName", card.displayName], + ["NickName", card.getProperty("NickName", "")], + ["Notes", card.getProperty("Notes", "")], + ]); + } else if (card._properties.has("_vCard")) { + try { + properties = card.vCardProperties.toPropertyMap(); + } catch (ex) { + // Parsing failed. Skip the vCard and just use the other properties. + console.error(ex); + properties = new Map(); + } + for (let [key, value] of card._properties) { + if (!properties.has(key)) { + properties.set(key, value); + } + } + } else { + properties = card._properties; + } + let matches = b => { + if ("condition" in b) { + let { name, condition, value } = b; + if (name == "IsMailList" && condition == "=") { + return card.isMailList == (value == "true"); + } + let cardValue = properties.get(name); + if (!cardValue) { + return condition == "!ex"; + } + if (condition == "ex") { + return true; + } + + cardValue = cardValue.toLowerCase(); + switch (condition) { + case "=": + return cardValue == value; + case "!=": + return cardValue != value; + case "lt": + return cardValue < value; + case "gt": + return cardValue > value; + case "bw": + return cardValue.startsWith(value); + case "ew": + return cardValue.endsWith(value); + case "c": + return cardValue.includes(value); + case "!c": + return !cardValue.includes(value); + case "~=": + case "regex": + default: + return false; + } + } + if (b.op == "or") { + return b.children.some(bb => matches(bb)); + } + if (b.op == "and") { + return b.children.every(bb => matches(bb)); + } + if (b.op == "not") { + return !matches(b.children[0]); + } + return false; + }; + + return matches(rootQuery); + }, this); + + for (let card of results) { + listener.onSearchFoundCard(card); + } + listener.onSearchFinished(Cr.NS_OK, true, null, ""); + } + generateName(generateFormat, bundle) { + return this.dirName; + } + cardForEmailAddress(emailAddress) { + if (!emailAddress) { + return null; + } + + // Check the properties. We copy the first two addresses to properties for + // this purpose, so it should be fast. + let card = this.getCardFromProperty("PrimaryEmail", emailAddress, false); + if (card) { + return card; + } + card = this.getCardFromProperty("SecondEmail", emailAddress, false); + if (card) { + return card; + } + + // Nothing so far? Go through all the cards checking all of the addresses. + // This could be slow. + emailAddress = emailAddress.toLowerCase(); + for (let [uid, properties] of this.cards) { + let vCard = properties.get("_vCard"); + // If the vCard string doesn't include the email address, the parsed + // vCard won't include it either, so don't waste time parsing it. + if (!vCard?.toLowerCase().includes(emailAddress)) { + continue; + } + card = this.getCard(uid); + if (card.emailAddresses.some(e => e.toLowerCase() == emailAddress)) { + return card; + } + } + + return null; + } + /** @abstract */ + getCardFromProperty(property, value, caseSensitive) { + throw new Components.Exception( + `${this.constructor.name} does not implement getCardFromProperty.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + /** @abstract */ + getCardsFromProperty(property, value, caseSensitive) { + throw new Components.Exception( + `${this.constructor.name} does not implement getCardsFromProperty.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + getMailListFromName(name) { + for (let list of this.lists.values()) { + if (list.name.toLowerCase() == name.toLowerCase()) { + return new lazy.AddrBookMailingList( + list.uid, + this, + list.name, + list.nickName, + list.description + ).asDirectory; + } + } + return null; + } + deleteDirectory(directory) { + if (this._readOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + let list = this.lists.get(directory.UID); + list = new lazy.AddrBookMailingList( + list.uid, + this, + list.name, + list.nickName, + list.description + ); + + this.deleteList(directory.UID); + + Services.obs.notifyObservers( + list.asDirectory, + "addrbook-list-deleted", + this.UID + ); + } + hasCard(card) { + return this.lists.has(card.UID) || this.cards.has(card.UID); + } + hasDirectory(dir) { + return this.lists.has(dir.UID); + } + hasMailListWithName(name) { + return this.getMailListFromName(name) != null; + } + addCard(card) { + return this.dropCard(card, false); + } + modifyCard(card) { + if (this._readOnly && !this._overrideReadOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + let oldProperties = this.loadCardProperties(card.UID); + let newProperties = this.prepareToSaveCard(card); + + let allProperties = new Set(oldProperties.keys()); + for (let key of newProperties.keys()) { + allProperties.add(key); + } + + if (this.hasOwnProperty("cards")) { + this.cards.set(card.UID, newProperties); + } + this.saveCardProperties(card.UID, newProperties); + + let changeData = {}; + for (let name of allProperties) { + if (name == "LastModifiedDate") { + continue; + } + + let oldValue = oldProperties.get(name) || null; + let newValue = newProperties.get(name) || null; + if (oldValue != newValue) { + changeData[name] = { oldValue, newValue }; + } + } + + // Increment this preference if one or both of these properties change. + // This will cause the UI to throw away cached values. + if ("DisplayName" in changeData || "PreferDisplayName" in changeData) { + Services.prefs.setIntPref( + "mail.displayname.version", + Services.prefs.getIntPref("mail.displayname.version", 0) + 1 + ); + } + + // Send the card as it is in this directory, not as passed to this function. + let newCard = this.getCard(card.UID); + Services.obs.notifyObservers(newCard, "addrbook-contact-updated", this.UID); + + Services.obs.notifyObservers( + newCard, + "addrbook-contact-properties-updated", + JSON.stringify(changeData) + ); + + // Return the card, even though the interface says not to, because + // subclasses may want it. + return newCard; + } + deleteCards(cards) { + if (this._readOnly && !this._overrideReadOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + if (cards === null) { + throw Components.Exception("", Cr.NS_ERROR_INVALID_POINTER); + } + + let updateDisplayNameVersion = false; + for (let card of cards) { + updateDisplayNameVersion = updateDisplayNameVersion || card.displayName; + // TODO: delete photo if there is one + this.deleteCard(card.UID); + if (this.hasOwnProperty("cards")) { + this.cards.delete(card.UID); + } + } + + // Increment this preference if one or more cards has a display name. + // This will cause the UI to throw away cached values. + if (updateDisplayNameVersion) { + Services.prefs.setIntPref( + "mail.displayname.version", + Services.prefs.getIntPref("mail.displayname.version", 0) + 1 + ); + } + + for (let card of cards) { + Services.obs.notifyObservers(card, "addrbook-contact-deleted", this.UID); + card.directoryUID = null; + } + + // We could just delete all non-existent cards from list_cards, but a + // notification should be fired for each one. Let the list handle that. + for (let list of this.childNodes) { + list.deleteCards(cards); + } + } + dropCard(card, needToCopyCard) { + if (this._readOnly && !this._overrideReadOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + if (!card.UID) { + throw new Error("Card must have a UID to be added to this directory."); + } + + let uid = needToCopyCard ? lazy.newUID() : card.UID; + let newProperties = this.prepareToSaveCard(card, uid); + if (card.directoryUID && card.directoryUID != this._uid) { + // These properties belong to a different directory. Don't keep them. + newProperties.delete("_etag"); + newProperties.delete("_href"); + } + + if (this.hasOwnProperty("cards")) { + this.cards.set(uid, newProperties); + } + this.saveCardProperties(uid, newProperties); + + // Increment this preference if the card has a display name. + // This will cause the UI to throw away cached values. + if (card.displayName) { + Services.prefs.setIntPref( + "mail.displayname.version", + Services.prefs.getIntPref("mail.displayname.version", 0) + 1 + ); + } + + let newCard = this.getCard(uid); + Services.obs.notifyObservers(newCard, "addrbook-contact-created", this.UID); + return newCard; + } + useForAutocomplete(identityKey) { + return ( + Services.prefs.getBoolPref("mail.enable_autocomplete") && + this.getBoolValue("enable_autocomplete", true) + ); + } + addMailList(list) { + if (this._readOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + if (!list.isMailList) { + throw Components.Exception( + "Can't add; not a mail list", + Cr.NS_ERROR_UNEXPECTED + ); + } + + // Check if the new name is empty. + if (!list.dirName) { + throw new Components.Exception( + `Mail list name must be set; list.dirName=${list.dirName}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + + // Check if the new name contains 2 spaces. + if (list.dirName.match(" ")) { + throw new Components.Exception( + `Invalid mail list name: ${list.dirName}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + + // Check if the new name contains the following special characters. + for (let char of ',;"<>') { + if (list.dirName.includes(char)) { + throw new Components.Exception( + `Invalid mail list name: ${list.dirName}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + } + + let newList = new lazy.AddrBookMailingList( + lazy.newUID(), + this, + list.dirName || "", + list.listNickName || "", + list.description || "" + ); + this.saveList(newList); + + let newListDirectory = newList.asDirectory; + Services.obs.notifyObservers( + newListDirectory, + "addrbook-list-created", + this.UID + ); + return newListDirectory; + } + editMailListToDatabase(listCard) { + // Deliberately not implemented, this isn't a mailing list. + throw Components.Exception( + "editMailListToDatabase not relevant here", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + copyMailList(srcList) { + // Deliberately not implemented, this isn't a mailing list. + throw Components.Exception( + "copyMailList not relevant here", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + getIntValue(name, defaultValue) { + return this._prefBranch + ? this._prefBranch.getIntPref(name, defaultValue) + : defaultValue; + } + getBoolValue(name, defaultValue) { + return this._prefBranch + ? this._prefBranch.getBoolPref(name, defaultValue) + : defaultValue; + } + getStringValue(name, defaultValue) { + return this._prefBranch + ? this._prefBranch.getStringPref(name, defaultValue) + : defaultValue; + } + getLocalizedStringValue(name, defaultValue) { + if (!this._prefBranch) { + return defaultValue; + } + if (this._prefBranch.getPrefType(name) == Ci.nsIPrefBranch.PREF_INVALID) { + return defaultValue; + } + try { + return this._prefBranch.getComplexValue(name, Ci.nsIPrefLocalizedString) + .data; + } catch (e) { + // getComplexValue doesn't work with autoconfig. + return this._prefBranch.getStringPref(name); + } + } + setIntValue(name, value) { + this._prefBranch.setIntPref(name, value); + } + setBoolValue(name, value) { + this._prefBranch.setBoolPref(name, value); + } + setStringValue(name, value) { + this._prefBranch.setStringPref(name, value); + } + setLocalizedStringValue(name, value) { + let valueLocal = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ); + valueLocal.data = value; + this._prefBranch.setComplexValue( + name, + Ci.nsIPrefLocalizedString, + valueLocal + ); + } +} diff --git a/comm/mailnews/addrbook/modules/AddrBookMailingList.jsm b/comm/mailnews/addrbook/modules/AddrBookMailingList.jsm new file mode 100644 index 0000000000..31d16e93aa --- /dev/null +++ b/comm/mailnews/addrbook/modules/AddrBookMailingList.jsm @@ -0,0 +1,420 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["AddrBookMailingList"]; + +/* Prototype for mailing lists. A mailing list can appear as nsIAbDirectory + * or as nsIAbCard. Here we keep all relevant information in the class itself + * and fulfill each interface on demand. This will make more sense and be + * a lot neater once we stop using two XPCOM interfaces for one job. */ + +function AddrBookMailingList(uid, parent, name, nickName, description) { + this._uid = uid; + this._parent = parent; + this._name = name; + this._nickName = nickName; + this._description = description; +} +AddrBookMailingList.prototype = { + get asDirectory() { + let self = this; + return { + QueryInterface: ChromeUtils.generateQI(["nsIAbDirectory"]), + classID: Components.ID("{e96ee804-0bd3-472f-81a6-8a9d65277ad3}"), + + get readOnly() { + return self._parent._readOnly; + }, + get isRemote() { + return self._parent.isRemote; + }, + get isSecure() { + return self._parent.isSecure; + }, + get propertiesChromeURI() { + return "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml"; + }, + get UID() { + return self._uid; + }, + get URI() { + return `${self._parent.URI}/${self._uid}`; + }, + get dirName() { + return self._name; + }, + set dirName(value) { + self._name = value; + }, + get listNickName() { + return self._nickName; + }, + set listNickName(value) { + self._nickName = value; + }, + get description() { + return self._description; + }, + set description(value) { + self._description = value; + }, + get isMailList() { + return true; + }, + get childNodes() { + return []; + }, + get childCards() { + let selectStatement = self._parent._dbConnection.createStatement( + "SELECT card FROM list_cards WHERE list = :list ORDER BY oid" + ); + selectStatement.params.list = self._uid; + let results = []; + while (selectStatement.executeStep()) { + results.push(self._parent.getCard(selectStatement.row.card)); + } + selectStatement.finalize(); + return results; + }, + get supportsMailingLists() { + return false; + }, + + search(query, string, listener) { + if (!listener) { + return; + } + if (!query) { + listener.onSearchFinished(Cr.NS_ERROR_FAILURE, true, null, ""); + return; + } + if (query[0] == "?") { + query = query.substring(1); + } + + let results = this.childCards; + + // Process the query string into a tree of conditions to match. + let lispRegexp = /^\((and|or|not|([^\)]*)(\)+))/; + let index = 0; + let rootQuery = { children: [], op: "or" }; + let currentQuery = rootQuery; + + while (true) { + let match = lispRegexp.exec(query.substring(index)); + if (!match) { + break; + } + index += match[0].length; + + if (["and", "or", "not"].includes(match[1])) { + // For the opening bracket, step down a level. + let child = { + parent: currentQuery, + children: [], + op: match[1], + }; + currentQuery.children.push(child); + currentQuery = child; + } else { + let [name, condition, value] = match[2].split(","); + currentQuery.children.push({ + name, + condition, + value: decodeURIComponent(value).toLowerCase(), + }); + + // For each closing bracket except the first, step up a level. + for (let i = match[3].length - 1; i > 0; i--) { + currentQuery = currentQuery.parent; + } + } + } + + results = results.filter(card => { + let properties = card._properties; + let matches = b => { + if ("condition" in b) { + let { name, condition, value } = b; + if (name == "IsMailList" && condition == "=") { + return value == "true"; + } + + if (!properties.has(name)) { + return condition == "!ex"; + } + if (condition == "ex") { + return true; + } + + let cardValue = properties.get(name).toLowerCase(); + switch (condition) { + case "=": + return cardValue == value; + case "!=": + return cardValue != value; + case "lt": + return cardValue < value; + case "gt": + return cardValue > value; + case "bw": + return cardValue.startsWith(value); + case "ew": + return cardValue.endsWith(value); + case "c": + return cardValue.includes(value); + case "!c": + return !cardValue.includes(value); + case "~=": + case "regex": + default: + return false; + } + } + if (b.op == "or") { + return b.children.some(bb => matches(bb)); + } + if (b.op == "and") { + return b.children.every(bb => matches(bb)); + } + if (b.op == "not") { + return !matches(b.children[0]); + } + return false; + }; + + return matches(rootQuery); + }, this); + + for (let card of results) { + listener.onSearchFoundCard(card); + } + listener.onSearchFinished(Cr.NS_OK, true, null, ""); + }, + addCard(card) { + if (this.readOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + if (!card.primaryEmail) { + return card; + } + if (!self._parent.hasCard(card)) { + card = self._parent.addCard(card); + } + let insertStatement = self._parent._dbConnection.createStatement( + "REPLACE INTO list_cards (list, card) VALUES (:list, :card)" + ); + insertStatement.params.list = self._uid; + insertStatement.params.card = card.UID; + insertStatement.execute(); + Services.obs.notifyObservers( + card, + "addrbook-list-member-added", + self._uid + ); + insertStatement.finalize(); + return card; + }, + deleteCards(cards) { + if (this.readOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + let deleteCardStatement = self._parent._dbConnection.createStatement( + "DELETE FROM list_cards WHERE list = :list AND card = :card" + ); + for (let card of cards) { + deleteCardStatement.params.list = self._uid; + deleteCardStatement.params.card = card.UID; + deleteCardStatement.execute(); + if (self._parent._dbConnection.affectedRows) { + Services.obs.notifyObservers( + card, + "addrbook-list-member-removed", + self._uid + ); + } + deleteCardStatement.reset(); + } + deleteCardStatement.finalize(); + }, + dropCard(card, needToCopyCard) { + if (this.readOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + if (needToCopyCard) { + card = self._parent.dropCard(card, true); + } + this.addCard(card); + Services.obs.notifyObservers( + card, + "addrbook-list-member-added", + self._uid + ); + }, + editMailListToDatabase(listCard) { + if (this.readOnly) { + throw new Components.Exception( + "Directory is read-only", + Cr.NS_ERROR_FAILURE + ); + } + + // Check if the new name is empty. + if (!self._name) { + throw new Components.Exception( + "Invalid mailing list name", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + + // Check if the new name contains 2 spaces. + if (self._name.match(" ")) { + throw new Components.Exception( + "Invalid mailing list name", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + + // Check if the new name contains the following special characters. + for (let char of ',;"<>') { + if (self._name.includes(char)) { + throw new Components.Exception( + "Invalid mailing list name", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + } + + self._parent.saveList(self); + Services.obs.notifyObservers( + this, + "addrbook-list-updated", + self._parent.UID + ); + }, + hasMailListWithName(name) { + return false; + }, + getMailListFromName(name) { + return null; + }, + }; + }, + get asCard() { + let self = this; + return { + QueryInterface: ChromeUtils.generateQI(["nsIAbCard"]), + classID: Components.ID("{1143991d-31cd-4ea6-9c97-c587d990d724}"), + + get UID() { + return self._uid; + }, + get isMailList() { + return true; + }, + get mailListURI() { + return `${self._parent.URI}/${self._uid}`; + }, + + get directoryUID() { + return self._parent.UID; + }, + get firstName() { + return ""; + }, + get lastName() { + return self._name; + }, + get displayName() { + return self._name; + }, + set displayName(value) { + self._name = value; + }, + get primaryEmail() { + return ""; + }, + get emailAddresses() { + // NOT the members of this list. + return []; + }, + + generateName(generateFormat) { + return self._name; + }, + getProperty(name, defaultValue) { + switch (name) { + case "NickName": + return self._nickName; + case "Notes": + return self._description; + } + return defaultValue; + }, + setProperty(name, value) { + switch (name) { + case "NickName": + self._nickName = value; + break; + case "Notes": + self._description = value; + break; + } + }, + equals(card) { + return self._uid == card.UID; + }, + hasEmailAddress(emailAddress) { + return false; + }, + get properties() { + const entries = [ + ["DisplayName", this.displayName], + ["NickName", this.getProperty("NickName", "")], + ["Notes", this.getProperty("Notes", "")], + ]; + let props = []; + for (const [name, value] of entries) { + props.push({ + get name() { + return name; + }, + get value() { + return value; + }, + QueryInterface: ChromeUtils.generateQI(["nsIProperty"]), + }); + } + return props; + }, + get supportsVCard() { + return false; + }, + get vCardProperties() { + return null; + }, + translateTo(type) { + // Get nsAbCardProperty to do the work, the code is in C++ anyway. + let cardCopy = Cc[ + "@mozilla.org/addressbook/cardproperty;1" + ].createInstance(Ci.nsIAbCard); + cardCopy.UID = this.UID; + cardCopy.copy(this); + return cardCopy.translateTo(type); + }, + }; + }, +}; diff --git a/comm/mailnews/addrbook/modules/AddrBookManager.jsm b/comm/mailnews/addrbook/modules/AddrBookManager.jsm new file mode 100644 index 0000000000..6e15a4c971 --- /dev/null +++ b/comm/mailnews/addrbook/modules/AddrBookManager.jsm @@ -0,0 +1,608 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["AddrBookManager"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + compareAddressBooks: "resource:///modules/AddrBookUtils.jsm", + MailGlue: "resource:///modules/MailGlue.jsm", +}); + +/** Test for valid directory URIs. */ +const URI_REGEXP = /^([\w-]+):\/\/([\w\.-]*)([/:].*|$)/; + +/** + * When initialized, a map of nsIAbDirectory objects. Keys to this map are + * the directories' URIs. + */ +let store = null; + +/** Valid address book types. This differs by operating system. */ +let types = ["jsaddrbook", "jscarddav", "moz-abldapdirectory"]; +if (AppConstants.platform == "macosx") { + types.push("moz-abosxdirectory"); +} else if (AppConstants.platform == "win") { + types.push("moz-aboutlookdirectory"); +} + +/** + * A pre-sorted list of directories in the right order, to be returned by + * AddrBookManager.directories. That function is called a lot, and there's + * no need to sort the list every time. + * + * Call updateSortedDirectoryList after `store` changes and before any + * notifications happen. + */ +let sortedDirectoryList = []; +function updateSortedDirectoryList() { + sortedDirectoryList = [...store.values()]; + sortedDirectoryList.sort(lazy.compareAddressBooks); +} + +/** + * Initialise an address book directory by URI. + * + * @param {string} uri - URI for the directory. + * @param {boolean} shouldStore - Whether to keep a reference to this address + * book in the store. + * @returns {nsIAbDirectory} + */ +function createDirectoryObject(uri, shouldStore = false) { + let uriParts = URI_REGEXP.exec(uri); + if (!uriParts) { + throw Components.Exception( + `Unexpected uri: ${uri}`, + Cr.NS_ERROR_MALFORMED_URI + ); + } + + let [, scheme] = uriParts; + let dir = Cc[ + `@mozilla.org/addressbook/directory;1?type=${scheme}` + ].createInstance(Ci.nsIAbDirectory); + + try { + if (shouldStore) { + // This must happen before .init is called, or the OS X provider breaks + // in some circumstances. If .init fails, we'll remove it again. + // The Outlook provider also needs this since during the initialisation + // of the top-most directory, contained mailing lists already need + // to loop that directory. + store.set(uri, dir); + } + dir.init(uri); + } catch (ex) { + if (shouldStore) { + store.delete(uri); + } + throw ex; + } + + return dir; +} + +/** + * Read the preferences and create any address books defined there. + */ +function ensureInitialized() { + if (store !== null) { + return; + } + if (lazy.MailGlue.isToolboxProcess) { + throw new Components.Exception( + "AddrBookManager tried to start in the Developer Tools process!", + Cr.NS_ERROR_UNEXPECTED + ); + } + + store = new Map(); + + for (let pref of Services.prefs.getChildList("ldap_2.servers.")) { + try { + if (pref.endsWith(".uri")) { + let uri = Services.prefs.getStringPref(pref); + if (uri.startsWith("ldap://") || uri.startsWith("ldaps://")) { + let prefName = pref.substring(0, pref.length - 4); + + uri = `moz-abldapdirectory://${prefName}`; + createDirectoryObject(uri, true); + } + } else if (pref.endsWith(".dirType")) { + let prefName = pref.substring(0, pref.length - 8); + let dirType = Services.prefs.getIntPref(pref); + let fileName = Services.prefs.getStringPref(`${prefName}.filename`, ""); + let uri = Services.prefs.getStringPref(`${prefName}.uri`, ""); + + switch (dirType) { + case Ci.nsIAbManager.MAPI_DIRECTORY_TYPE: + if ( + Cu.isInAutomation || + Services.env.exists("XPCSHELL_TEST_PROFILE_DIR") + ) { + // Don't load the OS Address Book in tests. + break; + } + if (Services.prefs.getIntPref(`${prefName}.position`, 1) < 1) { + // Migration: the previous address book manager set the position + // value to 0 to indicate the removal of an address book. + Services.prefs.clearUserPref(`${prefName}.position`); + Services.prefs.setIntPref(pref, -1); + break; + } + if (AppConstants.platform == "macosx") { + createDirectoryObject(uri, true); + } else if (AppConstants.platform == "win") { + let outlookInterface = Cc[ + "@mozilla.org/addressbook/outlookinterface;1" + ].getService(Ci.nsIAbOutlookInterface); + for (let folderURI of outlookInterface.getFolderURIs(uri)) { + createDirectoryObject(folderURI, true); + } + } + break; + case Ci.nsIAbManager.JS_DIRECTORY_TYPE: + if (fileName) { + let uri = `jsaddrbook://${fileName}`; + createDirectoryObject(uri, true); + } + break; + case Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE: + if (fileName) { + let uri = `jscarddav://${fileName}`; + createDirectoryObject(uri, true); + } + break; + } + } + } catch (ex) { + console.error(ex); + } + } + + updateSortedDirectoryList(); +} + +// Force the manager to shut down. For tests only. +Services.obs.addObserver(async () => { + // Allow directories to tidy up. + for (let directory of store.values()) { + await directory.cleanUp(); + } + // Clear the store. The next call to ensureInitialized will recreate it. + store = null; + Services.obs.notifyObservers(null, "addrbook-reloaded"); +}, "addrbook-reload"); + +/** Cache for the cardForEmailAddress function, and timer to clear it. */ +let addressCache = new Map(); +let addressCacheTimer = null; + +// Throw away cached cards if the display name properties change, so we can +// get the updated version of the card that changed. +Services.prefs.addObserver("mail.displayname.version", () => { + addressCache.clear(); + Services.obs.notifyObservers(null, "addrbook-displayname-changed"); +}); + +// When this prefence has been updated, we need to update the +// mail.displayname.version, which notifies it's preference observer (above). +// This will then notify the addrbook-displayname-changed observer, and change +// the displayname in the thread tree and message header. +Services.prefs.addObserver("mail.showCondensedAddresses", () => { + Services.prefs.setIntPref( + "mail.displayname.version", + Services.prefs.getIntPref("mail.displayname.version") + 1 + ); +}); + +/** + * @implements {nsIAbManager} + * @implements {nsICommandLineHandler} + */ +function AddrBookManager() {} +AddrBookManager.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIAbManager", + "nsICommandLineHandler", + ]), + classID: Components.ID("{224d3ef9-d81c-4d94-8826-a79a5835af93}"), + + /* nsIAbManager */ + + get directories() { + ensureInitialized(); + return sortedDirectoryList.slice(); + }, + getDirectory(uri) { + if (uri.startsWith("moz-abdirectory://")) { + throw new Components.Exception( + "The root address book no longer exists", + Cr.NS_ERROR_FAILURE + ); + } + + ensureInitialized(); + if (store.has(uri)) { + return store.get(uri); + } + + let uriParts = URI_REGEXP.exec(uri); + if (!uriParts) { + throw Components.Exception( + `Unexpected uri: ${uri}`, + Cr.NS_ERROR_MALFORMED_URI + ); + } + let [, scheme, fileName, tail] = uriParts; + if (tail && types.includes(scheme)) { + if ( + (scheme == "jsaddrbook" && tail.startsWith("/")) || + scheme == "moz-aboutlookdirectory" + ) { + let parent; + if (scheme == "jsaddrbook") { + parent = this.getDirectory(`${scheme}://${fileName}`); + } else { + parent = this.getDirectory(`${scheme}:///${tail.split("/")[1]}`); + } + for (let list of parent.childNodes) { + list.QueryInterface(Ci.nsIAbDirectory); + if (list.URI == uri) { + return list; + } + } + throw Components.Exception( + `No ${scheme} directory for uri=${uri}`, + Cr.NS_ERROR_UNEXPECTED + ); + } else if (scheme == "jscarddav") { + throw Components.Exception( + `No ${scheme} directory for uri=${uri}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + // `tail` could point to a mailing list. + return createDirectoryObject(uri); + } + throw Components.Exception( + `No directory for uri=${uri}`, + Cr.NS_ERROR_FAILURE + ); + }, + getDirectoryFromId(dirPrefId) { + ensureInitialized(); + for (let dir of store.values()) { + if (dir.dirPrefId == dirPrefId) { + return dir; + } + } + return null; + }, + getDirectoryFromUID(uid) { + ensureInitialized(); + for (let dir of store.values()) { + if (dir.UID == uid) { + return dir; + } + } + return null; + }, + getMailListFromName(name) { + ensureInitialized(); + for (let dir of store.values()) { + let hit = dir.getMailListFromName(name); + if (hit) { + return hit; + } + } + return null; + }, + newAddressBook(dirName, uri, type, uid) { + function ensureUniquePrefName() { + let leafName = dirName.replace(/\W/g, ""); + if (!leafName) { + leafName = "_nonascii"; + } + + let existingNames = Array.from(store.values(), dir => dir.dirPrefId); + let uniqueCount = 0; + prefName = `ldap_2.servers.${leafName}`; + while (existingNames.includes(prefName)) { + prefName = `ldap_2.servers.${leafName}_${++uniqueCount}`; + } + } + + if (!dirName) { + throw new Components.Exception( + "dirName must be specified", + Cr.NS_ERROR_INVALID_ARG + ); + } + if (uid && this.getDirectoryFromUID(uid)) { + throw new Components.Exception( + `An address book with the UID ${uid} already exists`, + Cr.NS_ERROR_ABORT + ); + } + + let prefName; + ensureInitialized(); + + switch (type) { + case Ci.nsIAbManager.LDAP_DIRECTORY_TYPE: { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("ldap.sqlite"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + + ensureUniquePrefName(); + Services.prefs.setStringPref(`${prefName}.description`, dirName); + Services.prefs.setStringPref(`${prefName}.filename`, file.leafName); + Services.prefs.setStringPref(`${prefName}.uri`, uri); + if (uid) { + Services.prefs.setStringPref(`${prefName}.uid`, uid); + } + + uri = `moz-abldapdirectory://${prefName}`; + let dir = createDirectoryObject(uri, true); + updateSortedDirectoryList(); + Services.obs.notifyObservers(dir, "addrbook-directory-created"); + break; + } + case Ci.nsIAbManager.MAPI_DIRECTORY_TYPE: { + if (AppConstants.platform == "macosx") { + uri = "moz-abosxdirectory:///"; + if (store.has(uri)) { + throw Components.Exception( + `Can't create new ab of type=${type} - already exists`, + Cr.NS_ERROR_UNEXPECTED + ); + } + prefName = "ldap_2.servers.osx"; + } else if (AppConstants.platform == "win") { + uri = "moz-aboutlookdirectory:///"; + if (store.has(uri)) { + throw Components.Exception( + `Can't create new ab of type=${type} - already exists`, + Cr.NS_ERROR_UNEXPECTED + ); + } + prefName = "ldap_2.servers.outlook"; + } else { + throw Components.Exception( + "Can't create new ab of type=MAPI_DIRECTORY_TYPE", + Cr.NS_ERROR_UNEXPECTED + ); + } + + Services.prefs.setIntPref( + `${prefName}.dirType`, + Ci.nsIAbManager.MAPI_DIRECTORY_TYPE + ); + Services.prefs.setStringPref( + `${prefName}.description`, + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + Services.prefs.setStringPref(`${prefName}.uri`, uri); + if (uid) { + Services.prefs.setStringPref(`${prefName}.uid`, uid); + } + + if (AppConstants.platform == "macosx") { + let dir = createDirectoryObject(uri, true); + updateSortedDirectoryList(); + Services.obs.notifyObservers(dir, "addrbook-directory-created"); + } else if (AppConstants.platform == "win") { + let outlookInterface = Cc[ + "@mozilla.org/addressbook/outlookinterface;1" + ].getService(Ci.nsIAbOutlookInterface); + for (let folderURI of outlookInterface.getFolderURIs(uri)) { + let dir = createDirectoryObject(folderURI, true); + updateSortedDirectoryList(); + Services.obs.notifyObservers(dir, "addrbook-directory-created"); + } + } + break; + } + case Ci.nsIAbManager.JS_DIRECTORY_TYPE: + case Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE: { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("abook.sqlite"); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + + ensureUniquePrefName(); + Services.prefs.setStringPref(`${prefName}.description`, dirName); + Services.prefs.setIntPref(`${prefName}.dirType`, type); + Services.prefs.setStringPref(`${prefName}.filename`, file.leafName); + if (uid) { + Services.prefs.setStringPref(`${prefName}.uid`, uid); + } + + let scheme = + type == Ci.nsIAbManager.JS_DIRECTORY_TYPE + ? "jsaddrbook" + : "jscarddav"; + uri = `${scheme}://${file.leafName}`; + let dir = createDirectoryObject(uri, true); + updateSortedDirectoryList(); + Services.obs.notifyObservers(dir, "addrbook-directory-created"); + break; + } + default: + throw Components.Exception( + `Unexpected directory type: ${type}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + + return prefName; + }, + addAddressBook(dir) { + if ( + !dir.URI || + !dir.dirName || + !dir.UID || + dir.isMailList || + dir.isQuery || + dir.dirPrefId + ) { + throw new Components.Exception( + "Invalid directory", + Cr.NS_ERROR_INVALID_ARG + ); + } + + ensureInitialized(); + if (store.has(dir.URI)) { + throw new Components.Exception( + "Directory already exists", + Cr.NS_ERROR_UNEXPECTED + ); + } + + store.set(dir.URI, dir); + updateSortedDirectoryList(); + Services.obs.notifyObservers(dir, "addrbook-directory-created"); + }, + deleteAddressBook(uri) { + let uriParts = URI_REGEXP.exec(uri); + if (!uriParts) { + throw Components.Exception("", Cr.NS_ERROR_MALFORMED_URI); + } + + let [, scheme, fileName, tail] = uriParts; + if (tail && tail.startsWith("/")) { + let dir; + if (scheme == "jsaddrbook") { + dir = store.get(`${scheme}://${fileName}`); + } else if (scheme == "moz-aboutlookdirectory") { + dir = store.get(`${scheme}:///${tail.split("/")[1]}`); + } + let list = this.getDirectory(uri); + if (dir && list) { + dir.deleteDirectory(list); + return; + } + } + + let dir = store.get(uri); + if (!dir) { + throw new Components.Exception( + `Address book not found: ${uri}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + + let prefName = dir.dirPrefId; + if (prefName) { + let dirType = Services.prefs.getIntPref(`${prefName}.dirType`, 0); + fileName = dir.fileName; + + // Deleting the built-in address books is very bad. + if (["ldap_2.servers.pab", "ldap_2.servers.history"].includes(prefName)) { + throw new Components.Exception( + "Refusing to delete a built-in address book", + Cr.NS_ERROR_FAILURE + ); + } + + for (let name of Services.prefs.getChildList(`${prefName}.`)) { + Services.prefs.clearUserPref(name); + } + if (dirType == Ci.nsIAbManager.MAPI_DIRECTORY_TYPE) { + // The prefs for this directory type are defaults. Setting the dirType + // to -1 ensures the directory is ignored. + Services.prefs.setIntPref(`${prefName}.dirType`, -1); + } + } + + store.delete(uri); + updateSortedDirectoryList(); + + // Clear this reference to the deleted address book. + if (Services.prefs.getStringPref("mail.collect_addressbook") == uri) { + Services.prefs.clearUserPref("mail.collect_addressbook"); + } + + dir.cleanUp().then(() => { + if (fileName) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(fileName); + if (file.exists()) { + file.remove(false); + } + } + + Services.obs.notifyObservers(dir, "addrbook-directory-deleted"); + }); + }, + mailListNameExists(name) { + ensureInitialized(); + for (let dir of store.values()) { + if (dir.hasMailListWithName(name)) { + return true; + } + } + return false; + }, + /** + * Finds out if the directory name already exists. + * + * @param {string} name - The name of a directory to check for. + */ + directoryNameExists(name) { + ensureInitialized(); + for (let dir of store.values()) { + if (dir.dirName.toLowerCase() === name.toLowerCase()) { + return true; + } + } + return false; + }, + cardForEmailAddress(emailAddress) { + if (!emailAddress) { + return null; + } + + if (addressCacheTimer) { + lazy.clearTimeout(addressCacheTimer); + } + addressCacheTimer = lazy.setTimeout(() => { + addressCacheTimer = null; + addressCache.clear(); + }, 60000); + + if (addressCache.has(emailAddress)) { + return addressCache.get(emailAddress); + } + + for (let directory of sortedDirectoryList) { + try { + let card = directory.cardForEmailAddress(emailAddress); + if (card) { + addressCache.set(emailAddress, card); + return card; + } + } catch (ex) { + // Directories can throw, that's okay. + } + } + + addressCache.set(emailAddress, null); + return null; + }, +}; diff --git a/comm/mailnews/addrbook/modules/AddrBookUtils.jsm b/comm/mailnews/addrbook/modules/AddrBookUtils.jsm new file mode 100644 index 0000000000..aea0c152ad --- /dev/null +++ b/comm/mailnews/addrbook/modules/AddrBookUtils.jsm @@ -0,0 +1,522 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = [ + "exportAttributes", + "AddrBookUtils", + "compareAddressBooks", + "newUID", + "SimpleEnumerator", +]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailStringUtils } = ChromeUtils.import( + "resource:///modules/MailStringUtils.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetters(lazy, { + attrMapService: [ + "@mozilla.org/addressbook/ldap-attribute-map-service;1", + "nsIAbLDAPAttributeMapService", + ], +}); + +function SimpleEnumerator(elements) { + this._elements = elements; + this._position = 0; +} +SimpleEnumerator.prototype = { + hasMoreElements() { + return this._position < this._elements.length; + }, + getNext() { + if (this.hasMoreElements()) { + return this._elements[this._position++]; + } + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + }, + QueryInterface: ChromeUtils.generateQI(["nsISimpleEnumerator"]), + *[Symbol.iterator]() { + while (this.hasMoreElements()) { + yield this.getNext(); + } + }, +}; + +function newUID() { + return Services.uuid.generateUUID().toString().substring(1, 37); +} + +let abSortOrder = { + [Ci.nsIAbManager.JS_DIRECTORY_TYPE]: 1, + [Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE]: 2, + [Ci.nsIAbManager.LDAP_DIRECTORY_TYPE]: 3, + [Ci.nsIAbManager.ASYNC_DIRECTORY_TYPE]: 3, + [Ci.nsIAbManager.MAPI_DIRECTORY_TYPE]: 4, +}; +let abNameComparer = new Intl.Collator(undefined, { numeric: true }); + +/** + * Comparator for address books. Any UI that lists address books should use + * this order, although generally speaking, using nsIAbManager.directories is + * all that is required to get the order. + * + * Note that directories should not be compared with mailing lists in this way, + * however two mailing lists with the same parent can be safely compared. + * + * @param {nsIAbDirectory} a + * @param {nsIAbDirectory} b + * @returns {integer} + */ +function compareAddressBooks(a, b) { + if (a.isMailList != b.isMailList) { + throw Components.Exception( + "Tried to compare a mailing list with a directory", + Cr.NS_ERROR_UNEXPECTED + ); + } + + // Only compare the names of mailing lists. + if (a.isMailList) { + return abNameComparer.compare(a.dirName, b.dirName); + } + + // The Personal Address Book is first and Collected Addresses last. + let aPrefId = a.dirPrefId; + let bPrefId = b.dirPrefId; + + if (aPrefId == "ldap_2.servers.pab" || bPrefId == "ldap_2.servers.history") { + return -1; + } + if (bPrefId == "ldap_2.servers.pab" || aPrefId == "ldap_2.servers.history") { + return 1; + } + + // Order remaining directories by type. + let aType = a.dirType; + let bType = b.dirType; + + if (aType != bType) { + return abSortOrder[aType] - abSortOrder[bType]; + } + + // Order directories of the same type by name, case-insensitively. + return abNameComparer.compare(a.dirName, b.dirName); +} + +const exportAttributes = [ + ["FirstName", 2100], + ["LastName", 2101], + ["DisplayName", 2102], + ["NickName", 2103], + ["PrimaryEmail", 2104], + ["SecondEmail", 2105], + ["_AimScreenName", 2136], + ["LastModifiedDate", 0], + ["WorkPhone", 2106], + ["WorkPhoneType", 0], + ["HomePhone", 2107], + ["HomePhoneType", 0], + ["FaxNumber", 2108], + ["FaxNumberType", 0], + ["PagerNumber", 2109], + ["PagerNumberType", 0], + ["CellularNumber", 2110], + ["CellularNumberType", 0], + ["HomeAddress", 2111], + ["HomeAddress2", 2112], + ["HomeCity", 2113], + ["HomeState", 2114], + ["HomeZipCode", 2115], + ["HomeCountry", 2116], + ["WorkAddress", 2117], + ["WorkAddress2", 2118], + ["WorkCity", 2119], + ["WorkState", 2120], + ["WorkZipCode", 2121], + ["WorkCountry", 2122], + ["JobTitle", 2123], + ["Department", 2124], + ["Company", 2125], + ["WebPage1", 2126], + ["WebPage2", 2127], + ["BirthYear", 2128], + ["BirthMonth", 2129], + ["BirthDay", 2130], + ["Custom1", 2131], + ["Custom2", 2132], + ["Custom3", 2133], + ["Custom4", 2134], + ["Notes", 2135], + ["AnniversaryYear", 0], + ["AnniversaryMonth", 0], + ["AnniversaryDay", 0], + ["SpouseName", 0], + ["FamilyName", 0], +]; +const LINEBREAK = AppConstants.platform == "win" ? "\r\n" : "\n"; + +var AddrBookUtils = { + compareAddressBooks, + async exportDirectory(directory) { + let systemCharset = "utf-8"; + if (AppConstants.platform == "win") { + // Some Windows applications (notably Outlook) still don't understand + // UTF-8 encoding when importing address books and instead use the current + // operating system encoding. We can get that encoding from the registry. + let registryKey = Cc[ + "@mozilla.org/windows-registry-key;1" + ].createInstance(Ci.nsIWindowsRegKey); + registryKey.open( + Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, + "SYSTEM\\CurrentControlSet\\Control\\Nls\\CodePage", + Ci.nsIWindowsRegKey.ACCESS_READ + ); + let acpValue = registryKey.readStringValue("ACP"); + + // This data converts the registry key value into encodings that + // nsIConverterOutputStream understands. It is from + // https://github.com/hsivonen/encoding_rs/blob/c3eb642cdf3f17003b8dac95c8fff478568e46da/generate-encoding-data.py#L188 + systemCharset = + { + 866: "IBM866", + 874: "windows-874", + 932: "Shift_JIS", + 936: "GBK", + 949: "EUC-KR", + 950: "Big5", + 1200: "UTF-16LE", + 1201: "UTF-16BE", + 1250: "windows-1250", + 1251: "windows-1251", + 1252: "windows-1252", + 1253: "windows-1253", + 1254: "windows-1254", + 1255: "windows-1255", + 1256: "windows-1256", + 1257: "windows-1257", + 1258: "windows-1258", + 10000: "macintosh", + 10017: "x-mac-cyrillic", + 20866: "KOI8-R", + 20932: "EUC-JP", + 21866: "KOI8-U", + 28592: "ISO-8859-2", + 28593: "ISO-8859-3", + 28594: "ISO-8859-4", + 28595: "ISO-8859-5", + 28596: "ISO-8859-6", + 28597: "ISO-8859-7", + 28598: "ISO-8859-8", + 28600: "ISO-8859-10", + 28603: "ISO-8859-13", + 28604: "ISO-8859-14", + 28605: "ISO-8859-15", + 28606: "ISO-8859-16", + 38598: "ISO-8859-8-I", + 50221: "ISO-2022-JP", + 54936: "gb18030", + }[acpValue] || systemCharset; + } + + let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance( + Ci.nsIFilePicker + ); + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + + let title = bundle.formatStringFromName("ExportAddressBookNameTitle", [ + directory.dirName, + ]); + filePicker.init(Services.ww.activeWindow, title, Ci.nsIFilePicker.modeSave); + filePicker.defaultString = directory.dirName; + + let filterString; + // Since the list of file picker filters isn't fixed, keep track of which + // ones are added, so we can use them in the switch block below. + let activeFilters = []; + + // CSV + if (systemCharset != "utf-8") { + filterString = bundle.GetStringFromName("CSVFilesSysCharset"); + filePicker.appendFilter(filterString, "*.csv"); + activeFilters.push("CSVFilesSysCharset"); + } + filterString = bundle.GetStringFromName("CSVFilesUTF8"); + filePicker.appendFilter(filterString, "*.csv"); + activeFilters.push("CSVFilesUTF8"); + + // Tab separated + if (systemCharset != "utf-8") { + filterString = bundle.GetStringFromName("TABFilesSysCharset"); + filePicker.appendFilter(filterString, "*.tab; *.txt"); + activeFilters.push("TABFilesSysCharset"); + } + filterString = bundle.GetStringFromName("TABFilesUTF8"); + filePicker.appendFilter(filterString, "*.tab; *.txt"); + activeFilters.push("TABFilesUTF8"); + + // vCard + filterString = bundle.GetStringFromName("VCFFiles"); + filePicker.appendFilter(filterString, "*.vcf"); + activeFilters.push("VCFFiles"); + + // LDIF + filterString = bundle.GetStringFromName("LDIFFiles"); + filePicker.appendFilter(filterString, "*.ldi; *.ldif"); + activeFilters.push("LDIFFiles"); + + let rv = await new Promise(resolve => filePicker.open(resolve)); + if ( + rv == Ci.nsIFilePicker.returnCancel || + !filePicker.file || + !filePicker.file.path + ) { + return; + } + + if (rv == Ci.nsIFilePicker.returnReplace) { + if (filePicker.file.isFile()) { + filePicker.file.remove(false); + } + } + + let exportFile = filePicker.file.clone(); + let leafName = exportFile.leafName; + let output = ""; + let charset = "utf-8"; + + switch (activeFilters[filePicker.filterIndex]) { + case "CSVFilesSysCharset": + charset = systemCharset; + // Falls through. + case "CSVFilesUTF8": + if (!leafName.endsWith(".csv")) { + exportFile.leafName += ".csv"; + } + output = AddrBookUtils.exportDirectoryToDelimitedText(directory, ","); + break; + case "TABFilesSysCharset": + charset = systemCharset; + // Falls through. + case "TABFilesUTF8": + if (!leafName.endsWith(".txt") && !leafName.endsWith(".tab")) { + exportFile.leafName += ".txt"; + } + output = AddrBookUtils.exportDirectoryToDelimitedText(directory, "\t"); + break; + case "VCFFiles": + if (!leafName.endsWith(".vcf")) { + exportFile.leafName += ".vcf"; + } + output = AddrBookUtils.exportDirectoryToVCard(directory); + break; + case "LDIFFiles": + if (!leafName.endsWith(".ldi") && !leafName.endsWith(".ldif")) { + exportFile.leafName += ".ldif"; + } + output = AddrBookUtils.exportDirectoryToLDIF(directory); + break; + } + + if (charset == "utf-8") { + await IOUtils.writeUTF8(exportFile.path, output); + return; + } + + let outputFileStream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + outputFileStream.init(exportFile, -1, -1, 0); + let outputStream = Cc[ + "@mozilla.org/intl/converter-output-stream;1" + ].createInstance(Ci.nsIConverterOutputStream); + outputStream.init(outputFileStream, charset); + outputStream.writeString(output); + outputStream.close(); + }, + exportDirectoryToDelimitedText(directory, delimiter) { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/importMsgs.properties" + ); + let output = ""; + for (let i = 0; i < exportAttributes.length; i++) { + let [, plainTextStringID] = exportAttributes[i]; + if (plainTextStringID != 0) { + if (i != 0) { + output += delimiter; + } + output += bundle.GetStringFromID(plainTextStringID); + } + } + output += LINEBREAK; + for (let card of directory.childCards) { + if (card.isMailList) { + // .tab, .txt and .csv aren't able to export mailing lists. + // Use LDIF for that. + continue; + } + let propertyMap = card.supportsVCard + ? card.vCardProperties.toPropertyMap() + : null; + for (let i = 0; i < exportAttributes.length; i++) { + let [abPropertyName, plainTextStringID] = exportAttributes[i]; + if (plainTextStringID == 0) { + continue; + } + if (i != 0) { + output += delimiter; + } + let value; + if (propertyMap) { + value = propertyMap.get(abPropertyName); + } + if (!value) { + value = card.getProperty(abPropertyName, ""); + } + + // If a string contains at least one comma, tab, double quote or line + // break then we need to quote the entire string. Also if double quote + // is part of the string we need to quote the double quote(s) as well. + let needsQuotes = false; + if (value.includes('"')) { + needsQuotes = true; + value = value.replace(/"/g, '""'); + } else if (/[,\t\r\n]/.test(value)) { + needsQuotes = true; + } + if (needsQuotes) { + value = `"${value}"`; + } + + output += value; + } + output += LINEBREAK; + } + + return output; + }, + exportDirectoryToLDIF(directory) { + function appendProperty(name, value) { + if (!value) { + return; + } + // Follow RFC 2849 to determine if something is safe "as is" for LDIF. + // If not, base 64 encode it as UTF-8. + if ( + value[0] == " " || + value[0] == ":" || + value[0] == "<" || + /[\0\r\n\u0080-\uffff]/.test(value) + ) { + // Convert 16bit JavaScript string to a byteString, to make it work with + // btoa(). + let byteString = MailStringUtils.stringToByteString(value); + output += name + ":: " + btoa(byteString) + LINEBREAK; + } else { + output += name + ": " + value + LINEBREAK; + } + } + + function appendDNForCard(property, card, attrMap) { + let value = ""; + if (card.displayName) { + value += + attrMap.getFirstAttribute("DisplayName") + "=" + card.displayName; + } + if (card.primaryEmail) { + if (card.displayName) { + value += ","; + } + value += + attrMap.getFirstAttribute("PrimaryEmail") + "=" + card.primaryEmail; + } + appendProperty(property, value); + } + + let output = ""; + let attrMap = lazy.attrMapService.getMapForPrefBranch( + "ldap_2.servers.default.attrmap" + ); + + for (let card of directory.childCards) { + if (card.isMailList) { + appendDNForCard("dn", card, attrMap); + appendProperty("objectclass", "top"); + appendProperty("objectclass", "groupOfNames"); + appendProperty( + attrMap.getFirstAttribute("DisplayName"), + card.displayName + ); + if (card.getProperty("NickName", "")) { + appendProperty( + attrMap.getFirstAttribute("NickName"), + card.getProperty("NickName", "") + ); + } + if (card.getProperty("Notes", "")) { + appendProperty( + attrMap.getFirstAttribute("Notes"), + card.getProperty("Notes", "") + ); + } + let listAsDirectory = MailServices.ab.getDirectory(card.mailListURI); + for (let childCard of listAsDirectory.childCards) { + appendDNForCard("member", childCard, attrMap); + } + } else { + appendDNForCard("dn", card, attrMap); + appendProperty("objectclass", "top"); + appendProperty("objectclass", "person"); + appendProperty("objectclass", "organizationalPerson"); + appendProperty("objectclass", "inetOrgPerson"); + appendProperty("objectclass", "mozillaAbPersonAlpha"); + + let propertyMap = card.supportsVCard + ? card.vCardProperties.toPropertyMap() + : null; + for (let [abPropertyName] of exportAttributes) { + let attrName = attrMap.getFirstAttribute(abPropertyName); + if (attrName) { + let attrValue; + if (propertyMap) { + attrValue = propertyMap.get(abPropertyName); + } + if (!attrValue) { + attrValue = card.getProperty(abPropertyName, ""); + } + appendProperty(attrName, attrValue); + } + } + } + output += LINEBREAK; + } + + return output; + }, + exportDirectoryToVCard(directory) { + let output = ""; + for (let card of directory.childCards) { + if (!card.isMailList) { + // We don't know how to export mailing lists to vcf. + // Use LDIF for that. + output += decodeURIComponent(card.translateTo("vcard")); + } + } + return output; + }, + newUID, + SimpleEnumerator, +}; diff --git a/comm/mailnews/addrbook/modules/CardDAVDirectory.jsm b/comm/mailnews/addrbook/modules/CardDAVDirectory.jsm new file mode 100644 index 0000000000..e4361d53bb --- /dev/null +++ b/comm/mailnews/addrbook/modules/CardDAVDirectory.jsm @@ -0,0 +1,925 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["CardDAVDirectory"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +const { SQLiteDirectory } = ChromeUtils.import( + "resource:///modules/SQLiteDirectory.jsm" +); + +ChromeUtils.defineESModuleGetters(lazy, { + clearInterval: "resource://gre/modules/Timer.sys.mjs", + setInterval: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + CardDAVUtils: "resource:///modules/CardDAVUtils.jsm", + NotificationCallbacks: "resource:///modules/CardDAVUtils.jsm", + OAuth2Module: "resource:///modules/OAuth2Module.jsm", + OAuth2Providers: "resource:///modules/OAuth2Providers.jsm", + VCardProperties: "resource:///modules/VCardUtils.jsm", + VCardUtils: "resource:///modules/VCardUtils.jsm", +}); + +const PREFIX_BINDINGS = { + card: "urn:ietf:params:xml:ns:carddav", + cs: "http://calendarserver.org/ns/", + d: "DAV:", +}; +const NAMESPACE_STRING = Object.entries(PREFIX_BINDINGS) + .map(([prefix, url]) => `xmlns:${prefix}="${url}"`) + .join(" "); + +const log = console.createInstance({ + prefix: "carddav.sync", + maxLogLevel: "Warn", + maxLogLevelPref: "carddav.sync.loglevel", +}); + +/** + * Adds CardDAV sync to SQLiteDirectory. + */ +class CardDAVDirectory extends SQLiteDirectory { + /** nsIAbDirectory */ + + init(uri) { + super.init(uri); + + let serverURL = this._serverURL; + if (serverURL) { + // Google's server enforces some vCard 3.0-isms (or just fails badly if + // you don't provide exactly what it wants) so we use this property to + // determine when to do things differently. Cards from this directory + // inherit the same property. + if (this.getBoolValue("carddav.vcard3")) { + this._isGoogleCardDAV = true; + } else { + this._isGoogleCardDAV = serverURL.startsWith( + "https://www.googleapis.com/" + ); + if (this._isGoogleCardDAV) { + this.setBoolValue("carddav.vcard3", true); + } + } + + // If this directory is configured, start sync'ing with the server in 30s. + // Don't do this immediately, as this code runs at start-up and could + // impact performance if there are lots of changes to process. + if (this.getIntValue("carddav.syncinterval", 30) > 0) { + this._syncTimer = lazy.setTimeout(() => this.syncWithServer(), 30000); + } + } + + let uidsToSync = this.getStringValue("carddav.uidsToSync", ""); + if (uidsToSync) { + this._uidsToSync = new Set(uidsToSync.split(" ").filter(Boolean)); + this.setStringValue("carddav.uidsToSync", ""); + log.debug(`Retrieved list of cards to sync: ${uidsToSync}`); + } else { + this._uidsToSync = new Set(); + } + + let hrefsToRemove = this.getStringValue("carddav.hrefsToRemove", ""); + if (hrefsToRemove) { + this._hrefsToRemove = new Set(hrefsToRemove.split(" ").filter(Boolean)); + this.setStringValue("carddav.hrefsToRemove", ""); + log.debug(`Retrieved list of cards to remove: ${hrefsToRemove}`); + } else { + this._hrefsToRemove = new Set(); + } + } + async cleanUp() { + await super.cleanUp(); + + if (this._syncTimer) { + lazy.clearInterval(this._syncTimer); + this._syncTimer = null; + } + + if (this._uidsToSync.size) { + let uidsToSync = [...this._uidsToSync].join(" "); + this.setStringValue("carddav.uidsToSync", uidsToSync); + log.debug(`Stored list of cards to sync: ${uidsToSync}`); + } + if (this._hrefsToRemove.size) { + let hrefsToRemove = [...this._hrefsToRemove].join(" "); + this.setStringValue("carddav.hrefsToRemove", hrefsToRemove); + log.debug(`Stored list of cards to remove: ${hrefsToRemove}`); + } + } + + get propertiesChromeURI() { + return "chrome://messenger/content/addressbook/abCardDAVProperties.xhtml"; + } + get dirType() { + return Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE; + } + get supportsMailingLists() { + return false; + } + + modifyCard(card) { + // Well this is awkward. Because it's defined in nsIAbDirectory, + // modifyCard must not be async, but we need to do async operations. + let newCard = super.modifyCard(card); + this._modifyCard(newCard); + } + async _modifyCard(card) { + try { + await this._sendCardToServer(card); + } catch (ex) { + console.error(ex); + } + } + deleteCards(cards) { + super.deleteCards(cards); + this._deleteCards(cards); + } + async _deleteCards(cards) { + for (let card of cards) { + try { + await this._deleteCardFromServer(card); + } catch (ex) { + console.error(ex); + break; + } + } + + for (let card of cards) { + this._uidsToSync.delete(card.UID); + } + } + dropCard(card, needToCopyCard) { + // Ideally, we'd not add the card until it was on the server, but we have + // to return newCard synchronously. + let newCard = super.dropCard(card, needToCopyCard); + this._sendCardToServer(newCard).catch(console.error); + return newCard; + } + addMailList() { + throw Components.Exception( + "CardDAVDirectory does not implement addMailList", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + setIntValue(name, value) { + super.setIntValue(name, value); + + // Capture changes to the sync interval from the UI. + if (name == "carddav.syncinterval") { + this._scheduleNextSync(); + } + } + + /** CardDAV specific */ + _syncInProgress = false; + _syncTimer = null; + + get _serverURL() { + return this.getStringValue("carddav.url", ""); + } + get _syncToken() { + return this.getStringValue("carddav.token", ""); + } + set _syncToken(value) { + this.setStringValue("carddav.token", value); + } + + /** + * Wraps CardDAVUtils.makeRequest, resolving path this directory's server + * URL, and providing a mechanism to give a username and password specific + * to this directory. + * + * @param {string} path - A path relative to the server URL. + * @param {object} details - See CardDAVUtils.makeRequest. + * @returns {Promise<object>} - See CardDAVUtils.makeRequest. + */ + async _makeRequest(path, details = {}) { + let serverURI = Services.io.newURI(this._serverURL); + let uri = serverURI.resolve(path); + + if (!("_oAuth" in this)) { + if (lazy.OAuth2Providers.getHostnameDetails(serverURI.host)) { + this._oAuth = new lazy.OAuth2Module(); + this._oAuth.initFromABDirectory(this, serverURI.host); + } else { + this._oAuth = null; + } + } + details.oAuth = this._oAuth; + + let username = this.getStringValue("carddav.username", ""); + let callbacks = new lazy.NotificationCallbacks(username); + details.callbacks = callbacks; + + details.userContextId = + this._userContextId ?? lazy.CardDAVUtils.contextForUsername(username); + + let response; + try { + Services.obs.notifyObservers( + this, + "addrbook-directory-request-start", + this.UID + ); + response = await lazy.CardDAVUtils.makeRequest(uri, details); + } finally { + Services.obs.notifyObservers( + this, + "addrbook-directory-request-end", + this.UID + ); + } + if ( + details.expectedStatuses && + !details.expectedStatuses.includes(response.status) + ) { + throw Components.Exception( + `Incorrect response from server: ${response.status} ${response.statusText}`, + Cr.NS_ERROR_FAILURE + ); + } + + if (callbacks.shouldSaveAuth) { + // The user was prompted for a username and password. Save the response. + this.setStringValue("carddav.username", callbacks.authInfo?.username); + callbacks.saveAuth(); + } + return response; + } + + /** + * Gets or creates the path for storing this card on the server. Cards that + * already exist on the server have this value in the _href property. + * + * @param {nsIAbCard} card + * @returns {string} + */ + _getCardHref(card) { + let href = card.getProperty("_href", ""); + if (href) { + return href; + } + href = Services.io.newURI(this._serverURL).resolve(`${card.UID}.vcf`); + return new URL(href).pathname; + } + + _multigetRequest(hrefsToFetch) { + hrefsToFetch = hrefsToFetch.map( + href => ` <d:href>${xmlEncode(href)}</d:href>` + ); + let data = `<card:addressbook-multiget ${NAMESPACE_STRING}> + <d:prop> + <d:getetag/> + <card:address-data/> + </d:prop> + ${hrefsToFetch.join("\n")} + </card:addressbook-multiget>`; + + return this._makeRequest("", { + method: "REPORT", + body: data, + headers: { + Depth: 1, + }, + expectedStatuses: [207], + }); + } + + /** + * Performs a multiget request for the provided hrefs, and adds each response + * to the directory, adding or modifying as necessary. + * + * @param {string[]} hrefsToFetch - The href of each card to be requested. + */ + async _fetchAndStore(hrefsToFetch) { + if (hrefsToFetch.length == 0) { + return; + } + + let response = await this._multigetRequest(hrefsToFetch); + + // If this directory is set to read-only, the following operations would + // throw NS_ERROR_FAILURE, but sync operations are allowed on a read-only + // directory, so set this._overrideReadOnly to avoid the exception. + // + // Do not use await while it is set, and use a try/finally block to ensure + // it is cleared. + + try { + this._overrideReadOnly = true; + for (let { href, properties } of this._readResponse(response.dom)) { + if (!properties) { + continue; + } + + let etag = properties.querySelector("getetag")?.textContent; + let vCard = normalizeLineEndings( + properties.querySelector("address-data")?.textContent + ); + + let abCard = lazy.VCardUtils.vCardToAbCard(vCard); + abCard.setProperty("_etag", etag); + abCard.setProperty("_href", href); + + if (!this.cards.has(abCard.UID)) { + super.dropCard(abCard, false); + } else if (this.loadCardProperties(abCard.UID).get("_etag") != etag) { + super.modifyCard(abCard); + } + } + } finally { + this._overrideReadOnly = false; + } + } + + /** + * Reads a multistatus response, yielding once for each response element. + * + * @param {Document} dom - as returned by CardDAVUtils.makeRequest. + * @yields {object} - An object representing a single <response> element + * from the document: + * - href, the href of the object represented + * - notFound, if a 404 status applies to this response + * - properties, the <prop> element, if any, containing properties + * of the object represented + */ + _readResponse = function* (dom) { + if (!dom || dom.documentElement.localName != "multistatus") { + throw Components.Exception( + `Expected a multistatus response, but didn't get one`, + Cr.NS_ERROR_FAILURE + ); + } + + for (let r of dom.querySelectorAll("response")) { + let response = { + href: r.querySelector("href")?.textContent, + }; + + let responseStatus = r.querySelector("response > status"); + if (responseStatus?.textContent.startsWith("HTTP/1.1 404")) { + response.notFound = true; + yield response; + continue; + } + + for (let p of r.querySelectorAll("response > propstat")) { + let status = p.querySelector("propstat > status").textContent; + if (status == "HTTP/1.1 200 OK") { + response.properties = p.querySelector("propstat > prop"); + } + } + + yield response; + } + }; + + /** + * Converts the card to a vCard and performs a PUT request to store it on the + * server. Then immediately performs a GET request ensuring the local copy + * matches the server copy. Stores the card in the database on success. + * + * @param {nsIAbCard} card + * @returns {boolean} true if the PUT request succeeded without conflict, + * false if there was a conflict. + * @throws if the server responded with anything other than a success or + * conflict status code. + */ + async _sendCardToServer(card) { + let href = this._getCardHref(card); + let requestDetails = { + method: "PUT", + contentType: "text/vcard", + }; + + let vCard = card.getProperty("_vCard", ""); + if (this._isGoogleCardDAV) { + // There must be an `N` property, even if empty. + let vCardProperties = lazy.VCardProperties.fromVCard(vCard); + if (!vCardProperties.getFirstEntry("n")) { + vCardProperties.addValue("n", ["", "", "", "", ""]); + } + requestDetails.body = vCardProperties.toVCard(); + } else { + requestDetails.body = vCard; + } + + let response; + try { + log.debug(`Sending ${href} to server.`); + response = await this._makeRequest(href, requestDetails); + } catch (ex) { + Services.obs.notifyObservers(this, "addrbook-directory-sync-failed"); + this._uidsToSync.add(card.UID); + throw ex; + } + + if (response.status >= 400) { + throw Components.Exception( + `Sending card to the server failed, response was ${response.status} ${response.statusText}`, + Cr.NS_ERROR_FAILURE + ); + } + + // At this point we *should* be able to make a simple GET request and + // store the response. But Google moves the data (fair enough) without + // telling us where it went (c'mon, really?). Fortunately a multiget + // request at the original location works. + + response = await this._multigetRequest([href]); + + for (let { href, properties } of this._readResponse(response.dom)) { + if (!properties) { + continue; + } + + let etag = properties.querySelector("getetag")?.textContent; + let vCard = normalizeLineEndings( + properties.querySelector("address-data")?.textContent + ); + + let abCard = lazy.VCardUtils.vCardToAbCard(vCard); + abCard.setProperty("_etag", etag); + abCard.setProperty("_href", href); + + if (abCard.UID == card.UID) { + super.modifyCard(abCard); + } else { + // Add a property so the UI can work out if it's still displaying the + // old card and respond appropriately. + abCard.setProperty("_originalUID", card.UID); + super.dropCard(abCard, false); + super.deleteCards([card]); + } + } + } + + /** + * Deletes card from the server. + * + * @param {nsIAbCard|string} cardOrHRef + */ + async _deleteCardFromServer(cardOrHRef) { + let href; + if (typeof cardOrHRef == "string") { + href = cardOrHRef; + } else { + href = cardOrHRef.getProperty("_href", ""); + } + if (!href) { + return; + } + + try { + log.debug(`Removing ${href} from server.`); + await this._makeRequest(href, { method: "DELETE" }); + } catch (ex) { + Services.obs.notifyObservers(this, "addrbook-directory-sync-failed"); + this._hrefsToRemove.add(href); + throw ex; + } + } + + /** + * Set up a repeating timer for synchronisation with the server. The timer's + * interval is defined by pref, set it to 0 to disable sync'ing altogether. + */ + _scheduleNextSync() { + if (this._syncTimer) { + lazy.clearInterval(this._syncTimer); + this._syncTimer = null; + } + + let interval = this.getIntValue("carddav.syncinterval", 30); + if (interval <= 0) { + return; + } + + this._syncTimer = lazy.setInterval( + () => this.syncWithServer(false), + interval * 60000 + ); + } + + /** + * Get all cards on the server and add them to this directory. + * + * This is usually used for the initial population of a directory, but it + * can also be used for a complete re-sync. + */ + async fetchAllFromServer() { + log.log("Fetching all cards from the server."); + this._syncInProgress = true; + + let data = `<propfind xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}> + <prop> + <resourcetype/> + <getetag/> + <cs:getctag/> + </prop> + </propfind>`; + + let response = await this._makeRequest("", { + method: "PROPFIND", + body: data, + headers: { + Depth: 1, + }, + expectedStatuses: [207], + }); + + // A map of all existing hrefs and etags. If the etag for an href matches + // what we already have, we won't fetch it. + let currentHrefs = new Map( + Array.from(this.cards.values(), c => [c.get("_href"), c.get("_etag")]) + ); + + let hrefsToFetch = []; + for (let { href, properties } of this._readResponse(response.dom)) { + if (!properties || properties.querySelector("resourcetype collection")) { + continue; + } + + let currentEtag = currentHrefs.get(href); + currentHrefs.delete(href); + + let etag = properties.querySelector("getetag")?.textContent; + if (etag && currentEtag == etag) { + continue; + } + + hrefsToFetch.push(href); + } + + // Delete any existing cards we didn't see. They're not on the server so + // they shouldn't be on the client. + let cardsToDelete = []; + for (let href of currentHrefs.keys()) { + cardsToDelete.push(this.getCardFromProperty("_href", href, true)); + } + if (cardsToDelete.length > 0) { + super.deleteCards(cardsToDelete); + } + + // Fetch any cards we don't already have, or that have changed. + if (hrefsToFetch.length > 0) { + response = await this._multigetRequest(hrefsToFetch); + + let abCards = []; + + for (let { href, properties } of this._readResponse(response.dom)) { + if (!properties) { + continue; + } + + let etag = properties.querySelector("getetag")?.textContent; + let vCard = normalizeLineEndings( + properties.querySelector("address-data")?.textContent + ); + + try { + let abCard = lazy.VCardUtils.vCardToAbCard(vCard); + abCard.setProperty("_etag", etag); + abCard.setProperty("_href", href); + abCards.push(abCard); + } catch (ex) { + log.error(`Error parsing: ${vCard}`); + console.error(ex); + } + } + + await this.bulkAddCards(abCards); + } + + await this._getSyncToken(); + + log.log("Sync with server completed successfully."); + Services.obs.notifyObservers(this, "addrbook-directory-synced"); + + this._scheduleNextSync(); + this._syncInProgress = false; + } + + /** + * Begin a sync operation. This function will decide which sync protocol to + * use based on the directory's configuration. It will also (re)start the + * timer for the next synchronisation unless told not to. + * + * @param {boolean} shouldResetTimer + */ + async syncWithServer(shouldResetTimer = true) { + if (this._syncInProgress || !this._serverURL) { + return; + } + + log.log("Performing sync with server."); + this._syncInProgress = true; + + try { + // First perform all pending removals. We don't want to have deleted cards + // reappearing when we sync. + for (let href of this._hrefsToRemove) { + await this._deleteCardFromServer(href); + } + this._hrefsToRemove.clear(); + + // Now update any cards that were modified while not connected to the server. + for (let uid of this._uidsToSync) { + let card = this.getCard(uid); + // The card may no longer exist. It shouldn't still be listed to send, + // but it might be. + if (card) { + await this._sendCardToServer(card); + } + } + this._uidsToSync.clear(); + + if (this._syncToken) { + await this.updateAllFromServerV2(); + } else { + await this.updateAllFromServerV1(); + } + } catch (ex) { + log.error("Sync with server failed."); + throw ex; + } finally { + if (shouldResetTimer) { + this._scheduleNextSync(); + } + this._syncInProgress = false; + } + } + + /** + * Compares cards in the directory with cards on the server, and updates the + * directory to match what is on the server. + */ + async updateAllFromServerV1() { + let data = `<propfind xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}> + <prop> + <resourcetype/> + <getetag/> + <cs:getctag/> + </prop> + </propfind>`; + + let response = await this._makeRequest("", { + method: "PROPFIND", + body: data, + headers: { + Depth: 1, + }, + expectedStatuses: [207], + }); + + let hrefMap = new Map(); + for (let { href, properties } of this._readResponse(response.dom)) { + if ( + !properties || + !properties.querySelector("resourcetype") || + properties.querySelector("resourcetype collection") + ) { + continue; + } + + let etag = properties.querySelector("getetag").textContent; + hrefMap.set(href, etag); + } + + let cardMap = new Map(); + let hrefsToFetch = []; + let cardsToDelete = []; + for (let card of this.childCards) { + let href = card.getProperty("_href", ""); + let etag = card.getProperty("_etag", ""); + + if (!href || !etag) { + // Not sure how we got here. Ignore it. + continue; + } + cardMap.set(href, card); + if (hrefMap.has(href)) { + if (hrefMap.get(href) != etag) { + // The card was updated on server. + hrefsToFetch.push(href); + } + } else { + // The card doesn't exist on the server. + cardsToDelete.push(card); + } + } + + for (let href of hrefMap.keys()) { + if (!cardMap.has(href)) { + // The card is new on the server. + hrefsToFetch.push(href); + } + } + + // If this directory is set to read-only, the following operations would + // throw NS_ERROR_FAILURE, but sync operations are allowed on a read-only + // directory, so set this._overrideReadOnly to avoid the exception. + // + // Do not use await while it is set, and use a try/finally block to ensure + // it is cleared. + + if (cardsToDelete.length > 0) { + this._overrideReadOnly = true; + try { + super.deleteCards(cardsToDelete); + } finally { + this._overrideReadOnly = false; + } + } + + await this._fetchAndStore(hrefsToFetch); + + log.log("Sync with server completed successfully."); + Services.obs.notifyObservers(this, "addrbook-directory-synced"); + } + + /** + * Retrieves the current sync token from the server. + * + * @see RFC 6578 + */ + async _getSyncToken() { + log.log("Fetching new sync token"); + + let data = `<propfind xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}> + <prop> + <displayname/> + <cs:getctag/> + <sync-token/> + </prop> + </propfind>`; + + let response = await this._makeRequest("", { + method: "PROPFIND", + body: data, + headers: { + Depth: 0, + }, + }); + + if (response.status == 207) { + for (let { properties } of this._readResponse(response.dom)) { + let token = properties?.querySelector("prop sync-token"); + if (token) { + this._syncToken = token.textContent; + return; + } + } + } + + this._syncToken = ""; + } + + /** + * Gets a list of changes on the server since the last call to getSyncToken + * or updateAllFromServerV2, and updates the directory to match what is on + * the server. + * + * @see RFC 6578 + */ + async updateAllFromServerV2() { + let syncToken = this._syncToken; + if (!syncToken) { + throw new Components.Exception("No sync token", Cr.NS_ERROR_UNEXPECTED); + } + + let data = `<sync-collection xmlns="${ + PREFIX_BINDINGS.d + }" ${NAMESPACE_STRING}> + <sync-token>${xmlEncode(syncToken)}</sync-token> + <sync-level>1</sync-level> + <prop> + <getetag/> + <card:address-data/> + </prop> + </sync-collection>`; + + let response = await this._makeRequest("", { + method: "REPORT", + body: data, + headers: { + Depth: 1, // Only Google seems to need this. + }, + expectedStatuses: [207, 400], + }); + + if (response.status == 400) { + log.warn( + `Server responded with: ${response.status} ${response.statusText}` + ); + await this.fetchAllFromServer(); + return; + } + + let dom = response.dom; + + // If this directory is set to read-only, the following operations would + // throw NS_ERROR_FAILURE, but sync operations are allowed on a read-only + // directory, so set this._overrideReadOnly to avoid the exception. + // + // Do not use await while it is set, and use a try/finally block to ensure + // it is cleared. + + let hrefsToFetch = []; + try { + this._overrideReadOnly = true; + let cardsToDelete = []; + for (let { href, notFound, properties } of this._readResponse(dom)) { + let card = this.getCardFromProperty("_href", href, true); + if (notFound) { + if (card) { + cardsToDelete.push(card); + } + continue; + } + if (!properties) { + continue; + } + + let etag = properties.querySelector("getetag")?.textContent; + if (!etag) { + continue; + } + let vCard = properties.querySelector("address-data")?.textContent; + if (!vCard) { + hrefsToFetch.push(href); + continue; + } + vCard = normalizeLineEndings(vCard); + + let abCard = lazy.VCardUtils.vCardToAbCard(vCard); + abCard.setProperty("_etag", etag); + abCard.setProperty("_href", href); + + if (card) { + if (card.getProperty("_etag", "") != etag) { + super.modifyCard(abCard); + } + } else { + super.dropCard(abCard, false); + } + } + + if (cardsToDelete.length > 0) { + super.deleteCards(cardsToDelete); + } + } finally { + this._overrideReadOnly = false; + } + + await this._fetchAndStore(hrefsToFetch); + + this._syncToken = dom.querySelector("sync-token").textContent; + + log.log("Sync with server completed successfully."); + Services.obs.notifyObservers(this, "addrbook-directory-synced"); + } + + static forFile(fileName) { + let directory = super.forFile(fileName); + if (directory instanceof CardDAVDirectory) { + return directory; + } + return undefined; + } +} +CardDAVDirectory.prototype.classID = Components.ID( + "{1fa9941a-07d5-4a6f-9673-15327fc2b9ab}" +); + +/** + * Ensure that `string` always has Windows line-endings. Some functions, + * notably DOMParser.parseFromString, strip \r, but we want it because \r\n + * is a part of the vCard specification. + */ +function normalizeLineEndings(string) { + if (string.includes("\r\n")) { + return string; + } + return string.replace(/\n/g, "\r\n"); +} + +/** + * Encode special characters safely for XML. + */ +function xmlEncode(string) { + return string + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/</g, "<") + .replace(/>/g, ">"); +} diff --git a/comm/mailnews/addrbook/modules/CardDAVUtils.jsm b/comm/mailnews/addrbook/modules/CardDAVUtils.jsm new file mode 100644 index 0000000000..d45b5a9b42 --- /dev/null +++ b/comm/mailnews/addrbook/modules/CardDAVUtils.jsm @@ -0,0 +1,718 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["CardDAVUtils", "NotificationCallbacks"]; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm"); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ContextualIdentityService: + "resource://gre/modules/ContextualIdentityService.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + CardDAVDirectory: "resource:///modules/CardDAVDirectory.jsm", + MsgAuthPrompt: "resource:///modules/MsgAsyncPrompter.jsm", + OAuth2: "resource:///modules/OAuth2.jsm", + OAuth2Providers: "resource:///modules/OAuth2Providers.jsm", +}); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "nssErrorsService", + "@mozilla.org/nss_errors_service;1", + "nsINSSErrorsService" +); + +// Use presets only where DNS discovery fails. Set to null to prevent +// auto-fill completely for a domain. +const PRESETS = { + // For testing purposes. + "bad.invalid": null, + // Google responds correctly but the provided address returns 404. + "gmail.com": "https://www.googleapis.com", + "googlemail.com": "https://www.googleapis.com", + // For testing purposes. + "test.invalid": "http://localhost:9999", + // Yahoo! OAuth is not working yet. + "yahoo.com": null, +}; + +// At least one of these ACL privileges must be present to consider an address +// book writable. +const writePrivs = ["write", "write-properties", "write-content", "all"]; + +// At least one of these ACL privileges must be present to consider an address +// book readable. +const readPrivs = ["read", "all"]; + +var CardDAVUtils = { + _contextMap: new Map(), + + /** + * Returns the id of a unique private context for each username. When the + * userContextId is set on a principal, this allows the use of multiple + * usernames on the same server without the networking code causing issues. + * + * @param {string} username + * @returns {integer} + */ + contextForUsername(username) { + if (username && CardDAVUtils._contextMap.has(username)) { + return CardDAVUtils._contextMap.get(username); + } + + // This could be any 32-bit integer, as long as it isn't already in use. + let nextId = 25000 + CardDAVUtils._contextMap.size; + lazy.ContextualIdentityService.remove(nextId); + CardDAVUtils._contextMap.set(username, nextId); + return nextId; + }, + + /** + * Make an HTTP request. If the request needs a username and password, the + * given authPrompt is called. + * + * @param {string} uri + * @param {object} details + * @param {string} [details.method] + * @param {object} [details.headers] + * @param {string} [details.body] + * @param {string} [details.contentType] + * @param {msgIOAuth2Module} [details.oAuth] - If this is present the + * request will use OAuth2 authorization. + * @param {NotificationCallbacks} [details.callbacks] - Handles usernames + * and passwords for this request. + * @param {integer} [details.userContextId] - See _contextForUsername. + * + * @returns {Promise<object>} - Resolves to an object with getters for: + * - status, the HTTP response code + * - statusText, the HTTP response message + * - text, the returned data as a String + * - dom, the returned data parsed into a Document + */ + async makeRequest(uri, details) { + if (typeof uri == "string") { + uri = Services.io.newURI(uri); + } + let { + method = "GET", + headers = {}, + body = null, + contentType = "text/xml", + oAuth = null, + callbacks = new NotificationCallbacks(), + userContextId = Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID, + } = details; + headers["Content-Type"] = contentType; + if (oAuth) { + headers.Authorization = await new Promise((resolve, reject) => { + oAuth.connect(true, { + onSuccess(token) { + resolve( + // `token` is a base64-encoded string for SASL XOAUTH2. That is + // not what we want, extract just the Bearer token part. + // (See OAuth2Module.connect.) + atob(token).split("\x01")[1].slice(5) + ); + }, + onFailure: reject, + }); + }); + } + + return new Promise((resolve, reject) => { + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + { userContextId } + ); + + let channel = Services.io.newChannelFromURI( + uri, + null, + principal, + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + channel.QueryInterface(Ci.nsIHttpChannel); + for (let [name, value] of Object.entries(headers)) { + channel.setRequestHeader(name, value, false); + } + if (body !== null) { + let stream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( + Ci.nsIStringInputStream + ); + stream.setUTF8Data(body, body.length); + + channel.QueryInterface(Ci.nsIUploadChannel); + channel.setUploadStream(stream, contentType, -1); + } + channel.requestMethod = method; // Must go after setUploadStream. + channel.notificationCallbacks = callbacks; + + let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance( + Ci.nsIStreamLoader + ); + listener.init({ + onStreamComplete(loader, context, status, resultLength, result) { + let finalChannel = loader.request.QueryInterface(Ci.nsIHttpChannel); + if (!Components.isSuccessCode(status)) { + let isCertError = false; + try { + let errorType = lazy.nssErrorsService.getErrorClass(status); + if (errorType == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) { + isCertError = true; + } + } catch (ex) { + // nsINSSErrorsService.getErrorClass throws if given a non-TLS, + // non-cert error, so ignore this. + } + + if (isCertError && finalChannel.securityInfo) { + let secInfo = finalChannel.securityInfo.QueryInterface( + Ci.nsITransportSecurityInfo + ); + let params = { + exceptionAdded: false, + securityInfo: secInfo, + prefetchCert: true, + location: finalChannel.originalURI.displayHostPort, + }; + Services.wm + .getMostRecentWindow("") + .openDialog( + "chrome://pippki/content/exceptionDialog.xhtml", + "", + "chrome,centerscreen,modal", + params + ); + + if (params.exceptionAdded) { + // Try again now that an exception has been added. + CardDAVUtils.makeRequest(uri, details).then(resolve, reject); + return; + } + } + + reject(new Components.Exception("Connection failure", status)); + return; + } + if (finalChannel.responseStatus == 401) { + // We tried to authenticate, but failed. + reject( + new Components.Exception( + "Authorization failure", + Cr.NS_ERROR_FAILURE + ) + ); + return; + } + resolve({ + get status() { + return finalChannel.responseStatus; + }, + get statusText() { + return finalChannel.responseStatusText; + }, + get text() { + return new TextDecoder().decode(Uint8Array.from(result)); + }, + get dom() { + if (this._dom === undefined) { + try { + this._dom = new DOMParser().parseFromString( + this.text, + "text/xml" + ); + } catch (ex) { + this._dom = null; + } + } + return this._dom; + }, + }); + }, + }); + channel.asyncOpen(listener, channel); + }); + }, + + /** + * @typedef foundBook + * @property {URL} url - The address for this address book. + * @param {string} name - The name of this address book on the server. + * @param {Function} create - A callback to add this address book locally. + */ + + /** + * Uses DNS look-ups and magic URLs to detects CardDAV address books. + * + * @param {string} username - Username for the server at `location`. + * @param {string} [password] - If not given, the user will be prompted. + * @param {string} location - The URL of a server to query. + * @param {boolean} [forcePrompt=false] - If true, the user will be shown a + * login prompt even if `password` is specified. If false, the user will + * be shown a prompt only if `password` is not specified and no saved + * password matches `username` and `location`. + * @returns {foundBook[]} - An array of found address books. + */ + async detectAddressBooks(username, password, location, forcePrompt = false) { + let log = console.createInstance({ + prefix: "carddav.setup", + maxLogLevel: "Warn", + maxLogLevelPref: "carddav.setup.loglevel", + }); + + // Use a unique context for each attempt, so a prompt is always shown. + let userContextId = Math.floor(Date.now() / 1000); + + let url = new URL(location); + + if (url.hostname in PRESETS) { + if (PRESETS[url.hostname] === null) { + throw new Components.Exception( + `${url} is known to be incompatible`, + Cr.NS_ERROR_NOT_AVAILABLE + ); + } + log.log(`Using preset URL for ${url}`); + url = new URL(PRESETS[url.hostname]); + } + + if (url.pathname == "/" && !(url.hostname in PRESETS)) { + log.log(`Looking up DNS record for ${url.hostname}`); + let domain = `_carddavs._tcp.${url.hostname}`; + let srvRecords = await DNS.srv(domain); + srvRecords.sort((a, b) => a.prio - b.prio || b.weight - a.weight); + + if (srvRecords[0]) { + url = new URL(`https://${srvRecords[0].host}:${srvRecords[0].port}`); + log.log(`Found a DNS SRV record pointing to ${url.host}`); + + let txtRecords = await DNS.txt(domain); + txtRecords.sort((a, b) => a.prio - b.prio || b.weight - a.weight); + txtRecords = txtRecords.filter(result => + result.data.startsWith("path=") + ); + + if (txtRecords[0]) { + url.pathname = txtRecords[0].data.substr(5); + log.log(`Found a DNS TXT record pointing to ${url.href}`); + } + } else { + let mxRecords = await DNS.mx(url.hostname); + if (mxRecords.some(r => /\bgoogle\.com$/.test(r.host))) { + log.log( + `Found a DNS MX record for Google, using preset URL for ${url}` + ); + url = new URL(PRESETS["gmail.com"]); + } + } + } + + let oAuth = null; + let callbacks = new NotificationCallbacks(username, password, forcePrompt); + + let requestParams = { + method: "PROPFIND", + callbacks, + userContextId, + headers: { + Depth: 0, + }, + body: `<propfind xmlns="DAV:"> + <prop> + <resourcetype/> + <displayname/> + <current-user-principal/> + <current-user-privilege-set/> + </prop> + </propfind>`, + }; + + let details = lazy.OAuth2Providers.getHostnameDetails(url.host); + if (details) { + let [issuer, scope] = details; + let issuerDetails = lazy.OAuth2Providers.getIssuerDetails(issuer); + + oAuth = new lazy.OAuth2(scope, issuerDetails); + oAuth._isNew = true; + oAuth._loginOrigin = `oauth://${issuer}`; + oAuth._scope = scope; + for (let login of Services.logins.findLogins( + oAuth._loginOrigin, + null, + "" + )) { + if ( + login.username == username && + (login.httpRealm == scope || + login.httpRealm.split(" ").includes(scope)) + ) { + oAuth.refreshToken = login.password; + oAuth._isNew = false; + break; + } + } + + if (username) { + oAuth.extraAuthParams = [["login_hint", username]]; + } + + // Implement msgIOAuth2Module.connect, which CardDAVUtils.makeRequest expects. + requestParams.oAuth = { + QueryInterface: ChromeUtils.generateQI(["msgIOAuth2Module"]), + connect(withUI, listener) { + oAuth.connect( + () => + listener.onSuccess( + // String format based on what OAuth2Module has. + btoa(`\x01auth=Bearer ${oAuth.accessToken}`) + ), + () => listener.onFailure(Cr.NS_ERROR_ABORT), + withUI, + false + ); + }, + }; + } + + let response; + let triedURLs = new Set(); + async function tryURL(url) { + if (triedURLs.has(url)) { + return; + } + triedURLs.add(url); + + log.log(`Attempting to connect to ${url}`); + response = await CardDAVUtils.makeRequest(url, requestParams); + if (response.status == 207 && response.dom) { + log.log(`${url} ... success`); + } else { + log.log( + `${url} ... response was "${response.status} ${response.statusText}"` + ); + response = null; + } + } + + if (url.pathname != "/") { + // This might be the full URL of an address book. + await tryURL(url.href); + if ( + !response?.dom?.querySelector("resourcetype addressbook") && + !response?.dom?.querySelector("current-user-principal href") + ) { + response = null; + } + } + if (!response || !response.dom) { + // Auto-discovery using a magic URL. + requestParams.body = `<propfind xmlns="DAV:"> + <prop> + <current-user-principal/> + </prop> + </propfind>`; + await tryURL(`${url.origin}/.well-known/carddav`); + } + if (!response) { + // Auto-discovery at the root of the domain. + await tryURL(`${url.origin}/`); + } + if (!response) { + // We've run out of ideas. + throw new Components.Exception( + "Address book discovery failed", + Cr.NS_ERROR_FAILURE + ); + } + + if (!response.dom.querySelector("resourcetype addressbook")) { + let userPrincipal = response.dom.querySelector( + "current-user-principal href" + ); + if (!userPrincipal) { + // We've run out of ideas. + throw new Components.Exception( + "Address book discovery failed", + Cr.NS_ERROR_FAILURE + ); + } + // Steps two and three of auto-discovery. If the entered URL did point + // to an address book, we won't get here. + url = new URL(userPrincipal.textContent, url); + requestParams.body = `<propfind xmlns="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"> + <prop> + <card:addressbook-home-set/> + </prop> + </propfind>`; + await tryURL(url.href); + + url = new URL( + response.dom.querySelector("addressbook-home-set href").textContent, + url + ); + requestParams.headers.Depth = 1; + requestParams.body = `<propfind xmlns="DAV:"> + <prop> + <resourcetype/> + <displayname/> + <current-user-privilege-set/> + </prop> + </propfind>`; + await tryURL(url.href); + } + + // Find any directories in the response. + + let foundBooks = []; + for (let r of response.dom.querySelectorAll("response")) { + if (r.querySelector("status")?.textContent != "HTTP/1.1 200 OK") { + continue; + } + if (!r.querySelector("resourcetype addressbook")) { + continue; + } + + // If the server provided ACL information, skip address books that we do + // not have read privileges to. + let privNode = r.querySelector("current-user-privilege-set"); + let isWritable = false; + let isReadable = false; + if (privNode) { + let privs = Array.from(privNode.querySelectorAll("privilege > *")).map( + node => node.localName + ); + + isWritable = writePrivs.some(priv => privs.includes(priv)); + isReadable = readPrivs.some(priv => privs.includes(priv)); + + if (!isWritable && !isReadable) { + continue; + } + } + + url = new URL(r.querySelector("href").textContent, url); + let name = r.querySelector("displayname")?.textContent; + if (!name) { + // The server didn't give a name, let's make one from the path. + name = url.pathname.replace(/\/$/, "").split("/").slice(-1)[0]; + } + if (!name) { + // That didn't work either, use the hostname. + name = url.hostname; + } + foundBooks.push({ + url, + name, + create() { + let dirPrefId = MailServices.ab.newAddressBook( + this.name, + null, + Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE, + null + ); + let book = MailServices.ab.getDirectoryFromId(dirPrefId); + book.setStringValue("carddav.url", this.url); + + if (!isWritable && isReadable) { + book.setBoolValue("readOnly", true); + } + + if (oAuth) { + if (oAuth._isNew) { + log.log(`Saving refresh token for ${username}`); + let newLoginInfo = Cc[ + "@mozilla.org/login-manager/loginInfo;1" + ].createInstance(Ci.nsILoginInfo); + newLoginInfo.init( + oAuth._loginOrigin, + null, + oAuth._scope, + username, + oAuth.refreshToken, + "", + "" + ); + try { + Services.logins.addLogin(newLoginInfo); + } catch (ex) { + console.error(ex); + } + oAuth._isNew = false; + } + book.setStringValue("carddav.username", username); + } else if (callbacks.authInfo?.username) { + log.log(`Saving login info for ${callbacks.authInfo.username}`); + book.setStringValue( + "carddav.username", + callbacks.authInfo.username + ); + callbacks.saveAuth(); + } + + let dir = lazy.CardDAVDirectory.forFile(book.fileName); + // Pass the context to the created address book. This prevents asking + // for a username/password again in the case that we didn't save it. + // The user won't be prompted again until Thunderbird is restarted. + dir._userContextId = userContextId; + dir.fetchAllFromServer(); + + return dir; + }, + }); + } + return foundBooks; + }, +}; + +/** + * Passed to nsIChannel.notificationCallbacks in CardDAVDirectory.makeRequest. + * This handles HTTP authentication, prompting the user if necessary. It also + * ensures important headers are copied from one channel to another if a + * redirection occurs. + * + * @implements {nsIInterfaceRequestor} + * @implements {nsIAuthPrompt2} + * @implements {nsIChannelEventSink} + */ +class NotificationCallbacks { + /** + * @param {string} [username] - Used to pre-fill any auth dialogs. + * @param {string} [password] - Used to pre-fill any auth dialogs. + * @param {boolean} [forcePrompt] - Skips checking the password manager for + * a password, even if username is given. The user will be prompted. + */ + constructor(username, password, forcePrompt) { + this.username = username; + this.password = password; + this.forcePrompt = forcePrompt; + } + QueryInterface = ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIAuthPrompt2", + "nsIChannelEventSink", + ]); + getInterface = ChromeUtils.generateQI([ + "nsIAuthPrompt2", + "nsIChannelEventSink", + ]); + promptAuth(channel, level, authInfo) { + if (authInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) { + return false; + } + + this.origin = channel.URI.prePath; + this.authInfo = authInfo; + + if (!this.forcePrompt) { + if (this.username && this.password) { + authInfo.username = this.username; + authInfo.password = this.password; + this.shouldSaveAuth = true; + return true; + } + + let logins = Services.logins.findLogins(channel.URI.prePath, null, ""); + for (let l of logins) { + if (l.username == this.username) { + authInfo.username = l.username; + authInfo.password = l.password; + return true; + } + } + } + + authInfo.username = this.username; + authInfo.password = this.password; + + let savePasswordLabel = null; + let savePassword = {}; + if (Services.prefs.getBoolPref("signon.rememberSignons", true)) { + savePasswordLabel = Services.strings + .createBundle("chrome://passwordmgr/locale/passwordmgr.properties") + .GetStringFromName("rememberPassword"); + savePassword.value = true; + } + + let returnValue = new lazy.MsgAuthPrompt().promptAuth( + channel, + level, + authInfo, + savePasswordLabel, + savePassword + ); + if (returnValue) { + this.shouldSaveAuth = savePassword.value; + } + return returnValue; + } + saveAuth() { + if (this.shouldSaveAuth) { + let newLoginInfo = Cc[ + "@mozilla.org/login-manager/loginInfo;1" + ].createInstance(Ci.nsILoginInfo); + newLoginInfo.init( + this.origin, + null, + this.authInfo.realm, + this.authInfo.username, + this.authInfo.password, + "", + "" + ); + try { + Services.logins.addLogin(newLoginInfo); + } catch (ex) { + console.error(ex); + } + } + } + asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) { + /** + * Copy the given header from the old channel to the new one, ignoring missing headers + * + * @param {string} header - The header to copy + */ + function copyHeader(header) { + try { + let headerValue = oldChannel.getRequestHeader(header); + if (headerValue) { + newChannel.setRequestHeader(header, headerValue, false); + } + } catch (e) { + if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) { + // The header could possibly not be available, ignore that + // case but throw otherwise + throw e; + } + } + } + + // Make sure we can get/set headers on both channels. + newChannel.QueryInterface(Ci.nsIHttpChannel); + oldChannel.QueryInterface(Ci.nsIHttpChannel); + + // If any other header is used, it should be added here. We might want + // to just copy all headers over to the new channel. + copyHeader("Authorization"); + copyHeader("Depth"); + copyHeader("Originator"); + copyHeader("Recipient"); + copyHeader("If-None-Match"); + copyHeader("If-Match"); + + newChannel.requestMethod = oldChannel.requestMethod; + callback.onRedirectVerifyCallback(Cr.NS_OK); + } +} diff --git a/comm/mailnews/addrbook/modules/LDAPClient.jsm b/comm/mailnews/addrbook/modules/LDAPClient.jsm new file mode 100644 index 0000000000..e26b7b5fce --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPClient.jsm @@ -0,0 +1,285 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPClient"]; + +var { CommonUtils } = ChromeUtils.importESModule( + "resource://services-common/utils.sys.mjs" +); +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); +var { + AbandonRequest, + BindRequest, + UnbindRequest, + SearchRequest, + LDAPResponse, +} = ChromeUtils.import("resource:///modules/LDAPMessage.jsm"); + +class LDAPClient { + /** + * @param {string} host - The LDAP server host. + * @param {number} port - The LDAP server port. + * @param {boolean} useSecureTransport - Whether to use TLS connection. + */ + constructor(host, port, useSecureTransport) { + this.onOpen = () => {}; + this.onError = () => {}; + + this._host = host; + this._port = port; + this._useSecureTransport = useSecureTransport; + + this._messageId = 1; + this._callbackMap = new Map(); + + this._logger = console.createInstance({ + prefix: "mailnews.ldap", + maxLogLevel: "Warn", + maxLogLevelPref: "mailnews.ldap.loglevel", + }); + + this._dataEventsQueue = []; + } + + connect() { + let hostname = this._host.toLowerCase(); + this._logger.debug( + `Connecting to ${ + this._useSecureTransport ? "ldaps" : "ldap" + }://${hostname}:${this._port}` + ); + this._socket = new TCPSocket(hostname, this._port, { + binaryType: "arraybuffer", + useSecureTransport: this._useSecureTransport, + }); + this._socket.onopen = this._onOpen; + this._socket.onerror = this._onError; + } + + /** + * Send a simple bind request to the server. + * + * @param {string} dn - The name to bind. + * @param {string} password - The password. + * @param {Function} callback - Callback function when receiving BindResponse. + * @returns {number} The id of the sent request. + */ + bind(dn, password, callback) { + this._logger.debug(`Binding ${dn}`); + let req = new BindRequest(dn || "", password || ""); + return this._send(req, callback); + } + + /** + * Send a SASL bind request to the server. + * + * @param {string} service - The service host name to bind. + * @param {string} mechanism - The SASL mechanism to use, e.g. GSSAPI. + * @param {string} authModuleType - The auth module type, @see nsIMailAuthModule. + * @param {ArrayBuffer} serverCredentials - The challenge token returned from + * the server, which must be used to generate a new request token. Or + * undefined for the first request. + * @param {Function} callback - Callback function when receiving BindResponse. + * @returns {number} The id of the sent request. + */ + saslBind(service, mechanism, authModuleType, serverCredentials, callback) { + this._logger.debug(`Binding ${service} using ${mechanism}`); + if (!this._authModule || this._authModuleType != authModuleType) { + this._authModuleType = authModuleType; + this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance( + Ci.nsIMailAuthModule + ); + this._authModule.init( + authModuleType, + service, + 0, // nsIAuthModule::REQ_DEFAULT + null, // domain + null, // username + null // password + ); + } + // getNextToken expects a base64 string. + let token = this._authModule.getNextToken( + serverCredentials + ? btoa( + CommonUtils.arrayBufferToByteString( + new Uint8Array(serverCredentials) + ) + ) + : "" + ); + // token is a base64 string, convert it to Uint8Array. + let credentials = CommonUtils.byteStringToArrayBuffer(atob(token)); + let req = new BindRequest("", "", { mechanism, credentials }); + return this._send(req, callback); + } + + /** + * Send an unbind request to the server. + */ + unbind() { + return this._send(new UnbindRequest(), () => this._socket.close()); + } + + /** + * Send a search request to the server. + * + * @param {string} dn - The name to search. + * @param {number} scope - The scope to search. + * @param {string} filter - The filter string. + * @param {string} attributes - Attributes to include in the search result. + * @param {number} timeout - The seconds to wait. + * @param {number} limit - Maximum number of entries to return. + * @param {Function} callback - Callback function when receiving search responses. + * @returns {number} The id of the sent request. + */ + search(dn, scope, filter, attributes, timeout, limit, callback) { + this._logger.debug(`Searching dn="${dn}" filter="${filter}"`); + let req = new SearchRequest(dn, scope, filter, attributes, timeout, limit); + return this._send(req, callback); + } + + /** + * Send an abandon request to the server. + * + * @param {number} messageId - The id of the message to abandon. + */ + abandon(messageId) { + this._logger.debug(`Abandoning ${messageId}`); + this._callbackMap.delete(messageId); + let req = new AbandonRequest(messageId); + this._send(req); + } + + /** + * The open event handler. + */ + _onOpen = () => { + this._logger.debug("Connected"); + this._socket.ondata = this._onData; + this._socket.onclose = this._onClose; + this.onOpen(); + }; + + /** + * The data event handler. Server may send multiple data events after a + * search, we want to handle them asynchonosly and in sequence. + * + * @param {TCPSocketEvent} event - The data event. + */ + _onData = async event => { + if (this._processingData) { + this._dataEventsQueue.push(event); + return; + } + this._processingData = true; + let data = event.data; + if (this._buffer) { + // Concatenate left over data from the last event with the new data. + let arr = new Uint8Array(this._buffer.byteLength + data.byteLength); + arr.set(new Uint8Array(this._buffer)); + arr.set(new Uint8Array(data), this._buffer.byteLength); + data = arr.buffer; + this._buffer = null; + } + let i = 0; + // The payload can contain multiple messages, parse it to the end. + while (data.byteLength) { + i++; + let res; + try { + res = LDAPResponse.fromBER(data); + if (typeof res == "number") { + data = data.slice(res); + continue; + } + } catch (e) { + if (e.result == Cr.NS_ERROR_CANNOT_CONVERT_DATA) { + // The remaining data doesn't form a valid LDAP message, save it for + // the next round. + this._buffer = data; + this._handleNextDataEvent(); + return; + } + throw e; + } + this._logger.debug( + `S: [${res.messageId}] ${res.constructor.name}`, + res.result.resultCode >= 0 + ? `resultCode=${res.result.resultCode} message="${res.result.diagnosticMessage}"` + : "" + ); + if (res.constructor.name == "SearchResultReference") { + this._logger.debug("References=", res.result); + } + let callback = this._callbackMap.get(res.messageId); + if (callback) { + callback(res); + if ( + !["SearchResultEntry", "SearchResultReference"].includes( + res.constructor.name + ) + ) { + this._callbackMap.delete(res.messageId); + } + } + data = data.slice(res.byteLength); + if (i % 10 == 0) { + // Prevent blocking the main thread for too long. + await new Promise(resolve => setTimeout(resolve)); + } + } + this._handleNextDataEvent(); + }; + + /** + * Process a queued data event, if there is any. + */ + _handleNextDataEvent() { + this._processingData = false; + let next = this._dataEventsQueue.shift(); + if (next) { + this._onData(next); + } + } + + /** + * The close event handler. + */ + _onClose = () => { + this._logger.debug("Connection closed"); + }; + + /** + * The error event handler. + * + * @param {TCPSocketErrorEvent} event - The error event. + */ + _onError = async event => { + this._logger.error(event); + this._socket.close(); + this.onError( + event.errorCode, + await event.target.transport?.tlsSocketControl?.asyncGetSecurityInfo() + ); + }; + + /** + * Send a message to the server. + * + * @param {LDAPMessage} msg - The message to send. + * @param {Function} callback - Callback function when receiving server responses. + * @returns {number} The id of the sent message. + */ + _send(msg, callback) { + if (callback) { + this._callbackMap.set(this._messageId, callback); + } + this._logger.debug(`C: [${this._messageId}] ${msg.constructor.name}`); + this._socket.send(msg.toBER(this._messageId)); + return this._messageId++; + } +} diff --git a/comm/mailnews/addrbook/modules/LDAPConnection.jsm b/comm/mailnews/addrbook/modules/LDAPConnection.jsm new file mode 100644 index 0000000000..f9a66f47a7 --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPConnection.jsm @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPConnection"]; + +const lazy = {}; + +ChromeUtils.defineModuleGetter( + lazy, + "LDAPClient", + "resource:///modules/LDAPClient.jsm" +); + +/** + * A module to manage LDAP connection. + * + * @implements {nsILDAPConnection} + */ +class LDAPConnection { + QueryInterface = ChromeUtils.generateQI(["nsILDAPConnection"]); + + get bindName() { + return this._bindName; + } + + init(url, bindName, listener, closure, version) { + let useSecureTransport = url.scheme == "ldaps"; + let port = url.port; + if (port == -1) { + // -1 corresponds to the protocol's default port. + port = useSecureTransport ? 636 : 389; + } + this.client = new lazy.LDAPClient(url.host, port, useSecureTransport); + this._url = url; + this._bindName = bindName; + this.client.onOpen = () => { + listener.onLDAPInit(); + }; + this.client.onError = (status, secInfo) => { + listener.onLDAPError(status, secInfo, `${url.host}:${port}`); + }; + this.client.connect(); + } + + get wrappedJSObject() { + return this; + } +} + +LDAPConnection.prototype.classID = Components.ID( + "{f87b71b5-2a0f-4b37-8e4f-3c899f6b8432}" +); diff --git a/comm/mailnews/addrbook/modules/LDAPDirectory.jsm b/comm/mailnews/addrbook/modules/LDAPDirectory.jsm new file mode 100644 index 0000000000..758cde2ed9 --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPDirectory.jsm @@ -0,0 +1,230 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPDirectory"]; + +const { AddrBookDirectory } = ChromeUtils.import( + "resource:///modules/AddrBookDirectory.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + QueryStringToExpression: "resource:///modules/QueryStringToExpression.jsm", +}); + +/** + * @implements {nsIAbLDAPDirectory} + * @implements {nsIAbDirectory} + */ + +class LDAPDirectory extends AddrBookDirectory { + QueryInterface = ChromeUtils.generateQI([ + "nsIAbLDAPDirectory", + "nsIAbDirectory", + ]); + + init(uri) { + this._uri = uri; + + let searchIndex = uri.indexOf("?"); + this._dirPrefId = uri.substr( + "moz-abldapdirectory://".length, + searchIndex == -1 ? undefined : searchIndex + ); + + super.init(uri); + } + + get readOnly() { + return true; + } + + get isRemote() { + return true; + } + + get isSecure() { + return this.lDAPURL.scheme == "ldaps"; + } + + get propertiesChromeURI() { + return "chrome://messenger/content/addressbook/pref-directory-add.xhtml"; + } + + get dirType() { + return Ci.nsIAbManager.LDAP_DIRECTORY_TYPE; + } + + get replicationFileName() { + return this.getStringValue("filename"); + } + + set replicationFileName(value) { + this.setStringValue("filename", value); + } + + get replicationFile() { + return lazy.FileUtils.getFile("ProfD", [this.replicationFileName]); + } + + get protocolVersion() { + return this.getStringValue("protocolVersion", "3") == "3" + ? Ci.nsILDAPConnection.VERSION3 + : Ci.nsILDAPConnection.VERSION2; + } + + set protocolVersion(value) { + this.setStringValue( + "protocolVersion", + value == Ci.nsILDAPConnection.VERSION3 ? "3" : "2" + ); + } + + get saslMechanism() { + return this.getStringValue("auth.saslmech"); + } + + set saslMechanism(value) { + this.setStringValue("auth.saslmech", value); + } + + get authDn() { + return this.getStringValue("auth.dn"); + } + + set authDn(value) { + this.setStringValue("auth.dn", value); + } + + get maxHits() { + return this.getIntValue("maxHits", 100); + } + + set maxHits(value) { + this.setIntValue("maxHits", value); + } + + get attributeMap() { + let mapSvc = Cc[ + "@mozilla.org/addressbook/ldap-attribute-map-service;1" + ].createInstance(Ci.nsIAbLDAPAttributeMapService); + return mapSvc.getMapForPrefBranch(this._dirPrefId); + } + + get lDAPURL() { + let uri = this.getStringValue("uri") || `ldap://${this._uri.slice(22)}`; + return Services.io.newURI(uri).QueryInterface(Ci.nsILDAPURL); + } + + set lDAPURL(uri) { + this.setStringValue("uri", uri.spec); + } + + get childCardCount() { + return 0; + } + + get childCards() { + if (Services.io.offline) { + return this.replicationDB.childCards; + } + return super.childCards; + } + + /** + * @see {AddrBookDirectory} + */ + get cards() { + return new Map(); + } + + /** + * @see {AddrBookDirectory} + */ + get lists() { + return new Map(); + } + + get replicationDB() { + this._replicationDB?.cleanUp(); + this._replicationDB = Cc[ + "@mozilla.org/addressbook/directory;1?type=jsaddrbook" + ].createInstance(Ci.nsIAbDirectory); + this._replicationDB.init(`jsaddrbook://${this.replicationFileName}`); + return this._replicationDB; + } + + getCardFromProperty(property, value, caseSensitive) { + return null; + } + + search(queryString, searchString, listener) { + if (Services.io.offline) { + this.replicationDB.search(queryString, searchString, listener); + return; + } + this._query = Cc[ + "@mozilla.org/addressbook/ldap-directory-query;1" + ].createInstance(Ci.nsIAbDirectoryQuery); + + let args = Cc[ + "@mozilla.org/addressbook/directory/query-arguments;1" + ].createInstance(Ci.nsIAbDirectoryQueryArguments); + args.expression = lazy.QueryStringToExpression.convert(queryString); + args.querySubDirectories = true; + args.typeSpecificArg = this.attributeMap; + + this._query.doQuery(this, args, listener, this.maxHits, 0); + } + + useForAutocomplete(identityKey) { + // If we're online, then don't allow search during local autocomplete - must + // use the separate LDAP autocomplete session due to the current interfaces + let useDirectory = Services.prefs.getBoolPref( + "ldap_2.autoComplete.useDirectory", + false + ); + if (!Services.io.offline || (!useDirectory && !identityKey)) { + return false; + } + + let prefName = ""; + if (identityKey) { + // If we have an identity string, try and find out the required directory + // server. + let identity = MailServices.accounts.getIdentity(identityKey); + if (identity.overrideGlobalPref) { + prefName = identity.directoryServer; + } + if (!prefName && !useDirectory) { + return false; + } + } + if (!prefName) { + prefName = Services.prefs.getCharPref( + "ldap_2.autoComplete.directoryServer" + ); + } + if (prefName == this.dirPrefId) { + return this.replicationFile.exists(); + } + + return false; + } +} + +LDAPDirectory.prototype.classID = Components.ID( + "{8683e821-f1b0-476d-ac15-07771c79bb11}" +); diff --git a/comm/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm b/comm/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm new file mode 100644 index 0000000000..88291cbaed --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPDirectoryQuery.jsm @@ -0,0 +1,218 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPDirectoryQuery"]; + +const { LDAPListenerBase } = ChromeUtils.import( + "resource:///modules/LDAPListenerBase.jsm" +); + +/** + * Convert a nsIAbBooleanExpression to a filter string. + * + * @param {nsIAbLDAPAttributeMap} attrMap - A mapping between address book + * properties and ldap attributes. + * @param {nsIAbBooleanExpression} exp - The expression to convert. + * @returns {string} + */ +function boolExpressionToFilter(attrMap, exp) { + let filter = "("; + filter += + { + [Ci.nsIAbBooleanOperationTypes.AND]: "&", + [Ci.nsIAbBooleanOperationTypes.OR]: "|", + [Ci.nsIAbBooleanOperationTypes.NOT]: "!", + }[exp.operation] || ""; + + if (exp.expressions) { + for (let childExp of exp.expressions) { + if (childExp instanceof Ci.nsIAbBooleanExpression) { + filter += boolExpressionToFilter(attrMap, childExp); + } else if (childExp instanceof Ci.nsIAbBooleanConditionString) { + filter += boolConditionToFilter(attrMap, childExp); + } + } + } + + filter += ")"; + return filter; +} + +/** + * Convert a nsIAbBooleanConditionString to a filter string. + * + * @param {nsIAbLDAPAttributeMap} attrMap - A mapping between addressbook + * properties and ldap attributes. + * @param {nsIAbBooleanConditionString} exp - The expression to convert. + * @returns {string} + */ +function boolConditionToFilter(attrMap, exp) { + let attr = attrMap.getFirstAttribute(exp.name); + if (!attr) { + return ""; + } + switch (exp.condition) { + case Ci.nsIAbBooleanConditionTypes.DoesNotExist: + return `(!(${attr}=*))`; + case Ci.nsIAbBooleanConditionTypes.Exists: + return `(${attr}=*)`; + case Ci.nsIAbBooleanConditionTypes.Contains: + return `(${attr}=*${exp.value}*)`; + case Ci.nsIAbBooleanConditionTypes.DoesNotContain: + return `(!(${attr}=*${exp.value}*))`; + case Ci.nsIAbBooleanConditionTypes.Is: + return `(${attr}=${exp.value})`; + case Ci.nsIAbBooleanConditionTypes.IsNot: + return `(!(${attr}=${exp.value}))`; + case Ci.nsIAbBooleanConditionTypes.BeginsWith: + return `(${attr}=${exp.value}*)`; + case Ci.nsIAbBooleanConditionTypes.EndsWith: + return `(${attr}=*${exp.value})`; + case Ci.nsIAbBooleanConditionTypes.LessThan: + return `(${attr}<=${exp.value})`; + case Ci.nsIAbBooleanConditionTypes.GreaterThan: + return `(${attr}>=${exp.value})`; + case Ci.nsIAbBooleanConditionTypes.SoundsLike: + return `(${attr}~=${exp.value})`; + default: + return ""; + } +} + +/** + * @implements {nsIAbDirectoryQuery} + * @implements {nsILDAPMessageListener} + */ +class LDAPDirectoryQuery extends LDAPListenerBase { + QueryInterface = ChromeUtils.generateQI([ + "nsIAbDirectoryQuery", + "nsILDAPMessageListener", + ]); + + i = 0; + + doQuery(directory, args, listener, limit, timeout) { + this._directory = directory.QueryInterface(Ci.nsIAbLDAPDirectory); + this._listener = listener; + this._attrMap = args.typeSpecificArg; + this._filter = + args.filter || boolExpressionToFilter(this._attrMap, args.expression); + this._limit = limit; + this._timeout = timeout; + + let urlFilter = this._directory.lDAPURL.filter; + // If urlFilter is empty or the default "(objectclass=*)", do nothing. + if (urlFilter && urlFilter != "(objectclass=*)") { + if (!urlFilter.startsWith("(")) { + urlFilter = `(${urlFilter})`; + } + this._filter = `(&${urlFilter}${this._filter})`; + } + + this._connection = Cc[ + "@mozilla.org/network/ldap-connection;1" + ].createInstance(Ci.nsILDAPConnection); + this._operation = Cc[ + "@mozilla.org/network/ldap-operation;1" + ].createInstance(Ci.nsILDAPOperation); + + this._connection.init( + directory.lDAPURL, + directory.authDn, + this, + null, + directory.protocolVersion + ); + return this.i++; + } + + stopQuery(contextId) { + this._operation?.abandonExt(); + } + + /** + * @see nsILDAPMessageListener + */ + onLDAPMessage(msg) { + switch (msg.type) { + case Ci.nsILDAPMessage.RES_BIND: + this._onLDAPBind(msg); + break; + case Ci.nsILDAPMessage.RES_SEARCH_ENTRY: + this._onLDAPSearchEntry(msg); + break; + case Ci.nsILDAPMessage.RES_SEARCH_RESULT: + this._onLDAPSearchResult(msg); + break; + default: + break; + } + } + + /** + * @see nsILDAPMessageListener + */ + onLDAPError(status, secInfo, location) { + this._onSearchFinished(status, secInfo, location); + } + + /** + * @see LDAPListenerBase + */ + _actionOnBindSuccess() { + let ldapUrl = this._directory.lDAPURL; + this._operation.searchExt( + ldapUrl.dn, + ldapUrl.scope, + this._filter, + ldapUrl.attributes, + this._timeout, + this._limit + ); + } + + /** + * @see LDAPListenerBase + */ + _actionOnBindFailure() { + this._onSearchFinished(Cr.NS_ERROR_FAILURE); + } + + /** + * Handler of nsILDAPMessage.RES_SEARCH_ENTRY message. + * + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPSearchEntry(msg) { + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + this._attrMap.setCardPropertiesFromLDAPMessage(msg, newCard); + newCard.directoryUID = this._directory.UID; + this._listener.onSearchFoundCard(newCard); + } + + /** + * Handler of nsILDAPMessage.RES_SEARCH_RESULT message. + * + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPSearchResult(msg) { + this._onSearchFinished( + [Ci.nsILDAPErrors.SUCCESS, Ci.nsILDAPErrors.SIZELIMIT_EXCEEDED].includes( + msg.errorCode + ) + ? Cr.NS_OK + : Cr.NS_ERROR_FAILURE + ); + } + + _onSearchFinished(status, secInfo, location) { + this._listener.onSearchFinished(status, false, secInfo, location); + } +} + +LDAPDirectoryQuery.prototype.classID = Components.ID( + "{5ad5d311-1a50-43db-a03c-63d45f443903}" +); diff --git a/comm/mailnews/addrbook/modules/LDAPListenerBase.jsm b/comm/mailnews/addrbook/modules/LDAPListenerBase.jsm new file mode 100644 index 0000000000..486dcaffbe --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPListenerBase.jsm @@ -0,0 +1,117 @@ +/* -*- Mode: JavaScript; indent-tabs-mode: nil; c-basic-offset: 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 EXPORTED_SYMBOLS = ["LDAPListenerBase"]; + +/** + * @implements {nsILDAPMessageListener} + */ +class LDAPListenerBase { + /** + * @see nsILDAPMessageListener + */ + async onLDAPInit() { + let outPassword = {}; + if (this._directory.authDn && this._directory.saslMechanism != "GSSAPI") { + // If authDn is set, we're expected to use it to get a password. + let bundle = Services.strings.createBundle( + "chrome://mozldap/locale/ldap.properties" + ); + + let authPrompt = Services.ww.getNewAuthPrompter( + Services.wm.getMostRecentWindow(null) + ); + await authPrompt.asyncPromptPassword( + bundle.GetStringFromName("authPromptTitle"), + bundle.formatStringFromName("authPromptText", [ + this._directory.lDAPURL.host, + ]), + this._directory.lDAPURL.spec, + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, + outPassword + ); + } + this._operation.init(this._connection, this, null); + + if (this._directory.saslMechanism != "GSSAPI") { + this._operation.simpleBind(outPassword.value); + return; + } + + // Handle GSSAPI now. + this._operation.saslBind( + `ldap@${this._directory.lDAPURL.host}`, + "GSSAPI", + "sasl-gssapi" + ); + } + + /** + * Handler of nsILDAPMessage.RES_BIND message. + * + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPBind(msg) { + let errCode = msg.errorCode; + if ( + errCode == Ci.nsILDAPErrors.INAPPROPRIATE_AUTH || + errCode == Ci.nsILDAPErrors.INVALID_CREDENTIALS + ) { + // Login failed, remove any existing login(s). + let ldapUrl = this._directory.lDAPURL; + let logins = Services.logins.findLogins( + ldapUrl.prePath, + "", + ldapUrl.spec + ); + for (let login of logins) { + Services.logins.removeLogin(login); + } + // Trigger the auth prompt. + this.onLDAPInit(); + return; + } + if (errCode != Ci.nsILDAPErrors.SUCCESS) { + this._actionOnBindFailure(); + return; + } + this._actionOnBindSuccess(); + } + + /** + * @see nsILDAPMessageListener + * @abstract + */ + onLDAPMessage() { + throw new Components.Exception( + `${this.constructor.name} does not implement onLDAPMessage.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Callback when BindResponse succeeded. + * + * @abstract + */ + _actionOnBindSuccess() { + throw new Components.Exception( + `${this.constructor.name} does not implement _actionOnBindSuccess.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Callback when BindResponse failed. + * + * @abstract + */ + _actionOnBindFailure() { + throw new Components.Exception( + `${this.constructor.name} does not implement _actionOnBindFailure.`, + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } +} diff --git a/comm/mailnews/addrbook/modules/LDAPMessage.jsm b/comm/mailnews/addrbook/modules/LDAPMessage.jsm new file mode 100644 index 0000000000..6ee7574605 --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPMessage.jsm @@ -0,0 +1,632 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = [ + "AbandonRequest", + "BindRequest", + "UnbindRequest", + "SearchRequest", + "LDAPResponse", +]; + +var { asn1js } = ChromeUtils.importESModule("resource:///modules/asn1js.mjs"); + +/** + * A base class for all LDAP request and response messages, see + * rfc4511#section-4.1.1. + * + * @property {number} messageId - The message id. + * @property {LocalBaseBlock} protocolOp - The message content, in a data + * structure provided by asn1js. + */ +class LDAPMessage { + /** + * Encode the current message by Basic Encoding Rules (BER). + * + * @param {number} messageId - The id of the current message. + * @returns {ArrayBuffer} BER encoded message. + */ + toBER(messageId = this.messageId) { + let msg = new asn1js.Sequence({ + value: [new asn1js.Integer({ value: messageId }), this.protocolOp], + }); + return msg.toBER(); + } + + static TAG_CLASS_APPLICATION = 2; + static TAG_CLASS_CONTEXT = 3; + + /** + * Get the idBlock of [APPLICATION n]. + * + * @param {number} tagNumber - The tag number of this block. + */ + _getApplicationId(tagNumber) { + return { + tagClass: LDAPMessage.TAG_CLASS_APPLICATION, + tagNumber, + }; + } + + /** + * Get the idBlock of context-specific [n]. + * + * @param {number} tagNumber - The tag number of this block. + */ + _getContextId(tagNumber) { + return { + tagClass: LDAPMessage.TAG_CLASS_CONTEXT, + tagNumber, + }; + } + + /** + * Create a string block with context-specific [n]. + * + * @param {number} tagNumber - The tag number of this block. + * @param {string} value - The string value of this block. + * @returns {LocalBaseBlock} + */ + _contextStringBlock(tagNumber, value) { + return new asn1js.Primitive({ + idBlock: this._getContextId(tagNumber), + valueHex: new TextEncoder().encode(value), + }); + } +} + +class BindRequest extends LDAPMessage { + static APPLICATION = 0; + + AUTH_SIMPLE = 0; + AUTH_SASL = 3; + + /** + * @param {string} dn - The name to bind. + * @param {string} password - The password. + * @param {object} sasl - The SASL configs. + * @param {string} sasl.mechanism - The SASL mechanism e.g. sasl-gssapi. + * @param {Uint8Array} sasl.credentials - The credential token for the request. + */ + constructor(dn, password, sasl) { + super(); + let authBlock; + if (sasl) { + authBlock = new asn1js.Constructed({ + idBlock: this._getContextId(this.AUTH_SASL), + value: [ + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(sasl.mechanism), + }), + new asn1js.OctetString({ + valueHex: sasl.credentials, + }), + ], + }); + } else { + authBlock = new asn1js.Primitive({ + idBlock: this._getContextId(this.AUTH_SIMPLE), + valueHex: new TextEncoder().encode(password), + }); + } + this.protocolOp = new asn1js.Constructed({ + // [APPLICATION 0] + idBlock: this._getApplicationId(BindRequest.APPLICATION), + value: [ + // version + new asn1js.Integer({ value: 3 }), + // name + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(dn), + }), + // authentication + authBlock, + ], + }); + } +} + +class UnbindRequest extends LDAPMessage { + static APPLICATION = 2; + + protocolOp = new asn1js.Primitive({ + // [APPLICATION 2] + idBlock: this._getApplicationId(UnbindRequest.APPLICATION), + }); +} + +class SearchRequest extends LDAPMessage { + static APPLICATION = 3; + + // Filter CHOICE. + FILTER_AND = 0; + FILTER_OR = 1; + FILTER_NOT = 2; + FILTER_EQUALITY_MATCH = 3; + FILTER_SUBSTRINGS = 4; + FILTER_GREATER_OR_EQUAL = 5; + FILTER_LESS_OR_EQUAL = 6; + FILTER_PRESENT = 7; + FILTER_APPROX_MATCH = 8; + FILTER_EXTENSIBLE_MATCH = 9; + + // SubstringFilter SEQUENCE. + SUBSTRINGS_INITIAL = 0; + SUBSTRINGS_ANY = 1; + SUBSTRINGS_FINAL = 2; + + // MatchingRuleAssertion SEQUENCE. + MATCHING_RULE = 1; // optional + MATCHING_TYPE = 2; // optional + MATCHING_VALUE = 3; + MATCHING_DN = 4; // default to FALSE + + /** + * @param {string} dn - The name to search. + * @param {number} scope - The scope to search. + * @param {string} filter - The filter string, e.g. "(&(|(k1=v1)(k2=v2)))". + * @param {string} attributes - Attributes to include in the search result. + * @param {number} timeout - The seconds to wait. + * @param {number} limit - Maximum number of entries to return. + */ + constructor(dn, scope, filter, attributes, timeout, limit) { + super(); + this.protocolOp = new asn1js.Constructed({ + // [APPLICATION 3] + idBlock: this._getApplicationId(SearchRequest.APPLICATION), + value: [ + // base DN + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(dn), + }), + // scope + new asn1js.Enumerated({ + value: scope, + }), + // derefAliases + new asn1js.Enumerated({ + value: 0, + }), + // sizeLimit + new asn1js.Integer({ value: limit }), + // timeLimit + new asn1js.Integer({ value: timeout }), + // typesOnly + new asn1js.Boolean({ value: false }), + // filter + this._convertFilterToBlock(filter), + // attributes + new asn1js.Sequence({ + value: attributes + .split(",") + .filter(Boolean) + .map( + attr => + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(attr), + }) + ), + }), + ], + }); + } + + /** + * Parse a single filter value "key=value" to [filterId, key, value]. + * + * @param {string} filter - A single filter value without parentheses. + * @returns {(number|string)[]} An array [filterId, key, value] as + * [number, string, string] + */ + _parseFilterValue(filter) { + for (let cond of [">=", "<=", "~=", ":=", "="]) { + let index = filter.indexOf(cond); + if (index > 0) { + let k = filter.slice(0, index); + let v = filter.slice(index + cond.length); + let filterId = { + ">=": this.FILTER_GREATER_OR_EQUAL, + "<=": this.FILTER_LESS_OR_EQUAL, + "~=": this.FILTER_APPROX_MATCH, + ":=": this.FILTER_EXTENSIBLE_MATCH, + }[cond]; + if (!filterId) { + if (v == "*") { + filterId = this.FILTER_PRESENT; + } else if (!v.includes("*")) { + filterId = this.FILTER_EQUALITY_MATCH; + } else { + filterId = this.FILTER_SUBSTRINGS; + v = v.split("*"); + } + } + return [filterId, k, v]; + } + } + throw Components.Exception( + `Invalid filter: ${filter}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + + /** + * Parse a full filter string to an array of tokens. + * + * @param {string} filter - The full filter string to parse. + * @param {number} depth - The depth of a token. + * @param {object[]} tokens - The tokens to return. + * @param {"op"|"field"} tokens[].type - The token type. + * @param {number} tokens[].depth - The token depth. + * @param {string|string[]} tokens[].value - The token value. + */ + _parseFilter(filter, depth = 0, tokens = []) { + while (filter[0] == ")" && depth > 0) { + depth--; + filter = filter.slice(1); + } + if (filter.length == 0) { + // End of input. + return tokens; + } + if (filter[0] != "(") { + throw Components.Exception( + `Invalid filter: ${filter}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + filter = filter.slice(1); + let nextOpen = filter.indexOf("("); + let nextClose = filter.indexOf(")"); + + if (nextOpen != -1 && nextOpen < nextClose) { + // Case: "OP(" + depth++; + tokens.push({ + type: "op", + depth, + value: { + "&": this.FILTER_AND, + "|": this.FILTER_OR, + "!": this.FILTER_NOT, + }[filter.slice(0, nextOpen)], + }); + this._parseFilter(filter.slice(nextOpen), depth, tokens); + } else if (nextClose != -1) { + // Case: "key=value)" + tokens.push({ + type: "field", + depth, + value: this._parseFilterValue(filter.slice(0, nextClose)), + }); + this._parseFilter(filter.slice(nextClose + 1), depth, tokens); + } + return tokens; + } + + /** + * Parse a filter string to a LocalBaseBlock. + * + * @param {string} filter - The filter string to parse. + * @returns {LocalBaseBlock} + */ + _convertFilterToBlock(filter) { + if (!filter.startsWith("(")) { + // Make sure filter is wrapped in parens, see rfc2254#section-4. + filter = `(${filter})`; + } + let tokens = this._parseFilter(filter); + let stack = []; + for (let { type, depth, value } of tokens) { + while (depth < stack.length) { + // We are done with the current block, go one level up. + stack.pop(); + } + if (type == "op") { + if (depth == stack.length) { + // We are done with the current block, go one level up. + stack.pop(); + } + // Found a new block, go one level down. + let parent = stack.slice(-1)[0]; + let curBlock = new asn1js.Constructed({ + idBlock: this._getContextId(value), + }); + stack.push(curBlock); + if (parent) { + parent.valueBlock.value.push(curBlock); + } + } else if (type == "field") { + let [tagNumber, field, fieldValue] = value; + let block; + let idBlock = this._getContextId(tagNumber); + if (tagNumber == this.FILTER_PRESENT) { + // A present filter. + block = new asn1js.Primitive({ + idBlock, + valueHex: new TextEncoder().encode(field), + }); + } else if (tagNumber == this.FILTER_EXTENSIBLE_MATCH) { + // An extensibleMatch filter is in the form of + // <type>:dn:<rule>:=<value>. We need to further parse the field. + let parts = field.split(":"); + let value = []; + if (parts.length == 3) { + // field is <type>:dn:<rule>. + if (parts[2]) { + value.push( + this._contextStringBlock(this.MATCHING_RULE, parts[2]) + ); + } + if (parts[0]) { + value.push( + this._contextStringBlock(this.MATCHING_TYPE, parts[0]) + ); + } + value.push( + this._contextStringBlock(this.MATCHING_VALUE, fieldValue) + ); + if (parts[1] == "dn") { + let dn = new asn1js.Boolean({ + value: true, + }); + dn.idBlock.tagClass = LDAPMessage.TAG_CLASS_CONTEXT; + dn.idBlock.tagNumber = this.MATCHING_DN; + value.push(dn); + } + } else if (parts.length == 2) { + // field is <type>:<rule>. + if (parts[1]) { + value.push( + this._contextStringBlock(this.MATCHING_RULE, parts[1]) + ); + } + + if (parts[0]) { + value.push( + this._contextStringBlock(this.MATCHING_TYPE, parts[0]) + ); + } + value.push( + this._contextStringBlock(this.MATCHING_VALUE, fieldValue) + ); + } else { + // field is <type>. + value = [ + this._contextStringBlock(this.MATCHING_TYPE, field), + this._contextStringBlock(this.MATCHING_VALUE, fieldValue), + ]; + } + block = new asn1js.Constructed({ + idBlock, + value, + }); + } else if (tagNumber != this.FILTER_SUBSTRINGS) { + // A filter that is not substrings filter. + block = new asn1js.Constructed({ + idBlock, + value: [ + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(field), + }), + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(fieldValue), + }), + ], + }); + } else { + // A substrings filter. + let substringsSeq = new asn1js.Sequence(); + block = new asn1js.Constructed({ + idBlock, + value: [ + new asn1js.OctetString({ + valueHex: new TextEncoder().encode(field), + }), + substringsSeq, + ], + }); + for (let i = 0; i < fieldValue.length; i++) { + let v = fieldValue[i]; + if (!v.length) { + // Case: * + continue; + } else if (i < fieldValue.length - 1) { + // Case: abc* + substringsSeq.valueBlock.value.push( + new asn1js.Primitive({ + idBlock: this._getContextId( + i == 0 ? this.SUBSTRINGS_INITIAL : this.SUBSTRINGS_ANY + ), + valueHex: new TextEncoder().encode(v), + }) + ); + } else { + // Case: *abc + substringsSeq.valueBlock.value.push( + new asn1js.Primitive({ + idBlock: this._getContextId(this.SUBSTRINGS_FINAL), + valueHex: new TextEncoder().encode(v), + }) + ); + } + } + } + let curBlock = stack.slice(-1)[0]; + if (curBlock) { + curBlock.valueBlock.value.push(block); + } else { + stack.push(block); + } + } + } + + return stack[0]; + } +} + +class AbandonRequest extends LDAPMessage { + static APPLICATION = 16; + + /** + * @param {string} messageId - The messageId to abandon. + */ + constructor(messageId) { + super(); + this.protocolOp = new asn1js.Integer({ value: messageId }); + // [APPLICATION 16] + this.protocolOp.idBlock.tagClass = LDAPMessage.TAG_CLASS_APPLICATION; + this.protocolOp.idBlock.tagNumber = AbandonRequest.APPLICATION; + } +} + +class LDAPResult { + /** + * @param {number} resultCode - The result code. + * @param {string} matchedDN - For certain result codes, matchedDN is the last entry used. + * @param {string} diagnosticMessage - A diagnostic message returned by the server. + */ + constructor(resultCode, matchedDN, diagnosticMessage) { + this.resultCode = resultCode; + this.matchedDN = matchedDN; + this.diagnosticMessage = diagnosticMessage; + } +} + +/** + * A base class for all LDAP response messages. + * + * @property {LDAPResult} result - The result of a response. + */ +class LDAPResponse extends LDAPMessage { + /** + * @param {number} messageId - The message id. + * @param {LocalBaseBlock} protocolOp - The message content. + * @param {number} byteLength - The byte size of this message in raw BER form. + */ + constructor(messageId, protocolOp, byteLength) { + super(); + this.messageId = messageId; + this.protocolOp = protocolOp; + this.byteLength = byteLength; + } + + /** + * Find the corresponding response class name from a tag number. + * + * @param {number} tagNumber - The tag number of a block. + * @returns {LDAPResponse} + */ + static _getResponseClassFromTagNumber(tagNumber) { + return [ + SearchResultEntry, + SearchResultDone, + SearchResultReference, + BindResponse, + ExtendedResponse, + ].find(x => x.APPLICATION == tagNumber); + } + + /** + * Decode a raw server response to LDAPResponse instance. + * + * @param {ArrayBuffer} buffer - The raw message received from the server. + * @returns {LDAPResponse} A concrete instance of LDAPResponse subclass. + */ + static fromBER(buffer) { + let decoded = asn1js.fromBER(buffer); + if (decoded.offset == -1 || decoded.result.error) { + throw Components.Exception( + decoded.result.error, + Cr.NS_ERROR_CANNOT_CONVERT_DATA + ); + } + let value = decoded.result.valueBlock.value; + let protocolOp = value[1]; + if (protocolOp.idBlock.tagClass != this.TAG_CLASS_APPLICATION) { + throw Components.Exception( + `Unexpected tagClass ${protocolOp.idBlock.tagClass}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let ProtocolOp = this._getResponseClassFromTagNumber( + protocolOp.idBlock.tagNumber + ); + if (!ProtocolOp) { + throw Components.Exception( + `Unexpected tagNumber ${protocolOp.idBlock.tagNumber}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let op = new ProtocolOp( + value[0].valueBlock.valueDec, + protocolOp, + decoded.offset + ); + op.parse(); + return op; + } + + /** + * Parse the protocolOp part of a LDAPMessage to LDAPResult. For LDAP + * responses that are simply LDAPResult, reuse this function. Other responses + * need to implement this function. + */ + parse() { + let value = this.protocolOp.valueBlock.value; + let resultCode = value[0].valueBlock.valueDec; + let matchedDN = new TextDecoder().decode(value[1].valueBlock.valueHex); + let diagnosticMessage = new TextDecoder().decode( + value[2].valueBlock.valueHex + ); + this.result = new LDAPResult(resultCode, matchedDN, diagnosticMessage); + } +} + +class BindResponse extends LDAPResponse { + static APPLICATION = 1; + + parse() { + super.parse(); + let serverSaslCredsBlock = this.protocolOp.valueBlock.value[3]; + if (serverSaslCredsBlock) { + this.result.serverSaslCreds = serverSaslCredsBlock.valueBlock.valueHex; + } + } +} + +class SearchResultEntry extends LDAPResponse { + static APPLICATION = 4; + + parse() { + let value = this.protocolOp.valueBlock.value; + let objectName = new TextDecoder().decode(value[0].valueBlock.valueHex); + let attributes = {}; + for (let attr of value[1].valueBlock.value) { + let attrValue = attr.valueBlock.value; + let type = new TextDecoder().decode(attrValue[0].valueBlock.valueHex); + let vals = attrValue[1].valueBlock.value.map(v => v.valueBlock.valueHex); + attributes[type] = vals; + } + this.result = { objectName, attributes }; + } +} + +class SearchResultDone extends LDAPResponse { + static APPLICATION = 5; +} + +class SearchResultReference extends LDAPResponse { + static APPLICATION = 19; + + parse() { + let value = this.protocolOp.valueBlock.value; + this.result = value.map(block => + new TextDecoder().decode(block.valueBlock.valueHex) + ); + } +} + +class ExtendedResponse extends LDAPResponse { + static APPLICATION = 24; +} diff --git a/comm/mailnews/addrbook/modules/LDAPOperation.jsm b/comm/mailnews/addrbook/modules/LDAPOperation.jsm new file mode 100644 index 0000000000..d0e2d64a54 --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPOperation.jsm @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPOperation"]; + +const lazy = {}; + +ChromeUtils.defineModuleGetter( + lazy, + "LDAPClient", + "resource:///modules/LDAPClient.jsm" +); + +/** + * A module to manage LDAP operation. + * + * @implements {nsILDAPOperation} + */ +class LDAPOperation { + QueryInterface = ChromeUtils.generateQI(["nsILDAPOperation"]); + + init(connection, listener, closure) { + this._listener = listener; + this._connection = connection; + this._client = connection.wrappedJSObject.client; + + this._referenceUrls = []; + + // Cache request arguments to use when searching references. + this._simpleBindPassword = null; + this._saslBindArgs = null; + this._searchArgs = null; + } + + simpleBind(password) { + this._password = password; + try { + this._messageId = this._client.bind( + this._connection.bindName, + password, + res => this._onBindSuccess(res.result.resultCode) + ); + } catch (e) { + this._listener.onLDAPError(e.result, null, ""); + } + } + + saslBind(service, mechanism, authModuleType, serverCredentials) { + this._saslBindArgs = [service, mechanism, authModuleType]; + try { + this._client.saslBind( + service, + mechanism, + authModuleType, + serverCredentials, + res => { + if (res.result.resultCode == Ci.nsILDAPErrors.SASL_BIND_IN_PROGRESS) { + this.saslBind( + service, + mechanism, + authModuleType, + res.result.serverSaslCreds + ); + } else if (res.result.resultCode == Ci.nsILDAPErrors.SUCCESS) { + this._onBindSuccess(res.result.resultCode); + } + } + ); + } catch (e) { + this._listener.onLDAPError(e.result, null, ""); + } + } + + searchExt(baseDN, scope, filter, attributes, timeout, limit) { + this._searchArgs = [baseDN, scope, filter, attributes, timeout, limit]; + try { + this._messageId = this._client.search( + baseDN, + scope, + filter, + attributes, + timeout, + limit, + res => { + if (res.constructor.name == "SearchResultEntry") { + this._listener.onLDAPMessage({ + QueryInterface: ChromeUtils.generateQI(["nsILDAPMessage"]), + errorCode: 0, + type: Ci.nsILDAPMessage.RES_SEARCH_ENTRY, + getAttributes() { + return Object.keys(res.result.attributes); + }, + // Find the matching attribute name while ignoring the case. + _getAttribute(attr) { + attr = attr.toLowerCase(); + return this.getAttributes().find(x => x.toLowerCase() == attr); + }, + getValues(attr) { + attr = this._getAttribute(attr); + return res.result.attributes[attr]?.map(v => + new TextDecoder().decode(v) + ); + }, + getBinaryValues(attr) { + attr = this._getAttribute(attr); + return res.result.attributes[attr]?.map(v => ({ + // @see nsILDAPBERValue + get: () => new Uint8Array(v), + })); + }, + }); + } else if (res.constructor.name == "SearchResultReference") { + this._referenceUrls.push(...res.result); + } else if (res.constructor.name == "SearchResultDone") { + // NOTE: we create a new connection for every search, can be changed + // to reuse connections. + this._client.onError = () => {}; + this._client.unbind(); + this._messageId = null; + if (this._referenceUrls.length) { + this._searchReference(this._referenceUrls.shift()); + } else { + this._listener.onLDAPMessage({ + errorCode: res.result.resultCode, + type: Ci.nsILDAPMessage.RES_SEARCH_RESULT, + }); + } + } + } + ); + } catch (e) { + this._listener.onLDAPError(e.result, null, ""); + } + } + + abandonExt() { + if (this._messageId) { + this._client.abandon(this._messageId); + } + } + + /** + * Decide what to do on bind success. When searching a reference url, trigger + * a new search. Otherwise, emit a message to this._listener. + * + * @param {number} errorCode - The result code of BindResponse. + */ + _onBindSuccess(errorCode) { + if (this._searchingReference) { + this.searchExt(...this._searchArgs); + } else { + this._listener.onLDAPMessage({ + errorCode, + type: Ci.nsILDAPMessage.RES_BIND, + }); + } + } + + /** + * Connect to a reference url and continue the search. + * + * @param {string} urlStr - A url string we get from SearchResultReference. + */ + _searchReference(urlStr) { + this._searchingReference = true; + let urlParser = Cc["@mozilla.org/network/ldap-url-parser;1"].createInstance( + Ci.nsILDAPURLParser + ); + let url; + try { + url = urlParser.parse(urlStr); + } catch (e) { + console.error(e); + return; + } + this._client = new lazy.LDAPClient( + url.host, + url.port, + url.options & Ci.nsILDAPURL.OPT_SECURE + ); + this._client.onOpen = () => { + if (this._password) { + this.simpleBind(this._password); + } else { + this.saslBind(...this._saslBindData); + } + }; + this._client.onError = (status, secInfo) => { + this._listener.onLDAPError(status, secInfo, `${url.host}:${url.port}`); + }; + this._client.connect(); + } +} + +LDAPOperation.prototype.classID = Components.ID( + "{a6f94ca4-cd2d-4983-bcf2-fe936190955c}" +); diff --git a/comm/mailnews/addrbook/modules/LDAPProtocolHandler.jsm b/comm/mailnews/addrbook/modules/LDAPProtocolHandler.jsm new file mode 100644 index 0000000000..c9d84f2d99 --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPProtocolHandler.jsm @@ -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/. */ + +var EXPORTED_SYMBOLS = ["LDAPProtocolHandler", "LDAPSProtocolHandler"]; + +/** + * @implements {nsIProtocolHandler} + */ +class LDAPProtocolHandler { + QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]); + + scheme = "ldap"; + + newChannel(aURI, aLoadInfo) { + let channel = Cc["@mozilla.org/network/ldap-channel;1"].createInstance( + Ci.nsIChannel + ); + channel.init(aURI); + channel.loadInfo = aLoadInfo; + return channel; + } + + allowPort(port, scheme) { + return port == 389; + } +} +LDAPProtocolHandler.prototype.classID = Components.ID( + "{b3de9249-b0e5-4c12-8d91-c9a434fd80f5}" +); + +class LDAPSProtocolHandler extends LDAPProtocolHandler { + scheme = "ldaps"; + + allowPort(port, scheme) { + return port == 636; + } +} +LDAPSProtocolHandler.prototype.classID = Components.ID( + "{c85a5ef2-9c56-445f-b029-76889f2dd29b}" +); diff --git a/comm/mailnews/addrbook/modules/LDAPReplicationService.jsm b/comm/mailnews/addrbook/modules/LDAPReplicationService.jsm new file mode 100644 index 0000000000..2a11d15eee --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPReplicationService.jsm @@ -0,0 +1,233 @@ +/* -*- Mode: JavaScript; indent-tabs-mode: nil; c-basic-offset: 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 EXPORTED_SYMBOLS = ["LDAPReplicationService"]; + +const { LDAPListenerBase } = ChromeUtils.import( + "resource:///modules/LDAPListenerBase.jsm" +); +var { SQLiteDirectory } = ChromeUtils.import( + "resource:///modules/SQLiteDirectory.jsm" +); + +/** + * A service to replicate a LDAP directory to a local SQLite db. + * + * @implements {nsIAbLDAPReplicationService} + * @implements {nsILDAPMessageListener} + */ +class LDAPReplicationService extends LDAPListenerBase { + QueryInterface = ChromeUtils.generateQI([ + "nsIAbLDAPReplicationService", + "nsILDAPMessageListener", + ]); + + /** + * @see nsIAbLDAPReplicationService + */ + startReplication(directory, progressListener) { + this._directory = directory; + this._listener = progressListener; + this._attrMap = directory.attributeMap; + this._count = 0; + this._cards = []; + this._connection = Cc[ + "@mozilla.org/network/ldap-connection;1" + ].createInstance(Ci.nsILDAPConnection); + this._operation = Cc[ + "@mozilla.org/network/ldap-operation;1" + ].createInstance(Ci.nsILDAPOperation); + + this._connection.init( + directory.lDAPURL, + directory.authDn, + this, + null, + directory.protocolVersion + ); + } + + /** + * @see nsIAbLDAPReplicationService + */ + cancelReplication(directory) { + this._operation.abandonExt(); + this.done(false); + } + + /** + * @see nsIAbLDAPReplicationService + */ + done(success) { + this._done(success); + } + + /** + * @see nsILDAPMessageListener + */ + onLDAPMessage(msg) { + switch (msg.type) { + case Ci.nsILDAPMessage.RES_BIND: + this._onLDAPBind(msg); + break; + case Ci.nsILDAPMessage.RES_SEARCH_ENTRY: + this._onLDAPSearchEntry(msg); + break; + case Ci.nsILDAPMessage.RES_SEARCH_RESULT: + this._onLDAPSearchResult(msg); + break; + default: + break; + } + } + + /** + * @see nsILDAPMessageListener + */ + onLDAPError(status, secInfo, location) { + this.done(false); + } + + /** + * @see LDAPListenerBase + */ + _actionOnBindSuccess() { + this._openABForReplicationDir(); + let ldapUrl = this._directory.lDAPURL; + this._operation.init(this._connection, this, null); + this._listener.onStateChange( + null, + null, + Ci.nsIWebProgressListener.STATE_START, + Cr.NS_OK + ); + this._operation.searchExt( + ldapUrl.dn, + ldapUrl.scope, + ldapUrl.filter, + ldapUrl.attributes, + 0, + 0 + ); + } + + /** + * @see LDAPListenerBase + */ + _actionOnBindFailure() { + this._done(false); + } + + /** + * Handler of nsILDAPMessage.RES_SEARCH_ENTRY message. + * + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + async _onLDAPSearchEntry(msg) { + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + this._attrMap.setCardPropertiesFromLDAPMessage(msg, newCard); + this._cards.push(newCard); + this._count++; + if (this._count % 10 == 0) { + // inform the listener every 10 entries + this._listener.onProgressChange( + null, + null, + this._count, + -1, + this._count, + -1 + ); + } + if (this._count % 100 == 0 && !this._writePromise) { + // Write to the db to release some memory. + this._writePromise = this._replicationDB.bulkAddCards(this._cards); + this._cards = []; + await this._writePromise; + this._writePromise = null; + } + } + + /** + * Handler of nsILDAPMessage.RES_SEARCH_RESULT message. + * + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + async _onLDAPSearchResult(msg) { + if ( + msg.errorCode == Ci.nsILDAPErrors.SUCCESS || + msg.errorCode == Ci.nsILDAPErrors.SIZELIMIT_EXCEEDED + ) { + if (this._writePromise) { + await this._writePromise; + } + await this._replicationDB.bulkAddCards(this._cards); + this.done(true); + return; + } + this.done(false); + } + + /** + * Init a jsaddrbook from the replicationFileName of the current LDAP directory. + */ + _openABForReplicationDir() { + this._oldReplicationFileName = this._directory.replicationFileName; + this._replicationFile = this._directory.replicationFile; + if (this._replicationFile.exists()) { + // If the database file already exists, create a new one here, and replace + // the old file in _done when success. + this._replicationFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + // What we need is the unique file name, _replicationDB will create an + // empty database file. + this._replicationFile.remove(false); + // Set replicationFileName to the new db file name, so that _replicationDB + // works correctly. + this._directory.replicationFileName = this._replicationFile.leafName; + } + + this._replicationDB = new SQLiteDirectory(); + this._replicationDB.init(`jsaddrbook://${this._replicationFile.leafName}`); + } + + /** + * Clean up depending on whether replication succeeded or failed, emit + * STATE_STOP event. + * + * @param {bool} success - Replication succeeded or failed. + */ + async _done(success) { + this._cards = []; + if (this._replicationDB) { + // Close the db. + await this._replicationDB.cleanUp(); + } + if (success) { + // Replace the old db file with new db file. + this._replicationFile.moveTo(null, this._oldReplicationFileName); + } else if ( + this._replicationFile && + this._replicationFile.path != this._oldReplicationFileName + ) { + this._replicationFile.remove(false); + } + if (this._oldReplicationFileName) { + // Reset replicationFileName to the old db file name. + this._directory.replicationFileName = this._oldReplicationFileName; + } + this._listener.onStateChange( + null, + null, + Ci.nsIWebProgressListener.STATE_STOP, + success ? Cr.NS_OK : Cr.NS_ERROR_FAILURE + ); + } +} + +LDAPReplicationService.prototype.classID = Components.ID( + "{dbe204e8-ae09-11eb-b4c8-a7e4b3e6e82e}" +); diff --git a/comm/mailnews/addrbook/modules/LDAPService.jsm b/comm/mailnews/addrbook/modules/LDAPService.jsm new file mode 100644 index 0000000000..d1def67afc --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPService.jsm @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPService"]; + +/** + * @implements {nsILDAPService} + */ +class LDAPService { + QueryInterface = ChromeUtils.generateQI(["nsILDAPService"]); + + createFilter(maxSize, pattern, prefix, suffix, attr, value) { + let words = value.split(" "); + // Get the Mth to Nth words. + function getMtoN(m, n) { + n = n || m; + return words.slice(m - 1, n).join(" "); + } + + let filter = prefix; + pattern.replaceAll("%a", attr); + while (pattern) { + let index = pattern.indexOf("%v"); + if (index == -1) { + filter += pattern; + pattern = ""; + } else { + filter += pattern.slice(0, index); + // Get the three characters after %v. + let [c1, c2, c3] = pattern.slice(index + 2, index + 5); + if (c1 >= "1" && c1 <= "9") { + if (c2 == "$") { + // %v$: means the last word + filter += getMtoN(words.length); + pattern = pattern.slice(index + 3); + } else if (c2 == "-") { + if (c3 >= "1" && c3 <= "9") { + // %vM-N: means from the Mth to the Nth word + filter += getMtoN(c1, c3); + pattern = pattern.slice(index + 5); + } else { + // %vN-: means from the Nth to the last word + filter += getMtoN(c1, words.length); + pattern = pattern.slice(index + 4); + } + } else { + // %vN: means the Nth word + filter += getMtoN(c1); + pattern = pattern.slice(index + 3); + } + } else { + // %v: means the entire search value + filter += value; + pattern = pattern.slice(index + 2); + } + } + } + filter += suffix; + return filter.length > maxSize ? "" : filter; + } +} + +LDAPService.prototype.classID = Components.ID( + "{e8b59b32-f83f-4d5f-8eb5-e3c1e5de0d47}" +); diff --git a/comm/mailnews/addrbook/modules/LDAPSyncQuery.jsm b/comm/mailnews/addrbook/modules/LDAPSyncQuery.jsm new file mode 100644 index 0000000000..d92fea191f --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPSyncQuery.jsm @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPSyncQuery"]; + +/** + * @implements {nsILDAPMessageListener} + * @implements {nsILDAPSyncQuery} + */ +class LDAPSyncQuery { + QueryInterface = ChromeUtils.generateQI([ + "nsILDAPMessageListener", + "nsILDAPSyncQuery", + ]); + + /** @see nsILDAPMessageListener */ + onLDAPInit() { + this._operation = Cc[ + "@mozilla.org/network/ldap-operation;1" + ].createInstance(Ci.nsILDAPOperation); + this._operation.init(this._connection, this, null); + this._operation.simpleBind(""); + } + + onLDAPMessage(msg) { + switch (msg.type) { + case Ci.nsILDAPMessage.RES_BIND: + this._onLDAPBind(msg); + break; + case Ci.nsILDAPMessage.RES_SEARCH_ENTRY: + this._onLDAPSearchEntry(msg); + break; + case Ci.nsILDAPMessage.RES_SEARCH_RESULT: + this._onLDAPSearchResult(msg); + break; + default: + break; + } + } + + onLDAPError(status, secInfo, location) { + this._statusCode = status; + this._finished = true; + } + + /** @see nsILDAPSyncQuery */ + getQueryResults(ldapUrl, protocolVersion) { + this._ldapUrl = ldapUrl; + this._connection = Cc[ + "@mozilla.org/network/ldap-connection;1" + ].createInstance(Ci.nsILDAPConnection); + this._connection.init(ldapUrl, "", this, null, protocolVersion); + + this._statusCode = 0; + this._result = ""; + this._finished = false; + + Services.tm.spinEventLoopUntil( + "getQueryResults is a sync function", + () => this._finished + ); + if (this._statusCode) { + throw Components.Exception("getQueryResults failed", this._statusCode); + } + return this._result; + } + + /** + * Handler of nsILDAPMessage.RES_BIND message. + * + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPBind(msg) { + if (msg.errorCode != Ci.nsILDAPErrors.SUCCESS) { + this._statusCode = msg.errorCode; + this._finished = true; + return; + } + this._operation.init(this._connection, this, null); + this._operation.searchExt( + this._ldapUrl.dn, + this._ldapUrl.scope, + this._ldapUrl.filter, + this._ldapUrl.attributes, + 0, + 0 + ); + } + + /** + * Handler of nsILDAPMessage.RES_SEARCH_ENTRY message. + * + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPSearchEntry(msg) { + for (let attr of msg.getAttributes()) { + for (let value of msg.getValues(attr)) { + this._result += `\n${attr}=${value}`; + } + } + } + + /** + * Handler of nsILDAPMessage.RES_SEARCH_RESULT message. + * + * @param {nsILDAPMessage} msg - The received LDAP message. + */ + _onLDAPSearchResult(msg) { + this._finished = true; + } +} diff --git a/comm/mailnews/addrbook/modules/LDAPURLParser.jsm b/comm/mailnews/addrbook/modules/LDAPURLParser.jsm new file mode 100644 index 0000000000..2c19be1386 --- /dev/null +++ b/comm/mailnews/addrbook/modules/LDAPURLParser.jsm @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPURLParser"]; + +/** + * @implements {nsILDAPURLParser} + */ +class LDAPURLParser { + QueryInterface = ChromeUtils.generateQI(["nsILDAPURLParser"]); + + parse(spec) { + // The url is in the form of scheme://hostport/dn?attributes?scope?filter, + // see RFC2255. + let matches = + /^(ldaps?):\/\/\[?([^\s\]/]+)\]?:?(\d*)\/([^\s?]*)\??(.*)$/.exec(spec); + if (!matches) { + throw Components.Exception( + `Invalid LDAP URL: ${spec}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let [, scheme, host, port, dn, query] = matches; + let [attributes, scopeString, filter] = query.split("?"); + let scope = + { + one: Ci.nsILDAPURL.SCOPE_ONELEVEL, + sub: Ci.nsILDAPURL.SCOPE_SUBTREE, + }[scopeString] || Ci.nsILDAPURL.SCOPE_BASE; + return { + QueryInterface: ChromeUtils.generateQI(["nsILDAPURLParserResult"]), + host, + port, + dn: decodeURIComponent(dn), + attributes, + scope, + filter: filter ? decodeURIComponent(filter) : "(objectclass=*)", + options: scheme == "ldaps" ? Ci.nsILDAPURL.OPT_SECURE : 0, + }; + } +} diff --git a/comm/mailnews/addrbook/modules/QueryStringToExpression.jsm b/comm/mailnews/addrbook/modules/QueryStringToExpression.jsm new file mode 100644 index 0000000000..0129d2e3d3 --- /dev/null +++ b/comm/mailnews/addrbook/modules/QueryStringToExpression.jsm @@ -0,0 +1,186 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["QueryStringToExpression"]; + +/** + * A module to parse a query string to a nsIAbBooleanExpression. A valid query + * string is in this form: + * + * (OP1(FIELD1,COND1,VALUE1)..(FIELDn,CONDn,VALUEn)(BOOL2(FIELD1,COND1,VALUE1)..)..) + * + * OPn A boolean operator joining subsequent terms delimited by (). + * + * @see {nsIAbBooleanOperationTypes}. + * FIELDn An addressbook card data field. + * CONDn A condition to compare FIELDn with VALUEn. + * @see {nsIAbBooleanConditionTypes}. + * VALUEn The value to be matched in the FIELDn via the CONDn. + * The value must be URL encoded by the caller, if it contains any + * special characters including '(' and ')'. + */ +var QueryStringToExpression = { + /** + * Convert a query string to a nsIAbBooleanExpression. + * + * @param {string} qs - The query string to convert. + * @returns {nsIAbBooleanExpression} + */ + convert(qs) { + let tokens = this.parse(qs); + + // An array of nsIAbBooleanExpression, the first element is the root exp, + // the last element is the current operating exp. + let stack = []; + for (let { type, depth, value } of tokens) { + while (depth < stack.length) { + // We are done with the current exp, go one level up. + stack.pop(); + } + if (type == "op") { + if (depth == stack.length) { + // We are done with the current exp, go one level up. + stack.pop(); + } + // Found a new exp, go one level down. + let parent = stack.slice(-1)[0]; + let exp = this.createBooleanExpression(value); + stack.push(exp); + if (parent) { + parent.expressions = [...parent.expressions, exp]; + } + } else if (type == "field") { + // Add a new nsIAbBooleanConditionString to the current exp. + let condition = this.createBooleanConditionString(...value); + let exp = stack.slice(-1)[0]; + exp.expressions = [...exp.expressions, condition]; + } + } + + return stack[0]; + }, + + /** + * Parse a query string to an array of tokens. + * + * @param {string} qs - The query string to parse. + * @param {number} depth - The depth of a token. + * @param {object[]} tokens - The tokens to return. + * @param {"op"|"field"} tokens[].type - The token type. + * @param {number} tokens[].depth - The token depth. + * @param {string|string[]} tokens[].value - The token value. + */ + parse(qs, depth = 0, tokens = []) { + if (qs[0] == "?") { + qs = qs.slice(1); + } + while (qs[0] == ")" && depth > 0) { + depth--; + qs = qs.slice(1); + } + if (qs.length == 0) { + // End of input. + return tokens; + } + if (qs[0] != "(") { + throw Components.Exception( + `Invalid query string: ${qs}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + qs = qs.slice(1); + let nextOpen = qs.indexOf("("); + let nextClose = qs.indexOf(")"); + + if (nextOpen != -1 && nextOpen < nextClose) { + // Case: "OP(" + depth++; + tokens.push({ + type: "op", + depth, + value: qs.slice(0, nextOpen), + }); + this.parse(qs.slice(nextOpen), depth, tokens); + } else if (nextClose != -1) { + // Case: "FIELD, COND, VALUE)" + tokens.push({ + type: "field", + depth, + value: qs.slice(0, nextClose).split(","), + }); + this.parse(qs.slice(nextClose + 1), depth, tokens); + } + return tokens; + }, + + /** + * Create a nsIAbBooleanExpression from a string. + * + * @param {string} operation - The operation string. + * @returns {nsIAbBooleanExpression} + */ + createBooleanExpression(operation) { + let op = { + and: Ci.nsIAbBooleanOperationTypes.AND, + or: Ci.nsIAbBooleanOperationTypes.OR, + not: Ci.nsIAbBooleanOperationTypes.NOT, + }[operation]; + if (op == undefined) { + throw Components.Exception( + `Invalid operation: ${operation}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let exp = Cc["@mozilla.org/boolean-expression/n-peer;1"].createInstance( + Ci.nsIAbBooleanExpression + ); + exp.operation = op; + return exp; + }, + + /** + * Create a nsIAbBooleanConditionString. + * + * @param {string} name - The field name. + * @param {nsIAbBooleanConditionTypes} condition - The condition. + * @param {string} value - The value string. + * @returns {nsIAbBooleanConditionString} + */ + createBooleanConditionString(name, condition, value) { + value = decodeURIComponent(value); + let cond = { + "=": Ci.nsIAbBooleanConditionTypes.Is, + "!=": Ci.nsIAbBooleanConditionTypes.IsNot, + lt: Ci.nsIAbBooleanConditionTypes.LessThan, + gt: Ci.nsIAbBooleanConditionTypes.GreaterThan, + bw: Ci.nsIAbBooleanConditionTypes.BeginsWith, + ew: Ci.nsIAbBooleanConditionTypes.EndsWith, + c: Ci.nsIAbBooleanConditionTypes.Contains, + "!c": Ci.nsIAbBooleanConditionTypes.DoesNotContain, + "~=": Ci.nsIAbBooleanConditionTypes.SoundsLike, + regex: Ci.nsIAbBooleanConditionTypes.RegExp, + ex: Ci.nsIAbBooleanConditionTypes.Exists, + "!ex": Ci.nsIAbBooleanConditionTypes.DoesNotExist, + }[condition]; + if (name == "" || condition == "" || value == "" || cond == undefined) { + throw Components.Exception( + `Failed to create condition string from name=${name}, condition=${condition}, value=${value}, cond=${cond}`, + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let cs = Cc[ + "@mozilla.org/boolean-expression/condition-string;1" + ].createInstance(Ci.nsIAbBooleanConditionString); + cs.condition = cond; + + try { + cs.name = Services.textToSubURI.unEscapeAndConvert("UTF-8", name); + cs.value = Services.textToSubURI.unEscapeAndConvert("UTF-8", value); + } catch (e) { + cs.name = name; + cs.value = value; + } + return cs; + }, +}; diff --git a/comm/mailnews/addrbook/modules/SQLiteDirectory.jsm b/comm/mailnews/addrbook/modules/SQLiteDirectory.jsm new file mode 100644 index 0000000000..a89f2880d7 --- /dev/null +++ b/comm/mailnews/addrbook/modules/SQLiteDirectory.jsm @@ -0,0 +1,474 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["SQLiteDirectory"]; + +const { AddrBookDirectory } = ChromeUtils.import( + "resource:///modules/AddrBookDirectory.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +const { AsyncShutdown } = ChromeUtils.importESModule( + "resource://gre/modules/AsyncShutdown.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + newUID: "resource:///modules/AddrBookUtils.jsm", +}); + +var log = console.createInstance({ + prefix: "mail.addr_book", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.addr_book.loglevel", +}); + +// Track all directories by filename, for SQLiteDirectory.forFile. +var directories = new Map(); + +// Keep track of all database connections, and close them at shutdown, since +// nothing else ever tells us to close them. +var connections = new Map(); + +/** + * Opens an SQLite connection to `file`, caches the connection, and upgrades + * the database schema if necessary. + */ +function openConnectionTo(file) { + const CURRENT_VERSION = 4; + + let connection = connections.get(file.path); + if (!connection) { + connection = Services.storage.openDatabase(file); + let fileVersion = connection.schemaVersion; + + // If we're upgrading the version, first create a backup. + if (fileVersion > 0 && fileVersion < CURRENT_VERSION) { + let backupFile = file.clone(); + backupFile.leafName = backupFile.leafName.replace( + /\.sqlite$/, + `.v${fileVersion}.sqlite` + ); + backupFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + + log.warn(`Backing up ${file.leafName} to ${backupFile.leafName}`); + file.copyTo(null, backupFile.leafName); + } + + switch (fileVersion) { + case 0: + connection.executeSimpleSQL("PRAGMA journal_mode=WAL"); + connection.executeSimpleSQL( + "CREATE TABLE properties (card TEXT, name TEXT, value TEXT)" + ); + connection.executeSimpleSQL( + "CREATE TABLE lists (uid TEXT PRIMARY KEY, name TEXT, nickName TEXT, description TEXT)" + ); + connection.executeSimpleSQL( + "CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card))" + ); + // Falls through. + case 1: + connection.executeSimpleSQL( + "CREATE INDEX properties_card ON properties(card)" + ); + connection.executeSimpleSQL( + "CREATE INDEX properties_name ON properties(name)" + ); + // Falls through. + case 2: + connection.executeSimpleSQL("DROP TABLE IF EXISTS cards"); + // The lists table may have a localId column we no longer use, but + // since SQLite can't drop columns it's not worth effort to remove it. + // Falls through. + case 3: + // This version exists only to create an automatic backup before cards + // are transitioned to vCard. + connection.schemaVersion = CURRENT_VERSION; + break; + } + connections.set(file.path, connection); + } + return connection; +} + +/** + * Closes the SQLite connection to `file` and removes it from the cache. + */ +function closeConnectionTo(file) { + let connection = connections.get(file.path); + if (connection) { + return new Promise(resolve => { + connection.asyncClose({ + complete() { + resolve(); + }, + }); + connections.delete(file.path); + }); + } + return Promise.resolve(); +} + +// Close all open connections at shut down time. +AsyncShutdown.profileBeforeChange.addBlocker( + "Address Book: closing databases", + async () => { + let promises = []; + for (let directory of directories.values()) { + promises.push(directory.cleanUp()); + } + await Promise.allSettled(promises); + } +); + +// Close a connection on demand. This serves as an escape hatch from C++ code. +Services.obs.addObserver(async file => { + file.QueryInterface(Ci.nsIFile); + await closeConnectionTo(file); + Services.obs.notifyObservers(file, "addrbook-close-ab-complete"); +}, "addrbook-close-ab"); + +/** + * Adds SQLite storage to AddrBookDirectory. + */ +class SQLiteDirectory extends AddrBookDirectory { + init(uri) { + let uriParts = /^[\w-]+:\/\/([\w\.-]+\.\w+)$/.exec(uri); + if (!uriParts) { + throw new Components.Exception( + `Unexpected uri: ${uri}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + + this._uri = uri; + let fileName = uriParts[1]; + if (fileName.includes("/")) { + fileName = fileName.substring(0, fileName.indexOf("/")); + } + + for (let child of Services.prefs.getChildList("ldap_2.servers.")) { + if ( + child.endsWith(".filename") && + Services.prefs.getStringPref(child) == fileName + ) { + this._dirPrefId = child.substring(0, child.length - ".filename".length); + break; + } + } + if (!this._dirPrefId) { + throw Components.Exception( + `Couldn't grab dirPrefId for uri=${uri}, fileName=${fileName}`, + Cr.NS_ERROR_UNEXPECTED + ); + } + + // Make sure we always have a file. If a file is not created, the + // filename may be accidentally reused. + let file = lazy.FileUtils.getFile("ProfD", [fileName]); + if (!file.exists()) { + file.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + } + + this._fileName = fileName; + + super.init(uri); + + directories.set(fileName, this); + // Create the DB connection here already, to let init() throw on corrupt SQLite files. + this._dbConnection; + } + async cleanUp() { + await super.cleanUp(); + + if (this.hasOwnProperty("_file")) { + await closeConnectionTo(this._file); + delete this._file; + } + + directories.delete(this._fileName); + } + + get _dbConnection() { + this._file = lazy.FileUtils.getFile("ProfD", [this.fileName]); + let connection = openConnectionTo(this._file); + + // SQLite cache size can be set by the cacheSize preference, in KiB. + // The default is 5 MiB but this can be lowered to 1 MiB if wanted. + // There is no maximum size. + let cacheSize = this.getIntValue("cacheSize", 5120); // 5 MiB + cacheSize = Math.max(cacheSize, 1024); // 1 MiB + connection.executeSimpleSQL(`PRAGMA cache_size=-${cacheSize}`); + + Object.defineProperty(this, "_dbConnection", { + enumerable: true, + value: connection, + writable: false, + }); + return connection; + } + get lists() { + let listCache = new Map(); + let selectStatement = this._dbConnection.createStatement( + "SELECT uid, name, nickName, description FROM lists" + ); + while (selectStatement.executeStep()) { + listCache.set(selectStatement.row.uid, { + uid: selectStatement.row.uid, + name: selectStatement.row.name, + nickName: selectStatement.row.nickName, + description: selectStatement.row.description, + }); + } + selectStatement.finalize(); + + Object.defineProperty(this, "lists", { + enumerable: true, + value: listCache, + writable: false, + }); + return listCache; + } + get cards() { + let cardCache = new Map(); + let propertiesStatement = this._dbConnection.createStatement( + "SELECT card, name, value FROM properties" + ); + while (propertiesStatement.executeStep()) { + let uid = propertiesStatement.row.card; + if (!cardCache.has(uid)) { + cardCache.set(uid, new Map()); + } + let card = cardCache.get(uid); + if (card) { + card.set(propertiesStatement.row.name, propertiesStatement.row.value); + } + } + propertiesStatement.finalize(); + + Object.defineProperty(this, "cards", { + enumerable: true, + value: cardCache, + writable: false, + }); + return cardCache; + } + + loadCardProperties(uid) { + if (this.hasOwnProperty("cards")) { + let cachedCard = this.cards.get(uid); + if (cachedCard) { + return new Map(cachedCard); + } + } + let properties = new Map(); + let propertyStatement = this._dbConnection.createStatement( + "SELECT name, value FROM properties WHERE card = :card" + ); + propertyStatement.params.card = uid; + while (propertyStatement.executeStep()) { + properties.set(propertyStatement.row.name, propertyStatement.row.value); + } + propertyStatement.finalize(); + return properties; + } + saveCardProperties(uid, properties) { + try { + this._dbConnection.beginTransaction(); + let deleteStatement = this._dbConnection.createStatement( + "DELETE FROM properties WHERE card = :card" + ); + deleteStatement.params.card = uid; + deleteStatement.execute(); + let insertStatement = this._dbConnection.createStatement( + "INSERT INTO properties VALUES (:card, :name, :value)" + ); + + for (let [name, value] of properties) { + if (value !== null && value !== undefined && value !== "") { + insertStatement.params.card = uid; + insertStatement.params.name = name; + insertStatement.params.value = value; + insertStatement.execute(); + insertStatement.reset(); + } + } + + this._dbConnection.commitTransaction(); + deleteStatement.finalize(); + insertStatement.finalize(); + } catch (ex) { + this._dbConnection.rollbackTransaction(); + throw ex; + } + } + deleteCard(uid) { + let deleteStatement = this._dbConnection.createStatement( + "DELETE FROM properties WHERE card = :cardUID" + ); + deleteStatement.params.cardUID = uid; + deleteStatement.execute(); + deleteStatement.finalize(); + } + saveList(list) { + // Ensure list cache exists. + this.lists; + + let replaceStatement = this._dbConnection.createStatement( + "REPLACE INTO lists (uid, name, nickName, description) " + + "VALUES (:uid, :name, :nickName, :description)" + ); + replaceStatement.params.uid = list._uid; + replaceStatement.params.name = list._name; + replaceStatement.params.nickName = list._nickName; + replaceStatement.params.description = list._description; + replaceStatement.execute(); + replaceStatement.finalize(); + + this.lists.set(list._uid, { + uid: list._uid, + name: list._name, + nickName: list._nickName, + description: list._description, + }); + } + deleteList(uid) { + let deleteListStatement = this._dbConnection.createStatement( + "DELETE FROM lists WHERE uid = :uid" + ); + deleteListStatement.params.uid = uid; + deleteListStatement.execute(); + deleteListStatement.finalize(); + + if (this.hasOwnProperty("lists")) { + this.lists.delete(uid); + } + + this._dbConnection.executeSimpleSQL( + "DELETE FROM list_cards WHERE list NOT IN (SELECT DISTINCT uid FROM lists)" + ); + } + async bulkAddCards(cards) { + if (cards.length == 0) { + return; + } + + let usedUIDs = new Set(); + let propertiesStatement = this._dbConnection.createStatement( + "INSERT INTO properties VALUES (:card, :name, :value)" + ); + let propertiesArray = propertiesStatement.newBindingParamsArray(); + for (let card of cards) { + let uid = card.UID; + if (!uid || usedUIDs.has(uid)) { + // A card cannot have the same UID as one that already exists. + // Assign a new UID to avoid losing data. + uid = lazy.newUID(); + } + usedUIDs.add(uid); + + let cachedCard; + if (this.hasOwnProperty("cards")) { + cachedCard = new Map(); + this.cards.set(uid, cachedCard); + } + + for (let [name, value] of this.prepareToSaveCard(card)) { + let propertiesParams = propertiesArray.newBindingParams(); + propertiesParams.bindByName("card", uid); + propertiesParams.bindByName("name", name); + propertiesParams.bindByName("value", value); + propertiesArray.addParams(propertiesParams); + + if (cachedCard) { + cachedCard.set(name, value); + } + } + } + try { + this._dbConnection.beginTransaction(); + if (propertiesArray.length > 0) { + propertiesStatement.bindParameters(propertiesArray); + await new Promise((resolve, reject) => { + propertiesStatement.executeAsync({ + handleError(error) { + this._error = error; + }, + handleCompletion(status) { + if (status == Ci.mozIStorageStatementCallback.REASON_ERROR) { + reject( + Components.Exception(this._error.message, Cr.NS_ERROR_FAILURE) + ); + } else { + resolve(); + } + }, + }); + }); + propertiesStatement.finalize(); + } + this._dbConnection.commitTransaction(); + + Services.obs.notifyObservers(this, "addrbook-directory-invalidated"); + } catch (ex) { + this._dbConnection.rollbackTransaction(); + throw ex; + } + } + + /* nsIAbDirectory */ + + get childCardCount() { + let countStatement = this._dbConnection.createStatement( + "SELECT COUNT(DISTINCT card) AS card_count FROM properties" + ); + countStatement.executeStep(); + let count = countStatement.row.card_count; + countStatement.finalize(); + return count; + } + getCardFromProperty(property, value, caseSensitive) { + let sql = caseSensitive + ? "SELECT card FROM properties WHERE name = :name AND value = :value LIMIT 1" + : "SELECT card FROM properties WHERE name = :name AND LOWER(value) = LOWER(:value) LIMIT 1"; + let selectStatement = this._dbConnection.createStatement(sql); + selectStatement.params.name = property; + selectStatement.params.value = value; + let result = null; + if (selectStatement.executeStep()) { + result = this.getCard(selectStatement.row.card); + } + selectStatement.finalize(); + return result; + } + getCardsFromProperty(property, value, caseSensitive) { + let sql = caseSensitive + ? "SELECT card FROM properties WHERE name = :name AND value = :value" + : "SELECT card FROM properties WHERE name = :name AND LOWER(value) = LOWER(:value)"; + let selectStatement = this._dbConnection.createStatement(sql); + selectStatement.params.name = property; + selectStatement.params.value = value; + let results = []; + while (selectStatement.executeStep()) { + results.push(this.getCard(selectStatement.row.card)); + } + selectStatement.finalize(); + return results; + } + + static forFile(fileName) { + return directories.get(fileName); + } +} +SQLiteDirectory.prototype.classID = Components.ID( + "{e96ee804-0bd3-472f-81a6-8a9d65277ad3}" +); diff --git a/comm/mailnews/addrbook/modules/VCardUtils.jsm b/comm/mailnews/addrbook/modules/VCardUtils.jsm new file mode 100644 index 0000000000..a3ff0f5e14 --- /dev/null +++ b/comm/mailnews/addrbook/modules/VCardUtils.jsm @@ -0,0 +1,973 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = [ + "VCardService", + "VCardMimeConverter", + "VCardProperties", + "VCardPropertyEntry", + "VCardUtils", + "BANISHED_PROPERTIES", +]; + +const { ICAL } = ChromeUtils.import("resource:///modules/calendar/Ical.jsm"); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AddrBookCard: "resource:///modules/AddrBookCard.jsm", +}); + +/** + * Utilities for working with vCard data. This file uses ICAL.js as parser and + * formatter to avoid reinventing the wheel. + * + * @see RFC 6350. + */ + +var VCardUtils = { + _decodeQuotedPrintable(value) { + let bytes = []; + for (let b = 0; b < value.length; b++) { + if (value[b] == "=") { + bytes.push(parseInt(value.substr(b + 1, 2), 16)); + b += 2; + } else { + bytes.push(value.charCodeAt(b)); + } + } + return new TextDecoder().decode(new Uint8Array(bytes)); + }, + _parse(vProps) { + let vPropMap = new Map(); + for (let index = 0; index < vProps.length; index++) { + let { name, params, value } = vProps[index]; + + // Work out which type in typeMap, if any, this property belongs to. + + // To make the next piece easier, the type param must always be an array + // of lower-case strings. + let type = params.type || []; + if (type) { + if (Array.isArray(type)) { + type = type.map(t => t.toLowerCase()); + } else { + type = [type.toLowerCase()]; + } + } + + // Special cases for address and telephone types. + if (name == "adr") { + name = type.includes("home") ? "adr.home" : "adr.work"; + } + if (name == "tel") { + name = "tel.work"; + for (let t of type) { + if (["home", "work", "cell", "pager", "fax"].includes(t)) { + name = `tel.${t}`; + break; + } + } + } + // Preserve URL if no URL with type work is given take for `url.work` the URL without any type. + if (name == "url") { + name = type.includes("home") ? "url.home" : name; + name = type.includes("work") ? "url.work" : name; + } + + // Special treatment for `url`, which is not in the typeMap. + if (!(name in typeMap) && name != "url") { + continue; + } + + // The preference param is 1-100, lower numbers indicate higher + // preference. If not specified, the value is least preferred. + let pref = parseInt(params.pref, 10) || 101; + + if (!vPropMap.has(name)) { + vPropMap.set(name, []); + } + vPropMap.get(name).push({ index, pref, value }); + } + + // If no URL with type is specified assume its the Work Web Page (WebPage 1). + if (vPropMap.has("url") && !vPropMap.has("url.work")) { + vPropMap.set("url.work", vPropMap.get("url")); + } + // AbCard only supports Work Web Page or Home Web Page. Get rid of the URL without type. + vPropMap.delete("url"); + + for (let props of vPropMap.values()) { + // Sort the properties by preference, or by the order they appeared. + props.sort((a, b) => { + if (a.pref == b.pref) { + return a.index - b.index; + } + return a.pref - b.pref; + }); + } + return vPropMap; + }, + /** + * ICAL.js's parser only supports vCard 3.0 and 4.0. To maintain + * interoperability with other applications, here we convert vCard 2.1 + * cards into a "good-enough" mimic of vCard 4.0 so that the parser will + * read it without throwing an error. + * + * @param {string} vCard + * @returns {string} + */ + translateVCard21(vCard) { + if (!/\bVERSION:2.1\b/i.test(vCard)) { + return vCard; + } + + // Convert known type parameters to valid vCard 4.0, ignore unknown ones. + vCard = vCard.replace(/\n(([A-Z]+)(;[\w-]*)+):/gi, (match, key) => { + let parts = key.split(";"); + let newParts = [parts[0]]; + for (let i = 1; i < parts.length; i++) { + if (parts[i] == "") { + continue; + } + if ( + ["HOME", "WORK", "FAX", "PAGER", "CELL"].includes( + parts[i].toUpperCase() + ) + ) { + newParts.push(`TYPE=${parts[i]}`); + } else if (parts[i].toUpperCase() == "PREF") { + newParts.push("PREF=1"); + } else if (parts[i].toUpperCase() == "QUOTED-PRINTABLE") { + newParts.push("ENCODING=QUOTED-PRINTABLE"); + } + } + return "\n" + newParts.join(";") + ":"; + }); + + // Join quoted-printable wrapped lines together. This regular expression + // only matches lines that are quoted-printable and end with `=`. + let quotedNewLineRegExp = /(;ENCODING=QUOTED-PRINTABLE[;:][^\r\n]*)=\r?\n/i; + while (vCard.match(quotedNewLineRegExp)) { + vCard = vCard.replace(quotedNewLineRegExp, "$1"); + } + + // Strip the version. + return vCard.replace(/(\r?\n)VERSION:2.1\r?\n/i, "$1"); + }, + /** + * Return a new AddrBookCard from the provided vCard string. + * + * @param {string} vCard - The vCard string. + * @param {string} [uid] - An optional UID to be used for the new card, + * overriding any UID specified in the vCard string. + * @returns {AddrBookCard} + */ + vCardToAbCard(vCard, uid) { + vCard = this.translateVCard21(vCard); + + let abCard = new lazy.AddrBookCard(); + abCard.setProperty("_vCard", vCard); + + let vCardUID = abCard.vCardProperties.getFirstValue("uid"); + if (uid || vCardUID) { + abCard.UID = uid || vCardUID; + if (abCard.UID != vCardUID) { + abCard.vCardProperties.clearValues("uid"); + abCard.vCardProperties.addValue("uid", abCard.UID); + } + } + + return abCard; + }, + abCardToVCard(abCard, version) { + if (abCard.supportsVCard && abCard.getProperty("_vCard")) { + return abCard.vCardProperties.toVCard(); + } + + // Collect all of the AB card properties into a Map. + let abProps = new Map( + Array.from(abCard.properties, p => [p.name, p.value]) + ); + abProps.set("UID", abCard.UID); + + return this.propertyMapToVCard(abProps, version); + }, + propertyMapToVCard(abProps, version = "4.0") { + let vProps = [["version", {}, "text", version]]; + + // Add the properties to the vCard. + for (let vPropName of Object.keys(typeMap)) { + for (let vProp of typeMap[vPropName].fromAbCard(abProps, vPropName)) { + if (vProp[3] !== null && vProp[3] !== undefined && vProp[3] !== "") { + vProps.push(vProp); + } + } + } + + // If there's only one address or telephone number, don't specify type. + let adrProps = vProps.filter(p => p[0] == "adr"); + if (adrProps.length == 1) { + delete adrProps[0][1].type; + } + let telProps = vProps.filter(p => p[0] == "tel"); + if (telProps.length == 1) { + delete telProps[0][1].type; + } + + if (abProps.has("UID")) { + vProps.push(["uid", {}, "text", abProps.get("UID")]); + } + return ICAL.stringify(["vcard", vProps]); + }, +}; + +function VCardService() {} +VCardService.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIMsgVCardService"]), + classID: Components.ID("{e2e0f615-bc5a-4441-a16b-a26e75949376}"), + + vCardToAbCard(vCard) { + return vCard ? VCardUtils.vCardToAbCard(vCard) : null; + }, + escapedVCardToAbCard(vCard) { + return vCard ? VCardUtils.vCardToAbCard(decodeURIComponent(vCard)) : null; + }, + abCardToEscapedVCard(abCard) { + return abCard ? encodeURIComponent(VCardUtils.abCardToVCard(abCard)) : null; + }, +}; + +function VCardMimeConverter() {} +VCardMimeConverter.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsISimpleMimeConverter"]), + classID: Components.ID("{dafab386-bd4c-4238-bb48-228fbc98ba29}"), + + mailChannel: null, + uri: null, + convertToHTML(contentType, data) { + function escapeHTML(template, ...parts) { + let arr = []; + for (let i = 0; i < parts.length; i++) { + arr.push(template[i]); + arr.push( + parts[i] + .replace(/&/g, "&") + .replace(/"/g, """) + .replace(/</g, "<") + .replace(/>/g, ">") + ); + } + arr.push(template[template.length - 1]); + return arr.join(""); + } + + let abCard; + try { + abCard = VCardUtils.vCardToAbCard(data); + } catch (e) { + // We were given invalid vcard data. + return ""; + } + + let escapedVCard = encodeURIComponent(data); + + let propertiesTable = `<table class="moz-vcard-properties-table">`; + propertiesTable += escapeHTML`<tr><td class="moz-vcard-title-property">${abCard.displayName}`; + if (abCard.primaryEmail) { + propertiesTable += escapeHTML` <<a href="mailto:${abCard.primaryEmail}" private>${abCard.primaryEmail}</a>>`; + } + propertiesTable += `</td></tr>`; + for (let propName of ["JobTitle", "Department", "Company"]) { + let propValue = abCard.getProperty(propName, ""); + if (propValue) { + propertiesTable += escapeHTML`<tr><td class="moz-vcard-property">${propValue}</td></tr>`; + } + } + propertiesTable += `</table>`; + + // VCardChild.jsm and VCardParent.jsm handle clicking on this link. + return `<html> + <body> + <table class="moz-vcard-table"> + <tr> + <td valign="top"><a class="moz-vcard-badge" href="data:text/vcard,${escapedVCard}"></a></td> + <td> + ${propertiesTable} + </td> + </tr> + </table> + </body> + </html>`; + }, +}; + +const BANISHED_PROPERTIES = [ + "UID", + "PrimaryEmail", + "SecondEmail", + "DisplayName", + "NickName", + "Notes", + "Company", + "Department", + "JobTitle", + "BirthDay", + "BirthMonth", + "BirthYear", + "AnniversaryDay", + "AnniversaryMonth", + "AnniversaryYear", + "LastName", + "FirstName", + "AdditionalNames", + "NamePrefix", + "NameSuffix", + "HomePOBox", + "HomeAddress2", + "HomeAddress", + "HomeCity", + "HomeState", + "HomeZipCode", + "HomeCountry", + "WorkPOBox", + "WorkAddress2", + "WorkAddress", + "WorkCity", + "WorkState", + "WorkZipCode", + "WorkCountry", + "HomePhone", + "WorkPhone", + "FaxNumber", + "PagerNumber", + "CellularNumber", + "WebPage1", + "WebPage2", + "Custom1", + "Custom2", + "Custom3", + "Custom4", +]; + +/** Helper functions for typeMap. */ + +function singleTextProperty( + abPropName, + vPropName, + vPropParams = {}, + vPropType = "text" +) { + return { + /** + * Formats nsIAbCard properties into an array for use by ICAL.js. + * + * @param {Map} map - A map of address book properties to map. + * @yields {Array} - Values in a jCard array for use with ICAL.js. + */ + *fromAbCard(map) { + yield [vPropName, { ...vPropParams }, vPropType, map.get(abPropName)]; + }, + /** + * Parses a vCard value into properties usable by nsIAbCard. + * + * @param {string} value - vCard string to map to an address book card property. + * @yields {string[]} - Any number of key, value pairs to set on the nsIAbCard. + */ + *toAbCard(value) { + if (typeof value != "string") { + console.warn(`Unexpected value for ${vPropName}: ${value}`); + return; + } + yield [abPropName, value]; + }, + }; +} +function dateProperty(abCardPrefix, vPropName) { + return { + *fromAbCard(map) { + let year = map.get(`${abCardPrefix}Year`); + let month = map.get(`${abCardPrefix}Month`); + let day = map.get(`${abCardPrefix}Day`); + + if (!year && !month && !day) { + return; + } + + let dateValue = new ICAL.VCardTime({}, null, "date"); + // Set the properties directly instead of using the VCardTime + // constructor argument, which causes null values to become 0. + dateValue.year = year ? Number(year) : null; + dateValue.month = month ? Number(month) : null; + dateValue.day = day ? Number(day) : null; + + yield [vPropName, {}, "date", dateValue.toString()]; + }, + *toAbCard(value) { + try { + let dateValue = ICAL.VCardTime.fromDateAndOrTimeString(value); + yield [`${abCardPrefix}Year`, String(dateValue.year ?? "")]; + yield [`${abCardPrefix}Month`, String(dateValue.month ?? "")]; + yield [`${abCardPrefix}Day`, String(dateValue.day ?? "")]; + } catch (ex) { + console.error(ex); + } + }, + }; +} +function multiTextProperty(abPropNames, vPropName, vPropParams = {}) { + return { + *fromAbCard(map) { + if (abPropNames.every(name => !map.has(name))) { + return; + } + let vPropValues = abPropNames.map(name => map.get(name) || ""); + if (vPropValues.some(Boolean)) { + yield [vPropName, { ...vPropParams }, "text", vPropValues]; + } + }, + *toAbCard(value) { + if (Array.isArray(value)) { + for (let abPropName of abPropNames) { + let valuePart = value.shift(); + if (abPropName && valuePart) { + yield [ + abPropName, + Array.isArray(valuePart) ? valuePart.join(" ") : valuePart, + ]; + } + } + } else if (typeof value == "string") { + // Only one value was given. + yield [abPropNames[0], value]; + } else { + console.warn(`Unexpected value for ${vPropName}: ${value}`); + } + }, + }; +} + +/** + * Properties we support for conversion between nsIAbCard and vCard. + * + * Keys correspond to vCard property keys, with the type appended where more + * than one type is supported (e.g. work and home). + * + * Values are objects with toAbCard and fromAbCard functions which convert + * property values in each direction. See the docs on the object returned by + * singleTextProperty. + */ +var typeMap = { + fn: singleTextProperty("DisplayName", "fn"), + email: { + *fromAbCard(map) { + yield ["email", { pref: "1" }, "text", map.get("PrimaryEmail")]; + yield ["email", {}, "text", map.get("SecondEmail")]; + }, + toAbCard: singleTextProperty("PrimaryEmail", "email", { pref: "1" }) + .toAbCard, + }, + nickname: singleTextProperty("NickName", "nickname"), + note: singleTextProperty("Notes", "note"), + org: multiTextProperty(["Company", "Department"], "org"), + title: singleTextProperty("JobTitle", "title"), + bday: dateProperty("Birth", "bday"), + anniversary: dateProperty("Anniversary", "anniversary"), + n: multiTextProperty( + ["LastName", "FirstName", "AdditionalNames", "NamePrefix", "NameSuffix"], + "n" + ), + "adr.home": multiTextProperty( + [ + "HomePOBox", + "HomeAddress2", + "HomeAddress", + "HomeCity", + "HomeState", + "HomeZipCode", + "HomeCountry", + ], + "adr", + { type: "home" } + ), + "adr.work": multiTextProperty( + [ + "WorkPOBox", + "WorkAddress2", + "WorkAddress", + "WorkCity", + "WorkState", + "WorkZipCode", + "WorkCountry", + ], + "adr", + { type: "work" } + ), + "tel.home": singleTextProperty("HomePhone", "tel", { type: "home" }), + "tel.work": singleTextProperty("WorkPhone", "tel", { type: "work" }), + "tel.fax": singleTextProperty("FaxNumber", "tel", { type: "fax" }), + "tel.pager": singleTextProperty("PagerNumber", "tel", { type: "pager" }), + "tel.cell": singleTextProperty("CellularNumber", "tel", { type: "cell" }), + "url.work": singleTextProperty("WebPage1", "url", { type: "work" }, "url"), + "url.home": singleTextProperty("WebPage2", "url", { type: "home" }, "url"), + "x-custom1": singleTextProperty("Custom1", "x-custom1"), + "x-custom2": singleTextProperty("Custom2", "x-custom2"), + "x-custom3": singleTextProperty("Custom3", "x-custom3"), + "x-custom4": singleTextProperty("Custom4", "x-custom4"), +}; + +/** + * Any value that can be represented in a vCard. A value can be a boolean, + * number, string, or an array, depending on the data. A top-level array might + * contain primitives and/or second-level arrays of primitives. + * + * @see ICAL.design + * @see RFC6350 + * + * @typedef {boolean|number|string|vCardValue[]} vCardValue + */ + +/** + * Represents a single entry in a vCard ("contentline" in RFC6350 terms). + * The name, params, type and value are as returned by ICAL. + */ +class VCardPropertyEntry { + #name = null; + #params = null; + #type = null; + #value = null; + _original = null; + + /** + * @param {string} name + * @param {object} params + * @param {string} type + * @param {vCardValue} value + */ + constructor(name, params, type, value) { + this.#name = name; + this.#params = params; + this.#type = type; + if (params.encoding?.toUpperCase() == "QUOTED-PRINTABLE") { + if (Array.isArray(value)) { + value = value.map(VCardUtils._decodeQuotedPrintable); + } else { + value = VCardUtils._decodeQuotedPrintable(value); + } + delete params.encoding; + delete params.charset; + } + this.#value = value; + this._original = this; + } + + /** + * @type {string} + */ + get name() { + return this.#name; + } + + /** + * @type {object} + */ + get params() { + return this.#params; + } + + /** + * @type {string} + */ + get type() { + return this.#type; + } + set type(type) { + this.#type = type; + } + + /** + * @type {vCardValue} + */ + get value() { + return this.#value; + } + set value(value) { + this.#value = value; + } + + /** + * Clone this object. + * + * @returns {VCardPropertyEntry} + */ + clone() { + let cloneValue; + if (Array.isArray(this.#value)) { + cloneValue = this.#value.map(v => (Array.isArray(v) ? v.slice() : v)); + } else { + cloneValue = this.#value; + } + + let clone = new VCardPropertyEntry( + this.#name, + { ...this.#params }, + this.#type, + cloneValue + ); + clone._original = this; + return clone; + } + + /** + * @param {VCardPropertyEntry} other + */ + equals(other) { + if (other.constructor.name != "VCardPropertyEntry") { + return false; + } + return this._original == other._original; + } +} + +/** + * Represents an entire vCard as a collection of `VCardPropertyEntry` objects. + */ +class VCardProperties { + /** + * All of the vCard entries in this object. + * + * @type {VCardPropertyEntry[]} + */ + entries = []; + + /** + * @param {?string} version - The version of vCard to use. Valid values are + * "3.0" and "4.0". If unspecified, vCard 3.0 will be used. + */ + constructor(version) { + if (version) { + if (!["3.0", "4.0"].includes(version)) { + throw new Error(`Unsupported vCard version: ${version}`); + } + this.addEntry(new VCardPropertyEntry("version", {}, "text", version)); + } + } + + /** + * Parse a vCard into a VCardProperties object. + * + * @param {string} vCard + * @returns {VCardProperties} + */ + static fromVCard(vCard, { isGoogleCardDAV = false } = {}) { + vCard = VCardUtils.translateVCard21(vCard); + + let rv = new VCardProperties(); + let [, properties] = ICAL.parse(vCard); + for (let property of properties) { + let [name, params, type, value] = property; + if (property.length > 4) { + // The jCal format stores multiple values as the 4th...nth items. + // VCardPropertyEntry has only one place for a value, so store an + // array instead. This applies to CATEGORIES and NICKNAME types in + // vCard 4 and also NOTE in vCard 3. + value = property.slice(3); + } + if (isGoogleCardDAV) { + // Google escapes the characters \r : , ; and \ unnecessarily, in + // violation of RFC6350. Removing the escaping at this point means no + // other code requires a special case for it. + if (Array.isArray(value)) { + value = value.map(v => v.replace(/\\r/g, "\r").replace(/\\:/g, ":")); + } else { + value = value.replace(/\\r/g, "\r").replace(/\\:/g, ":"); + if (["phone-number", "uri"].includes(type)) { + value = value.replace(/\\([,;\\])/g, "$1"); + } + } + } + rv.addEntry(new VCardPropertyEntry(name, params, type, value)); + } + return rv; + } + + /** + * Parse a Map of Address Book properties into a VCardProperties object. + * + * @param {Map<string, string>} propertyMap + * @param {string} [version="4.0"] + * @returns {VCardProperties} + */ + static fromPropertyMap(propertyMap, version = "4.0") { + let rv = new VCardProperties(version); + + for (let vPropName of Object.keys(typeMap)) { + for (let vProp of typeMap[vPropName].fromAbCard(propertyMap, vPropName)) { + if (vProp[3] !== null && vProp[3] !== undefined && vProp[3] !== "") { + rv.addEntry(new VCardPropertyEntry(...vProp)); + } + } + } + + return rv; + } + + /** + * Used to determine the default value type when adding values. + * Either `ICAL.design.vcard` for (vCard 4.0) or `ICAL.design.vcard3` (3.0). + * + * @type {ICAL.design.designSet} + */ + designSet = ICAL.design.vcard3; + + /** + * Add an entry to this object. + * + * @param {VCardPropertyEntry} entry - The entry to add. + * @returns {boolean} - If the entry was added. + */ + addEntry(entry) { + if (entry.constructor.name != "VCardPropertyEntry") { + throw new Error("Not a VCardPropertyEntry"); + } + + if (this.entries.find(e => e.equals(entry))) { + return false; + } + + if (entry.name == "version") { + if (entry.value == "3.0") { + this.designSet = ICAL.design.vcard3; + } else if (entry.value == "4.0") { + this.designSet = ICAL.design.vcard; + } else { + throw new Error(`Unsupported vCard version: ${entry.value}`); + } + // Version must be the first entry, so clear out any existing values + // and add it to the start of the collection. + this.clearValues("version"); + this.entries.unshift(entry); + return true; + } + + this.entries.push(entry); + return true; + } + + /** + * Add an entry to this object by name and value. + * + * @param {string} name + * @param {string} value + * @returns {VCardPropertyEntry} + */ + addValue(name, value) { + for (let entry of this.getAllEntries(name)) { + if (entry.value == value) { + return entry; + } + } + + let newEntry = new VCardPropertyEntry( + name, + {}, + this.designSet.property[name].defaultType, + value + ); + this.entries.push(newEntry); + return newEntry; + } + + /** + * Remove an entry from this object. + * + * @param {VCardPropertyEntry} entry - The entry to remove. + * @returns {boolean} - If an entry was found and removed. + */ + removeEntry(entry) { + if (entry.constructor.name != "VCardPropertyEntry") { + throw new Error("Not a VCardPropertyEntry"); + } + + let index = this.entries.findIndex(e => e.equals(entry)); + if (index >= 0) { + this.entries.splice(index, 1); + return true; + } + return false; + } + + /** + * Remove entries from this object by name and value. All entries matching + * the name and value will be removed. + * + * @param {string} name + * @param {string} value + */ + removeValue(name, value) { + for (let entry of this.getAllEntries(name)) { + if (entry.value == value) { + this.removeEntry(entry); + } + } + } + + /** + * Remove entries from this object by name. All entries matching the name + * will be removed. + * + * @param {string} name + */ + clearValues(name) { + for (let entry of this.getAllEntries(name)) { + this.removeEntry(entry); + } + } + + /** + * Get the first value matching the given name, or null if no entry matches. + * + * @param {string} name + * @returns {?vCardValue} + */ + getFirstValue(name) { + let entry = this.entries.find(e => e.name == name); + if (entry) { + return entry.value; + } + return null; + } + + /** + * Get all values matching the given name. + * + * @param {string} name + * @returns {vCardValue[]} + */ + getAllValues(name) { + return this.getAllEntries(name).map(e => e.value); + } + + /** + * Get all values matching the given name, sorted in order of preference. + * Preference is determined by the `pref` parameter if it exists, then by + * the position in `entries`. + * + * @param {string} name + * @returns {vCardValue[]} + */ + getAllValuesSorted(name) { + return this.getAllEntriesSorted(name).map(e => e.value); + } + + /** + * Get the first entry matching the given name, or null if no entry matches. + * + * @param {string} name + * @returns {?VCardPropertyEntry} + */ + getFirstEntry(name) { + return this.entries.find(e => e.name == name) ?? null; + } + + /** + * Get all entries matching the given name. + * + * @param {string} name + * @returns {VCardPropertyEntry[]} + */ + getAllEntries(name) { + return this.entries.filter(e => e.name == name); + } + + /** + * Get all entries matching the given name, sorted in order of preference. + * Preference is determined by the `pref` parameter if it exists, then by + * the position in `entries`. + * + * @param {string} name + * @returns {VCardPropertyEntry[]} + */ + getAllEntriesSorted(name) { + let nextPref = 101; + let entries = this.getAllEntries(name).map(e => { + return { entry: e, pref: e.params.pref || nextPref++ }; + }); + entries.sort((a, b) => a.pref - b.pref); + return entries.map(e => e.entry); + } + + /** + * Get all entries matching the given group. + * + * @param {string} group + * @returns {VCardPropertyEntry[]} + */ + getGroupedEntries(group) { + return this.entries.filter(e => e.params.group == group); + } + + /** + * Clone this object. + * + * @returns {VCardProperties} + */ + clone() { + let copy = new VCardProperties(); + copy.entries = this.entries.map(e => e.clone()); + return copy; + } + + /** + * Get a Map of Address Book properties from this object. + * + * @returns {Map<string, string>} propertyMap + */ + toPropertyMap() { + let vPropMap = VCardUtils._parse(this.entries.map(e => e.clone())); + let propertyMap = new Map(); + + for (let [name, props] of vPropMap) { + // Store the value(s) on the abCard. + for (let [abPropName, abPropValue] of typeMap[name].toAbCard( + props[0].value + )) { + if (abPropValue) { + propertyMap.set(abPropName, abPropValue); + } + } + // Special case for email, which can also have a second preference. + if (name == "email" && props.length > 1) { + propertyMap.set("SecondEmail", props[1].value); + } + } + + return propertyMap; + } + + /** + * Serialize this object into a vCard. + * + * @returns {string} vCard + */ + toVCard() { + let jCal = this.entries.map(e => { + if (Array.isArray(e.value)) { + let design = this.designSet.property[e.name]; + if (design.multiValue == "," && !design.structuredValue) { + // The jCal format stores multiple values as the 4th...nth items, + // but VCardPropertyEntry stores them as an array. This applies to + // CATEGORIES and NICKNAME types in vCard 4 and also NOTE in vCard 3. + return [e.name, e.params, e.type, ...e.value]; + } + } + return [e.name, e.params, e.type, e.value]; + }); + return ICAL.stringify(["vcard", jCal]); + } +} diff --git a/comm/mailnews/addrbook/modules/components.conf b/comm/mailnews/addrbook/modules/components.conf new file mode 100644 index 0000000000..137150d06f --- /dev/null +++ b/comm/mailnews/addrbook/modules/components.conf @@ -0,0 +1,136 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + "cid": "{e96ee804-0bd3-472f-81a6-8a9d65277ad3}", + "contract_ids": ["@mozilla.org/addressbook/directory;1?type=jsaddrbook"], + "jsm": "resource:///modules/SQLiteDirectory.jsm", + "constructor": "SQLiteDirectory", + }, + { + "cid": "{1143991d-31cd-4ea6-9c97-c587d990d724}", + "contract_ids": ["@mozilla.org/addressbook/jsaddrbookcard;1"], + "jsm": "resource:///modules/AddrBookCard.jsm", + "constructor": "AddrBookCard", + }, + { + "cid": "{224d3ef9-d81c-4d94-8826-a79a5835af93}", + "contract_ids": ["@mozilla.org/abmanager;1"], + "jsm": "resource:///modules/AddrBookManager.jsm", + "constructor": "AddrBookManager", + "name": "AbManager", + "interfaces": ["nsIAbManager"], + }, + { + "cid": "{1fa9941a-07d5-4a6f-9673-15327fc2b9ab}", + "contract_ids": ["@mozilla.org/addressbook/directory;1?type=jscarddav"], + "jsm": "resource:///modules/CardDAVDirectory.jsm", + "constructor": "CardDAVDirectory", + }, + { + "cid": "{e2e0f615-bc5a-4441-a16b-a26e75949376}", + "contract_ids": ["@mozilla.org/addressbook/msgvcardservice;1"], + "jsm": "resource:///modules/VCardUtils.jsm", + "constructor": "VCardService", + }, + { + "cid": "{e9fb36ec-c980-4a77-9f68-0eb10491eda8}", + "contract_ids": ["@mozilla.org/mimecth;1?type=text/vcard"], + "jsm": "resource:///modules/VCardUtils.jsm", + "constructor": "VCardMimeConverter", + "categories": {"simple-mime-converters": "text/vcard"}, + }, + { + "cid": "{dafab386-bd4c-4238-bb48-228fbc98ba29}", + "contract_ids": ["@mozilla.org/mimecth;1?type=text/x-vcard"], + "jsm": "resource:///modules/VCardUtils.jsm", + "constructor": "VCardMimeConverter", + "categories": {"simple-mime-converters": "text/x-vcard"}, + }, + { + "cid": "{f87b71b5-2a0f-4b37-8e4f-3c899f6b8432}", + "contract_ids": ["@mozilla.org/network/ldap-connection;1"], + "jsm": "resource:///modules/LDAPConnection.jsm", + "constructor": "LDAPConnection", + }, + { + "cid": "{a6f94ca4-cd2d-4983-bcf2-fe936190955c}", + "contract_ids": ["@mozilla.org/network/ldap-operation;1"], + "jsm": "resource:///modules/LDAPOperation.jsm", + "constructor": "LDAPOperation", + }, + { + "cid": "{8683e821-f1b0-476d-ac15-07771c79bb11}", + "contract_ids": [ + "@mozilla.org/addressbook/directory;1?type=moz-abldapdirectory" + ], + "jsm": "resource:///modules/LDAPDirectory.jsm", + "constructor": "LDAPDirectory", + }, + { + "cid": "{5ad5d311-1a50-43db-a03c-63d45f443903}", + "contract_ids": ["@mozilla.org/addressbook/ldap-directory-query;1"], + "jsm": "resource:///modules/LDAPDirectoryQuery.jsm", + "constructor": "LDAPDirectoryQuery", + }, + { + "cid": "{dbe204e8-ae09-11eb-b4c8-a7e4b3e6e82e}", + "contract_ids": ["@mozilla.org/addressbook/ldap-replication-service;1"], + "jsm": "resource:///modules/LDAPReplicationService.jsm", + "constructor": "LDAPReplicationService", + }, + { + "cid": "{e8b59b32-f83f-4d5f-8eb5-e3c1e5de0d47}", + "contract_ids": ["@mozilla.org/network/ldap-service;1"], + "jsm": "resource:///modules/LDAPService.jsm", + "constructor": "LDAPService", + }, + { + "cid": "{50ca73fa-7deb-42b9-9eec-e219e31e6d4b}", + "contract_ids": ["@mozilla.org/network/ldap-url-parser;1"], + "jsm": "resource:///modules/LDAPURLParser.jsm", + "constructor": "LDAPURLParser", + }, + { + "cid": "{b3de9249-b0e5-4c12-8d91-c9a434fd80f5}", + "contract_ids": ["@mozilla.org/network/protocol;1?name=ldap"], + "jsm": "resource:///modules/LDAPProtocolHandler.jsm", + "constructor": "LDAPProtocolHandler", + "protocol_config": { + "scheme": "ldap", + "flags": [ + "URI_NORELATIVE", + "URI_DANGEROUS_TO_LOAD", + "ALLOWS_PROXY", + ], + "default_port": 389, + }, + }, + { + "cid": "{c85a5ef2-9c56-445f-b029-76889f2dd29b}", + "contract_ids": ["@mozilla.org/network/protocol;1?name=ldaps"], + "jsm": "resource:///modules/LDAPProtocolHandler.jsm", + "constructor": "LDAPSProtocolHandler", + "protocol_config": { + "scheme": "ldaps", + "flags": [ + "URI_NORELATIVE", + "URI_DANGEROUS_TO_LOAD", + "ALLOWS_PROXY", + ], + "default_port": 636, + }, + }, +] + +if buildconfig.substs["MOZ_PREF_EXTENSIONS"]: + Classes += [ + { + "cid": "{53d16809-1114-44e2-b585-41a2abb18f66}", + "contract_ids": ["@mozilla.org/ldapsyncquery;1"], + "jsm": "resource:///modules/LDAPSyncQuery.jsm", + "constructor": "LDAPSyncQuery", + }, + ] diff --git a/comm/mailnews/addrbook/modules/moz.build b/comm/mailnews/addrbook/modules/moz.build new file mode 100644 index 0000000000..8b86aa9555 --- /dev/null +++ b/comm/mailnews/addrbook/modules/moz.build @@ -0,0 +1,34 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +EXTRA_JS_MODULES += [ + "AddrBookCard.jsm", + "AddrBookDirectory.jsm", + "AddrBookMailingList.jsm", + "AddrBookManager.jsm", + "AddrBookUtils.jsm", + "CardDAVDirectory.jsm", + "CardDAVUtils.jsm", + "LDAPClient.jsm", + "LDAPConnection.jsm", + "LDAPDirectory.jsm", + "LDAPDirectoryQuery.jsm", + "LDAPListenerBase.jsm", + "LDAPMessage.jsm", + "LDAPOperation.jsm", + "LDAPProtocolHandler.jsm", + "LDAPReplicationService.jsm", + "LDAPService.jsm", + "LDAPURLParser.jsm", + "QueryStringToExpression.jsm", + "SQLiteDirectory.jsm", + "VCardUtils.jsm", +] + +if CONFIG["MOZ_PREF_EXTENSIONS"]: + EXTRA_JS_MODULES += ["LDAPSyncQuery.jsm"] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/comm/mailnews/addrbook/moz.build b/comm/mailnews/addrbook/moz.build new file mode 100644 index 0000000000..ecf2c5590b --- /dev/null +++ b/comm/mailnews/addrbook/moz.build @@ -0,0 +1,12 @@ +# 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 += [ + "modules", + "public", + "src", +] + +TEST_DIRS += ["test"] diff --git a/comm/mailnews/addrbook/prefs/content/pref-directory-add.js b/comm/mailnews/addrbook/prefs/content/pref-directory-add.js new file mode 100644 index 0000000000..23bee12a0e --- /dev/null +++ b/comm/mailnews/addrbook/prefs/content/pref-directory-add.js @@ -0,0 +1,454 @@ +/* -*- Mode: JavaScript; indent-tabs-mode: nil; c-basic-offset: 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" +); +var { isLegalHostNameOrIP, cleanUpHostName } = ChromeUtils.import( + "resource:///modules/hostnameUtils.jsm" +); + +var gCurrentDirectory = null; +var gReplicationBundle = null; +var gReplicationService = Cc[ + "@mozilla.org/addressbook/ldap-replication-service;1" +].getService(Ci.nsIAbLDAPReplicationService); +var gReplicationCancelled = false; +var gProgressText; +var gProgressMeter; +var gDownloadInProgress = false; + +var kDefaultLDAPPort = 389; +var kDefaultSecureLDAPPort = 636; + +window.addEventListener("DOMContentLoaded", Startup); +window.addEventListener("unload", onUnload); +document.addEventListener("dialogaccept", onAccept); +document.addEventListener("dialogcancel", onCancel); + +var ldapOfflineObserver = { + observe(subject, topic, state) { + // sanity checks + if (topic != "network:offline-status-changed") { + return; + } + setDownloadOfflineOnlineState(state == "offline"); + }, +}; + +function Startup() { + gReplicationBundle = document.getElementById("bundle_replication"); + + document.getElementById("download").label = + gReplicationBundle.getString("downloadButton"); + document.getElementById("download").accessKey = gReplicationBundle.getString( + "downloadButton.accesskey" + ); + + if ( + "arguments" in window && + window.arguments[0] && + window.arguments[0].selectedDirectory + ) { + gCurrentDirectory = window.arguments[0].selectedDirectory; + try { + fillSettings(); + } catch (ex) { + dump( + "pref-directory-add.js:Startup(): fillSettings() exception: " + + ex + + "\n" + ); + } + + let oldListName = gCurrentDirectory.dirName; + document.title = gReplicationBundle.getFormattedString( + "directoryTitleEdit", + [oldListName] + ); + + // Only set up the download button for online/offline status toggling + // if the pref isn't locked to disable the button. + if ( + !Services.prefs.prefIsLocked( + gCurrentDirectory.dirPrefId + ".disable_button_download" + ) + ) { + // Now connect to the offline/online observer + Services.obs.addObserver( + ldapOfflineObserver, + "network:offline-status-changed" + ); + + // Now set the initial offline/online state and update the state + setDownloadOfflineOnlineState(Services.io.offline); + } + } else { + document.title = gReplicationBundle.getString("directoryTitleNew"); + fillDefaultSettings(); + // Don't add observer here as it doesn't make any sense. + } +} + +function onUnload() { + if ( + "arguments" in window && + window.arguments[0] && + window.arguments[0].selectedDirectory && + !Services.prefs.prefIsLocked( + gCurrentDirectory.dirPrefId + ".disable_button_download" + ) + ) { + // Remove the observer that we put in on dialog startup + Services.obs.removeObserver( + ldapOfflineObserver, + "network:offline-status-changed" + ); + } +} + +var progressListener = { + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + // start the spinning + gProgressMeter.removeAttribute("value"); + gProgressText.value = gReplicationBundle.getString( + aStatus ? "replicationStarted" : "changesStarted" + ); + gDownloadInProgress = true; + document.getElementById("download").label = gReplicationBundle.getString( + "cancelDownloadButton" + ); + document.getElementById("download").accessKey = + gReplicationBundle.getString("cancelDownloadButton.accesskey"); + } + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + EndDownload(aStatus); + } + }, + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) { + gProgressText.value = gReplicationBundle.getFormattedString( + "currentCount", + [aCurSelfProgress] + ); + }, + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {}, + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {}, + onSecurityChange(aWebProgress, aRequest, state) {}, + onContentBlockingEvent(aWebProgress, aRequest, aEvent) {}, + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), +}; + +function DownloadNow() { + if (!gDownloadInProgress) { + gProgressText = document.getElementById("replicationProgressText"); + gProgressMeter = document.getElementById("replicationProgressMeter"); + + gProgressText.hidden = false; + gProgressMeter.hidden = false; + gReplicationCancelled = false; + + try { + if (gCurrentDirectory instanceof Ci.nsIAbLDAPDirectory) { + gReplicationService.startReplication( + gCurrentDirectory, + progressListener + ); + } else { + EndDownload(Cr.NS_ERROR_FAILURE); + } + } catch (ex) { + EndDownload(Cr.NS_ERROR_FAILURE); + } + } else { + gReplicationCancelled = true; + try { + gReplicationService.cancelReplication(gCurrentDirectory); + } catch (ex) { + // XXX todo + // perhaps replication hasn't started yet? This can happen if you hit cancel after attempting to replication when offline + dump("unexpected failure while cancelling. ex=" + ex + "\n"); + } + } +} + +function EndDownload(aStatus) { + document.getElementById("download").label = + gReplicationBundle.getString("downloadButton"); + document.getElementById("download").accessKey = gReplicationBundle.getString( + "downloadButton.accesskey" + ); + + // stop the spinning + gProgressMeter.value = 100; + gProgressMeter.hidden = true; + + gDownloadInProgress = false; + if (Components.isSuccessCode(aStatus)) { + gProgressText.value = gReplicationBundle.getString("replicationSucceeded"); + } else if (gReplicationCancelled) { + gProgressText.value = gReplicationBundle.getString("replicationCancelled"); + } else { + gProgressText.value = gReplicationBundle.getString("replicationFailed"); + } +} + +// fill the settings panel with the data from the preferences. +// +function fillSettings() { + document.getElementById("description").value = gCurrentDirectory.dirName; + + if (gCurrentDirectory instanceof Ci.nsIAbLDAPDirectory) { + var ldapUrl = gCurrentDirectory.lDAPURL; + + document.getElementById("results").value = gCurrentDirectory.maxHits; + document.getElementById("login").value = gCurrentDirectory.authDn; + document.getElementById("hostname").value = ldapUrl.host; + document.getElementById("basedn").value = ldapUrl.dn; + document.getElementById("search").value = ldapUrl.filter; + + var sub = document.getElementById("sub"); + switch (ldapUrl.scope) { + case Ci.nsILDAPURL.SCOPE_ONELEVEL: + sub.radioGroup.selectedItem = document.getElementById("one"); + break; + default: + sub.radioGroup.selectedItem = sub; + break; + } + + var sasl = document.getElementById("saslMechanism"); + switch (gCurrentDirectory.saslMechanism) { + case "GSSAPI": + sasl.selectedItem = document.getElementById("GSSAPI"); + break; + default: + sasl.selectedItem = document.getElementById("Simple"); + break; + } + + var secure = ldapUrl.options & ldapUrl.OPT_SECURE; + if (secure) { + document.getElementById("secure").setAttribute("checked", "true"); + } + + if (ldapUrl.port == -1) { + document.getElementById("port").value = secure + ? kDefaultSecureLDAPPort + : kDefaultLDAPPort; + } else { + document.getElementById("port").value = ldapUrl.port; + } + } + + // check if any of the preferences for this server are locked. + // If they are locked disable them + DisableUriFields(gCurrentDirectory.dirPrefId + ".uri"); + DisableElementIfPrefIsLocked( + gCurrentDirectory.dirPrefId + ".description", + "description" + ); + DisableElementIfPrefIsLocked( + gCurrentDirectory.dirPrefId + ".disable_button_download", + "download" + ); + DisableElementIfPrefIsLocked( + gCurrentDirectory.dirPrefId + ".maxHits", + "results" + ); + DisableElementIfPrefIsLocked( + gCurrentDirectory.dirPrefId + ".auth.dn", + "login" + ); +} + +function DisableElementIfPrefIsLocked(aPrefName, aElementId) { + if (Services.prefs.prefIsLocked(aPrefName)) { + document.getElementById(aElementId).setAttribute("disabled", true); + } +} + +// disables all the text fields corresponding to the .uri pref. +function DisableUriFields(aPrefName) { + if (Services.prefs.prefIsLocked(aPrefName)) { + let lockedElements = document.querySelectorAll('[disableiflocked="true"]'); + for (let i = 0; i < lockedElements.length; i++) { + lockedElements[i].setAttribute("disabled", "true"); + } + } +} + +function onSecure() { + document.getElementById("port").value = document.getElementById("secure") + .checked + ? kDefaultSecureLDAPPort + : kDefaultLDAPPort; +} + +function fillDefaultSettings() { + document.getElementById("port").value = kDefaultLDAPPort; + var sub = document.getElementById("sub"); + sub.radioGroup.selectedItem = sub; + + // Disable the download button and add some text indicating why. + document.getElementById("download").disabled = true; + document.getElementById("downloadWarningMsg").hidden = false; + document.getElementById("downloadWarningMsg").textContent = document + .getElementById("bundle_addressBook") + .getString("abReplicationSaveSettings"); +} + +function hasCharacters(number) { + var re = /[0-9]/g; + var num = number.match(re); + if (num && num.length == number.length) { + return false; + } + return true; +} + +function onAccept(event) { + try { + let description = document.getElementById("description").value.trim(); + let hostname = cleanUpHostName(document.getElementById("hostname").value); + let port = document.getElementById("port").value; + let secure = document.getElementById("secure"); + let results = document.getElementById("results").value; + let errorValue = null; + let errorArg = null; + let saslMechanism = ""; + + let findDupeName = function (newName) { + // Do not allow an already existing name. + for (let ab of MailServices.ab.directories) { + if ( + ab.dirName.toLowerCase() == newName.toLowerCase() && + (!gCurrentDirectory || ab.URI != gCurrentDirectory.URI) + ) { + return ab.dirName; + } + } + return null; + }; + + if (!description) { + errorValue = "invalidName"; + } else if ((errorArg = findDupeName(description))) { + errorValue = "duplicateNameText"; + } else if (!isLegalHostNameOrIP(hostname)) { + errorValue = "invalidHostname"; + } else if (port && hasCharacters(port)) { + // XXX write isValidDn and call it on the dn string here? + errorValue = "invalidPortNumber"; + } else if (results && hasCharacters(results)) { + errorValue = "invalidResults"; + } + + if (!errorValue) { + if (!port) { + port = secure.checked ? kDefaultSecureLDAPPort : kDefaultLDAPPort; + } + if (hostname.includes(":")) { + // Wrap IPv6 address in []. + hostname = `[${hostname}]`; + } + let ldapUrl = Services.io + .newURI(`${secure.checked ? "ldaps" : "ldap"}://${hostname}:${port}`) + .QueryInterface(Ci.nsILDAPURL); + + ldapUrl.dn = document.getElementById("basedn").value; + ldapUrl.scope = document.getElementById("one").selected + ? Ci.nsILDAPURL.SCOPE_ONELEVEL + : Ci.nsILDAPURL.SCOPE_SUBTREE; + + ldapUrl.filter = document.getElementById("search").value; + if (document.getElementById("GSSAPI").selected) { + saslMechanism = "GSSAPI"; + } + + // check if we are modifying an existing directory or adding a new directory + if (gCurrentDirectory) { + gCurrentDirectory.dirName = description; + gCurrentDirectory.lDAPURL = ldapUrl; + window.opener.gNewServerString = gCurrentDirectory.dirPrefId; + } else { + // adding a new directory + window.opener.gNewServerString = MailServices.ab.newAddressBook( + description, + ldapUrl.spec, + Ci.nsIAbManager.LDAP_DIRECTORY_TYPE + ); + } + + // XXX This is really annoying - both new/modify Address Book don't + // give us back the new directory we just created - so go find it from + // rdf so we can set a few final things up on it. + var targetURI = "moz-abldapdirectory://" + window.opener.gNewServerString; + var theDirectory = MailServices.ab + .getDirectory(targetURI) + .QueryInterface(Ci.nsIAbLDAPDirectory); + + theDirectory.maxHits = results; + theDirectory.authDn = document.getElementById("login").value; + theDirectory.saslMechanism = saslMechanism; + + window.opener.gNewServer = description; + // set window.opener.gUpdate to true so that LDAP Directory Servers + // dialog gets updated + window.opener.gUpdate = true; + window.arguments[0].newDirectoryUID = theDirectory.UID; + if ("onNewDirectory" in window.arguments[0]) { + window.arguments[0].onNewDirectory(theDirectory); + } + } else { + let addressBookBundle = document.getElementById("bundle_addressBook"); + + let errorText; + if (errorArg) { + errorText = addressBookBundle.getFormattedString(errorValue, [ + errorArg, + ]); + } else { + errorText = addressBookBundle.getString(errorValue); + } + + Services.prompt.alert(window, document.title, errorText); + event.preventDefault(); + return; + } + } catch (outer) { + console.error( + "Internal error in pref-directory-add.js:onAccept() " + outer + ); + } +} + +function onCancel() { + window.opener.gUpdate = false; +} + +// Sets the download button state for offline or online. +// This function should only be called for ldap edit dialogs. +function setDownloadOfflineOnlineState(isOffline) { + if (isOffline) { + // Disable the download button and add some text indicating why. + document.getElementById("downloadWarningMsg").textContent = document + .getElementById("bundle_addressBook") + .getString("abReplicationOfflineWarning"); + } + document.getElementById("downloadWarningMsg").hidden = !isOffline; + document.getElementById("download").disabled = isOffline; +} diff --git a/comm/mailnews/addrbook/prefs/content/pref-directory-add.xhtml b/comm/mailnews/addrbook/prefs/content/pref-directory-add.xhtml new file mode 100644 index 0000000000..ff2e40df1e --- /dev/null +++ b/comm/mailnews/addrbook/prefs/content/pref-directory-add.xhtml @@ -0,0 +1,190 @@ +<?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/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/pref-directory-add.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + scrolling="false"> +<head> + <title><!-- directoryTitleEdit --></title> + <style> + #directoryTabPanels radiogroup { + margin-inline-start: 4px; + } + #directoryTabPanels textarea { + width: calc(100% - 22px); + } + #directoryTabPanels menulist { + width: calc(100% - 4px); + margin-inline-start: 4px; + } + </style> + <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script> + <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script> + <script defer="defer" src="chrome://messenger/content/dialogShadowDom.js"></script> + <script defer="defer" src="chrome://messenger/content/addressbook/pref-directory-add.js"></script> +</head> +<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<dialog id="addDirectory" buttons="accept,cancel" style="width:100vw; min-width:&newDirectoryWidth;"> + <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/> + <stringbundle id="bundle_replication" src="chrome://messenger/locale/addressbook/replicationProgress.properties"/> + + <vbox id="editDirectory"> + + <tabbox style="margin:5px"> + <tabs id="directoryTabBox"> + <tab label="&General.tab;"/> + <tab label="&Offline.tab;"/> + <tab label="&Advanced.tab;"/> + </tabs> + + <tabpanels id="directoryTabPanels" flex="1"> + <vbox> + <div xmlns="http://www.w3.org/1999/xhtml" class="grid-three-column"> + <div class="flex-items-center"> + <xul:label id="descriptionLabel" value="&directoryName.label;" + accesskey="&directoryName.accesskey;" + control="description"/> + </div> + <div> + <input id="description" type="text" class="input-inline" + aria-labelledby="descriptionLabel"/> + </div> + <div></div> + <div class="flex-items-center"> + <xul:label id="hostnameLabel" + value="&directoryHostname.label;" + accesskey="&directoryHostname.accesskey;" + control="hostname"/> + </div> + <div> + <input id="hostname" type="text" + class="uri-element input-inline" + aria-labelledby="descriptionLabel" + disableiflocked="true"/> + </div> + <div></div> + <div class="flex-items-center"> + <xul:label id="basednLabel" + value="&directoryBaseDN.label;" + accesskey="&directoryBaseDN.accesskey;" + control="basedn"/> + </div> + <div> + <input id="basedn" type="text" + class="uri-element input-inline" + aria-labelledby="basednLabel" + disableiflocked="true"/> + </div> + <div class="flex-items-center flex-content-center"> + <xul:button label="&findButton.label;" + accesskey="&findButton.accesskey;" disabled="true"/> + </div> + <div class="flex-items-center"> + <xul:label id="portLabel" value="&portNumber.label;" + accesskey="&portNumber.accesskey;" + control="port"/> + </div> + <div> + <input id="port" type="number" + class="size5 input-inline" + min="1" max="65535" + aria-labelledby="portLabel" + disableiflocked="true"/> + </div> + <div></div> + <div class="flex-items-center"> + <xul:label id="loginLabel" value="&directoryLogin.label;" + accesskey="&directoryLogin.accesskey;" + control="login"/> + </div> + <div> + <input id="login" type="text" class="uri-element input-inline" + aria-labelledby="loginLabel"/> + </div> + <div></div> + </div> + <separator/> + <checkbox id="secure" label="&directorySecure.label;" + accesskey="&directorySecure.accesskey;" + oncommand="onSecure();" disableiflocked="true"/> + </vbox> + <vbox> + <description>&offlineText.label;</description> + <separator/> + <hbox> + <button id="download" oncommand="DownloadNow();"/> + <spacer flex="1"/> + </hbox> + <description id="downloadWarningMsg" hidden="true" class="error"/> + <description id="replicationProgressText" hidden="true"/> + + <html:progress id="replicationProgressMeter" value="0" max="100" hidden="hidden"/> + </vbox> + <vbox> + <div xmlns="http://www.w3.org/1999/xhtml" class="grid-two-column"> + <div class="flex-items-center"> + <xul:label id="returnMaxLabel" value="&return.label;" + accesskey="&return.accesskey;" + control="results"/> + </div> + <div class="flex-items-center"> + <input id="results" type="number" + class="size5 input-inline" + min="1" max="2147483647" value="100" + aria-labelledby="returnMaxLabel"/> + <xul:label value="&results.label;"/> + </div> + <div class="flex-items-center"> + <xul:label value="&scope.label;" control="scope" + accesskey="&scope.accesskey;"/> + </div> + <div> + <xul:radiogroup id="scope" + orient="horizontal"> + <xul:radio id="one" value="1" label="&scopeOneLevel.label;" + disableiflocked="true" accesskey="&scopeOneLevel.accesskey;"/> + <xul:radio id="sub" value="2" label="&scopeSubtree.label;" + disableiflocked="true" accesskey="&scopeSubtree.accesskey;"/> + </xul:radiogroup> + </div> + <div class="flex-items-center"> + <xul:label value="&searchFilter.label;" + accesskey="&searchFilter.accesskey;" + control="search"/> + </div> + <div> + <textarea id="search" disableiflocked="true"></textarea> + </div> + <div class="flex-items-center"> + <xul:label value="&saslMechanism.label;" control="saslMechanism" + accesskey="&saslMechanism.accesskey;"/> + </div> + <div> + <xul:menulist id="saslMechanism"> + <xul:menupopup> + <xul:menuitem id="Simple" value="" label="&saslOff.label;" + accesskey="&saslOff.accesskey;"/> + <xul:menuitem id="GSSAPI" value="GSSAPI" label="&saslGSSAPI.label;" + accesskey="&saslGSSAPI.accesskey;"/> + </xul:menupopup> + </xul:menulist> + </div> + </div> + </vbox> + </tabpanels> + </tabbox> + </vbox> +</dialog> +</html:body> +</html> diff --git a/comm/mailnews/addrbook/prefs/content/pref-editdirectories.js b/comm/mailnews/addrbook/prefs/content/pref-editdirectories.js new file mode 100644 index 0000000000..2ba3421c5a --- /dev/null +++ b/comm/mailnews/addrbook/prefs/content/pref-editdirectories.js @@ -0,0 +1,188 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 ../../../../mail/components/addrbook/content/abCommon.js */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +window.addEventListener("DOMContentLoaded", onInitEditDirectories); + +// Listener to refresh the list items if something changes. In all these +// cases we just rebuild the list as it is easier than searching/adding in the +// correct places an would be an infrequent operation. +var gAddressBookAbListener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + init() { + for (let topic of [ + "addrbook-directory-created", + "addrbook-directory-updated", + "addrbook-directory-deleted", + ]) { + Services.obs.addObserver(this, topic, true); + } + }, + + observe(subject, topic, data) { + subject.QueryInterface(Ci.nsIAbDirectory); + fillDirectoryList(subject); + }, +}; + +function onInitEditDirectories() { + // If the pref is locked disable the "Add" button + if (Services.prefs.prefIsLocked("ldap_2.disable_button_add")) { + document.getElementById("addButton").setAttribute("disabled", true); + } + + // Fill out the directory list + fillDirectoryList(); + + // Add a listener so we can update correctly if the list should change + gAddressBookAbListener.init(); +} + +function fillDirectoryList(aItem = null) { + var abList = document.getElementById("directoriesList"); + + // Empty out anything in the list + while (abList.hasChildNodes()) { + abList.lastChild.remove(); + } + + // Init the address book list + let holdingArray = []; + for (let ab of MailServices.ab.directories) { + if (ab.isRemote) { + holdingArray.push(ab); + } + } + + holdingArray.sort(function (a, b) { + return a.dirName.localeCompare(b.dirName); + }); + + holdingArray.forEach(function (ab) { + let item = document.createXULElement("richlistitem"); + let label = document.createXULElement("label"); + label.setAttribute("value", ab.dirName); + item.appendChild(label); + item.setAttribute("value", ab.URI); + + abList.appendChild(item); + }); + + // Forces the focus back on the list and on the first item. + // We also select an edited or recently added item. + abList.focus(); + if (aItem) { + abList.selectedIndex = holdingArray.findIndex(d => { + return d && d.URI == aItem.URI; + }); + } +} + +function selectDirectory() { + var abList = document.getElementById("directoriesList"); + var editButton = document.getElementById("editButton"); + var removeButton = document.getElementById("removeButton"); + + if (abList && abList.selectedItem) { + editButton.removeAttribute("disabled"); + + // If the disable delete button pref for the selected directory is set, + // disable the delete button for that directory. + let ab = MailServices.ab.getDirectory(abList.value); + let disable = Services.prefs.getBoolPref( + ab.dirPrefId + ".disable_delete", + false + ); + if (disable) { + removeButton.setAttribute("disabled", true); + } else { + removeButton.removeAttribute("disabled"); + } + } else { + editButton.setAttribute("disabled", true); + removeButton.setAttribute("disabled", true); + } +} + +function dblClickDirectory(event) { + // We only care about left click events. + if (event.button != 0) { + return; + } + + editDirectory(); +} + +function addDirectory() { + parent.gSubDialog.open( + "chrome://messenger/content/addressbook/pref-directory-add.xhtml", + { features: "resizable=no" } + ); +} + +function editDirectory() { + var abList = document.getElementById("directoriesList"); + + if (abList && abList.selectedItem) { + let abURI = abList.value; + let ab = MailServices.ab.getDirectory(abURI); + + parent.gSubDialog.open( + "chrome://messenger/content/addressbook/pref-directory-add.xhtml", + { features: "resizable=no" }, + { selectedDirectory: ab } + ); + } +} + +async function removeDirectory() { + let abList = document.getElementById("directoriesList"); + + if (!abList.selectedItem) { + return; + } + + let directory = GetDirectoryFromURI(abList.value); + if ( + !directory || + ["ldap_2.servers.history", "ldap_2.servers.pab"].includes( + directory.dirPrefId + ) + ) { + return; + } + + let action = "delete-book"; + if (directory.isMailList) { + action = "delete-lists"; + } else if ( + [ + Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE, + Ci.nsIAbManager.LDAP_DIRECTORY_TYPE, + ].includes(directory.dirType) + ) { + action = "remove-remote-book"; + } + + let [title, message] = await document.l10n.formatValues([ + { id: `about-addressbook-confirm-${action}-title`, args: { count: 1 } }, + { + id: `about-addressbook-confirm-${action}`, + args: { name: directory.dirName, count: 1 }, + }, + ]); + + if (Services.prompt.confirm(window, title, message)) { + MailServices.ab.deleteAddressBook(directory.URI); + } +} diff --git a/comm/mailnews/addrbook/prefs/content/pref-editdirectories.xhtml b/comm/mailnews/addrbook/prefs/content/pref-editdirectories.xhtml new file mode 100644 index 0000000000..381aee3050 --- /dev/null +++ b/comm/mailnews/addrbook/prefs/content/pref-editdirectories.xhtml @@ -0,0 +1,77 @@ +<?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/messenger.css" type="text/css"?> + +<!DOCTYPE html SYSTEM "chrome://messenger/locale/addressbook/pref-directory.dtd"> + +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + scrolling="false" +> + <head> + <title>&pref.ldap.window.title;</title> + <link + rel="localization" + href="messenger/addressbook/aboutAddressBook.ftl" + /> + <script + defer="defer" + src="chrome://messenger/content/addressbook/abCommon.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/addressbook/pref-editdirectories.js" + ></script> + </head> + <html:body + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + > + <dialog id="editDirectories" buttons="accept"> + <stringbundle + id="bundle_addressBook" + src="chrome://messenger/locale/addressbook/addressBook.properties" + /> + + <label + value="&directoriesText.label;" + accesskey="&directoriesText.accesskey;" + control="directoriesList" + /> + <hbox flex="1"> + <richlistbox + id="directoriesList" + flex="1" + onselect="selectDirectory();" + ondblclick="dblClickDirectory(event);" + /> + <vbox> + <button + id="addButton" + label="&addDirectory.label;" + accesskey="&addDirectory.accesskey;" + oncommand="addDirectory();" + /> + <button + id="editButton" + label="&editDirectory.label;" + accesskey="&editDirectory.accesskey;" + disabled="true" + oncommand="editDirectory();" + /> + <button + id="removeButton" + label="&deleteDirectory.label;" + accesskey="&deleteDirectory.accesskey;" + disabled="true" + oncommand="removeDirectory();" + /> + </vbox> + </hbox> + </dialog> + </html:body> +</html> diff --git a/comm/mailnews/addrbook/public/moz.build b/comm/mailnews/addrbook/public/moz.build new file mode 100644 index 0000000000..ece3c8d37b --- /dev/null +++ b/comm/mailnews/addrbook/public/moz.build @@ -0,0 +1,48 @@ +# 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/. + +XPIDL_SOURCES += [ + "nsIAbAddressCollector.idl", + "nsIAbAutoCompleteResult.idl", + "nsIAbBooleanExpression.idl", + "nsIAbCard.idl", + "nsIAbDirectory.idl", + "nsIAbDirectoryQuery.idl", + "nsIAbDirectoryQueryProxy.idl", + "nsIAbDirSearchListener.idl", + "nsIAbLDAPAttributeMap.idl", + "nsIAbLDAPDirectory.idl", + "nsIAbLDAPReplicationData.idl", + "nsIAbLDAPReplicationQuery.idl", + "nsIAbLDAPReplicationService.idl", + "nsIAbLDIFService.idl", + "nsIAbManager.idl", + "nsILDAPBERElement.idl", + "nsILDAPBERValue.idl", + "nsILDAPConnection.idl", + "nsILDAPControl.idl", + "nsILDAPErrors.idl", + "nsILDAPMessage.idl", + "nsILDAPMessageListener.idl", + "nsILDAPModification.idl", + "nsILDAPOperation.idl", + "nsILDAPService.idl", + "nsILDAPURL.idl", + "nsIMsgVCardService.idl", +] + +if CONFIG["OS_ARCH"] == "WINNT" and CONFIG["MOZ_MAPI_SUPPORT"]: + XPIDL_SOURCES += [ + "nsIAbOutlookInterface.idl", + ] + +if CONFIG["MOZ_PREF_EXTENSIONS"]: + XPIDL_SOURCES += [ + "nsILDAPSyncQuery.idl", + ] + +XPIDL_MODULE = "addrbook" + +EXPORTS += [] diff --git a/comm/mailnews/addrbook/public/nsIAbAddressCollector.idl b/comm/mailnews/addrbook/public/nsIAbAddressCollector.idl new file mode 100644 index 0000000000..d1c90ca35d --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbAddressCollector.idl @@ -0,0 +1,46 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +/** + * nsIAbAddressCollector is the interface to the address collecter service. + * It will save and update the supplied addresses into the address book + * specified by the "mail.collect_addressbook" pref. + */ +[scriptable, uuid(069d3fba-37d4-4158-b401-a8efaeea0b66)] +interface nsIAbAddressCollector : nsISupports { + /** + * Collects email addresses into the address book. + * If a card already exists for the email, the first/last/display names + * will be updated if they are supplied alongside the address. + * If a card does not exist for the email it will be created if aCreateCard + * is true. + * + * @param aAddresses The list of emails (in standard header format) + * to collect into the address book. + * @param aCreateCard Set to true if a card should be created if the + * email address doesn't exist. + */ + void collectAddress(in AUTF8String aAddresses, in boolean aCreateCard); + + /** + * Collects a single name and email address into the address book. + * By default, it saves the address without checking for an existing one. + * See collectAddress for the general implementation. + * + * @param aEmail The email address to collect. + * @param aDisplayName The display name associated with the email address. + * @param aCreateCard Set to true if a card should be created if the + * email address doesn't exist (ignored if + * aSkipCheckExisting is true). + * @param aSkipCheckExisting Optional parameter, if this is set then the + * implementation will skip checking for an + * existing card, and just create a new card. + */ + void collectSingleAddress(in AUTF8String aEmail, in AUTF8String aDisplayName, + in boolean aCreateCard, + [optional] in boolean aSkipCheckExisting); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbAutoCompleteResult.idl b/comm/mailnews/addrbook/public/nsIAbAutoCompleteResult.idl new file mode 100644 index 0000000000..ceeeeefe67 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbAutoCompleteResult.idl @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsIAutoCompleteResult.idl" + +interface nsIAbCard; +interface nsIAbDirectory; + +/** + * This interface is used to extend the nsIAutoCompleteResult interface to + * provide extra facilities for obtaining more details of the results of + * an address book search. + */ +[scriptable, uuid(c0d35623-f719-4e43-ae24-573e393f87f9)] +interface nsIAbAutoCompleteResult : nsIAutoCompleteResult { + /** + * Get the card from the result at the given index + */ + nsIAbCard getCardAt(in long index); + + /** + * Gets the email to use for the card within the result at the given index. + * This is the email that was matched against for the card where there are + * multiple email addresses on a card. + * + * @param index Index of the autocomplete result to return the value for. + * @result The email address to use from the card. + */ + AString getEmailToUse(in long index); + + /** + * Indicates whether the source that returned this result returned a + * complete result for the query. If true, refining the search will not + * trigger a new query, instead simply filtering the previous results. + * If false, the directory will be present in asyncDirectories. + */ + bool isCompleteResult(in long index); + + /** + * The template used to build the query for this search. Optional. + */ + attribute AString modelQuery; + + /** + * Asynchronous address books that were unable to return full results. + * This means that they need to be required rather than simply filtered. + */ + attribute Array<nsIAbDirectory> asyncDirectories; +}; diff --git a/comm/mailnews/addrbook/public/nsIAbBooleanExpression.idl b/comm/mailnews/addrbook/public/nsIAbBooleanExpression.idl new file mode 100644 index 0000000000..37ab350bbc --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbBooleanExpression.idl @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +typedef long nsAbBooleanConditionType; + +/** + * Condition types + * + * Constants defining the types of condition + * to obtain a boolean result of TRUE or FALSE + * + */ +[scriptable, uuid(F51387B1-5AEF-4A1C-830E-7CD3B02366CE)] +interface nsIAbBooleanConditionTypes : nsISupports +{ + const long Exists = 0; + const long DoesNotExist = 1; + const long Contains = 2; + const long DoesNotContain = 3; + const long Is = 4; + const long IsNot = 5; + const long BeginsWith = 6; + const long EndsWith = 7; + const long LessThan = 8; + const long GreaterThan = 9; + const long SoundsLike = 10; + const long RegExp = 11; +}; + + +typedef long nsAbBooleanOperationType; + +/* + * Operation types + * + * Constants defining the boolean operation that + * should be performed between two boolean expressions + * + */ +[scriptable, uuid(9bdd2e51-2be4-49a4-a558-36d1a812231a)] +interface nsIAbBooleanOperationTypes : nsISupports +{ + const long AND = 0; + const long OR = 1; + const long NOT = 2; +}; + + +/** + * String condition + * + * A string condition represents a leaf node in a + * boolean expression tree and represents + * test which will return TRUE or FALSE + * + * Condition is an expression which is a + * leaf node in a boolean expression tree + * + */ +[scriptable, uuid(C3869D72-CFD0-45F0-A0EC-3F67D83C7110)] +interface nsIAbBooleanConditionString : nsISupports +{ + /** + * The condition for how the a value + * should be compared + * + */ + attribute nsAbBooleanConditionType condition; + + /** + * The lhs of the condition + * + * Represents a property name which + * should be evaluated to obtain the + * lhs. + * + */ + attribute string name; + + /** + * The rhs of the condition + * + * <name> [condition] value + * + */ + attribute wstring value; +}; + +/** + * N Boolean expression type + * + * Supports Unary Binary and N boolean expressions + * + * An operation represents a node in a boolean + * expression tree which may contain one or more + * child conditions or expressions + * + */ +[scriptable, uuid(223a9462-1aeb-4c1f-b069-5fc6278989b2)] +interface nsIAbBooleanExpression: nsISupports +{ + /** + * The boolean operation to be applied to + * results of all evaluated expressions + * + */ + attribute nsAbBooleanOperationType operation; + + /** + * List of peer expressions + * + * e1 [op] e2 [op] .... en + * + */ + attribute Array<nsISupports> expressions; +}; diff --git a/comm/mailnews/addrbook/public/nsIAbCard.idl b/comm/mailnews/addrbook/public/nsIAbCard.idl new file mode 100644 index 0000000000..69acf7fa95 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbCard.idl @@ -0,0 +1,402 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsIProperty; +interface nsIStringBundle; +interface nsIVariant; + +[scriptable, uuid(97448252-F189-11d4-A422-001083003D0C)] +interface nsIAbPreferMailFormat : nsISupports { + const unsigned long unknown = 0; + const unsigned long plaintext = 1; + const unsigned long html = 2; +}; + +/** + * An interface representing an address book card. + * + * None of these IDs will be reflected in the property collection. Neither + * nsIAbCard::properties, nsIAbCard::deleteProperty, nor any of the property + * getters and setters are able to interact with these properties. + * + * Fundamentally, a card is a collection of properties. Modifying a property in + * some way on a card does not change the backend used to store the card; the + * directory is required to do make the changes here. + * + * The following are the core properties that are used: + * - Names: + * - FirstName, LastName + * - PhoneticFirstName, PhoneticLastName + * - DisplayName, NickName + * - SpouseName, FamilyName + * - PrimaryEmail, SecondEmail + * - Home Contact: + * - HomeAddress, HomeAddress2, HomeCity, HomeState, HomeZipCode, HomeCountry + * - HomePhone, HomePhoneType + * - Work contact. Same as home, but with `Work' instead of `Home' + * - Other Contact: + * - FaxNumber, FaxNumberType + * - PagerNumber, PagerNumberType + * - CellularNumber, CellularNumberType + * - JobTitle, Department, Company + * - _AimScreenName + * - Dates: + * - AnniversaryYear, AnniversaryMonth, AnniversaryDay + * - BirthYear, BirthMonth, BirthDay + * - WebPage1 (work), WebPage2 (home) + * - Custom1, Custom2, Custom3, Custom4 + * - Notes + * - Integral properties: + * - LastModifiedDate + * - PopularityIndex + * - Photo properties: + * - PhotoName + * - PhotoType + * - PhotoURI + * + * The contract id for the standard implementation is + * <tt>\@mozilla.org/addressbook/cardproperty;1</tt>. + */ +[scriptable, uuid(9bddf024-5178-4097-894e-d84b4ddde101)] +interface nsIAbCard : nsISupports { + /** + * @{ + * These constants reflect the possible values of the + * mail.addr_book.lastnamefirst preferences. They are intended to be used in + * generateName, defined below. + */ + const unsigned long GENERATE_DISPLAY_NAME = 0; + const unsigned long GENERATE_LAST_FIRST_ORDER = 1; + const unsigned long GENERATE_FIRST_LAST_ORDER = 2; + /** @} */ + + /** + * Generate a name from the item for display purposes. + * + * If this item is an nsIAbCard, then it will use the aGenerateFormat option + * to determine the string to return. + * If this item is not an nsIAbCard, then the aGenerateFormat option may be + * ignored, and the displayName of the item returned. + * + * @param aGenerateFormat The format to generate as per the GENERATE_* + * constants above. + * @param aBundle An optional parameter that is a pointer to a string + * bundle that holds: + * chrome://messenger/locale/addressbook/addressBook.properties + * If this bundle is not supplied, then the function + * will obtain the bundle itself. If cached by the + * caller and supplied to this function, then + * performance will be improved over many calls. + * @return A string containing the generated name. + */ + AString generateName(in long aGenerateFormat, + [optional] in nsIStringBundle aBundle); + + /** + * The UID for the nsIAbDirectory containing this card. + * + * The directory considered to contain this card is the directory which + * produced this card (e.g., through nsIAbDirectory::getCardForProperty) or + * the last directory to modify this card, if another directory did so. If the + * last directory to modify this card deleted it, then this card is considered + * unassociated. + * + * If this card is not associated with a directory, this string will be empty. + * + * There is no standardized way to associate a card with multiple directories. + * + * Consumers of this interface outside of directory implementations SHOULD + * NOT, in general, modify this property. + */ + attribute AUTF8String directoryUID; + + /** + * A 128-bit unique identifier for this card. This can only be set if it is not + * already set. The getter sets a value if there is not one. + */ + attribute AUTF8String UID; + + /** + * A list of all the properties that this card has as an enumerator, whose + * members are all nsIProperty objects. + */ + readonly attribute Array<nsIProperty> properties; + + /** + * Returns a property for the given name. + * + * @param name The case-sensitive name of the property to get. + * @param defaultValue The value to return if the property does not exist. + * @exception NS_ERROR_NOT_AVAILABLE if the named property does not exist. + * @exception NS_ERROR_CANNOT_CONVERT_DATA if the property cannot be converted + * to the desired type. + */ + nsIVariant getProperty(in AUTF8String name, in nsIVariant defaultValue); + /** + * @{ + * Returns a property for the given name. Javascript callers should NOT use these, + * but use getProperty instead. XPConnect will do the type conversion automagically. + * + * These functions convert values in the same manner as the default + * implementation of nsIVariant. Of particular note is that boolean variables + * are converted to integers as in C/C++ (true is a non-zero value), so that + * false will be converted to a string of "0" and not "false." + * + * + * @param name The case-sensitive name of the property to get. + * @exception NS_ERROR_NOT_AVAILABLE if the named property does not exist. + * @exception NS_ERROR_CANNOT_CONVERT_DATA if the property cannot be converted + * to the desired type. + */ + AString getPropertyAsAString(in string name); + AUTF8String getPropertyAsAUTF8String(in string name); + unsigned long getPropertyAsUint32(in string name); + + /** + * Returns a property for the given name. + * + * @param name The case-sensitive name of the property to get. + * @param defaultValue The value to return if the property does not exist. + */ + boolean getPropertyAsBool(in string name, in boolean defaultValue); + + /** @} */ + + /** + * Assigns the given to value to the property of the given name. + * + * Should the property exist, its value will be overwritten. An + * implementation may impose additional semantic constraints for certain + * properties. However, such constraints might not be checked by this method. + * + * @warning A value MUST be convertible to a string; if this convention is not + * followed, consumers of cards may fail unpredictably or return incorrect + * results. + * + * @param name The case-sensitive name of the property to set. + * @param value The new value of the property. + */ + void setProperty(in AUTF8String name, in nsIVariant value); + + /** + * @{ + * Sets a property for the given name. Javascript callers should NOT use these, + * but use setProperty instead. XPConnect will do the type conversion automagically. + * + * These functions convert values in the same manner as the default + * implementation of nsIVariant. + */ + void setPropertyAsAString(in string name, in AString value); + void setPropertyAsAUTF8String(in string name, in AUTF8String value); + void setPropertyAsUint32(in string name, in unsigned long value); + void setPropertyAsBool(in string name, in boolean value); + + /** @} */ + + /** + * Deletes the property with the given name. + * + * Some properties may not be deleted. However, the implementation will not + * check this constraint at this method. If such a property is deleted, an + * error may be thrown when the card is modified at the database level. + * + * @param name The case-sensitive name of the property to set. + */ + void deleteProperty(in AUTF8String name); + + /** + * Whether this card supports vCard properties. Currently only AddrBookCard + * supports vCard properties. + */ + readonly attribute boolean supportsVCard; + + /** + * A `VCardProperties` object for this card, or null. If `supportsVCard` is + * true, this attribute MUST be a `VCardProperties` object, otherwise it + * MUST be null. + * + * @see VCardProperties in VCardUtils.jsm + */ + readonly attribute jsval vCardProperties; + + /** + * @{ + * These properties are shorthand for getProperty and setProperty. + */ + attribute AString firstName; + attribute AString lastName; + attribute AString displayName; + attribute AString primaryEmail; + /** @} */ + + /** + * All email addresses associated with this card, in order of preference. + */ + readonly attribute Array<AString> emailAddresses; + + /** + * Determines whether or not a card has the supplied email address in either + * of its PrimaryEmail or SecondEmail attributes. + * + * Note: This function is likely to be temporary whilst we work out proper + * APIs for multi-valued attributes in bug 118665. + * + * @param aEmailAddress The email address to attempt to match against. + * @return True if aEmailAddress matches any of the email + * addresses stored in the card. + */ + boolean hasEmailAddress(in AUTF8String aEmailAddress); + + /** + * A URL to a photo for this card, or an empty string if there isn't one. + * This is probably a file: or data: URL but other schemes are possible. + */ + readonly attribute AString photoURL; + + /** + * Translates a card into a specific format. + * The following types are supported: + * - base64xml + * - xml + * - vcard + * + * @param aType The type of item to translate the card into. + * @return A string containing the translated card. + * @exception NS_ERROR_ILLEGAL_VALUE if we do not recognize the type. + */ + AUTF8String translateTo(in AUTF8String aType); + + /** + * Translates a card from the specified format + */ + //void translateFrom(in AUTF8String aType, in AUTF8String aData); + + /** + * Generate a phonetic name from the card, using the firstName and lastName + * values. + * + * @param aLastNameFirst Set to True to put the last name before the first. + * @return A string containing the generated phonetic name. + */ + AString generatePhoneticName(in boolean aLastNameFirst); + + /** + * Generate a chat name from the card, containing the value of the + * first non-empty chat field. + * + * @return A string containing the generated chat name. + */ + AString generateChatName(); + + /** + * This function will copy all values from one card to another. + * + * @param srcCard The source card to copy values from. + */ + void copy(in nsIAbCard aSrcCard); + + /** + * Returns true if this card is equal to the other card. + * + * The default implementation defines equal as this card pointing to the + * same object as @arg aCard; another implementation defines it as equality of + * properties and values. + * + * @warning The exact nature of equality is still undefined, and actual + * results may not match theoretical results. Most notably, the code + * <tt>a.equals(b) == b.equals(a)</tt> might not return true. In + * particular, calling equals on cards from different address books + * may return inaccurate results. + * + * + * @return Equality, as defined above. + * @param aCard The card to compare against. + */ + boolean equals(in nsIAbCard aCard); + + // PROPERTIES TO BE DELETED AS PART OF REWRITE + + attribute boolean isMailList; + /** + * If isMailList is true then mailListURI + * will contain the URI of the associated + * mail list + */ + attribute string mailListURI; +}; + +%{C++ +// A nice list of properties for the benefit of C++ clients +#define kUIDProperty "UID" +#define kFirstNameProperty "FirstName" +#define kLastNameProperty "LastName" +#define kDisplayNameProperty "DisplayName" +#define kNicknameProperty "NickName" +#define kPriEmailProperty "PrimaryEmail" +#define kLastModifiedDateProperty "LastModifiedDate" +#define kPopularityIndexProperty "PopularityIndex" + +#define kPhoneticFirstNameProperty "PhoneticFirstName" +#define kPhoneticLastNameProperty "PhoneticLastName" +#define kSpouseNameProperty "SpouseName" +#define kFamilyNameProperty "FamilyName" +#define k2ndEmailProperty "SecondEmail" + +#define kHomeAddressProperty "HomeAddress" +#define kHomeAddress2Property "HomeAddress2" +#define kHomeCityProperty "HomeCity" +#define kHomeStateProperty "HomeState" +#define kHomeZipCodeProperty "HomeZipCode" +#define kHomeCountryProperty "HomeCountry" +#define kHomeWebPageProperty "WebPage2" + +#define kWorkAddressProperty "WorkAddress" +#define kWorkAddress2Property "WorkAddress2" +#define kWorkCityProperty "WorkCity" +#define kWorkStateProperty "WorkState" +#define kWorkZipCodeProperty "WorkZipCode" +#define kWorkCountryProperty "WorkCountry" +#define kWorkWebPageProperty "WebPage1" + +#define kHomePhoneProperty "HomePhone" +#define kHomePhoneTypeProperty "HomePhoneType" +#define kWorkPhoneProperty "WorkPhone" +#define kWorkPhoneTypeProperty "WorkPhoneType" +#define kFaxProperty "FaxNumber" +#define kFaxTypeProperty "FaxNumberType" +#define kPagerTypeProperty "PagerNumberType" +#define kPagerProperty "PagerNumber" +#define kCellularProperty "CellularNumber" +#define kCellularTypeProperty "CellularNumberType" + +#define kJobTitleProperty "JobTitle" +#define kDepartmentProperty "Department" +#define kCompanyProperty "Company" +#define kScreenNameProperty "_AimScreenName" +#define kCustom1Property "Custom1" +#define kCustom2Property "Custom2" +#define kCustom3Property "Custom3" +#define kCustom4Property "Custom4" +#define kNotesProperty "Notes" + +#define kGtalkProperty "_GoogleTalk" +#define kAIMProperty "_AimScreenName" +#define kYahooProperty "_Yahoo" +#define kSkypeProperty "_Skype" +#define kQQProperty "_QQ" +#define kMSNProperty "_MSN" +#define kICQProperty "_ICQ" +#define kXMPPProperty "_JabberId" +#define kIRCProperty "_IRC" + +#define kAnniversaryYearProperty "AnniversaryYear" +#define kAnniversaryMonthProperty "AnniversaryMonth" +#define kAnniversaryDayProperty "AnniversaryDay" +#define kBirthYearProperty "BirthYear" +#define kBirthMonthProperty "BirthMonth" +#define kBirthDayProperty "BirthDay" +%} diff --git a/comm/mailnews/addrbook/public/nsIAbDirSearchListener.idl b/comm/mailnews/addrbook/public/nsIAbDirSearchListener.idl new file mode 100644 index 0000000000..cc78f1137f --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbDirSearchListener.idl @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsIAbCard; +interface nsITransportSecurityInfo; + +/** + * Listener callbacks for addressbook nsIAbDirectory searches and queries. + */ +[scriptable, uuid(eafe2488-4efb-4ac8-a6b4-7756eb1650a3)] +interface nsIAbDirSearchListener : nsISupports { + /** + * Invoked for each matching result found by the search. + * + * @param aCard A matching addressbook card. + */ + void onSearchFoundCard(in nsIAbCard aCard); + + /** + * Invoked when the search finishes. + * + * @param status The result of the search. NS_OK means the search + * completed without error. NS_ERROR_ABORT means the user + * stopped the search. But there are many other error codes + * which may be seen here (LDAP or NSS errors, for example). + * @param complete Whether this search returned all possible results. + * @param secInfo If status is an NSS error code, the securityInfo of the + * failing operation is passed out here. This can be used + * to obtain a failing certificate, to present the user an + * option to add it as a security exception (handy for + * LDAP servers with self-signed certs). + * @param location If status is an NSS error code, this holds the location + * of the failed operation ("<host>:<port>"). + */ + void onSearchFinished(in nsresult status, in bool complete, in nsITransportSecurityInfo secInfo, in ACString location); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbDirectory.idl b/comm/mailnews/addrbook/public/nsIAbDirectory.idl new file mode 100644 index 0000000000..07eab7aa81 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbDirectory.idl @@ -0,0 +1,371 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsIAbCard; +interface nsIAbDirSearchListener; + +/* moz-abdirectory:// is the URI to access nsAbBSDirectory, + * which is the root directory for all types of address books + * this is used to get all address book directories. */ + +%{C++ +#define kAllDirectoryRoot "moz-abdirectory://" + +#define kPersonalAddressbook "abook.sqlite" +#define kPersonalAddressbookUri "jsaddrbook://abook.sqlite" +#define kCollectedAddressbook "history.sqlite" +#define kCollectedAddressbookUri "jsaddrbook://history.sqlite" + +#define kABFileName_PreviousSuffix ".na2" /* final v2 address book format */ +#define kABFileName_PreviousSuffixLen 4 +#define kABFileName_CurrentSuffix ".mab" /* v3 address book extension */ + +#define kJSDirectoryRoot "jsaddrbook://" +#define kJSAddressBook "abook.sqlite" +%} + +/** + * A top-level address book directory. + * + * Please note that in order to be properly instantiated by nsIAbManager, every + * type of nsIAbDirectory must have a contract ID of the form: + * + * @mozilla.org/addressbook/directory;1?type=<AB URI Scheme> + * + * Where AB URI Scheme does not include the ://. For example, for the + * SQLite-based address book, the scheme is "jsaddrbook", so the contract ID for + * the SQLite-based address book type is: + * + * @mozilla.org/addressbook/directory;1?type=jsaddrbook + */ +[scriptable, uuid(72dc868b-db5b-4daa-b6c6-071be4a05d02)] +interface nsIAbDirectory : nsISupports { + /** + * Returns true if this collection is read-only. + */ + readonly attribute boolean readOnly; + + /** + * Returns true if this collection is accessed over a network connection. + */ + readonly attribute boolean isRemote; + + /** + * Returns true if this collection is accessed over a secure connection. + * + * If isRemote returns false, then this value MUST be false as well. + */ + readonly attribute boolean isSecure; + + /** + * Returns an address book card for the specified email address if found. + * + * If there are multiple cards with the given email address, this method will + * return one of these cards in an implementation-defined manner. + * + * Matching is performed in a case-insensitive manner. + * + * This method performs a synchronous operation. If the collection cannot do + * the search in such a manner, then it should throw NS_ERROR_NOT_IMPLEMENTED. + * + * @param emailAddress The email address to find in any of the email address + * fields. If emailAddress is empty, the database won't + * be searched and the function will return as if no card + * was found. + * @return An nsIAbCard if one was found, else returns NULL. + * @exception NS_ERROR_NOT_IMPLEMENTED If the collection cannot do this. + */ + nsIAbCard cardForEmailAddress(in AUTF8String emailAddress); + + /** + * Returns an address book card for the specified property if found. + * + * If there are multiple cards with the given value for the property, this + * method will return one of these cards in an implementation-defined manner. + * + * This method performs a synchronous operation. If the collection cannot do + * the search in such a manner, then it should throw NS_ERROR_NOT_IMPLEMENTED. + * + * If the property is not natively a string, it can still be searched for + * using the string-encoded value of the property, e.g. "0". See + * nsIAbCard::getPropertyAsAUTF8String for more information. Empty values will + * return no match, to prevent spurious results. + * + * @param aProperty The property to look for. + * @param aValue The value to search for. + * @param aCaseSensitive True if matching should be done case-sensitively. + * @result An nsIAbCard if one was found, else returns NULL. + * @exception NS_ERROR_NOT_IMPLEMENTED If the collection cannot do this. + */ + nsIAbCard getCardFromProperty(in string aProperty, in AUTF8String aValue, + in boolean aCaseSensitive); + + /** + * Returns all address book cards with a specific property matching value + * + * This function is almost identical to getCardFromProperty, with the + * exception of returning all cards rather than just the first. + * + * @param aProperty The property to look for. + * @param aValue The value to search for. + * @param aCaseSensitive True if matching should be done case-sensitively. + * @result The matching nsIAbCard instances. + */ + Array<nsIAbCard> getCardsFromProperty(in string aProperty, + in AUTF8String aValue, + in boolean aCaseSensitive); + + /** + * Returns the nsIAbDirectory for a mailing list with the specified name. + */ + nsIAbDirectory getMailListFromName(in AString aName); + + /** + * The chrome URI to use for bringing up a dialog to edit this directory. + * When opening the dialog, use a JS argument of + * {selectedDirectory: thisdir} where thisdir is this directory that you just + * got the chrome URI from. + */ + readonly attribute ACString propertiesChromeURI; + + /** + * The description of the directory. If this directory is not a mailing list, + * then setting this attribute will send round a "DirName" update via + * nsIAddrBookSession. + */ + attribute AString dirName; + + // XXX This should really be replaced by a QI or something better + readonly attribute long dirType; + + // The filename for address books within this directory. + readonly attribute ACString fileName; + + /** + * A 128-bit unique identifier for this directory. + */ + readonly attribute AUTF8String UID; + [noscript] void setUID(in AUTF8String aUID); + + // The URI of the address book + readonly attribute ACString URI; + + // The position of the directory on the display. + readonly attribute long position; + + // will be used for LDAP replication + attribute unsigned long lastModifiedDate; + + // Defines whether this directory is a mail + // list or not + attribute boolean isMailList; + + // Get the children directories + readonly attribute Array<nsIAbDirectory> childNodes; + + /** + * Get the count of cards associated with the directory. This includes the + * cards associated with the mailing lists too. + */ + readonly attribute unsigned long childCardCount; + + /** + * Get the cards associated with the directory. This will return the cards + * associated with the mailing lists too. + */ + readonly attribute Array<nsIAbCard> childCards; + + /** + * Searches the directory for cards matching query. + * + * The query takes the form: + * (BOOL1(FIELD1,OP1,VALUE1)..(FIELDn,OPn,VALUEn)(BOOL2(FIELD1,OP1,VALUE1)...)...) + * + * BOOLn A boolean operator joining subsequent terms delimited by (). + * For possible values see CreateBooleanExpression(). + * FIELDn An addressbook card data field. + * OPn An operator for the search term. + * For possible values see CreateBooleanConditionString(). + * VALUEn The value to be matched in the FIELDn via the OPn operator. + * The value must be URL encoded by the caller, if it contains any + * special characters including '(' and ')'. + */ + void search(in AString query, in AString searchString, in nsIAbDirSearchListener listener); + + /** + * Initializes a directory, pointing to a particular URI. + */ + void init(in string aURI); + + /** + * Clean up any database connections or open file handles. + * Called at shutdown or if the directory is about to be deleted. + */ + [implicit_jscontext] + Promise cleanUp(); + + // Deletes either a mailing list or a top + // level directory, which also updates the + // preferences + void deleteDirectory(in nsIAbDirectory directory); + + // Check if directory contains card + // If the implementation is asynchronous the card + // may not yet have arrived. If it is in the process + // of obtaining cards the method will throw an + // NS_ERROR_NOT_AVAILABLE exception if the card + // cannot be found. + boolean hasCard(in nsIAbCard cards); + + // Check if directory contains directory + boolean hasDirectory(in nsIAbDirectory dir); + + // Check if directory contains a mailinglist by name + boolean hasMailListWithName(in AString aName); + + /** + * Adds a card to the database. + * + * This card does not need to be of the same type as the database, e.g., one + * can add an nsIAbLDAPCard to an nsIAbMDBDirectory. + * + * @return "Real" card (eg nsIAbLDAPCard) that can be used for some + * extra functions. + */ + nsIAbCard addCard(in nsIAbCard card); + + /** + * Modifies a card in the database to match that supplied. + */ + void modifyCard(in nsIAbCard modifiedCard); + + /** + * Deletes the array of cards from the database. + * + * @param aCards The cards to delete from the database. + */ + void deleteCards(in Array<nsIAbCard> aCards); + + void dropCard(in nsIAbCard card, in boolean needToCopyCard); + + /** + * Whether or not the directory should be searched when doing autocomplete, + * (currently by using GetChildCards); LDAP does not support this in online + * mode, so that should return false; additionally any other directory types + * that also do not support GetChildCards should return false. + * + * @param aIdentity An optional parameter detailing the identity key (see + * nsIMsgAccountManager) that this autocomplete is being + * run against. + * @return True if this directory should/can be used during + * local autocomplete. + */ + boolean useForAutocomplete(in ACString aIdentityKey); + + /** + * Does this directory support mailing lists? Note that in the case + * this directory is a mailing list and nested mailing lists are not + * supported, this will return false rather than true which the parent + * directory might. + */ + readonly attribute boolean supportsMailingLists; + + // Specific to a directory which stores mail lists + + /** + * Creates a new mailing list in the directory. Currently only supported + * for top-level directories. + * + * @param list The new mailing list to add. + * @return The mailing list directory added, which may have been modified. + */ + nsIAbDirectory addMailList(in nsIAbDirectory list); + + /** + * Nick Name of the mailing list. This attribute is only really used when + * the nsIAbDirectory represents a mailing list. + */ + attribute AString listNickName; + + /** + * Description of the mailing list. This attribute is only really used when + * the nsIAbDirectory represents a mailing list. + */ + attribute AString description; + + /** + * Edits an existing mailing list (specified as listCard) into its parent + * directory. You should call this function on the resource with the same + * uri as the listCard. + * + * @param listCard A nsIAbCard version of the mailing list with the new + * values. + */ + void editMailListToDatabase(in nsIAbCard listCard); + + // Copies mail list properties from the srcList + void copyMailList(in nsIAbDirectory srcList); + + /** + * The id of the directory used in prefs e.g. "ldap_2.servers.pab" + */ + readonly attribute ACString dirPrefId; + + /** + * @name getXXXValue + * + * Helper functions to get different types of pref, but return a default + * value if a pref value was not obtained. + * + * @param aName The name of the pref within the branch dirPrefId to + * get a value from. + * + * @param aDefaultValue The default value to return if getting the pref fails + * or the pref is not present. + * + * @return The value of the pref or the default value. + * + * @exception NS_ERROR_NOT_INITIALIZED if the pref branch couldn't + * be obtained (e.g. dirPrefId isn't set). + */ + //@{ + long getIntValue(in string aName, in long aDefaultValue); + boolean getBoolValue(in string aName, in boolean aDefaultValue); + ACString getStringValue(in string aName, in ACString aDefaultValue); + AUTF8String getLocalizedStringValue(in string aName, in AUTF8String aDefaultValue); + //@} + + /** + * The following attributes are read from an nsIAbDirectory via the above methods: + * + * HidesRecipients (Boolean) + * If true, and this nsIAbDirectory is a mailing list, then when sending mail to + * this list, recipients addresses will be hidden from one another by sending + * via BCC. + */ + + /** + * @name setXXXValue + * + * Helper functions to set different types of pref values. + * + * @param aName The name of the pref within the branch dirPrefId to + * get a value from. + * + * @param aValue The value to set the pref to. + * + * @exception NS_ERROR_NOT_INITIALIZED if the pref branch couldn't + * be obtained (e.g. dirPrefId isn't set). + */ + //@{ + void setIntValue(in string aName, in long aValue); + void setBoolValue(in string aName, in boolean aValue); + void setStringValue(in string aName, in ACString aValue); + void setLocalizedStringValue(in string aName, in AUTF8String aValue); + //@} + +}; diff --git a/comm/mailnews/addrbook/public/nsIAbDirectoryQuery.idl b/comm/mailnews/addrbook/public/nsIAbDirectoryQuery.idl new file mode 100644 index 0000000000..e21bca5c05 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbDirectoryQuery.idl @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#include "nsISupports.idl" + +interface nsIAbDirSearchListener; +interface nsIAbCard; +interface nsIAbDirectory; + +/** + * The arguments for a query. + * + * Contains an expression for perform matches + * and an array of properties which should be + * returned if a match is found from the expression + * + */ +[scriptable, uuid(03af3018-2590-4f4c-a88c-1fff6595ef05)] +interface nsIAbDirectoryQueryArguments : nsISupports +{ + /** + * Defines the boolean expression for + * the matching of cards + * + */ + attribute nsISupports expression; + + /** + * Defines if sub directories should be + * queried + * + */ + attribute boolean querySubDirectories; + + /** + * A parameter which can be used to pass in data specific to a particular + * type of addressbook. + */ + attribute nsISupports typeSpecificArg; + + /** + * A custom search filter which user wants to use in LDAP query. + */ + attribute AUTF8String filter; +}; + + +[scriptable, uuid(3A6E0C0C-1DD2-11B2-B23D-EA3A8CCB333C)] +interface nsIAbDirectoryQueryPropertyValue : nsISupports +{ + /** + * The property which should be matched + * + * For example 'primaryEmail' or 'homePhone' + * for card properties. + * + * Two further properties are defined that + * do not exist as properties on a card. + * + * 'card:nsIAbCard' which represents the interface + * of a card component + * + */ + readonly attribute string name; + + /** + * The value of the property + * + */ + readonly attribute wstring value; + + /** + * The value of the property + * as an interface + * + * Only valid if the corresponding + * property name is related to an + * interface instead of a wstring + * + */ + readonly attribute nsISupports valueISupports; +}; + +[scriptable, uuid(60b5961c-ce61-47b3-aa99-6d865f734dee)] +interface nsIAbDirectoryQuery : nsISupports +{ + /** + * Initiates a query on a directory and sub-directories for properties + * on cards + * + * @param aDirectory A directory that the query may get extra details + * from. + * + * @param aArguments The properties and values to match value could of + * type nsIAbDirectoryQueryMatchItem for matches other + * than ?contains? + * + * @param aListener The listener which will obtain individual query + * results. + * + * @param aResultLimit Limits the number of results returned to a maximum + * value. + * + * @param aTimeOut The maximum length of time for the query + * + * @return A context id for the query + */ + long doQuery(in nsIAbDirectory aDirectory, + in nsIAbDirectoryQueryArguments aArguments, + in nsIAbDirSearchListener aListener, + in long aResultLimit, + in long aTimeOut); + + /** + * Stops an existing query operation if + * query operation is asynchronous + * + * The nsIAbDirSearchListener will + * be notified when query has stopped + * + * It is implementation specific if notification + * synchronous or asynchronous + * + * @param contextID + * The unique number returned from + * the doQuery methods + * + */ + void stopQuery(in long contextID); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbDirectoryQueryProxy.idl b/comm/mailnews/addrbook/public/nsIAbDirectoryQueryProxy.idl new file mode 100644 index 0000000000..ced61ff8f4 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbDirectoryQueryProxy.idl @@ -0,0 +1,13 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#include "nsIAbDirectoryQuery.idl" + +[scriptable, uuid(b8034849-1e98-4d39-819c-15ba61a7434f)] +interface nsIAbDirectoryQueryProxy : nsIAbDirectoryQuery +{ + void initiate(); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPAttributeMap.idl b/comm/mailnews/addrbook/public/nsIAbLDAPAttributeMap.idl new file mode 100644 index 0000000000..c5bf6c565c --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbLDAPAttributeMap.idl @@ -0,0 +1,192 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsILDAPMessage; +interface nsIAbCard; + +/** + * A mapping between addressbook properties and ldap attributes. + * + * Each addressbook property can map to one or more attributes. If + * there is no entry in preferences for a field, the getters generally + * return null; empty strings are passed through as usual. The intent is + * that properties with a non-zero number of attributes can be overridden for + * a specific server by supplying a zero-length string. For this to work, + * most callers are likely to want to check for both success and a + * non-empty string. + * + * Note that the one exception to this pattern is getAttributes, which + * throws NS_ERROR_FAILURE for non-existent property entries, since + * XPConnect doesn't like returning null arrays. + * + * Note that each LDAP attribute can map to at most one addressbook + * property. The checkState method is a useful tool in enforcing + * this. Failure to enforce it may make it impossible to guarantee + * that getProperty will do something consistent and reasonable. + * + * Maybe someday once we support ldap autoconfig stuff (ie + * draft-joslin-config-schema-11.txt), we can simplify this and other + * code and only allow a property to map to a single attribute. + */ +[scriptable, uuid(fa019fd1-7f3d-417a-8957-154cca0240be)] +interface nsIAbLDAPAttributeMap : nsISupports +{ + /** + * Get all the LDAP attributes associated with a given property + * name, in order of precedence (highest to lowest). + * + * @param aProperty the address book property to return attrs for + * + * @return a comma-separated list of attributes, null if no entry is + * present + */ + ACString getAttributeList(in ACString aProperty); + + /** + * Get all the LDAP attributes associated with a given property name, in + * order of precedence (highest to lowest). + * + * @param aProperty the address book property to return attrs for + * + * @return an array of attributes + * + * @exception NS_ERROR_FAILURE if there is no entry for this property + */ + Array<ACString> getAttributes(in ACString aProperty); + + /** + * Get the first (canonical) LDAP attribute associated with a given property + * name + * + * @param aProperty the address book property to return attrs for + * + * @return the first attribute associated with a given property, + * null if there is no entry for this property + */ + ACString getFirstAttribute(in ACString aProperty); + + /** + * Set an existing mapping to the comma-separated list of attributes. + * + * @param aProperty the mozilla addressbook property name + * + * @param aAttributeList a comma-separated list of attributes in + * order of precedence from high to low + * + * @param aAllowInconsistencies allow changes that would result in + * a map with an LDAP attribute associated + * with more than one property. Useful for + * doing a bunch of sets at once, and + * calling checkState at the end. + * + * @exception NS_ERROR_FAILURE making this change would result in a map + * with an LDAP attribute pointing to more + * than one property + */ + void setAttributeList(in ACString aProperty, in ACString aAttributeList, + in boolean allowInconsistencies); + + /** + * Find the Mozilla addressbook property name that this attribute should + * map to. + * + * @return the addressbook property name, null if it's not used in the map + */ + ACString getProperty(in ACString aAttribute); + + /** + * Get all attributes that may be used in an addressbook card via this + * property map (used for passing to to an LDAP search when you want + * everything that could be in a card returned). + * + * @return a comma-separated list of attribute names + * + * @exception NS_ERROR_FAILURE there are no attributes in this property map + */ + ACString getAllCardAttributes(); + + /** + * Get all properties that may be used in an addressbook card via this + * property map. + * + * @return an array of properties + * + * @exception NS_ERROR_FAILURE there are no attributes in this property map + */ + Array<ACString> getAllCardProperties(); + + /** + * Check that no LDAP attributes are listed in more than one property. + * + * @exception NS_ERROR_FAILURE one or more LDAP attributes are listed + * multiple times. The object is now in an + * inconsistent state, and should be either + * manually repaired or discarded. + */ + void checkState(); + + /* These last two methods are really just for the convenience of the caller + * and to avoid tons of unnecessary crossing of the XPConnect boundary. + */ + + /** + * Set any attributes specified in the given prefbranch on this object. + * + * @param aPrefBranchName the pref branch containing all the + * property names + * + * @exception NS_ERROR_FAILURE one or more LDAP attributes are listed + * multiple times. The object is now in an + * inconsistent state, and should be either + * manually repaired or discarded. + */ + void setFromPrefs(in ACString aPrefBranchName); + + /** + * Set the properties on an addressbook card from the given LDAP message + * using the map in this object. + * + * @param aCard is the card object whose values are to be set + * @param aMessage is the LDAP message to get the values from + * + * @exception NS_ERROR_FAILURE is thrown if no addressbook properties + * are found in the message + */ + void setCardPropertiesFromLDAPMessage(in nsILDAPMessage aMessage, + in nsIAbCard aCard); +}; + +/** + * The nsIAbLDAPAttributeMapService is used to build and hold a cache + * of maps. + */ +[scriptable, uuid(12e2d589-3c2a-48e4-8c82-b1e6464a0dfd)] +interface nsIAbLDAPAttributeMapService : nsISupports +{ + /** + * Accessor to construct or return a cached copy of the attribute + * map for a given preference branch. The map is constructed by + * first taking the default map (as specified by the + * "ldap_2.servers.default.attrmap" prefbranch), and then having any + * preferences specified by aPrefBranchName override the defaults. + * LDIF import and export code should use the default map. + * + * @return the requested map + * + * @exception NS_ERROR_FAILURE error constructing the map; + * possibly because of a failure + * from checkState() + */ + nsIAbLDAPAttributeMap getMapForPrefBranch(in ACString aPrefBranchName); +}; + + +%{C++ +// test whether one of the getters has actually found an attribute +#define ATTRMAP_FOUND_ATTR(rv, str) (NS_SUCCEEDED(rv) && !(str).IsEmpty()) +%} diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPDirectory.idl b/comm/mailnews/addrbook/public/nsIAbLDAPDirectory.idl new file mode 100644 index 0000000000..8d84bf688e --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbLDAPDirectory.idl @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsIFile; +interface nsIAbLDAPAttributeMap; +interface nsILDAPURL; + +%{C++ +#define kLDAPDirectoryRoot "moz-abldapdirectory://" +#define kLDAPDirectoryRootLen 22 +%} + +/** + * XXX This should really inherit from nsIAbDirectory, and some day it will. + * But for now, doing that complicates implementation. + */ +[scriptable, uuid(90dde295-e354-4d58-Add8-f9b29a95942d)] +interface nsIAbLDAPDirectory : nsISupports +{ + /** + * The Replication File Name to use. + */ + attribute ACString replicationFileName; + + /** + * The version of LDAP protocol in use. + */ + attribute unsigned long protocolVersion; + + /** + * The SASL mechanism to use to authenticate to the LDAP server + * If this is an empty string, then a simple bind will be performed + * A non-zero string is assumed to be the name of the SASL mechanism. + * Currently the only supported mechanism is GSSAPI + */ + attribute ACString saslMechanism; + + /** + * The AuthDN to use to access the server. + */ + attribute AUTF8String authDn; + + /** + * The maximum number of matches that the server will return per a search. + */ + attribute long maxHits; + + /** + * The Last Change Number used for replication. + */ + attribute long lastChangeNumber; + + /** + * The LDAP server's scoping of the lastChangeNumber. + */ + attribute ACString dataVersion; + + /** + * The attribute map that is associated with this directory's server. + */ + readonly attribute nsIAbLDAPAttributeMap attributeMap; + + /** + * The LDAP URL for this directory. Note that this differs from + * nsIAbDirectory::URI. This attribute will give you a true ldap + * url, e.g. ldap://localhost:389/ whereas the uri will give you the + * directories rdf uri, e.g. moz-abldapdirectory://<pref base name>/. + */ + attribute nsILDAPURL lDAPURL; + + /** + * The replication (offline) file that this database uses. + */ + readonly attribute nsIFile replicationFile; + + /** + * The LDAP attributes used to build the Relative Distinguished Name + * of new cards, in the form of a comma separated list. + * + * The default is to use the common name (cn) attribute. + */ + attribute ACString rdnAttributes; + + /** + * The LDAP objectClass values added to cards when they are created/added, + * in the form of a comma separated list. + * + * The default is to use the following classes: + * top,person,organizationalPerson,inetOrgPerson,mozillaAbPersonAlpha + */ + attribute ACString objectClasses; + +}; diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPReplicationData.idl b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationData.idl new file mode 100644 index 0000000000..c42ffc9f4d --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationData.idl @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsIAbLDAPDirectory; +interface nsILDAPConnection; +interface nsILDAPURL; +interface nsIAbLDAPReplicationQuery; +interface nsIWebProgressListener; + +/** + * this service does replication of an LDAP directory to a local AB Database. + */ +[scriptable, uuid(e628bbc9-8793-4f0b-bce4-990d399b1fca)] +interface nsIAbLDAPProcessReplicationData : nsISupports +{ + /** + * readonly attribute giving the current replication state + */ + readonly attribute int32_t replicationState; + + /** + * replication states + */ + const long kIdle = 0; + const long kAnonymousBinding = 1; + const long kAuthenticatedBinding = 2; + const long kSyncServerBinding = 3; + const long kSearchingAuthDN = 4; + const long kDecidingProtocol = 5; + const long kAuthenticating = 6; + const long kReplicatingAll = 7; + const long kSearchingRootDSE = 8; + const long kFindingChanges = 9; + const long kReplicatingChanges = 10; + const long kReplicationDone = 11; + + /** + * this method initializes the implementation + */ + void init(in nsIAbLDAPDirectory directory, + in nsILDAPConnection connection, + in nsILDAPURL url, + in nsIAbLDAPReplicationQuery query, + in nsIWebProgressListener progressListener); + + /** + * this method a aborts the ongoing processing + */ + void abort(); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPReplicationQuery.idl b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationQuery.idl new file mode 100644 index 0000000000..aa9db9ab61 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationQuery.idl @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsIWebProgressListener; +interface nsIAbLDAPDirectory; + +/** + * this interface provides methods to perform LDAP Replication Queries + */ +[scriptable, uuid(460a739c-a8c1-4f24-b705-c89d136ab9f5)] +interface nsIAbLDAPReplicationQuery : nsISupports +{ + /** + * initialize for the query + */ + void init(in nsIAbLDAPDirectory aDirectory, + in nsIWebProgressListener aProgressListener); + + /** + * Starts an LDAP query to do replication as needed + */ + void doReplicationQuery(); + + /** + * Cancels the currently executing query + */ + void cancelQuery(); + + /** + * this method is the callback when query is done, failed or successful + */ + void done(in boolean aSuccess); +}; + +// XXX This interface currently isn't implemented as it didn't work. +// Bug 311632 should fix it +[scriptable, uuid(126202D1-4460-11d6-B7C2-00B0D06E5F27)] +interface nsIAbLDAPChangeLogQuery : nsISupports +{ + /** + * Starts an LDAP query to find auth DN + */ + void queryAuthDN(in AUTF8String aValueUsedToFindDn); + + /** + * Starts an LDAP query to search server's Root DSE + */ + void queryRootDSE(); + + /** + * Starts an LDAP ChangeLog query to find changelog entries + */ + void queryChangeLog(in AUTF8String aChangeLogDN, in int32_t aLastChangeNo); + + /** + * Starts an LDAP query to find changed entries + */ + void queryChangedEntries(in AUTF8String aChangedEntryDN); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbLDAPReplicationService.idl b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationService.idl new file mode 100644 index 0000000000..010c73d956 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbLDAPReplicationService.idl @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIWebProgressListener; +interface nsIAbLDAPDirectory; + +/** + * this service does replication of an LDAP directory to a local AB Database. + */ +[scriptable, uuid(3f499c70-5ceb-4b91-8b7f-62c366859383)] +interface nsIAbLDAPReplicationService: nsISupports { + + /** + * Start Replication of given LDAP directory represented by the URI + */ + void startReplication(in nsIAbLDAPDirectory aDirectory, + in nsIWebProgressListener progressListener); + + /** + * Cancel Replication of given LDAP directory represented by the URI + */ + void cancelReplication(in nsIAbLDAPDirectory aDirectory); + + /** + * callback when replication is done, failure or success + */ + void done(in boolean aSuccess); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbLDIFService.idl b/comm/mailnews/addrbook/public/nsIAbLDIFService.idl new file mode 100644 index 0000000000..6a32c458da --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbLDIFService.idl @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +#include "nsISupports.idl" + +interface nsIFile; +interface nsIAbDirectory; + +[scriptable, uuid(7afaa95f-0b1c-4d8a-a65f-bb5073ed6d39)] +interface nsIAbLDIFService : nsISupports { + + /** + * Determine if a file is likely to be an LDIF file based on field + * names that commonly appear in LDIF files. + * + * @param aSrc The file to examine + * + * @return true if the file appears to be of LDIF type, + * false otherwise + */ + boolean isLDIFFile(in nsIFile aSrc); + + /** + * Imports a file into the specified address book. + * + * @param aDirectory The address book to import addresses into. + * + * @param aSrc The file to import addresses from. + * + * @param aStoreLocAsHome Stores the address as a home rather than work + * address. + * + * @param aProgress May be null, but if a pointer is supplied, + * then it will be updated regularly with the + * current position of reading from the file. + * + */ + void importLDIFFile(in nsIAbDirectory aDirectory, + in nsIFile aSrc, + in boolean aStoreLocAsHome, + inout unsigned long aProgress); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbManager.idl b/comm/mailnews/addrbook/public/nsIAbManager.idl new file mode 100644 index 0000000000..366866d05b --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbManager.idl @@ -0,0 +1,132 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindowProxy; +interface nsIAbDirectory; +interface nsIAbCard; +interface nsIFile; +interface nsIAbBooleanExpression; + +/** + * nsIAbManager is an interface to the main address book manager + * via the contract id "@mozilla.org/abmanager;1" + * + * It contains the main functions to create and delete address books as well + * as some helper functions. + */ +[scriptable, uuid(ea0d8b3d-a549-4874-82d8-3a82cee2a3f1)] +interface nsIAbManager : nsISupports +{ + const unsigned long LDAP_DIRECTORY_TYPE = 0; + const unsigned long MAPI_DIRECTORY_TYPE = 3; + const unsigned long JS_DIRECTORY_TYPE = 101; + const unsigned long CARDDAV_DIRECTORY_TYPE = 102; + const unsigned long ASYNC_DIRECTORY_TYPE = 103; + + /** + * Returns an array containing all the top-level directories. + */ + readonly attribute Array<nsIAbDirectory> directories; + + /** + * Returns the directory that represents the supplied URI. + * + * @param aURI The URI of the address book to find. + * @return The found address book. + */ + nsIAbDirectory getDirectory(in ACString aURI); + + /** + * Returns the directory that has the supplied dirPrefId. + * + * @param aDirPrefId The dirPrefId of the directory. + * @return The found AB directory. + */ + nsIAbDirectory getDirectoryFromId(in ACString aDirPrefId); + + /** + * Returns the directory that has the supplied UID. + * + * @param aUID The UID of the directory. + * @return The found AB directory. + */ + nsIAbDirectory getDirectoryFromUID(in ACString aUID); + + /** + * Creates a new address book. + * + * @param aDirName The description of the address book. + * @param aURI The URI for the address book. This is specific to each + * type of address book. + * @param aType One of the *_DIRECTORY_TYPE constants. + * @param aUID Sets the UID of the new Address Book. + */ + ACString newAddressBook(in AString aDirName, in ACString aURI, + in unsigned long aType, + [optional] in AUTF8String aUID); + + /** + * Adds a previously created address book object. If it has not been removed + * (using `deleteAddressBook`) it will be removed at the end of the session. + * + * @param aDir The address book object. + */ + void addAddressBook(in nsIAbDirectory aDir); + + /** + * Deletes an address book. + * + * @param aURI The URI for the address book. This is specific to each + * type of address book. + */ + void deleteAddressBook(in ACString aURI); + + /** + * Finds out if the mailing list name exists in any address book. + * + * @param aName The name of the list to try and find. + * + * @return True if the name exists. + */ + boolean mailListNameExists(in AString name); + + /** + * Finds out if the directory name already exists. + * + * @param aName The name of a directory to check for. + * + * @return True if a directory called name already exists. + */ + boolean directoryNameExists(in AString name); + + /** + * Returns an address book card for the specified email address if found, in + * any directory. The first matching card found is returned. + * + * *** Results of this function are cached! *** + * This function is for where speed is more important than accuracy. Results + * are stored in a cache until 60s passes without this function being called. + * The address book *could* change in this time, in a way that produces a + * different result, but probably won't. + * + * @see nsIAbCard.cardForEmailAddress + * @param emailAddress The email address to find in any of the email address + * fields. If emailAddress is empty, the directories + * won't be searched and the function will return as if + * no card was found. + * @return An nsIAbCard if one was found, else returns NULL. + */ + nsIAbCard cardForEmailAddress(in AUTF8String emailAddress); + + /** + * Returns the mailing lists that has the supplied name. + * + * @param aName The name of the list. + * @return The found AB directory. + */ + nsIAbDirectory getMailListFromName(in AString aName); +}; diff --git a/comm/mailnews/addrbook/public/nsIAbOutlookInterface.idl b/comm/mailnews/addrbook/public/nsIAbOutlookInterface.idl new file mode 100644 index 0000000000..5ab66ac095 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIAbOutlookInterface.idl @@ -0,0 +1,12 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(088d3dea-4a6a-41ce-b974-5043d00f1798)] +interface nsIAbOutlookInterface : nsISupports +{ + Array<ACString> getFolderURIs(in AUTF8String aURI); +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPBERElement.idl b/comm/mailnews/addrbook/public/nsILDAPBERElement.idl new file mode 100644 index 0000000000..087fd1b2d2 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPBERElement.idl @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsILDAPBERValue; + + +/** + * nsILDAPBERElement is a wrapper interface for a C-SDK BerElement object. + * Typically, this is used as an intermediate object to aid in the manual + * construction of a BER value. Once the construction is completed by calling + * methods on this object, an nsILDAPBERValue can be retrieved from the + * asValue attribute on this interface. + * + * <http://www.mozilla.org/directory/ietf-docs/draft-ietf-ldapext-ldap-c-api-05.txt> + * contains some documentation that mostly (but not exactly) matches + * the code that this wraps in section 17. + */ + +[scriptable, uuid(409f5b31-c062-4d11-a35b-0a09e7967bf2)] +interface nsILDAPBERElement : nsISupports +{ + /** + * Initialize this object. Must be called before calling any other method + * on this interface. + * + * @param aValue value to preinitialize with; 0 for a new empty object + * + * @exception NS_ERROR_NOT_IMPLEMENTED preinitialization is currently + * not implemented + * @exception NS_ERROR_OUT_OF_MEMORY unable to allocate the internal + * BerElement + */ + void init(in nsILDAPBERValue aValue); + + /** + * Most TAG_* constants can be used in the construction or passing in of + * values to the aTag arguments to most of the methods in this interface. + */ + + /** + * When returned from a parsing method, 0xffffffff is referred to + * has the parse-error semantic (ie TAG_LBER_ERROR); when passing it to + * a construction method, it is used to mean "pick the default tag for + * this type" (ie TAG_LBER_DEFAULT). + */ + const unsigned long TAG_LBER_ERROR = 0xffffffff; + const unsigned long TAG_LBER_DEFAULT = 0xffffffff; + const unsigned long TAG_LBER_END_OF_SEQORSET = 0xfffffffe; + + /** + * BER encoding types and masks + */ + const unsigned long TAG_LBER_PRIMITIVE = 0x00; + + /** + * The following two tags are carried over from the LDAP C SDK; their + * exact purpose there is not well documented. They both have + * the same value there as well. + */ + const unsigned long TAG_LBER_CONSTRUCTED = 0x20; + const unsigned long TAG_LBER_ENCODING_MASK = 0x20; + + const unsigned long TAG_LBER_BIG_TAG_MASK = 0x1f; + const unsigned long TAG_LBER_MORE_TAG_MASK = 0x80; + + /** + * general BER types we know about + */ + const unsigned long TAG_LBER_BOOLEAN = 0x01; + const unsigned long TAG_LBER_INTEGER = 0x02; + const unsigned long TAG_LBER_BITSTRING = 0x03; + const unsigned long TAG_LBER_OCTETSTRING = 0x04; + const unsigned long TAG_LBER_NULL = 0x05; + const unsigned long TAG_LBER_ENUMERATED = 0x0a; + const unsigned long TAG_LBER_SEQUENCE = 0x30; + const unsigned long TAG_LBER_SET = 0x31; + + /** + * Write a string to this element. + * + * @param aString string to write + * @param aTag tag for this string (if TAG_LBER_DEFAULT is used, + * TAG_LBER_OCTETSTRING will be written). + * + * @return number of bytes written + * + * @exception NS_ERROR_FAILUE C-SDK returned error + */ + unsigned long putString(in AUTF8String aString, in unsigned long aTag); + + /** + * Start a set. Sets may be nested. + * + * @param aTag tag for this set (if TAG_LBER_DEFAULT is used, + * TAG_LBER_SET will be written). + * + * @exception NS_ERROR_FAILUE C-SDK returned an error + */ + void startSet(in unsigned long aTag); + + /** + * Cause the entire set started by the last startSet() call to be written. + * + * @exception NS_ERROR_FAILUE C-SDK returned an error + * + * @return number of bytes written + */ + unsigned long putSet(); + + /** + * an nsILDAPBERValue version of this element. Calls ber_flatten() under + * the hood. + * + * @exception NS_ERROR_OUT_OF_MEMORY + */ + readonly attribute nsILDAPBERValue asValue; +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPBERValue.idl b/comm/mailnews/addrbook/public/nsILDAPBERValue.idl new file mode 100644 index 0000000000..e4a5651476 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPBERValue.idl @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * Representation of a BER value as an interface containing an array of + * bytes. Someday this should perhaps be obsoleted by a better, more + * generalized version of nsIByteBuffer, but that's currently not even + * scriptable (see bug 125596). + */ +[scriptable, uuid(c817c5fe-1dd1-11b2-a10b-ae9885762ea9)] +interface nsILDAPBERValue : nsISupports +{ + /** + * Set the BER value from an array of bytes (copies). + * + * @exception NS_ERROR_OUT_OF_MEMORY couldn't allocate buffer to copy to + */ + void set(in Array<octet> aValue); + + /** + * Set the BER value from a UTF8 string (copies). + * + * @exception NS_ERROR_OUT_OF_MEMORY couldn't allocate buffer to copy to + */ + void setFromUTF8(in AUTF8String aValue); + + /** + * Get the BER value as an array of bytes. Note that if this value is + * zero-length, aCount and aRetVal will both be 0. This means that + * (in C++ anyway) the caller MUST test either aCount or aRetval before + * dereferencing aRetVal. + * + * @exception NS_ERROR_OUT_OF_MEMORY couldn't allocate buffer to copy to + */ + Array<octet> get(); +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPConnection.idl b/comm/mailnews/addrbook/public/nsILDAPConnection.idl new file mode 100644 index 0000000000..5fb44dd67c --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPConnection.idl @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsILDAPOperation; +interface nsILDAPMessageListener; +interface nsILDAPURL; + +%{C++ +#define NS_LDAPCONNECTION_CONTRACTID "@mozilla.org/network/ldap-connection;1" +%} + +[scriptable, uuid(360c1ff7-15e3-4ffe-b4b8-0eda72ebc096)] +interface nsILDAPConnection : nsISupports +{ + /** + * the string version of lderrno + */ + readonly attribute wstring errorString; + + /** + * DN to bind as. use the init() method to set this. + * + * @exception NS_ERROR_OUT_OF_MEMORY + */ + readonly attribute AUTF8String bindName; + + /** + * private parameter (anything caller desires) + */ + attribute nsISupports closure; + + /** + * Set up the connection. Note that init() must be called on a thread + * that already has an nsIEventQueue. + * + * @param aUrl A URL for the ldap server. The host, port and + * ssl connection type will be extracted from this + * @param aBindName DN to bind as + * @param aMessageListener Callback for DNS resolution completion + * @param aClosure private parameter (anything caller desires) + * @param aVersion LDAP version to use (currently VERSION2 or + * VERSION3) + * + * @exception NS_ERROR_ILLEGAL_VALUE null pointer or invalid version + * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory + * @exception NS_ERROR_OFFLINE we are in off-line mode + * @exception NS_ERROR_FAILURE + * @exception NS_ERROR_UNEXPECTED internal error + */ + void init(in nsILDAPURL aUrl, + in AUTF8String aBindName, + in nsILDAPMessageListener aMessageListener, + in nsISupports aClosure, in unsigned long aVersion); + + const unsigned long VERSION2 = 2; + const unsigned long VERSION3 = 3; + + /** + * Get information about the last error that occurred on this connection. + * + * @param matched if the server is returning LDAP_NO_SUCH_OBJECT, + * LDAP_ALIAS_PROBLEM, LDAP_INVALID_DN_SYNTAX, + * or LDAP_ALIAS_DEREF_PROBLEM, this will contain + * the portion of DN that matches the entry that is + * closest to the requested entry + * + * @param s additional error information from the server + * + * @return the error code, as defined in nsILDAPErrors.idl + */ + long getLdErrno(out AUTF8String matched, out AUTF8String s); +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPControl.idl b/comm/mailnews/addrbook/public/nsILDAPControl.idl new file mode 100644 index 0000000000..97a70a4d93 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPControl.idl @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsILDAPBERValue; + +/** + * XPCOM representation of the C SDK LDAPControl structure. + */ +[scriptable, uuid(3a7ceb8e-482a-4a4f-9aa4-26b9a69a3595)] +interface nsILDAPControl : nsISupports +{ + /** + * Control type, represented as a string. + * + * @exceptions none + */ + attribute ACString oid; + + /** + * The data associated with a control, if any. To specify that no data + * is to be associated with the control, don't set this at all (which + * is equivalent to setting it to null). + * + * @note Specifying a zero-length value is not currently supported. At some + * date, setting this to an nsILDAPBERValue which has not had any of the + * set methods called will be the appropriate way to do that. + * + * @exceptions none + */ + attribute nsILDAPBERValue value; + + /** + * Should the client or server abort if the control is not understood? + * Should be set to false for server controls used in abandon and unbind + * operations, since those have no server response. + * + * @exceptions none + */ + attribute boolean isCritical; +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPErrors.idl b/comm/mailnews/addrbook/public/nsILDAPErrors.idl new file mode 100644 index 0000000000..0251f63293 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPErrors.idl @@ -0,0 +1,447 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * Error codes used in the LDAP XPCOM SDK. + * + * Taken from the Mozilla C SDK's ldap.h include file, these should be + * the same as those specified in the draft-ietf-ldapext-ldap-c-api-04.txt + * Internet Draft. + * + * The only good documentation I'm aware of for these error codes is + * at <http://docs.iplanet.com/docs/manuals/directory.html#SDKC>. + * Unfortunately, this does not currently seem to be available under any + * open source license, so I can't include that documentation here as + * doxygen comments. + * + */ +[scriptable, uuid(f9ac10fa-1dd1-11b2-9798-8d5cbda95d74)] +interface nsILDAPErrors : nsISupports +{ + + const long SUCCESS = 0x00; + + const long OPERATIONS_ERROR = 0x01; + + const long PROTOCOL_ERROR = 0x02; + + const long TIMELIMIT_EXCEEDED = 0x03; + + const long SIZELIMIT_EXCEEDED = 0x04; + + const long COMPARE_FALSE = 0x05; + + const long COMPARE_TRUE = 0x06; + + const long STRONG_AUTH_NOT_SUPPORTED = 0x07; + + const long STRONG_AUTH_REQUIRED = 0x08; + + + /** + * UMich LDAPv2 extension + */ + const long PARTIAL_RESULTS = 0x09; + + /** + * new in LDAPv3 + */ + const long REFERRAL = 0x0a; + + /** + * new in LDAPv3 + */ + const long ADMINLIMIT_EXCEEDED = 0x0b; + + /** + * new in LDAPv3 + */ + const long UNAVAILABLE_CRITICAL_EXTENSION = 0x0c; + + /** + * new in LDAPv3 + */ + const long CONFIDENTIALITY_REQUIRED = 0x0d; + + /** + * new in LDAPv3 + */ + const long SASL_BIND_IN_PROGRESS = 0x0e; + + const long NO_SUCH_ATTRIBUTE = 0x10; + + const long UNDEFINED_TYPE = 0x11; + + const long INAPPROPRIATE_MATCHING = 0x12; + + const long CONSTRAINT_VIOLATION = 0x13; + + const long TYPE_OR_VALUE_EXISTS = 0x14; + + const long INVALID_SYNTAX = 0x15; + + const long NO_SUCH_OBJECT = 0x20; + + const long ALIAS_PROBLEM = 0x21; + + const long INVALID_DN_SYNTAX = 0x22; + + /** + * not used in LDAPv3 + */ + const long IS_LEAF = 0x23; + + const long ALIAS_DEREF_PROBLEM = 0x24; + + const long INAPPROPRIATE_AUTH = 0x30; + + const long INVALID_CREDENTIALS = 0x31; + + const long INSUFFICIENT_ACCESS = 0x32; + + const long BUSY = 0x33; + + const long UNAVAILABLE = 0x34; + + const long UNWILLING_TO_PERFORM = 0x35; + + const long LOOP_DETECT = 0x36; + + /** + * server side sort extension + */ + const long SORT_CONTROL_MISSING = 0x3C; + + /** + * VLV extension + */ + const long INDEX_RANGE_ERROR = 0x3D; + + const long NAMING_VIOLATION = 0x40; + + const long OBJECT_CLASS_VIOLATION = 0x41; + + const long NOT_ALLOWED_ON_NONLEAF = 0x42; + + const long NOT_ALLOWED_ON_RDN = 0x43; + + const long ALREADY_EXISTS = 0x44; + + const long NO_OBJECT_CLASS_MODS = 0x45; + + /** + * reserved CLDAP + */ + const long RESULTS_TOO_LARGE = 0x46; + + /** + * new in LDAPv3 + */ + const long AFFECTS_MULTIPLE_DSAS = 0x47; + + const long OTHER = 0x50; + + const long SERVER_DOWN = 0x51; + + const long LOCAL_ERROR = 0x52; + + const long ENCODING_ERROR = 0x53; + + const long DECODING_ERROR = 0x54; + + const long TIMEOUT = 0x55; + + const long AUTH_UNKNOWN = 0x56; + + const long FILTER_ERROR = 0x57; + + const long USER_CANCELLED = 0x58; + + const long PARAM_ERROR = 0x59; + + const long NO_MEMORY = 0x5a; + + const long CONNECT_ERROR = 0x5b; + + /** + * new in LDAPv3 + */ + const long NOT_SUPPORTED = 0x5c; + + /** + * new in LDAPv3 + */ + const long CONTROL_NOT_FOUND = 0x5d; + + /** + * new in LDAPv3 + */ + const long NO_RESULTS_RETURNED = 0x5e; + + /** + * new in LDAPv3 + */ + const long MORE_RESULTS_TO_RETURN = 0x5f; + + /** + * new in LDAPv3 + */ + const long CLIENT_LOOP = 0x60; + + /** + * new in LDAPv3 + */ + const long REFERRAL_LIMIT_EXCEEDED = 0x61; +}; + +/* + * Map these errors codes into the nsresult namespace in C++ + */ +%{C++ + +#define NS_ERROR_LDAP_OPERATIONS_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::OPERATIONS_ERROR) + +#define NS_ERROR_LDAP_PROTOCOL_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::PROTOCOL_ERROR) + +#define NS_ERROR_LDAP_TIMELIMIT_EXCEEDED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::TIMELIMIT_EXCEEDED) + +#define NS_ERROR_LDAP_SIZELIMIT_EXCEEDED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::SIZELIMIT_EXCEEDED) + +#define NS_ERROR_LDAP_COMPARE_FALSE \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::COMPARE_FALSE) + +#define NS_ERROR_LDAP_COMPARE_TRUE \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::COMPARE_TRUE) + +#define NS_ERROR_LDAP_STRONG_AUTH_NOT_SUPPORTED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::STRONG_AUTH_NOT_SUPPORTED) + +#define NS_ERROR_LDAP_STRONG_AUTH_REQUIRED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::STRONG_AUTH_REQUIRED) + +#define NS_ERROR_LDAP_PARTIAL_RESULTS \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::PARTIAL_RESULTS) + +#define NS_ERROR_LDAP_REFERRAL \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::REFERRAL) + +#define NS_ERROR_LDAP_ADMINLIMIT_EXCEEDED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::ADMINLIMIT_EXCEEDED) + +#define NS_ERROR_LDAP_UNAVAILABLE_CRITICAL_EXTENSION \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::UNAVAILABLE_CRITICAL_EXTENSION) + +#define NS_ERROR_LDAP_CONFIDENTIALITY_REQUIRED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::CONFIDENTIALITY_REQUIRED) + +#define NS_ERROR_LDAP_SASL_BIND_IN_PROGRESS \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::SASL_BIND_IN_PROGRESS) + +#define NS_ERROR_LDAP_NO_SUCH_ATTRIBUTE \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NO_SUCH_ATTRIBUTE) + +#define NS_ERROR_LDAP_UNDEFINED_TYPE \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::UNDEFINED_TYPE) + +#define NS_ERROR_LDAP_INAPPROPRIATE_MATCHING \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::INAPPROPRIATE_MATCHING) + +#define NS_ERROR_LDAP_CONSTRAINT_VIOLATION \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::CONSTRAINT_VIOLATION) + +#define NS_ERROR_LDAP_TYPE_OR_VALUE_EXISTS \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::TYPE_OR_VALUE_EXISTS) + +#define NS_ERROR_LDAP_INVALID_SYNTAX \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::INVALID_SYNTAX) + +#define NS_ERROR_LDAP_NO_SUCH_OBJECT \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NO_SUCH_OBJECT) + +#define NS_ERROR_LDAP_ALIAS_PROBLEM \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::ALIAS_PROBLEM) + +#define NS_ERROR_LDAP_INVALID_DN_SYNTAX \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::INVALID_DN_SYNTAX) + +#define NS_ERROR_LDAP_IS_LEAF \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::IS_LEAF) + +#define NS_ERROR_LDAP_ALIAS_DEREF_PROBLEM \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::ALIAS_DEREF_PROBLEM) + +#define NS_ERROR_LDAP_INAPPROPRIATE_AUTH \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::INAPPROPRIATE_AUTH) + +#define NS_ERROR_LDAP_INVALID_CREDENTIALS \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::INVALID_CREDENTIALS) + +#define NS_ERROR_LDAP_INSUFFICIENT_ACCESS \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::INSUFFICIENT_ACCESS) + +#define NS_ERROR_LDAP_BUSY \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::BUSY) + +#define NS_ERROR_LDAP_UNAVAILABLE \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::UNAVAILABLE) + +#define NS_ERROR_LDAP_UNWILLING_TO_PERFORM \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::UNWILLING_TO_PERFORM) + +#define NS_ERROR_LDAP_LOOP_DETECT \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::LOOP_DETECT) + +#define NS_ERROR_LDAP_SORT_CONTROL_MISSING \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::SORT_CONTROL_MISSING) + +#define NS_ERROR_LDAP_INDEX_RANGE_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::INDEX_RANGE_ERROR) + +#define NS_ERROR_LDAP_NAMING_VIOLATION \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NAMING_VIOLATION) + +#define NS_ERROR_LDAP_OBJECT_CLASS_VIOLATION \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::OBJECT_CLASS_VIOLATION) + +#define NS_ERROR_LDAP_NOT_ALLOWED_ON_NONLEAF \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NOT_ALLOWED_ON_NONLEAF) + +#define NS_ERROR_LDAP_NOT_ALLOWED_ON_RDN \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NOT_ALLOWED_ON_RDN) + +#define NS_ERROR_LDAP_ALREADY_EXISTS \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::ALREADY_EXISTS) + +#define NS_ERROR_LDAP_NO_OBJECT_CLASS_MODS \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NO_OBJECT_CLASS_MODS) + +#define NS_ERROR_LDAP_RESULTS_TOO_LARGE \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::RESULTS_TOO_LARGE) + +#define NS_ERROR_LDAP_AFFECTS_MULTIPLE_DSAS \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::AFFECTS_MULTIPLE_DSAS) + +#define NS_ERROR_LDAP_OTHER \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::OTHER) + +#define NS_ERROR_LDAP_SERVER_DOWN \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::SERVER_DOWN) + +#define NS_ERROR_LDAP_LOCAL_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::LOCAL_ERROR) + +#define NS_ERROR_LDAP_ENCODING_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::ENCODING_ERROR) + +#define NS_ERROR_LDAP_DECODING_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::DECODING_ERROR) + +#define NS_ERROR_LDAP_TIMEOUT \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::TIMEOUT) + +#define NS_ERROR_LDAP_AUTH_UNKNOWN \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::AUTH_UNKNOWN) + +#define NS_ERROR_LDAP_FILTER_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::FILTER_ERROR) + +#define NS_ERROR_LDAP_USER_CANCELLED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::USER_CANCELLED) + +#define NS_ERROR_LDAP_PARAM_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::PARAM_ERROR) + +#define NS_ERROR_LDAP_NO_MEMORY \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NO_MEMORY) + +#define NS_ERROR_LDAP_CONNECT_ERROR \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::CONNECT_ERROR) + +#define NS_ERROR_LDAP_NOT_SUPPORTED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NOT_SUPPORTED) + +#define NS_ERROR_LDAP_CONTROL_NOT_FOUND \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::CONTROL_NOT_FOUND) + +#define NS_ERROR_LDAP_NO_RESULTS_RETURNED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::NO_RESULTS_RETURNED) + +#define NS_ERROR_LDAP_MORE_RESULTS_TO_RETURN \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::MORE_RESULTS_TO_RETURN) + +#define NS_ERROR_LDAP_CLIENT_LOOP \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::CLIENT_LOOP) + +#define NS_ERROR_LDAP_REFERRAL_LIMIT_EXCEEDED \ + NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_LDAP, \ + nsILDAPErrors::REFERRAL_LIMIT_EXCEEDED) + +%} diff --git a/comm/mailnews/addrbook/public/nsILDAPMessage.idl b/comm/mailnews/addrbook/public/nsILDAPMessage.idl new file mode 100644 index 0000000000..f6dd94efc2 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPMessage.idl @@ -0,0 +1,167 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsILDAPBERValue; +interface nsILDAPOperation; + +%{C++ +#define NS_LDAPMESSAGE_CONTRACTID "@mozilla.org/network/ldap-message;1" +%} + +[scriptable, uuid(973ff50f-2002-4f0c-b57d-2242156139a2)] +interface nsILDAPMessage : nsISupports +{ + /** + * The Distinguished Name of the entry associated with this message. + * + * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory + * @exception NS_ERROR_ILLEGAL_VALUE null pointer passed in + * @exception NS_ERROR_LDAP_DECODING_ERROR problem during BER-decoding + * @exception NS_ERROR_UNEXPECTED bug or memory corruption + */ + readonly attribute AUTF8String dn; + + /** + * Get all the attributes in this message. + * + * @exception NS_ERROR_OUT_OF_MEMORY + * @exception NS_ERROR_ILLEGAL_VALUE null pointer passed in + * @exception NS_ERROR_UNEXPECTED bug or memory corruption + * @exception NS_ERROR_LDAP_DECODING_ERROR problem during BER decoding + * + * @return array of all attributes in the current message + */ + Array<AUTF8String> getAttributes(); + + /** + * Get an array of all the attribute values in this message. + * + * @param attr The attribute whose values are to be returned + * + * @exception NS_ERROR_UNEXPECTED Bug or memory corruption + * @exception NS_ERROR_LDAP_DECODING_ERROR Attribute not found or other + * decoding error. + * @exception NS_ERROR_OUT_OF_MEMORY + * + * @return Array of values for attr. + */ + Array<AString> getValues(in string attr); + + /** + * The operation this message originated from + * + * @exception NS_ERROR_NULL_POINTER NULL pointer to getter + */ + readonly attribute nsILDAPOperation operation; + + /** + * The result code (aka lderrno) for this message. + * + * IDL definitions for these constants live in nsILDAPErrors.idl. + * + * @exception NS_ERROR_ILLEGAL_VALUE null pointer passed in + */ + readonly attribute long errorCode; + + /** + * The result type of this message. Possible types listed below, the + * values chosen are taken from the draft-ietf-ldapext-ldap-c-api-04.txt + * and are the same ones used in the ldap.h include file from the Mozilla + * LDAP C SDK. + * + * @exception NS_ERROR_ILLEGAL_VALUE null pointer passed in + * @exception NS_ERROR_UNEXPECTED internal error (possible memory + * corruption) + */ + readonly attribute long type; + + /** + * Result of a bind operation + */ + const long RES_BIND = 0x61; + + /** + * An entry found in an search operation. + */ + const long RES_SEARCH_ENTRY = 0x64; + + /** + * An LDAPv3 search reference (a referral to another server) + */ + const long RES_SEARCH_REFERENCE = 0x73; + + /** + * The result of a search operation (i.e. the search is done; no more + * entries to follow). + */ + const long RES_SEARCH_RESULT = 0x65; + + /** + * The result of a modify operation. + */ + const long RES_MODIFY = 0x67; + + /** + * The result of an add operation + */ + const long RES_ADD = 0x69; + + /** + * The result of a delete operation + */ + const long RES_DELETE = 0x6B; + + /** + * The result of an modify DN operation + */ + const long RES_MODDN = 0x6D; + + /** + * The result of a compare operation + */ + const long RES_COMPARE = 0x6F; + + /** + * The result of an LDAPv3 extended operation + */ + const long RES_EXTENDED = 0x78; + + /** + * get an LDIF-like string representation of this message + * + * @return unicode encoded string representation. + */ + wstring toUnicode(); + + /** + * Additional error information optionally sent by the server. + */ + readonly attribute AUTF8String errorMessage; + + /** + * In LDAPv3, when the server returns any of the following errors: + * NO_SUCH_OBJECT, ALIAS_PROBLEM, INVALID_DN_SYNTAX, ALIAS_DEREF_PROBLEM, + * it also returns the closest existing DN to the entry requested. + */ + readonly attribute AUTF8String matchedDn; + + /** + * Get an array of all the attribute values in this message (a wrapper + * around the LDAP C SDK's get_values_len()). + * + * @param attr The attribute whose values are to be returned + * + * @exception NS_ERROR_UNEXPECTED Bug or memory corruption + * @exception NS_ERROR_LDAP_DECODING_ERROR Attribute not found or other + * decoding error. + * @exception NS_ERROR_OUT_OF_MEMORY + * + * @return Array of nsILDAPBERValue objects. + */ + Array<nsILDAPBERValue> getBinaryValues(in string attr); +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPMessageListener.idl b/comm/mailnews/addrbook/public/nsILDAPMessageListener.idl new file mode 100644 index 0000000000..2907840df5 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPMessageListener.idl @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsILDAPMessage; +interface nsILDAPConnection; +interface nsITransportSecurityInfo; + +/** + * A callback interface to be implemented by any objects that want to + * receive results from an nsILDAPOperation (ie nsILDAPMessages) as they + * come in. + */ +[scriptable, uuid(dc721d4b-3ff2-4387-a80c-5e29545f774a)] +interface nsILDAPMessageListener : nsISupports +{ + /** + * Invoked when Init has completed successfully LDAP operations can + * proceed. + */ + void onLDAPInit(); + + /** + * Messages from LDAP operations are passed back via this function. + * + * @param aMessage The message that was returned, NULL if none was. + * + * XXX semantics of NULL? + */ + void onLDAPMessage(in nsILDAPMessage aMessage); + + + /** + * Indicates that an error has occurred - either during init, or due to + * an LDAP operation. + * + * @param status The error code. + * @param secInfo The securityInfo object for the connection, if status + * is a security (NSS) error. Null otherwise. + * @param location If status is an NSS error code, this holds the location + * of the failed operation ("<host>:<port>"). + */ + void onLDAPError(in nsresult status, in nsITransportSecurityInfo secInfo, in ACString location); +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPModification.idl b/comm/mailnews/addrbook/public/nsILDAPModification.idl new file mode 100644 index 0000000000..453efc4aaa --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPModification.idl @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" + +interface nsILDAPBERValue; + +[scriptable, uuid(f64ef501-0623-11d6-a7f2-b65476fc49dc)] +interface nsILDAPModification : nsISupports +{ + /** + * The operation to perform. + */ + attribute long operation; + + /** + * Add operation + */ + const long MOD_ADD = 0x00; + + /** + * Delete operation + */ + const long MOD_DELETE = 0x01; + + /** + * Replace operation + */ + const long MOD_REPLACE = 0x02; + + /** + * Values are BER encoded + */ + const long MOD_BVALUES = 0x80; + + /** + * The attribute to modify. + */ + attribute ACString type; + + /** + * The array of values this modification sets for the attribute + */ + attribute Array<nsILDAPBERValue> values; + + /** + * Function that allows all the attributes to be set at the same + * time to avoid multiple function calls. + */ + void setUpModification(in long aOperation, in ACString aType, + in Array<nsILDAPBERValue> aValues); + + void setUpModificationOneValue(in long aOperation, in ACString aType, + in nsILDAPBERValue aValue); +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPOperation.idl b/comm/mailnews/addrbook/public/nsILDAPOperation.idl new file mode 100644 index 0000000000..3f3c9562c4 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPOperation.idl @@ -0,0 +1,278 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsILDAPConnection.idl" +#include "nsIAuthModule.idl" + +interface nsILDAPMessage; +interface nsILDAPMessageListener; +interface nsILDAPModification; +interface nsILDAPControl; + +%{C++ +#define NS_LDAPOPERATION_CONTRACTID "@mozilla.org/network/ldap-operation;1" +%} + +// XXXdmose check to make sure ctl-related err codes documented + +typedef uint32_t PRIntervalTime; + +[scriptable, uuid(4dfb1b19-fc8f-4525-92e7-f97b78a9747a)] +interface nsILDAPOperation : nsISupports +{ + /** + * The connection this operation is on. + * + * @exception NS_ERROR_ILLEGAL_VALUE a NULL pointer was passed in + */ + readonly attribute nsILDAPConnection connection; + + /** + * Callback for individual result messages related to this operation (set + * by the init() method). This is actually an nsISupports proxy object, + * as the callback will happen from another thread. + * + * @exception NS_ERROR_ILLEGAL_VALUE a NULL pointer was passed in + */ + readonly attribute nsILDAPMessageListener messageListener; + + /** + * The message-id associated with this operation. + * + * @exception NS_ERROR_ILLEGAL_VALUE a NULL pointer was passed in + */ + readonly attribute long messageID; + + /** + * private parameter (anything caller desires) + */ + attribute nsISupports closure; + /** + * number of the request for compare that the request is still valid. + */ + attribute unsigned long requestNum; + + /** + * No time and/or size limit specified + */ + const long NO_LIMIT = 0; + + /** + * If specified, these arrays of nsILDAPControls are passed into the LDAP + * C SDK for any extended operations (ie method calls on this interface + * ending in "Ext"). + */ + attribute Array<nsILDAPControl> serverControls; + attribute Array<nsILDAPControl> clientControls; + + /** + * Initializes this operation. Must be called prior to initiating + * any actual operations. Note that by default, the aMessageListener + * callbacks happen on the LDAP connection thread. If you need them + * to happen on the main thread (or any other thread), then you should + * created an nsISupports proxy object and pass that in. + * + * @param aConnection connection this operation should use + * @param aMessageListener interface used to call back the results. + * @param aClosure private parameter (anything caller desires) + * + * @exception NS_ERROR_ILLEGAL_VALUE a NULL pointer was passed in + * @exception NS_ERROR_UNEXPECTED failed to get connection handle + */ + void init(in nsILDAPConnection aConnection, + in nsILDAPMessageListener aMessageListener, + in nsISupports aClosure); + + /** + * Asynchronously authenticate to the LDAP server. + * + * @param passwd the password used for binding; NULL for anon-binds + * + * @exception NS_ERROR_LDAP_ENCODING_ERROR problem encoding bind request + * @exception NS_ERROR_LDAP_SERVER_DOWN server down (XXX rebinds?) + * @exception NS_ERROR_LDAP_CONNECT_ERROR connection failed or lost + * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory + * @exception NS_ERROR_UNEXPECTED internal error + */ + void simpleBind(in AUTF8String passwd); + + /** + * Asynchronously perform a SASL bind against the LDAP server + * + * @param service the host name of the service being connected to + * @param mechanism the name of the SASL mechanism in use + * @param authModuleType the type of auth module to be used to perform the operation + * + */ + void saslBind(in ACString service, in ACString mechanism, + in ACString authModuleType); + + /** + * Continue a SASL bind operation + * + * @param token the next SASL token to send to the server + * @param tokenLen the length of the token to send + * + */ + void saslStep(in string token, in unsigned long tokenLen); + + /** + * Kicks off an asynchronous add request. The "ext" stands for + * "extensions", and is intended to convey that this method will + * eventually support the extensions described in the + * draft-ietf-ldapext-ldap-c-api-04.txt Internet Draft. + * + * @param aBaseDn Base DN to add + * @param aModCount Number of modifications + * @param aMods Array of modifications + * + * @exception NS_ERROR_NOT_INITIALIZED operation not initialized + * @exception NS_ERROR_INVALID_ARG invalid argument + * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding + * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not + * receive the request or the + * connection was lost + * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory + * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version + * of the LDAP protocol that the + * client is using + * @exception NS_ERROR_UNEXPECTED an unexpected error has + * occurred + * + * XXX doesn't currently handle LDAPControl params + */ + void addExt(in AUTF8String aBaseDn, in Array<nsILDAPModification> aMods); + + /** + * Kicks off an asynchronous delete request. The "ext" stands for + * "extensions", and is intended to convey that this method will + * eventually support the extensions described in the + * draft-ietf-ldapext-ldap-c-api-04.txt Internet Draft. + * + * @param aBaseDn Base DN to delete + * + * @exception NS_ERROR_NOT_INITIALIZED operation not initialized + * @exception NS_ERROR_INVALID_ARG invalid argument + * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding + * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not + * receive the request or the + * connection was lost + * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory + * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version + * of the LDAP protocol that the + * client is using + * @exception NS_ERROR_UNEXPECTED an unexpected error has + * occurred + * + * XXX doesn't currently handle LDAPControl params + */ + void deleteExt(in AUTF8String aBaseDn); + + /** + * Kicks off an asynchronous modify request. The "ext" stands for + * "extensions", and is intended to convey that this method will + * eventually support the extensions described in the + * draft-ietf-ldapext-ldap-c-api-04.txt Internet Draft. + * + * @param aBaseDn Base DN to modify + * @param aModCount Number of modifications + * @param aMods Array of modifications + * + * @exception NS_ERROR_NOT_INITIALIZED operation not initialized + * @exception NS_ERROR_INVALID_ARG invalid argument + * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding + * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not + * receive the request or the + * connection was lost + * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory + * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version + * of the LDAP protocol that the + * client is using + * @exception NS_ERROR_UNEXPECTED an unexpected error has + * occurred + * + * XXX doesn't currently handle LDAPControl params + */ + void modifyExt(in AUTF8String aBaseDn, in Array<nsILDAPModification> aMods); + + /** + * Kicks off an asynchronous rename request. + * + * @param aBaseDn Base DN to rename + * @param aNewRDn New relative DN + * @param aNewParent DN of the new parent under which to move the + * entry + * @param aDeleteOldRDn Indicates whether to remove the old relative + * DN as a value in the entry or not + * + * @exception NS_ERROR_NOT_INITIALIZED operation not initialized + * @exception NS_ERROR_INVALID_ARG invalid argument + * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding + * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not + * receive the request or the + * connection was lost + * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory + * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version + * of the LDAP protocol that the + * client is using + * @exception NS_ERROR_UNEXPECTED an unexpected error has + * occurred + * + * XXX doesn't currently handle LDAPControl params + */ + void rename(in AUTF8String aBaseDn, in AUTF8String aNewRDn, + in AUTF8String aNewParent, in boolean aDeleteOldRDn); + + /** + * Kicks off an asynchronous search request. The "ext" stands for + * "extensions", and is intended to convey that this method will + * eventually support the extensions described in the + * draft-ietf-ldapext-ldap-c-api-04.txt Internet Draft. + * + * @param aBaseDn Base DN to search + * @param aScope One of SCOPE_{BASE,ONELEVEL,SUBTREE} + * @param aFilter Search filter + * @param aAttributes Comma separated list of values, holding the + * attributes we need + * @param aTimeOut How long to wait + * @param aSizeLimit Maximum number of entries to return. + * + * @exception NS_ERROR_NOT_INITIALIZED operation not initialized + * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding + * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not + * receive the request or the + * connection was lost + * @exception NS_ERROR_OUT_OF_MEMORY ran out of memory + * @exception NS_ERROR_INVALID_ARG invalid argument + * @exception NS_ERROR_LDAP_NOT_SUPPORTED not supported in the version + * of the LDAP protocol that the + * client is using + * @exception NS_ERROR_LDAP_FILTER_ERROR + * @exception NS_ERROR_UNEXPECTED + */ + void searchExt(in AUTF8String aBaseDn, in int32_t aScope, + in AUTF8String aFilter, in ACString aAttributes, + in PRIntervalTime aTimeOut, in int32_t aSizeLimit); + + /** + * Cancels an async operation that is in progress. + * + * XXX controls not supported yet + * + * @exception NS_ERROR_NOT_IMPLEMENTED server or client controls + * were set on this object + * @exception NS_ERROR_NOT_INITIALIZED operation not initialized + * @exception NS_ERROR_LDAP_ENCODING_ERROR error during BER-encoding + * @exception NS_ERROR_LDAP_SERVER_DOWN the LDAP server did not + * receive the request or the + * connection was lost + * @exception NS_ERROR_OUT_OF_MEMORY out of memory + * @exception NS_ERROR_INVALID_ARG invalid argument + * @exception NS_ERROR_UNEXPECTED internal error + */ + void abandonExt(); +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPService.idl b/comm/mailnews/addrbook/public/nsILDAPService.idl new file mode 100644 index 0000000000..6396dafc21 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPService.idl @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +/** + * This interface is a catch-all for any LDAP functionality that is needed + * in XPCOM and not provided elsewhere. + */ +[scriptable, uuid(69de6fbc-2e8c-4482-bf14-358d68b785d1)] +interface nsILDAPService : nsISupports { + + /** + * Generates and returns an LDAP search filter by substituting + * aValue, aAttr, aPrefix, and aSuffix into aPattern. + * + * Exposes the functionality of ldap_create_filter() via XPCOM. + * + * There is some documentation on the filter template format + * (passed in via aPattern) here: + * https://docs.oracle.com/cd/E19957-01/817-6707/filter.html + * + * @param aMaxSize maximum size (in char) of string to be + * created and returned (including final \0) + * @param aPattern pattern to be used for the filter + * @param aPrefix prefix to prepend to the filter + * @param aSuffix suffix to be appended to the filer + * @param aAttr replacement for %a in the pattern + * @param aValue replacement for %v in the pattern + * + * @exception NS_ERROR_INVALID_ARG invalid parameter passed in + * @exception NS_ERROR_OUT_OF_MEMORY allocation failed + * @exception NS_ERROR_NOT_AVAILABLE filter longer than maxsiz chars + * @exception NS_ERROR_UNEXPECTED ldap_create_filter returned + * unexpected error code + */ + AUTF8String createFilter(in unsigned long aMaxSize, in AUTF8String aPattern, + in AUTF8String aPrefix, in AUTF8String aSuffix, + in AUTF8String aAttr, in AUTF8String aValue); +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPSyncQuery.idl b/comm/mailnews/addrbook/public/nsILDAPSyncQuery.idl new file mode 100644 index 0000000000..d2709ed2a2 --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPSyncQuery.idl @@ -0,0 +1,27 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsISupports.idl" +interface nsILDAPURL; + + +[scriptable, uuid (0308fb36-1dd2-11b2-b16f-8510e8c5311a)] +interface nsILDAPSyncQuery : nsISupports { + + /** + * getQueryResults + * + * Create a new LDAP connection do a synchronous LDAP search and return + * the results. + * @param aServerURL - LDAP URL with parameters to a LDAP search + * ("ldap://host/base?attributes?one/sub?filter") + * @param aProtocolVersion - LDAP protocol version to use for connection + * (nsILDAPConnection.idl has symbolic constants) + * @return results + */ + wstring getQueryResults (in nsILDAPURL aServerURL, + in unsigned long aProtocolVersion); + +}; diff --git a/comm/mailnews/addrbook/public/nsILDAPURL.idl b/comm/mailnews/addrbook/public/nsILDAPURL.idl new file mode 100644 index 0000000000..fd0c3e28ec --- /dev/null +++ b/comm/mailnews/addrbook/public/nsILDAPURL.idl @@ -0,0 +1,170 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIURI.idl" + +%{C++ +#define NS_LDAPURL_CONTRACTID "@mozilla.org/network/ldap-url;1" +%} + +/** + * Strings in methods inherited from nsIURI, which are using XPIDL + * |string| types, are expected to be UTF8 encoded. All such strings + * in this interface, except attribute types (e.g. "cn"), should in fact + * be UTF8. It's important to remember that attributes can not be UTF8, + * they can only be of a limited subset of ASCII (see RFC 2251). + */ + +[scriptable, builtinclass, uuid(8e3a6d33-2e68-40ba-8f94-6ac03f69066e)] +interface nsILDAPURL : nsIURI { + /** + * Initialize an LDAP URL + * + * @param aUrlType - one of the URLTYPE_ flags @seealso nsIStandardURL + * @param aDefaultPort - if the port parsed from the URL string matches + * this port, then the port will be removed from the + * canonical form of the URL. + * @param aSpec - URL string. + * @param aOriginCharset - the charset from which this URI string + * originated. this corresponds to the charset + * that should be used when communicating this + * URI to an origin server, for example. if + * null, then provide aBaseURI implements this + * interface, the origin charset of aBaseURI will + * be assumed, otherwise defaulting to UTF-8 (i.e., + * no charset transformation from aSpec). + * @param aBaseURI - if null, aSpec must specify an absolute URI. + * otherwise, aSpec will be resolved relative + * to aBaseURI. + */ + void init(in unsigned long aUrlType, + in long aDefaultPort, + in AUTF8String aSpec, + in string aOriginCharset, + in nsIURI aBaseURI); + + /** + * The distinguished name of the URL (ie the base DN for the search). + * This string is expected to be a valid UTF8 string. + * + * for the getter: + * + * @exception NS_ERROR_NULL_POINTER NULL pointer to GET method + * @exception NS_ERROR_OUT_OF_MEMORY Ran out of memory + */ + attribute AUTF8String dn; + + /** + * The attributes to get for this URL, in comma-separated format. If the + * list is empty, all attributes are requested. + */ + attribute ACString attributes; + + /** + * Add one attribute to the array of attributes to request. If the + * attribute is already in our array, this becomes a noop. + * + * @param aAttribute An LDAP attribute (e.g. "cn") + */ + void addAttribute(in ACString aAttribute); + + /** + * Remove one attribute from the array of attributes to request. If + * the attribute didn't exist in the array, this becomes a noop. + * + * @param aAttribute An LDAP attribute (e.g. "cn") + * @exception NS_ERROR_OUT_OF_MEMORY Ran out of memory + */ + void removeAttribute(in ACString aAttribute); + + /** + * Test if an attribute is in our list of attributes already + * + * @param aAttribute An LDAP attribute (e.g. "cn") + * @return boolean Truth value + * @exception NS_ERROR_NULL_POINTER NULL pointer to GET method + */ + boolean hasAttribute(in ACString aAttribute); + + /** + * The scope of the search. defaults to SCOPE_BASE. + * + * @exception NS_ERROR_NULL_POINTER NULL pointer to GET method + * @exception NS_ERROR_MALFORMED_URI Illegal base to SET method + */ + attribute long scope; + + /** + * Search just the base object + */ + const long SCOPE_BASE = 0; + + /** + * Search only the children of the base object + */ + const long SCOPE_ONELEVEL = 1; + + /** + * Search the entire subtree under and including the base object + */ + const long SCOPE_SUBTREE = 2; + + /** + * The search filter. "(objectClass=*)" is the default. + */ + attribute AUTF8String filter; + + /** + * Any options defined for this URL (check options using a bitwise and) + * + * @exception NS_ERROR_NULL_POINTER NULL pointer to GET method + * @exception NS_ERROR_OUT_OF_MEMORY Ran out of memory + */ + attribute unsigned long options; + + /** + * If this is set/true, this is an ldaps: URL, not an ldap: URL + */ + const unsigned long OPT_SECURE = 0x01; +}; + +/** + * A structure to represent an LDAP URL. + */ +[scriptable, uuid(c0376fe9-2c7c-4f7b-a991-db9c3d95c1bb)] +interface nsILDAPURLParserResult : nsISupports { + /** The host name of the URL. */ + readonly attribute AUTF8String host; + + /** The port number of the URL. */ + readonly attribute long port; + + /** The distinguished name of the URL. */ + readonly attribute AUTF8String dn; + + /** The attributes to request when searching. */ + readonly attribute ACString attributes; + + /** The scope to use when searching. */ + readonly attribute long scope; + + /** The filter to use when searching. */ + readonly attribute AUTF8String filter; + + /** The options of the URL. */ + readonly attribute unsigned long options; +}; + +/** + * A helper module to parse a string to an LDAP URL. + */ +[scriptable, uuid(340098c0-a881-49ab-a5e8-f79d04e6651c)] +interface nsILDAPURLParser : nsISupports { + /** + * Parse a string to an LDAP URL. + */ + nsILDAPURLParserResult parse(in AUTF8String aSpec); +}; diff --git a/comm/mailnews/addrbook/public/nsIMsgVCardService.idl b/comm/mailnews/addrbook/public/nsIMsgVCardService.idl new file mode 100644 index 0000000000..b2d542f14c --- /dev/null +++ b/comm/mailnews/addrbook/public/nsIMsgVCardService.idl @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIAbCard; + +[scriptable, uuid(8b6ae917-676d-4f1f-bbad-2ecc9be0d9b1)] +interface nsIMsgVCardService : nsISupports { + + /** + * Translates a vCard string into a nsIAbCard. + * + * @param vCardStr - The string containing the vCard data. + * @return - A card containing the translated vCard data. + */ + nsIAbCard vCardToAbCard(in AString vCardStr); + + /** + * Translates an URL-encoded vCard string into a nsIAbCard. + * + * @param escapedVCardStr - The string containing the vCard data. + * @return - A card containing the translated vCard data. + */ + nsIAbCard escapedVCardToAbCard(in AString escapedVCardStr); + + /** + * Translates a nsIAbCard into an URL-encoded vCard. + * + * @param abCard - A card to be translated. + * @return - The string containing the vCard data. + */ + AString abCardToEscapedVCard(in nsIAbCard abCard); +}; diff --git a/comm/mailnews/addrbook/src/AbAutoCompleteMyDomain.jsm b/comm/mailnews/addrbook/src/AbAutoCompleteMyDomain.jsm new file mode 100644 index 0000000000..08a2654d03 --- /dev/null +++ b/comm/mailnews/addrbook/src/AbAutoCompleteMyDomain.jsm @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["AbAutoCompleteMyDomain"]; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function AbAutoCompleteMyDomain() {} + +AbAutoCompleteMyDomain.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]), + + cachedIdKey: "", + cachedIdentity: null, + + applicableHeaders: new Set(["addr_to", "addr_cc", "addr_bcc", "addr_reply"]), + + startSearch(aString, aSearchParam, aResult, aListener) { + let params = aSearchParam ? JSON.parse(aSearchParam) : {}; + let applicable = + "type" in params && this.applicableHeaders.has(params.type); + const ACR = Ci.nsIAutoCompleteResult; + var address = null; + if (applicable && aString && !aString.includes(",")) { + if ("idKey" in params && params.idKey != this.cachedIdKey) { + this.cachedIdentity = MailServices.accounts.getIdentity(params.idKey); + this.cachedIdKey = params.idKey; + } + if (this.cachedIdentity.autocompleteToMyDomain) { + address = aString.includes("@") + ? aString + : this.cachedIdentity.email.replace(/[^@]*/, aString); + } + } + + var result = { + searchString: aString, + searchResult: address ? ACR.RESULT_SUCCESS : ACR.RESULT_FAILURE, + defaultIndex: -1, + errorDescription: null, + matchCount: address ? 1 : 0, + getValueAt() { + return address; + }, + getLabelAt() { + return this.getValueAt(); + }, + getCommentAt() { + return null; + }, + getStyleAt() { + return "default-match"; + }, + getImageAt() { + return null; + }, + getFinalCompleteValueAt(aIndex) { + return this.getValueAt(aIndex); + }, + removeValueAt() {}, + }; + aListener.onSearchResult(this, result); + }, + + stopSearch() {}, +}; diff --git a/comm/mailnews/addrbook/src/AbAutoCompleteSearch.jsm b/comm/mailnews/addrbook/src/AbAutoCompleteSearch.jsm new file mode 100644 index 0000000000..8beff4f670 --- /dev/null +++ b/comm/mailnews/addrbook/src/AbAutoCompleteSearch.jsm @@ -0,0 +1,608 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["AbAutoCompleteSearch"]; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { + getSearchTokens, + getModelQuery, + modelQueryHasUserValue, + generateQueryURI, +} = ChromeUtils.import("resource:///modules/ABQueryUtils.jsm"); + +var ACR = Ci.nsIAutoCompleteResult; +var nsIAbAutoCompleteResult = Ci.nsIAbAutoCompleteResult; + +var MAX_ASYNC_RESULTS = 100; + +function nsAbAutoCompleteResult(aSearchString) { + // Can't create this in the prototype as we'd get the same array for + // all instances + this.asyncDirectories = []; + this._searchResults = []; // final results + this.searchString = aSearchString; + this._collectedValues = new Map(); // temporary unsorted results + // Get model query from pref; this will return mail.addr_book.autocompletequery.format.phonetic + // if mail.addr_book.show_phonetic_fields == true + this.modelQuery = getModelQuery("mail.addr_book.autocompletequery.format"); + // check if the currently active model query has been modified by user + this._modelQueryHasUserValue = modelQueryHasUserValue( + "mail.addr_book.autocompletequery.format" + ); +} + +nsAbAutoCompleteResult.prototype = { + _searchResults: null, + + // nsIAutoCompleteResult + + searchString: null, + searchResult: ACR.RESULT_NOMATCH, + defaultIndex: -1, + errorDescription: null, + + get matchCount() { + return this._searchResults.length; + }, + + getValueAt(aIndex) { + return this._searchResults[aIndex].value; + }, + + getLabelAt(aIndex) { + return this.getValueAt(aIndex); + }, + + getCommentAt(aIndex) { + return this._searchResults[aIndex].comment; + }, + + getStyleAt(aIndex) { + return "local-abook"; + }, + + getImageAt(aIndex) { + return ""; + }, + + getFinalCompleteValueAt(aIndex) { + return this.getValueAt(aIndex); + }, + + removeValueAt(aRowIndex, aRemoveFromDB) {}, + + // nsIAbAutoCompleteResult + + getCardAt(aIndex) { + return this._searchResults[aIndex].card; + }, + + getEmailToUse(aIndex) { + return this._searchResults[aIndex].emailToUse; + }, + + isCompleteResult(aIndex) { + return this._searchResults[aIndex].isCompleteResult; + }, + + modelQuery: null, + asyncDirectories: null, + + // nsISupports + + QueryInterface: ChromeUtils.generateQI([ + "nsIAutoCompleteResult", + "nsIAbAutoCompleteResult", + ]), +}; + +function AbAutoCompleteSearch() {} + +AbAutoCompleteSearch.prototype = { + // This is set from a preference, + // 0 = no comment column, 1 = name of address book this card came from + // Other numbers currently unused (hence default to zero) + _commentColumn: 0, + _parser: MailServices.headerParser, + _abManager: MailServices.ab, + applicableHeaders: new Set(["addr_to", "addr_cc", "addr_bcc", "addr_reply"]), + _result: null, + + // Private methods + + /** + * Returns the popularity index for a given card. This takes account of a + * translation bug whereby Thunderbird 2 stores its values in mork as + * hexadecimal, and Thunderbird 3 stores as decimal. + * + * @param {nsIAbDirectory} aDirectory - The directory that the card is in. + * @param {nsIAbCard} aCard - The card to return the popularity index for. + */ + _getPopularityIndex(aDirectory, aCard) { + let popularityValue = aCard.getProperty("PopularityIndex", "0"); + let popularityIndex = parseInt(popularityValue); + + // If we haven't parsed it the first time round, parse it as hexadecimal + // and repair so that we don't have to keep repairing. + if (isNaN(popularityIndex)) { + popularityIndex = parseInt(popularityValue, 16); + + // If its still NaN, just give up, we shouldn't ever get here. + if (isNaN(popularityIndex)) { + popularityIndex = 0; + } + + // Now store this change so that we're not changing it each time around. + if (!aDirectory.readOnly) { + aCard.setProperty("PopularityIndex", popularityIndex); + try { + aDirectory.modifyCard(aCard); + } catch (ex) { + console.error(ex); + } + } + } + return popularityIndex; + }, + + /** + * Gets the score of the (full) address, given the search input. We want + * results that match the beginning of a "word" in the result to score better + * than a result that matches only in the middle of the word. + * + * @param {nsIAbCard} aCard - The card whose score is being decided. + * @param {string} aAddress - Full lower-cased address, including display + * name and address. + * @param {string} aSearchString - Search string provided by user. + * @returns {integer} a score; a higher score is better than a lower one. + */ + _getScore(aCard, aAddress, aSearchString) { + const BEST = 100; + + // We will firstly check if the search term provided by the user + // is the nick name for the card or at least in the beginning of it. + let nick = aCard.getProperty("NickName", "").toLocaleLowerCase(); + aSearchString = aSearchString.toLocaleLowerCase(); + if (nick == aSearchString) { + return BEST + 1; + } + if (nick.indexOf(aSearchString) == 0) { + return BEST; + } + + // We'll do this case-insensitively and ignore the domain. + let atIdx = aAddress.lastIndexOf("@"); + if (atIdx != -1) { + // mail lists don't have an @ + aAddress = aAddress.substr(0, atIdx); + } + let idx = aAddress.indexOf(aSearchString); + if (idx == 0) { + return BEST; + } + if (idx == -1) { + return 0; + } + + // We want to treat firstname, lastname and word boundary(ish) parts of + // the email address the same. E.g. for "John Doe (:xx) <jd.who@example.com>" + // all of these should score the same: "John", "Doe", "xx", + // ":xx", "jd", "who". + let prevCh = aAddress.charAt(idx - 1); + if (/[ :."'(\-_<&]/.test(prevCh)) { + return BEST; + } + + // The match was inside a word -> we don't care about the position. + return 0; + }, + + /** + * Searches cards in the given directory. If a card is matched (and isn't + * a mailing list) then the function will add a result for each email address + * that exists. + * + * @param {string} searchQuery - The boolean search query to use. + * @param {string} searchString - The original search string. + * @param {nsIAbDirectory} directory - An nsIAbDirectory to search. + * @param {nsIAbAutoCompleteResult} result - The result element to append + * results to. + */ + _searchCards(searchQuery, searchString, directory, result) { + // Cache this values to save going through xpconnect each time + let commentColumn = this._commentColumn == 1 ? directory.dirName : ""; + + if (searchQuery[0] == "?") { + searchQuery = searchQuery.substring(1); + } + return new Promise(resolve => { + directory.search(searchQuery, searchString, { + onSearchFoundCard: card => { + if (card.isMailList) { + this._addToResult(commentColumn, directory, card, "", true, result); + } else { + let first = true; + for (let emailAddress of card.emailAddresses) { + this._addToResult( + commentColumn, + directory, + card, + emailAddress, + first, + result + ); + first = false; + } + } + }, + onSearchFinished(status, complete, secInfo, location) { + resolve(); + }, + }); + }); + }, + + /** + * Checks the parent card and email address of an autocomplete results entry + * from a previous result against the search parameters to see if that entry + * should still be included in the narrowed-down result. + * + * @param {nsIAbCard} aCard - The card to check. + * @param {string} aEmailToUse - The email address to check against. + * @param {string[]} aSearchWords - Words in the multi word search string. + * @returns {boolean} True if the card matches the search parameters, + * false otherwise. + */ + _checkEntry(aCard, aEmailToUse, aSearchWords) { + // Joining values of many fields in a single string so that a single + // search query can be fired on all of them at once. Separating them + // using spaces so that field1=> "abc" and field2=> "def" on joining + // shouldn't return true on search for "bcd". + // Note: This should be constructed from model query pref using + // getModelQuery("mail.addr_book.autocompletequery.format") + // but for now we hard-code the default value equivalent of the pref here + // or else bail out before and reconstruct the full c++ query if the pref + // has been customized (modelQueryHasUserValue), so that we won't get here. + let cumulativeFieldText = + aCard.displayName + + " " + + aCard.firstName + + " " + + aCard.lastName + + " " + + aEmailToUse + + " " + + aCard.getProperty("NickName", ""); + if (aCard.isMailList) { + cumulativeFieldText += " " + aCard.getProperty("Notes", ""); + } + cumulativeFieldText = cumulativeFieldText.toLocaleLowerCase(); + + return aSearchWords.every(String.prototype.includes, cumulativeFieldText); + }, + + /** + * Checks to see if an emailAddress (name/address) is a duplicate of an + * existing entry already in the results. If the emailAddress is found, it + * will remove the existing element if the popularity of the new card is + * higher than the previous card. + * + * @param {nsIAbDirectory} directory - The directory that the card is in. + * @param {nsIAbCard} card - The card that could be a duplicate. + * @param {string} lcEmailAddress - The emailAddress (name/address + * combination) to check for duplicates against. Lowercased. + * @param {nsIAbAutoCompleteResult} currentResults - The current results list. + */ + _checkDuplicate(directory, card, lcEmailAddress, currentResults) { + let existingResult = currentResults._collectedValues.get(lcEmailAddress); + if (!existingResult) { + return false; + } + + let popIndex = this._getPopularityIndex(directory, card); + // It's a duplicate, is the new one more popular? + if (popIndex > existingResult.popularity) { + // Yes it is, so delete this element, return false and allow + // _addToResult to sort the new element into the correct place. + currentResults._collectedValues.delete(lcEmailAddress); + return false; + } + // Not more popular, but still a duplicate. Return true and _addToResult + // will just forget about it. + return true; + }, + + /** + * Adds a card to the results list if it isn't a duplicate. The function will + * order the results by popularity. + * + * @param {string} commentColumn - The text to be displayed in the comment + * column (if any). + * @param {nsIAbDirectory} directory - The directory that the card is in. + * @param {nsIAbCard} card - The card being added to the results. + * @param {string} emailToUse - The email address from the card that should + * be used for this result. + * @param {boolean} isPrimaryEmail - Is the emailToUse the primary email? + * Set to true if it is the case. For mailing lists set it to true. + * @param {nsIAbAutoCompleteResult} result - The result to add the new entry to. + */ + _addToResult( + commentColumn, + directory, + card, + emailToUse, + isPrimaryEmail, + result + ) { + let mbox = this._parser.makeMailboxObject( + card.displayName, + card.isMailList + ? card.getProperty("Notes", "") || card.displayName + : emailToUse + ); + if (!mbox.email) { + return; + } + + let emailAddress = mbox.toString(); + let lcEmailAddress = emailAddress.toLocaleLowerCase(); + + // If it is a duplicate, then just return and don't add it. The + // _checkDuplicate function deals with it all for us. + if (this._checkDuplicate(directory, card, lcEmailAddress, result)) { + return; + } + + result._collectedValues.set(lcEmailAddress, { + value: emailAddress, + comment: commentColumn, + card, + isPrimaryEmail, + emailToUse, + isCompleteResult: true, + popularity: this._getPopularityIndex(directory, card), + score: this._getScore(card, lcEmailAddress, result.searchString), + }); + }, + + // nsIAutoCompleteSearch + + /** + * Starts a search based on the given parameters. + * + * @see nsIAutoCompleteSearch for parameter details. + * + * It is expected that aSearchParam contains the identity (if any) to use + * for determining if an address book should be autocompleted against. + */ + async startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) { + let params = aSearchParam ? JSON.parse(aSearchParam) : {}; + var result = new nsAbAutoCompleteResult(aSearchString); + if ("type" in params && !this.applicableHeaders.has(params.type)) { + result.searchResult = ACR.RESULT_IGNORED; + aListener.onSearchResult(this, result); + return; + } + + let fullString = aSearchString && aSearchString.trim().toLocaleLowerCase(); + + // If the search string is empty, or the user hasn't enabled autocomplete, + // then just return no matches or the result ignored. + if (!fullString) { + result.searchResult = ACR.RESULT_IGNORED; + aListener.onSearchResult(this, result); + return; + } + + // Array of all the terms from the fullString search query + // (separated on the basis of spaces or exact terms on the + // basis of quotes). + let searchWords = getSearchTokens(fullString); + + // Find out about the comment column + this._commentColumn = Services.prefs.getIntPref( + "mail.autoComplete.commentColumn", + 0 + ); + + let asyncDirectories = []; + + if ( + aPreviousResult instanceof nsIAbAutoCompleteResult && + aSearchString.startsWith(aPreviousResult.searchString) && + aPreviousResult.searchResult == ACR.RESULT_SUCCESS && + !result._modelQueryHasUserValue && + result.modelQuery == aPreviousResult.modelQuery + ) { + // We have successful previous matches, and model query has not changed since + // previous search, therefore just iterate through the list of previous result + // entries and reduce as appropriate (via _checkEntry function). + // Test for model query change is required: when reverting back from custom to + // default query, result._modelQueryHasUserValue==false, but we must bail out. + // Todo: However, if autocomplete model query has been customized, we fall + // back to using the full query again instead of reducing result list in js; + // The full query might be less performant as it's fired against entire AB, + // so we should try morphing the query for js. We can't use the _checkEntry + // js query yet because it is hardcoded (mimic default model query). + // At least we now allow users to customize their autocomplete model query... + for (let i = 0; i < aPreviousResult.matchCount; ++i) { + if (aPreviousResult.isCompleteResult(i)) { + let card = aPreviousResult.getCardAt(i); + let email = aPreviousResult.getEmailToUse(i); + if (this._checkEntry(card, email, searchWords)) { + // Add matches into the results array. We re-sort as needed later. + result._searchResults.push({ + value: aPreviousResult.getValueAt(i), + comment: aPreviousResult.getCommentAt(i), + card, + isPrimaryEmail: card.primaryEmail == email, + emailToUse: email, + isCompleteResult: true, + popularity: parseInt(card.getProperty("PopularityIndex", "0")), + score: this._getScore( + card, + aPreviousResult.getValueAt(i).toLocaleLowerCase(), + fullString + ), + }); + } + } + } + + asyncDirectories = aPreviousResult.asyncDirectories; + } else { + // Construct the search query from pref; using a query means we can + // optimise on running the search through c++ which is better for string + // comparisons (_checkEntry is relatively slow). + // When user's fullstring search expression is a multiword query, search + // for each word separately so that each result contains all the words + // from the fullstring in the fields of the addressbook card + // (see bug 558931 for explanations). + // Use helper method to split up search query to multi-word search + // query against multiple fields. + let searchWords = getSearchTokens(fullString); + let searchQuery = generateQueryURI(result.modelQuery, searchWords); + + // Now do the searching + // We're not going to bother searching sub-directories, currently the + // architecture forces all cards that are in mailing lists to be in ABs as + // well, therefore by searching sub-directories (aka mailing lists) we're + // just going to find duplicates. + for (let dir of this._abManager.directories) { + // A failure in one address book should no break the whole search. + try { + if (dir.useForAutocomplete("idKey" in params ? params.idKey : null)) { + await this._searchCards(searchQuery, aSearchString, dir, result); + } else if (dir.dirType == Ci.nsIAbManager.ASYNC_DIRECTORY_TYPE) { + asyncDirectories.push(dir); + } + } catch (ex) { + console.error( + new Components.Exception( + `Exception thrown by ${dir.URI}: ${ex.message}`, + ex + ) + ); + } + } + + result._searchResults = [...result._collectedValues.values()]; + // Make sure a result with direct email match will be the one used. + for (let sr of result._searchResults) { + if (sr.emailToUse == fullString.replace(/.*<(.+@.+)>$/, "$1")) { + sr.score = 100; + } + } + } + + // Sort the results. Scoring may have changed so do it even if this is + // just filtered previous results. Only local results are sorted, + // because the autocomplete widget doesn't let us alter the order of + // results that have already been notified. + result._searchResults.sort(function (a, b) { + // Order by 1) descending score, then 2) descending popularity, + // then 3) any emails that actually match the search string, + // 4) primary email before secondary for the same card, then + // 5) by emails sorted alphabetically. + return ( + b.score - a.score || + b.popularity - a.popularity || + (b.emailToUse.includes(aSearchString) && + !a.emailToUse.includes(aSearchString) + ? 1 + : 0) || + (a.card == b.card && a.isPrimaryEmail ? -1 : 0) || + a.value.localeCompare(b.value) + ); + }); + + if (result.matchCount) { + result.searchResult = ACR.RESULT_SUCCESS; + result.defaultIndex = 0; + } + + if (!asyncDirectories.length) { + // We're done. Just return our result immediately. + aListener.onSearchResult(this, result); + return; + } + + // Let the widget know the sync results we have so far. + result.searchResult = result.matchCount + ? ACR.RESULT_SUCCESS_ONGOING + : ACR.RESULT_NOMATCH_ONGOING; + aListener.onSearchResult(this, result); + + // Start searching our asynchronous autocomplete directories. + this._result = result; + let searches = new Set(); + for (let dir of asyncDirectories) { + let comment = this._commentColumn == 1 ? dir.dirName : ""; + let cards = []; + let searchListener = { + onSearchFoundCard: card => { + cards.push(card); + }, + onSearchFinished: (status, isCompleteResult, secInfo, location) => { + if (this._result != result) { + // The search was aborted, so give up. + return; + } + searches.delete(searchListener); + if (cards.length) { + // Avoid overwhelming the UI with excessive results. + if (cards.length > MAX_ASYNC_RESULTS) { + cards.length = MAX_ASYNC_RESULTS; + isCompleteResult = false; + } + // We can't guarantee to score the extension's results accurately so + // we assume that the extension has sorted the results appropriately + for (let card of cards) { + let emailToUse = card.primaryEmail; + let value = MailServices.headerParser + .makeMailboxObject(card.displayName, emailToUse) + .toString(); + result._searchResults.push({ + value, + comment, + card, + emailToUse, + isCompleteResult, + }); + } + if (!isCompleteResult) { + // Next time perform a full search again to get better results. + result.asyncDirectories.push(dir); + } + } + if (result._searchResults.length) { + result.searchResult = searches.size + ? ACR.RESULT_SUCCESS_ONGOING + : ACR.RESULT_SUCCESS; + result.defaultIndex = 0; + } else { + result.searchResult = searches.size + ? ACR.RESULT_NOMATCH_ONGOING + : ACR.RESULT_NOMATCH; + } + aListener.onSearchResult(this, result); + }, + }; + // Keep track of the pending searches so that we know when we've finished. + searches.add(searchListener); + dir.search(null, aSearchString, searchListener); + } + }, + + stopSearch() { + this._result = null; + }, + + // nsISupports + + QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]), +}; diff --git a/comm/mailnews/addrbook/src/AbLDAPAttributeMap.jsm b/comm/mailnews/addrbook/src/AbLDAPAttributeMap.jsm new file mode 100644 index 0000000000..dae7b97630 --- /dev/null +++ b/comm/mailnews/addrbook/src/AbLDAPAttributeMap.jsm @@ -0,0 +1,219 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["AbLDAPAttributeMap", "AbLDAPAttributeMapService"]; + +function AbLDAPAttributeMap() { + this.mPropertyMap = {}; + this.mAttrMap = {}; +} + +AbLDAPAttributeMap.prototype = { + getAttributeList(aProperty) { + if (!(aProperty in this.mPropertyMap)) { + return null; + } + + // return the joined list + return this.mPropertyMap[aProperty].join(","); + }, + + getAttributes(aProperty) { + // fail if no entry for this + if (!(aProperty in this.mPropertyMap)) { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + return this.mPropertyMap[aProperty]; + }, + + getFirstAttribute(aProperty) { + // fail if no entry for this + if (!(aProperty in this.mPropertyMap)) { + return null; + } + + return this.mPropertyMap[aProperty][0]?.replace(/\[(\d+)\]$/, ""); + }, + + setAttributeList(aProperty, aAttributeList, aAllowInconsistencies) { + var attrs = aAttributeList.split(","); + + // check to make sure this call won't allow multiple mappings to be + // created, if requested + if (!aAllowInconsistencies) { + for (var attr of attrs) { + if (attr in this.mAttrMap && this.mAttrMap[attr] != aProperty) { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + } + } + + // delete any attr mappings created by the existing property map entry + if (aProperty in this.mPropertyMap) { + for (attr of this.mPropertyMap[aProperty]) { + delete this.mAttrMap[attr]; + } + } + + // add these attrs to the attrmap + for (attr of attrs) { + this.mAttrMap[attr] = aProperty; + } + + // add them to the property map + this.mPropertyMap[aProperty] = attrs; + }, + + getProperty(aAttribute) { + if (!(aAttribute in this.mAttrMap)) { + return null; + } + + return this.mAttrMap[aAttribute]; + }, + + getAllCardAttributes() { + var attrs = []; + for (let attrArray of Object.entries(this.mPropertyMap)) { + for (let attrName of attrArray) { + attrName = attrName.toString().replace(/\[(\d+)\]$/, ""); + if (attrs.includes(attrName)) { + continue; + } + attrs.push(attrName); + } + } + + if (!attrs.length) { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + return attrs.join(","); + }, + + getAllCardProperties() { + var props = []; + for (var prop in this.mPropertyMap) { + props.push(prop); + } + return props; + }, + + setFromPrefs(aPrefBranchName) { + // get the right pref branch + let branch = Services.prefs.getBranch(aPrefBranchName + "."); + + // get the list of children + var children = branch.getChildList(""); + + // do the actual sets + for (var child of children) { + this.setAttributeList(child, branch.getCharPref(child), true); + } + + // ensure that everything is kosher + this.checkState(); + }, + + setCardPropertiesFromLDAPMessage(aMessage, aCard) { + var cardValueWasSet = false; + + var msgAttrs = aMessage.getAttributes(); + + // downcase the array for comparison + function toLower(a) { + return a.toLowerCase(); + } + msgAttrs = msgAttrs.map(toLower); + + // deal with each addressbook property + for (var prop in this.mPropertyMap) { + // go through the list of possible attrs in precedence order + for (var attr of this.mPropertyMap[prop]) { + attr = attr.toLowerCase(); + // allow an index in attr + let valueIndex = 0; + const valueIndexMatch = /^(.+)\[(\d+)\]$/.exec(attr); + if (valueIndexMatch !== null) { + attr = valueIndexMatch[1]; + valueIndex = parseInt(valueIndexMatch[2]); + } + + // find the first attr that exists in this message + if (msgAttrs.includes(attr)) { + try { + var values = aMessage.getValues(attr); + // strip out the optional label from the labeledURI + if (attr == "labeleduri" && values[valueIndex]) { + var index = values[valueIndex].indexOf(" "); + if (index != -1) { + values[valueIndex] = values[valueIndex].substring(0, index); + } + } + aCard.setProperty(prop, values[valueIndex]); + + cardValueWasSet = true; + break; + } catch (ex) { + // ignore any errors getting message values or setting card values + } + } + } + } + + if (!cardValueWasSet) { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + }, + + checkState() { + var attrsSeen = []; + + for (var prop in this.mPropertyMap) { + let attrArray = this.mPropertyMap[prop]; + for (var attr of attrArray) { + // multiple attributes that mapped to the empty string are permitted + if (!attr.length) { + continue; + } + + // if we've seen this before, there's a problem + if (attrsSeen.includes(attr)) { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } + + // remember that we've seen it now + attrsSeen.push(attr); + } + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIAbLDAPAttributeMap"]), +}; + +function AbLDAPAttributeMapService() {} + +AbLDAPAttributeMapService.prototype = { + mAttrMaps: {}, + + getMapForPrefBranch(aPrefBranchName) { + // if we've already got this map, return it + if (aPrefBranchName in this.mAttrMaps) { + return this.mAttrMaps[aPrefBranchName]; + } + + // otherwise, try and create it + var attrMap = new AbLDAPAttributeMap(); + attrMap.setFromPrefs("ldap_2.servers.default.attrmap"); + attrMap.setFromPrefs(aPrefBranchName + ".attrmap"); + + // cache + this.mAttrMaps[aPrefBranchName] = attrMap; + + // and return + return attrMap; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIAbLDAPAttributeMapService"]), +}; diff --git a/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm b/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm new file mode 100644 index 0000000000..8df1c0f71e --- /dev/null +++ b/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm @@ -0,0 +1,364 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["AbLDAPAutoCompleteSearch"]; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var ACR = Ci.nsIAutoCompleteResult; + +// nsAbLDAPAutoCompleteResult +// Derived from nsIAbAutoCompleteResult, provides a LDAP specific result +// implementation. + +function nsAbLDAPAutoCompleteResult(aSearchString) { + // Can't create this in the prototype as we'd get the same array for + // all instances + this._searchResults = []; + this.searchString = aSearchString; +} + +nsAbLDAPAutoCompleteResult.prototype = { + _searchResults: null, + _commentColumn: "", + + // nsIAutoCompleteResult + + searchString: null, + searchResult: ACR.RESULT_NOMATCH, + defaultIndex: -1, + errorDescription: null, + + get matchCount() { + return this._searchResults.length; + }, + + getLabelAt(aIndex) { + return this.getValueAt(aIndex); + }, + + getValueAt(aIndex) { + return this._searchResults[aIndex].value; + }, + + getCommentAt(aIndex) { + return this._commentColumn; + }, + + getStyleAt(aIndex) { + return this.searchResult == ACR.RESULT_FAILURE + ? "remote-err" + : "remote-abook"; + }, + + getImageAt(aIndex) { + return ""; + }, + + getFinalCompleteValueAt(aIndex) { + return this.getValueAt(aIndex); + }, + + removeValueAt(aRowIndex, aRemoveFromDB) {}, + + // nsIAbAutoCompleteResult + + getCardAt(aIndex) { + return this._searchResults[aIndex].card; + }, + + // nsISupports + + QueryInterface: ChromeUtils.generateQI([ + "nsIAutoCompleteResult", + "nsIAbAutoCompleteResult", + ]), +}; + +function AbLDAPAutoCompleteSearch() { + Services.obs.addObserver(this, "quit-application"); + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); +} + +AbLDAPAutoCompleteSearch.prototype = { + // A short-lived LDAP directory cache. + // To avoid recreating components as the user completes, we maintain the most + // recently used address book, nsAbLDAPDirectoryQuery and search context. + // However the cache is discarded if it has not been used for a minute. + // This is done to avoid problems with LDAP sessions timing out and hanging. + _query: null, + _book: null, + _attributes: null, + _context: -1, + _timer: null, + + // The current search result. + _result: null, + // The listener to pass back results to. + _listener: null, + + _parser: MailServices.headerParser, + + applicableHeaders: new Set(["addr_to", "addr_cc", "addr_bcc", "addr_reply"]), + + // Private methods + + _checkDuplicate(card, emailAddress) { + var lcEmailAddress = emailAddress.toLocaleLowerCase(); + + return this._result._searchResults.some(function (result) { + return result.value.toLocaleLowerCase() == lcEmailAddress; + }); + }, + + _addToResult(card, address) { + let mbox = this._parser.makeMailboxObject( + card.displayName, + card.isMailList + ? card.getProperty("Notes", "") || card.displayName + : address + ); + if (!mbox.email) { + return; + } + + let emailAddress = mbox.toString(); + + // If it is a duplicate, then just return and don't add it. The + // _checkDuplicate function deals with it all for us. + if (this._checkDuplicate(card, emailAddress)) { + return; + } + + // Find out where to insert the card. + var insertPosition = 0; + + // Next sort on full address + while ( + insertPosition < this._result._searchResults.length && + emailAddress > this._result._searchResults[insertPosition].value + ) { + ++insertPosition; + } + + this._result._searchResults.splice(insertPosition, 0, { + value: emailAddress, + card, + }); + }, + + // nsIObserver + + observe(subject, topic, data) { + if (topic == "quit-application") { + Services.obs.removeObserver(this, "quit-application"); + } else if (topic != "timer-callback") { + return; + } + + // Force the individual query items to null, so that the memory + // gets collected straight away. + this.stopSearch(); + this._book = null; + this._context = -1; + this._query = null; + this._attributes = null; + }, + + // nsIAutoCompleteSearch + + startSearch(aSearchString, aParam, aPreviousResult, aListener) { + let params = JSON.parse(aParam) || {}; + let applicable = + !("type" in params) || this.applicableHeaders.has(params.type); + + this._result = new nsAbLDAPAutoCompleteResult(aSearchString); + aSearchString = aSearchString.toLocaleLowerCase(); + + // If the search string isn't value, or contains a comma, or the user + // hasn't enabled autocomplete, then just return no matches / or the + // result ignored. + // The comma check is so that we don't autocomplete against the user + // entering multiple addresses. + if (!applicable || !aSearchString || aSearchString.includes(",")) { + this._result.searchResult = ACR.RESULT_IGNORED; + aListener.onSearchResult(this, this._result); + return; + } + + // The rules here: If the current identity has a directoryServer set, then + // use that, otherwise, try the global preference instead. + var acDirURI = null; + var identity; + + if ("idKey" in params) { + try { + identity = MailServices.accounts.getIdentity(params.idKey); + } catch (ex) { + console.error( + "Couldn't get specified identity, " + + "falling back to global settings" + ); + } + } + + // Does the current identity override the global preference? + if (identity && identity.overrideGlobalPref) { + acDirURI = identity.directoryServer; + } else if (Services.prefs.getBoolPref("ldap_2.autoComplete.useDirectory")) { + // Try the global one + acDirURI = Services.prefs.getCharPref( + "ldap_2.autoComplete.directoryServer" + ); + } + + if (!acDirURI || Services.io.offline) { + // No directory to search or we are offline, send a no match and return. + aListener.onSearchResult(this, this._result); + return; + } + + this.stopSearch(); + + // If we don't already have a cached query for this URI, build a new one. + acDirURI = "moz-abldapdirectory://" + acDirURI; + if (!this._book || this._book.URI != acDirURI) { + this._query = Cc[ + "@mozilla.org/addressbook/ldap-directory-query;1" + ].createInstance(Ci.nsIAbDirectoryQuery); + this._book = MailServices.ab + .getDirectory(acDirURI) + .QueryInterface(Ci.nsIAbLDAPDirectory); + + // Create a minimal map just for the display name and primary email. + this._attributes = Cc[ + "@mozilla.org/addressbook/ldap-attribute-map;1" + ].createInstance(Ci.nsIAbLDAPAttributeMap); + this._attributes.setAttributeList( + "DisplayName", + this._book.attributeMap.getAttributeList("DisplayName", {}), + true + ); + this._attributes.setAttributeList( + "PrimaryEmail", + this._book.attributeMap.getAttributeList("PrimaryEmail", {}), + true + ); + this._attributes.setAttributeList( + "SecondEmail", + this._book.attributeMap.getAttributeList("SecondEmail", {}), + true + ); + } + + this._result._commentColumn = this._book.dirName; + this._listener = aListener; + this._timer.init(this, 60000, Ci.nsITimer.TYPE_ONE_SHOT); + + var args = Cc[ + "@mozilla.org/addressbook/directory/query-arguments;1" + ].createInstance(Ci.nsIAbDirectoryQueryArguments); + + var filterTemplate = this._book.getStringValue( + "autoComplete.filterTemplate", + "" + ); + + // Use default value when preference is not set or it contains empty string + if (!filterTemplate) { + filterTemplate = + "(|(cn=*%v1*%v2-*)(mail=*%v*)(givenName=*%v1*)(sn=*%v*))"; + } + + // Create filter from filter template and search string + var ldapSvc = Cc["@mozilla.org/network/ldap-service;1"].getService( + Ci.nsILDAPService + ); + var filter = ldapSvc.createFilter( + 1024, + filterTemplate, + "", + "", + "", + aSearchString + ); + if (!filter) { + throw new Error( + "Filter string is empty, check if filterTemplate variable is valid in prefs.js." + ); + } + args.typeSpecificArg = this._attributes; + args.querySubDirectories = true; + args.filter = filter; + + // Start the actual search + this._context = this._query.doQuery( + this._book, + args, + this, + this._book.maxHits, + 0 + ); + }, + + stopSearch() { + if (this._listener) { + this._query.stopQuery(this._context); + this._listener = null; + } + }, + + // nsIAbDirSearchListener + + onSearchFinished(status, complete, secInfo, location) { + if (!this._listener) { + return; + } + + if (status == Cr.NS_OK) { + if (this._result.matchCount) { + this._result.searchResult = ACR.RESULT_SUCCESS; + this._result.defaultIndex = 0; + } else { + this._result.searchResult = ACR.RESULT_NOMATCH; + } + } else { + this._result.searchResult = ACR.RESULT_FAILURE; + this._result.defaultIndex = 0; + } + // const long queryResultStopped = 2; + // const long queryResultError = 3; + this._listener.onSearchResult(this, this._result); + this._listener = null; + }, + + onSearchFoundCard(aCard) { + if (!this._listener) { + return; + } + + for (let emailAddress of aCard.emailAddresses) { + this._addToResult(aCard, emailAddress); + } + + /* XXX autocomplete doesn't expect you to rearrange while searching + if (this._result.matchCount) { + this._result.searchResult = ACR.RESULT_SUCCESS_ONGOING; + } else { + this._result.searchResult = ACR.RESULT_NOMATCH_ONGOING; + } + this._listener.onSearchResult(this, this._result); + */ + }, + + // nsISupports + + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsIAutoCompleteSearch", + "nsIAbDirSearchListener", + ]), +}; diff --git a/comm/mailnews/addrbook/src/components.conf b/comm/mailnews/addrbook/src/components.conf new file mode 100644 index 0000000000..622fba5951 --- /dev/null +++ b/comm/mailnews/addrbook/src/components.conf @@ -0,0 +1,129 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# 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/. + +Classes = [ + { + "cid": "{5b259db2-e451-4de9-8a6f-cfba91402973}", + "contract_ids": ["@mozilla.org/autocomplete/search;1?name=mydomain"], + "jsm": "resource:///modules/AbAutoCompleteMyDomain.jsm", + "constructor": "AbAutoCompleteMyDomain", + }, + { + "cid": "{2f946df9-114c-41fe-8899-81f10daf4f0c}", + "contract_ids": ["@mozilla.org/autocomplete/search;1?name=addrbook"], + "jsm": "resource:///modules/AbAutoCompleteSearch.jsm", + "constructor": "AbAutoCompleteSearch", + }, + { + "cid": "{127b341a-bdda-4270-85e1-edff569a9b85}", + "contract_ids": ["@mozilla.org/addressbook/ldap-attribute-map;1"], + "jsm": "resource:///modules/AbLDAPAttributeMap.jsm", + "constructor": "AbLDAPAttributeMap", + }, + { + "cid": "{4ed7d5e1-8800-40da-9e78-c4f509d7ac5e}", + "contract_ids": ["@mozilla.org/addressbook/ldap-attribute-map-service;1"], + "jsm": "resource:///modules/AbLDAPAttributeMap.jsm", + "constructor": "AbLDAPAttributeMapService", + }, + { + "cid": "{227e6482-fe9f-441f-9b7d-7b60375e7449}", + "contract_ids": ["@mozilla.org/autocomplete/search;1?name=ldap"], + "jsm": "resource:///modules/AbLDAPAutoCompleteSearch.jsm", + "constructor": "AbLDAPAutoCompleteSearch", + }, + { + "cid": "{cb7c67f8-0053-4072-89e9-501cbd1b35ab}", + "contract_ids": ["@mozilla.org/network/ldap-url;1"], + "type": "nsLDAPURL", + "headers": ["/comm/mailnews/addrbook/src/nsLDAPURL.h"], + }, + { + "cid": "{2b722171-2cea-11d3-9e0b-00a0c92b5f0d}", + "contract_ids": ["@mozilla.org/addressbook/cardproperty;1"], + "type": "nsAbCardProperty", + "headers": ["/comm/mailnews/addrbook/src/nsAbCardProperty.h"], + }, + { + "cid": "{6fd8ec67-3965-11d3-a316-001083003d0c}", + "contract_ids": ["@mozilla.org/addressbook/directoryproperty;1"], + "type": "nsAbDirProperty", + "headers": ["/comm/mailnews/addrbook/src/nsAbDirProperty.h"], + }, + { + "cid": "{e7702d5a-99d8-4648-bab7-919ea29f30b6}", + "contract_ids": ["@mozilla.org/addressbook/services/addressCollector;1"], + "type": "nsAbAddressCollector", + "init_method": "Init", + "headers": ["/comm/mailnews/addrbook/src/nsAbAddressCollector.h"], + }, + { + "cid": "{f7dc2aeb-8e62-4750-965c-24b9e09ed8d2}", + "contract_ids": ["@mozilla.org/addressbook/directory/query-arguments;1"], + "type": "nsAbDirectoryQueryArguments", + "headers": ["/comm/mailnews/addrbook/src/nsAbDirectoryQuery.h"], + }, + { + "cid": "{ca1944a9-527e-4c77-895d-d0466dd41cf5}", + "contract_ids": ["@mozilla.org/boolean-expression/condition-string;1"], + "type": "nsAbBooleanConditionString", + "headers": ["/comm/mailnews/addrbook/src/nsAbBooleanExpression.h"], + }, + { + "cid": "{2c2e75c8-6f56-4a50-af1c-72af5d0e8d41}", + "contract_ids": ["@mozilla.org/boolean-expression/n-peer;1"], + "type": "nsAbBooleanExpression", + "headers": ["/comm/mailnews/addrbook/src/nsAbBooleanExpression.h"], + }, + { + "cid": "{e162e335-541b-43b4-aaea-fe591e240caf}", + "contract_ids": ["@mozilla.org/addressbook/directory-query/proxy;1"], + "type": "nsAbDirectoryQueryProxy", + "headers": ["/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h"], + }, + { + "cid": "{db6f46da-8de3-478d-b539-801398656cf6}", + "contract_ids": ["@mozilla.org/addressbook/abldifservice;1"], + "type": "nsAbLDIFService", + "headers": ["/comm/mailnews/addrbook/src/nsAbLDIFService.h"], + }, +] + +if buildconfig.substs["OS_ARCH"] == "Darwin": + Classes += [ + { + "cid": "{83781cc6-c682-11d6-bdeb-0005024967b8}", + "contract_ids": [ + "@mozilla.org/addressbook/directory;1?type=moz-abosxdirectory" + ], + "type": "nsAbOSXDirectory", + "headers": ["/comm/mailnews/addrbook/src/nsAbOSXDirectory.h"], + }, + { + "cid": "{89bbf582-c682-11d6-bc9d-0005024967b8}", + "contract_ids": ["@mozilla.org/addressbook/directory;1?type=moz-abosxcard"], + "type": "nsAbOSXCard", + "headers": ["/comm/mailnews/addrbook/src/nsAbOSXCard.h"], + }, + ] + +if buildconfig.substs["OS_ARCH"] == "WINNT" and buildconfig.substs["MOZ_MAPI_SUPPORT"]: + Classes += [ + { + "cid": "{9cc57822-0599-4c47-a399-1c6fa185a05c}", + "contract_ids": [ + "@mozilla.org/addressbook/directory;1?type=moz-aboutlookdirectory" + ], + "type": "nsAbOutlookDirectory", + "headers": ["/comm/mailnews/addrbook/src/nsAbOutlookDirectory.h"], + }, + { + "cid": "{558ccc0f-2681-4dac-a066-debd8d26faf6}", + "contract_ids": ["@mozilla.org/addressbook/outlookinterface;1"], + "type": "nsAbOutlookInterface", + "headers": ["/comm/mailnews/addrbook/src/nsAbOutlookInterface.h"], + }, + ] diff --git a/comm/mailnews/addrbook/src/moz.build b/comm/mailnews/addrbook/src/moz.build new file mode 100644 index 0000000000..f7fbe7c58f --- /dev/null +++ b/comm/mailnews/addrbook/src/moz.build @@ -0,0 +1,49 @@ +# 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/. + +EXPORTS += [ + "nsAbDirProperty.h", +] + +SOURCES += [ + "nsAbAddressCollector.cpp", + "nsAbBooleanExpression.cpp", + "nsAbCardProperty.cpp", + "nsAbDirectoryQuery.cpp", + "nsAbDirectoryQueryProxy.cpp", + "nsAbDirProperty.cpp", + "nsAbLDIFService.cpp", + "nsAbQueryStringToExpression.cpp", + "nsLDAPURL.cpp", +] + +if CONFIG["OS_ARCH"] == "WINNT" and CONFIG["MOZ_MAPI_SUPPORT"]: + SOURCES += [ + "nsAbOutlookDirectory.cpp", + "nsAbOutlookInterface.cpp", + "nsAbWinHelper.cpp", + "nsMapiAddressBook.cpp", + ] + LOCAL_INCLUDES += ["/comm/mailnews/mapi/include"] + +if CONFIG["OS_ARCH"] == "Darwin": + SOURCES += [ + "nsAbOSXCard.mm", + "nsAbOSXDirectory.mm", + "nsAbOSXUtils.mm", + ] + +EXTRA_JS_MODULES += [ + "AbAutoCompleteMyDomain.jsm", + "AbAutoCompleteSearch.jsm", + "AbLDAPAttributeMap.jsm", + "AbLDAPAutoCompleteSearch.jsm", +] + +FINAL_LIBRARY = "mail" + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/comm/mailnews/addrbook/src/nsAbAddressCollector.cpp b/comm/mailnews/addrbook/src/nsAbAddressCollector.cpp new file mode 100644 index 0000000000..09018daf96 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbAddressCollector.cpp @@ -0,0 +1,281 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "msgCore.h" // for pre-compiled headers +#include "nsISimpleEnumerator.h" + +#include "nsIAbCard.h" +#include "nsAbAddressCollector.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsString.h" +#include "prmem.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsIAbManager.h" +#include "mozilla/mailnews/MimeHeaderParser.h" + +using namespace mozilla::mailnews; + +NS_IMPL_ISUPPORTS(nsAbAddressCollector, nsIAbAddressCollector, nsIObserver) + +#define PREF_MAIL_COLLECT_ADDRESSBOOK "mail.collect_addressbook" + +nsAbAddressCollector::nsAbAddressCollector() {} + +nsAbAddressCollector::~nsAbAddressCollector() { + nsresult rv; + nsCOMPtr<nsIPrefBranch> pPrefBranchInt( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_SUCCEEDED(rv)) + pPrefBranchInt->RemoveObserver(PREF_MAIL_COLLECT_ADDRESSBOOK, this); +} + +/** + * Returns the first card found with the specified email address. This + * returns an already addrefed pointer to the card if the card is found. + */ +already_AddRefed<nsIAbCard> nsAbAddressCollector::GetCardForAddress( + const char* aProperty, const nsACString& aEmailAddress, + nsIAbDirectory** aDirectory) { + nsresult rv; + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsTArray<RefPtr<nsIAbDirectory>> directories; + rv = abManager->GetDirectories(directories); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr<nsIAbCard> result; + uint32_t count = directories.Length(); + for (uint32_t i = 0; i < count; i++) { + // Some implementations may return NS_ERROR_NOT_IMPLEMENTED here, + // so just catch the value and continue. + if (NS_FAILED(directories[i]->GetCardFromProperty( + aProperty, aEmailAddress, false, getter_AddRefs(result)))) { + continue; + } + + if (result) { + if (aDirectory) directories[i].forget(aDirectory); + return result.forget(); + } + } + return nullptr; +} + +NS_IMETHODIMP +nsAbAddressCollector::CollectAddress(const nsACString& aAddresses, + bool aCreateCard) { + // If we've not got a valid directory, no point in going any further + if (!mDirectory) return NS_OK; + + // note that we're now setting the whole recipient list, + // not just the pretty name of the first recipient. + nsTArray<nsCString> names; + nsTArray<nsCString> addresses; + ExtractAllAddresses(EncodedHeader(aAddresses), UTF16ArrayAdapter<>(names), + UTF16ArrayAdapter<>(addresses)); + uint32_t numAddresses = names.Length(); + + for (uint32_t i = 0; i < numAddresses; i++) { + // Don't allow collection of addresses with no email address, it makes + // no sense. Whilst we should never get here in most normal cases, we + // should still be careful. + if (addresses[i].IsEmpty()) continue; + + CollectSingleAddress(addresses[i], names[i], aCreateCard, false); + } + return NS_OK; +} + +NS_IMETHODIMP +nsAbAddressCollector::CollectSingleAddress(const nsACString& aEmail, + const nsACString& aDisplayName, + bool aCreateCard, + bool aSkipCheckExisting) { + if (!mDirectory) return NS_OK; + + nsresult rv; + + nsCOMPtr<nsIAbDirectory> originDirectory; + nsCOMPtr<nsIAbCard> card; + if (!aSkipCheckExisting) { + card = GetCardForAddress(kPriEmailProperty, aEmail, + getter_AddRefs(originDirectory)); + + // If a card has aEmail, but it's the secondary address, we don't want to + // update any properties, so just return. + if (!card) { + card = GetCardForAddress(k2ndEmailProperty, aEmail, + getter_AddRefs(originDirectory)); + if (card) return NS_OK; + } + } + + if (!card && (aCreateCard || aSkipCheckExisting)) { + card = do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv); + if (NS_SUCCEEDED(rv) && card) { + // Set up the fields for the new card. + SetNamesForCard(card, aDisplayName); + AutoCollectScreenName(card, aEmail); + + if (NS_SUCCEEDED(card->SetPrimaryEmail(NS_ConvertUTF8toUTF16(aEmail)))) { + nsCOMPtr<nsIAbCard> addedCard; + rv = mDirectory->AddCard(card, getter_AddRefs(addedCard)); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to add card"); + } + } + } else if (card && originDirectory) { + // It could be that the origin directory is read-only, so don't try and + // write to it if it is. + bool readOnly; + rv = originDirectory->GetReadOnly(&readOnly); + NS_ENSURE_SUCCESS(rv, rv); + + if (readOnly) return NS_OK; + + // address is already in the AB, so update the names + bool modifiedCard = false; + + nsString displayName; + card->GetDisplayName(displayName); + // If we already have a display name, don't set the names on the card. + if (displayName.IsEmpty() && !aDisplayName.IsEmpty()) + modifiedCard = SetNamesForCard(card, aDisplayName); + + if (modifiedCard) originDirectory->ModifyCard(card); + } + + return NS_OK; +} + +// Works out the screen name to put on the card for some well-known addresses +void nsAbAddressCollector::AutoCollectScreenName(nsIAbCard* aCard, + const nsACString& aEmail) { + if (!aCard) return; + + int32_t atPos = aEmail.FindChar('@'); + if (atPos == -1) return; + + const nsACString& domain = Substring(aEmail, atPos + 1); + + if (domain.IsEmpty()) return; + // username in + // username@aol.com (America Online) + // username@cs.com (Compuserve) + // username@netscape.net (Netscape webmail) + // are all AIM screennames. autocollect that info. + if (domain.EqualsLiteral("aol.com") || domain.EqualsLiteral("cs.com") || + domain.EqualsLiteral("netscape.net")) + aCard->SetPropertyAsAUTF8String(kScreenNameProperty, + Substring(aEmail, 0, atPos)); + else if (domain.EqualsLiteral("gmail.com") || + domain.EqualsLiteral("googlemail.com")) + aCard->SetPropertyAsAUTF8String(kGtalkProperty, + Substring(aEmail, 0, atPos)); +} + +// Returns true if the card was modified successfully. +bool nsAbAddressCollector::SetNamesForCard(nsIAbCard* aSenderCard, + const nsACString& aFullName) { + nsCString firstName; + nsCString lastName; + bool modifiedCard = false; + + if (NS_SUCCEEDED( + aSenderCard->SetDisplayName(NS_ConvertUTF8toUTF16(aFullName)))) + modifiedCard = true; + + // Now split up the full name. + SplitFullName(nsCString(aFullName), firstName, lastName); + + if (!firstName.IsEmpty() && + NS_SUCCEEDED(aSenderCard->SetFirstName(NS_ConvertUTF8toUTF16(firstName)))) + modifiedCard = true; + + if (!lastName.IsEmpty() && + NS_SUCCEEDED(aSenderCard->SetLastName(NS_ConvertUTF8toUTF16(lastName)))) + modifiedCard = true; + + if (modifiedCard) aSenderCard->SetPropertyAsBool("PreferDisplayName", false); + + return modifiedCard; +} + +// Splits the first and last name based on the space between them. +void nsAbAddressCollector::SplitFullName(const nsCString& aFullName, + nsCString& aFirstName, + nsCString& aLastName) { + int index = aFullName.RFindChar(' '); + if (index != -1) { + aLastName = Substring(aFullName, index + 1); + aFirstName = Substring(aFullName, 0, index); + } +} + +// Observes the collected address book pref in case it changes. +NS_IMETHODIMP +nsAbAddressCollector::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + nsCOMPtr<nsIPrefBranch> prefBranch = do_QueryInterface(aSubject); + if (!prefBranch) { + NS_ASSERTION(prefBranch, "failed to get prefs"); + return NS_OK; + } + + SetUpAbFromPrefs(prefBranch); + return NS_OK; +} + +// Initialises the collector with the required items. +nsresult nsAbAddressCollector::Init(void) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefBranch->AddObserver(PREF_MAIL_COLLECT_ADDRESSBOOK, this, false); + NS_ENSURE_SUCCESS(rv, rv); + + SetUpAbFromPrefs(prefBranch); + return NS_OK; +} + +// Performs the necessary changes to set up the collector for the specified +// collected address book. +void nsAbAddressCollector::SetUpAbFromPrefs(nsIPrefBranch* aPrefBranch) { + nsCString abURI; + aPrefBranch->GetCharPref(PREF_MAIL_COLLECT_ADDRESSBOOK, abURI); + + if (abURI.IsEmpty()) abURI.AssignLiteral(kPersonalAddressbookUri); + + if (abURI == mABURI) return; + + mDirectory = nullptr; + mABURI = abURI; + + nsresult rv; + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = abManager->GetDirectory(mABURI, getter_AddRefs(mDirectory)); + NS_ENSURE_SUCCESS_VOID(rv); + + bool readOnly; + rv = mDirectory->GetReadOnly(&readOnly); + NS_ENSURE_SUCCESS_VOID(rv); + + // If the directory is read-only, we can't write to it, so just blank it out + // here, and warn because we shouldn't hit this (UI is wrong). + if (readOnly) { + NS_ERROR( + "Address Collection book preferences is set to a read-only book. " + "Address collection will not take place."); + mDirectory = nullptr; + } +} diff --git a/comm/mailnews/addrbook/src/nsAbAddressCollector.h b/comm/mailnews/addrbook/src/nsAbAddressCollector.h new file mode 100644 index 0000000000..a2bfcaf802 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbAddressCollector.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#ifndef _nsAbAddressCollector_H_ +#define _nsAbAddressCollector_H_ + +#include "nsIAbAddressCollector.h" +#include "nsCOMPtr.h" +#include "nsIAbDirectory.h" +#include "nsIAbCard.h" +#include "nsIObserver.h" +#include "nsString.h" + +class nsIPrefBranch; + +class nsAbAddressCollector : public nsIAbAddressCollector, public nsIObserver { + public: + nsAbAddressCollector(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIABADDRESSCOLLECTOR + NS_DECL_NSIOBSERVER + + nsresult Init(); + + private: + virtual ~nsAbAddressCollector(); + already_AddRefed<nsIAbCard> GetCardForAddress(const char* aProperty, + const nsACString& aEmailAddress, + nsIAbDirectory** aDirectory); + void AutoCollectScreenName(nsIAbCard* aCard, const nsACString& aEmail); + bool SetNamesForCard(nsIAbCard* aSenderCard, const nsACString& aFullName); + void SplitFullName(const nsCString& aFullName, nsCString& aFirstName, + nsCString& aLastName); + void SetUpAbFromPrefs(nsIPrefBranch* aPrefBranch); + nsCOMPtr<nsIAbDirectory> mDirectory; + nsCString mABURI; +}; + +#endif // _nsAbAddressCollector_H_ diff --git a/comm/mailnews/addrbook/src/nsAbBooleanExpression.cpp b/comm/mailnews/addrbook/src/nsAbBooleanExpression.cpp new file mode 100644 index 0000000000..434c8756e6 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbBooleanExpression.cpp @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsAbBooleanExpression.h" +#include "nsComponentManagerUtils.h" + +NS_IMPL_ISUPPORTS(nsAbBooleanConditionString, nsIAbBooleanConditionString) + +nsAbBooleanConditionString::nsAbBooleanConditionString() + : mCondition(nsIAbBooleanConditionTypes::Exists) {} + +nsAbBooleanConditionString::~nsAbBooleanConditionString() {} + +/* attribute nsAbBooleanConditionType condition; */ +NS_IMETHODIMP nsAbBooleanConditionString::GetCondition( + nsAbBooleanConditionType* aCondition) { + if (!aCondition) return NS_ERROR_NULL_POINTER; + + *aCondition = mCondition; + + return NS_OK; +} +NS_IMETHODIMP nsAbBooleanConditionString::SetCondition( + nsAbBooleanConditionType aCondition) { + mCondition = aCondition; + + return NS_OK; +} + +/* attribute string name; */ +NS_IMETHODIMP nsAbBooleanConditionString::GetName(char** aName) { + if (!aName) return NS_ERROR_NULL_POINTER; + + *aName = mName.IsEmpty() ? 0 : ToNewCString(mName); + + return NS_OK; +} +NS_IMETHODIMP nsAbBooleanConditionString::SetName(const char* aName) { + if (!aName) return NS_ERROR_NULL_POINTER; + + mName = aName; + + return NS_OK; +} + +/* attribute wstring value; */ +NS_IMETHODIMP nsAbBooleanConditionString::GetValue(char16_t** aValue) { + if (!aValue) return NS_ERROR_NULL_POINTER; + + *aValue = ToNewUnicode(mValue); + + return NS_OK; +} +NS_IMETHODIMP nsAbBooleanConditionString::SetValue(const char16_t* aValue) { + if (!aValue) return NS_ERROR_NULL_POINTER; + + mValue = aValue; + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsAbBooleanExpression, nsIAbBooleanExpression) + +nsAbBooleanExpression::nsAbBooleanExpression() + : mOperation(nsIAbBooleanOperationTypes::AND) {} + +nsAbBooleanExpression::~nsAbBooleanExpression() {} + +/* attribute nsAbBooleanOperationType operation; */ +NS_IMETHODIMP nsAbBooleanExpression::GetOperation( + nsAbBooleanOperationType* aOperation) { + if (!aOperation) return NS_ERROR_NULL_POINTER; + + *aOperation = mOperation; + + return NS_OK; +} +NS_IMETHODIMP nsAbBooleanExpression::SetOperation( + nsAbBooleanOperationType aOperation) { + mOperation = aOperation; + + return NS_OK; +} + +/* attribute Array<nsISupports> expressions; */ +NS_IMETHODIMP nsAbBooleanExpression::GetExpressions( + nsTArray<RefPtr<nsISupports>>& aExpressions) { + aExpressions = mExpressions.Clone(); + return NS_OK; +} + +NS_IMETHODIMP nsAbBooleanExpression::SetExpressions( + const nsTArray<RefPtr<nsISupports>>& aExpressions) { + mExpressions = aExpressions.Clone(); + return NS_OK; +} diff --git a/comm/mailnews/addrbook/src/nsAbBooleanExpression.h b/comm/mailnews/addrbook/src/nsAbBooleanExpression.h new file mode 100644 index 0000000000..c739e0c2df --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbBooleanExpression.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAbBooleanExpression_h__ +#define nsAbBooleanExpression_h__ + +#include "nsIAbBooleanExpression.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIArray.h" + +class nsAbBooleanConditionString : public nsIAbBooleanConditionString { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIABBOOLEANCONDITIONSTRING + + nsAbBooleanConditionString(); + + protected: + virtual ~nsAbBooleanConditionString(); + nsAbBooleanConditionType mCondition; + nsCString mName; + nsString mValue; +}; + +class nsAbBooleanExpression : public nsIAbBooleanExpression { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIABBOOLEANEXPRESSION + + nsAbBooleanExpression(); + + protected: + virtual ~nsAbBooleanExpression(); + nsAbBooleanOperationType mOperation; + nsTArray<RefPtr<nsISupports>> mExpressions; +}; + +#endif diff --git a/comm/mailnews/addrbook/src/nsAbCardProperty.cpp b/comm/mailnews/addrbook/src/nsAbCardProperty.cpp new file mode 100644 index 0000000000..cc34655afd --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbCardProperty.cpp @@ -0,0 +1,1004 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsAbCardProperty.h" +#include "nsIPrefService.h" +#include "nsIAbDirectory.h" +#include "plbase64.h" +#include "nsIStringBundle.h" +#include "plstr.h" +#include "nsMsgUtils.h" +#include "nsINetUtil.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMemory.h" +#include "mozITXTToHTMLConv.h" +#include "nsIAbManager.h" +#include "nsIUUIDGenerator.h" +#include "nsIMsgVCardService.h" +#include "nsVariant.h" +#include "nsIProperty.h" +#include "nsCOMArray.h" +#include "prmem.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Components.h" +using namespace mozilla; + +#define PREF_MAIL_ADDR_BOOK_LASTNAMEFIRST "mail.addr_book.lastnamefirst" + +const char sAddrbookProperties[] = + "chrome://messenger/locale/addressbook/addressBook.properties"; + +enum EAppendType { + eAppendLine, + eAppendLabel, + eAppendCityStateZip, + eAppendUndefined +}; + +struct AppendItem { + const char* mColumn; + const char* mLabel; + EAppendType mAppendType; +}; + +static const AppendItem NAME_ATTRS_ARRAY[] = { + {kDisplayNameProperty, "propertyDisplayName", eAppendLabel}, + {kNicknameProperty, "propertyNickname", eAppendLabel}, + {kPriEmailProperty, "", eAppendLine}, + {k2ndEmailProperty, "", eAppendLine}}; + +static const AppendItem PHONE_ATTRS_ARRAY[] = { + {kWorkPhoneProperty, "propertyWork", eAppendLabel}, + {kHomePhoneProperty, "propertyHome", eAppendLabel}, + {kFaxProperty, "propertyFax", eAppendLabel}, + {kPagerProperty, "propertyPager", eAppendLabel}, + {kCellularProperty, "propertyCellular", eAppendLabel}}; + +static const AppendItem HOME_ATTRS_ARRAY[] = { + {kHomeAddressProperty, "", eAppendLine}, + {kHomeAddress2Property, "", eAppendLine}, + {kHomeCityProperty, "", eAppendCityStateZip}, + {kHomeCountryProperty, "", eAppendLine}, + {kHomeWebPageProperty, "", eAppendLine}}; + +static const AppendItem WORK_ATTRS_ARRAY[] = { + {kJobTitleProperty, "", eAppendLine}, + {kDepartmentProperty, "", eAppendLine}, + {kCompanyProperty, "", eAppendLine}, + {kWorkAddressProperty, "", eAppendLine}, + {kWorkAddress2Property, "", eAppendLine}, + {kWorkCityProperty, "", eAppendCityStateZip}, + {kWorkCountryProperty, "", eAppendLine}, + {kWorkWebPageProperty, "", eAppendLine}}; + +static const AppendItem CUSTOM_ATTRS_ARRAY[] = { + {kCustom1Property, "propertyCustom1", eAppendLabel}, + {kCustom2Property, "propertyCustom2", eAppendLabel}, + {kCustom3Property, "propertyCustom3", eAppendLabel}, + {kCustom4Property, "propertyCustom4", eAppendLabel}, + {kNotesProperty, "", eAppendLine}}; + +static const AppendItem CHAT_ATTRS_ARRAY[] = { + {kGtalkProperty, "propertyGtalk", eAppendLabel}, + {kAIMProperty, "propertyAIM", eAppendLabel}, + {kYahooProperty, "propertyYahoo", eAppendLabel}, + {kSkypeProperty, "propertySkype", eAppendLabel}, + {kQQProperty, "propertyQQ", eAppendLabel}, + {kMSNProperty, "propertyMSN", eAppendLabel}, + {kICQProperty, "propertyICQ", eAppendLabel}, + {kXMPPProperty, "propertyXMPP", eAppendLabel}, + {kIRCProperty, "propertyIRC", eAppendLabel}}; + +nsAbCardProperty::nsAbCardProperty() : m_IsMailList(false) { + // Initialize some default properties + SetPropertyAsUint32(kPopularityIndexProperty, 0); + // Uninitialized... + SetPropertyAsUint32(kLastModifiedDateProperty, 0); +} + +nsAbCardProperty::~nsAbCardProperty(void) {} + +NS_IMPL_ISUPPORTS(nsAbCardProperty, nsIAbCard) + +NS_IMETHODIMP nsAbCardProperty::GetDirectoryUID(nsACString& dirUID) { + dirUID = m_directoryUID; + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::SetDirectoryUID(const nsACString& aDirUID) { + m_directoryUID = aDirUID; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsAbCardProperty::GetIsMailList(bool* aIsMailList) { + *aIsMailList = m_IsMailList; + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::SetIsMailList(bool aIsMailList) { + m_IsMailList = aIsMailList; + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GetMailListURI(char** aMailListURI) { + if (aMailListURI) { + *aMailListURI = ToNewCString(m_MailListURI); + return (*aMailListURI) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + } else + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsAbCardProperty::SetMailListURI(const char* aMailListURI) { + if (aMailListURI) { + m_MailListURI = aMailListURI; + return NS_OK; + } else + return NS_ERROR_NULL_POINTER; +} + +/////////////////////////////////////////////////////////////////////////////// +// Property bag portion of nsAbCardProperty +/////////////////////////////////////////////////////////////////////////////// + +class nsAbSimpleProperty final : public nsIProperty { + public: + nsAbSimpleProperty(const nsACString& aName, nsIVariant* aValue) + : mName(aName), mValue(aValue) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIPROPERTY + protected: + ~nsAbSimpleProperty() {} + nsCString mName; + nsCOMPtr<nsIVariant> mValue; +}; + +NS_IMPL_ISUPPORTS(nsAbSimpleProperty, nsIProperty) + +NS_IMETHODIMP +nsAbSimpleProperty::GetName(nsAString& aName) { + aName.Assign(NS_ConvertUTF8toUTF16(mName)); + return NS_OK; +} + +NS_IMETHODIMP +nsAbSimpleProperty::GetValue(nsIVariant** aValue) { + NS_IF_ADDREF(*aValue = mValue); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GetProperties( + nsTArray<RefPtr<nsIProperty>>& props) { + props.ClearAndRetainStorage(); + props.SetCapacity(m_properties.Count()); + for (auto iter = m_properties.Iter(); !iter.Done(); iter.Next()) { + props.AppendElement(new nsAbSimpleProperty(iter.Key(), iter.UserData())); + } + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GetProperty(const nsACString& name, + nsIVariant* defaultValue, + nsIVariant** value) { + if (!m_properties.Get(name, value)) NS_ADDREF(*value = defaultValue); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GetPropertyAsAString(const char* name, + nsAString& value) { + NS_ENSURE_ARG_POINTER(name); + + nsCOMPtr<nsIVariant> variant; + return m_properties.Get(nsDependentCString(name), getter_AddRefs(variant)) + ? variant->GetAsAString(value) + : NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsAbCardProperty::GetPropertyAsAUTF8String(const char* name, + nsACString& value) { + NS_ENSURE_ARG_POINTER(name); + + nsCOMPtr<nsIVariant> variant; + return m_properties.Get(nsDependentCString(name), getter_AddRefs(variant)) + ? variant->GetAsAUTF8String(value) + : NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsAbCardProperty::GetPropertyAsUint32(const char* name, + uint32_t* value) { + NS_ENSURE_ARG_POINTER(name); + + nsCOMPtr<nsIVariant> variant; + return m_properties.Get(nsDependentCString(name), getter_AddRefs(variant)) + ? variant->GetAsUint32(value) + : NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsAbCardProperty::GetPropertyAsBool(const char* name, + bool defaultValue, + bool* value) { + NS_ENSURE_ARG_POINTER(name); + + *value = defaultValue; + + nsCOMPtr<nsIVariant> variant; + return m_properties.Get(nsDependentCString(name), getter_AddRefs(variant)) + ? variant->GetAsBool(value) + : NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::SetProperty(const nsACString& name, + nsIVariant* value) { + m_properties.InsertOrUpdate(name, value); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::SetPropertyAsAString(const char* name, + const nsAString& value) { + NS_ENSURE_ARG_POINTER(name); + + nsCOMPtr<nsIWritableVariant> variant = new nsVariant(); + variant->SetAsAString(value); + m_properties.InsertOrUpdate(nsDependentCString(name), variant); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::SetPropertyAsAUTF8String( + const char* name, const nsACString& value) { + NS_ENSURE_ARG_POINTER(name); + + nsCOMPtr<nsIWritableVariant> variant = new nsVariant(); + variant->SetAsAUTF8String(value); + m_properties.InsertOrUpdate(nsDependentCString(name), variant); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::SetPropertyAsUint32(const char* name, + uint32_t value) { + NS_ENSURE_ARG_POINTER(name); + + nsCOMPtr<nsIWritableVariant> variant = new nsVariant(); + variant->SetAsUint32(value); + m_properties.InsertOrUpdate(nsDependentCString(name), variant); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::SetPropertyAsBool(const char* name, + bool value) { + NS_ENSURE_ARG_POINTER(name); + + nsCOMPtr<nsIWritableVariant> variant = new nsVariant(); + variant->SetAsBool(value); + m_properties.InsertOrUpdate(nsDependentCString(name), variant); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::DeleteProperty(const nsACString& name) { + m_properties.Remove(name); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GetSupportsVCard(bool* aSupportsVCard) { + *aSupportsVCard = false; + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GetVCardProperties( + JS::MutableHandle<JS::Value> properties) { + properties.setNull(); + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GetUID(nsACString& uid) { + nsAutoString aString; + nsresult rv = GetPropertyAsAString(kUIDProperty, aString); + if (NS_SUCCEEDED(rv)) { + uid = NS_ConvertUTF16toUTF8(aString); + return rv; + } + + nsCOMPtr<nsIUUIDGenerator> uuidgen = + mozilla::components::UUIDGenerator::Service(); + NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE); + + nsID id; + rv = uuidgen->GenerateUUIDInPlace(&id); + NS_ENSURE_SUCCESS(rv, rv); + + char idString[NSID_LENGTH]; + id.ToProvidedString(idString); + + uid.AppendASCII(idString + 1, NSID_LENGTH - 3); + return SetUID(uid); +} + +NS_IMETHODIMP nsAbCardProperty::SetUID(const nsACString& aUID) { + nsAutoString aString; + nsresult rv = GetPropertyAsAString(kUIDProperty, aString); + if (NS_SUCCEEDED(rv)) { + if (!aString.Equals(NS_ConvertUTF8toUTF16(aUID))) { + return NS_ERROR_FAILURE; + } + } + + rv = SetPropertyAsAString(kUIDProperty, NS_ConvertUTF8toUTF16(aUID)); + NS_ENSURE_SUCCESS(rv, rv); + + if (m_directoryUID.IsEmpty()) { + // This card's not in a directory. + return NS_OK; + } + + nsCOMPtr<nsIAbManager> abManager = + do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbDirectory> directory = nullptr; + rv = + abManager->GetDirectoryFromUID(m_directoryUID, getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!directory) { + // This card claims to be in a directory, but we can't find it. + return NS_OK; + } + + bool readOnly; + rv = directory->GetReadOnly(&readOnly); + NS_ENSURE_SUCCESS(rv, rv); + + if (readOnly) { + // The directory is read-only. + return NS_OK; + } + + // Save the new UID so we can use it again in the future. + return directory->ModifyCard(this); +} + +NS_IMETHODIMP nsAbCardProperty::GetFirstName(nsAString& aString) { + nsresult rv = GetPropertyAsAString(kFirstNameProperty, aString); + if (rv == NS_ERROR_NOT_AVAILABLE) { + aString.Truncate(); + return NS_OK; + } + return rv; +} + +NS_IMETHODIMP nsAbCardProperty::SetFirstName(const nsAString& aString) { + return SetPropertyAsAString(kFirstNameProperty, aString); +} + +NS_IMETHODIMP nsAbCardProperty::GetLastName(nsAString& aString) { + nsresult rv = GetPropertyAsAString(kLastNameProperty, aString); + if (rv == NS_ERROR_NOT_AVAILABLE) { + aString.Truncate(); + return NS_OK; + } + return rv; +} + +NS_IMETHODIMP nsAbCardProperty::SetLastName(const nsAString& aString) { + return SetPropertyAsAString(kLastNameProperty, aString); +} + +NS_IMETHODIMP nsAbCardProperty::GetDisplayName(nsAString& aString) { + nsresult rv = GetPropertyAsAString(kDisplayNameProperty, aString); + if (rv == NS_ERROR_NOT_AVAILABLE) { + aString.Truncate(); + return NS_OK; + } + return rv; +} + +NS_IMETHODIMP nsAbCardProperty::SetDisplayName(const nsAString& aString) { + return SetPropertyAsAString(kDisplayNameProperty, aString); +} + +NS_IMETHODIMP nsAbCardProperty::GetPrimaryEmail(nsAString& aString) { + nsresult rv = GetPropertyAsAString(kPriEmailProperty, aString); + if (rv == NS_ERROR_NOT_AVAILABLE) { + aString.Truncate(); + return NS_OK; + } + return rv; +} + +NS_IMETHODIMP nsAbCardProperty::SetPrimaryEmail(const nsAString& aString) { + return SetPropertyAsAString(kPriEmailProperty, aString); +} + +NS_IMETHODIMP nsAbCardProperty::GetEmailAddresses( + nsTArray<nsString>& aEmailAddresses) { + aEmailAddresses.Clear(); + + nsresult rv; + nsString emailAddress; + + rv = GetPropertyAsAString(kPriEmailProperty, emailAddress); + if (rv != NS_ERROR_NOT_AVAILABLE && !emailAddress.IsEmpty()) { + aEmailAddresses.AppendElement(emailAddress); + } + + rv = GetPropertyAsAString(k2ndEmailProperty, emailAddress); + if (rv != NS_ERROR_NOT_AVAILABLE && !emailAddress.IsEmpty()) { + aEmailAddresses.AppendElement(emailAddress); + } + + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::HasEmailAddress(const nsACString& aEmailAddress, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = false; + + nsCString emailAddress; + nsresult rv = GetPropertyAsAUTF8String(kPriEmailProperty, emailAddress); + if (rv != NS_ERROR_NOT_AVAILABLE && + emailAddress.Equals(aEmailAddress, nsCaseInsensitiveCStringComparator)) { + *aResult = true; + return NS_OK; + } + + rv = GetPropertyAsAUTF8String(k2ndEmailProperty, emailAddress); + if (rv != NS_ERROR_NOT_AVAILABLE && + emailAddress.Equals(aEmailAddress, nsCaseInsensitiveCStringComparator)) + *aResult = true; + + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GetPhotoURL(nsAString& aPhotoURL) { + aPhotoURL.Truncate(); + return NS_OK; +} + +// This function may be overridden by derived classes for +// nsAb*Card specific implementations. +NS_IMETHODIMP nsAbCardProperty::Copy(nsIAbCard* srcCard) { + NS_ENSURE_ARG_POINTER(srcCard); + + nsTArray<RefPtr<nsIProperty>> properties; + nsresult rv = srcCard->GetProperties(properties); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIProperty* property : properties) { + nsAutoString name; + property->GetName(name); + nsCOMPtr<nsIVariant> value; + property->GetValue(getter_AddRefs(value)); + + SetProperty(NS_ConvertUTF16toUTF8(name), value); + } + + bool isMailList; + srcCard->GetIsMailList(&isMailList); + SetIsMailList(isMailList); + + nsCString mailListURI; + srcCard->GetMailListURI(getter_Copies(mailListURI)); + SetMailListURI(mailListURI.get()); + + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::Equals(nsIAbCard* card, bool* result) { + *result = (card == this); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// The following methods are other views of a card +//////////////////////////////////////////////////////////////////////////////// + +// XXX: Use the category manager instead of this file to implement these +NS_IMETHODIMP nsAbCardProperty::TranslateTo(const nsACString& type, + nsACString& result) { + if (type.EqualsLiteral("base64xml")) { + return ConvertToBase64EncodedXML(result); + } else if (type.EqualsLiteral("xml")) { + nsString utf16String; + nsresult rv = ConvertToXMLPrintData(utf16String); + NS_ENSURE_SUCCESS(rv, rv); + result = NS_ConvertUTF16toUTF8(utf16String); + return NS_OK; + } else if (type.EqualsLiteral("vcard")) { + return ConvertToEscapedVCard(result); + } + + return NS_ERROR_ILLEGAL_VALUE; +} + +nsresult nsAbCardProperty::ConvertToEscapedVCard(nsACString& aResult) { + nsresult rv; + nsCOMPtr<nsIMsgVCardService> vCardService = + do_GetService("@mozilla.org/addressbook/msgvcardservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString result; + rv = vCardService->AbCardToEscapedVCard(this, result); + NS_ENSURE_SUCCESS(rv, rv); + + aResult = NS_ConvertUTF16toUTF8(result); + return NS_OK; +} + +nsresult nsAbCardProperty::ConvertToBase64EncodedXML(nsACString& result) { + nsresult rv; + nsString xmlStr; + + xmlStr.AppendLiteral( + "<?xml version=\"1.0\"?>\n" + "<?xml-stylesheet type=\"text/css\" " + "href=\"chrome://messagebody/skin/abPrint.css\"?>\n" + "<directory>\n"); + + // Get Address Book string and set it as title of XML document + nsCOMPtr<nsIStringBundle> bundle; + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::components::StringBundle::Service(); + if (stringBundleService) { + rv = stringBundleService->CreateBundle(sAddrbookProperties, + getter_AddRefs(bundle)); + if (NS_SUCCEEDED(rv)) { + nsString addrBook; + rv = bundle->GetStringFromName("addressBook", addrBook); + if (NS_SUCCEEDED(rv)) { + xmlStr.AppendLiteral("<title xmlns=\"http://www.w3.org/1999/xhtml\">"); + xmlStr.Append(addrBook); + xmlStr.AppendLiteral("</title>\n"); + } + } + } + + nsString xmlSubstr; + rv = ConvertToXMLPrintData(xmlSubstr); + NS_ENSURE_SUCCESS(rv, rv); + + xmlStr.Append(xmlSubstr); + xmlStr.AppendLiteral("</directory>\n"); + + char* tmpRes = + PL_Base64Encode(NS_ConvertUTF16toUTF8(xmlStr).get(), 0, nullptr); + result.Assign(tmpRes); + PR_Free(tmpRes); + return NS_OK; +} + +nsresult nsAbCardProperty::ConvertToXMLPrintData(nsAString& aXMLSubstr) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t generatedNameFormat; + rv = prefBranch->GetIntPref(PREF_MAIL_ADDR_BOOK_LASTNAMEFIRST, + &generatedNameFormat); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(stringBundleService, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIStringBundle> bundle; + rv = stringBundleService->CreateBundle(sAddrbookProperties, + getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString generatedName; + rv = GenerateName(generatedNameFormat, bundle, generatedName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<mozITXTToHTMLConv> conv = + do_CreateInstance(MOZ_TXTTOHTMLCONV_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsString xmlStr; + xmlStr.SetLength( + 4096); // to reduce allocations. should be enough for most cards + xmlStr.AssignLiteral("<GeneratedName>\n"); + + // use ScanTXT to convert < > & to safe values. + nsString safeText; + if (!generatedName.IsEmpty()) { + rv = conv->ScanTXT(generatedName, mozITXTToHTMLConv::kEntities, safeText); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (safeText.IsEmpty()) { + nsAutoString primaryEmail; + GetPrimaryEmail(primaryEmail); + + // use ScanTXT to convert < > & to safe values. + rv = conv->ScanTXT(primaryEmail, mozITXTToHTMLConv::kEntities, safeText); + NS_ENSURE_SUCCESS(rv, rv); + } + xmlStr.Append(safeText); + + xmlStr.AppendLiteral( + "</GeneratedName>\n" + "<table><tr><td>"); + + rv = AppendSection(NAME_ATTRS_ARRAY, + sizeof(NAME_ATTRS_ARRAY) / sizeof(AppendItem), + EmptyString(), bundle, conv, xmlStr); + + xmlStr.AppendLiteral("</td></tr><tr><td>"); + + rv = AppendSection(PHONE_ATTRS_ARRAY, + sizeof(PHONE_ATTRS_ARRAY) / sizeof(AppendItem), + u"headingPhone"_ns, bundle, conv, xmlStr); + + if (!m_IsMailList) { + rv = AppendSection(CUSTOM_ATTRS_ARRAY, + sizeof(CUSTOM_ATTRS_ARRAY) / sizeof(AppendItem), + u"headingOther"_ns, bundle, conv, xmlStr); + rv = AppendSection(CHAT_ATTRS_ARRAY, + sizeof(CHAT_ATTRS_ARRAY) / sizeof(AppendItem), + u"headingChat"_ns, bundle, conv, xmlStr); + } else { + rv = AppendSection(CUSTOM_ATTRS_ARRAY, + sizeof(CUSTOM_ATTRS_ARRAY) / sizeof(AppendItem), + u"headingDescription"_ns, bundle, conv, xmlStr); + + xmlStr.AppendLiteral("<section><sectiontitle>"); + + nsString headingAddresses; + rv = bundle->GetStringFromName("headingAddresses", headingAddresses); + NS_ENSURE_SUCCESS(rv, rv); + + xmlStr.Append(headingAddresses); + xmlStr.AppendLiteral("</sectiontitle>"); + + nsCOMPtr<nsIAbManager> abManager = + do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbDirectory> mailList = nullptr; + rv = abManager->GetDirectory(m_MailListURI, getter_AddRefs(mailList)); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIAbCard>> mailListAddresses; + rv = mailList->GetChildCards(mailListAddresses); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIAbCard* listCard : mailListAddresses) { + xmlStr.AppendLiteral("<PrimaryEmail>\n"); + + nsAutoString displayName; + rv = listCard->GetDisplayName(displayName); + NS_ENSURE_SUCCESS(rv, rv); + + // use ScanTXT to convert < > & to safe values. + nsString safeText; + rv = conv->ScanTXT(displayName, mozITXTToHTMLConv::kEntities, safeText); + NS_ENSURE_SUCCESS(rv, rv); + xmlStr.Append(safeText); + + xmlStr.AppendLiteral(" <"); + + nsAutoString primaryEmail; + listCard->GetPrimaryEmail(primaryEmail); + + // use ScanTXT to convert < > & to safe values. + nsString safeText2; + rv = conv->ScanTXT(primaryEmail, mozITXTToHTMLConv::kEntities, safeText2); + NS_ENSURE_SUCCESS(rv, rv); + xmlStr.Append(safeText2); + + xmlStr.AppendLiteral("></PrimaryEmail>\n"); + } + xmlStr.AppendLiteral("</section>"); + } + + xmlStr.AppendLiteral("</td><td>"); + + rv = AppendSection(HOME_ATTRS_ARRAY, + sizeof(HOME_ATTRS_ARRAY) / sizeof(AppendItem), + u"headingHome"_ns, bundle, conv, xmlStr); + rv = AppendSection(WORK_ATTRS_ARRAY, + sizeof(WORK_ATTRS_ARRAY) / sizeof(AppendItem), + u"headingWork"_ns, bundle, conv, xmlStr); + + xmlStr.AppendLiteral("</td></tr></table>"); + + aXMLSubstr = xmlStr; + + return NS_OK; +} + +nsresult nsAbCardProperty::AppendSection( + const AppendItem* aArray, int16_t aCount, const nsString& aHeading, + nsIStringBundle* aBundle, mozITXTToHTMLConv* aConv, nsString& aResult) { + nsresult rv = NS_OK; + + aResult.AppendLiteral("<section>"); + + nsString attrValue; + bool sectionIsEmpty = true; + + int16_t i = 0; + for (i = 0; i < aCount; i++) { + rv = GetPropertyAsAString(aArray[i].mColumn, attrValue); + if (NS_SUCCEEDED(rv) && !attrValue.IsEmpty()) sectionIsEmpty = false; + } + + if (!sectionIsEmpty && !aHeading.IsEmpty()) { + nsString heading; + rv = aBundle->GetStringFromName(NS_ConvertUTF16toUTF8(aHeading).get(), + heading); + NS_ENSURE_SUCCESS(rv, rv); + + aResult.AppendLiteral("<sectiontitle>"); + aResult.Append(heading); + aResult.AppendLiteral("</sectiontitle>"); + } + + for (i = 0; i < aCount; i++) { + switch (aArray[i].mAppendType) { + case eAppendLine: + rv = AppendLine(aArray[i], aConv, aResult); + break; + case eAppendLabel: + rv = AppendLabel(aArray[i], aBundle, aConv, aResult); + break; + case eAppendCityStateZip: + rv = AppendCityStateZip(aArray[i], aBundle, aConv, aResult); + break; + default: + rv = NS_ERROR_FAILURE; + break; + } + + if (NS_FAILED(rv)) { + NS_WARNING("append item failed"); + break; + } + } + aResult.AppendLiteral("</section>"); + + return rv; +} + +nsresult nsAbCardProperty::AppendLine(const AppendItem& aItem, + mozITXTToHTMLConv* aConv, + nsString& aResult) { + NS_ENSURE_ARG_POINTER(aConv); + + nsString attrValue; + nsresult rv = GetPropertyAsAString(aItem.mColumn, attrValue); + + if (NS_FAILED(rv) || attrValue.IsEmpty()) return NS_OK; + + aResult.Append(char16_t('<')); + aResult.Append(NS_ConvertUTF8toUTF16(aItem.mColumn)); + aResult.Append(char16_t('>')); + + // use ScanTXT to convert < > & to safe values. + nsString safeText; + rv = aConv->ScanTXT(attrValue, mozITXTToHTMLConv::kEntities, safeText); + NS_ENSURE_SUCCESS(rv, rv); + aResult.Append(safeText); + + aResult.AppendLiteral("</"); + aResult.Append(NS_ConvertUTF8toUTF16(aItem.mColumn)); + aResult.Append(char16_t('>')); + + return NS_OK; +} + +nsresult nsAbCardProperty::AppendLabel(const AppendItem& aItem, + nsIStringBundle* aBundle, + mozITXTToHTMLConv* aConv, + nsString& aResult) { + NS_ENSURE_ARG_POINTER(aBundle); + + nsresult rv; + nsString label, attrValue; + + rv = GetPropertyAsAString(aItem.mColumn, attrValue); + + if (NS_FAILED(rv) || attrValue.IsEmpty()) return NS_OK; + + rv = aBundle->GetStringFromName(aItem.mLabel, label); + NS_ENSURE_SUCCESS(rv, rv); + + aResult.AppendLiteral("<labelrow><label>"); + + aResult.Append(label); + aResult.AppendLiteral(": </label>"); + + rv = AppendLine(aItem, aConv, aResult); + NS_ENSURE_SUCCESS(rv, rv); + + aResult.AppendLiteral("</labelrow>"); + + return NS_OK; +} + +nsresult nsAbCardProperty::AppendCityStateZip(const AppendItem& aItem, + nsIStringBundle* aBundle, + mozITXTToHTMLConv* aConv, + nsString& aResult) { + NS_ENSURE_ARG_POINTER(aBundle); + + nsresult rv; + AppendItem item; + const char *statePropName, *zipPropName; + + if (strcmp(aItem.mColumn, kHomeCityProperty) == 0) { + statePropName = kHomeStateProperty; + zipPropName = kHomeZipCodeProperty; + } else { + statePropName = kWorkStateProperty; + zipPropName = kWorkZipCodeProperty; + } + + nsAutoString cityResult, stateResult, zipResult; + + rv = AppendLine(aItem, aConv, cityResult); + NS_ENSURE_SUCCESS(rv, rv); + + item.mColumn = statePropName; + item.mLabel = ""; + item.mAppendType = eAppendUndefined; + + rv = AppendLine(item, aConv, stateResult); + NS_ENSURE_SUCCESS(rv, rv); + + item.mColumn = zipPropName; + + rv = AppendLine(item, aConv, zipResult); + NS_ENSURE_SUCCESS(rv, rv); + + nsString formattedString; + + if (!cityResult.IsEmpty() && !stateResult.IsEmpty() && !zipResult.IsEmpty()) { + AutoTArray<nsString, 3> formatStrings = {cityResult, stateResult, + zipResult}; + rv = aBundle->FormatStringFromName("cityAndStateAndZip", formatStrings, + formattedString); + NS_ENSURE_SUCCESS(rv, rv); + } else if (!cityResult.IsEmpty() && !stateResult.IsEmpty() && + zipResult.IsEmpty()) { + AutoTArray<nsString, 2> formatStrings = {cityResult, stateResult}; + rv = aBundle->FormatStringFromName("cityAndStateNoZip", formatStrings, + formattedString); + NS_ENSURE_SUCCESS(rv, rv); + } else if ((!cityResult.IsEmpty() && stateResult.IsEmpty() && + !zipResult.IsEmpty()) || + (cityResult.IsEmpty() && !stateResult.IsEmpty() && + !zipResult.IsEmpty())) { + AutoTArray<nsString, 2> formatStrings = { + cityResult.IsEmpty() ? stateResult : cityResult, zipResult}; + rv = aBundle->FormatStringFromName("cityOrStateAndZip", formatStrings, + formattedString); + NS_ENSURE_SUCCESS(rv, rv); + } else { + if (!cityResult.IsEmpty()) + formattedString = cityResult; + else if (!stateResult.IsEmpty()) + formattedString = stateResult; + else + formattedString = zipResult; + } + + aResult.Append(formattedString); + + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GenerateName(int32_t aGenerateFormat, + nsIStringBundle* aBundle, + nsAString& aResult) { + aResult.Truncate(); + + // Cache the first and last names + nsAutoString firstName, lastName; + GetFirstName(firstName); + GetLastName(lastName); + + // No need to check for aBundle present straight away, only do that if we're + // actually going to use it. + if (aGenerateFormat == GENERATE_DISPLAY_NAME) + GetDisplayName(aResult); + else if (lastName.IsEmpty()) + aResult = firstName; + else if (firstName.IsEmpty()) + aResult = lastName; + else { + nsresult rv; + nsCOMPtr<nsIStringBundle> bundle(aBundle); + if (!bundle) { + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(stringBundleService, NS_ERROR_UNEXPECTED); + + rv = stringBundleService->CreateBundle(sAddrbookProperties, + getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsString result; + + if (aGenerateFormat == GENERATE_LAST_FIRST_ORDER) { + AutoTArray<nsString, 2> stringParams = {lastName, firstName}; + + rv = + bundle->FormatStringFromName("lastFirstFormat", stringParams, result); + } else { + AutoTArray<nsString, 2> stringParams = {firstName, lastName}; + + rv = + bundle->FormatStringFromName("firstLastFormat", stringParams, result); + } + NS_ENSURE_SUCCESS(rv, rv); + + aResult.Assign(result); + } + + if (aResult.IsEmpty()) { + // The normal names have failed, does this card have a company name? If so, + // use that instead, because that is likely to be more meaningful than an + // email address. + // + // If this errors, the string isn't found and we'll fall into the next + // check. + (void)GetPropertyAsAString(kCompanyProperty, aResult); + } + + if (aResult.IsEmpty()) { + // see bug #211078 + // if there is no generated name at this point + // use the userid from the email address + // it is better than nothing. + GetPrimaryEmail(aResult); + int32_t index = aResult.FindChar('@'); + if (index != -1) aResult.SetLength(index); + } + + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GeneratePhoneticName(bool aLastNameFirst, + nsAString& aResult) { + nsAutoString firstName, lastName; + GetPropertyAsAString(kPhoneticFirstNameProperty, firstName); + GetPropertyAsAString(kPhoneticLastNameProperty, lastName); + + if (aLastNameFirst) { + aResult = lastName; + aResult += firstName; + } else { + aResult = firstName; + aResult += lastName; + } + + return NS_OK; +} + +NS_IMETHODIMP nsAbCardProperty::GenerateChatName(nsAString& aResult) { + aResult.Truncate(); + +#define CHECK_CHAT_PROPERTY(aProtocol) \ + if (NS_SUCCEEDED(GetPropertyAsAString(k##aProtocol##Property, aResult)) && \ + !aResult.IsEmpty()) \ + return NS_OK + CHECK_CHAT_PROPERTY(Gtalk); + CHECK_CHAT_PROPERTY(AIM); + CHECK_CHAT_PROPERTY(Yahoo); + CHECK_CHAT_PROPERTY(Skype); + CHECK_CHAT_PROPERTY(QQ); + CHECK_CHAT_PROPERTY(MSN); + CHECK_CHAT_PROPERTY(ICQ); + CHECK_CHAT_PROPERTY(XMPP); + CHECK_CHAT_PROPERTY(IRC); + return NS_OK; +} diff --git a/comm/mailnews/addrbook/src/nsAbCardProperty.h b/comm/mailnews/addrbook/src/nsAbCardProperty.h new file mode 100644 index 0000000000..a724b592a6 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbCardProperty.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +/******************************************************************************************************** + + Interface for representing Address Book Person Card Property + +*********************************************************************************************************/ + +#ifndef nsAbCardProperty_h__ +#define nsAbCardProperty_h__ + +#include "nsIAbCard.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +#include "nsInterfaceHashtable.h" +#include "nsIVariant.h" + +class nsIStringBundle; +class mozITXTToHTMLConv; +struct AppendItem; + +/* + * Address Book Card Property + */ + +class nsAbCardProperty : public nsIAbCard { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIABCARD + + nsAbCardProperty(); + + protected: + virtual ~nsAbCardProperty(); + bool m_IsMailList; + nsCString m_MailListURI; + + // Store most of the properties here + nsInterfaceHashtable<nsCStringHashKey, nsIVariant> m_properties; + + nsCString m_directoryUID; + + private: + nsresult AppendSection(const AppendItem* aArray, int16_t aCount, + const nsString& aHeading, nsIStringBundle* aBundle, + mozITXTToHTMLConv* aConv, nsString& aResult); + nsresult AppendLine(const AppendItem& aItem, mozITXTToHTMLConv* aConv, + nsString& aResult); + nsresult AppendLabel(const AppendItem& aItem, nsIStringBundle* aBundle, + mozITXTToHTMLConv* aConv, nsString& aResult); + nsresult AppendCityStateZip(const AppendItem& aItem, nsIStringBundle* aBundle, + mozITXTToHTMLConv* aConv, nsString& aResult); + + nsresult ConvertToBase64EncodedXML(nsACString& result); + nsresult ConvertToXMLPrintData(nsAString& result); + nsresult ConvertToEscapedVCard(nsACString& result); +}; + +#endif diff --git a/comm/mailnews/addrbook/src/nsAbDirProperty.cpp b/comm/mailnews/addrbook/src/nsAbDirProperty.cpp new file mode 100644 index 0000000000..67860e424d --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbDirProperty.cpp @@ -0,0 +1,573 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsAbDirProperty.h" +#include "nsIAbCard.h" +#include "nsIPrefService.h" +#include "nsIPrefLocalizedString.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "prmem.h" +#include "nsIAbManager.h" +#include "nsArrayUtils.h" +#include "nsIUUIDGenerator.h" +#include "mozilla/Components.h" +#include "mozilla/Services.h" +#include "nsIObserverService.h" +#include "mozilla/dom/Promise.h" + +using mozilla::ErrorResult; +using mozilla::dom::Promise; +using namespace mozilla; + +// From nsDirPrefs +#define kDefaultPosition 1 + +nsAbDirProperty::nsAbDirProperty(void) + : m_LastModifiedDate(0), mIsValidURI(false) { + m_IsMailList = false; + mUID = EmptyCString(); +} + +nsAbDirProperty::~nsAbDirProperty(void) { +#if 0 + // this code causes a regression #138647 + // don't turn it on until you figure it out + if (m_AddressList) { + uint32_t count; + nsresult rv; + rv = m_AddressList->GetLength(&count); + NS_ASSERTION(NS_SUCCEEDED(rv), "Count failed"); + int32_t i; + for (i = count - 1; i >= 0; i--) + m_AddressList->RemoveElementAt(i); + } +#endif +} + +NS_IMPL_ISUPPORTS(nsAbDirProperty, nsIAbDirectory, nsISupportsWeakReference) + +NS_IMETHODIMP nsAbDirProperty::GetPropertiesChromeURI(nsACString& aResult) { + aResult.AssignLiteral( + "chrome://messenger/content/addressbook/abAddressBookNameDialog.xhtml"); + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetDirName(nsAString& aDirName) { + if (m_DirPrefId.IsEmpty()) { + aDirName = m_ListDirName; + return NS_OK; + } + + nsCString dirName; + nsresult rv = GetLocalizedStringValue("description", EmptyCString(), dirName); + NS_ENSURE_SUCCESS(rv, rv); + + // In TB 2 only some prefs had chrome:// URIs. We had code in place that would + // only get the localized string pref for the particular address books that + // were built-in. + // Additionally, nsIPrefBranch::getComplexValue will only get a non-user-set, + // non-locked pref value if it is a chrome:// URI and will get the string + // value at that chrome URI. This breaks extensions/autoconfig that want to + // set default pref values and allow users to change directory names. + // + // Now we have to support this, and so if for whatever reason we fail to get + // the localized version, then we try and get the non-localized version + // instead. If the string value is empty, then we'll just get the empty value + // back here. + if (dirName.IsEmpty()) { + rv = GetStringValue("description", EmptyCString(), dirName); + NS_ENSURE_SUCCESS(rv, rv); + } + + CopyUTF8toUTF16(dirName, aDirName); + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::SetDirName(const nsAString& aDirName) { + if (m_DirPrefId.IsEmpty()) { + m_ListDirName = aDirName; + return NS_OK; + } + + // Store the old value. + nsString oldDirName; + nsresult rv = GetDirName(oldDirName); + NS_ENSURE_SUCCESS(rv, rv); + + // Save the new value + rv = SetLocalizedStringValue("description", NS_ConvertUTF16toUTF8(aDirName)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbManager> abManager = + do_GetService("@mozilla.org/abmanager;1", &rv); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + // We inherit from nsIAbDirectory, so this static cast should be safe. + observerService->NotifyObservers(static_cast<nsIAbDirectory*>(this), + "addrbook-directory-updated", u"DirName"); + } + + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetDirType(int32_t* aDirType) { + return GetIntValue("dirType", nsIAbManager::LDAP_DIRECTORY_TYPE, aDirType); +} + +NS_IMETHODIMP nsAbDirProperty::GetFileName(nsACString& aFileName) { + return GetStringValue("filename", EmptyCString(), aFileName); +} + +NS_IMETHODIMP nsAbDirProperty::GetUID(nsACString& aUID) { + nsresult rv = NS_OK; + if (!mUID.IsEmpty()) { + aUID = mUID; + return rv; + } + if (!m_IsMailList) { + rv = GetStringValue("uid", EmptyCString(), aUID); + if (!aUID.IsEmpty()) { + return rv; + } + } + + nsCOMPtr<nsIUUIDGenerator> uuidgen = + mozilla::components::UUIDGenerator::Service(); + NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE); + + nsID id; + rv = uuidgen->GenerateUUIDInPlace(&id); + NS_ENSURE_SUCCESS(rv, rv); + + char idString[NSID_LENGTH]; + id.ToProvidedString(idString); + + aUID.AppendASCII(idString + 1, NSID_LENGTH - 3); + return SetUID(aUID); +} + +NS_IMETHODIMP nsAbDirProperty::SetUID(const nsACString& aUID) { + mUID = aUID; + if (m_IsMailList) { + return NS_OK; + } + return SetStringValue("uid", aUID); +} + +NS_IMETHODIMP nsAbDirProperty::GetURI(nsACString& aURI) { + // XXX Should we complete this for Mailing Lists? + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::GetPosition(int32_t* aPosition) { + return GetIntValue("position", kDefaultPosition, aPosition); +} + +NS_IMETHODIMP nsAbDirProperty::GetLastModifiedDate( + uint32_t* aLastModifiedDate) { + NS_ENSURE_ARG_POINTER(aLastModifiedDate); + *aLastModifiedDate = m_LastModifiedDate; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::SetLastModifiedDate(uint32_t aLastModifiedDate) { + if (aLastModifiedDate) { + m_LastModifiedDate = aLastModifiedDate; + } + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetListNickName(nsAString& aListNickName) { + aListNickName = m_ListNickName; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::SetListNickName(const nsAString& aListNickName) { + m_ListNickName = aListNickName; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetDescription(nsAString& aDescription) { + aDescription = m_Description; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::SetDescription(const nsAString& aDescription) { + m_Description = aDescription; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetIsMailList(bool* aIsMailList) { + *aIsMailList = m_IsMailList; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::SetIsMailList(bool aIsMailList) { + m_IsMailList = aIsMailList; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::CopyMailList(nsIAbDirectory* srcList) { + SetIsMailList(true); + + nsString str; + srcList->GetDirName(str); + SetDirName(str); + srcList->GetListNickName(str); + SetListNickName(str); + srcList->GetDescription(str); + SetDescription(str); + + nsAutoCString uid; + srcList->GetUID(uid); + SetUID(uid); + + return NS_OK; +} + +NS_IMETHODIMP +nsAbDirProperty::Init(const char* aURI) { + mURI = aURI; + mIsValidURI = true; + return NS_OK; +} + +NS_IMETHODIMP +nsAbDirProperty::CleanUp(JSContext* cx, Promise** retval) { + nsIGlobalObject* globalObject = + xpc::NativeGlobal(JS::CurrentGlobalOrNull(cx)); + if (NS_WARN_IF(!globalObject)) { + return NS_ERROR_FAILURE; + } + + ErrorResult result; + RefPtr<Promise> promise = Promise::Create(globalObject, result); + promise->MaybeResolveWithUndefined(); + promise.forget(retval); + + return NS_OK; +} + +// nsIAbDirectory NOT IMPLEMENTED methods +NS_IMETHODIMP +nsAbDirProperty::GetChildNodes(nsTArray<RefPtr<nsIAbDirectory>>& childList) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsAbDirProperty::GetChildCardCount(uint32_t* count) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsAbDirProperty::GetChildCards(nsTArray<RefPtr<nsIAbCard>>& childCards) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsAbDirProperty::DeleteDirectory(nsIAbDirectory* directory) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsAbDirProperty::HasCard(nsIAbCard* cards, bool* hasCard) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsAbDirProperty::HasDirectory(nsIAbDirectory* dir, bool* hasDir) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsAbDirProperty::HasMailListWithName(const nsAString& aName, bool* aHasList) { + NS_ENSURE_ARG_POINTER(aHasList); + + *aHasList = false; + nsCOMPtr<nsIAbDirectory> aDir; + nsresult rv = GetMailListFromName(aName, getter_AddRefs(aDir)); + + NS_ENSURE_SUCCESS(rv, rv); + + if (aDir) { + *aHasList = true; + } + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::AddMailList(nsIAbDirectory* list, + nsIAbDirectory** addedList) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::EditMailListToDatabase(nsIAbCard* listCard) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::AddCard(nsIAbCard* childCard, + nsIAbCard** addedCard) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::ModifyCard(nsIAbCard* aModifiedCard) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::DeleteCards( + const nsTArray<RefPtr<nsIAbCard>>& aCards) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::DropCard(nsIAbCard* childCard, + bool needToCopyCard) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::CardForEmailAddress( + const nsACString& aEmailAddress, nsIAbCard** aAbCard) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::GetCardFromProperty(const char* aProperty, + const nsACString& aValue, + bool caseSensitive, + nsIAbCard** result) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsAbDirProperty::GetCardsFromProperty( + const char* aProperty, const nsACString& aValue, bool caseSensitive, + nsTArray<RefPtr<nsIAbCard>>& result) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsAbDirProperty::GetMailListFromName(const nsAString& aName, + nsIAbDirectory** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = nullptr; + bool supportsLists = false; + nsresult rv = GetSupportsMailingLists(&supportsLists); + if (NS_FAILED(rv) || !supportsLists) return NS_OK; + + if (m_IsMailList) return NS_OK; + + if (!m_AddressList) { + nsresult rv; + m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + uint32_t listCount = 0; + rv = m_AddressList->GetLength(&listCount); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0; i < listCount; i++) { + nsCOMPtr<nsIAbDirectory> listDir(do_QueryElementAt(m_AddressList, i, &rv)); + if (NS_SUCCEEDED(rv) && listDir) { + nsAutoString listName; + rv = listDir->GetDirName(listName); + if (NS_SUCCEEDED(rv) && listName.Equals(aName)) { + listDir.forget(aResult); + return NS_OK; + } + } + } + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetSupportsMailingLists( + bool* aSupportsMailingsLists) { + NS_ENSURE_ARG_POINTER(aSupportsMailingsLists); + // We don't currently support nested mailing lists, so only return true if + // we're not a mailing list. + *aSupportsMailingsLists = !m_IsMailList; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetReadOnly(bool* aReadOnly) { + NS_ENSURE_ARG_POINTER(aReadOnly); + // Default is that we are writable. Any implementation that is read-only must + // override this method. + *aReadOnly = false; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetIsRemote(bool* aIsRemote) { + NS_ENSURE_ARG_POINTER(aIsRemote); + *aIsRemote = false; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetIsSecure(bool* aIsSecure) { + NS_ENSURE_ARG_POINTER(aIsSecure); + *aIsSecure = false; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::UseForAutocomplete( + const nsACString& aIdentityKey, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + // Is local autocomplete enabled? + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefBranch->GetBoolPref("mail.enable_autocomplete", aResult); + NS_ENSURE_SUCCESS(rv, rv); + + // If autocomplete is generally enabled, check if it has been disabled + // explicitly for this directory. + if (*aResult) { + (void)GetBoolValue("enable_autocomplete", true, aResult); + } + + return rv; +} + +NS_IMETHODIMP nsAbDirProperty::GetDirPrefId(nsACString& aDirPrefId) { + aDirPrefId = m_DirPrefId; + return NS_OK; +} + +nsresult nsAbDirProperty::InitDirectoryPrefs() { + if (m_DirPrefId.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + nsCOMPtr<nsIPrefService> prefService( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString realPrefId(m_DirPrefId); + realPrefId.Append('.'); + + return prefService->GetBranch(realPrefId.get(), + getter_AddRefs(m_DirectoryPrefs)); +} + +NS_IMETHODIMP nsAbDirProperty::GetIntValue(const char* aName, + int32_t aDefaultValue, + int32_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs())) + return NS_ERROR_NOT_INITIALIZED; + + if (NS_FAILED(m_DirectoryPrefs->GetIntPref(aName, aResult))) + *aResult = aDefaultValue; + + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetBoolValue(const char* aName, + bool aDefaultValue, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs())) + return NS_ERROR_NOT_INITIALIZED; + + if (NS_FAILED(m_DirectoryPrefs->GetBoolPref(aName, aResult))) + *aResult = aDefaultValue; + + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::GetStringValue(const char* aName, + const nsACString& aDefaultValue, + nsACString& aResult) { + if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs())) + return NS_ERROR_NOT_INITIALIZED; + + nsCString value; + + /* unfortunately, there may be some prefs out there which look like (null) */ + if (NS_SUCCEEDED(m_DirectoryPrefs->GetCharPref(aName, value)) && + !value.EqualsLiteral("(null")) + aResult = value; + else + aResult = aDefaultValue; + + return NS_OK; +} +/* + * Get localized unicode string pref from properties file, convert into an + * UTF8 string since address book prefs store as UTF8 strings. So far there + * are 2 default prefs stored in addressbook.properties. + * "ldap_2.servers.pab.description" + * "ldap_2.servers.history.description" + */ +NS_IMETHODIMP nsAbDirProperty::GetLocalizedStringValue( + const char* aName, const nsACString& aDefaultValue, nsACString& aResult) { + if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs())) + return NS_ERROR_NOT_INITIALIZED; + + nsString wvalue; + nsCOMPtr<nsIPrefLocalizedString> locStr; + + nsresult rv = m_DirectoryPrefs->GetComplexValue( + aName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(locStr)); + if (NS_SUCCEEDED(rv)) { + rv = locStr->ToString(getter_Copies(wvalue)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (wvalue.IsEmpty()) + aResult = aDefaultValue; + else + CopyUTF16toUTF8(wvalue, aResult); + + return NS_OK; +} + +NS_IMETHODIMP nsAbDirProperty::SetIntValue(const char* aName, int32_t aValue) { + if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs())) + return NS_ERROR_NOT_INITIALIZED; + + return m_DirectoryPrefs->SetIntPref(aName, aValue); +} + +NS_IMETHODIMP nsAbDirProperty::SetBoolValue(const char* aName, bool aValue) { + if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs())) + return NS_ERROR_NOT_INITIALIZED; + + return m_DirectoryPrefs->SetBoolPref(aName, aValue); +} + +NS_IMETHODIMP nsAbDirProperty::SetStringValue(const char* aName, + const nsACString& aValue) { + if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs())) + return NS_ERROR_NOT_INITIALIZED; + + return m_DirectoryPrefs->SetCharPref(aName, aValue); +} + +NS_IMETHODIMP nsAbDirProperty::SetLocalizedStringValue( + const char* aName, const nsACString& aValue) { + if (!m_DirectoryPrefs && NS_FAILED(InitDirectoryPrefs())) + return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + nsCOMPtr<nsIPrefLocalizedString> locStr( + do_CreateInstance(NS_PREFLOCALIZEDSTRING_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = locStr->SetData(NS_ConvertUTF8toUTF16(aValue)); + NS_ENSURE_SUCCESS(rv, rv); + + return m_DirectoryPrefs->SetComplexValue( + aName, NS_GET_IID(nsIPrefLocalizedString), locStr); +} + +NS_IMETHODIMP nsAbDirProperty::Search(const nsAString& query, + const nsAString& searchString, + nsIAbDirSearchListener* listener) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/comm/mailnews/addrbook/src/nsAbDirProperty.h b/comm/mailnews/addrbook/src/nsAbDirProperty.h new file mode 100644 index 0000000000..cf3bd59b68 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbDirProperty.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +/******************************************************************************************************** + + Interface for representing Address Book Directory + +*********************************************************************************************************/ + +#ifndef nsAbDirProperty_h__ +#define nsAbDirProperty_h__ + +#include "nsIAbDirectory.h" /* include the interface we are going to support */ +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIPrefBranch.h" +#include "nsIMutableArray.h" +#include "nsWeakReference.h" + +/* + * Address Book Directory + */ + +class nsAbDirProperty : public nsIAbDirectory, public nsSupportsWeakReference { + public: + nsAbDirProperty(void); + + NS_DECL_ISUPPORTS + NS_DECL_NSIABDIRECTORY + + protected: + virtual ~nsAbDirProperty(void); + + /** + * Initialise the directory prefs for this branch + */ + nsresult InitDirectoryPrefs(); + + uint32_t m_LastModifiedDate; + + nsString m_ListDirName; + nsString m_ListName; + nsString m_ListNickName; + nsString m_Description; + bool m_IsMailList; + + nsCString mURI; + nsCString mUID; + bool mIsValidURI; + + /* + * Note that any derived implementations should ensure that this item + * (m_DirPrefId) is correctly initialised correctly + */ + nsCString m_DirPrefId; // ie,"ldap_2.servers.pab" + + nsCOMPtr<nsIPrefBranch> m_DirectoryPrefs; + nsCOMPtr<nsIMutableArray> m_AddressList; +}; +#endif diff --git a/comm/mailnews/addrbook/src/nsAbDirectoryQuery.cpp b/comm/mailnews/addrbook/src/nsAbDirectoryQuery.cpp new file mode 100644 index 0000000000..f0f5d18b0c --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbDirectoryQuery.cpp @@ -0,0 +1,421 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsIAbCard.h" +#include "nsAbDirectoryQuery.h" +#include "nsAbDirectoryQueryProxy.h" +#include "nsAbBooleanExpression.h" +#include "nsComponentManagerUtils.h" +#include "nsString.h" +#include "nsUnicharUtils.h" +#include "nsIAbDirSearchListener.h" +#include "nsISimpleEnumerator.h" +#include "nsMsgUtils.h" +#include "nsQueryObject.h" + +NS_IMPL_ISUPPORTS(nsAbDirectoryQuerySimpleBooleanExpression, + nsIAbBooleanExpression) + +nsAbDirectoryQuerySimpleBooleanExpression:: + nsAbDirectoryQuerySimpleBooleanExpression() + : mOperation(nsIAbBooleanOperationTypes::AND) {} + +nsAbDirectoryQuerySimpleBooleanExpression:: + ~nsAbDirectoryQuerySimpleBooleanExpression() {} + +/* attribute nsAbBooleanOperationType operation; */ +NS_IMETHODIMP nsAbDirectoryQuerySimpleBooleanExpression::GetOperation( + nsAbBooleanOperationType* aOperation) { + if (!aOperation) return NS_ERROR_NULL_POINTER; + + *aOperation = mOperation; + + return NS_OK; +} +NS_IMETHODIMP nsAbDirectoryQuerySimpleBooleanExpression::SetOperation( + nsAbBooleanOperationType aOperation) { + if (aOperation != nsIAbBooleanOperationTypes::AND && + aOperation != nsIAbBooleanOperationTypes::OR) + return NS_ERROR_FAILURE; + + mOperation = aOperation; + + return NS_OK; +} + +/* attribute Array<nsISupports> expressions; */ +NS_IMETHODIMP nsAbDirectoryQuerySimpleBooleanExpression::GetExpressions( + nsTArray<RefPtr<nsISupports>>& aExpressions) { + aExpressions = mExpressions.Clone(); + return NS_OK; +} + +NS_IMETHODIMP nsAbDirectoryQuerySimpleBooleanExpression::SetExpressions( + const nsTArray<RefPtr<nsISupports>>& aExpressions) { + // Ensure all the items are of the right type. + nsresult rv; + nsCOMPtr<nsIAbBooleanConditionString> queryExpression; + for (auto expression : aExpressions) { + queryExpression = do_QueryInterface(expression, &rv); + if (NS_FAILED(rv)) return NS_ERROR_ILLEGAL_VALUE; + } + + // Values ok, so we can just save and return. + mExpressions = aExpressions.Clone(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsAbDirectoryQueryArguments, nsIAbDirectoryQueryArguments) + +nsAbDirectoryQueryArguments::nsAbDirectoryQueryArguments() + : mQuerySubDirectories(true) {} + +nsAbDirectoryQueryArguments::~nsAbDirectoryQueryArguments() {} + +/* attribute nsISupports matchItems; */ +NS_IMETHODIMP nsAbDirectoryQueryArguments::GetExpression( + nsISupports** aExpression) { + if (!aExpression) return NS_ERROR_NULL_POINTER; + + NS_IF_ADDREF(*aExpression = mExpression); + return NS_OK; +} + +NS_IMETHODIMP nsAbDirectoryQueryArguments::SetExpression( + nsISupports* aExpression) { + mExpression = aExpression; + return NS_OK; +} + +/* attribute boolean querySubDirectories; */ +NS_IMETHODIMP nsAbDirectoryQueryArguments::GetQuerySubDirectories( + bool* aQuerySubDirectories) { + NS_ENSURE_ARG_POINTER(aQuerySubDirectories); + *aQuerySubDirectories = mQuerySubDirectories; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirectoryQueryArguments::SetQuerySubDirectories( + bool aQuerySubDirectories) { + mQuerySubDirectories = aQuerySubDirectories; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirectoryQueryArguments::GetTypeSpecificArg( + nsISupports** aArg) { + NS_ENSURE_ARG_POINTER(aArg); + + NS_IF_ADDREF(*aArg = mTypeSpecificArg); + return NS_OK; +} + +NS_IMETHODIMP nsAbDirectoryQueryArguments::SetTypeSpecificArg( + nsISupports* aArg) { + mTypeSpecificArg = aArg; + return NS_OK; +} + +NS_IMETHODIMP nsAbDirectoryQueryArguments::GetFilter(nsACString& aFilter) { + aFilter.Assign(mFilter); + return NS_OK; +} + +NS_IMETHODIMP nsAbDirectoryQueryArguments::SetFilter( + const nsACString& aFilter) { + mFilter.Assign(aFilter); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsAbDirectoryQueryPropertyValue, + nsIAbDirectoryQueryPropertyValue) + +nsAbDirectoryQueryPropertyValue::nsAbDirectoryQueryPropertyValue() {} + +nsAbDirectoryQueryPropertyValue::nsAbDirectoryQueryPropertyValue( + const char* aName, const char16_t* aValue) { + mName = aName; + mValue = aValue; +} + +nsAbDirectoryQueryPropertyValue::nsAbDirectoryQueryPropertyValue( + const char* aName, nsISupports* aValueISupports) { + mName = aName; + mValueISupports = aValueISupports; +} + +nsAbDirectoryQueryPropertyValue::~nsAbDirectoryQueryPropertyValue() {} + +/* read only attribute string name; */ +NS_IMETHODIMP nsAbDirectoryQueryPropertyValue::GetName(char** aName) { + *aName = mName.IsEmpty() ? 0 : ToNewCString(mName); + + return NS_OK; +} + +/* read only attribute wstring value; */ +NS_IMETHODIMP nsAbDirectoryQueryPropertyValue::GetValue(char16_t** aValue) { + *aValue = ToNewUnicode(mValue); + if (!(*aValue)) + return NS_ERROR_OUT_OF_MEMORY; + else + return NS_OK; +} + +/* readonly attribute nsISupports valueISupports; */ +NS_IMETHODIMP nsAbDirectoryQueryPropertyValue::GetValueISupports( + nsISupports** aValueISupports) { + if (!mValueISupports) return NS_ERROR_NULL_POINTER; + + NS_IF_ADDREF(*aValueISupports = mValueISupports); + return NS_OK; +} + +/* Implementation file */ +NS_IMPL_ISUPPORTS(nsAbDirectoryQuery, nsIAbDirectoryQuery) + +nsAbDirectoryQuery::nsAbDirectoryQuery() {} + +nsAbDirectoryQuery::~nsAbDirectoryQuery() {} + +NS_IMETHODIMP nsAbDirectoryQuery::DoQuery( + nsIAbDirectory* aDirectory, nsIAbDirectoryQueryArguments* arguments, + nsIAbDirSearchListener* listener, int32_t resultLimit, int32_t timeOut, + int32_t* _retval) { + NS_ENSURE_ARG_POINTER(aDirectory); + + nsCOMPtr<nsISupports> supportsExpression; + nsresult rv = arguments->GetExpression(getter_AddRefs(supportsExpression)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbBooleanExpression> expression( + do_QueryInterface(supportsExpression, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool doSubDirectories; + rv = arguments->GetQuerySubDirectories(&doSubDirectories); + NS_ENSURE_SUCCESS(rv, rv); + + rv = query(aDirectory, expression, listener, doSubDirectories, &resultLimit); + NS_ENSURE_SUCCESS(rv, rv); + + rv = listener->OnSearchFinished(rv, true, nullptr, ""_ns); + + *_retval = 0; + return rv; +} + +/* void stopQuery (in long contextID); */ +NS_IMETHODIMP nsAbDirectoryQuery::StopQuery(int32_t contextID) { return NS_OK; } + +nsresult nsAbDirectoryQuery::query(nsIAbDirectory* directory, + nsIAbBooleanExpression* expression, + nsIAbDirSearchListener* listener, + bool doSubDirectories, + int32_t* resultLimit) { + if (*resultLimit == 0) return NS_OK; + + nsresult rv = queryCards(directory, expression, listener, resultLimit); + NS_ENSURE_SUCCESS(rv, rv); + + if (*resultLimit != 0 && doSubDirectories) { + rv = queryChildren(directory, expression, listener, doSubDirectories, + resultLimit); + NS_ENSURE_SUCCESS(rv, rv); + } + + return rv; +} + +nsresult nsAbDirectoryQuery::queryChildren(nsIAbDirectory* directory, + nsIAbBooleanExpression* expression, + nsIAbDirSearchListener* listener, + bool doSubDirectories, + int32_t* resultLimit) { + nsTArray<RefPtr<nsIAbDirectory>> subDirectories; + nsresult rv = directory->GetChildNodes(subDirectories); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIAbDirectory* subDirectory : subDirectories) { + rv = query(subDirectory, expression, listener, doSubDirectories, + resultLimit); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult nsAbDirectoryQuery::queryCards(nsIAbDirectory* directory, + nsIAbBooleanExpression* expression, + nsIAbDirSearchListener* listener, + int32_t* resultLimit) { + nsTArray<RefPtr<nsIAbCard>> cards; + nsresult rv = directory->GetChildCards(cards); + if (rv == NS_ERROR_NOT_IMPLEMENTED) { + return NS_OK; + } + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIAbCard* card : cards) { + rv = matchCard(card, expression, listener, resultLimit); + NS_ENSURE_SUCCESS(rv, rv); + + if (*resultLimit == 0) return NS_OK; + } + + return NS_OK; +} + +nsresult nsAbDirectoryQuery::matchCard(nsIAbCard* card, + nsIAbBooleanExpression* expression, + nsIAbDirSearchListener* listener, + int32_t* resultLimit) { + bool matchFound = false; + nsresult rv = matchCardExpression(card, expression, &matchFound); + NS_ENSURE_SUCCESS(rv, rv); + + if (matchFound) { + (*resultLimit)--; + rv = listener->OnSearchFoundCard(card); + NS_ENSURE_SUCCESS(rv, rv); + } + + return rv; +} + +nsresult nsAbDirectoryQuery::matchCardExpression( + nsIAbCard* card, nsIAbBooleanExpression* expression, bool* result) { + nsAbBooleanOperationType operation; + nsresult rv = expression->GetOperation(&operation); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsISupports>> childExpressions; + rv = expression->GetExpressions(childExpressions); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t count = childExpressions.Length(); + NS_ENSURE_SUCCESS(rv, rv); + + if (operation == nsIAbBooleanOperationTypes::NOT && count > 1) + return NS_ERROR_FAILURE; + + bool value = *result = false; + nsCOMPtr<nsIAbBooleanConditionString> childCondition; + nsCOMPtr<nsIAbBooleanExpression> childExpression; + + for (uint32_t i = 0; i < count; i++) { + childCondition = do_QueryObject(childExpressions[i], &rv); + if (NS_SUCCEEDED(rv)) { + rv = matchCardCondition(card, childCondition, &value); + NS_ENSURE_SUCCESS(rv, rv); + } else { + childExpression = do_QueryObject(childExpressions[i], &rv); + if (NS_SUCCEEDED(rv)) { + rv = matchCardExpression(card, childExpression, &value); + NS_ENSURE_SUCCESS(rv, rv); + } else + return NS_ERROR_FAILURE; + } + if (operation == nsIAbBooleanOperationTypes::OR && value) + break; + else if (operation == nsIAbBooleanOperationTypes::AND && !value) + break; + else if (operation == nsIAbBooleanOperationTypes::NOT) + value = !value; + } + *result = value; + + return NS_OK; +} + +nsresult nsAbDirectoryQuery::matchCardCondition( + nsIAbCard* card, nsIAbBooleanConditionString* condition, bool* matchFound) { + nsAbBooleanConditionType conditionType; + nsresult rv = condition->GetCondition(&conditionType); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString name; + rv = condition->GetName(getter_Copies(name)); + NS_ENSURE_SUCCESS(rv, rv); + + if (name.Equals("card:nsIAbCard")) { + *matchFound = (conditionType == nsIAbBooleanConditionTypes::Exists); + return NS_OK; + } + + nsString matchValue; + rv = condition->GetValue(getter_Copies(matchValue)); + NS_ENSURE_SUCCESS(rv, rv); + + if (name.EqualsLiteral("IsMailList")) { + bool isMailList; + rv = card->GetIsMailList(&isMailList); + NS_ENSURE_SUCCESS(rv, rv); + + // Only equals is supported. + if (conditionType != nsIAbBooleanConditionTypes::Is) + return NS_ERROR_FAILURE; + + *matchFound = isMailList ? matchValue.EqualsLiteral("TRUE") + : matchValue.EqualsLiteral("FALSE"); + return NS_OK; + } + + nsString value; + (void)card->GetPropertyAsAString(name.get(), value); + + if (value.IsEmpty()) { + *matchFound = (conditionType == nsIAbBooleanConditionTypes::DoesNotExist) + ? true + : false; + return NS_OK; + } + + /* TODO + * What about allowing choice between case insensitive + * and case sensitive comparisons? + * + */ + switch (conditionType) { + case nsIAbBooleanConditionTypes::Exists: + *matchFound = true; + break; + case nsIAbBooleanConditionTypes::Contains: + *matchFound = CaseInsensitiveFindInReadable(matchValue, value); + break; + case nsIAbBooleanConditionTypes::DoesNotContain: + *matchFound = !CaseInsensitiveFindInReadable(matchValue, value); + break; + case nsIAbBooleanConditionTypes::Is: + *matchFound = value.Equals(matchValue, nsCaseInsensitiveStringComparator); + break; + case nsIAbBooleanConditionTypes::IsNot: + *matchFound = + !value.Equals(matchValue, nsCaseInsensitiveStringComparator); + break; + case nsIAbBooleanConditionTypes::BeginsWith: + *matchFound = StringBeginsWith(value, matchValue, + nsCaseInsensitiveStringComparator); + break; + case nsIAbBooleanConditionTypes::LessThan: + *matchFound = + Compare(value, matchValue, nsCaseInsensitiveStringComparator) < 0; + break; + case nsIAbBooleanConditionTypes::GreaterThan: + *matchFound = + Compare(value, matchValue, nsCaseInsensitiveStringComparator) > 0; + break; + case nsIAbBooleanConditionTypes::EndsWith: + *matchFound = + StringEndsWith(value, matchValue, nsCaseInsensitiveStringComparator); + break; + case nsIAbBooleanConditionTypes::SoundsLike: + case nsIAbBooleanConditionTypes::RegExp: + *matchFound = false; + break; + default: + *matchFound = false; + } + + return rv; +} diff --git a/comm/mailnews/addrbook/src/nsAbDirectoryQuery.h b/comm/mailnews/addrbook/src/nsAbDirectoryQuery.h new file mode 100644 index 0000000000..2c0e7b0654 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbDirectoryQuery.h @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAbDirectoryQuery_h__ +#define nsAbDirectoryQuery_h__ + +#include "nsIAbDirectoryQuery.h" +#include "nsIAbDirectory.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIArray.h" +#include "nsIAbBooleanExpression.h" + +class nsAbDirectoryQuerySimpleBooleanExpression + : public nsIAbBooleanExpression { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIABBOOLEANEXPRESSION + + nsAbDirectoryQuerySimpleBooleanExpression(); + + private: + virtual ~nsAbDirectoryQuerySimpleBooleanExpression(); + + public: + nsTArray<RefPtr<nsISupports>> mExpressions; + nsAbBooleanOperationType mOperation; +}; + +class nsAbDirectoryQueryArguments : public nsIAbDirectoryQueryArguments { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIABDIRECTORYQUERYARGUMENTS + + nsAbDirectoryQueryArguments(); + + private: + virtual ~nsAbDirectoryQueryArguments(); + + protected: + nsCOMPtr<nsISupports> mExpression; + nsCOMPtr<nsISupports> mTypeSpecificArg; + bool mQuerySubDirectories; + nsCString mFilter; +}; + +class nsAbDirectoryQueryPropertyValue + : public nsIAbDirectoryQueryPropertyValue { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIABDIRECTORYQUERYPROPERTYVALUE + + nsAbDirectoryQueryPropertyValue(); + nsAbDirectoryQueryPropertyValue(const char* aName, const char16_t* aValue); + nsAbDirectoryQueryPropertyValue(const char* aName, + nsISupports* aValueISupports); + + protected: + virtual ~nsAbDirectoryQueryPropertyValue(); + nsCString mName; + nsString mValue; + nsCOMPtr<nsISupports> mValueISupports; +}; + +class nsAbDirectoryQuery : public nsIAbDirectoryQuery { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIABDIRECTORYQUERY + + nsAbDirectoryQuery(); + + protected: + virtual ~nsAbDirectoryQuery(); + nsresult query(nsIAbDirectory* directory, nsIAbBooleanExpression* expression, + nsIAbDirSearchListener* listener, bool doSubDirectories, + int32_t* resultLimit); + nsresult queryChildren(nsIAbDirectory* directory, + nsIAbBooleanExpression* expression, + nsIAbDirSearchListener* listener, + bool doSubDirectories, int32_t* resultLimit); + nsresult queryCards(nsIAbDirectory* directory, + nsIAbBooleanExpression* expression, + nsIAbDirSearchListener* listener, int32_t* resultLimit); + nsresult matchCard(nsIAbCard* card, nsIAbBooleanExpression* expression, + nsIAbDirSearchListener* listener, int32_t* resultLimit); + nsresult matchCardExpression(nsIAbCard* card, + nsIAbBooleanExpression* expression, + bool* result); + nsresult matchCardCondition(nsIAbCard* card, + nsIAbBooleanConditionString* condition, + bool* matchFound); +}; + +#endif diff --git a/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.cpp b/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.cpp new file mode 100644 index 0000000000..984746b82a --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.cpp @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsAbDirectoryQuery.h" +#include "nsAbDirectoryQueryProxy.h" + +NS_IMPL_ISUPPORTS(nsAbDirectoryQueryProxy, nsIAbDirectoryQueryProxy, + nsIAbDirectoryQuery) + +nsAbDirectoryQueryProxy::nsAbDirectoryQueryProxy() : mInitiated(false) {} + +nsAbDirectoryQueryProxy::~nsAbDirectoryQueryProxy() {} + +/* void initiate (in nsIAbDirectory directory); */ +NS_IMETHODIMP nsAbDirectoryQueryProxy::Initiate() { + if (mInitiated) return NS_OK; + + mDirectoryQuery = new nsAbDirectoryQuery(); + + mInitiated = true; + + return NS_OK; +} diff --git a/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h b/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h new file mode 100644 index 0000000000..542d58e6e3 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbDirectoryQueryProxy.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAbDirectoryQueryProxy_h__ +#define nsAbDirectoryQueryProxy_h__ + +#include "nsIAbDirectoryQueryProxy.h" +#include "nsCOMPtr.h" + +class nsAbDirectoryQueryProxy : public nsIAbDirectoryQueryProxy { + public: + NS_DECL_ISUPPORTS + NS_FORWARD_NSIABDIRECTORYQUERY(mDirectoryQuery->) + NS_DECL_NSIABDIRECTORYQUERYPROXY + + nsAbDirectoryQueryProxy(); + + protected: + virtual ~nsAbDirectoryQueryProxy(); + bool mInitiated; + nsCOMPtr<nsIAbDirectoryQuery> mDirectoryQuery; +}; + +#endif diff --git a/comm/mailnews/addrbook/src/nsAbLDIFService.cpp b/comm/mailnews/addrbook/src/nsAbLDIFService.cpp new file mode 100644 index 0000000000..2d40bec9b2 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbLDIFService.cpp @@ -0,0 +1,787 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +#include "nsIAbDirectory.h" +#include "nsIAbCard.h" +#include "nsString.h" +#include "nsAbLDIFService.h" +#include "nsIFile.h" +#include "nsILineInputStream.h" +#include "nsIInputStream.h" +#include "nsNetUtil.h" +#include "nsISeekableStream.h" +#include "mdb.h" +#include "plstr.h" +#include "prmem.h" +#include "prprf.h" +#include "nsCRTGlue.h" +#include "nsTArray.h" +#include "nsIComponentManager.h" + +#include <ctype.h> + +NS_IMPL_ISUPPORTS(nsAbLDIFService, nsIAbLDIFService) + +// If we get a line longer than 32K it's just toooooo bad! +#define kTextAddressBufferSz (64 * 1024) + +nsAbLDIFService::nsAbLDIFService() { + mStoreLocAsHome = false; + mLFCount = 0; + mCRCount = 0; +} + +nsAbLDIFService::~nsAbLDIFService() {} + +#define RIGHT2 0x03 +#define RIGHT4 0x0f +#define CONTINUED_LINE_MARKER '\001' + +// XXX TODO fix me +// use the NSPR base64 library. see plbase64.h +// see bug #145367 +static unsigned char b642nib[0x80] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, 0xff, 0x3f, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, + 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, + 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff}; + +NS_IMETHODIMP nsAbLDIFService::ImportLDIFFile(nsIAbDirectory* aDirectory, + nsIFile* aSrc, + bool aStoreLocAsHome, + uint32_t* aProgress) { + NS_ENSURE_ARG_POINTER(aSrc); + NS_ENSURE_ARG_POINTER(aDirectory); + + mStoreLocAsHome = aStoreLocAsHome; + + char buf[1024]; + char* pBuf = &buf[0]; + int32_t startPos = 0; + uint32_t len = 0; + nsTArray<int32_t> listPosArray; // where each list/group starts in ldif file + nsTArray<int32_t> listSizeArray; // size of the list/group info + int32_t savedStartPos = 0; + int32_t filePos = 0; + uint64_t bytesLeft = 0; + + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aSrc); + NS_ENSURE_SUCCESS(rv, rv); + + // Initialize the parser for a run... + mLdifLine.Truncate(); + + while (NS_SUCCEEDED(inputStream->Available(&bytesLeft)) && bytesLeft > 0) { + if (NS_SUCCEEDED(inputStream->Read(pBuf, sizeof(buf), &len)) && len > 0) { + startPos = 0; + + while (NS_SUCCEEDED(GetLdifStringRecord(buf, len, startPos))) { + if (mLdifLine.Find("groupOfNames") == -1) + AddLdifRowToDatabase(aDirectory, false); + else { + // keep file position for mailing list + listPosArray.AppendElement(savedStartPos); + listSizeArray.AppendElement(filePos + startPos - savedStartPos); + ClearLdifRecordBuffer(); + } + savedStartPos = filePos + startPos; + } + filePos += len; + if (aProgress) *aProgress = (uint32_t)filePos; + } + } + // last row + if (!mLdifLine.IsEmpty() && mLdifLine.Find("groupOfNames") == -1) + AddLdifRowToDatabase(aDirectory, false); + + // mail Lists + int32_t i, pos; + uint32_t size; + int32_t listTotal = listPosArray.Length(); + char* listBuf; + ClearLdifRecordBuffer(); // make sure the buffer is clean + + nsCOMPtr<nsISeekableStream> seekableStream = + do_QueryInterface(inputStream, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + for (i = 0; i < listTotal; i++) { + pos = listPosArray[i]; + size = listSizeArray[i]; + if (NS_SUCCEEDED( + seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, pos))) { + // Allocate enough space for the lists/groups as the size varies. + listBuf = (char*)PR_Malloc(size); + if (!listBuf) continue; + if (NS_SUCCEEDED(inputStream->Read(listBuf, size, &len)) && len > 0) { + startPos = 0; + + while (NS_SUCCEEDED(GetLdifStringRecord(listBuf, len, startPos))) { + if (mLdifLine.Find("groupOfNames") != -1) { + AddLdifRowToDatabase(aDirectory, true); + if (NS_SUCCEEDED( + seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, 0))) + break; + } + } + } + PR_FREEIF(listBuf); + } + } + + rv = inputStream->Close(); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +/* + * str_parse_line - takes a line of the form "type:[:] value" and splits it + * into components "type" and "value". if a double colon separates type from + * value, then value is encoded in base 64, and parse_line un-decodes it + * (in place) before returning. + * in LDIF, non-ASCII data is treated as base64 encoded UTF-8 + */ + +nsresult nsAbLDIFService::str_parse_line(char* line, char** type, char** value, + int* vlen) const { + char *p, *s, *d, *byte, *stop; + char nib; + int i, b64; + + /* skip any leading space */ + while (isspace(*line)) { + line++; + } + *type = line; + + for (s = line; *s && *s != ':'; s++) + ; /* NULL */ + if (*s == '\0') { + return NS_ERROR_FAILURE; + } + + /* trim any space between type and : */ + for (p = s - 1; p > line && isspace(*p); p--) { + *p = '\0'; + } + *s++ = '\0'; + + /* check for double : - indicates base 64 encoded value */ + if (*s == ':') { + s++; + b64 = 1; + /* single : - normally encoded value */ + } else { + b64 = 0; + } + + /* skip space between : and value */ + while (isspace(*s)) { + s++; + } + + /* if no value is present, error out */ + if (*s == '\0') { + return NS_ERROR_FAILURE; + } + + /* check for continued line markers that should be deleted */ + for (p = s, d = s; *p; p++) { + if (*p != CONTINUED_LINE_MARKER) *d++ = *p; + } + *d = '\0'; + + *value = s; + if (b64) { + stop = PL_strchr(s, '\0'); + byte = s; + for (p = s, *vlen = 0; p < stop; p += 4, *vlen += 3) { + for (i = 0; i < 3; i++) { + if (p[i] != '=' && (p[i] & 0x80 || b642nib[p[i] & 0x7f] > 0x3f)) { + return NS_ERROR_FAILURE; + } + } + + /* first digit */ + nib = b642nib[p[0] & 0x7f]; + byte[0] = nib << 2; + /* second digit */ + nib = b642nib[p[1] & 0x7f]; + byte[0] |= nib >> 4; + byte[1] = (nib & RIGHT4) << 4; + /* third digit */ + if (p[2] == '=') { + *vlen += 1; + break; + } + nib = b642nib[p[2] & 0x7f]; + byte[1] |= nib >> 2; + byte[2] = (nib & RIGHT2) << 6; + /* fourth digit */ + if (p[3] == '=') { + *vlen += 2; + break; + } + nib = b642nib[p[3] & 0x7f]; + byte[2] |= nib; + + byte += 3; + } + s[*vlen] = '\0'; + } else { + *vlen = (int)(d - s); + } + return NS_OK; +} + +/* + * str_getline - return the next "line" (minus newline) of input from a + * string buffer of lines separated by newlines, terminated by \n\n + * or \0. this routine handles continued lines, bundling them into + * a single big line before returning. if a line begins with a white + * space character, it is a continuation of the previous line. the white + * space character (nb: only one char), and preceding newline are changed + * into CONTINUED_LINE_MARKER chars, to be deleted later by the + * str_parse_line() routine above. + * + * it takes a pointer to a pointer to the buffer on the first call, + * which it updates and must be supplied on subsequent calls. + */ + +char* nsAbLDIFService::str_getline(char** next) const { + char* lineStr; + char c; + + if (*next == nullptr || **next == '\n' || **next == '\0') { + return (nullptr); + } + + lineStr = *next; + while ((*next = PL_strchr(*next, '\n')) != NULL) { + c = *(*next + 1); + if (isspace(c) && c != '\n') { + **next = CONTINUED_LINE_MARKER; + *(*next + 1) = CONTINUED_LINE_MARKER; + } else { + *(*next)++ = '\0'; + break; + } + } + + return (lineStr); +} + +nsresult nsAbLDIFService::GetLdifStringRecord(char* buf, int32_t len, + int32_t& stopPos) { + for (; stopPos < len; stopPos++) { + char c = buf[stopPos]; + + if (c == 0xA) { + mLFCount++; + } else if (c == 0xD) { + mCRCount++; + } else { + if (mLFCount == 0 && mCRCount == 0) + mLdifLine.Append(c); + else if ((mLFCount > 1) || (mCRCount > 2 && mLFCount) || + (!mLFCount && mCRCount > 1)) { + return NS_OK; + } else if ((mLFCount == 1 || mCRCount == 1)) { + mLdifLine.Append('\n'); + mLdifLine.Append(c); + mLFCount = 0; + mCRCount = 0; + } + } + } + + if (((stopPos == len) && (mLFCount > 1)) || (mCRCount > 2 && mLFCount) || + (!mLFCount && mCRCount > 1)) + return NS_OK; + + return NS_ERROR_FAILURE; +} + +void nsAbLDIFService::AddLdifRowToDatabase(nsIAbDirectory* aDirectory, + bool bIsList) { + if (!aDirectory) { + return; + } + + // If no data to process then reset CR/LF counters and return. + if (mLdifLine.IsEmpty()) { + mLFCount = 0; + mCRCount = 0; + return; + } + + nsCOMPtr<nsIAbCard> newCard = + do_CreateInstance("@mozilla.org/addressbook/cardproperty;1"); + nsTArray<nsCString> members; + + char* cursor = ToNewCString(mLdifLine); + char* saveCursor = cursor; /* keep for deleting */ + char* line = 0; + char* typeSlot = 0; + char* valueSlot = 0; + int length = 0; // the length of an ldif attribute + while ((line = str_getline(&cursor)) != nullptr) { + if (NS_SUCCEEDED(str_parse_line(line, &typeSlot, &valueSlot, &length))) { + nsAutoCString colType(typeSlot); + nsAutoCString column(valueSlot); + + // 4.x exports attributes like "givenname", + // mozilla does "givenName" to be compliant with RFC 2798 + ToLowerCase(colType); + + if (colType.EqualsLiteral("member") || + colType.EqualsLiteral("uniquemember")) { + members.AppendElement(column); + } else { + AddLdifColToDatabase(aDirectory, newCard, colType, column, bIsList); + } + } else + continue; // parse error: continue with next loop iteration + } + free(saveCursor); + + if (bIsList) { + nsCOMPtr<nsIAbDirectory> newList = + do_CreateInstance("@mozilla.org/addressbook/directoryproperty;1"); + newList->SetIsMailList(true); + + nsAutoString temp; + newCard->GetDisplayName(temp); + newList->SetDirName(temp); + temp.Truncate(); + newCard->GetPropertyAsAString(kNicknameProperty, temp); + newList->SetListNickName(temp); + temp.Truncate(); + newCard->GetPropertyAsAString(kNotesProperty, temp); + newList->SetDescription(temp); + + nsIAbDirectory* outList; + nsresult rv = aDirectory->AddMailList(newList, &outList); + NS_ENSURE_SUCCESS_VOID(rv); + + int32_t count = members.Length(); + for (int32_t i = 0; i < count; ++i) { + nsAutoCString email; + int32_t emailPos = members[i].Find("mail="); + emailPos += strlen("mail="); + email = Substring(members[i], emailPos); + + nsCOMPtr<nsIAbCard> emailCard; + aDirectory->CardForEmailAddress(email, getter_AddRefs(emailCard)); + if (emailCard) { + nsIAbCard* outCard; + outList->AddCard(emailCard, &outCard); + } + } + } else { + nsIAbCard* outCard; + aDirectory->AddCard(newCard, &outCard); + } + + // Clear buffer for next record + ClearLdifRecordBuffer(); +} + +void nsAbLDIFService::AddLdifColToDatabase(nsIAbDirectory* aDirectory, + nsIAbCard* newCard, + nsCString colType, nsCString column, + bool bIsList) { + nsString value = NS_ConvertUTF8toUTF16(column); + + char firstByte = colType.get()[0]; + switch (firstByte) { + case 'b': + if (colType.EqualsLiteral("birthyear")) + newCard->SetPropertyAsAString(kBirthYearProperty, value); + else if (colType.EqualsLiteral("birthmonth")) + newCard->SetPropertyAsAString(kBirthMonthProperty, value); + else if (colType.EqualsLiteral("birthday")) + newCard->SetPropertyAsAString(kBirthDayProperty, value); + break; // 'b' + + case 'c': + if (colType.EqualsLiteral("cn") || colType.EqualsLiteral("commonname")) { + newCard->SetDisplayName(value); + } else if (colType.EqualsLiteral("c") || + colType.EqualsLiteral("countryname")) { + if (mStoreLocAsHome) + newCard->SetPropertyAsAString(kHomeCountryProperty, value); + else + newCard->SetPropertyAsAString(kWorkCountryProperty, value); + } + + else if (colType.EqualsLiteral("cellphone")) + newCard->SetPropertyAsAString(kCellularProperty, value); + + else if (colType.EqualsLiteral("carphone")) + newCard->SetPropertyAsAString(kCellularProperty, value); + + else if (colType.EqualsLiteral("custom1")) + newCard->SetPropertyAsAString(kCustom1Property, value); + + else if (colType.EqualsLiteral("custom2")) + newCard->SetPropertyAsAString(kCustom2Property, value); + + else if (colType.EqualsLiteral("custom3")) + newCard->SetPropertyAsAString(kCustom3Property, value); + + else if (colType.EqualsLiteral("custom4")) + newCard->SetPropertyAsAString(kCustom4Property, value); + + else if (colType.EqualsLiteral("company")) + newCard->SetPropertyAsAString(kCompanyProperty, value); + break; // 'c' + + case 'd': + if (colType.EqualsLiteral("description")) + newCard->SetPropertyAsAString(kNotesProperty, value); + + else if (colType.EqualsLiteral("department")) + newCard->SetPropertyAsAString(kDepartmentProperty, value); + + else if (colType.EqualsLiteral("displayname")) + newCard->SetDisplayName(value); + break; // 'd' + + case 'f': + + if (colType.EqualsLiteral("fax") || + colType.EqualsLiteral("facsimiletelephonenumber")) + newCard->SetPropertyAsAString(kFaxProperty, value); + break; // 'f' + + case 'g': + if (colType.EqualsLiteral("givenname")) newCard->SetFirstName(value); + break; // 'g' + + case 'h': + if (colType.EqualsLiteral("homephone")) + newCard->SetPropertyAsAString(kHomePhoneProperty, value); + + else if (colType.EqualsLiteral("homestreet")) + newCard->SetPropertyAsAString(kHomeAddressProperty, value); + + else if (colType.EqualsLiteral("homeurl")) + newCard->SetPropertyAsAString(kHomeWebPageProperty, value); + break; // 'h' + + case 'l': + if (colType.EqualsLiteral("l") || colType.EqualsLiteral("locality")) { + if (mStoreLocAsHome) + newCard->SetPropertyAsAString(kHomeCityProperty, value); + else + newCard->SetPropertyAsAString(kWorkCityProperty, value); + } + // labeledURI contains a URI and, optionally, a label + // This will remove the label and place the URI as the work URL + else if (colType.EqualsLiteral("labeleduri")) { + int32_t index = column.FindChar(' '); + if (index != -1) column.SetLength(index); + + newCard->SetPropertyAsAString(kWorkWebPageProperty, + NS_ConvertUTF8toUTF16(column)); + } + + break; // 'l' + + case 'm': + if (colType.EqualsLiteral("mail")) + newCard->SetPrimaryEmail(value); + + else if (colType.EqualsLiteral("mobile")) + newCard->SetPropertyAsAString(kCellularProperty, value); + + else if (colType.EqualsLiteral("mozilla_aimscreenname")) + newCard->SetPropertyAsAString(kAIMProperty, value); + + else if (colType.EqualsLiteral("mozillacustom1")) + newCard->SetPropertyAsAString(kCustom1Property, value); + + else if (colType.EqualsLiteral("mozillacustom2")) + newCard->SetPropertyAsAString(kCustom2Property, value); + + else if (colType.EqualsLiteral("mozillacustom3")) + newCard->SetPropertyAsAString(kCustom3Property, value); + + else if (colType.EqualsLiteral("mozillacustom4")) + newCard->SetPropertyAsAString(kCustom4Property, value); + + else if (colType.EqualsLiteral("mozillahomecountryname")) + newCard->SetPropertyAsAString(kHomeCountryProperty, value); + + else if (colType.EqualsLiteral("mozillahomelocalityname")) + newCard->SetPropertyAsAString(kHomeCityProperty, value); + + else if (colType.EqualsLiteral("mozillahomestate")) + newCard->SetPropertyAsAString(kHomeStateProperty, value); + + else if (colType.EqualsLiteral("mozillahomestreet")) + newCard->SetPropertyAsAString(kHomeAddressProperty, value); + + else if (colType.EqualsLiteral("mozillahomestreet2")) + newCard->SetPropertyAsAString(kHomeAddress2Property, value); + + else if (colType.EqualsLiteral("mozillahomepostalcode")) + newCard->SetPropertyAsAString(kHomeZipCodeProperty, value); + + else if (colType.EqualsLiteral("mozillahomeurl")) + newCard->SetPropertyAsAString(kHomeWebPageProperty, value); + + else if (colType.EqualsLiteral("mozillanickname")) + newCard->SetPropertyAsAString(kNicknameProperty, value); + + else if (colType.EqualsLiteral("mozillasecondemail")) + newCard->SetPropertyAsAString(k2ndEmailProperty, value); + + else if (colType.EqualsLiteral("mozillaworkstreet2")) + newCard->SetPropertyAsAString(kWorkAddress2Property, value); + + else if (colType.EqualsLiteral("mozillaworkurl")) + newCard->SetPropertyAsAString(kWorkWebPageProperty, value); + + break; // 'm' + + case 'n': + if (colType.EqualsLiteral("notes")) + newCard->SetPropertyAsAString(kNotesProperty, value); + + else if (colType.EqualsLiteral("nscpaimscreenname") || + colType.EqualsLiteral("nsaimid")) + newCard->SetPropertyAsAString(kAIMProperty, value); + + break; // 'n' + + case 'o': + if (colType.EqualsLiteral("objectclass")) + break; + + else if (colType.EqualsLiteral("ou") || colType.EqualsLiteral("orgunit")) + newCard->SetPropertyAsAString(kDepartmentProperty, value); + + else if (colType.EqualsLiteral("o")) // organization + newCard->SetPropertyAsAString(kCompanyProperty, value); + + break; // 'o' + + case 'p': + if (colType.EqualsLiteral("postalcode")) { + if (mStoreLocAsHome) + newCard->SetPropertyAsAString(kHomeZipCodeProperty, value); + else + newCard->SetPropertyAsAString(kWorkZipCodeProperty, value); + } + + else if (colType.EqualsLiteral("postofficebox")) { + nsAutoCString workAddr1, workAddr2; + SplitCRLFAddressField(column, workAddr1, workAddr2); + newCard->SetPropertyAsAString(kWorkAddressProperty, + NS_ConvertUTF8toUTF16(workAddr1)); + newCard->SetPropertyAsAString(kWorkAddress2Property, + NS_ConvertUTF8toUTF16(workAddr2)); + } else if (colType.EqualsLiteral("pager") || + colType.EqualsLiteral("pagerphone")) + newCard->SetPropertyAsAString(kPagerProperty, value); + + break; // 'p' + + case 'r': + if (colType.EqualsLiteral("region")) { + newCard->SetPropertyAsAString(kWorkStateProperty, value); + } + + break; // 'r' + + case 's': + if (colType.EqualsLiteral("sn") || colType.EqualsLiteral("surname")) + newCard->SetPropertyAsAString(kLastNameProperty, value); + + else if (colType.EqualsLiteral("street")) + newCard->SetPropertyAsAString(kWorkAddressProperty, value); + + else if (colType.EqualsLiteral("streetaddress")) { + nsAutoCString addr1, addr2; + SplitCRLFAddressField(column, addr1, addr2); + if (mStoreLocAsHome) { + newCard->SetPropertyAsAString(kHomeAddressProperty, + NS_ConvertUTF8toUTF16(addr1)); + newCard->SetPropertyAsAString(kHomeAddress2Property, + NS_ConvertUTF8toUTF16(addr2)); + } else { + newCard->SetPropertyAsAString(kWorkAddressProperty, + NS_ConvertUTF8toUTF16(addr1)); + newCard->SetPropertyAsAString(kWorkAddress2Property, + NS_ConvertUTF8toUTF16(addr2)); + } + } else if (colType.EqualsLiteral("st")) { + if (mStoreLocAsHome) + newCard->SetPropertyAsAString(kHomeStateProperty, value); + else + newCard->SetPropertyAsAString(kWorkStateProperty, value); + } + + break; // 's' + + case 't': + if (colType.EqualsLiteral("title")) + newCard->SetPropertyAsAString(kJobTitleProperty, value); + + else if (colType.EqualsLiteral("telephonenumber")) { + newCard->SetPropertyAsAString(kWorkPhoneProperty, value); + } + + break; // 't' + + case 'w': + if (colType.EqualsLiteral("workurl")) + newCard->SetPropertyAsAString(kWorkWebPageProperty, value); + + break; // 'w' + + case 'x': + if (colType.EqualsLiteral("xmozillanickname")) { + newCard->SetPropertyAsAString(kNicknameProperty, value); + } + + break; // 'x' + + case 'z': + if (colType.EqualsLiteral("zip")) // alias for postalcode + { + if (mStoreLocAsHome) + newCard->SetPropertyAsAString(kHomeZipCodeProperty, value); + else + newCard->SetPropertyAsAString(kWorkZipCodeProperty, value); + } + + break; // 'z' + + default: + break; // default + } +} + +void nsAbLDIFService::ClearLdifRecordBuffer() { + if (!mLdifLine.IsEmpty()) { + mLdifLine.Truncate(); + mLFCount = 0; + mCRCount = 0; + } +} + +// Some common ldif fields, it an ldif file has NONE of these entries +// then it is most likely NOT an ldif file! +static const char* const sLDIFFields[] = {"objectclass", "sn", "dn", "cn", + "givenName", "mail", nullptr}; +#define kMaxLDIFLen 14 + +// Count total number of legal ldif fields and records in the first 100 lines of +// the file and if the average legal ldif field is 3 or higher than it's a valid +// ldif file. +NS_IMETHODIMP nsAbLDIFService::IsLDIFFile(nsIFile* pSrc, bool* _retval) { + NS_ENSURE_ARG_POINTER(pSrc); + NS_ENSURE_ARG_POINTER(_retval); + + *_retval = false; + + nsresult rv = NS_OK; + + nsCOMPtr<nsIInputStream> fileStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), pSrc); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILineInputStream> lineInputStream( + do_QueryInterface(fileStream, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t lineLen = 0; + int32_t lineCount = 0; + int32_t ldifFields = 0; // total number of legal ldif fields. + char field[kMaxLDIFLen]; + int32_t fLen = 0; + const char* pChar; + int32_t recCount = 0; // total number of records. + int32_t i; + bool gotLDIF = false; + bool more = true; + nsCString line; + + while (more && NS_SUCCEEDED(rv) && (lineCount < 100)) { + rv = lineInputStream->ReadLine(line, &more); + + if (NS_SUCCEEDED(rv) && more) { + pChar = line.get(); + lineLen = line.Length(); + if (!lineLen && gotLDIF) { + recCount++; + gotLDIF = false; + } + + if (lineLen && (*pChar != ' ') && (*pChar != '\t')) { + fLen = 0; + + while (lineLen && (fLen < (kMaxLDIFLen - 1)) && (*pChar != ':')) { + field[fLen] = *pChar; + pChar++; + fLen++; + lineLen--; + } + + field[fLen] = 0; + + if (lineLen && (*pChar == ':') && (fLen < (kMaxLDIFLen - 1))) { + // see if this is an ldif field (case insensitive)? + i = 0; + while (sLDIFFields[i]) { + if (!PL_strcasecmp(sLDIFFields[i], field)) { + ldifFields++; + gotLDIF = true; + break; + } + i++; + } + } + } + } + lineCount++; + } + + // If we just saw ldif address, increment recCount. + if (gotLDIF) recCount++; + + rv = fileStream->Close(); + + if (recCount > 1) ldifFields /= recCount; + + // If the average field number >= 3 then it's a good ldif file. + if (ldifFields >= 3) { + *_retval = true; + } + + return rv; +} + +void nsAbLDIFService::SplitCRLFAddressField(nsCString& inputAddress, + nsCString& outputLine1, + nsCString& outputLine2) const { + int32_t crlfPos = inputAddress.Find("\r\n"); + if (crlfPos != -1) { + outputLine1 = Substring(inputAddress, 0, crlfPos); + outputLine2 = Substring(inputAddress, crlfPos + 2); + } else + outputLine1.Assign(inputAddress); +} diff --git a/comm/mailnews/addrbook/src/nsAbLDIFService.h b/comm/mailnews/addrbook/src/nsAbLDIFService.h new file mode 100644 index 0000000000..84d892fdff --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbLDIFService.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +#ifndef __nsAbLDIFService_h +#define __nsAbLDIFService_h + +#include "nsIAbLDIFService.h" +#include "nsCOMPtr.h" + +class nsAbLDIFService : public nsIAbLDIFService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIABLDIFSERVICE + + nsAbLDIFService(); + + private: + virtual ~nsAbLDIFService(); + nsresult str_parse_line(char* line, char** type, char** value, + int* vlen) const; + char* str_getline(char** next) const; + nsresult GetLdifStringRecord(char* buf, int32_t len, int32_t& stopPos); + void AddLdifRowToDatabase(nsIAbDirectory* aDirectory, bool aIsList); + void AddLdifColToDatabase(nsIAbDirectory* aDirectory, nsIAbCard* newCard, + nsCString colType, nsCString column, bool bIsList); + void ClearLdifRecordBuffer(); + void SplitCRLFAddressField(nsCString& inputAddress, nsCString& outputLine1, + nsCString& outputLine2) const; + + bool mStoreLocAsHome; + nsCString mLdifLine; + int32_t mLFCount; + int32_t mCRCount; +}; + +#endif diff --git a/comm/mailnews/addrbook/src/nsAbOSXCard.h b/comm/mailnews/addrbook/src/nsAbOSXCard.h new file mode 100644 index 0000000000..2c0650fd64 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOSXCard.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#ifndef nsAbOSXCard_h___ +#define nsAbOSXCard_h___ + +#include "mozilla/Attributes.h" +#include "nsAbCardProperty.h" + +#define NS_ABOSXCARD_URI_PREFIX "moz-abosxcard://" + +#define NS_IABOSXCARD_IID \ + { \ + 0xa7e5b697, 0x772d, 0x4fb5, { \ + 0x81, 0x16, 0x23, 0xb7, 0x5a, 0xac, 0x94, 0x56 \ + } \ + } + +class nsIAbOSXCard : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IABOSXCARD_IID) + + virtual nsresult Init(const char* aUri) = 0; + virtual nsresult Update(bool aNotify) = 0; + virtual nsresult GetURI(nsACString& aURI) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIAbOSXCard, NS_IABOSXCARD_IID) + +class nsAbOSXCard : public nsAbCardProperty, public nsIAbOSXCard { + public: + NS_DECL_ISUPPORTS_INHERITED + + nsresult Update(bool aNotify) override; + nsresult GetURI(nsACString& aURI) override; + nsresult Init(const char* aUri) override; + NS_IMETHOD GetUID(nsACString& uid) override; + NS_IMETHOD SetUID(const nsACString& aUID) override; + // this is needed so nsAbOSXUtils.mm can get at nsAbCardProperty + friend class nsAbOSXUtils; + + private: + nsCString mURI; + nsCString mUID; + + virtual ~nsAbOSXCard() {} +}; + +#endif // nsAbOSXCard_h___ diff --git a/comm/mailnews/addrbook/src/nsAbOSXCard.mm b/comm/mailnews/addrbook/src/nsAbOSXCard.mm new file mode 100644 index 0000000000..ab77242490 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOSXCard.mm @@ -0,0 +1,353 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsAbOSXCard.h" +#include "nsAbOSXDirectory.h" +#include "nsAbOSXUtils.h" +#include "nsIAbManager.h" +#include "nsObjCExceptions.h" +#include "nsServiceManagerUtils.h" + +#include <AddressBook/AddressBook.h> + +NS_IMPL_ISUPPORTS_INHERITED(nsAbOSXCard, nsAbCardProperty, nsIAbOSXCard) + +#ifdef DEBUG +static ABPropertyType GetPropertType(ABRecord* aCard, NSString* aProperty) { + ABPropertyType propertyType = kABErrorInProperty; + if ([aCard isKindOfClass:[ABPerson class]]) + propertyType = [ABPerson typeOfProperty:aProperty]; + else if ([aCard isKindOfClass:[ABGroup class]]) + propertyType = [ABGroup typeOfProperty:aProperty]; + return propertyType; +} +#endif + +static void SetStringProperty(nsAbOSXCard* aCard, const nsString& aValue, const char* aMemberName, + bool aNotify, nsIAbManager* aAbManager) { + nsString oldValue; + nsresult rv = aCard->GetPropertyAsAString(aMemberName, oldValue); + if (NS_FAILED(rv)) oldValue.Truncate(); + + if (!aNotify) { + aCard->SetPropertyAsAString(aMemberName, aValue); + } else if (!oldValue.Equals(aValue)) { + aCard->SetPropertyAsAString(aMemberName, aValue); + } +} + +static void SetStringProperty(nsAbOSXCard* aCard, NSString* aValue, const char* aMemberName, + bool aNotify, nsIAbManager* aAbManager) { + nsAutoString value; + if (aValue) AppendToString(aValue, value); + + SetStringProperty(aCard, value, aMemberName, aNotify, aAbManager); +} + +static void MapStringProperty(nsAbOSXCard* aCard, ABRecord* aOSXCard, NSString* aProperty, + const char* aMemberName, bool aNotify, nsIAbManager* aAbManager) { + NS_ASSERTION(aProperty, "This is bad! You asked for an unresolved symbol."); + NS_ASSERTION(GetPropertType(aOSXCard, aProperty) == kABStringProperty, "Wrong type!"); + + SetStringProperty(aCard, [aOSXCard valueForProperty:aProperty], aMemberName, aNotify, aAbManager); +} + +static ABMutableMultiValue* GetMultiValue(ABRecord* aCard, NSString* aProperty) { + NS_ASSERTION(aProperty, "This is bad! You asked for an unresolved symbol."); + NS_ASSERTION(GetPropertType(aCard, aProperty) & kABMultiValueMask, "Wrong type!"); + + return [aCard valueForProperty:aProperty]; +} + +static void MapDate(nsAbOSXCard* aCard, NSDate* aDate, const char* aYearPropName, + const char* aMonthPropName, const char* aDayPropName, bool aNotify, + nsIAbManager* aAbManager) { + // XXX Should we pass a format and timezone? + NSCalendarDate* date = [aDate dateWithCalendarFormat:nil timeZone:nil]; + + nsAutoString value; + value.AppendInt(static_cast<int32_t>([date yearOfCommonEra])); + SetStringProperty(aCard, value, aYearPropName, aNotify, aAbManager); + value.Truncate(); + value.AppendInt(static_cast<int32_t>([date monthOfYear])); + SetStringProperty(aCard, value, aMonthPropName, aNotify, aAbManager); + value.Truncate(); + value.AppendInt(static_cast<int32_t>([date dayOfMonth])); + SetStringProperty(aCard, value, aDayPropName, aNotify, aAbManager); +} + +static bool MapMultiValue(nsAbOSXCard* aCard, ABRecord* aOSXCard, const nsAbOSXPropertyMap& aMap, + bool aNotify, nsIAbManager* aAbManager) { + ABMultiValue* value = GetMultiValue(aOSXCard, aMap.mOSXProperty); + if (value) { + unsigned int j; + unsigned int count = [value count]; + for (j = 0; j < count; ++j) { + if ([[value labelAtIndex:j] isEqualToString:aMap.mOSXLabel]) { + NSString* stringValue = (aMap.mOSXKey) ? [[value valueAtIndex:j] objectForKey:aMap.mOSXKey] + : [value valueAtIndex:j]; + + SetStringProperty(aCard, stringValue, aMap.mPropertyName, aNotify, aAbManager); + + return true; + } + } + } + // String wasn't found, set value of card to empty if it was set previously + SetStringProperty(aCard, EmptyString(), aMap.mPropertyName, aNotify, aAbManager); + + return false; +} + +// Maps Address Book's instant messenger name to the corresponding nsIAbCard field name. +static const char* InstantMessengerFieldName(NSString* aInstantMessengerName) { + if ([aInstantMessengerName isEqualToString:@"AIMInstant"]) { + return "_AimScreenName"; + } + if ([aInstantMessengerName isEqualToString:@"GoogleTalkInstant"]) { + return "_GoogleTalk"; + } + if ([aInstantMessengerName isEqualToString:@"ICQInstant"]) { + return "_ICQ"; + } + if ([aInstantMessengerName isEqualToString:@"JabberInstant"]) { + return "_JabberId"; + } + if ([aInstantMessengerName isEqualToString:@"MSNInstant"]) { + return "_MSN"; + } + if ([aInstantMessengerName isEqualToString:@"QQInstant"]) { + return "_QQ"; + } + if ([aInstantMessengerName isEqualToString:@"SkypeInstant"]) { + return "_Skype"; + } + if ([aInstantMessengerName isEqualToString:@"YahooInstant"]) { + return "_Yahoo"; + } + + // Fall back to AIM for everything else. + // We don't have nsIAbCard fields for FacebookInstant and GaduGaduInstant. + return "_AimScreenName"; +} + +nsresult nsAbOSXCard::Init(const char* aUri) { + if (strncmp(aUri, NS_ABOSXCARD_URI_PREFIX, sizeof(NS_ABOSXCARD_URI_PREFIX) - 1) != 0) + return NS_ERROR_FAILURE; + + mURI = aUri; + + // Extract the UID part. + mUID = Substring(mURI, 16, mURI.Length()); + // Now make sure we don't use the `:ABPerson` on the end, so that + // we don't expose it to extensions etc. + int32_t pos = mUID.RFindChar(':'); + if (pos != kNotFound) { + mUID = Substring(mUID, 0, pos); + } + // Also lower case so that we match other UIDs generated by the address book. + ToLowerCase(mUID); + + return Update(false); +} + +nsresult nsAbOSXCard::GetURI(nsACString& aURI) { + if (mURI.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + + aURI = mURI; + return NS_OK; +} + +NS_IMETHODIMP nsAbOSXCard::GetUID(nsACString& uid) { + uid = mUID; + return NS_OK; +} + +NS_IMETHODIMP nsAbOSXCard::SetUID(const nsACString& aUID) { + // The UIDs are obtained from the OS X contacts and cannot be changed. + return NS_ERROR_FAILURE; +} + +nsresult nsAbOSXCard::Update(bool aNotify) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + ABAddressBook* addressBook = [ABAddressBook sharedAddressBook]; + + const char* uid = &((mURI.get())[16]); + ABRecord* card = [addressBook recordForUniqueId:[NSString stringWithUTF8String:uid]]; + NS_ENSURE_TRUE(card, NS_ERROR_FAILURE); + + nsCOMPtr<nsIAbManager> abManager; + nsresult rv; + if (aNotify) { + abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + if ([card isKindOfClass:[ABGroup class]]) { + m_IsMailList = true; + m_MailListURI.AssignLiteral(NS_ABOSXDIRECTORY_URI_PREFIX); + m_MailListURI.Append(uid); + MapStringProperty(this, card, kABGroupNameProperty, "DisplayName", aNotify, abManager); + MapStringProperty(this, card, kABGroupNameProperty, "LastName", aNotify, abManager); + + return NS_OK; + } + + bool foundHome = false, foundWork = false; + + uint32_t i; + for (i = 0; i < nsAbOSXUtils::kPropertyMapSize; ++i) { + const nsAbOSXPropertyMap& propertyMap = nsAbOSXUtils::kPropertyMap[i]; + if (!propertyMap.mOSXProperty) continue; + + if (propertyMap.mOSXLabel) { + if (MapMultiValue(this, card, propertyMap, aNotify, abManager) && + propertyMap.mOSXProperty == kABAddressProperty) { + if (propertyMap.mOSXLabel == kABAddressHomeLabel) + foundHome = true; + else + foundWork = true; + } + } else { + MapStringProperty(this, card, propertyMap.mOSXProperty, propertyMap.mPropertyName, aNotify, + abManager); + } + } + + int flags = 0; + if (kABPersonFlags) flags = [[card valueForProperty:kABPersonFlags] intValue]; + +#define SET_STRING(_value, _name, _notify, _session) \ + SetStringProperty(this, _value, #_name, _notify, _session) + + // If kABShowAsCompany is set we use the company name as display name. + if (kABPersonFlags && (flags & kABShowAsCompany)) { + nsString company; + nsresult rv = GetPropertyAsAString(kCompanyProperty, company); + if (NS_FAILED(rv)) company.Truncate(); + SET_STRING(company, DisplayName, aNotify, abManager); + } else { + // Use the order used in the OS X address book to set DisplayName. + int order = kABPersonFlags && (flags & kABNameOrderingMask); + if (kABPersonFlags && (order == kABDefaultNameOrdering)) { + order = [addressBook defaultNameOrdering]; + } + + nsAutoString displayName, tempName; + if (kABPersonFlags && (order == kABFirstNameFirst)) { + GetFirstName(tempName); + displayName.Append(tempName); + + GetLastName(tempName); + + // Only append a space if the last name and the first name are not empty + if (!tempName.IsEmpty() && !displayName.IsEmpty()) displayName.Append(' '); + + displayName.Append(tempName); + } else { + GetLastName(tempName); + displayName.Append(tempName); + + GetFirstName(tempName); + + // Only append a space if the last name and the first name are not empty + if (!tempName.IsEmpty() && !displayName.IsEmpty()) displayName.Append(' '); + + displayName.Append(tempName); + } + SET_STRING(displayName, DisplayName, aNotify, abManager); + } + + ABMultiValue* value = GetMultiValue(card, kABEmailProperty); + if (value) { + unsigned int count = [value count]; + if (count > 0) { + unsigned int j = [value indexForIdentifier:[value primaryIdentifier]]; + + if (j < count) SET_STRING([value valueAtIndex:j], PrimaryEmail, aNotify, abManager); + + // If j is 0 (first in the list) we want the second in the list + // (index 1), if j is anything else we want the first in the list + // (index 0). + j = (j == 0); + if (j < count) SET_STRING([value valueAtIndex:j], SecondEmail, aNotify, abManager); + } + } + + // We map the first home address we can find and the first work address + // we can find. If we find none, we map the primary address to the home + // address. + if (!foundHome && !foundWork) { + value = GetMultiValue(card, kABAddressProperty); + if (value) { + unsigned int count = [value count]; + unsigned int j = [value indexForIdentifier:[value primaryIdentifier]]; + + if (j < count) { + NSDictionary* address = [value valueAtIndex:j]; + if (address) { + SET_STRING([address objectForKey:kABAddressStreetKey], HomeAddress, aNotify, abManager); + SET_STRING([address objectForKey:kABAddressCityKey], HomeCity, aNotify, abManager); + SET_STRING([address objectForKey:kABAddressStateKey], HomeState, aNotify, abManager); + SET_STRING([address objectForKey:kABAddressZIPKey], HomeZipCode, aNotify, abManager); + SET_STRING([address objectForKey:kABAddressCountryKey], HomeCountry, aNotify, abManager); + } + } + } + } + // This was kABAIMInstantProperty previously, but it was deprecated in OS X 10.7. + value = GetMultiValue(card, kABInstantMessageProperty); + if (value) { + unsigned int count = [value count]; + for (size_t i = 0; i < count; i++) { + id imValue = [value valueAtIndex:i]; + // Depending on the macOS version, imValue can be an NSString or an NSDictionary. + if ([imValue isKindOfClass:[NSString class]]) { + if (i == [value indexForIdentifier:[value primaryIdentifier]]) { + SET_STRING(imValue, _AimScreenName, aNotify, abManager); + } + } else if ([imValue isKindOfClass:[NSDictionary class]]) { + NSString* instantMessageService = [imValue objectForKey:@"InstantMessageService"]; + const char* fieldName = InstantMessengerFieldName(instantMessageService); + NSString* userName = [imValue objectForKey:@"InstantMessageUsername"]; + SetStringProperty(this, userName, fieldName, aNotify, abManager); + } + } + } + +#define MAP_DATE(_date, _name, _notify, _session) \ + MapDate(this, _date, #_name "Year", #_name "Month", #_name "Day", _notify, _session) + + NSDate* date = [card valueForProperty:kABBirthdayProperty]; + if (date) MAP_DATE(date, Birth, aNotify, abManager); + + if (kABOtherDatesProperty) { + value = GetMultiValue(card, kABOtherDatesProperty); + if (value) { + unsigned int j, count = [value count]; + for (j = 0; j < count; ++j) { + if ([[value labelAtIndex:j] isEqualToString:kABAnniversaryLabel]) { + date = [value valueAtIndex:j]; + if (date) { + MAP_DATE(date, Anniversary, aNotify, abManager); + + break; + } + } + } + } + } +#undef MAP_DATE +#undef SET_STRING + + date = [card valueForProperty:kABModificationDateProperty]; + if (date) SetPropertyAsUint32("LastModifiedDate", uint32_t([date timeIntervalSince1970])); + // XXX No way to notify about this? + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} diff --git a/comm/mailnews/addrbook/src/nsAbOSXDirectory.h b/comm/mailnews/addrbook/src/nsAbOSXDirectory.h new file mode 100644 index 0000000000..3d5b0384a9 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOSXDirectory.h @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#ifndef nsAbOSXDirectory_h___ +#define nsAbOSXDirectory_h___ + +#include "mozilla/Attributes.h" +#include "nsISupports.h" +#include "nsAbDirProperty.h" +#include "nsIAbDirSearchListener.h" +#include "nsIMutableArray.h" +#include "nsInterfaceHashtable.h" +#include "nsAbOSXCard.h" + +#include <CoreFoundation/CoreFoundation.h> +class nsIAbManager; +class nsIAbBooleanExpression; + +#define NS_ABOSXDIRECTORY_URI_PREFIX "moz-abosxdirectory://" + +#define NS_IABOSXDIRECTORY_IID \ + { \ + 0x87ee4bd9, 0x8552, 0x498f, { \ + 0x80, 0x85, 0x34, 0xf0, 0x2a, 0xbb, 0x56, 0x16 \ + } \ + } + +class nsIAbOSXDirectory : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IABOSXDIRECTORY_IID) + + virtual nsresult AssertChildNodes() = 0; + virtual nsresult Update() = 0; + virtual nsresult AssertDirectory(nsIAbManager* aManager, + nsIAbDirectory* aDirectory) = 0; + virtual nsresult AssertCard(nsIAbManager* aManager, nsIAbCard* aCard) = 0; + virtual nsresult UnassertCard(nsIAbManager* aManager, nsIAbCard* aCard, + nsIMutableArray* aCardList) = 0; + virtual nsresult UnassertDirectory(nsIAbManager* aManager, + nsIAbDirectory* aDirectory) = 0; + virtual nsresult DeleteUid(const nsACString& aUid) = 0; + virtual nsresult GetURI(nsACString& aURI) = 0; + virtual nsresult Init(const char* aUri) = 0; + virtual nsresult GetCardByUri(const nsACString& aUri, + nsIAbOSXCard** aResult) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIAbOSXDirectory, NS_IABOSXDIRECTORY_IID) + +class nsAbOSXDirectory final : public nsAbDirProperty, + public nsIAbOSXDirectory { + public: + nsAbOSXDirectory(); + + NS_DECL_ISUPPORTS_INHERITED + + // nsIAbOSXDirectory method + NS_IMETHOD Init(const char* aUri) override; + + // nsAbDirProperty methods + NS_IMETHOD GetReadOnly(bool* aReadOnly) override; + NS_IMETHOD GetChildCardCount(uint32_t* aCount) override; + NS_IMETHOD GetChildCards(nsTArray<RefPtr<nsIAbCard>>& result) override; + NS_IMETHOD GetChildNodes(nsTArray<RefPtr<nsIAbDirectory>>& result) override; + NS_IMETHOD HasCard(nsIAbCard* aCard, bool* aHasCard) override; + NS_IMETHOD HasDirectory(nsIAbDirectory* aDirectory, + bool* aHasDirectory) override; + NS_IMETHOD GetURI(nsACString& aURI) override; + NS_IMETHOD GetCardFromProperty(const char* aProperty, + const nsACString& aValue, bool caseSensitive, + nsIAbCard** aResult) override; + NS_IMETHOD GetCardsFromProperty( + const char* aProperty, const nsACString& aValue, bool aCaseSensitive, + nsTArray<RefPtr<nsIAbCard>>& aResult) override; + NS_IMETHOD CardForEmailAddress(const nsACString& aEmailAddress, + nsIAbCard** aResult) override; + NS_IMETHOD Search(const nsAString& query, const nsAString& searchString, + nsIAbDirSearchListener* listener) override; + + // nsIAbOSXDirectory + nsresult AssertChildNodes() override; + nsresult AssertDirectory(nsIAbManager* aManager, + nsIAbDirectory* aDirectory) override; + nsresult AssertCard(nsIAbManager* aManager, nsIAbCard* aCard) override; + nsresult UnassertCard(nsIAbManager* aManager, nsIAbCard* aCard, + nsIMutableArray* aCardList) override; + nsresult UnassertDirectory(nsIAbManager* aManager, + nsIAbDirectory* aDirectory) override; + + nsresult Update() override; + + nsresult DeleteUid(const nsACString& aUid) override; + + nsresult GetCardByUri(const nsACString& aUri, + nsIAbOSXCard** aResult) override; + + nsresult GetRootOSXDirectory(nsIAbOSXDirectory** aResult); + + private: + ~nsAbOSXDirectory(); + + // This is a list of nsIAbCards, kept separate from m_AddressList because: + // - nsIAbDirectory items that are mailing lists, must keep a list of + // nsIAbCards in m_AddressList, however + // - nsIAbDirectory items that are address books, must keep a list of + // nsIAbDirectory (i.e. mailing lists) in m_AddressList, AND no nsIAbCards. + // + // This wasn't too bad for mork, as that just gets a list from its database, + // but because we store our own copy of the list, we must store a separate + // list of nsIAbCards here. nsIMutableArray is used, because then it is + // interchangeable with m_AddressList. + nsCOMPtr<nsIMutableArray> mCardList; + nsInterfaceHashtable<nsCStringHashKey, nsIAbOSXCard> mCardStore; + nsCOMPtr<nsIAbOSXDirectory> mCacheTopLevelOSXAb; +}; + +#endif // nsAbOSXDirectory_h___ diff --git a/comm/mailnews/addrbook/src/nsAbOSXDirectory.mm b/comm/mailnews/addrbook/src/nsAbOSXDirectory.mm new file mode 100644 index 0000000000..fe29a8a0d2 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOSXDirectory.mm @@ -0,0 +1,911 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsAbOSXDirectory.h" +#include "nsAbOSXCard.h" +#include "nsAbOSXUtils.h" +#include "nsAbQueryStringToExpression.h" +#include "nsCOMArray.h" +#include "nsEnumeratorUtils.h" +#include "nsIAbDirectoryQueryProxy.h" +#include "nsIAbManager.h" +#include "nsObjCExceptions.h" +#include "nsServiceManagerUtils.h" +#include "nsIMutableArray.h" +#include "nsArrayUtils.h" +#include "nsIAbBooleanExpression.h" +#include "nsComponentManagerUtils.h" +#include "nsISimpleEnumerator.h" + +#include <AddressBook/AddressBook.h> + +#define kABDeletedRecords (kABDeletedRecords ? kABDeletedRecords : @"ABDeletedRecords") +#define kABUpdatedRecords (kABUpdatedRecords ? kABUpdatedRecords : @"ABUpdatedRecords") +#define kABInsertedRecords (kABInsertedRecords ? kABInsertedRecords : @"ABInsertedRecords") + +static nsresult GetOrCreateGroup(NSString* aUid, nsIAbDirectory** aResult) { + NS_ASSERTION(aUid, "No UID for group!."); + + nsAutoCString uri(NS_ABOSXDIRECTORY_URI_PREFIX); + AppendToCString(aUid, uri); + + nsresult rv; + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbDirectory> directory; + rv = abManager->GetDirectory(uri, getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*aResult = directory); + return NS_OK; +} + +static nsresult GetCard(ABRecord* aRecord, nsIAbCard** aResult, nsIAbOSXDirectory* osxDirectory) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + NSString* uid = [aRecord uniqueId]; + NS_ASSERTION(uid, "No UID for card!."); + if (!uid) return NS_ERROR_FAILURE; + + nsAutoCString uri(NS_ABOSXCARD_URI_PREFIX); + AppendToCString(uid, uri); + nsCOMPtr<nsIAbOSXCard> osxCard; + nsresult rv = osxDirectory->GetCardByUri(uri, getter_AddRefs(osxCard)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbCard> card = do_QueryInterface(osxCard, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*aResult = card); + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +static nsresult CreateCard(ABRecord* aRecord, nsIAbCard** aResult) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + NSString* uid = [aRecord uniqueId]; + NS_ASSERTION(uid, "No UID for card!."); + if (!uid) return NS_ERROR_FAILURE; + + nsresult rv; + nsCOMPtr<nsIAbOSXCard> osxCard = + do_CreateInstance("@mozilla.org/addressbook/directory;1?type=moz-abosxcard", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString uri(NS_ABOSXCARD_URI_PREFIX); + AppendToCString(uid, uri); + + rv = osxCard->Init(uri.get()); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbCard> card = do_QueryInterface(osxCard, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*aResult = card); + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +static nsresult Sync(NSString* aUid) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + ABAddressBook* addressBook = [ABAddressBook sharedAddressBook]; + ABRecord* card = [addressBook recordForUniqueId:aUid]; + if ([card isKindOfClass:[ABGroup class]]) { + nsCOMPtr<nsIAbDirectory> directory; + GetOrCreateGroup(aUid, getter_AddRefs(directory)); + nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory); + + if (osxDirectory) { + osxDirectory->Update(); + } + } else { + nsCOMPtr<nsIAbCard> abCard; + nsresult rv; + + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbDirectory> directory; + rv = abManager->GetDirectory(nsLiteralCString(NS_ABOSXDIRECTORY_URI_PREFIX "/"), + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetCard(card, getter_AddRefs(abCard), osxDirectory); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbOSXCard> osxCard = do_QueryInterface(abCard); + osxCard->Update(true); + } + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +@interface ABChangedMonitor : NSObject +- (void)ABChanged:(NSNotification*)aNotification; +@end + +@implementation ABChangedMonitor +- (void)ABChanged:(NSNotification*)aNotification { + NSDictionary* changes = [aNotification userInfo]; + + nsresult rv; + NSArray* inserted = [changes objectForKey:kABInsertedRecords]; + + if (inserted) { + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIAbDirectory> directory; + rv = abManager->GetDirectory(nsLiteralCString(NS_ABOSXDIRECTORY_URI_PREFIX "/"), + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + unsigned int i, count = [inserted count]; + for (i = 0; i < count; ++i) { + ABAddressBook* addressBook = [ABAddressBook sharedAddressBook]; + ABRecord* card = [addressBook recordForUniqueId:[inserted objectAtIndex:i]]; + if ([card isKindOfClass:[ABGroup class]]) { + nsCOMPtr<nsIAbDirectory> directory; + GetOrCreateGroup([inserted objectAtIndex:i], getter_AddRefs(directory)); + + rv = osxDirectory->AssertDirectory(abManager, directory); + NS_ENSURE_SUCCESS_VOID(rv); + } else { + nsCOMPtr<nsIAbCard> abCard; + // Construct a card + nsresult rv = CreateCard(card, getter_AddRefs(abCard)); + NS_ENSURE_SUCCESS_VOID(rv); + rv = osxDirectory->AssertCard(abManager, abCard); + NS_ENSURE_SUCCESS_VOID(rv); + } + } + } + + NSArray* updated = [changes objectForKey:kABUpdatedRecords]; + if (updated) { + unsigned int i, count = [updated count]; + for (i = 0; i < count; ++i) { + NSString* uid = [updated objectAtIndex:i]; + Sync(uid); + } + } + + NSArray* deleted = [changes objectForKey:kABDeletedRecords]; + if (deleted) { + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIAbDirectory> directory; + rv = abManager->GetDirectory(nsLiteralCString(NS_ABOSXDIRECTORY_URI_PREFIX "/"), + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + unsigned int i, count = [deleted count]; + for (i = 0; i < count; ++i) { + NSString* deletedUid = [deleted objectAtIndex:i]; + + nsAutoCString uid; + AppendToCString(deletedUid, uid); + + rv = osxDirectory->DeleteUid(uid); + NS_ENSURE_SUCCESS_VOID(rv); + } + } + + if (!inserted && !updated && !deleted) { + // XXX This is supposed to mean "everything was updated", but we get + // this whenever something has changed, so not sure what to do. + } +} +@end + +static uint32_t sObserverCount = 0; +static ABChangedMonitor* sObserver = nullptr; + +nsAbOSXDirectory::nsAbOSXDirectory() {} + +nsAbOSXDirectory::~nsAbOSXDirectory() { + if (--sObserverCount == 0) { + [[NSNotificationCenter defaultCenter] removeObserver:sObserver]; + [sObserver release]; + } +} + +NS_IMPL_ISUPPORTS_INHERITED(nsAbOSXDirectory, nsAbDirProperty, nsIAbOSXDirectory) + +NS_IMETHODIMP +nsAbOSXDirectory::Init(const char* aUri) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + nsresult rv; + rv = nsAbDirProperty::Init(aUri); + NS_ENSURE_SUCCESS(rv, rv); + + ABAddressBook* addressBook = [ABAddressBook sharedAddressBook]; + if (sObserverCount == 0) { + sObserver = [[ABChangedMonitor alloc] init]; + [[NSNotificationCenter defaultCenter] addObserver:(ABChangedMonitor*)sObserver + selector:@selector(ABChanged:) + name:kABDatabaseChangedExternallyNotification + object:nil]; + } + ++sObserverCount; + + NSArray* cards; + nsCOMPtr<nsIMutableArray> cardList; + bool isRootOSXDirectory = false; + + if (mURI.Length() <= sizeof(NS_ABOSXDIRECTORY_URI_PREFIX)) { + isRootOSXDirectory = true; + + m_DirPrefId.AssignLiteral("ldap_2.servers.osx"); + + cards = [[addressBook people] arrayByAddingObjectsFromArray:[addressBook groups]]; + if (!mCardList) + mCardList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + else + rv = mCardList->Clear(); + NS_ENSURE_SUCCESS(rv, rv); + + cardList = mCardList; + } else { + nsAutoCString uid(Substring(mURI, sizeof(NS_ABOSXDIRECTORY_URI_PREFIX) - 1)); + ABRecord* card = [addressBook recordForUniqueId:[NSString stringWithUTF8String:uid.get()]]; + NS_ASSERTION([card isKindOfClass:[ABGroup class]], "Huh."); + + m_IsMailList = true; + AppendToString([card valueForProperty:kABGroupNameProperty], m_ListDirName); + + ABGroup* group = (ABGroup*)[addressBook + recordForUniqueId:[NSString stringWithUTF8String:nsAutoCString(Substring(mURI, 21)).get()]]; + cards = [[group members] arrayByAddingObjectsFromArray:[group subgroups]]; + + if (!m_AddressList) + m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + else + rv = m_AddressList->Clear(); + NS_ENSURE_SUCCESS(rv, rv); + + cardList = m_AddressList; + } + + unsigned int nbCards = [cards count]; + nsCOMPtr<nsIAbCard> card; + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbOSXDirectory> rootOSXDirectory; + if (!isRootOSXDirectory) { + rv = GetRootOSXDirectory(getter_AddRefs(rootOSXDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + } + + for (unsigned int i = 0; i < nbCards; ++i) { + // If we're a Group, it's likely that the cards we're going + // to create were already created in the root nsAbOSXDirectory, + if (!isRootOSXDirectory) + rv = GetCard([cards objectAtIndex:i], getter_AddRefs(card), rootOSXDirectory); + else { + // If we're not a Group, that means we're the root nsAbOSXDirectory, + // which means we have to create the cards from scratch. + rv = CreateCard([cards objectAtIndex:i], getter_AddRefs(card)); + } + + NS_ENSURE_SUCCESS(rv, rv); + + // We're going to want to tell the AB Manager that we've added some cards + // so that they show up in the address book views. + AssertCard(abManager, card); + } + + if (isRootOSXDirectory) { + AssertChildNodes(); + } + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +NS_IMETHODIMP +nsAbOSXDirectory::GetURI(nsACString& aURI) { + if (mURI.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + + aURI = mURI; + return NS_OK; +} + +NS_IMETHODIMP +nsAbOSXDirectory::GetReadOnly(bool* aReadOnly) { + NS_ENSURE_ARG_POINTER(aReadOnly); + + *aReadOnly = true; + return NS_OK; +} + +static bool CheckRedundantCards(nsIAbManager* aManager, nsIAbDirectory* aDirectory, + nsIAbCard* aCard, NSMutableArray* aCardList) { + nsresult rv; + nsCOMPtr<nsIAbOSXCard> osxCard = do_QueryInterface(aCard, &rv); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoCString uri; + rv = osxCard->GetURI(uri); + NS_ENSURE_SUCCESS(rv, false); + NSString* uid = [NSString stringWithUTF8String:(uri.get() + 21)]; + + unsigned int i, count = [aCardList count]; + for (i = 0; i < count; ++i) { + if ([[[aCardList objectAtIndex:i] uniqueId] isEqualToString:uid]) { + [aCardList removeObjectAtIndex:i]; + break; + } + } + + if (i == count) { + return true; + } + + return false; +} + +nsresult nsAbOSXDirectory::GetRootOSXDirectory(nsIAbOSXDirectory** aResult) { + if (!mCacheTopLevelOSXAb) { + // Attempt to get card from the toplevel directories + nsresult rv; + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbDirectory> directory; + rv = abManager->GetDirectory(nsLiteralCString(NS_ABOSXDIRECTORY_URI_PREFIX "/"), + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbOSXDirectory> osxDirectory = do_QueryInterface(directory, &rv); + NS_ENSURE_SUCCESS(rv, rv); + mCacheTopLevelOSXAb = osxDirectory; + } + + NS_IF_ADDREF(*aResult = mCacheTopLevelOSXAb); + return NS_OK; +} + +nsresult nsAbOSXDirectory::Update() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + nsresult rv; + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + ABAddressBook* addressBook = [ABAddressBook sharedAddressBook]; + // Due to the horrible way the address book code works wrt mailing lists + // we have to use a different list depending on what we are. This pointer + // holds a reference to that list. + nsIMutableArray* cardList; + NSArray *groups, *cards; + if (m_IsMailList) { + ABGroup* group = (ABGroup*)[addressBook + recordForUniqueId:[NSString stringWithUTF8String:nsAutoCString(Substring(mURI, 21)).get()]]; + groups = nil; + cards = [[group members] arrayByAddingObjectsFromArray:[group subgroups]]; + + if (!m_AddressList) { + m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + // For mailing lists, store the cards in m_AddressList + cardList = m_AddressList; + } else { + groups = [addressBook groups]; + cards = [[addressBook people] arrayByAddingObjectsFromArray:groups]; + + if (!mCardList) { + mCardList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + // For directories, store the cards in mCardList + cardList = mCardList; + } + + NSMutableArray* mutableArray = [NSMutableArray arrayWithArray:cards]; + uint32_t addressCount; + rv = cardList->GetLength(&addressCount); + NS_ENSURE_SUCCESS(rv, rv); + + while (addressCount--) { + nsCOMPtr<nsIAbCard> card(do_QueryElementAt(cardList, addressCount, &rv)); + if (NS_FAILED(rv)) break; + + if (CheckRedundantCards(abManager, this, card, mutableArray)) + cardList->RemoveElementAt(addressCount); + } + + NSEnumerator* enumerator = [mutableArray objectEnumerator]; + ABRecord* card; + nsCOMPtr<nsIAbCard> abCard; + nsCOMPtr<nsIAbOSXDirectory> rootOSXDirectory; + rv = GetRootOSXDirectory(getter_AddRefs(rootOSXDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + + while ((card = [enumerator nextObject])) { + rv = GetCard(card, getter_AddRefs(abCard), rootOSXDirectory); + if (NS_FAILED(rv)) rv = CreateCard(card, getter_AddRefs(abCard)); + NS_ENSURE_SUCCESS(rv, rv); + AssertCard(abManager, abCard); + } + + card = (ABRecord*)[addressBook + recordForUniqueId:[NSString stringWithUTF8String:nsAutoCString(Substring(mURI, 21)).get()]]; + NSString* stringValue = [card valueForProperty:kABGroupNameProperty]; + if (![stringValue isEqualToString:WrapString(m_ListDirName)]) { + nsAutoString oldValue(m_ListDirName); + AssignToString(stringValue, m_ListDirName); + } + + if (groups) { + mutableArray = [NSMutableArray arrayWithArray:groups]; + nsCOMPtr<nsIAbDirectory> directory; + // It is ok to use m_AddressList here as only top-level directories have + // groups, and they will be in m_AddressList + if (m_AddressList) { + rv = m_AddressList->GetLength(&addressCount); + NS_ENSURE_SUCCESS(rv, rv); + + while (addressCount--) { + directory = do_QueryElementAt(m_AddressList, addressCount, &rv); + if (NS_FAILED(rv)) continue; + + nsAutoCString uri; + directory->GetURI(uri); + uri.Cut(0, 21); + NSString* uid = [NSString stringWithUTF8String:uri.get()]; + + unsigned int j, arrayCount = [mutableArray count]; + for (j = 0; j < arrayCount; ++j) { + if ([[[mutableArray objectAtIndex:j] uniqueId] isEqualToString:uid]) { + [mutableArray removeObjectAtIndex:j]; + break; + } + } + + if (j == arrayCount) { + UnassertDirectory(abManager, directory); + } + } + } + + enumerator = [mutableArray objectEnumerator]; + while ((card = [enumerator nextObject])) { + rv = GetOrCreateGroup([card uniqueId], getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + AssertDirectory(abManager, directory); + } + } + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +nsresult nsAbOSXDirectory::AssertChildNodes() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + // Mailing lists can't have childnodes. + if (m_IsMailList) { + return NS_OK; + } + + nsresult rv; + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + NSArray* groups = [[ABAddressBook sharedAddressBook] groups]; + + unsigned int i, count = [groups count]; + + if (count > 0 && !m_AddressList) { + m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIAbDirectory> directory; + for (i = 0; i < count; ++i) { + rv = GetOrCreateGroup([[groups objectAtIndex:i] uniqueId], getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AssertDirectory(abManager, directory); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +nsresult nsAbOSXDirectory::AssertDirectory(nsIAbManager* aManager, nsIAbDirectory* aDirectory) { + uint32_t pos; + if (m_AddressList && NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos))) + // We already have this directory, so no point in adding it again. + return NS_OK; + + nsresult rv; + if (!m_AddressList) { + m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = m_AddressList->AppendElement(aDirectory); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult nsAbOSXDirectory::AssertCard(nsIAbManager* aManager, nsIAbCard* aCard) { + nsAutoCString ourUID; + GetUID(ourUID); + aCard->SetDirectoryUID(ourUID); + + nsresult rv = + m_IsMailList ? m_AddressList->AppendElement(aCard) : mCardList->AppendElement(aCard); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the card's URI and add it to our card store + nsCOMPtr<nsIAbOSXCard> osxCard = do_QueryInterface(aCard, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString uri; + rv = osxCard->GetURI(uri); + + nsCOMPtr<nsIAbOSXCard> retrievedCard; + if (!mCardStore.Get(uri, getter_AddRefs(retrievedCard))) mCardStore.InsertOrUpdate(uri, osxCard); + + return NS_OK; +} + +nsresult nsAbOSXDirectory::UnassertCard(nsIAbManager* aManager, nsIAbCard* aCard, + nsIMutableArray* aCardList) { + uint32_t pos; + if (NS_SUCCEEDED(aCardList->IndexOf(0, aCard, &pos))) { + nsresult rv = aCardList->RemoveElementAt(pos); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult nsAbOSXDirectory::UnassertDirectory(nsIAbManager* aManager, nsIAbDirectory* aDirectory) { + NS_ENSURE_TRUE(m_AddressList, NS_ERROR_NULL_POINTER); + + uint32_t pos; + if (NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos))) { + nsresult rv = m_AddressList->RemoveElementAt(pos); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP nsAbOSXDirectory::GetChildNodes(nsTArray<RefPtr<nsIAbDirectory>>& aNodes) { + aNodes.Clear(); + // Mailing lists don't have childnodes. + if (m_IsMailList || !m_AddressList) { + return NS_OK; + } + + uint32_t count = 0; + nsresult rv = m_AddressList->GetLength(&count); + NS_ENSURE_SUCCESS(rv, rv); + aNodes.SetCapacity(count); + for (uint32_t i = 0; i < count; i++) { + nsCOMPtr<nsIAbDirectory> dir = do_QueryElementAt(m_AddressList, i, &rv); + NS_ENSURE_SUCCESS(rv, rv); + aNodes.AppendElement(&*dir); + } + return NS_OK; +} + +NS_IMETHODIMP +nsAbOSXDirectory::GetChildCardCount(uint32_t* aCount) { + nsIMutableArray* srcCards = m_IsMailList ? m_AddressList : mCardList; + return srcCards->GetLength(aCount); +} + +NS_IMETHODIMP +nsAbOSXDirectory::GetChildCards(nsTArray<RefPtr<nsIAbCard>>& aCards) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + aCards.Clear(); + + // Not a search, so just return the appropriate list of items. + nsIMutableArray* srcCards = m_IsMailList ? m_AddressList : mCardList; + uint32_t count = 0; + nsresult rv = srcCards->GetLength(&count); + NS_ENSURE_SUCCESS(rv, rv); + aCards.SetCapacity(count); + for (uint32_t i = 0; i < count; i++) { + nsCOMPtr<nsIAbCard> card = do_QueryElementAt(srcCards, i, &rv); + NS_ENSURE_SUCCESS(rv, rv); + aCards.AppendElement(&*card); + } + return NS_OK; + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +/* Recursive method that searches for a child card by URI. If it cannot find + * it within this directory, it checks all subfolders. + */ +NS_IMETHODIMP +nsAbOSXDirectory::GetCardByUri(const nsACString& aUri, nsIAbOSXCard** aResult) { + nsCOMPtr<nsIAbOSXCard> osxCard; + + // Base Case + if (mCardStore.Get(aUri, getter_AddRefs(osxCard))) { + NS_IF_ADDREF(*aResult = osxCard); + return NS_OK; + } + // Search children + nsTArray<RefPtr<nsIAbDirectory>> children; + nsresult rv = this->GetChildNodes(children); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIAbDirectory* dir : children) { + nsCOMPtr<nsIAbOSXDirectory> childDirectory = do_QueryInterface(dir, &rv); + if (NS_SUCCEEDED(rv)) { + rv = childDirectory->GetCardByUri(aUri, getter_AddRefs(osxCard)); + if (NS_SUCCEEDED(rv)) { + NS_IF_ADDREF(*aResult = osxCard); + return NS_OK; + } + } + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsAbOSXDirectory::GetCardFromProperty(const char* aProperty, const nsACString& aValue, + bool aCaseSensitive, nsIAbCard** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = nullptr; + + if (aValue.IsEmpty()) return NS_OK; + + nsIMutableArray* list = m_IsMailList ? m_AddressList : mCardList; + + if (!list) return NS_OK; + + uint32_t length; + nsresult rv = list->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbCard> card; + nsAutoCString cardValue; + + for (uint32_t i = 0; i < length && !*aResult; ++i) { + card = do_QueryElementAt(list, i, &rv); + if (NS_SUCCEEDED(rv)) { + rv = card->GetPropertyAsAUTF8String(aProperty, cardValue); + if (NS_SUCCEEDED(rv)) { + bool equal = aCaseSensitive ? cardValue.Equals(aValue) + : cardValue.Equals(aValue, nsCaseInsensitiveCStringComparator); + if (equal) NS_IF_ADDREF(*aResult = card); + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsAbOSXDirectory::GetCardsFromProperty(const char* aProperty, const nsACString& aValue, + bool aCaseSensitive, nsTArray<RefPtr<nsIAbCard>>& aResult) { + aResult.Clear(); + if (aValue.IsEmpty()) return NS_OK; + + nsIMutableArray* list = m_IsMailList ? m_AddressList : mCardList; + if (!list) return NS_OK; + + uint32_t length; + nsresult rv = list->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + aResult.SetCapacity(length); + for (uint32_t i = 0; i < length; ++i) { + nsCOMPtr<nsIAbCard> card = do_QueryElementAt(list, i, &rv); + if (NS_SUCCEEDED(rv)) { + nsAutoCString cardValue; + rv = card->GetPropertyAsAUTF8String(aProperty, cardValue); + if (NS_SUCCEEDED(rv)) { + bool equal = aCaseSensitive ? cardValue.Equals(aValue) + : cardValue.Equals(aValue, nsCaseInsensitiveCStringComparator); + if (equal) aResult.AppendElement(&*card); + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsAbOSXDirectory::CardForEmailAddress(const nsACString& aEmailAddress, nsIAbCard** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = nullptr; + + if (aEmailAddress.IsEmpty()) return NS_OK; + + nsIMutableArray* list = m_IsMailList ? m_AddressList : mCardList; + + if (!list) return NS_OK; + + uint32_t length; + nsresult rv = list->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbCard> card; + + for (uint32_t i = 0; i < length && !*aResult; ++i) { + card = do_QueryElementAt(list, i, &rv); + if (NS_SUCCEEDED(rv)) { + bool hasEmailAddress = false; + + rv = card->HasEmailAddress(aEmailAddress, &hasEmailAddress); + if (NS_SUCCEEDED(rv) && hasEmailAddress) NS_IF_ADDREF(*aResult = card); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsAbOSXDirectory::HasCard(nsIAbCard* aCard, bool* aHasCard) { + NS_ENSURE_ARG_POINTER(aCard); + NS_ENSURE_ARG_POINTER(aHasCard); + + nsresult rv = NS_OK; + uint32_t index; + if (m_IsMailList) { + if (m_AddressList) rv = m_AddressList->IndexOf(0, aCard, &index); + } else if (mCardList) + rv = mCardList->IndexOf(0, aCard, &index); + + *aHasCard = NS_SUCCEEDED(rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsAbOSXDirectory::HasDirectory(nsIAbDirectory* aDirectory, bool* aHasDirectory) { + NS_ENSURE_ARG_POINTER(aDirectory); + NS_ENSURE_ARG_POINTER(aHasDirectory); + + *aHasDirectory = false; + + uint32_t pos; + if (m_AddressList && NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos))) + *aHasDirectory = true; + + return NS_OK; +} + +NS_IMETHODIMP +nsAbOSXDirectory::Search(const nsAString& query, const nsAString& searchString, + nsIAbDirSearchListener* listener) { + nsresult rv; + + nsCOMPtr<nsIAbBooleanExpression> expression; + rv = nsAbQueryStringToExpression::Convert(NS_ConvertUTF16toUTF8(query), + getter_AddRefs(expression)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbDirectoryQueryArguments> arguments = + do_CreateInstance("@mozilla.org/addressbook/directory/query-arguments;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = arguments->SetExpression(expression); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't search the subdirectories. If the current directory is a mailing + // list, it won't have any subdirectories. If the current directory is an + // addressbook, searching both it and the subdirectories (the mailing + // lists), will yield duplicate results because every entry in a mailing + // list will be an entry in the parent addressbook. + rv = arguments->SetQuerySubDirectories(false); + NS_ENSURE_SUCCESS(rv, rv); + + // Initiate the proxy query with the no query directory + nsCOMPtr<nsIAbDirectoryQueryProxy> queryProxy = + do_CreateInstance("@mozilla.org/addressbook/directory-query/proxy;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = queryProxy->Initiate(); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t context = 0; + rv = queryProxy->DoQuery(this, arguments, listener, -1, 0, &context); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult nsAbOSXDirectory::DeleteUid(const nsACString& aUid) { + if (!m_AddressList) return NS_ERROR_NULL_POINTER; + + nsresult rv; + nsCOMPtr<nsIAbManager> abManager = do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // At this stage we don't know if aUid represents a card or group. The OS X + // interfaces don't give us chance to find out, so we have to go through + // our lists to find it. + + // First, we'll see if its in the group list as it is likely to be shorter. + + // See if this item is in our address list + uint32_t addressCount; + rv = m_AddressList->GetLength(&addressCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString uri(NS_ABOSXDIRECTORY_URI_PREFIX); + uri.Append(aUid); + + // Iterate backwards in case we remove something + while (addressCount--) { + nsCOMPtr<nsISupports> abItem(do_QueryElementAt(m_AddressList, addressCount, &rv)); + if (NS_FAILED(rv)) continue; + + nsCOMPtr<nsIAbDirectory> directory(do_QueryInterface(abItem, &rv)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString dirUri; + directory->GetURI(dirUri); + if (uri.Equals(dirUri)) return UnassertDirectory(abManager, directory); + } else { + nsCOMPtr<nsIAbOSXCard> osxCard(do_QueryInterface(abItem, &rv)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString cardUri; + osxCard->GetURI(cardUri); + if (uri.Equals(cardUri)) { + nsCOMPtr<nsIAbCard> card(do_QueryInterface(osxCard, &rv)); + if (NS_SUCCEEDED(rv)) return UnassertCard(abManager, card, m_AddressList); + } + } + } + } + + // Second, see if it is one of the cards. + if (!mCardList) return NS_ERROR_FAILURE; + + uri = NS_ABOSXCARD_URI_PREFIX; + uri.Append(aUid); + + rv = mCardList->GetLength(&addressCount); + NS_ENSURE_SUCCESS(rv, rv); + + while (addressCount--) { + nsCOMPtr<nsIAbOSXCard> osxCard(do_QueryElementAt(mCardList, addressCount, &rv)); + if (NS_FAILED(rv)) continue; + + nsAutoCString cardUri; + osxCard->GetURI(cardUri); + + if (uri.Equals(cardUri)) { + nsCOMPtr<nsIAbCard> card(do_QueryInterface(osxCard, &rv)); + if (NS_SUCCEEDED(rv)) return UnassertCard(abManager, card, mCardList); + } + } + return NS_OK; +} diff --git a/comm/mailnews/addrbook/src/nsAbOSXUtils.h b/comm/mailnews/addrbook/src/nsAbOSXUtils.h new file mode 100644 index 0000000000..42a57f45f6 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOSXUtils.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#ifndef nsAbOSXUtils_h___ +#define nsAbOSXUtils_h___ + +#include <Foundation/NSString.h> +#include "nsString.h" + +NSString* WrapString(const nsString& aString); +void AppendToString(const NSString* aString, nsString& aResult); +void AssignToString(const NSString* aString, nsString& aResult); +void AppendToCString(const NSString* aString, nsCString& aResult); + +struct nsAbOSXPropertyMap { + NSString* const mOSXProperty; + NSString* const mOSXLabel; + NSString* const mOSXKey; + const char* mPropertyName; +}; + +class nsAbOSXUtils { + public: + static const nsAbOSXPropertyMap kPropertyMap[]; + static const uint32_t kPropertyMapSize; +}; + +#endif // nsAbOSXUtils_h___ diff --git a/comm/mailnews/addrbook/src/nsAbOSXUtils.mm b/comm/mailnews/addrbook/src/nsAbOSXUtils.mm new file mode 100644 index 0000000000..5c6f7b2b8d --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOSXUtils.mm @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsAbOSXUtils.h" +#include "nsString.h" +#include "nsAbOSXCard.h" +#include "nsMemory.h" +#include "mozilla/ArrayUtils.h" +using namespace mozilla; + +#include <AddressBook/AddressBook.h> +#define kABDepartmentProperty (kABDepartmentProperty ? kABDepartmentProperty : @"ABDepartment") + +NSString* WrapString(const nsString& aString) { + unichar* chars = reinterpret_cast<unichar*>(const_cast<char16_t*>(aString.get())); + + return [NSString stringWithCharacters:chars length:aString.Length()]; +} + +void AppendToString(const NSString* aString, nsString& aResult) { + if (aString) { + const char* chars = [aString UTF8String]; + if (chars) { + aResult.Append(NS_ConvertUTF8toUTF16(chars)); + } + } +} + +void AssignToString(const NSString* aString, nsString& aResult) { + if (aString) { + const char* chars = [aString UTF8String]; + if (chars) CopyUTF8toUTF16(nsDependentCString(chars), aResult); + } +} + +void AppendToCString(const NSString* aString, nsCString& aResult) { + if (aString) { + const char* chars = [aString UTF8String]; + if (chars) { + aResult.Append(chars); + } + } +} + +// Some properties can't be easily mapped back and forth. +#define DONT_MAP(moz_name, osx_property, osx_label, osx_key) + +#define DEFINE_PROPERTY(moz_name, osx_property, osx_label, osx_key) \ + {osx_property, osx_label, osx_key, #moz_name}, + +// clang-format off +const nsAbOSXPropertyMap nsAbOSXUtils::kPropertyMap[] = { + DEFINE_PROPERTY(FirstName, kABFirstNameProperty, nil, nil) + DEFINE_PROPERTY(LastName, kABLastNameProperty, nil, nil) + DONT_MAP("DisplayName", nil, nil, nil) + DEFINE_PROPERTY(PhoneticFirstName, kABFirstNamePhoneticProperty, nil, nil) + DEFINE_PROPERTY(PhoneticLastName, kABLastNamePhoneticProperty, nil, nil) + DEFINE_PROPERTY(NickName, kABNicknameProperty, nil, nil) + DONT_MAP(PrimaryEmail, kABEmailProperty, nil, nil) + DONT_MAP(SecondEmail, kABEmailProperty, nil, nil) + DEFINE_PROPERTY(WorkPhone, kABPhoneProperty, kABPhoneWorkLabel, nil) + DEFINE_PROPERTY(HomePhone, kABPhoneProperty, kABPhoneHomeLabel, nil) + DEFINE_PROPERTY(FaxNumber, kABPhoneProperty, kABPhoneWorkFAXLabel, nil) + DEFINE_PROPERTY(PagerNumber, kABPhoneProperty, kABPhonePagerLabel, nil) + DEFINE_PROPERTY(CellularNumber, kABPhoneProperty, kABPhoneMobileLabel, nil) + DEFINE_PROPERTY(HomeAddress, kABAddressProperty, kABAddressHomeLabel, + kABAddressStreetKey) + DEFINE_PROPERTY(HomeCity, kABAddressProperty, kABAddressHomeLabel, + kABAddressCityKey) + DEFINE_PROPERTY(HomeState, kABAddressProperty, kABAddressHomeLabel, + kABAddressStateKey) + DEFINE_PROPERTY(HomeZipCode, kABAddressProperty, kABAddressHomeLabel, + kABAddressZIPKey) + DEFINE_PROPERTY(HomeCountry, kABAddressProperty, kABAddressHomeLabel, + kABAddressCountryKey) + DEFINE_PROPERTY(WorkAddress, kABAddressProperty, kABAddressWorkLabel, + kABAddressStreetKey) + DEFINE_PROPERTY(WorkCity, kABAddressProperty, kABAddressWorkLabel, + kABAddressCityKey) + DEFINE_PROPERTY(WorkState, kABAddressProperty, kABAddressWorkLabel, + kABAddressStateKey) + DEFINE_PROPERTY(WorkZipCode, kABAddressProperty, kABAddressWorkLabel, + kABAddressZIPKey) + DEFINE_PROPERTY(WorkCountry, kABAddressProperty, kABAddressWorkLabel, + kABAddressCountryKey) + DEFINE_PROPERTY(JobTitle, kABJobTitleProperty, nil, nil) + DEFINE_PROPERTY(Department, kABDepartmentProperty, nil, nil) + DEFINE_PROPERTY(Company, kABOrganizationProperty, nil, nil) + // This was kABAIMInstantProperty previously, but it was deprecated in OS X 10.7. + DONT_MAP(_AimScreenName, kABInstantMessageProperty, nil, nil) + DEFINE_PROPERTY(WebPage1, kABHomePageProperty, nil, nil) + DONT_MAP(WebPage2, kABHomePageProperty, nil, nil) + DONT_MAP(BirthYear, "birthyear", nil, nil) + DONT_MAP(BirthMonth, "birthmonth", nil, nil) + DONT_MAP(BirthDay, "birthday", nil, nil) + DONT_MAP(Custom1, "custom1", nil, nil) + DONT_MAP(Custom2, "custom2", nil, nil) + DONT_MAP(Custom3, "custom3", nil, nil) + DONT_MAP(Custom4, "custom4", nil, nil) + DEFINE_PROPERTY(Note, kABNoteProperty, nil, nil) + DONT_MAP("LastModifiedDate", modifytimestamp, nil, nil) +}; +// clang-format on + +const uint32_t nsAbOSXUtils::kPropertyMapSize = ArrayLength(nsAbOSXUtils::kPropertyMap); diff --git a/comm/mailnews/addrbook/src/nsAbOutlookDirectory.cpp b/comm/mailnews/addrbook/src/nsAbOutlookDirectory.cpp new file mode 100644 index 0000000000..77568a62f2 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOutlookDirectory.cpp @@ -0,0 +1,1418 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +#include "nsAbOutlookDirectory.h" +#include "nsAbWinHelper.h" + +#include "nsString.h" +#include "nsAbDirectoryQuery.h" +#include "nsIAbBooleanExpression.h" +#include "nsIAbManager.h" +#include "nsAbQueryStringToExpression.h" +#include "nsEnumeratorUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "mozilla/Logging.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsCRTGlue.h" +#include "nsArrayUtils.h" +#include "nsMsgUtils.h" +#include "nsQueryObject.h" +#include "mozilla/Services.h" +#include "nsIObserverService.h" +#include "mozilla/JSONStringWriteFuncs.h" + +#define PRINT_TO_CONSOLE 0 +#if PRINT_TO_CONSOLE +# define PRINTF(args) printf args +#else +static mozilla::LazyLogModule gAbOutlookDirectoryLog("AbOutlookDirectory"); +# define PRINTF(args) \ + MOZ_LOG(gAbOutlookDirectoryLog, mozilla::LogLevel::Debug, args) +#endif + +nsAbOutlookDirectory::nsAbOutlookDirectory(void) + : nsAbDirProperty(), + mDirEntry(nullptr), + mCurrentQueryId(0), + mSearchContext(-1) { + mDirEntry = new nsMapiEntry; +} + +nsAbOutlookDirectory::~nsAbOutlookDirectory(void) { + if (mDirEntry) { + delete mDirEntry; + } +} + +NS_IMPL_ISUPPORTS_INHERITED(nsAbOutlookDirectory, nsAbDirProperty, + nsIAbDirectoryQuery, nsIAbDirSearchListener) + +NS_IMETHODIMP nsAbOutlookDirectory::Init(const char* aUri) { + nsresult rv = nsAbDirProperty::Init(aUri); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString entry; + makeEntryIdFromURI(kOutlookDirectoryScheme, mURI.get(), entry); + nsAbWinHelperGuard mapiAddBook; + nsAutoString unichars; + ULONG objectType = 0; + + if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE; + + mDirEntry->Assign(entry); + if (!mapiAddBook->GetPropertyLong(*mDirEntry, PR_OBJECT_TYPE, objectType)) { + PRINTF(("Cannot get type.\n")); + return NS_ERROR_FAILURE; + } + if (!mapiAddBook->GetPropertyUString(*mDirEntry, PR_DISPLAY_NAME_W, + unichars)) { + PRINTF(("Cannot get name.\n")); + return NS_ERROR_FAILURE; + } + + m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + mCardList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (objectType == MAPI_DISTLIST) { + m_IsMailList = true; + SetDirName(unichars); + // For a mailing list, we get all the cards into our member variable. + rv = GetCards(m_AddressList, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } else { + m_IsMailList = false; + if (unichars.IsEmpty()) { + SetDirName(u"Outlook"_ns); + } else { + SetDirName(unichars); + } + // First, get the mailing lists, then the cards. + rv = GetNodes(m_AddressList); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetCards(mCardList, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// nsIAbDirectory methods + +NS_IMETHODIMP nsAbOutlookDirectory::GetDirType(int32_t* aDirType) { + NS_ENSURE_ARG_POINTER(aDirType); + *aDirType = nsIAbManager::MAPI_DIRECTORY_TYPE; + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::GetURI(nsACString& aURI) { + if (mURI.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + + aURI = mURI; + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::GetChildNodes( + nsTArray<RefPtr<nsIAbDirectory>>& aNodes) { + aNodes.Clear(); + // Mailing lists don't have childnodes. + if (m_IsMailList || !m_AddressList) { + return NS_OK; + } + + uint32_t count = 0; + nsresult rv = m_AddressList->GetLength(&count); + NS_ENSURE_SUCCESS(rv, rv); + aNodes.SetCapacity(count); + for (uint32_t i = 0; i < count; i++) { + nsCOMPtr<nsIAbDirectory> dir = do_QueryElementAt(m_AddressList, i, &rv); + NS_ENSURE_SUCCESS(rv, rv); + aNodes.AppendElement(&*dir); + } + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::GetChildCardCount(uint32_t* aCount) { + nsIMutableArray* srcCards = m_IsMailList ? m_AddressList : mCardList; + return srcCards->GetLength(aCount); +} + +NS_IMETHODIMP nsAbOutlookDirectory::GetChildCards( + nsTArray<RefPtr<nsIAbCard>>& aCards) { + aCards.Clear(); + + // Not a search, so just return the appropriate list of items. + nsIMutableArray* srcCards = m_IsMailList ? m_AddressList : mCardList; + uint32_t count = 0; + nsresult rv = srcCards->GetLength(&count); + NS_ENSURE_SUCCESS(rv, rv); + aCards.SetCapacity(count); + for (uint32_t i = 0; i < count; i++) { + nsCOMPtr<nsIAbCard> card = do_QueryElementAt(srcCards, i, &rv); + NS_ENSURE_SUCCESS(rv, rv); + aCards.AppendElement(&*card); + } + return NS_OK; +} + +// This is an exact copy of nsAbOSXDirectory::HasCard(). +NS_IMETHODIMP nsAbOutlookDirectory::HasCard(nsIAbCard* aCard, bool* aHasCard) { + NS_ENSURE_ARG_POINTER(aCard); + NS_ENSURE_ARG_POINTER(aHasCard); + + nsresult rv = NS_OK; + uint32_t index; + if (m_IsMailList) { + if (m_AddressList) rv = m_AddressList->IndexOf(0, aCard, &index); + } else if (mCardList) + rv = mCardList->IndexOf(0, aCard, &index); + + *aHasCard = NS_SUCCEEDED(rv); + + return NS_OK; +} + +// This is an exact copy of nsAbOSXDirectory::HasDirectory(). +NS_IMETHODIMP nsAbOutlookDirectory::HasDirectory(nsIAbDirectory* aDirectory, + bool* aHasDirectory) { + NS_ENSURE_ARG_POINTER(aDirectory); + NS_ENSURE_ARG_POINTER(aHasDirectory); + + *aHasDirectory = false; + + uint32_t pos; + if (m_AddressList && + NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos))) + *aHasDirectory = true; + + return NS_OK; +} + +// This is an exact copy of nsAbOSXDirectory::CardForEmailAddress(). +NS_IMETHODIMP +nsAbOutlookDirectory::CardForEmailAddress(const nsACString& aEmailAddress, + nsIAbCard** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = nullptr; + + if (aEmailAddress.IsEmpty()) return NS_OK; + + nsIMutableArray* list = m_IsMailList ? m_AddressList : mCardList; + + if (!list) return NS_OK; + + uint32_t length; + nsresult rv = list->GetLength(&length); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbCard> card; + + for (uint32_t i = 0; i < length && !*aResult; ++i) { + card = do_QueryElementAt(list, i, &rv); + if (NS_SUCCEEDED(rv)) { + bool hasEmailAddress = false; + + rv = card->HasEmailAddress(aEmailAddress, &hasEmailAddress); + if (NS_SUCCEEDED(rv) && hasEmailAddress) NS_IF_ADDREF(*aResult = card); + } + } + return NS_OK; +} + +nsresult nsAbOutlookDirectory::ExtractCardEntry(nsIAbCard* aCard, + nsCString& aEntry) { + aEntry.Truncate(); + + nsCString uri; + aCard->GetPropertyAsAUTF8String("OutlookEntryURI", uri); + + // If we don't have a URI, uri will be empty. makeEntryIdFromURI doesn't set + // aEntry to anything if uri is empty, so it will be truncated, allowing us + // to accept cards not initialized by us. + makeEntryIdFromURI(kOutlookCardScheme, uri.get(), aEntry); + return NS_OK; +} + +nsresult nsAbOutlookDirectory::ExtractDirectoryEntry(nsIAbDirectory* aDirectory, + nsCString& aEntry) { + aEntry.Truncate(); + nsCString uri; + nsresult rv = aDirectory->GetURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + + makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), aEntry); + + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::DeleteCards( + const nsTArray<RefPtr<nsIAbCard>>& aCards) { + nsresult retCode = NS_OK; + nsAbWinHelperGuard mapiAddBook; + + if (!mapiAddBook->IsOK()) { + return NS_ERROR_FAILURE; + } + + nsAutoCString cardEntryString; + nsMapiEntry cardEntry; + + for (auto card : aCards) { + retCode = ExtractCardEntry(card, cardEntryString); + if (NS_SUCCEEDED(retCode) && !cardEntryString.IsEmpty()) { + cardEntry.Assign(cardEntryString); + bool success = false; + if (m_IsMailList) { + nsAutoCString uri(mURI); + // Trim off the mailing list entry ID from the mailing list URI + // to get the top-level directory entry ID. + nsAutoCString topEntryString; + int32_t slashPos = uri.RFindChar('/'); + uri.SetLength(slashPos); + makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), topEntryString); + nsMapiEntry topDirEntry; + topDirEntry.Assign(topEntryString); + success = + mapiAddBook->DeleteEntryfromDL(topDirEntry, *mDirEntry, cardEntry); + } else { + success = mapiAddBook->DeleteEntry(*mDirEntry, cardEntry); + } + if (!success) { + PRINTF(("Cannot delete card %s.\n", cardEntryString.get())); + } else { + if (m_IsMailList) { + // It appears that removing a card from a mailing list makes + // our list go stale, so refresh it. + m_AddressList->Clear(); + GetCards(m_AddressList, nullptr); + } else if (mCardList) { + uint32_t pos; + if (NS_SUCCEEDED(mCardList->IndexOf(0, card, &pos))) + mCardList->RemoveElementAt(pos); + } + retCode = NotifyItemDeletion(card, true); + NS_ENSURE_SUCCESS(retCode, retCode); + + card->SetDirectoryUID(EmptyCString()); + } + } else { + PRINTF(("Card doesn't belong in this directory.\n")); + } + } + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::DeleteDirectory( + nsIAbDirectory* aDirectory) { + if (!aDirectory) { + return NS_ERROR_NULL_POINTER; + } + nsresult retCode = NS_OK; + nsAbWinHelperGuard mapiAddBook; + nsAutoCString dirEntryString; + + if (!mapiAddBook->IsOK()) { + return NS_ERROR_FAILURE; + } + retCode = ExtractDirectoryEntry(aDirectory, dirEntryString); + if (NS_SUCCEEDED(retCode) && !dirEntryString.IsEmpty()) { + nsMapiEntry directoryEntry; + + directoryEntry.Assign(dirEntryString); + if (!mapiAddBook->DeleteEntry(*mDirEntry, directoryEntry)) { + PRINTF(("Cannot delete directory %s.\n", dirEntryString.get())); + } else { + uint32_t pos; + if (m_AddressList && + NS_SUCCEEDED(m_AddressList->IndexOf(0, aDirectory, &pos))) + m_AddressList->RemoveElementAt(pos); + + // Iterate over the cards of the directory to find the one + // representing the mailing list and also remove it. + if (mCardList) { + nsAutoCString listUID; + aDirectory->GetUID(listUID); + + uint32_t nbCards = 0; + nsresult rv = mCardList->GetLength(&nbCards); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t i = 0; i < nbCards; i++) { + nsCOMPtr<nsIAbCard> card = do_QueryElementAt(mCardList, i, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString cardUID; + rv = card->GetUID(cardUID); + NS_ENSURE_SUCCESS(rv, rv); + if (cardUID.Equals(listUID)) { + mCardList->RemoveElementAt(i); + break; + } + } + } + retCode = NotifyItemDeletion(aDirectory, false); + NS_ENSURE_SUCCESS(retCode, retCode); + } + } else { + PRINTF(("Directory doesn't belong to this folder.\n")); + } + return retCode; +} + +NS_IMETHODIMP nsAbOutlookDirectory::AddCard(nsIAbCard* aCard, + nsIAbCard** aNewCard) { + NS_ENSURE_ARG_POINTER(aCard); + NS_ENSURE_ARG_POINTER(aNewCard); + + *aNewCard = nullptr; + nsresult retCode = NS_OK; + nsAbWinHelperGuard mapiAddBook; + nsMapiEntry newEntry; + nsAutoCString cardEntryString; + bool isNewCard = false; + nsCOMPtr<nsIAbDirectory> topDir; + + if (!mapiAddBook->IsOK()) { + return NS_ERROR_FAILURE; + } + + nsAutoCString ourUID; + if (!m_IsMailList) { + // We're not dealing with a mailing list, so just create a new entry. + if (!mapiAddBook->CreateEntry(*mDirEntry, newEntry)) { + return NS_ERROR_FAILURE; + } + isNewCard = true; + GetUID(ourUID); + } else { + nsAutoCString dirURI(mURI); + // Trim off the mailing list entry ID from the mailing list URI + // to get the top-level directory entry ID. + nsAutoCString topEntryString; + int32_t slashPos = dirURI.RFindChar('/'); + dirURI.SetLength(slashPos); + makeEntryIdFromURI(kOutlookDirectoryScheme, dirURI.get(), topEntryString); + nsMapiEntry topDirEntry; + topDirEntry.Assign(topEntryString); + + // Add a card to a mailing list. We distinguish two cases: + // If there is already an Outlook card, we can just add it. + // If none exists, we need to create it first. Outlook has an option + // that allows a creation of a mailing list member solely in the list + // but we don't support this for now to avoid more MAPI complication. + retCode = ExtractCardEntry(aCard, cardEntryString); + if (NS_SUCCEEDED(retCode) && !cardEntryString.IsEmpty()) { + newEntry.Assign(cardEntryString); + } else { + if (!mapiAddBook->CreateEntry(topDirEntry, newEntry)) { + return NS_ERROR_FAILURE; + } + isNewCard = true; + } + nsAutoString display; + nsAutoString email; + aCard->GetDisplayName(display); + aCard->GetPrimaryEmail(email); + if (!mapiAddBook->AddEntryToDL(topDirEntry, *mDirEntry, newEntry, + display.get(), email.get())) { + return NS_ERROR_FAILURE; + } + // The UID of the card is the top directory's UID. + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &retCode)); + NS_ENSURE_SUCCESS(retCode, retCode); + retCode = abManager->GetDirectory(dirURI, getter_AddRefs(topDir)); + NS_ENSURE_SUCCESS(retCode, retCode); + topDir->GetUID(ourUID); + } + + newEntry.ToString(cardEntryString); + nsAutoCString cardURI(kOutlookCardScheme); + cardURI.Append(cardEntryString); + + nsCOMPtr<nsIAbCard> newCard; + retCode = OutlookCardForURI(cardURI, getter_AddRefs(newCard)); + NS_ENSURE_SUCCESS(retCode, retCode); + + // Make sure the card has a UID before setting its directory UID. + // This is a bit of a hack. If we get the UID of the card before setting its + // directory UID, we can avoid an unwanted `ModifyCard()` call inside + // `nsAbCardProperty::SetUID()`. + nsCString dummy; + newCard->GetUID(dummy); + newCard->SetDirectoryUID(ourUID); + + if (isNewCard) { + retCode = newCard->Copy(aCard); + NS_ENSURE_SUCCESS(retCode, retCode); + + // Set a decent display name of the card. This needs to be set + // on the card and not on the related contact via `SetPropertiesUString()`. + nsAutoString displayName; + newCard->GetDisplayName(displayName); + mapiAddBook->SetPropertyUString(newEntry, PR_DISPLAY_NAME_W, + displayName.get()); + + if (m_IsMailList) { + // Observed behavior for a new card in a mailing list is that + // Outlook returns __MailUser__ as first name. That value was + // previously set as display name when creating the bare card. + nsAutoString firstName; + newCard->GetFirstName(firstName); + if (StringBeginsWith(firstName, + NS_LITERAL_STRING_FROM_CSTRING(kDummyDisplayName))) { + newCard->SetFirstName(EmptyString()); + } + } + + retCode = ModifyCardInternal(newCard, true); + NS_ENSURE_SUCCESS(retCode, retCode); + } + + if (m_IsMailList) { + m_AddressList->AppendElement(newCard); + if (isNewCard) { + // Add the new card to the cards of the top directory as well. + nsAbOutlookDirectory* topDirOL = + static_cast<nsAbOutlookDirectory*>(topDir.get()); + topDirOL->mCardList->AppendElement(newCard); + } + } else { + mCardList->AppendElement(newCard); + } + + NotifyItemAddition(newCard, true); + + newCard.forget(aNewCard); + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::DropCard(nsIAbCard* aData, + bool needToCopyCard) { + nsCOMPtr<nsIAbCard> addedCard; + return AddCard(aData, getter_AddRefs(addedCard)); +} + +NS_IMETHODIMP nsAbOutlookDirectory::AddMailList(nsIAbDirectory* aMailList, + nsIAbDirectory** addedList) { + NS_ENSURE_ARG_POINTER(aMailList); + NS_ENSURE_ARG_POINTER(addedList); + if (m_IsMailList) return NS_OK; + + nsAbWinHelperGuard mapiAddBook; + nsMapiEntry newEntry; + nsAutoCString newEntryString; + + if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE; + + nsAutoString name; + nsresult rv = aMailList->GetDirName(name); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mapiAddBook->CreateDistList(*mDirEntry, newEntry, name.get())) + return NS_ERROR_FAILURE; + + newEntry.ToString(newEntryString); + nsAutoCString uri(mURI); + nsAutoCString topEntryString; + makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), topEntryString); + uri.Append('/'); + uri.Append(newEntryString); + + RefPtr<nsAbOutlookDirectory> directory = new nsAbOutlookDirectory; + + // We will later need the URI of the parent directory, so store it here. + directory->mParentEntryId = topEntryString; + + // Light-weight initialisation. `nsAbOutlookDirectory::Init()` will get + // the object type wrong since we don't have cards yet and scan for cards + // which we don't have yet. + rv = directory->nsAbDirProperty::Init(uri.get()); + NS_ENSURE_SUCCESS(rv, rv); + directory->mDirEntry->Assign(newEntryString); + directory->m_IsMailList = true; + directory->m_AddressList = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbDirectory> newList = do_QueryObject(directory); + + rv = newList->CopyMailList(aMailList); + NS_ENSURE_SUCCESS(rv, rv); + + // Also create a card to match the list. + // That needs to happen before the notification. + nsAutoCString cardURI(kOutlookCardScheme); + cardURI.Append(newEntryString); + nsCOMPtr<nsIAbCard> newCard = + do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + newCard->SetPropertyAsAUTF8String("OutlookEntryURI", cardURI); + + // This is a bit of a hack. If we set the UID of the card before setting its + // directory UID, we can avoid an unwanted `ModifyCard()` call inside + // `nsAbCardProperty::SetUID()`. + nsAutoCString listUID; + newList->GetUID(listUID); + newCard->SetUID(listUID); + nsAutoCString ourUID; + GetUID(ourUID); + newCard->SetDirectoryUID(ourUID); + newCard->SetIsMailList(true); + newCard->SetMailListURI(uri.get()); + newCard->SetDisplayName(name); + newCard->SetLastName(name); + + mCardList->AppendElement(newCard); + m_AddressList->AppendElement(newList); + + NotifyItemAddition(newList, false); + + newList.forget(addedList); + return rv; +} + +NS_IMETHODIMP nsAbOutlookDirectory::EditMailListToDatabase( + nsIAbCard* listCard) { + nsresult rv; + nsString name; + nsAbWinHelperGuard mapiAddBook; + + if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE; + + rv = GetDirName(name); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mapiAddBook->SetPropertyUString(*mDirEntry, PR_DISPLAY_NAME_W, + name.get())) + return NS_ERROR_FAILURE; + + // Iterate over the cards of the parent directory to find the one + // representing the mailing list and also change its name. + nsAutoCString uri(mURI); + // Trim off the mailing list entry ID from the mailing list URI + // to get the top-level directory entry ID. + nsAutoCString topEntryString; + int32_t slashPos = uri.RFindChar('/'); + uri.SetLength(slashPos); + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIAbDirectory> parent; + rv = abManager->GetDirectory(uri, getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString listUID; + GetUID(listUID); + + uint32_t nbCards = 0; + nsAbOutlookDirectory* olDir = + static_cast<nsAbOutlookDirectory*>(parent.get()); + rv = olDir->mCardList->GetLength(&nbCards); + NS_ENSURE_SUCCESS(rv, rv); + for (uint32_t i = 0; i < nbCards; i++) { + nsCOMPtr<nsIAbCard> card = do_QueryElementAt(olDir->mCardList, i, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString cardUID; + rv = card->GetUID(cardUID); + NS_ENSURE_SUCCESS(rv, rv); + if (cardUID.Equals(listUID)) { + card->SetDisplayName(name); + break; + } + } + + nsAutoCString dirUID; + if (listCard) { + // For mailing list cards, we use the UID of the top level directory. + listCard->GetDirectoryUID(dirUID); + NotifyItemModification(listCard, true, dirUID.get()); + } + nsCOMPtr<nsIAbDirectory> dir = do_QueryObject(this); + // Use the UID of the parent. + parent->GetUID(dirUID); + NotifyItemModification(dir, false, dirUID.get()); + return NS_OK; +} + +static nsresult FindPrimaryEmailCondition(nsIAbBooleanExpression* aLevel, + nsAString& value) { + if (!aLevel) { + return NS_ERROR_NULL_POINTER; + } + nsresult retCode = NS_OK; + nsTArray<RefPtr<nsISupports>> expressions; + + retCode = aLevel->GetExpressions(expressions); + NS_ENSURE_SUCCESS(retCode, retCode); + + for (uint32_t i = 0; i < expressions.Length(); ++i) { + RefPtr<nsIAbBooleanConditionString> condition = + do_QueryObject(expressions[i], &retCode); + if (NS_SUCCEEDED(retCode)) { + nsCString name; + retCode = condition->GetName(getter_Copies(name)); + NS_ENSURE_SUCCESS(retCode, retCode); + if (name.EqualsLiteral("PrimaryEmail")) { + // We found a leaf in the boolean expression tree that compares + // "PrimaryEmail". So return the value and be done. + retCode = condition->GetValue(getter_Copies(value)); + return retCode; + } + continue; + } + + RefPtr<nsIAbBooleanExpression> subExpression = + do_QueryObject(expressions[i], &retCode); + if (NS_SUCCEEDED(retCode)) { + // Recurse into the sub-tree. + retCode = FindPrimaryEmailCondition(subExpression, value); + // If we found our leaf there, we're done. + if (NS_SUCCEEDED(retCode)) return retCode; + } + } + return NS_ERROR_UNEXPECTED; +} + +static nsresult GetConditionValue(nsIAbDirectoryQueryArguments* aArguments, + nsAString& value) { + if (!aArguments) { + return NS_ERROR_NULL_POINTER; + } + nsresult retCode = NS_OK; + + nsCOMPtr<nsISupports> supports; + retCode = aArguments->GetExpression(getter_AddRefs(supports)); + NS_ENSURE_SUCCESS(retCode, retCode); + nsCOMPtr<nsIAbBooleanExpression> booleanQuery = + do_QueryInterface(supports, &retCode); + NS_ENSURE_SUCCESS(retCode, retCode); + + // Outlook can only query the PR_ANR property. So get its value from the + // PrimaryEmail condition. + retCode = FindPrimaryEmailCondition(booleanQuery, value); + return retCode; +} + +NS_IMETHODIMP nsAbOutlookDirectory::DoQuery( + nsIAbDirectory* aDirectory, nsIAbDirectoryQueryArguments* aArguments, + nsIAbDirSearchListener* aListener, int32_t aResultLimit, int32_t aTimeout, + int32_t* aReturnValue) { + if (!aArguments || !aListener || !aReturnValue) { + return NS_ERROR_NULL_POINTER; + } + + // The only thing we can search here is PR_ANR. All other properties are + // skipped. Note that PR_ANR also searches in the recipient's name and + // e-mail address. + // https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/address-book-restrictions + // states: + // Ambiguous name restrictions are property restrictions using the PR_ANR + // property to match recipient names with entries in address book containers. + + SRestriction restriction; + SPropValue val; + restriction.rt = RES_PROPERTY; + restriction.res.resProperty.relop = RELOP_EQ; + restriction.res.resProperty.ulPropTag = PR_ANR_W; + restriction.res.resProperty.lpProp = &val; + restriction.res.resProperty.lpProp->ulPropTag = PR_ANR_W; + + nsAutoString value; + nsresult rv = GetConditionValue(aArguments, value); + NS_ENSURE_SUCCESS(rv, rv); + restriction.res.resProperty.lpProp->Value.lpszW = value.get(); + + rv = ExecuteQuery(&restriction, aListener, aResultLimit); + NS_ENSURE_SUCCESS(rv, rv); + + *aReturnValue = ++mCurrentQueryId; + + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::StopQuery(int32_t aContext) { + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::Search(const nsAString& query, + const nsAString& searchString, + nsIAbDirSearchListener* listener) { + nsresult retCode = NS_OK; + + // Note the following: We get a rather complicated query passed here from + // preference mail.addr_book.quicksearchquery.format. + // Outlook address book search only allows search by PR_ANR, which is a fuzzy + // Ambiguous Name Restriction search. + + retCode = StopSearch(); + NS_ENSURE_SUCCESS(retCode, retCode); + + nsCOMPtr<nsIAbBooleanExpression> expression; + + nsCOMPtr<nsIAbDirectoryQueryArguments> arguments = do_CreateInstance( + "@mozilla.org/addressbook/directory/query-arguments;1", &retCode); + NS_ENSURE_SUCCESS(retCode, retCode); + + retCode = nsAbQueryStringToExpression::Convert(NS_ConvertUTF16toUTF8(query), + getter_AddRefs(expression)); + NS_ENSURE_SUCCESS(retCode, retCode); + retCode = arguments->SetExpression(expression); + NS_ENSURE_SUCCESS(retCode, retCode); + + retCode = arguments->SetQuerySubDirectories(true); + NS_ENSURE_SUCCESS(retCode, retCode); + + return DoQuery(this, arguments, listener, -1, 0, &mSearchContext); +} + +nsresult nsAbOutlookDirectory::StopSearch(void) { + return StopQuery(mSearchContext); +} + +// nsIAbDirSearchListener +NS_IMETHODIMP nsAbOutlookDirectory::OnSearchFinished( + nsresult status, bool complete, nsITransportSecurityInfo* secInfo, + nsACString const& location) { + return NS_OK; +} + +NS_IMETHODIMP nsAbOutlookDirectory::OnSearchFoundCard(nsIAbCard* aCard) { + mCardList->AppendElement(aCard); + return NS_OK; +} + +nsresult nsAbOutlookDirectory::ExecuteQuery(SRestriction* aRestriction, + nsIAbDirSearchListener* aListener, + int32_t aResultLimit) + +{ + if (!aListener) return NS_ERROR_NULL_POINTER; + + nsresult retCode = NS_OK; + + nsCOMPtr<nsIMutableArray> resultsArray( + do_CreateInstance(NS_ARRAY_CONTRACTID, &retCode)); + NS_ENSURE_SUCCESS(retCode, retCode); + + retCode = GetCards(resultsArray, aRestriction); + NS_ENSURE_SUCCESS(retCode, retCode); + + uint32_t nbResults = 0; + retCode = resultsArray->GetLength(&nbResults); + NS_ENSURE_SUCCESS(retCode, retCode); + + if (aResultLimit > 0 && nbResults > static_cast<uint32_t>(aResultLimit)) { + nbResults = static_cast<uint32_t>(aResultLimit); + } + + uint32_t i = 0; + nsCOMPtr<nsIAbCard> card; + + for (i = 0; i < nbResults; ++i) { + card = do_QueryElementAt(resultsArray, i, &retCode); + NS_ENSURE_SUCCESS(retCode, retCode); + + aListener->OnSearchFoundCard(card); + } + + aListener->OnSearchFinished(NS_OK, true, nullptr, ""_ns); + return retCode; +} + +// This function expects the aCards array to already be created. +nsresult nsAbOutlookDirectory::GetCards(nsIMutableArray* aCards, + SRestriction* aRestriction) { + nsAbWinHelperGuard mapiAddBook; + + if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE; + + nsMapiEntryArray cardEntries; + LPSRestriction restriction = (LPSRestriction)aRestriction; + + if (!mapiAddBook->GetCards(*mDirEntry, restriction, cardEntries)) { + PRINTF(("Cannot get cards.\n")); + return NS_ERROR_FAILURE; + } + + nsresult rv; + nsAutoCString ourUID; + if (m_IsMailList) { + // Look up the parent directory (top-level directory) in the + // AddrBookManager. That relies on the fact that the top-level + // directory is already in its map before being initialised. + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString dirURI(kOutlookDirectoryScheme); + dirURI.Append(mParentEntryId); + nsCOMPtr<nsIAbDirectory> owningDir; + rv = abManager->GetDirectory(dirURI, getter_AddRefs(owningDir)); + NS_ENSURE_SUCCESS(rv, rv); + owningDir->GetUID(ourUID); + } else { + GetUID(ourUID); + } + + rv = NS_OK; + + for (ULONG card = 0; card < cardEntries.mNbEntries; ++card) { + nsAutoCString cardEntryString; + nsAutoCString cardURI(kOutlookCardScheme); + nsCOMPtr<nsIAbCard> childCard; + cardEntries.mEntries[card].ToString(cardEntryString); + cardURI.Append(cardEntryString); + + rv = OutlookCardForURI(cardURI, getter_AddRefs(childCard)); + NS_ENSURE_SUCCESS(rv, rv); + // Make sure the card has a UID before setting its directory UID. + // This is a bit of a hack. If we get the UID of the card before setting its + // directory UID, we can avoid an unwanted `ModifyCard()` call inside + // `nsAbCardProperty::SetUID()`. + nsCString dummy; + childCard->GetUID(dummy); + childCard->SetDirectoryUID(ourUID); + + aCards->AppendElement(childCard); + } + return rv; +} + +nsresult nsAbOutlookDirectory::GetNodes(nsIMutableArray* aNodes) { + NS_ENSURE_ARG_POINTER(aNodes); + + nsAbWinHelperGuard mapiAddBook; + nsMapiEntryArray nodeEntries; + + if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE; + + if (!mapiAddBook->GetNodes(*mDirEntry, nodeEntries)) { + PRINTF(("Cannot get nodes.\n")); + return NS_ERROR_FAILURE; + } + + nsresult rv = NS_OK; + + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString topEntryString; + mDirEntry->ToString(topEntryString); + + for (ULONG node = 0; node < nodeEntries.mNbEntries; ++node) { + nsAutoCString dirEntryString; + nsAutoCString uri(kOutlookDirectoryScheme); + uri.Append(topEntryString); + uri.Append('/'); + nodeEntries.mEntries[node].ToString(dirEntryString); + uri.Append(dirEntryString); + + RefPtr<nsAbOutlookDirectory> directory = new nsAbOutlookDirectory; + + // We will later need the URI of the parent directory, so store it here. + directory->mParentEntryId = topEntryString; + directory->Init(uri.get()); + + nsCOMPtr<nsIAbDirectory> dir = do_QueryObject(directory); + aNodes->AppendElement(dir); + } + return rv; +} + +nsresult nsAbOutlookDirectory::commonNotification( + nsISupports* aItem, const char* aTopic, const char* aNotificationUID) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + // `dirUID` needs to stay in scope until the end of the function. + nsAutoCString dirUID; + if (!aNotificationUID) { + // Use the UID of the directory. + GetUID(dirUID); + aNotificationUID = dirUID.get(); + } + + observerService->NotifyObservers( + aItem, aTopic, NS_ConvertUTF8toUTF16(aNotificationUID).get()); + return NS_OK; +} + +nsresult nsAbOutlookDirectory::NotifyItemDeletion( + nsISupports* aItem, bool aIsCard, const char* aNotificationUID) { + const char* topic; + if (aIsCard) { + topic = m_IsMailList ? "addrbook-list-member-removed" + : "addrbook-contact-deleted"; + } else { + topic = "addrbook-list-deleted"; + } + return commonNotification(aItem, topic, aNotificationUID); +} + +nsresult nsAbOutlookDirectory::NotifyItemAddition( + nsISupports* aItem, bool aIsCard, const char* aNotificationUID) { + const char* topic; + if (aIsCard) { + topic = m_IsMailList ? "addrbook-list-member-added" + : "addrbook-contact-created"; + } else { + topic = "addrbook-list-created"; + } + return commonNotification(aItem, topic, aNotificationUID); +} + +nsresult nsAbOutlookDirectory::NotifyItemModification( + nsISupports* aItem, bool aIsCard, const char* aNotificationUID) { + return commonNotification( + aItem, aIsCard ? "addrbook-contact-updated" : "addrbook-list-updated", + aNotificationUID); +} + +nsresult nsAbOutlookDirectory::NotifyCardPropertyChanges(nsIAbCard* aOld, + nsIAbCard* aNew) { + mozilla::JSONStringWriteFunc<nsCString> jsonString; + mozilla::JSONWriter w(jsonString); + w.Start(); + w.StartObjectElement(); + bool somethingChanged = false; + for (uint32_t i = 0; i < sizeof(CardStringProperties) / sizeof(char*); i++) { + nsAutoCString oldValue; + nsAutoCString newValue; + aOld->GetPropertyAsAUTF8String(CardStringProperties[i], oldValue); + aNew->GetPropertyAsAUTF8String(CardStringProperties[i], newValue); + + if (!oldValue.Equals(newValue)) { + somethingChanged = true; + w.StartObjectProperty(mozilla::MakeStringSpan(CardStringProperties[i])); + if (oldValue.IsEmpty()) { + w.NullProperty("oldValue"); + } else { + w.StringProperty("oldValue", mozilla::MakeStringSpan(oldValue.get())); + } + if (newValue.IsEmpty()) { + w.NullProperty("newValue"); + } else { + w.StringProperty("newValue", mozilla::MakeStringSpan(newValue.get())); + } + w.EndObject(); + } + } + + for (uint32_t i = 0; i < sizeof(CardIntProperties) / sizeof(char*); i++) { + uint32_t oldValue = 0; + uint32_t newValue = 0; + aOld->GetPropertyAsUint32(CardIntProperties[i], &oldValue); + aNew->GetPropertyAsUint32(CardIntProperties[i], &newValue); + + if (oldValue != newValue) { + somethingChanged = true; + w.StartObjectProperty(mozilla::MakeStringSpan(CardIntProperties[i])); + if (oldValue == 0) { + w.NullProperty("oldValue"); + } else { + w.IntProperty("oldValue", oldValue); + } + if (newValue == 0) { + w.NullProperty("newValue"); + } else { + w.IntProperty("newValue", newValue); + } + w.EndObject(); + } + } + w.EndObject(); + w.End(); + +#if PRINT_TO_CONSOLE + printf("%s", jsonString.StringCRef().get()); +#endif + + if (somethingChanged) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + observerService->NotifyObservers( + aNew, "addrbook-contact-properties-updated", + NS_ConvertUTF8toUTF16(jsonString.StringCRef()).get()); + } + return NS_OK; +} + +static void UnicodeToWord(const char16_t* aUnicode, WORD& aWord) { + aWord = 0; + if (aUnicode == nullptr || *aUnicode == 0) { + return; + } + nsresult errorCode = NS_OK; + nsAutoString unichar(aUnicode); + + aWord = static_cast<WORD>(unichar.ToInteger(&errorCode)); + if (NS_FAILED(errorCode)) { + PRINTF(("Error conversion string %S: %08x.\n", (wchar_t*)(unichar.get()), + errorCode)); + } +} + +#define PREF_MAIL_ADDR_BOOK_LASTNAMEFIRST "mail.addr_book.lastnamefirst" + +NS_IMETHODIMP nsAbOutlookDirectory::ModifyCard(nsIAbCard* aModifiedCard) { + return ModifyCardInternal(aModifiedCard, false); +} + +nsresult nsAbOutlookDirectory::ModifyCardInternal(nsIAbCard* aModifiedCard, + bool aIsAddition) { + NS_ENSURE_ARG_POINTER(aModifiedCard); + + nsString* properties = nullptr; + nsAutoString utility; + nsAbWinHelperGuard mapiAddBook; + + if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE; + + nsCString cardEntryString; + nsresult retCode = ExtractCardEntry(aModifiedCard, cardEntryString); + NS_ENSURE_SUCCESS(retCode, retCode); + // If we don't have the card entry, we can't work. + if (cardEntryString.IsEmpty()) return NS_ERROR_FAILURE; + + nsMapiEntry cardEntry; + cardEntry.Assign(cardEntryString); + + // Get the existing card. + nsCString uri; + nsCOMPtr<nsIAbCard> oldCard; + aModifiedCard->GetPropertyAsAUTF8String("OutlookEntryURI", uri); + // If the following fails, we didn't get the old card, not fatal. + OutlookCardForURI(uri, getter_AddRefs(oldCard)); + + // First, all the standard properties in one go + properties = new nsString[index_LastProp]; + if (!properties) { + return NS_ERROR_OUT_OF_MEMORY; + } + aModifiedCard->GetFirstName(properties[index_FirstName]); + aModifiedCard->GetLastName(properties[index_LastName]); + // This triple search for something to put in the name + // is because in the case of a mailing list edition in + // Mozilla, the display name will not be provided, and + // MAPI doesn't allow that, so we fall back on an optional + // name, and when all fails, on the email address. + aModifiedCard->GetDisplayName(properties[index_DisplayName]); + if (properties[index_DisplayName].IsEmpty()) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t format; + rv = prefBranch->GetIntPref(PREF_MAIL_ADDR_BOOK_LASTNAMEFIRST, &format); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aModifiedCard->GenerateName(format, nullptr, + properties[index_DisplayName]); + NS_ENSURE_SUCCESS(rv, rv); + + if (properties[index_DisplayName].IsEmpty()) { + aModifiedCard->GetPrimaryEmail(properties[index_DisplayName]); + } + } + + nsMapiEntry dirEntry; + if (m_IsMailList) { + nsAutoCString uri(mURI); + // Trim off the mailing list entry ID from the mailing list URI + // to get the top-level directory entry ID. + nsAutoCString topEntryString; + int32_t slashPos = uri.RFindChar('/'); + uri.SetLength(slashPos); + makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), topEntryString); + dirEntry.Assign(topEntryString); + } else { + dirEntry.Assign(mDirEntry->mByteCount, mDirEntry->mEntryId); + } + + aModifiedCard->SetDisplayName(properties[index_DisplayName]); + aModifiedCard->GetPropertyAsAString(kNicknameProperty, + properties[index_NickName]); + aModifiedCard->GetPropertyAsAString(kWorkPhoneProperty, + properties[index_WorkPhoneNumber]); + aModifiedCard->GetPropertyAsAString(kHomePhoneProperty, + properties[index_HomePhoneNumber]); + aModifiedCard->GetPropertyAsAString(kFaxProperty, + properties[index_WorkFaxNumber]); + aModifiedCard->GetPropertyAsAString(kPagerProperty, + properties[index_PagerNumber]); + aModifiedCard->GetPropertyAsAString(kCellularProperty, + properties[index_MobileNumber]); + aModifiedCard->GetPropertyAsAString(kHomeCityProperty, + properties[index_HomeCity]); + aModifiedCard->GetPropertyAsAString(kHomeStateProperty, + properties[index_HomeState]); + aModifiedCard->GetPropertyAsAString(kHomeZipCodeProperty, + properties[index_HomeZip]); + aModifiedCard->GetPropertyAsAString(kHomeCountryProperty, + properties[index_HomeCountry]); + aModifiedCard->GetPropertyAsAString(kWorkCityProperty, + properties[index_WorkCity]); + aModifiedCard->GetPropertyAsAString(kWorkStateProperty, + properties[index_WorkState]); + aModifiedCard->GetPropertyAsAString(kWorkZipCodeProperty, + properties[index_WorkZip]); + aModifiedCard->GetPropertyAsAString(kWorkCountryProperty, + properties[index_WorkCountry]); + aModifiedCard->GetPropertyAsAString(kJobTitleProperty, + properties[index_JobTitle]); + aModifiedCard->GetPropertyAsAString(kDepartmentProperty, + properties[index_Department]); + aModifiedCard->GetPropertyAsAString(kCompanyProperty, + properties[index_Company]); + aModifiedCard->GetPropertyAsAString(kWorkWebPageProperty, + properties[index_WorkWebPage]); + aModifiedCard->GetPropertyAsAString(kHomeWebPageProperty, + properties[index_HomeWebPage]); + aModifiedCard->GetPropertyAsAString(kNotesProperty, properties[index_Notes]); + if (!mapiAddBook->SetPropertiesUString(dirEntry, cardEntry, + OutlookCardMAPIProps, index_LastProp, + properties)) { + PRINTF(("Cannot set general properties.\n")); + } + + delete[] properties; + nsString unichar; + nsString unichar2; + WORD year = 0; + WORD month = 0; + WORD day = 0; + + aModifiedCard->GetPrimaryEmail(unichar); + if (!mapiAddBook->SetPropertyUString(cardEntry, PR_EMAIL_ADDRESS_W, + unichar.get())) { + PRINTF(("Cannot set primary email.\n")); + } + aModifiedCard->GetPropertyAsAString(kHomeAddressProperty, unichar); + aModifiedCard->GetPropertyAsAString(kHomeAddress2Property, unichar2); + + utility.Assign(unichar.get()); + if (!utility.IsEmpty()) utility.AppendLiteral("\r\n"); + + utility.Append(unichar2.get()); + if (!mapiAddBook->SetPropertyUString(cardEntry, PR_HOME_ADDRESS_STREET_W, + utility.get())) { + PRINTF(("Cannot set home address.\n")); + } + + unichar.Truncate(); + aModifiedCard->GetPropertyAsAString(kWorkAddressProperty, unichar); + unichar2.Truncate(); + aModifiedCard->GetPropertyAsAString(kWorkAddress2Property, unichar2); + + utility.Assign(unichar.get()); + if (!utility.IsEmpty()) utility.AppendLiteral("\r\n"); + + utility.Append(unichar2.get()); + if (!mapiAddBook->SetPropertyUString(cardEntry, PR_BUSINESS_ADDRESS_STREET_W, + utility.get())) { + PRINTF(("Cannot set work address.\n")); + } + + unichar.Truncate(); + aModifiedCard->GetPropertyAsAString(kBirthYearProperty, unichar); + UnicodeToWord(unichar.get(), year); + unichar.Truncate(); + aModifiedCard->GetPropertyAsAString(kBirthMonthProperty, unichar); + UnicodeToWord(unichar.get(), month); + unichar.Truncate(); + aModifiedCard->GetPropertyAsAString(kBirthDayProperty, unichar); + UnicodeToWord(unichar.get(), day); + if (!mapiAddBook->SetPropertyDate(dirEntry, cardEntry, true, PR_BIRTHDAY, + year, month, day)) { + PRINTF(("Cannot set date.\n")); + } + + if (!aIsAddition) { + NotifyItemModification(aModifiedCard, true); + if (oldCard) NotifyCardPropertyChanges(oldCard, aModifiedCard); + } + + return retCode; +} + +static void splitString(nsString& aSource, nsString& aTarget) { + aTarget.Truncate(); + int32_t offset = aSource.FindChar('\n'); + + if (offset >= 0) { + const char16_t* source = aSource.get() + offset + 1; + while (*source) { + if (*source == '\n' || *source == '\r') + aTarget.Append(char16_t(' ')); + else + aTarget.Append(*source); + ++source; + } + int32_t offsetCR = aSource.FindChar('\r'); + aSource.SetLength(offsetCR >= 0 ? offsetCR : offset); + } +} + +nsresult nsAbOutlookDirectory::OutlookCardForURI(const nsACString& aUri, + nsIAbCard** newCard) { + NS_ENSURE_ARG_POINTER(newCard); + + nsAutoCString cardEntryString; + makeEntryIdFromURI(kOutlookCardScheme, PromiseFlatCString(aUri).get(), + cardEntryString); + + nsAbWinHelperGuard mapiAddBook; + if (!mapiAddBook->IsOK()) return NS_ERROR_FAILURE; + + nsresult rv; + nsCOMPtr<nsIAbCard> card = + do_CreateInstance("@mozilla.org/addressbook/cardproperty;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + card->SetPropertyAsAUTF8String("OutlookEntryURI", aUri); + + nsMapiEntry cardEntry; + cardEntry.Assign(cardEntryString); + + nsString unichars[index_LastProp]; + bool success[index_LastProp]; + + nsMapiEntry dirEntry; + if (m_IsMailList) { + nsAutoCString uri(mURI); + // Trim off the mailing list entry ID from the mailing list URI + // to get the top-level directory entry ID. + nsAutoCString topEntryString; + int32_t slashPos = uri.RFindChar('/'); + uri.SetLength(slashPos); + makeEntryIdFromURI(kOutlookDirectoryScheme, uri.get(), topEntryString); + dirEntry.Assign(topEntryString); + } else { + dirEntry.Assign(mDirEntry->mByteCount, mDirEntry->mEntryId); + } + + if (mapiAddBook->GetPropertiesUString(dirEntry, cardEntry, + OutlookCardMAPIProps, index_LastProp, + unichars, success)) { + if (success[index_FirstName]) card->SetFirstName(unichars[index_FirstName]); + if (success[index_LastName]) card->SetLastName(unichars[index_LastName]); + if (success[index_DisplayName]) + card->SetDisplayName(unichars[index_DisplayName]); + +#define SETPROP(name, index) \ + if (success[index]) card->SetPropertyAsAString(name, unichars[index]) + SETPROP(kNicknameProperty, index_NickName); + SETPROP(kWorkPhoneProperty, index_WorkPhoneNumber); + SETPROP(kHomePhoneProperty, index_HomePhoneNumber); + SETPROP(kFaxProperty, index_WorkFaxNumber); + SETPROP(kPagerProperty, index_PagerNumber); + SETPROP(kCellularProperty, index_MobileNumber); + SETPROP(kHomeCityProperty, index_HomeCity); + SETPROP(kHomeStateProperty, index_HomeState); + SETPROP(kHomeZipCodeProperty, index_HomeZip); + SETPROP(kHomeCountryProperty, index_HomeCountry); + SETPROP(kWorkCityProperty, index_WorkCity); + SETPROP(kWorkStateProperty, index_WorkState); + SETPROP(kWorkZipCodeProperty, index_WorkZip); + SETPROP(kWorkCountryProperty, index_WorkCountry); + SETPROP(kJobTitleProperty, index_JobTitle); + SETPROP(kDepartmentProperty, index_Department); + SETPROP(kCompanyProperty, index_Company); + SETPROP(kWorkWebPageProperty, index_WorkWebPage); + SETPROP(kHomeWebPageProperty, index_HomeWebPage); + SETPROP(kNotesProperty, index_Notes); + } + + ULONG cardType = 0; + if (mapiAddBook->GetPropertyLong(cardEntry, PR_OBJECT_TYPE, cardType)) { + card->SetIsMailList(cardType == MAPI_DISTLIST); + if (cardType == MAPI_DISTLIST) { + nsCString dirEntryString; + mDirEntry->ToString(dirEntryString); + nsAutoCString uri(kOutlookDirectoryScheme); + uri.Append(dirEntryString); + uri.Append('/'); + nsCString originalUID; + AlignListEntryStringAndGetUID(cardEntryString, originalUID); + uri.Append(cardEntryString); + card->SetMailListURI(uri.get()); + if (!originalUID.IsEmpty()) card->SetUID(originalUID); + + // In case the display is by "First Last" or "Last, First", give the card + // a name, otherwise nothing is displayed. + if (success[index_DisplayName]) + card->SetLastName(unichars[index_DisplayName]); + } + } + + nsAutoString unichar; + nsAutoString unicharBis; + if (mapiAddBook->GetPropertyUString(cardEntry, PR_EMAIL_ADDRESS_W, unichar)) { + card->SetPrimaryEmail(unichar); + } + if (mapiAddBook->GetPropertyUString(cardEntry, PR_HOME_ADDRESS_STREET_W, + unichar)) { + splitString(unichar, unicharBis); + card->SetPropertyAsAString(kHomeAddressProperty, unichar); + card->SetPropertyAsAString(kHomeAddress2Property, unicharBis); + } + if (mapiAddBook->GetPropertyUString(cardEntry, PR_BUSINESS_ADDRESS_STREET_W, + unichar)) { + splitString(unichar, unicharBis); + card->SetPropertyAsAString(kWorkAddressProperty, unichar); + card->SetPropertyAsAString(kWorkAddress2Property, unicharBis); + } + + WORD year = 0, month = 0, day = 0; + if (mapiAddBook->GetPropertyDate(dirEntry, cardEntry, true, PR_BIRTHDAY, year, + month, day)) { + card->SetPropertyAsUint32(kBirthYearProperty, year); + card->SetPropertyAsUint32(kBirthMonthProperty, month); + card->SetPropertyAsUint32(kBirthDayProperty, day); + } + + card.forget(newCard); + return NS_OK; +} + +void nsAbOutlookDirectory::AlignListEntryStringAndGetUID( + nsCString& aEntryString, nsCString& aOriginalUID) { + // Sadly when scanning for cards and finding a distribution list, the + // entry ID is different to the entry ID returned when scanning the top level + // directory for distribution lists. We make the adjustment here. + // We also retrieve the original UID from the mailing list. + nsAbWinHelperGuard mapiAddBook; + if (!mapiAddBook->IsOK()) return; + + uint32_t nbLists = 0; + nsresult rv = m_AddressList->GetLength(&nbLists); + NS_ENSURE_SUCCESS_VOID(rv); + for (uint32_t i = 0; i < nbLists; i++) { + nsCOMPtr<nsIAbDirectory> list = do_QueryElementAt(m_AddressList, i, &rv); + NS_ENSURE_SUCCESS_VOID(rv); + + // Get URI and extract entry ID. + nsAutoCString listURI; + list->GetURI(listURI); + int ind = listURI.RFindChar('/'); + listURI = Substring(listURI, ind + 1); + + if (aEntryString.Equals(listURI)) { + list->GetUID(aOriginalUID); + return; + } + if (mapiAddBook->CompareEntryIDs(aEntryString, listURI)) { + PRINTF(("Entry ID for mailing list replaced:\nWas: %s\nNow: %s\n", + aEntryString.get(), listURI.get())); + aEntryString = listURI; + list->GetUID(aOriginalUID); + return; + } + } + PRINTF(("Entry ID for mailing list not found.\n")); +} diff --git a/comm/mailnews/addrbook/src/nsAbOutlookDirectory.h b/comm/mailnews/addrbook/src/nsAbOutlookDirectory.h new file mode 100644 index 0000000000..203f82df4a --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOutlookDirectory.h @@ -0,0 +1,181 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +#ifndef nsAbOutlookDirectory_h___ +#define nsAbOutlookDirectory_h___ + +#include "mozilla/Attributes.h" +#include "nsIAbCard.h" +#include "nsAbDirProperty.h" +#include "nsIAbDirectoryQuery.h" +#include "nsIAbDirSearchListener.h" +#include "nsInterfaceHashtable.h" +#include "nsIMutableArray.h" +#include "nsAbWinHelper.h" + +struct nsMapiEntry; + +class nsAbOutlookDirectory : public nsAbDirProperty, // nsIAbDirectory + public nsIAbDirectoryQuery, + public nsIAbDirSearchListener { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIABDIRSEARCHLISTENER + + nsAbOutlookDirectory(void); + + // nsAbDirProperty methods + NS_IMETHOD GetDirType(int32_t* aDirType) override; + NS_IMETHOD GetURI(nsACString& aURI) override; + NS_IMETHOD GetChildCardCount(uint32_t* aCount) override; + NS_IMETHOD GetChildCards(nsTArray<RefPtr<nsIAbCard>>& result) override; + NS_IMETHOD GetChildNodes(nsTArray<RefPtr<nsIAbDirectory>>& result) override; + NS_IMETHOD HasCard(nsIAbCard* aCard, bool* aHasCard) override; + NS_IMETHOD HasDirectory(nsIAbDirectory* aDirectory, + bool* aHasDirectory) override; + NS_IMETHOD DeleteCards(const nsTArray<RefPtr<nsIAbCard>>& aCards) override; + NS_IMETHOD DeleteDirectory(nsIAbDirectory* aDirectory) override; + NS_IMETHOD AddCard(nsIAbCard* aData, nsIAbCard** addedCard) override; + NS_IMETHOD ModifyCard(nsIAbCard* aModifiedCard) override; + NS_IMETHOD DropCard(nsIAbCard* aData, bool needToCopyCard) override; + NS_IMETHOD AddMailList(nsIAbDirectory* aMailList, + nsIAbDirectory** addedList) override; + NS_IMETHOD EditMailListToDatabase(nsIAbCard* listCard) override; + NS_IMETHOD CardForEmailAddress(const nsACString& aEmailAddress, + nsIAbCard** aResult) override; + + // nsAbDirProperty method + NS_IMETHOD Init(const char* aUri) override; + // nsIAbDirectoryQuery methods + NS_DECL_NSIABDIRECTORYQUERY + // Perform a MAPI query. + nsresult ExecuteQuery(SRestriction* aRestriction, + nsIAbDirSearchListener* aListener, + int32_t aResultLimit); + NS_IMETHOD Search(const nsAString& query, const nsAString& searchString, + nsIAbDirSearchListener* listener) override; + + protected: + nsresult StopSearch(); + nsresult ExtractCardEntry(nsIAbCard* aCard, nsCString& aEntry); + nsresult ExtractDirectoryEntry(nsIAbDirectory* aDirectory, nsCString& aEntry); + void AlignListEntryStringAndGetUID(nsCString& aEntryString, + nsCString& aOriginalUID); + + // Retrieve hierarchy as cards, with an optional restriction + nsresult GetCards(nsIMutableArray* aCards, SRestriction* aRestriction); + // Retrieve hierarchy as directories + nsresult GetNodes(nsIMutableArray* aNodes); + nsresult ModifyCardInternal(nsIAbCard* aModifiedCard, bool aIsAddition); + // Notification for the UI. + nsresult NotifyItemDeletion(nsISupports* aItem, bool aIsCard, + const char* aNotificationUID = nullptr); + nsresult NotifyItemAddition(nsISupports* aItem, bool aIsCard, + const char* aNotificationUID = nullptr); + nsresult NotifyItemModification(nsISupports* aItem, bool aIsCard, + const char* aNotificationUID = nullptr); + nsresult NotifyCardPropertyChanges(nsIAbCard* aOld, nsIAbCard* aNew); + nsresult commonNotification(nsISupports* aItem, const char* aTopic, + const char* aNotificationUID); + // Utility to produce a card from a URI. + nsresult OutlookCardForURI(const nsACString& aUri, nsIAbCard** card); + + nsMapiEntry* mDirEntry; + // Keep track of context ID to be passed back from `DoQuery()`. + int32_t mCurrentQueryId; + // Data for the search interfaces + int32_t mSearchContext; + + private: + virtual ~nsAbOutlookDirectory(void); + nsCString mParentEntryId; + + // This is totally quirky. `m_AddressList` is defined in + // class nsAbDirProperty to hold a list of mailing lists, + // but there is no member to hold a list of cards. + // It gets worse: For mailing lists, `m_AddressList` holds the + // list of cards. + // So we'll do it as the Mac AB does and define a member for it. + // nsIMutableArray is used, because then it is interchangeable with + // `m_AddressList`. + nsCOMPtr<nsIMutableArray> mCardList; +}; + +enum { + index_DisplayName = 0, + index_FirstName, + index_LastName, + index_NickName, + index_WorkPhoneNumber, + index_HomePhoneNumber, + index_WorkFaxNumber, + index_PagerNumber, + index_MobileNumber, + index_HomeCity, + index_HomeState, + index_HomeZip, + index_HomeCountry, + index_WorkCity, + index_WorkState, + index_WorkZip, + index_WorkCountry, + index_JobTitle, + index_Department, + index_Company, + index_WorkWebPage, + index_HomeWebPage, + index_Notes, + index_LastProp +}; + +// The following properties are retrieved from the contact associated +// with the address book entry. Email not available on contact, +// the contact has three named email properties. +static const ULONG OutlookCardMAPIProps[] = { + PR_DISPLAY_NAME_W, + PR_GIVEN_NAME_W, + PR_SURNAME_W, + PR_NICKNAME_W, + PR_BUSINESS_TELEPHONE_NUMBER_W, + PR_HOME_TELEPHONE_NUMBER_W, + PR_BUSINESS_FAX_NUMBER_W, + PR_PAGER_TELEPHONE_NUMBER_W, + PR_MOBILE_TELEPHONE_NUMBER_W, + PR_HOME_ADDRESS_CITY_W, + PR_HOME_ADDRESS_STATE_OR_PROVINCE_W, + PR_HOME_ADDRESS_POSTAL_CODE_W, + PR_HOME_ADDRESS_COUNTRY_W, + PR_BUSINESS_ADDRESS_CITY_W, + PR_BUSINESS_ADDRESS_STATE_OR_PROVINCE_W, + PR_BUSINESS_ADDRESS_POSTAL_CODE_W, + PR_BUSINESS_ADDRESS_COUNTRY_W, + PR_TITLE_W, + PR_DEPARTMENT_NAME_W, + PR_COMPANY_NAME_W, + PR_BUSINESS_HOME_PAGE_W, + PR_PERSONAL_HOME_PAGE_W, + PR_BODY_W}; + +static const char* CardStringProperties[] = { + kFirstNameProperty, kLastNameProperty, kDisplayNameProperty, + kNicknameProperty, kPriEmailProperty, + + kHomeAddressProperty, kHomeAddress2Property, kHomeCityProperty, + kHomeStateProperty, kHomeZipCodeProperty, kHomeCountryProperty, + kHomeWebPageProperty, + + kWorkAddressProperty, kWorkAddress2Property, kWorkCityProperty, + kWorkStateProperty, kWorkZipCodeProperty, kWorkCountryProperty, + kWorkWebPageProperty, + + kHomePhoneProperty, kWorkPhoneProperty, kFaxProperty, + kPagerProperty, kCellularProperty, + + kJobTitleProperty, kDepartmentProperty, kCompanyProperty, + kNotesProperty}; + +static const char* CardIntProperties[] = { + kBirthYearProperty, kBirthMonthProperty, kBirthDayProperty}; + +#endif // nsAbOutlookDirectory_h___ diff --git a/comm/mailnews/addrbook/src/nsAbOutlookInterface.cpp b/comm/mailnews/addrbook/src/nsAbOutlookInterface.cpp new file mode 100644 index 0000000000..7000c90317 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOutlookInterface.cpp @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +#include "nsAbOutlookInterface.h" +#include "nsAbWinHelper.h" +#include "nsComponentManagerUtils.h" + +NS_IMPL_ISUPPORTS(nsAbOutlookInterface, nsIAbOutlookInterface) + +nsAbOutlookInterface::nsAbOutlookInterface(void) {} + +nsAbOutlookInterface::~nsAbOutlookInterface(void) {} + +NS_IMETHODIMP +nsAbOutlookInterface::GetFolderURIs(const nsACString& aURI, + nsTArray<nsCString>& uris) { + uris.Clear(); + nsresult rv = NS_OK; + + nsAbWinHelperGuard mapiAddBook; + nsMapiEntryArray folders; + NS_ENSURE_SUCCESS(rv, rv); + if (!mapiAddBook->IsOK() || !mapiAddBook->GetFolders(folders)) { + return NS_ERROR_FAILURE; + } + + uris.SetCapacity(folders.mNbEntries); + + for (ULONG i = 0; i < folders.mNbEntries; ++i) { + nsAutoCString entryId; + nsAutoCString uri(kOutlookDirectoryScheme); + folders.mEntries[i].ToString(entryId); + uri.Append(entryId); + uris.AppendElement(uri); + } + return NS_OK; +} diff --git a/comm/mailnews/addrbook/src/nsAbOutlookInterface.h b/comm/mailnews/addrbook/src/nsAbOutlookInterface.h new file mode 100644 index 0000000000..fd51f83516 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbOutlookInterface.h @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsAbOutlookInterface_h___ +#define nsAbOutlookInterface_h___ + +#include "nsIAbOutlookInterface.h" + +class nsAbOutlookInterface : public nsIAbOutlookInterface { + public: + nsAbOutlookInterface(void); + + NS_DECL_ISUPPORTS + NS_DECL_NSIABOUTLOOKINTERFACE + + private: + virtual ~nsAbOutlookInterface(void); +}; + +#endif // nsAbOutlookInterface_h___ diff --git a/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.cpp b/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.cpp new file mode 100644 index 0000000000..bf5e158de3 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.cpp @@ -0,0 +1,293 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsAbQueryStringToExpression.h" + +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsITextToSubURI.h" +#include "nsAbBooleanExpression.h" +#include "plstr.h" + +/** + * This code parses the query expression passed in as an addressbook URI. + * The expression takes the form: + * (BOOL1(FIELD1,OP1,VALUE1)..(FIELDn,OPn,VALUEn)(BOOL2(FIELD1,OP1,VALUE1)...)...) + * + * BOOLn A boolean operator joining subsequent terms delimited by (). + * For possible values see CreateBooleanExpression(). + * FIELDn An addressbook card data field. + * OPn An operator for the search term. + * For possible values see CreateBooleanConditionString(). + * VALUEn The value to be matched in the FIELDn via the OPn operator. + * The value must be URL encoded by the caller, if it contains any + * special characters including '(' and ')'. + */ +nsresult nsAbQueryStringToExpression::Convert( + const nsACString& aQueryString, nsIAbBooleanExpression** expression) { + nsresult rv; + + nsAutoCString q(aQueryString); + q.StripWhitespace(); + const char* queryChars = q.get(); + + nsCOMPtr<nsISupports> s; + rv = ParseExpression(&queryChars, getter_AddRefs(s)); + NS_ENSURE_SUCCESS(rv, rv); + + // Case: Not end of string + if (*queryChars != 0) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIAbBooleanExpression> e(do_QueryInterface(s, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + e.forget(expression); + return rv; +} + +nsresult nsAbQueryStringToExpression::ParseExpression( + const char** index, nsISupports** expression) { + nsresult rv; + + if (**index == '?') { + (*index)++; + } + + if (**index != '(') return NS_ERROR_FAILURE; + + const char* indexBracket = *index + 1; + while (*indexBracket && *indexBracket != '(' && *indexBracket != ')') + indexBracket++; + + // Case: End of string + if (*indexBracket == 0) return NS_ERROR_FAILURE; + + // Case: "((" or "()" + if (indexBracket == *index + 1) { + return NS_ERROR_FAILURE; + } + // Case: "(*(" + else if (*indexBracket == '(') { + // printf ("Case: (*(: %s\n", *index); + + nsCString operation; + rv = ParseOperationEntry(*index, indexBracket, getter_Copies(operation)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbBooleanExpression> e; + rv = CreateBooleanExpression(operation.get(), getter_AddRefs(e)); + NS_ENSURE_SUCCESS(rv, rv); + + // Case: "(*)(*)....(*))" + *index = indexBracket; + rv = ParseExpressions(index, e); + NS_ENSURE_SUCCESS(rv, rv); + + e.forget(expression); + } + // Case" "(*)" + else if (*indexBracket == ')') { + // printf ("Case: (*): %s\n", *index); + + nsCOMPtr<nsIAbBooleanConditionString> conditionString; + rv = ParseCondition(index, indexBracket, getter_AddRefs(conditionString)); + NS_ENSURE_SUCCESS(rv, rv); + + conditionString.forget(expression); + } + + if (**index != ')') return NS_ERROR_FAILURE; + + (*index)++; + + return NS_OK; +} + +nsresult nsAbQueryStringToExpression::ParseExpressions( + const char** index, nsIAbBooleanExpression* expression) { + nsresult rv; + nsTArray<RefPtr<nsISupports>> expressions; + + // Case: ")(*)(*)....(*))" + // printf ("Case: )(*)(*)....(*)): %s\n", *index); + while (**index == '(') { + nsCOMPtr<nsISupports> childExpression; + rv = ParseExpression(index, getter_AddRefs(childExpression)); + NS_ENSURE_SUCCESS(rv, rv); + + expressions.AppendElement(childExpression); + } + + if (**index == 0) return NS_ERROR_FAILURE; + + // Case: "))" + // printf ("Case: )): %s\n", *index); + + if (**index != ')') return NS_ERROR_FAILURE; + + expression->SetExpressions(expressions); + + return NS_OK; +} + +nsresult nsAbQueryStringToExpression::ParseCondition( + const char** index, const char* indexBracketClose, + nsIAbBooleanConditionString** conditionString) { + nsresult rv; + + (*index)++; + + nsCString entries[3]; + for (int i = 0; i < 3; i++) { + rv = ParseConditionEntry(index, indexBracketClose, + getter_Copies(entries[i])); + NS_ENSURE_SUCCESS(rv, rv); + + if (*index == indexBracketClose) break; + } + + if (*index != indexBracketClose) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIAbBooleanConditionString> c; + rv = CreateBooleanConditionString(entries[0].get(), entries[1].get(), + entries[2].get(), getter_AddRefs(c)); + NS_ENSURE_SUCCESS(rv, rv); + + c.forget(conditionString); + return NS_OK; +} + +nsresult nsAbQueryStringToExpression::ParseConditionEntry( + const char** index, const char* indexBracketClose, char** entry) { + const char* indexDeliminator = *index; + while (indexDeliminator != indexBracketClose && *indexDeliminator != ',') + indexDeliminator++; + + int entryLength = indexDeliminator - *index; + if (entryLength) + *entry = PL_strndup(*index, entryLength); + else + *entry = 0; + + if (indexDeliminator != indexBracketClose) + *index = indexDeliminator + 1; + else + *index = indexDeliminator; + + return NS_OK; +} + +nsresult nsAbQueryStringToExpression::ParseOperationEntry( + const char* indexBracketOpen1, const char* indexBracketOpen2, + char** operation) { + int operationLength = indexBracketOpen2 - indexBracketOpen1 - 1; + if (operationLength) + *operation = PL_strndup(indexBracketOpen1 + 1, operationLength); + else + *operation = 0; + + return NS_OK; +} + +nsresult nsAbQueryStringToExpression::CreateBooleanExpression( + const char* operation, nsIAbBooleanExpression** expression) { + nsAbBooleanOperationType op; + if (PL_strcasecmp(operation, "and") == 0) + op = nsIAbBooleanOperationTypes::AND; + else if (PL_strcasecmp(operation, "or") == 0) + op = nsIAbBooleanOperationTypes::OR; + else if (PL_strcasecmp(operation, "not") == 0) + op = nsIAbBooleanOperationTypes::NOT; + else + return NS_ERROR_FAILURE; + + nsresult rv; + + nsCOMPtr<nsIAbBooleanExpression> expr = + do_CreateInstance("@mozilla.org/boolean-expression/n-peer;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = expr->SetOperation(op); + expr.forget(expression); + return rv; +} + +nsresult nsAbQueryStringToExpression::CreateBooleanConditionString( + const char* attribute, const char* condition, const char* value, + nsIAbBooleanConditionString** conditionString) { + if (attribute == 0 || condition == 0 || value == 0) return NS_ERROR_FAILURE; + + nsAbBooleanConditionType c; + + if (PL_strcasecmp(condition, "=") == 0) + c = nsIAbBooleanConditionTypes::Is; + else if (PL_strcasecmp(condition, "!=") == 0) + c = nsIAbBooleanConditionTypes::IsNot; + else if (PL_strcasecmp(condition, "lt") == 0) + c = nsIAbBooleanConditionTypes::LessThan; + else if (PL_strcasecmp(condition, "gt") == 0) + c = nsIAbBooleanConditionTypes::GreaterThan; + else if (PL_strcasecmp(condition, "bw") == 0) + c = nsIAbBooleanConditionTypes::BeginsWith; + else if (PL_strcasecmp(condition, "ew") == 0) + c = nsIAbBooleanConditionTypes::EndsWith; + else if (PL_strcasecmp(condition, "c") == 0) + c = nsIAbBooleanConditionTypes::Contains; + else if (PL_strcasecmp(condition, "!c") == 0) + c = nsIAbBooleanConditionTypes::DoesNotContain; + else if (PL_strcasecmp(condition, "~=") == 0) + c = nsIAbBooleanConditionTypes::SoundsLike; + else if (PL_strcasecmp(condition, "regex") == 0) + c = nsIAbBooleanConditionTypes::RegExp; + else if (PL_strcasecmp(condition, "ex") == 0) + c = nsIAbBooleanConditionTypes::Exists; + else if (PL_strcasecmp(condition, "!ex") == 0) + c = nsIAbBooleanConditionTypes::DoesNotExist; + else + return NS_ERROR_FAILURE; + + nsresult rv; + + nsCOMPtr<nsIAbBooleanConditionString> cs = do_CreateInstance( + "@mozilla.org/boolean-expression/condition-string;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cs->SetCondition(c); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsITextToSubURI> textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + nsString attributeUCS2; + nsString valueUCS2; + + rv = textToSubURI->UnEscapeAndConvert( + "UTF-8"_ns, nsDependentCString(attribute), attributeUCS2); + NS_ENSURE_SUCCESS(rv, rv); + + rv = textToSubURI->UnEscapeAndConvert("UTF-8"_ns, nsDependentCString(value), + valueUCS2); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ConvertUTF16toUTF8 attributeUTF8(attributeUCS2); + + rv = cs->SetName(attributeUTF8.get()); + NS_ENSURE_SUCCESS(rv, rv); + rv = cs->SetValue(valueUCS2.get()); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NS_ConvertUTF8toUTF16 valueUCS2(value); + + rv = cs->SetName(attribute); + NS_ENSURE_SUCCESS(rv, rv); + rv = cs->SetValue(valueUCS2.get()); + NS_ENSURE_SUCCESS(rv, rv); + } + + cs.forget(conditionString); + return NS_OK; +} diff --git a/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.h b/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.h new file mode 100644 index 0000000000..acab014278 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbQueryStringToExpression.h @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsAbQueryStringToExpression_h__ +#define nsAbQueryStringToExpression_h__ + +#include "nsIAbBooleanExpression.h" + +class nsAbQueryStringToExpression { + public: + static nsresult Convert(const nsACString& aQueryString, + nsIAbBooleanExpression** expression); + + protected: + static nsresult ParseExpression(const char** index, nsISupports** expression); + static nsresult ParseExpressions(const char** index, + nsIAbBooleanExpression* expression); + static nsresult ParseCondition(const char** index, + const char* indexBracketClose, + nsIAbBooleanConditionString** conditionString); + + static nsresult ParseConditionEntry(const char** index, + const char* indexBracketClose, + char** entry); + static nsresult ParseOperationEntry(const char* indexBracketOpen1, + const char* indexBracketOpen2, + char** operation); + + static nsresult CreateBooleanExpression(const char* operation, + nsIAbBooleanExpression** expression); + static nsresult CreateBooleanConditionString( + const char* attribute, const char* condition, const char* value, + nsIAbBooleanConditionString** conditionString); +}; + +#endif diff --git a/comm/mailnews/addrbook/src/nsAbWinHelper.cpp b/comm/mailnews/addrbook/src/nsAbWinHelper.cpp new file mode 100644 index 0000000000..79379c45c7 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbWinHelper.cpp @@ -0,0 +1,1491 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +#define INITGUID +#define USES_IID_IMAPIProp +#define USES_IID_IMessage +#define USES_IID_IMAPIFolder +#define USES_IID_IMAPIContainer +#define USES_IID_IABContainer +#define USES_IID_IMAPITable +#define USES_IID_IDistList + +#include "nsAbWinHelper.h" +#include "nsMapiAddressBook.h" + +#include <mapiguid.h> + +#include "mozilla/Logging.h" + +#define PRINT_TO_CONSOLE 0 +#if PRINT_TO_CONSOLE +# define PRINTF(args) printf args +#else +static mozilla::LazyLogModule gAbWinHelperLog("AbWinHelper"); +# define PRINTF(args) MOZ_LOG(gAbWinHelperLog, mozilla::LogLevel::Debug, args) +#endif + +// Small utility to ensure release of all MAPI interfaces +template <class tInterface> +struct nsMapiInterfaceWrapper { + tInterface mInterface; + + nsMapiInterfaceWrapper(void) : mInterface(NULL) {} + ~nsMapiInterfaceWrapper(void) { + if (mInterface != NULL) { + mInterface->Release(); + } + } + operator LPUNKNOWN*(void) { + return reinterpret_cast<LPUNKNOWN*>(&mInterface); + } + tInterface operator->(void) const { return mInterface; } + operator tInterface*(void) { return &mInterface; } + tInterface Get(void) const { return mInterface; } +}; + +static void assignEntryID(LPENTRYID& aTarget, LPENTRYID aSource, + ULONG aByteCount) { + if (aTarget != NULL) { + delete[] (reinterpret_cast<LPBYTE>(aTarget)); + aTarget = NULL; + } + if (aSource != NULL) { + aTarget = reinterpret_cast<LPENTRYID>(new BYTE[aByteCount]); + memcpy(aTarget, aSource, aByteCount); + } +} + +nsMapiEntry::nsMapiEntry(void) : mByteCount(0), mEntryId(NULL) { + MOZ_COUNT_CTOR(nsMapiEntry); +} + +nsMapiEntry::nsMapiEntry(ULONG aByteCount, LPENTRYID aEntryId) + : mByteCount(0), mEntryId(NULL) { + Assign(aByteCount, aEntryId); + MOZ_COUNT_CTOR(nsMapiEntry); +} + +void nsMapiEntry::Move(nsMapiEntry& target, nsMapiEntry& source) { + target.mByteCount = source.mByteCount; + target.mEntryId = source.mEntryId; + source.mByteCount = 0; + source.mEntryId = NULL; +} + +nsMapiEntry::~nsMapiEntry(void) { + Assign(0, NULL); + MOZ_COUNT_DTOR(nsMapiEntry); +} + +void nsMapiEntry::Assign(ULONG aByteCount, LPENTRYID aEntryId) { + assignEntryID(mEntryId, aEntryId, aByteCount); + mByteCount = aByteCount; +} + +void nsMapiEntry::Assign(const nsCString& aString) { + Assign(0, NULL); + ULONG byteCount = aString.Length() / 2; + + if ((aString.Length() & 0x01) != 0) { + // Something wrong here, we should always get an even number of hex digits. + byteCount += 1; + } + unsigned char* currentTarget = new unsigned char[byteCount]; + + mByteCount = byteCount; + mEntryId = reinterpret_cast<LPENTRYID>(currentTarget); + ULONG j = 0; + for (uint32_t i = 0; i < aString.Length(); i += 2) { + char c1 = aString.CharAt(i); + char c2 = i + 1 < aString.Length() ? aString.CharAt(i + 1) : '0'; + // clang-format off + currentTarget[j] = + ((c1 <= '9' ? c1 - '0' : c1 - 'A' + 10) << 4) | + (c2 <= '9' ? c2 - '0' : c2 - 'A' + 10); + // clang-format on + j++; + } +} + +void nsMapiEntry::ToString(nsCString& aString) const { + aString.Truncate(); + aString.SetCapacity(mByteCount * 2); + char twoBytes[3]; + + for (ULONG i = 0; i < mByteCount; i++) { + sprintf(twoBytes, "%02X", (reinterpret_cast<unsigned char*>(mEntryId))[i]); + aString.Append(twoBytes); + } +} + +void nsMapiEntry::Dump(void) const { + PRINTF(("%lu\n", mByteCount)); + for (ULONG i = 0; i < mByteCount; ++i) { + PRINTF(("%02X", (reinterpret_cast<unsigned char*>(mEntryId))[i])); + } + PRINTF(("\n")); +} + +nsMapiEntryArray::nsMapiEntryArray(void) : mEntries(NULL), mNbEntries(0) { + MOZ_COUNT_CTOR(nsMapiEntryArray); +} + +nsMapiEntryArray::~nsMapiEntryArray(void) { + if (mEntries) { + delete[] mEntries; + } + MOZ_COUNT_DTOR(nsMapiEntryArray); +} + +void nsMapiEntryArray::CleanUp(void) { + if (mEntries != NULL) { + delete[] mEntries; + mEntries = NULL; + mNbEntries = 0; + } +} + +// Microsoft distinguishes between address book entries and contacts. +// Address book entries are of class IMailUser and are stored in containers +// of class IABContainer. +// Local contacts are stored in the "contacts folder" of class IMAPIFolder and +// are of class IMessage with "message class" IPM.Contact. +// For local address books the entry ID of the contact can be derived from the +// entry ID of the address book entry and vice versa. +// Most attributes can be retrieved from both classes with some exceptions: +// The primary e-mail address is only stored on the IMailUser, the contact +// has three named email properties (which are not used so far). +// The birthday is only stored on the contact. +// `OpenMAPIObject()` can open the address book entry as well as the contact, +// to open the concact it needs to get the message store from via the +// address book container (or "directory" in Thunderbird terms). +// Apart from Microsoft documentation, the best source of information +// is the MAPI programmers mailing list at MAPI-L@PEACH.EASE.LSOFT.COM. +// All the information that was needed to "refresh" the MAPI implementation +// in Thunderbird was obtained via these threads: +// https://peach.ease.lsoft.com/scripts/wa-PEACH.exe?A2=2012&L=MAPI-L&D=0&P=20988415 +// https://peach.ease.lsoft.com/scripts/wa-PEACH.exe?A2=2101&L=MAPI-L&D=0&P=21034512 + +// Some stuff to access the entry ID of the contact (IMessage, IPM.Contact) +// from the address book entry ID (IMailUser). +// The address book entry ID has the following structure, see: +// https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcdata/c33d5b9c-d044-4727-96e2-2051f8419ab1 +#define ABENTRY_FLAGS_LENGTH 4 +#define CONTAB_PROVIDER_ID \ + "\xFE\x42\xAA\x0A\x18\xC7\x1A\x10\xE8\x85\x0B\x65\x1C\x24\x00\x00" +#define CONTAB_PROVIDER_ID_LENGTH 16 +#define ABENTRY_VERSION "\x03\x00\x00\x00" +#define ABENTRY_VERSION_LENGTH 4 +#define ABENTRY_TYPE "\x04\x00\x00\x00" +#define ABENTRY_TYPE_LENGTH 4 + +struct AbEntryId { + BYTE flags[ABENTRY_FLAGS_LENGTH]; + BYTE provider[CONTAB_PROVIDER_ID_LENGTH]; + BYTE version[ABENTRY_VERSION_LENGTH]; + BYTE type[ABENTRY_TYPE_LENGTH]; + ULONG index; + ULONG length; + BYTE idBytes[]; +}; + +// Some stuff to access the entry IDs of members in a distribution list +// (IMessage, IPM.DistList): +// https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxocntc/02656215-1cb0-4b06-a077-b07e756216be +// Also handy the reference to the so-called "one off" members: +// https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcdata/b32d23af-85f6-4e92-8387-53a1950ae7ba +#define DLENTRY_FLAGS_LENGTH 4 +#define DL_PROVIDER_ID \ + "\xC0\x91\xAD\xD3\x51\x9D\xCF\x11\xA4\xA9\x00\xAA\x00\x47\xFA\xA4" +#define DL_PROVIDER_ID_LENGTH 16 +#define DLENTRY_TYPE_LENGTH 1 +struct DlEntryId { + BYTE flags[DLENTRY_FLAGS_LENGTH]; + BYTE provider[DL_PROVIDER_ID_LENGTH]; + BYTE type[DLENTRY_TYPE_LENGTH]; + BYTE idBytes[]; +}; + +#define DLENTRY_OO_FLAGS_LENGTH 4 +#define DL_OO_PROVIDER_ID \ + "\x81\x2B\x1F\xA4\xBE\xA3\x10\x19\x9D\x6E\x00\xDD\x01\x0F\x54\x02" +#define DL_OO_PROVIDER_ID_LENGTH 16 +struct DlEntryIdOo { + BYTE flags[DLENTRY_OO_FLAGS_LENGTH]; + BYTE provider[DL_OO_PROVIDER_ID_LENGTH]; + // Note that the documentation specifies a two-byte version followed by a + // two-byte "bit collection", but MFCMapi + // (https://github.com/stephenegriffin/mfcmapi) shows, for example: + // dwBitmask: 0x80010000 = MAPI_UNICODE | MAPI_SEND_NO_RICH_INFO. + // Intel x86 and AMD64 / x86-64 hardware is little-endian, so that + // equates to 0x0000 0x01 0x80 in memory: + // M (1 bit): (mask 0x0100) (MIME) and U (1 bit): (mask 0x0080) (Unicode). + ULONG versionAndBits; + BYTE variable[]; +}; + +using namespace mozilla; + +uint32_t nsAbWinHelper::sEntryCounter = 0; +mozilla::StaticMutex nsAbWinHelper::sMutex; +// There seems to be a deadlock/auto-destruction issue +// in MAPI when multiple threads perform init/release +// operations at the same time. So I've put a mutex +// around both the initialize process and the destruction +// one. I just hope the rest of the calls don't need the +// same protection (MAPI is supposed to be thread-safe). + +nsAbWinHelper::nsAbWinHelper(void) : mLastError(S_OK), mAddressBook(NULL) { + MOZ_COUNT_CTOR(nsAbWinHelper); +} + +nsAbWinHelper::~nsAbWinHelper(void) { MOZ_COUNT_DTOR(nsAbWinHelper); } + +BOOL nsAbWinHelper::GetFolders(nsMapiEntryArray& aFolders) { + aFolders.CleanUp(); + nsMapiInterfaceWrapper<LPABCONT> rootFolder; + nsMapiInterfaceWrapper<LPMAPITABLE> folders; + ULONG objType = 0; + ULONG rowCount = 0; + SRestriction restriction; + SPropTagArray folderColumns; + + mLastError = mAddressBook->OpenEntry(0, NULL, NULL, 0, &objType, rootFolder); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open root %08lx.\n", mLastError)); + return FALSE; + } + mLastError = rootFolder->GetHierarchyTable(0, folders); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot get hierarchy %08lx.\n", mLastError)); + return FALSE; + } + // We only take into account modifiable containers, + // otherwise, we end up with all the directory services... + restriction.rt = RES_BITMASK; + restriction.res.resBitMask.ulPropTag = PR_CONTAINER_FLAGS; + restriction.res.resBitMask.relBMR = BMR_NEZ; + restriction.res.resBitMask.ulMask = AB_MODIFIABLE; + mLastError = folders->Restrict(&restriction, 0); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot restrict table %08lx.\n", mLastError)); + } + folderColumns.cValues = 1; + folderColumns.aulPropTag[0] = PR_ENTRYID; + mLastError = folders->SetColumns(&folderColumns, 0); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot set columns %08lx.\n", mLastError)); + return FALSE; + } + mLastError = folders->GetRowCount(0, &rowCount); + if (HR_SUCCEEDED(mLastError)) { + aFolders.mEntries = new nsMapiEntry[rowCount]; + aFolders.mNbEntries = 0; + do { + LPSRowSet rowSet = NULL; + + rowCount = 0; + mLastError = folders->QueryRows(1, 0, &rowSet); + if (HR_SUCCEEDED(mLastError)) { + rowCount = rowSet->cRows; + if (rowCount > 0) { + nsMapiEntry& current = aFolders.mEntries[aFolders.mNbEntries++]; + SPropValue& currentValue = rowSet->aRow->lpProps[0]; + + current.Assign( + currentValue.Value.bin.cb, + reinterpret_cast<LPENTRYID>(currentValue.Value.bin.lpb)); + } + MyFreeProws(rowSet); + } else { + PRINTF(("Cannot query rows %08lx.\n", mLastError)); + } + } while (rowCount > 0); + } + return HR_SUCCEEDED(mLastError); +} + +BOOL nsAbWinHelper::GetCards(const nsMapiEntry& aParent, + LPSRestriction aRestriction, + nsMapiEntryArray& aCards) { + aCards.CleanUp(); + return GetContents(aParent, aRestriction, &aCards.mEntries, aCards.mNbEntries, + 0); +} + +BOOL nsAbWinHelper::GetNodes(const nsMapiEntry& aParent, + nsMapiEntryArray& aNodes) { + aNodes.CleanUp(); + return GetContents(aParent, NULL, &aNodes.mEntries, aNodes.mNbEntries, + MAPI_DISTLIST); +} + +BOOL nsAbWinHelper::GetCardsCount(const nsMapiEntry& aParent, ULONG& aNbCards) { + aNbCards = 0; + return GetContents(aParent, NULL, NULL, aNbCards, 0); +} + +BOOL nsAbWinHelper::GetPropertyString(const nsMapiEntry& aObject, + ULONG aPropertyTag, nsCString& aName) { + aName.Truncate(); + LPSPropValue values = NULL; + ULONG valueCount = 0; + + nsMapiEntry nullEntry; + if (!GetMAPIProperties(nullEntry, aObject, &aPropertyTag, 1, values, + valueCount)) { + return FALSE; + } + + if (valueCount != 1 || values == NULL) { + PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyString")); + return FALSE; + } + + BOOL success = TRUE; + if (PROP_TYPE(values->ulPropTag) == PT_STRING8) { + aName = values->Value.lpszA; + } else if (PROP_TYPE(values->ulPropTag) == PT_UNICODE) { + aName = NS_LossyConvertUTF16toASCII(values->Value.lpszW); + } else { + PRINTF(("Unexpected return value for property %08lx (x0A is PT_ERROR).\n", + values->ulPropTag)); + success = FALSE; + } + FreeBuffer(values); + return success; +} + +BOOL nsAbWinHelper::GetPropertyUString(const nsMapiEntry& aObject, + ULONG aPropertyTag, nsString& aName) { + aName.Truncate(); + LPSPropValue values = NULL; + ULONG valueCount = 0; + + nsMapiEntry nullEntry; + if (!GetMAPIProperties(nullEntry, aObject, &aPropertyTag, 1, values, + valueCount)) { + return FALSE; + } + if (valueCount != 1 || values == NULL) { + PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyUString")); + return FALSE; + } + + BOOL success = TRUE; + if (PROP_TYPE(values->ulPropTag) == PT_UNICODE) { + aName = values->Value.lpszW; + } else if (PROP_TYPE(values->ulPropTag) == PT_STRING8) { + aName.AssignASCII(values->Value.lpszA); + } else { + PRINTF(("Unexpected return value for property %08lx (x0A is PT_ERROR).\n", + values->ulPropTag)); + success = FALSE; + } + return success; +} + +BOOL nsAbWinHelper::GetPropertiesUString(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + const ULONG aPropertyTags[], + ULONG aNbProperties, nsString aNames[], + bool aSuccess[]) { + LPSPropValue values = NULL; + ULONG valueCount = 0; + + if (!GetMAPIProperties(aDir, aObject, aPropertyTags, aNbProperties, values, + valueCount, true)) + return FALSE; + + if (valueCount != aNbProperties || values == NULL) { + PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertiesUString")); + return FALSE; + } + for (ULONG i = 0; i < valueCount; ++i) { + aNames[i].Truncate(); + aSuccess[i] = false; + if (PROP_ID(values[i].ulPropTag) == PROP_ID(aPropertyTags[i])) { + if (PROP_TYPE(values[i].ulPropTag) == PT_STRING8) { + aNames[i].AssignASCII(values[i].Value.lpszA); + aSuccess[i] = true; + } else if (PROP_TYPE(values[i].ulPropTag) == PT_UNICODE) { + aNames[i] = values[i].Value.lpszW; + aSuccess[i] = true; + } else { + PRINTF( + ("Unexpected return value for property %08lx (x0A is PT_ERROR).\n", + values[i].ulPropTag)); + } + } + } + FreeBuffer(values); + return TRUE; +} + +BOOL nsAbWinHelper::GetPropertyDate(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + bool fromContact, ULONG aPropertyTag, + WORD& aYear, WORD& aMonth, WORD& aDay) { + aYear = 0; + aMonth = 0; + aDay = 0; + LPSPropValue values = NULL; + ULONG valueCount = 0; + + if (!GetMAPIProperties(aDir, aObject, &aPropertyTag, 1, values, valueCount, + fromContact)) { + return FALSE; + } + if (valueCount != 1 || values == NULL) { + PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyDate")); + return FALSE; + } + + BOOL success = TRUE; + if (PROP_TYPE(values->ulPropTag) == PT_SYSTIME) { + SYSTEMTIME readableTime; + if (FileTimeToSystemTime(&values->Value.ft, &readableTime)) { + aYear = readableTime.wYear; + aMonth = readableTime.wMonth; + aDay = readableTime.wDay; + } + } else { + PRINTF(("Cannot retrieve PT_SYSTIME property %08lx (x0A is PT_ERROR).\n", + values->ulPropTag)); + success = FALSE; + } + FreeBuffer(values); + return success; +} + +BOOL nsAbWinHelper::GetPropertyLong(const nsMapiEntry& aObject, + ULONG aPropertyTag, ULONG& aValue) { + aValue = 0; + LPSPropValue values = NULL; + ULONG valueCount = 0; + + nsMapiEntry nullEntry; + if (!GetMAPIProperties(nullEntry, aObject, &aPropertyTag, 1, values, + valueCount)) { + return FALSE; + } + if (valueCount != 1 || values == NULL) { + PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyLong")); + return FALSE; + } + + BOOL success = TRUE; + if (PROP_TYPE(values->ulPropTag) == PT_LONG) { + aValue = values->Value.ul; + } else { + PRINTF(("Cannot retrieve PT_LONG property %08lx (x0A is PT_ERROR).\n", + values->ulPropTag)); + success = FALSE; + } + FreeBuffer(values); + return success; +} + +BOOL nsAbWinHelper::GetPropertyBin(const nsMapiEntry& aObject, + ULONG aPropertyTag, nsMapiEntry& aValue) { + aValue.Assign(0, NULL); + LPSPropValue values = NULL; + ULONG valueCount = 0; + + nsMapiEntry nullEntry; + if (!GetMAPIProperties(nullEntry, aObject, &aPropertyTag, 1, values, + valueCount)) { + return FALSE; + } + if (valueCount != 1 || values == NULL) { + PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyBin")); + return FALSE; + } + + BOOL success = TRUE; + if (PROP_TYPE(values->ulPropTag) == PT_BINARY) { + aValue.Assign(values->Value.bin.cb, + reinterpret_cast<LPENTRYID>(values->Value.bin.lpb)); + } else { + PRINTF(("Cannot retrieve PT_BINARY property %08lx (x0A is PT_ERROR).\n", + values->ulPropTag)); + success = FALSE; + } + + FreeBuffer(values); + return success; +} + +BOOL nsAbWinHelper::GetPropertiesMVBin( + const nsMapiEntry& aDir, const nsMapiEntry& aObject, + const ULONG aPropertyTags[], ULONG aNbProperties, nsMapiEntry* aEntryIDs[], + ULONG aNbElements[], bool aAllocateMore) { + LPSPropValue values = NULL; + ULONG valueCount = 0; + + // Initialise output arrays. + for (ULONG i = 0; i < aNbProperties; i++) { + aEntryIDs[i] = NULL; + aNbElements[i] = 0; + } + + if (!GetMAPIProperties(aDir, aObject, aPropertyTags, aNbProperties, values, + valueCount, true)) { + return FALSE; + } + if (valueCount != aNbProperties || values == NULL) { + PRINTF(("Unexpected return value in nsAbWinHelper::GetPropertyMVBin")); + return FALSE; + } + + BOOL success = TRUE; + for (ULONG i = 0; i < valueCount; i++) { + if (PROP_TYPE(values[i].ulPropTag) == PT_MV_BINARY) { + ULONG count = values[i].Value.MVbin.cValues; + PRINTF(("Found %lu members in DL.\n", count)); + aEntryIDs[i] = new nsMapiEntry[aAllocateMore ? count + 1 : count]; + aNbElements[i] = count; + SBinary* currentValue = values[i].Value.MVbin.lpbin; + for (ULONG j = 0; j < count; j++) { + nsMapiEntry& current = aEntryIDs[i][j]; + current.Assign(currentValue->cb, + reinterpret_cast<LPENTRYID>(currentValue->lpb)); + currentValue++; + } + } else { + PRINTF( + ("Cannot retrieve PT_MV_BINARY property %08lx (x0A is PT_ERROR).\n", + values[i].ulPropTag)); + success = FALSE; + } + } + + FreeBuffer(values); + if (!success) { + for (ULONG i = 0; i < aNbProperties; i++) { + if (aNbElements[i] > 0) delete[] aEntryIDs[i]; + aEntryIDs[i] = NULL; + aNbElements[i] = 0; + } + } + return success; +} + +BOOL nsAbWinHelper::SetPropertiesMVBin(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + const ULONG aPropertyTags[], + ULONG aNbProperties, + nsMapiEntry* aEntryIDs[], + ULONG aNbElements[]) { + LPSPropValue values = new SPropValue[aNbProperties]; + if (!values) return FALSE; + + for (ULONG i = 0; i < aNbProperties; i++) { + values[i].ulPropTag = aPropertyTags[i]; + values[i].Value.MVbin.cValues = aNbElements[i]; + values[i].Value.MVbin.lpbin = new SBinary[aNbElements[i]]; + + SBinary* currentValue = values[i].Value.MVbin.lpbin; + for (ULONG j = 0; j < aNbElements[i]; j++) { + currentValue->cb = aEntryIDs[i][j].mByteCount; + currentValue->lpb = reinterpret_cast<LPBYTE>(aEntryIDs[i][j].mEntryId); + currentValue++; + } + } + BOOL retCode = SetMAPIProperties(aDir, aObject, aNbProperties, values, true); + for (ULONG i = 0; i < aNbProperties; i++) { + delete[] values[i].Value.MVbin.lpbin; + } + delete[] values; + return retCode; +} + +// This function, supposedly indicating whether a particular entry was +// in a particular container, doesn't seem to work very well (has +// a tendency to return TRUE even if we're talking to different containers...). +BOOL nsAbWinHelper::TestOpenEntry(const nsMapiEntry& aContainer, + const nsMapiEntry& aEntry) { + nsMapiInterfaceWrapper<LPMAPICONTAINER> container; + nsMapiInterfaceWrapper<LPMAPIPROP> subObject; + ULONG objType = 0; + + mLastError = + mAddressBook->OpenEntry(aContainer.mByteCount, aContainer.mEntryId, + &IID_IMAPIContainer, 0, &objType, container); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open container %08lx.\n", mLastError)); + return FALSE; + } + mLastError = container->OpenEntry(aEntry.mByteCount, aEntry.mEntryId, NULL, 0, + &objType, subObject); + return HR_SUCCEEDED(mLastError); +} + +BOOL nsAbWinHelper::DeleteEntry(const nsMapiEntry& aContainer, + const nsMapiEntry& aEntry) { + nsMapiInterfaceWrapper<LPABCONT> container; + ULONG objType = 0; + SBinary entry; + SBinaryArray entryArray; + + mLastError = mAddressBook->OpenEntry(aContainer.mByteCount, + aContainer.mEntryId, &IID_IABContainer, + MAPI_MODIFY, &objType, container); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open container %08lx.\n", mLastError)); + return FALSE; + } + entry.cb = aEntry.mByteCount; + entry.lpb = reinterpret_cast<LPBYTE>(aEntry.mEntryId); + entryArray.cValues = 1; + entryArray.lpbin = &entry; + mLastError = container->DeleteEntries(&entryArray, 0); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot delete entry %08lx.\n", mLastError)); + return FALSE; + } + return TRUE; +} + +BOOL nsAbWinHelper::GetDlMembersTag(IMAPIProp* aMsg, ULONG& aDlMembersTag, + ULONG& aDlMembersTagOneOff) { + const GUID guid = {0x00062004, + 0x0000, + 0x0000, + {0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}}; + MAPINAMEID nameID; + nameID.lpguid = (GUID*)&guid; + nameID.ulKind = MNID_ID; + LPSPropTagArray lppPropTags; + LPMAPINAMEID lpNameID[1] = {&nameID}; + + // Strangely requesting two tags at the same time doesn't appear to work, + // so request them separately. + // One should be able to set up `lpNameID` with two entries and get two + // tags returned in `lppPropTags`, but sadly the second one is always 0. + nameID.Kind.lID = 0x8055; // PidLidDistributionListMembers + mLastError = aMsg->GetIDsFromNames(1, lpNameID, 0, &lppPropTags); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot get DL prop tag %08lx.\n", mLastError)); + return FALSE; + } + aDlMembersTag = lppPropTags[0].aulPropTag[0] | PT_MV_BINARY; + mAddressFreeBuffer(lppPropTags); + + nameID.Kind.lID = 0x8054; // PidLidDistributionListOneOffMembers + mLastError = aMsg->GetIDsFromNames(1, lpNameID, 0, &lppPropTags); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open DL prop tag (one off) %08lx.\n", mLastError)); + return FALSE; + } + aDlMembersTagOneOff = lppPropTags[0].aulPropTag[0] | PT_MV_BINARY; + mAddressFreeBuffer(lppPropTags); + + return TRUE; +} + +BOOL nsAbWinHelper::GetDlNameTag(IMAPIProp* aMsg, ULONG& aDlNameTag) { + const GUID guid = {0x00062004, + 0x0000, + 0x0000, + {0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}}; + MAPINAMEID nameID; + nameID.lpguid = (GUID*)&guid; + nameID.ulKind = MNID_ID; + LPSPropTagArray lppPropTags; + LPMAPINAMEID lpNameID[1] = {&nameID}; + + nameID.Kind.lID = 0x8053; // PidLidDistributionListName + mLastError = aMsg->GetIDsFromNames(1, lpNameID, 0, &lppPropTags); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot get DL prop tag %08lx.\n", mLastError)); + return FALSE; + } + aDlNameTag = lppPropTags[0].aulPropTag[0] | PT_UNICODE; + mAddressFreeBuffer(lppPropTags); + + return TRUE; +} + +BOOL nsAbWinHelper::DeleteEntryfromDL(const nsMapiEntry& aTopDir, + const nsMapiEntry& aDistList, + const nsMapiEntry& aEntry) { + // First we need to open the distribution list to get the property tag. + ULONG dlMembersTag = 0; + ULONG dlMembersTagOnOff = 0; + { + // We do this in a block is `msg` going out of scope will release the + // object. + nsMapiInterfaceWrapper<LPMAPIPROP> msg; + mLastError = OpenMAPIObject(aTopDir, aDistList, true, 0, msg); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open DL entry %08lx.\n", mLastError)); + return FALSE; + } + if (!GetDlMembersTag(msg.Get(), dlMembersTag, dlMembersTagOnOff)) + return FALSE; + } + + // This will self-destruct when it goes out of scope. + nsMapiEntryArray dlMembers; + nsMapiEntryArray dlMembersOneOff; + + // Turn IMailUser into IMessage/IPM.Contact. + // Check for magic provider GUID. + struct AbEntryId* abEntryId = (struct AbEntryId*)aEntry.mEntryId; + if (memcmp(abEntryId->provider, CONTAB_PROVIDER_ID, + CONTAB_PROVIDER_ID_LENGTH) != 0) { + PRINTF(("Cannot get to IMessage/IPM.Contact.\n")); + return FALSE; + } + ULONG contactIdLength = abEntryId->length; + LPENTRYID contactId = reinterpret_cast<LPENTRYID>(&(abEntryId->idBytes)); + + ULONG tags[2] = {dlMembersTag, dlMembersTagOnOff}; + nsMapiEntry* values[2]; + ULONG counts[2]; + if (!GetPropertiesMVBin(aTopDir, aDistList, tags, 2, values, counts)) { + PRINTF(("Cannot get DL members.\n")); + return FALSE; + } + dlMembers.mEntries = values[0]; + dlMembersOneOff.mEntries = values[1]; + dlMembers.mNbEntries = counts[0]; + dlMembersOneOff.mNbEntries = counts[1]; + + if (dlMembers.mNbEntries == 0) return FALSE; + if (dlMembers.mNbEntries != dlMembersOneOff.mNbEntries) { + PRINTF(("DL members and DL one off members have different length.\n")); + return FALSE; + } + + ULONG result; + for (ULONG i = 0; i < dlMembers.mNbEntries; i++) { + struct DlEntryId* dlEntryId = + (struct DlEntryId*)dlMembers.mEntries[i].mEntryId; + if (memcmp(dlEntryId->provider, DL_PROVIDER_ID, DL_PROVIDER_ID_LENGTH) != 0) + continue; + mLastError = mAddressSession->CompareEntryIDs( + contactIdLength, contactId, + dlMembers.mEntries[i].mByteCount - sizeof(struct DlEntryId), + reinterpret_cast<LPENTRYID>(dlEntryId->idBytes), 0, &result); + if (HR_FAILED(mLastError)) { + PRINTF(("CompareEntryIDs failed with %08lx (DeleteEntryfromDL()).\n", + mLastError)); + } + if (result) { + PRINTF(("Found card to be deleted at position %lu.\n", i)); + + // Kill/free entry and shuffle remaining cards down. + dlMembers.mEntries[i].Assign(0, NULL); + dlMembersOneOff.mEntries[i].Assign(0, NULL); + for (ULONG j = i + 1; j < dlMembers.mNbEntries; j++) { + nsMapiEntry::Move(dlMembers.mEntries[j - 1], dlMembers.mEntries[j]); + nsMapiEntry::Move(dlMembersOneOff.mEntries[j - 1], + dlMembersOneOff.mEntries[j]); + } + dlMembers.mNbEntries--; + dlMembersOneOff.mNbEntries--; + + counts[0] = dlMembers.mNbEntries; + counts[1] = dlMembersOneOff.mNbEntries; + if (counts[0] >= 1) { + if (!SetPropertiesMVBin(aTopDir, aDistList, tags, 2, values, counts)) { + PRINTF(("Cannot set DL members.\n")); + return FALSE; + } + } else { + static const SizedSPropTagArray(2, properties) = { + 2, {dlMembersTag, dlMembersTagOnOff}}; + if (!DeleteMAPIProperties(aTopDir, aDistList, + (LPSPropTagArray)&properties, true)) { + PRINTF(("Cannot delete DL members.\n")); + return FALSE; + } + } + return TRUE; + } + } + return FALSE; +} + +BOOL nsAbWinHelper::AddEntryToDL(const nsMapiEntry& aTopDir, + const nsMapiEntry& aDistList, + const nsMapiEntry& aEntry, + const wchar_t* aDisplay, + const wchar_t* aEmail) { + // First we need to open the distribution list to get the property tag. + ULONG dlMembersTag = 0; + ULONG dlMembersTagOnOff = 0; + { + // We do this in a block is `msg` going out of scope will release the + // object. + nsMapiInterfaceWrapper<LPMAPIPROP> msg; + mLastError = OpenMAPIObject(aTopDir, aDistList, true, 0, msg); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open DL entry %08lx.\n", mLastError)); + return FALSE; + } + if (!GetDlMembersTag(msg.Get(), dlMembersTag, dlMembersTagOnOff)) + return FALSE; + } + + // This will self-destruct when it goes out of scope. + nsMapiEntryArray dlMembers; + nsMapiEntryArray dlMembersOneOff; + + // Turn IMailUser into IMessage/IPM.Contact. + // Check for magic provider GUID. + struct AbEntryId* abEntryId = (struct AbEntryId*)aEntry.mEntryId; + if (memcmp(abEntryId->provider, CONTAB_PROVIDER_ID, + CONTAB_PROVIDER_ID_LENGTH) != 0) { + PRINTF(("Cannot get to IMessage/IPM.Contact.\n")); + return FALSE; + } + ULONG contactIdLength = abEntryId->length; + LPENTRYID contactId = reinterpret_cast<LPENTRYID>(&(abEntryId->idBytes)); + + ULONG tags[2] = {dlMembersTag, dlMembersTagOnOff}; + nsMapiEntry* values[2]; + ULONG counts[2]; + // We ask for and array one entry larger. + if (!GetPropertiesMVBin(aTopDir, aDistList, tags, 2, values, counts, true)) { + // If the properties aren't there, the list has no entries so far. + values[0] = new nsMapiEntry[1]; + values[1] = new nsMapiEntry[1]; + counts[0] = counts[1] = 0; + } + dlMembers.mEntries = values[0]; + dlMembersOneOff.mEntries = values[1]; + dlMembers.mNbEntries = counts[0]; + dlMembersOneOff.mNbEntries = counts[1]; + + if (dlMembers.mNbEntries != dlMembersOneOff.mNbEntries) { + PRINTF(("DL members and DL one off members have different length.\n")); + return FALSE; + } + + // Append a new entry at the end. The array is already large enough. + + // Construct a distribution list entry based on a contact. + size_t dlEntryIdLength = sizeof(struct DlEntryId) + contactIdLength; + struct DlEntryId* dlEntryId = (DlEntryId*)moz_xmalloc(dlEntryIdLength); + memset(dlEntryId->flags, 0, DLENTRY_FLAGS_LENGTH); + memcpy(dlEntryId->provider, DL_PROVIDER_ID, DL_PROVIDER_ID_LENGTH); + // See documentation referenced above: 0xC3 = 0x80 | 0x40 | 0x03. + memset(dlEntryId->type, 0xC3, DLENTRY_TYPE_LENGTH); + memcpy(dlEntryId->idBytes, contactId, contactIdLength); + dlMembers.mEntries[dlMembers.mNbEntries].Assign( + dlEntryIdLength, reinterpret_cast<LPENTRYID>(dlEntryId)); + + // Construct a one-off entry. + size_t dlEntryIdOoLength = sizeof(struct DlEntryIdOo) + + 2 * (wcslen(aDisplay) + 4 + wcslen(aEmail) + 3); + struct DlEntryIdOo* dlEntryIdOo = + (DlEntryIdOo*)moz_xmalloc(dlEntryIdOoLength); + memset(dlEntryIdOo->flags, 0, DLENTRY_OO_FLAGS_LENGTH); + memcpy(dlEntryIdOo->provider, DL_OO_PROVIDER_ID, DL_OO_PROVIDER_ID_LENGTH); + dlEntryIdOo->versionAndBits = MAPI_UNICODE | MAPI_SEND_NO_RICH_INFO; + + // Populate the variable part. A bit of stone-age programming ;-) + size_t length = 2 * (wcslen(aDisplay) + 1); + memcpy(dlEntryIdOo->variable, aDisplay, length); + size_t offset = length; + + length = 2 * (4 + 1); + memcpy(dlEntryIdOo->variable + offset, L"SMTP", length); + offset += length; + + length = 2 * (wcslen(aEmail) + 1); + memcpy(dlEntryIdOo->variable + offset, aEmail, length); + + dlMembersOneOff.mEntries[dlMembersOneOff.mNbEntries].Assign( + dlEntryIdOoLength, reinterpret_cast<LPENTRYID>(dlEntryIdOo)); + + free(dlEntryId); + free(dlEntryIdOo); + + dlMembers.mNbEntries++; + dlMembersOneOff.mNbEntries++; + + counts[0] = dlMembers.mNbEntries; + counts[1] = dlMembersOneOff.mNbEntries; + if (!SetPropertiesMVBin(aTopDir, aDistList, tags, 2, values, counts)) { + PRINTF(("Cannot set DL members.\n")); + return FALSE; + } + return TRUE; +} + +BOOL nsAbWinHelper::SetPropertyUString(const nsMapiEntry& aObject, + ULONG aPropertyTag, + const char16_t* aValue) { + SPropValue value; + nsAutoCString alternativeValue; + + value.ulPropTag = aPropertyTag; + if (PROP_TYPE(aPropertyTag) == PT_UNICODE) { + value.Value.lpszW = + reinterpret_cast<wchar_t*>(const_cast<char16_t*>(aValue)); + } else if (PROP_TYPE(aPropertyTag) == PT_STRING8) { + alternativeValue = NS_LossyConvertUTF16toASCII(aValue); + value.Value.lpszA = const_cast<char*>(alternativeValue.get()); + } else { + PRINTF(("Property %08lx is not a string.\n", aPropertyTag)); + return FALSE; + } + nsMapiEntry nullEntry; + return SetMAPIProperties(nullEntry, aObject, 1, &value, false); +} + +BOOL nsAbWinHelper::SetPropertiesUString(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + const ULONG aPropertyTags[], + ULONG aNbProperties, + nsString aValues[]) { + LPSPropValue values = new SPropValue[aNbProperties]; + if (!values) return FALSE; + + ULONG currentValue = 0; + nsAutoCString alternativeValue; + BOOL retCode = TRUE; + + for (ULONG i = 0; i < aNbProperties; ++i) { + values[currentValue].ulPropTag = aPropertyTags[i]; + if (PROP_TYPE(aPropertyTags[i]) == PT_UNICODE) { + const wchar_t* value = aValues[i].get(); + values[currentValue++].Value.lpszW = const_cast<wchar_t*>(value); + } else if (PROP_TYPE(aPropertyTags[i]) == PT_STRING8) { + LossyCopyUTF16toASCII(aValues[i], alternativeValue); + char* av = strdup(alternativeValue.get()); + if (!av) { + retCode = FALSE; + break; + } + values[currentValue++].Value.lpszA = av; + } + } + if (retCode) + retCode = SetMAPIProperties(aDir, aObject, currentValue, values, true); + for (ULONG i = 0; i < currentValue; ++i) { + if (PROP_TYPE(aPropertyTags[i]) == PT_STRING8) { + free(values[i].Value.lpszA); + } + } + delete[] values; + return retCode; +} + +BOOL nsAbWinHelper::SetPropertyDate(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + bool fromContact, ULONG aPropertyTag, + WORD aYear, WORD aMonth, WORD aDay) { + SPropValue value; + + value.ulPropTag = aPropertyTag; + if (PROP_TYPE(aPropertyTag) == PT_SYSTIME) { + SYSTEMTIME readableTime; + + readableTime.wYear = aYear; + readableTime.wMonth = aMonth; + readableTime.wDay = aDay; + readableTime.wDayOfWeek = 0; + readableTime.wHour = 0; + readableTime.wMinute = 0; + readableTime.wSecond = 0; + readableTime.wMilliseconds = 0; + if (SystemTimeToFileTime(&readableTime, &value.Value.ft)) { + return SetMAPIProperties(aDir, aObject, 1, &value, fromContact); + } + return TRUE; + } + return FALSE; +} + +BOOL nsAbWinHelper::CreateEntryInternal(const nsMapiEntry& aParent, + nsMapiEntry& aNewEntry, + const char* aContactClass, + const wchar_t* aName) { + // We create an IPM.Contact or IPM.DistList message in the contacts folder. + // To find that folder, we look for our `aParent` in the hierarchy table + // and use the matching `PR_CONTAB_FOLDER_ENTRYID` for the folder. + nsMapiInterfaceWrapper<LPABCONT> rootFolder; + nsMapiInterfaceWrapper<LPMAPITABLE> folders; + ULONG objType = 0; + mLastError = mAddressBook->OpenEntry(0, NULL, NULL, 0, &objType, rootFolder); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open root %08lx (creating new entry).\n", mLastError)); + return FALSE; + } + mLastError = rootFolder->GetHierarchyTable(CONVENIENT_DEPTH, folders); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot get hierarchy %08lx (creating new entry).\n", mLastError)); + return FALSE; + } + + // Request `PR_ENTRYID` and `PR_CONTAB_FOLDER_ENTRYID`. +#define PR_CONTAB_FOLDER_ENTRYID PROP_TAG(PT_BINARY, 0x6610) + static const SizedSPropTagArray(2, properties) = { + 2, {PR_ENTRYID, PR_CONTAB_FOLDER_ENTRYID}}; + mLastError = folders->SetColumns((LPSPropTagArray)&properties, 0); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot set columns %08lx (creating new entry).\n", mLastError)); + return FALSE; + } + + ULONG rowCount = 0; + bool found = false; + nsMapiEntry conTab; + mLastError = folders->GetRowCount(0, &rowCount); + if (HR_SUCCEEDED(mLastError)) { + do { + LPSRowSet rowSet = NULL; + + rowCount = 0; + mLastError = folders->QueryRows(1, 0, &rowSet); + if (HR_SUCCEEDED(mLastError)) { + rowCount = rowSet->cRows; + if (rowCount > 0) { + ULONG result; + // Get entry ID from row and compare. + SPropValue& colValue = rowSet->aRow->lpProps[0]; + + mLastError = mAddressSession->CompareEntryIDs( + aParent.mByteCount, aParent.mEntryId, colValue.Value.bin.cb, + reinterpret_cast<LPENTRYID>(colValue.Value.bin.lpb), 0, &result); + if (HR_FAILED(mLastError)) { + PRINTF(("CompareEntryIDs failed with %08lx (creating new entry).\n", + mLastError)); + } + if (result) { + SPropValue& conTabValue = rowSet->aRow->lpProps[1]; + conTab.Assign( + conTabValue.Value.bin.cb, + reinterpret_cast<LPENTRYID>(conTabValue.Value.bin.lpb)); + found = true; + break; + } + } + MyFreeProws(rowSet); + } else { + PRINTF(("Cannot query rows %08lx (creating new entry).\n", mLastError)); + } + } while (rowCount > 0); + } + if (HR_FAILED(mLastError)) return HR_SUCCEEDED(mLastError); + + if (!found) { + PRINTF(("Cannot find folder for contact in hierarchy table.\n")); + return FALSE; + } + + // Open store and contact folder. + PRINTF(("Found contact folder associated with AB container.\n")); + nsMapiEntry storeEntry; + // Get the entry ID of the related store. This won't work for the + // Global Address List (GAL) since it doesn't provide contacts from a + // local store. + if (!GetPropertyBin(aParent, PR_STORE_ENTRYID, storeEntry)) { + PRINTF(("Cannot get PR_STORE_ENTRYID, likely not a local AB.\n")); + return FALSE; + } + nsMapiInterfaceWrapper<LPMDB> store; + mLastError = mAddressSession->OpenMsgStore( + 0, storeEntry.mByteCount, storeEntry.mEntryId, NULL, 0, store); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open MAPI message store %08lx.\n", mLastError)); + return FALSE; + } + nsMapiInterfaceWrapper<LPMAPIFOLDER> contactFolder; + mLastError = + store->OpenEntry(conTab.mByteCount, conTab.mEntryId, &IID_IMAPIFolder, + MAPI_MODIFY, &objType, contactFolder); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open contact folder %08lx.\n", mLastError)); + return FALSE; + } + + // Crazy as it seems, contacts and distribution lists are stored as message. + nsMapiInterfaceWrapper<LPMESSAGE> newEntry; + mLastError = contactFolder->CreateMessage(&IID_IMessage, 0, newEntry); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot create new entry %08lx.\n", mLastError)); + return FALSE; + } + + SPropValue propValue; + LPSPropProblemArray problems = NULL; + propValue.ulPropTag = PR_MESSAGE_CLASS_A; + propValue.Value.lpszA = const_cast<char*>(aContactClass); + mLastError = newEntry->SetProps(1, &propValue, &problems); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot set message class %08lx.\n", mLastError)); + return FALSE; + } + + if (strcmp(aContactClass, "IPM.DistList") == 0) { + // Set distribution list name. + problems = NULL; + GetDlNameTag(newEntry.Get(), propValue.ulPropTag); + propValue.Value.lpszW = const_cast<wchar_t*>(aName); + mLastError = newEntry->SetProps(1, &propValue, &problems); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot set DL name %08lx.\n", mLastError)); + return FALSE; + } + } + + mLastError = newEntry->SaveChanges(KEEP_OPEN_READONLY); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot commit new entry %08lx.\n", mLastError)); + return FALSE; + } + + // Get the entry ID of the contact (IMessage). + SPropTagArray property; + LPSPropValue value = NULL; + ULONG valueCount = 0; + property.cValues = 1; + property.aulPropTag[0] = PR_ENTRYID; + mLastError = newEntry->GetProps(&property, 0, &valueCount, &value); + if (HR_FAILED(mLastError) || valueCount != 1) { + PRINTF(("Cannot get entry id %08lx.\n", mLastError)); + return FALSE; + } + + // Construct the entry ID of the related address book entry (IMailUser). + AbEntryId* abEntryId = + (AbEntryId*)moz_xmalloc(sizeof(AbEntryId) + value->Value.bin.cb); + if (!abEntryId) return FALSE; + memset(abEntryId, 0, 4); // Null out the flags. + memcpy(abEntryId->provider, CONTAB_PROVIDER_ID, CONTAB_PROVIDER_ID_LENGTH); + memcpy(abEntryId->version, ABENTRY_VERSION, ABENTRY_VERSION_LENGTH); + memcpy(abEntryId->type, ABENTRY_TYPE, ABENTRY_TYPE_LENGTH); + abEntryId->index = 0; + abEntryId->length = value->Value.bin.cb; + memcpy(abEntryId->idBytes, value->Value.bin.lpb, abEntryId->length); + + aNewEntry.Assign(sizeof(AbEntryId) + value->Value.bin.cb, + reinterpret_cast<LPENTRYID>(abEntryId)); + FreeBuffer(value); + + // We need to set a display name otherwise MAPI is really unhappy internally. + SPropValue displayName; + displayName.ulPropTag = PR_DISPLAY_NAME_W; + displayName.Value.lpszW = const_cast<wchar_t*>(aName); + nsMapiInterfaceWrapper<LPMAPIPROP> object; + mLastError = + mAddressBook->OpenEntry(aNewEntry.mByteCount, aNewEntry.mEntryId, + &IID_IMAPIProp, MAPI_MODIFY, &objType, object); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open newly created AB entry %08lx.\n", mLastError)); + return FALSE; + } + mLastError = object->SetProps(1, &displayName, &problems); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot set display name %08lx.\n", mLastError)); + return FALSE; + } + + return TRUE; +} + +BOOL nsAbWinHelper::CreateEntry(const nsMapiEntry& aParent, + nsMapiEntry& aNewEntry) { + nsAutoString tempName(L"" kDummyDisplayName); + tempName.AppendInt(sEntryCounter++); + return CreateEntryInternal(aParent, aNewEntry, "IPM.Contact", tempName.get()); +} + +BOOL nsAbWinHelper::CreateDistList(const nsMapiEntry& aParent, + nsMapiEntry& aNewEntry, + const wchar_t* aName) { + return CreateEntryInternal(aParent, aNewEntry, "IPM.DistList", aName); +} + +enum { + ContentsColumnEntryId = 0, + ContentsColumnObjectType, + ContentsColumnsSize +}; + +static const SizedSPropTagArray(ContentsColumnsSize, ContentsColumns) = { + ContentsColumnsSize, {PR_ENTRYID, PR_OBJECT_TYPE}}; + +BOOL nsAbWinHelper::GetContents(const nsMapiEntry& aParent, + LPSRestriction aRestriction, + nsMapiEntry** aList, ULONG& aNbElements, + ULONG aMapiType) { + if (aList != NULL) { + *aList = NULL; + } + aNbElements = 0; + nsMapiInterfaceWrapper<LPMAPICONTAINER> parent; + nsMapiInterfaceWrapper<LPMAPITABLE> contents; + ULONG objType = 0; + ULONG rowCount = 0; + + mLastError = + mAddressBook->OpenEntry(aParent.mByteCount, aParent.mEntryId, + &IID_IMAPIContainer, 0, &objType, parent); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open parent %08lx.\n", mLastError)); + return FALSE; + } + // Historic comment: May be relevant in the future. + // WAB removed in bug 1687132. + // Here, flags for WAB and MAPI could be different, so this works + // only as long as we don't want to use any flag in GetContentsTable + mLastError = parent->GetContentsTable(0, contents); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot get contents %08lx.\n", mLastError)); + return FALSE; + } + if (aRestriction != NULL) { + mLastError = contents->Restrict(aRestriction, 0); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot set restriction %08lx.\n", mLastError)); + return FALSE; + } + } + mLastError = contents->SetColumns((LPSPropTagArray)&ContentsColumns, 0); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot set columns %08lx.\n", mLastError)); + return FALSE; + } + mLastError = contents->GetRowCount(0, &rowCount); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot get result count %08lx.\n", mLastError)); + return FALSE; + } + if (aList != NULL) { + *aList = new nsMapiEntry[rowCount]; + } + aNbElements = 0; + do { + LPSRowSet rowSet = NULL; + + rowCount = 0; + mLastError = contents->QueryRows(1, 0, &rowSet); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot query rows %08lx.\n", mLastError)); + return FALSE; + } + rowCount = rowSet->cRows; + if (rowCount > 0 && + (aMapiType == 0 || + rowSet->aRow->lpProps[ContentsColumnObjectType].Value.ul == + aMapiType)) { + if (aList != NULL) { + nsMapiEntry& current = (*aList)[aNbElements]; + SPropValue& currentValue = rowSet->aRow->lpProps[ContentsColumnEntryId]; + + // Sometimes Outlooks spits the dummy here :-( + // That is meant to be a byte count and NOT an error code of 0x8004010F. + // We gloss over it. + if (currentValue.Value.bin.cb == (ULONG)MAPI_E_NOT_FOUND || + currentValue.Value.bin.lpb == NULL) { + PRINTF(("Error fetching rows.\n")); + return TRUE; + } + current.Assign(currentValue.Value.bin.cb, + reinterpret_cast<LPENTRYID>(currentValue.Value.bin.lpb)); + } + ++aNbElements; + } + MyFreeProws(rowSet); + } while (rowCount > 0); + return TRUE; +} + +HRESULT nsAbWinHelper::OpenMAPIObject(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + bool aFromContact, ULONG aFlags, + LPUNKNOWN* aResult) { + nsMapiEntry storeEntry; + ULONG contactIdLength = 0; + LPENTRYID contactId = NULL; + if (aFromContact) { + // Get the entry ID of the related store. This won't work for the + // Global Address List (GAL) since it doesn't provide contacts from a + // local store. + if (!GetPropertyBin(aDir, PR_STORE_ENTRYID, storeEntry)) { + PRINTF(("Cannot get PR_STORE_ENTRYID, likely not a local AB.\n")); + aFromContact = false; + } + // Check for magic provider GUID. + struct AbEntryId* abEntryId = (struct AbEntryId*)aObject.mEntryId; + if (memcmp(abEntryId->provider, CONTAB_PROVIDER_ID, + CONTAB_PROVIDER_ID_LENGTH) != 0) { + aFromContact = false; + } else { + contactIdLength = abEntryId->length; + contactId = reinterpret_cast<LPENTRYID>(&(abEntryId->idBytes)); + } + } + + ULONG objType = 0; + if (aFromContact) { + // Open the store. + HRESULT retCode; + nsMapiInterfaceWrapper<LPMDB> store; + retCode = mAddressSession->OpenMsgStore( + 0, storeEntry.mByteCount, storeEntry.mEntryId, NULL, 0, store); + if (HR_FAILED(retCode)) { + PRINTF(("Cannot open MAPI message store %08lx.\n", retCode)); + return retCode; + } + // Open the contact object. + retCode = store->OpenEntry(contactIdLength, contactId, &IID_IMessage, 0, + &objType, aResult); + return retCode; + } else { + // Open the address book object. + return mAddressBook->OpenEntry(aObject.mByteCount, aObject.mEntryId, + &IID_IMAPIProp, 0, &objType, aResult); + } +} + +BOOL nsAbWinHelper::GetMAPIProperties(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + const ULONG aPropertyTags[], + ULONG aNbProperties, LPSPropValue& aValue, + ULONG& aValueCount, bool aFromContact) { + nsMapiInterfaceWrapper<LPMAPIPROP> object; + LPSPropTagArray properties = NULL; + + mLastError = OpenMAPIObject(aDir, aObject, aFromContact, 0, object); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open entry %08lx.\n", mLastError)); + return FALSE; + } + AllocateBuffer(CbNewSPropTagArray(aNbProperties), + reinterpret_cast<void**>(&properties)); + properties->cValues = aNbProperties; + for (ULONG i = 0; i < aNbProperties; ++i) { + properties->aulPropTag[i] = aPropertyTags[i]; + } + mLastError = object->GetProps(properties, 0, &aValueCount, &aValue); + FreeBuffer(properties); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot get props %08lx.\n", mLastError)); + } + return HR_SUCCEEDED(mLastError); +} + +BOOL nsAbWinHelper::SetMAPIProperties(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + ULONG aNbProperties, + const LPSPropValue& aValues, + bool aFromContact) { + nsMapiInterfaceWrapper<LPMAPIPROP> object; + LPSPropProblemArray problems = NULL; + + mLastError = OpenMAPIObject(aDir, aObject, aFromContact, MAPI_MODIFY, object); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open entry %08lx.\n", mLastError)); + return FALSE; + } + mLastError = object->SetProps(aNbProperties, aValues, &problems); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot update the object %08lx.\n", mLastError)); + return FALSE; + } + if (problems != NULL) { + for (ULONG i = 0; i < problems->cProblem; ++i) { + PRINTF(("Problem %lu: index %lu code %08lx.\n", i, + problems->aProblem[i].ulIndex, problems->aProblem[i].scode)); + } + mAddressFreeBuffer(problems); + } + mLastError = object->SaveChanges(0); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot commit changes %08lx.\n", mLastError)); + } + return HR_SUCCEEDED(mLastError); +} + +BOOL nsAbWinHelper::DeleteMAPIProperties(const nsMapiEntry& aDir, + const nsMapiEntry& aObject, + const LPSPropTagArray aProps, + bool aFromContact) { + nsMapiInterfaceWrapper<LPMAPIPROP> object; + LPSPropProblemArray problems = NULL; + + mLastError = OpenMAPIObject(aDir, aObject, aFromContact, MAPI_MODIFY, object); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot open entry %08lx.\n", mLastError)); + return FALSE; + } + mLastError = object->DeleteProps(aProps, &problems); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot update the object (DeleteProps) %08lx.\n", mLastError)); + return FALSE; + } + if (problems != NULL) { + for (ULONG i = 0; i < problems->cProblem; ++i) { + PRINTF(("Problem %lu: index %lu code %08lx.\n", i, + problems->aProblem[i].ulIndex, problems->aProblem[i].scode)); + } + mAddressFreeBuffer(problems); + } + mLastError = object->SaveChanges(0); + if (HR_FAILED(mLastError)) { + PRINTF(("Cannot commit changes %08lx.\n", mLastError)); + } + return HR_SUCCEEDED(mLastError); +} + +void nsAbWinHelper::MyFreeProws(LPSRowSet aRowset) { + if (aRowset == NULL) { + return; + } + ULONG i = 0; + + for (i = 0; i < aRowset->cRows; ++i) { + FreeBuffer(aRowset->aRow[i].lpProps); + } + FreeBuffer(aRowset); +} + +nsAbWinHelperGuard::nsAbWinHelperGuard() : mHelper(NULL) { + mHelper = new nsMapiAddressBook; +} + +nsAbWinHelperGuard::~nsAbWinHelperGuard(void) { delete mHelper; } + +void makeEntryIdFromURI(const char* aScheme, const char* aUri, + nsCString& aEntry) { + aEntry.Truncate(); + uint32_t schemeLength = strlen(aScheme); + + if (strncmp(aUri, aScheme, schemeLength) == 0) { + // Assign string from position `schemeLength`. + aEntry = aUri + schemeLength; + + // Now strip the parent directory before the /. + int ind = aEntry.FindChar('/'); + if (ind != kNotFound) { + aEntry = Substring(aEntry, ind + 1); + } + } +} + +bool nsAbWinHelper::CompareEntryIDs(nsCString& aEntryID1, + nsCString& aEntryID2) { + ULONG result; + nsMapiEntry e1; + nsMapiEntry e2; + e1.Assign(aEntryID1); + e2.Assign(aEntryID2); + mLastError = mAddressSession->CompareEntryIDs( + e1.mByteCount, e1.mEntryId, e2.mByteCount, e2.mEntryId, 0, &result); + if (HR_FAILED(mLastError)) { + PRINTF(("CompareEntryIDs failed with %08lx (CompareEntryIDs()).\n", + mLastError)); + return false; + } + return result ? true : false; +} diff --git a/comm/mailnews/addrbook/src/nsAbWinHelper.h b/comm/mailnews/addrbook/src/nsAbWinHelper.h new file mode 100644 index 0000000000..5411ae94fd --- /dev/null +++ b/comm/mailnews/addrbook/src/nsAbWinHelper.h @@ -0,0 +1,183 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsAbWinHelper_h___ +#define nsAbWinHelper_h___ + +#include <windows.h> +#include "../../mapi/include/mapix.h" + +#include "nsString.h" +#include "mozilla/StaticMutex.h" + +#define kOutlookDirectoryScheme "moz-aboutlookdirectory:///" +#define kOutlookCardScheme "moz-aboutlookcard:///" +#define kDummyDisplayName "__MailUser__" + +struct nsMapiEntry { + // Can't be assigned since it would double up the reference in `mEntryId`. + nsMapiEntry& operator=(nsMapiEntry&) = delete; + ULONG mByteCount; + LPENTRYID mEntryId; + + nsMapiEntry(void); + ~nsMapiEntry(void); + nsMapiEntry(ULONG aByteCount, LPENTRYID aEntryId); + + static void Move(nsMapiEntry& target, nsMapiEntry& source); + void Assign(ULONG aByteCount, LPENTRYID aEntryId); + void Assign(const nsCString& aString); + void ToString(nsCString& aString) const; + void Dump(void) const; +}; + +struct nsMapiEntryArray { + nsMapiEntry* mEntries; + ULONG mNbEntries; + + nsMapiEntryArray(void); + ~nsMapiEntryArray(void); + + void CleanUp(void); +}; + +class nsAbWinHelper { + public: + nsAbWinHelper(void); + virtual ~nsAbWinHelper(void); + + // Get the top address books + BOOL GetFolders(nsMapiEntryArray& aFolders); + // Get a list of entries for cards/mailing lists in a folder/mailing list + BOOL GetCards(const nsMapiEntry& aParent, LPSRestriction aRestriction, + nsMapiEntryArray& aCards); + // Get a list of mailing lists in a folder + BOOL GetNodes(const nsMapiEntry& aParent, nsMapiEntryArray& aNodes); + // Get the number of cards/mailing lists in a folder/mailing list + BOOL GetCardsCount(const nsMapiEntry& aParent, ULONG& aNbCards); + // Access last MAPI error + HRESULT LastError(void) const { return mLastError; } + // Get the value of a MAPI property of type string + BOOL GetPropertyString(const nsMapiEntry& aObject, ULONG aPropertyTag, + nsCString& aValue); + // Same as previous, but string is returned as unicode. + BOOL GetPropertyUString(const nsMapiEntry& aObject, ULONG aPropertyTag, + nsString& aValue); + // Get multiple string MAPI properties in one call. + // Retrieves the properties from the associated contact object (IMessage) + // not the address book entry (IMailUser). + BOOL GetPropertiesUString(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + const ULONG aPropertyTags[], ULONG aNbProperties, + nsString aValues[], bool aSuccess[]); + // Get the value of a MAPI property of type SYSTIME + BOOL GetPropertyDate(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + bool fromContact, ULONG aPropertyTag, WORD& aYear, + WORD& aMonth, WORD& aDay); + // Get the value of a MAPI property of type LONG + BOOL GetPropertyLong(const nsMapiEntry& aObject, ULONG aPropertyTag, + ULONG& aValue); + // Get the value of a MAPI property of type BIN + BOOL GetPropertyBin(const nsMapiEntry& aObject, ULONG aPropertyTag, + nsMapiEntry& aValue); + // Get the values of a multiple MAPI properties of type MV BIN + BOOL GetPropertiesMVBin(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + const ULONG aPropertyTags[], ULONG aNbProperties, + nsMapiEntry* aEntryIDs[], ULONG aNbElements[], + bool aAllocateMore = false); + // Set the value of a MAPI property of type MV BIN + BOOL SetPropertiesMVBin(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + const ULONG aPropertyTags[], ULONG aNbProperties, + nsMapiEntry* aEntryIDs[], ULONG aNbElements[]); + // Tests if a container contains an entry + BOOL TestOpenEntry(const nsMapiEntry& aContainer, const nsMapiEntry& aEntry); + // Delete an entry in the address book + BOOL DeleteEntry(const nsMapiEntry& aContainer, const nsMapiEntry& aEntry); + // Delete an entry from an Outlook distribution list. + BOOL DeleteEntryfromDL(const nsMapiEntry& aTopDir, + const nsMapiEntry& aDistList, + const nsMapiEntry& aEntry); + // Add an entry to an Outlook distribution list. + BOOL AddEntryToDL(const nsMapiEntry& aTopDir, const nsMapiEntry& aDistList, + const nsMapiEntry& aEntry, const wchar_t* aDisplay, + const wchar_t* aEmail); + // Set the value of a MAPI property of type string in unicode + BOOL SetPropertyUString(const nsMapiEntry& aObject, ULONG aPropertyTag, + const char16_t* aValue); + // Same as previous, but with a bunch of properties in one call. + // Sets the properties on the associated contact object (IMessage) + // not the address book entry (IMailUser). + BOOL SetPropertiesUString(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + const ULONG aPropertyTags[], ULONG aNbProperties, + nsString aValues[]); + // Set the value of a MAPI property of type SYSTIME + BOOL SetPropertyDate(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + bool fromContact, ULONG aPropertyTag, WORD aYear, + WORD aMonth, WORD aDay); + // Create entry in the address book + BOOL CreateEntry(const nsMapiEntry& aParent, nsMapiEntry& aNewEntry); + // Create a distribution list in the address book + BOOL CreateDistList(const nsMapiEntry& aParent, nsMapiEntry& aNewEntry, + const wchar_t* aName); + // Create entry worker + BOOL CreateEntryInternal(const nsMapiEntry& aParent, nsMapiEntry& aNewEntry, + const char* aContactClass, const wchar_t* aName); + // Is the helper correctly initialised? + BOOL IsOK(void) const { return mAddressBook != NULL; } + // Helper to get distribution list members tag. + BOOL GetDlMembersTag(IMAPIProp* aMsg, ULONG& aDlMembersTag, + ULONG& aDlMembersTagOneOff); + // Helper to get distribution list name tag. + BOOL GetDlNameTag(IMAPIProp* aMsg, ULONG& aDlNameTag); + // Helper to compare entry IDs. + bool CompareEntryIDs(nsCString& aEntryID1, nsCString& aEntryID2); + + protected: + HRESULT mLastError; + LPADRBOOK mAddressBook; + LPMAPISESSION mAddressSession; + LPMAPIFREEBUFFER mAddressFreeBuffer; + static uint32_t sEntryCounter; + static mozilla::StaticMutex sMutex; + + // Retrieve the contents of a container, with an optional restriction + BOOL GetContents(const nsMapiEntry& aParent, LPSRestriction aRestriction, + nsMapiEntry** aList, ULONG& aNbElements, ULONG aMapiType); + // Retrieve the values of a set of properties on a MAPI object + BOOL GetMAPIProperties(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + const ULONG aPropertyTags[], ULONG aNbProperties, + LPSPropValue& aValues, ULONG& aValueCount, + bool aFromContact = false); + // Set the values of a set of properties on a MAPI object + BOOL SetMAPIProperties(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + ULONG aNbProperties, const LPSPropValue& aValues, + bool aFromContact); + // Delete a set of properties on a MAPI object + BOOL DeleteMAPIProperties(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + const LPSPropTagArray aProps, bool aFromContact); + HRESULT OpenMAPIObject(const nsMapiEntry& aDir, const nsMapiEntry& aObject, + bool aFromContact, ULONG aFlags, LPUNKNOWN* aResult); + // Clean-up a rowset returned by QueryRows + void MyFreeProws(LPSRowSet aSet); + // Allocation of a buffer for transmission to interfaces + virtual void AllocateBuffer(ULONG aByteCount, LPVOID* aBuffer) = 0; + // Destruction of a buffer provided by the interfaces + virtual void FreeBuffer(LPVOID aBuffer) = 0; + + private: +}; + +class nsAbWinHelperGuard { + public: + explicit nsAbWinHelperGuard(); + ~nsAbWinHelperGuard(void); + + nsAbWinHelper* operator->(void) { return mHelper; } + + private: + nsAbWinHelper* mHelper; +}; + +void makeEntryIdFromURI(const char* aScheme, const char* aUri, + nsCString& aEntry); +#endif // nsAbWinHelper_h___ diff --git a/comm/mailnews/addrbook/src/nsLDAPURL.cpp b/comm/mailnews/addrbook/src/nsLDAPURL.cpp new file mode 100644 index 0000000000..90e4e370e4 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsLDAPURL.cpp @@ -0,0 +1,591 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsLDAPURL.h" +#include "netCore.h" +#include "plstr.h" +#include "nsCOMPtr.h" +#include "nsNetCID.h" +#include "nsComponentManagerUtils.h" +#include "nsIStandardURL.h" +#include "nsMsgUtils.h" +#include "nsUnicharUtils.h" +#include "mozilla/Encoding.h" + +// The two schemes we support, LDAP and LDAPS +// +constexpr auto LDAP_SCHEME = "ldap"_ns; +constexpr auto LDAP_SSL_SCHEME = "ldaps"_ns; + +NS_IMPL_ISUPPORTS(nsLDAPURL, nsILDAPURL, nsIURI) + +nsLDAPURL::nsLDAPURL() : mScope(SCOPE_BASE), mOptions(0) {} + +nsLDAPURL::~nsLDAPURL() {} + +nsresult nsLDAPURL::Init(uint32_t aUrlType, int32_t aDefaultPort, + const nsACString& aSpec, const char* aOriginCharset, + nsIURI* aBaseURI) { + nsresult rv; + nsCOMPtr<nsIURI> base(aBaseURI); + rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .Apply(&nsIStandardURLMutator::Init, + nsIStandardURL::URLTYPE_STANDARD, aDefaultPort, + PromiseFlatCString(aSpec), aOriginCharset, aBaseURI, nullptr) + .Finalize(mBaseURL); + NS_ENSURE_SUCCESS(rv, rv); + + // Now get the spec from the mBaseURL in case it was a relative one + nsCString spec; + rv = mBaseURL->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + return SetSpecInternal(spec); +} + +void nsLDAPURL::GetPathInternal(nsCString& aPath) { + aPath.Assign('/'); + + if (!mDN.IsEmpty()) aPath.Append(mDN); + + if (!mAttributes.IsEmpty()) aPath.Append('?'); + + // If mAttributes isn't empty, cut off the internally stored commas at start + // and end, and append to the path. + if (!mAttributes.IsEmpty()) + aPath.Append(Substring(mAttributes, 1, mAttributes.Length() - 2)); + + if (mScope || !mFilter.IsEmpty()) { + aPath.Append((mAttributes.IsEmpty() ? "??" : "?")); + if (mScope) { + if (mScope == SCOPE_ONELEVEL) + aPath.Append("one"); + else if (mScope == SCOPE_SUBTREE) + aPath.Append("sub"); + } + if (!mFilter.IsEmpty()) { + aPath.Append('?'); + aPath.Append(mFilter); + } + } +} + +nsresult nsLDAPURL::SetPathInternal(const nsCString& aPath) { + nsCOMPtr<nsILDAPURLParser> parser = + do_CreateInstance("@mozilla.org/network/ldap-url-parser;1"); + nsCOMPtr<nsILDAPURLParserResult> parserResult; + nsresult rv = parser->Parse(aPath, getter_AddRefs(parserResult)); + NS_ENSURE_SUCCESS(rv, rv); + + parserResult->GetDn(mDN); + parserResult->GetScope(&mScope); + parserResult->GetFilter(mFilter); + parserResult->GetOptions(&mOptions); + + nsCString attributes; + parserResult->GetAttributes(attributes); + mAttributes.Truncate(); + if (!attributes.IsEmpty()) { + // Always start and end with a comma if not empty. + mAttributes.Append(','); + mAttributes.Append(attributes); + mAttributes.Append(','); + } + + return NS_OK; +} + +// A string representation of the URI. Setting the spec +// causes the new spec to be parsed, initializing the URI. Setting +// the spec (or any of the accessors) causes also any currently +// open streams on the URI's channel to be closed. + +NS_IMETHODIMP +nsLDAPURL::GetSpec(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetSpec(_retval); +} + +nsresult nsLDAPURL::SetSpecInternal(const nsACString& aSpec) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + // Cache the original spec in case we don't like what we've been passed and + // need to reset ourselves. + nsCString originalSpec; + nsresult rv = mBaseURL->GetSpec(originalSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_MutateURI(mBaseURL).SetSpec(aSpec).Finalize(mBaseURL); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetPathInternal(PromiseFlatCString(aSpec)); + if (NS_FAILED(rv)) { + nsresult rv2 = + NS_MutateURI(mBaseURL).SetSpec(originalSpec).Finalize(mBaseURL); + NS_ENSURE_SUCCESS(rv2, rv2); + } + + return rv; +} + +NS_IMETHODIMP nsLDAPURL::GetPrePath(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetPrePath(_retval); +} + +NS_IMETHODIMP nsLDAPURL::GetScheme(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetScheme(_retval); +} + +nsresult nsLDAPURL::SetScheme(const nsACString& aScheme) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + if (aScheme.Equals(LDAP_SCHEME, nsCaseInsensitiveCStringComparator)) + mOptions &= ~OPT_SECURE; + else if (aScheme.Equals(LDAP_SSL_SCHEME, nsCaseInsensitiveCStringComparator)) + mOptions |= OPT_SECURE; + else + return NS_ERROR_MALFORMED_URI; + + return NS_MutateURI(mBaseURL).SetScheme(aScheme).Finalize(mBaseURL); +} + +NS_IMETHODIMP +nsLDAPURL::GetUserPass(nsACString& _retval) { + _retval.Truncate(); + return NS_OK; +} + +nsresult nsLDAPURL::SetUserPass(const nsACString& aUserPass) { return NS_OK; } + +NS_IMETHODIMP +nsLDAPURL::GetUsername(nsACString& _retval) { + _retval.Truncate(); + return NS_OK; +} + +nsresult nsLDAPURL::SetUsername(const nsACString& aUsername) { return NS_OK; } + +NS_IMETHODIMP +nsLDAPURL::GetPassword(nsACString& _retval) { + _retval.Truncate(); + return NS_OK; +} + +nsresult nsLDAPURL::SetPassword(const nsACString& aPassword) { return NS_OK; } + +NS_IMETHODIMP +nsLDAPURL::GetHostPort(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetHostPort(_retval); +} + +nsresult nsLDAPURL::SetHostPort(const nsACString& aHostPort) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return NS_MutateURI(mBaseURL).SetHostPort(aHostPort).Finalize(mBaseURL); +} + +NS_IMETHODIMP +nsLDAPURL::GetHost(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetHost(_retval); +} + +nsresult nsLDAPURL::SetHost(const nsACString& aHost) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return NS_MutateURI(mBaseURL).SetHost(aHost).Finalize(mBaseURL); +} + +NS_IMETHODIMP +nsLDAPURL::GetPort(int32_t* _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetPort(_retval); +} + +nsresult nsLDAPURL::SetPort(int32_t aPort) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return NS_MutateURI(mBaseURL).SetPort(aPort).Finalize(mBaseURL); +} + +NS_IMETHODIMP nsLDAPURL::GetPathQueryRef(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetPathQueryRef(_retval); +} + +nsresult nsLDAPURL::SetPathQueryRef(const nsACString& aPath) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv = SetPathInternal(PromiseFlatCString(aPath)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_MutateURI(mBaseURL).SetPathQueryRef(aPath).Finalize(mBaseURL); +} + +NS_IMETHODIMP nsLDAPURL::GetAsciiSpec(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + // XXX handle extra items? + return mBaseURL->GetAsciiSpec(_retval); +} + +NS_IMETHODIMP nsLDAPURL::GetAsciiHost(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetAsciiHost(_retval); +} + +NS_IMETHODIMP +nsLDAPURL::GetAsciiHostPort(nsACString& _retval) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->GetAsciiHostPort(_retval); +} + +// boolean equals (in nsIURI other) +// (based on nsSimpleURI::Equals) +NS_IMETHODIMP nsLDAPURL::Equals(nsIURI* other, bool* _retval) { + *_retval = false; + if (other) { + nsresult rv; + nsCOMPtr<nsILDAPURL> otherURL(do_QueryInterface(other, &rv)); + if (NS_SUCCEEDED(rv)) { + nsAutoCString thisSpec, otherSpec; + uint32_t otherOptions; + + rv = GetSpec(thisSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = otherURL->GetSpec(otherSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = otherURL->GetOptions(&otherOptions); + NS_ENSURE_SUCCESS(rv, rv); + + if (thisSpec == otherSpec && mOptions == otherOptions) *_retval = true; + } + } + return NS_OK; +} + +// boolean schemeIs(in const char * scheme); +// +NS_IMETHODIMP nsLDAPURL::SchemeIs(const char* aScheme, bool* aEquals) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + return mBaseURL->SchemeIs(aScheme, aEquals); +} + +// nsIURI clone (); +// +nsresult nsLDAPURL::Clone(nsIURI** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + RefPtr<nsLDAPURL> clone = new nsLDAPURL(); + + clone->mDN = mDN; + clone->mScope = mScope; + clone->mFilter = mFilter; + clone->mOptions = mOptions; + clone->mAttributes = mAttributes; + + nsresult rv = NS_MutateURI(mBaseURL).Finalize(clone->mBaseURL); + NS_ENSURE_SUCCESS(rv, rv); + + clone.forget(aResult); + return NS_OK; +} + +// string resolve (in string relativePath); +// +NS_IMETHODIMP nsLDAPURL::Resolve(const nsACString& relativePath, + nsACString& _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +// The following attributes come from nsILDAPURL + +// attribute AUTF8String dn; +// +NS_IMETHODIMP nsLDAPURL::GetDn(nsACString& _retval) { + _retval.Assign(mDN); + return NS_OK; +} +NS_IMETHODIMP nsLDAPURL::SetDn(const nsACString& aDn) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + mDN.Assign(aDn); + + // Now get the current path + nsCString newPath; + GetPathInternal(newPath); + + // and update the base url + return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL); +} + +NS_IMETHODIMP nsLDAPURL::GetAttributes(nsACString& aAttributes) { + if (mAttributes.IsEmpty()) { + aAttributes.Truncate(); + return NS_OK; + } + + NS_ASSERTION( + mAttributes[0] == ',' && mAttributes[mAttributes.Length() - 1] == ',', + "mAttributes does not begin and end with a comma"); + + // We store the string internally with comma before and after, so strip + // them off here. + aAttributes = Substring(mAttributes, 1, mAttributes.Length() - 2); + return NS_OK; +} + +NS_IMETHODIMP nsLDAPURL::SetAttributes(const nsACString& aAttributes) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + if (aAttributes.IsEmpty()) + mAttributes.Truncate(); + else { + // We need to make sure we start off the string with a comma. + if (aAttributes[0] != ',') mAttributes = ','; + + mAttributes.Append(aAttributes); + + // Also end with a comma if appropriate. + if (mAttributes[mAttributes.Length() - 1] != ',') mAttributes.Append(','); + } + + // Now get the current path + nsCString newPath; + GetPathInternal(newPath); + + // and update the base url + return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL); +} + +NS_IMETHODIMP nsLDAPURL::AddAttribute(const nsACString& aAttribute) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + if (mAttributes.IsEmpty()) { + mAttributes = ','; + mAttributes.Append(aAttribute); + mAttributes.Append(','); + } else { + // Wrap the attribute in commas, so that we can do an exact match. + nsAutoCString findAttribute(","); + findAttribute.Append(aAttribute); + findAttribute.Append(','); + + // Check to see if the attribute is already stored. If it is, then also + // check to see if it is the last attribute in the string, or if the next + // character is a comma, this means we won't match substrings. + if (FindInReadable(findAttribute, mAttributes, + nsCaseInsensitiveCStringComparator)) { + return NS_OK; + } + + mAttributes.Append(Substring(findAttribute, 1)); + } + + // Now get the current path + nsCString newPath; + GetPathInternal(newPath); + + // and update the base url + return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL); +} + +NS_IMETHODIMP nsLDAPURL::RemoveAttribute(const nsACString& aAttribute) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + if (mAttributes.IsEmpty()) return NS_OK; + + // We use comma as delimiter (even first attr has a leading comma). + nsAutoCString findAttribute(","); + findAttribute.Append(aAttribute); + findAttribute.Append(','); + + if (!FindInReadable(findAttribute, mAttributes, + nsCaseInsensitiveCStringComparator)) { + return NS_OK; + } + if (mAttributes.Equals(findAttribute, nsCaseInsensitiveCStringComparator)) { + mAttributes.Truncate(); + } else { + mAttributes.ReplaceSubstring(findAttribute, ","_ns); + } + + // Now get the current path + nsCString newPath; + GetPathInternal(newPath); + + // and update the base url + return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL); +} + +NS_IMETHODIMP nsLDAPURL::HasAttribute(const nsACString& aAttribute, + bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + // We use comma as delimiter (even first attr has a leading comma). + nsAutoCString findAttribute(","); + findAttribute.Append(aAttribute); + findAttribute.Append(','); + + *_retval = FindInReadable(findAttribute, mAttributes, + nsCaseInsensitiveCStringComparator); + return NS_OK; +} + +NS_IMETHODIMP nsLDAPURL::GetScope(int32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = mScope; + return NS_OK; +} + +NS_IMETHODIMP nsLDAPURL::SetScope(int32_t aScope) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + // Only allow scopes supported by the C-SDK + if ((aScope != SCOPE_BASE) && (aScope != SCOPE_ONELEVEL) && + (aScope != SCOPE_SUBTREE)) + return NS_ERROR_MALFORMED_URI; + + mScope = aScope; + + // Now get the current path + nsCString newPath; + GetPathInternal(newPath); + + // and update the base url + return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL); +} + +NS_IMETHODIMP nsLDAPURL::GetFilter(nsACString& _retval) { + _retval.Assign(mFilter); + return NS_OK; +} +NS_IMETHODIMP nsLDAPURL::SetFilter(const nsACString& aFilter) { + if (!mBaseURL) return NS_ERROR_NOT_INITIALIZED; + + mFilter.Assign(aFilter); + + if (mFilter.IsEmpty()) mFilter.AssignLiteral("(objectclass=*)"); + + // Now get the current path + nsCString newPath; + GetPathInternal(newPath); + + // and update the base url + return NS_MutateURI(mBaseURL).SetPathQueryRef(newPath).Finalize(mBaseURL); +} + +NS_IMETHODIMP nsLDAPURL::GetOptions(uint32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = mOptions; + return NS_OK; +} + +NS_IMETHODIMP nsLDAPURL::SetOptions(uint32_t aOptions) { + // Secure is the only option supported at the moment + if ((mOptions & OPT_SECURE) == (aOptions & OPT_SECURE)) return NS_OK; + + mOptions = aOptions; + + if ((aOptions & OPT_SECURE) == OPT_SECURE) return SetScheme(LDAP_SSL_SCHEME); + + return SetScheme(LDAP_SCHEME); +} + +nsresult nsLDAPURL::SetRef(const nsACString& aRef) { + return NS_MutateURI(mBaseURL).SetRef(aRef).Finalize(mBaseURL); +} + +NS_IMETHODIMP +nsLDAPURL::GetRef(nsACString& result) { return mBaseURL->GetRef(result); } + +NS_IMETHODIMP nsLDAPURL::EqualsExceptRef(nsIURI* other, bool* result) { + return mBaseURL->EqualsExceptRef(other, result); +} + +NS_IMETHODIMP +nsLDAPURL::GetSpecIgnoringRef(nsACString& result) { + return mBaseURL->GetSpecIgnoringRef(result); +} + +NS_IMETHODIMP +nsLDAPURL::GetDisplaySpec(nsACString& aUnicodeSpec) { + return mBaseURL->GetDisplaySpec(aUnicodeSpec); +} + +NS_IMETHODIMP +nsLDAPURL::GetDisplayHostPort(nsACString& aUnicodeHostPort) { + return mBaseURL->GetDisplayHostPort(aUnicodeHostPort); +} + +NS_IMETHODIMP +nsLDAPURL::GetDisplayHost(nsACString& aUnicodeHost) { + return mBaseURL->GetDisplayHost(aUnicodeHost); +} + +NS_IMETHODIMP +nsLDAPURL::GetDisplayPrePath(nsACString& aPrePath) { + return mBaseURL->GetDisplayPrePath(aPrePath); +} + +NS_IMETHODIMP +nsLDAPURL::GetHasRef(bool* result) { return mBaseURL->GetHasRef(result); } + +NS_IMETHODIMP +nsLDAPURL::GetFilePath(nsACString& aFilePath) { + return mBaseURL->GetFilePath(aFilePath); +} + +nsresult nsLDAPURL::SetFilePath(const nsACString& aFilePath) { + return NS_MutateURI(mBaseURL).SetFilePath(aFilePath).Finalize(mBaseURL); +} + +NS_IMETHODIMP +nsLDAPURL::GetQuery(nsACString& aQuery) { return mBaseURL->GetQuery(aQuery); } + +nsresult nsLDAPURL::SetQuery(const nsACString& aQuery) { + return NS_MutateURI(mBaseURL).SetQuery(aQuery).Finalize(mBaseURL); +} + +nsresult nsLDAPURL::SetQueryWithEncoding(const nsACString& aQuery, + const mozilla::Encoding* aEncoding) { + return NS_MutateURI(mBaseURL) + .SetQueryWithEncoding(aQuery, aEncoding) + .Finalize(mBaseURL); +} + +NS_IMETHODIMP_(void) +nsLDAPURL::Serialize(mozilla::ipc::URIParams& aParams) { + mBaseURL->Serialize(aParams); +} + +NS_IMPL_ISUPPORTS(nsLDAPURL::Mutator, nsIURISetters, nsIURIMutator) + +NS_IMETHODIMP +nsLDAPURL::Mutate(nsIURIMutator** aMutator) { + RefPtr<nsLDAPURL::Mutator> mutator = new nsLDAPURL::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; +} diff --git a/comm/mailnews/addrbook/src/nsLDAPURL.h b/comm/mailnews/addrbook/src/nsLDAPURL.h new file mode 100644 index 0000000000..60c3cbb6de --- /dev/null +++ b/comm/mailnews/addrbook/src/nsLDAPURL.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +#include "nsString.h" +#include "nsILDAPURL.h" +#include "nsCOMPtr.h" +#include "nsIURIMutator.h" + +/** + * nsLDAPURL + * + * nsLDAPURL uses an nsStandardURL stored in mBaseURL as its main url formatter. + * + * This is done to ensure that the pre-path sections of the URI are correctly + * formatted and to re-use the functions for nsIURI as appropriate. + * + * Handling of the path sections of the URI are done within nsLDAPURL/parts of + * the LDAP c-sdk. nsLDAPURL holds the individual sections of the path of the + * URI locally (to allow convenient get/set), but always updates the mBaseURL + * when one changes to ensure that mBaseURL.spec and the local data are kept + * consistent. + */ + +class nsLDAPURL : public nsILDAPURL { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIURI + NS_DECL_NSILDAPURL + + nsLDAPURL(); + + protected: + virtual nsresult Clone(nsIURI** _retval); + virtual nsresult SetSpecInternal(const nsACString& aSpec); + virtual nsresult SetScheme(const nsACString& aScheme); + virtual nsresult SetUserPass(const nsACString& aUserPass); + virtual nsresult SetUsername(const nsACString& aUsername); + virtual nsresult SetPassword(const nsACString& aPassword); + virtual nsresult SetHostPort(const nsACString& aHostPort); + virtual nsresult SetHost(const nsACString& aHost); + virtual nsresult SetPort(int32_t aPort); + virtual nsresult SetPathQueryRef(const nsACString& aPath); + virtual nsresult SetRef(const nsACString& aRef); + virtual nsresult SetFilePath(const nsACString& aFilePath); + virtual nsresult SetQuery(const nsACString& aQuery); + virtual nsresult SetQueryWithEncoding(const nsACString& aQuery, + const mozilla::Encoding* aEncoding); + + public: + class Mutator : public nsIURIMutator, public BaseURIMutator<nsLDAPURL> { + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI) + + NS_IMETHOD Deserialize(const mozilla::ipc::URIParams& aParams) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD Finalize(nsIURI** aURI) override { + mURI.forget(aURI); + return NS_OK; + } + + NS_IMETHOD SetSpec(const nsACString& aSpec, + nsIURIMutator** aMutator) override { + if (aMutator) NS_ADDREF(*aMutator = this); + return InitFromSpec(aSpec); + } + + explicit Mutator() {} + + private: + virtual ~Mutator() {} + + friend class nsLDAPURL; + }; + friend BaseURIMutator<nsLDAPURL>; + + protected: + virtual ~nsLDAPURL(); + + void GetPathInternal(nsCString& aPath); + nsresult SetPathInternal(const nsCString& aPath); + + nsCString mDN; // Base Distinguished Name (Base DN) + int32_t mScope; // Search scope (base, one or sub) + nsCString mFilter; // LDAP search filter + uint32_t mOptions; // Options + nsCString + mAttributes; // Either empty ("") or comma-separated list with + // leading _and_ trailing commas (i.e ",attr1,attr2,"). + nsCOMPtr<nsIURI> mBaseURL; +}; diff --git a/comm/mailnews/addrbook/src/nsMapiAddressBook.cpp b/comm/mailnews/addrbook/src/nsMapiAddressBook.cpp new file mode 100644 index 0000000000..bd6e080443 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsMapiAddressBook.cpp @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ +#include "nsMapiAddressBook.h" + +#include "mozilla/Logging.h" +#include "mozilla/DebugOnly.h" + +#define PRINT_TO_CONSOLE 0 +#if PRINT_TO_CONSOLE +# define PRINTF(args) printf args +#else +static mozilla::LazyLogModule gMapiAddressBookLog("MAPIAddressBook"); +# define PRINTF(args) \ + MOZ_LOG(gMapiAddressBookLog, mozilla::LogLevel::Debug, args) +#endif + +using namespace mozilla; + +HMODULE nsMapiAddressBook::mLibrary = NULL; +int32_t nsMapiAddressBook::mLibUsage = 0; +LPMAPIINITIALIZE nsMapiAddressBook::mMAPIInitialize = NULL; +LPMAPIUNINITIALIZE nsMapiAddressBook::mMAPIUninitialize = NULL; +LPMAPIALLOCATEBUFFER nsMapiAddressBook::mMAPIAllocateBuffer = NULL; +LPMAPIFREEBUFFER nsMapiAddressBook::mMAPIFreeBuffer = NULL; +LPMAPILOGONEX nsMapiAddressBook::mMAPILogonEx = NULL; + +BOOL nsMapiAddressBook::mInitialized = FALSE; +BOOL nsMapiAddressBook::mLogonDone = FALSE; +LPMAPISESSION nsMapiAddressBook::mRootSession = NULL; +LPADRBOOK nsMapiAddressBook::mRootBook = NULL; + +BOOL nsMapiAddressBook::LoadMapiLibrary(void) { + if (mLibrary) { + ++mLibUsage; + return TRUE; + } + HMODULE libraryHandle = LoadLibraryW(L"MAPI32.DLL"); + + if (!libraryHandle) { + return FALSE; + } + FARPROC entryPoint = GetProcAddress(libraryHandle, "MAPIGetNetscapeVersion"); + + if (entryPoint) { + FreeLibrary(libraryHandle); + libraryHandle = LoadLibraryW(L"MAPI32BAK.DLL"); + if (!libraryHandle) { + return FALSE; + } + } + mLibrary = libraryHandle; + ++mLibUsage; + mMAPIInitialize = reinterpret_cast<LPMAPIINITIALIZE>( + GetProcAddress(mLibrary, "MAPIInitialize")); + if (!mMAPIInitialize) { + return FALSE; + } + mMAPIUninitialize = reinterpret_cast<LPMAPIUNINITIALIZE>( + GetProcAddress(mLibrary, "MAPIUninitialize")); + if (!mMAPIUninitialize) { + return FALSE; + } + mMAPIAllocateBuffer = reinterpret_cast<LPMAPIALLOCATEBUFFER>( + GetProcAddress(mLibrary, "MAPIAllocateBuffer")); + if (!mMAPIAllocateBuffer) { + return FALSE; + } + mMAPIFreeBuffer = reinterpret_cast<LPMAPIFREEBUFFER>( + GetProcAddress(mLibrary, "MAPIFreeBuffer")); + if (!mMAPIFreeBuffer) { + return FALSE; + } + mMAPILogonEx = + reinterpret_cast<LPMAPILOGONEX>(GetProcAddress(mLibrary, "MAPILogonEx")); + if (!mMAPILogonEx) { + return FALSE; + } + MAPIINIT_0 mapiInit = {MAPI_INIT_VERSION, MAPI_MULTITHREAD_NOTIFICATIONS}; + HRESULT retCode = mMAPIInitialize(&mapiInit); + + if (HR_FAILED(retCode)) { + PRINTF(("Cannot initialize MAPI %08lx.\n", retCode)); + return FALSE; + } + mInitialized = TRUE; + retCode = mMAPILogonEx( + 0, NULL, NULL, + MAPI_NO_MAIL | MAPI_USE_DEFAULT | MAPI_EXTENDED | MAPI_NEW_SESSION, + &mRootSession); + if (HR_FAILED(retCode)) { + PRINTF(("Cannot logon to MAPI %08lx.\n", retCode)); + return FALSE; + } + mLogonDone = TRUE; + retCode = mRootSession->OpenAddressBook(0, NULL, 0, &mRootBook); + if (HR_FAILED(retCode)) { + PRINTF(("Cannot open MAPI address book %08lx.\n", retCode)); + } + return HR_SUCCEEDED(retCode); +} + +void nsMapiAddressBook::FreeMapiLibrary(void) { + if (mLibrary) { + if (--mLibUsage == 0) { + { + if (mRootBook) { + mRootBook->Release(); + } + if (mRootSession) { + if (mLogonDone) { + mRootSession->Logoff(NULL, 0, 0); + mLogonDone = FALSE; + } + mRootSession->Release(); + } + if (mInitialized) { + mMAPIUninitialize(); + mInitialized = FALSE; + } + } + FreeLibrary(mLibrary); + mLibrary = NULL; + } + } +} + +nsMapiAddressBook::nsMapiAddressBook(void) : nsAbWinHelper() { + mozilla::DebugOnly<BOOL> result = Initialize(); + + NS_ASSERTION(result == TRUE, "Couldn't initialize Mapi Helper"); + MOZ_COUNT_CTOR(nsMapiAddressBook); +} + +nsMapiAddressBook::~nsMapiAddressBook(void) { + StaticMutexAutoLock guard(sMutex); + + FreeMapiLibrary(); + MOZ_COUNT_DTOR(nsMapiAddressBook); +} + +BOOL nsMapiAddressBook::Initialize(void) { + if (mAddressBook) { + return TRUE; + } + StaticMutexAutoLock guard(sMutex); + + if (!LoadMapiLibrary()) { + PRINTF(("Cannot load library.\n")); + return FALSE; + } + mAddressBook = mRootBook; + mAddressSession = mRootSession; + mAddressFreeBuffer = mMAPIFreeBuffer; + return TRUE; +} + +void nsMapiAddressBook::AllocateBuffer(ULONG aByteCount, LPVOID* aBuffer) { + mMAPIAllocateBuffer(aByteCount, aBuffer); +} + +void nsMapiAddressBook::FreeBuffer(LPVOID aBuffer) { mMAPIFreeBuffer(aBuffer); } diff --git a/comm/mailnews/addrbook/src/nsMapiAddressBook.h b/comm/mailnews/addrbook/src/nsMapiAddressBook.h new file mode 100644 index 0000000000..dd0463dba0 --- /dev/null +++ b/comm/mailnews/addrbook/src/nsMapiAddressBook.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsMapiAddressBook_h___ +#define nsMapiAddressBook_h___ + +#include "mozilla/Attributes.h" +#include "nsAbWinHelper.h" + +class nsMapiAddressBook : public nsAbWinHelper { + public: + nsMapiAddressBook(void); + virtual ~nsMapiAddressBook(void); + + protected: + // Class members to handle the library/entry points + static HMODULE mLibrary; + static int32_t mLibUsage; + static LPMAPIINITIALIZE mMAPIInitialize; + static LPMAPIUNINITIALIZE mMAPIUninitialize; + static LPMAPIALLOCATEBUFFER mMAPIAllocateBuffer; + static LPMAPIFREEBUFFER mMAPIFreeBuffer; + static LPMAPILOGONEX mMAPILogonEx; + // Shared session and address book used by all instances. + // For reasons best left unknown, MAPI doesn't seem to like + // having different threads playing with supposedly different + // sessions and address books. They ll end up fighting over + // the same resources, with hangups and GPF resulting. Not nice. + // So it seems that if everybody (as long as some client is + // still alive) is using the same sessions and address books, + // MAPI feels better. And who are we to get in the way of MAPI + // happiness? Thus the following class members: + static BOOL mInitialized; + static BOOL mLogonDone; + static LPMAPISESSION mRootSession; + static LPADRBOOK mRootBook; + + // Load the MAPI environment + BOOL Initialize(void); + // Allocation of a buffer for transmission to interfaces + virtual void AllocateBuffer(ULONG aByteCount, LPVOID* aBuffer) override; + // Destruction of a buffer provided by the interfaces + virtual void FreeBuffer(LPVOID aBuffer) override; + // Library management + static BOOL LoadMapiLibrary(void); + static void FreeMapiLibrary(void); + + private: +}; + +#endif // nsMapiAddressBook_h___ diff --git a/comm/mailnews/addrbook/test/CardDAVServer.jsm b/comm/mailnews/addrbook/test/CardDAVServer.jsm new file mode 100644 index 0000000000..5bd1275b41 --- /dev/null +++ b/comm/mailnews/addrbook/test/CardDAVServer.jsm @@ -0,0 +1,634 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["CardDAVServer"]; + +const PREFIX_BINDINGS = { + card: "urn:ietf:params:xml:ns:carddav", + cs: "http://calendarserver.org/ns/", + d: "DAV:", +}; +const NAMESPACE_STRING = Object.entries(PREFIX_BINDINGS) + .map(([prefix, url]) => `xmlns:${prefix}="${url}"`) + .join(" "); + +const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" +); +const { CommonUtils } = ChromeUtils.importESModule( + "resource://services-common/utils.sys.mjs" +); +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); + +var CardDAVServer = { + books: { + "/addressbooks/me/default/": "Not This One", + "/addressbooks/me/test/": "CardDAV Test", + }, + cards: new Map(), + movedCards: new Map(), + deletedCards: new Map(), + changeCount: 0, + server: null, + isOpen: false, + + open(username, password, port = -1) { + this.server = new HttpServer(); + this.server.start(port); + this.port = this.server.identity.primaryPort; + this.isOpen = true; + + this.username = username; + this.password = password; + this.server.registerPathHandler("/ping", this.ping); + + this.reset(); + }, + + reopen() { + this.server.start(this.port); + this.isOpen = true; + }, + + reset() { + this.cards.clear(); + this.deletedCards.clear(); + this.changeCount = 0; + this.resetHandlers(); + }, + + resetHandlers() { + // Address book discovery. + + this.server.registerPathHandler("/", this.wellKnown.bind(this)); + this.server.registerPathHandler( + "/.well-known/carddav", + this.wellKnown.bind(this) + ); + this.server.registerPathHandler("/principals/", this.principals.bind(this)); + this.server.registerPathHandler( + "/principals/me/", + this.myPrincipal.bind(this) + ); + this.server.registerPathHandler( + "/addressbooks/me/", + this.myAddressBooks.bind(this) + ); + + // Address book interaction. + + for (let path of Object.keys(this.books)) { + this.server.registerPathHandler(path, this.directoryHandler.bind(this)); + this.server.registerPrefixHandler(path, this.cardHandler.bind(this)); + } + }, + + close() { + if (!this.isOpen) { + return Promise.resolve(); + } + return new Promise(resolve => + this.server.stop({ + onStopped: () => { + this.isOpen = false; + resolve(); + }, + }) + ); + }, + + get origin() { + return `http://localhost:${this.server.identity.primaryPort}`; + }, + + get path() { + return "/addressbooks/me/test/"; + }, + + get url() { + return `${this.origin}${this.path}`; + }, + + get altPath() { + return "/addressbooks/me/default/"; + }, + + get altURL() { + return `${this.origin}${this.altPath}`; + }, + + checkAuth(request, response) { + if (!this.username || !this.password) { + return true; + } + if (!request.hasHeader("Authorization")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + let value = request.getHeader("Authorization"); + if (!value.startsWith("Basic ")) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + let [username, password] = atob(value.substring(6)).split(":"); + if (username != this.username || password != this.password) { + response.setStatusLine("1.1", 401, "Unauthorized"); + response.setHeader("WWW-Authenticate", `Basic realm="test"`); + return false; + } + + return true; + }, + + ping(request, response) { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/plain"); + response.write("pong"); + }, + + wellKnown(request, response) { + response.setStatusLine("1.1", 301, "Moved Permanently"); + response.setHeader("Location", "/principals/"); + }, + + principals(request, response) { + if (!this.checkAuth(request, response)) { + return; + } + + let input = new DOMParser().parseFromString( + CommonUtils.readBytesFromInputStream(request.bodyInputStream), + "text/xml" + ); + + let propNames = this._inputProps(input); + let propValues = { + "d:current-user-principal": "<href>/principals/me/</href>", + }; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write( + `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}> + <response> + <href>/principals/</href> + ${this._outputProps(propNames, propValues)} + </response> + </multistatus>`.replace(/>\s+</g, "><") + ); + }, + + myPrincipal(request, response) { + if (!this.checkAuth(request, response)) { + return; + } + + let input = new DOMParser().parseFromString( + CommonUtils.readBytesFromInputStream(request.bodyInputStream), + "text/xml" + ); + + let propNames = this._inputProps(input); + let propValues = { + "d:resourcetype": "<principal/>", + "card:addressbook-home-set": "<href>/addressbooks/me/</href>", + }; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write( + `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}> + <response> + <href>/principals/me/</href> + ${this._outputProps(propNames, propValues)} + </response> + </multistatus>`.replace(/>\s+</g, "><") + ); + }, + + myAddressBooks(request, response) { + if (!this.checkAuth(request, response)) { + return; + } + + let input = new DOMParser().parseFromString( + CommonUtils.readBytesFromInputStream(request.bodyInputStream), + "text/xml" + ); + + let propNames = this._inputProps(input); + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + + let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}> + <response> + <href>/addressbooks/me/</href> + ${this._outputProps(propNames, { + "d:resourcetype": "<collection/>", + "d:displayname": "#addressbooks", + })} + </response>`; + + for (let [path, name] of Object.entries(this.books)) { + output += `<response> + <href>${path}</href> + ${this._outputProps(propNames, { + "d:resourcetype": "<collection/><card:addressbook/>", + "d:displayname": name, + "d:current-user-privilege-set": + "<d:privilege><d:all/></d:privilege>", + })} + </response>`; + } + + output += `</multistatus>`; + response.write(output.replace(/>\s+</g, "><")); + }, + + /** Handle any requests to the address book itself. */ + + directoryHandler(request, response) { + if (!this.checkAuth(request, response)) { + return; + } + + let isRealDirectory = request.path == this.path; + let input = new DOMParser().parseFromString( + CommonUtils.readBytesFromInputStream(request.bodyInputStream), + "text/xml" + ); + + switch (input.documentElement.localName) { + case "addressbook-query": + Assert.equal(request.method, "REPORT"); + Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.card); + this.addressBookQuery(input, response, isRealDirectory); + return; + case "addressbook-multiget": + Assert.equal(request.method, "REPORT"); + Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.card); + this.addressBookMultiGet(input, response, isRealDirectory); + return; + case "propfind": + Assert.equal(request.method, "PROPFIND"); + Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.d); + this.propFind( + input, + request.hasHeader("Depth") ? request.getHeader("Depth") : 0, + response, + isRealDirectory + ); + return; + case "sync-collection": + Assert.equal(request.method, "REPORT"); + Assert.equal(input.documentElement.namespaceURI, PREFIX_BINDINGS.d); + this.syncCollection(input, response, isRealDirectory); + return; + } + + Assert.report(true, undefined, undefined, "Should not have reached here"); + response.setStatusLine("1.1", 404, "Not Found"); + response.setHeader("Content-Type", "text/plain"); + response.write(`No handler found for <${input.documentElement.localName}>`); + }, + + addressBookQuery(input, response, isRealDirectory) { + if (this.mimicYahoo) { + response.setStatusLine("1.1", 400, "Bad Request"); + return; + } + + let propNames = this._inputProps(input); + let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`; + if (isRealDirectory) { + for (let [href, card] of this.cards) { + output += this._cardResponse(href, card, propNames); + } + } + output += `</multistatus>`; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write(output.replace(/>\s+</g, "><")); + }, + + addressBookMultiGet(input, response, isRealDirectory) { + let propNames = this._inputProps(input); + let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`; + if (isRealDirectory) { + for (let href of input.querySelectorAll("href")) { + href = href.textContent; + if (this.movedCards.has(href)) { + href = this.movedCards.get(href); + } + let card = this.cards.get(href); + if (card) { + output += this._cardResponse(href, card, propNames); + } + } + } + output += `</multistatus>`; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write(output.replace(/>\s+</g, "><")); + }, + + propFind(input, depth, response, isRealDirectory) { + let propNames = this._inputProps(input); + + if (this.mimicYahoo && !propNames.includes("cs:getctag")) { + response.setStatusLine("1.1", 400, "Bad Request"); + return; + } + + let propValues = { + "cs:getctag": this.changeCount, + "d:displayname": isRealDirectory ? "CardDAV Test" : "Not This One", + "d:resourcetype": "<collection/><card:addressbook/>", + "d:current-user-privilege-set": "<d:privilege><d:all/></d:privilege>", + }; + if (!this.mimicYahoo) { + propValues["d:sync-token"] = `http://mochi.test/sync/${this.changeCount}`; + } + + let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}> + <response> + <href>${isRealDirectory ? this.path : this.altPath}</href> + ${this._outputProps(propNames, propValues)} + </response>`; + if (depth == 1 && isRealDirectory) { + for (let [href, card] of this.cards) { + output += this._cardResponse(href, card, propNames); + } + } + output += `</multistatus>`; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write(output.replace(/>\s+</g, "><")); + }, + + syncCollection(input, response, isRealDirectory) { + let token = input + .querySelector("sync-token") + .textContent.replace(/\D/g, ""); + if (!token) { + response.setStatusLine("1.1", 400, "Bad Request"); + return; + } + let propNames = this._inputProps(input); + + let output = `<multistatus xmlns="${PREFIX_BINDINGS.d}" ${NAMESPACE_STRING}>`; + if (isRealDirectory) { + for (let [href, card] of this.cards) { + if (card.changed > token) { + output += this._cardResponse( + href, + card, + propNames, + !this.mimicGoogle + ); + } + } + for (let [href, deleted] of this.deletedCards) { + if (deleted > token) { + output += `<response> + <status>HTTP/1.1 404 Not Found</status> + <href>${href}</href> + <propstat> + <prop/> + <status>HTTP/1.1 418 I'm a teapot</status> + </propstat> + </response>`; + } + } + } + output += `<sync-token>http://mochi.test/sync/${this.changeCount}</sync-token> + </multistatus>`; + + response.setStatusLine("1.1", 207, "Multi-Status"); + response.setHeader("Content-Type", "text/xml"); + response.write(output.replace(/>\s+</g, "><")); + }, + + _cardResponse(href, card, propNames, includeAddressData = true) { + let propValues = { + "d:getetag": card.etag, + "d:resourcetype": null, + }; + + if (includeAddressData) { + propValues["card:address-data"] = card.vCard; + } + + let outString = `<response> + <href>${href}</href> + ${this._outputProps(propNames, propValues)} + </response>`; + return outString; + }, + + _inputProps(input) { + let props = input.querySelectorAll("prop > *"); + let propNames = []; + + for (let p of props) { + Assert.equal(p.childElementCount, 0); + switch (p.localName) { + case "address-data": + case "addressbook-home-set": + Assert.equal(p.namespaceURI, PREFIX_BINDINGS.card); + propNames.push(`card:${p.localName}`); + break; + case "getctag": + Assert.equal(p.namespaceURI, PREFIX_BINDINGS.cs); + propNames.push(`cs:${p.localName}`); + break; + case "current-user-principal": + case "current-user-privilege-set": + case "displayname": + case "getetag": + case "resourcetype": + case "sync-token": + Assert.equal(p.namespaceURI, PREFIX_BINDINGS.d); + propNames.push(`d:${p.localName}`); + break; + default: + Assert.report( + true, + undefined, + undefined, + `Unknown property requested: ${p.nodeName}` + ); + break; + } + } + + return propNames; + }, + + _outputProps(propNames, propValues) { + let output = ""; + + let found = []; + let notFound = []; + for (let p of propNames) { + if (p in propValues && propValues[p] !== undefined) { + found.push(`<${p}>${propValues[p]}</${p}>`); + } else { + notFound.push(`<${p}/>`); + } + } + + if (found.length > 0) { + output += `<propstat> + <prop> + ${found.join("\n")} + </prop> + <status>HTTP/1.1 200 OK</status> + </propstat>`; + } + if (notFound.length > 0) { + output += `<propstat> + <prop> + ${notFound.join("\n")} + </prop> + <status>HTTP/1.1 404 Not Found</status> + </propstat>`; + } + + return output; + }, + + /** Handle any requests to address book cards. */ + + cardHandler(request, response) { + if (!this.checkAuth(request, response)) { + return; + } + + let isRealDirectory = request.path.startsWith(this.path); + if (!isRealDirectory || !/\/[\w-]+\.vcf$/.test(request.path)) { + response.setStatusLine("1.1", 404, "Not Found"); + response.setHeader("Content-Type", "text/plain"); + response.write(`Card not found at ${request.path}`); + return; + } + + switch (request.method) { + case "GET": + this.getCard(request, response); + return; + case "PUT": + this.putCard(request, response); + return; + case "DELETE": + this.deleteCard(request, response); + return; + } + + Assert.report(true, undefined, undefined, "Should not have reached here"); + response.setStatusLine("1.1", 405, "Method Not Allowed"); + response.setHeader("Content-Type", "text/plain"); + response.write(`Method not allowed: ${request.method}`); + }, + + getCard(request, response) { + let card = this.cards.get(request.path); + if (!card) { + response.setStatusLine("1.1", 404, "Not Found"); + response.setHeader("Content-Type", "text/plain"); + response.write(`Card not found at ${request.path}`); + return; + } + + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/vcard"); + response.setHeader("ETag", card.etag); + response.write(card.vCard); + }, + + putCard(request, response) { + if (request.hasHeader("If-Match")) { + let card = this.cards.get(request.path); + if (!card || card.etag != request.getHeader("If-Match")) { + response.setStatusLine("1.1", 412, "Precondition Failed"); + return; + } + } + + let vCard = CommonUtils.readBytesFromInputStream(request.bodyInputStream); + if (this.mimicGoogle && !/^N[;:]/im.test(vCard)) { + response.setStatusLine("1.1", 400, "Bad Request"); + return; + } + + this.putCardInternal(request.path, vCard); + response.setStatusLine("1.1", 204, "No Content"); + + if (this.responseDelay) { + response.processAsync(); + this.responseDelay.promise.then(() => { + delete this.responseDelay; + response.finish(); + }); + } + }, + + putCardInternal(name, vCard) { + if (!name.startsWith("/")) { + name = this.path + name; + } + if (this.modifyCardOnPut && !this.cards.has(name)) { + vCard = vCard.replace(/UID:(\S+)/, (match, uid) => { + let newUID = [...uid].reverse().join(""); + let newName = this.path + newUID + ".vcf"; + this.movedCards.set(name, newName); + name = newName; + return "UID:" + newUID + "\r\nX-MODIFIED-BY-SERVER:1"; + }); + } + if (this.mimicGoogle && vCard.includes("\nPHOTO")) { + let [, version] = vCard.match(/VERSION:([34]\.0)/); + if (version && version != "3.0") { + let start = vCard.indexOf("\nPHOTO") + 1; + let end = vCard.indexOf("\n", start) + 1; + while (vCard[end] == " ") { + end = vCard.indexOf("\n", end) + 1; + } + vCard = vCard.substring(0, start) + vCard.substring(end); + } + } + let etag = "" + vCard.length; + this.cards.set(name, { etag, vCard, changed: ++this.changeCount }); + this.deletedCards.delete(name); + }, + + deleteCard(request, response) { + this.deleteCardInternal(request.path); + response.setStatusLine("1.1", 204, "No Content"); + + if (this.responseDelay) { + response.processAsync(); + this.responseDelay.promise.then(() => { + delete this.responseDelay; + response.finish(); + }); + } + }, + + deleteCardInternal(name) { + if (!name.startsWith("/")) { + name = this.path + name; + } + this.cards.delete(name); + this.deletedCards.set(name, ++this.changeCount); + }, +}; diff --git a/comm/mailnews/addrbook/test/LDAPServer.jsm b/comm/mailnews/addrbook/test/LDAPServer.jsm new file mode 100644 index 0000000000..c8d8edb82b --- /dev/null +++ b/comm/mailnews/addrbook/test/LDAPServer.jsm @@ -0,0 +1,324 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 EXPORTED_SYMBOLS = ["LDAPServer"]; +const PRINT_DEBUG = false; + +const { Assert } = ChromeUtils.importESModule( + "resource://testing-common/Assert.sys.mjs" +); + +/** + * This is a partial implementation of an LDAP server as defined by RFC 4511. + * It's not intended to serve any particular dataset, rather, tests should + * cause the application to make requests and tell the server what to respond. + * + * https://docs.ldap.com/specs/rfc4511.txt + * + * @implements {nsIInputStreamCallback} + * @implements {nsIServerSocketListener} + */ +var LDAPServer = { + BindRequest: 0x60, + UnbindRequest: 0x42, + SearchRequest: 0x63, + AbandonRequest: 0x50, + + serverSocket: null, + + QueryInterface: ChromeUtils.generateQI([ + "nsIInputStreamCallback", + "nsIServerSocketListener", + ]), + + /** + * Start listening on an OS-selected port. The port number can be found at + * LDAPServer.port. + */ + open() { + this.serverSocket = Cc[ + "@mozilla.org/network/server-socket;1" + ].createInstance(Ci.nsIServerSocket); + this.serverSocket.init(-1, true, 1); + console.log(`socket open on port ${this.serverSocket.port}`); + + this.serverSocket.asyncListen(this); + }, + /** + * Stop listening for new connections and close any that are open. + */ + close() { + this.serverSocket.close(); + }, + /** + * The port this server is listening on. + */ + get port() { + return this.serverSocket.port; + }, + + /** + * Retrieves any data sent to the server since connection or the previous + * call to read(). This should be called every time the application is + * expected to send data. + * + * @returns {Promise} Resolves when data is received by the server, with the + * data as a byte array. + */ + async read(expectedOperation) { + let data; + if (this._data) { + data = this._data; + delete this._data; + } else { + data = await new Promise(resolve => { + this._inputStreamReadyResolve = resolve; + }); + } + + // Simplified parsing to get the message ID and operation code. + + let index = 4; + // The value at [1] may be more than one byte. If it is, skip more bytes. + if (data[1] & 0x80) { + index += data[1] & 0x7f; + } + + // Assumes the ID is not greater than 127. + this._lastMessageID = data[index]; + + if (expectedOperation) { + let actualOperation = data[index + 1]; + + // Unbind and abandon requests can happen at any point, when an + // nsLDAPConnection is destroyed. This is unpredictable, and irrelevant + // for testing. Ignore. + if ( + actualOperation == LDAPServer.UnbindRequest || + actualOperation == LDAPServer.AbandonRequest + ) { + if (PRINT_DEBUG) { + console.log("Ignoring unbind or abandon request"); + } + return this.read(expectedOperation); + } + + Assert.equal( + actualOperation.toString(16), + expectedOperation.toString(16), + "LDAP Operation type" + ); + } + + return data; + }, + /** + * Sends raw data to the application. Generally this shouldn't be used + * directly but it may be useful for testing. + * + * @param {byte[]} data - The data to write. + */ + write(data) { + if (PRINT_DEBUG) { + console.log( + ">>> " + data.map(b => b.toString(16).padStart(2, 0)).join(" ") + ); + } + this._outputStream.writeByteArray(data); + }, + /** + * Sends a simple BindResponse to the application. + * See section 4.2.2 of the RFC. + */ + writeBindResponse() { + let message = new Sequence(0x30, new IntegerValue(this._lastMessageID)); + let person = new Sequence( + 0x61, + new EnumeratedValue(0), + new StringValue(""), + new StringValue("") + ); + message.children.push(person); + this.write(message.getBytes()); + }, + /** + * Sends a SearchResultEntry to the application. + * See section 4.5.2 of the RFC. + * + * @param {object} entry + * @param {string} entry.dn - The LDAP DN of the person. + * @param {string} entry.attributes - A key/value or key/array-of-values + * object representing the person. + */ + writeSearchResultEntry({ dn, attributes }) { + let message = new Sequence(0x30, new IntegerValue(this._lastMessageID)); + + let person = new Sequence(0x64, new StringValue(dn)); + message.children.push(person); + + let attributeSequence = new Sequence(0x30); + person.children.push(attributeSequence); + + for (let [key, value] of Object.entries(attributes)) { + let seq = new Sequence(0x30, new StringValue(key), new Sequence(0x31)); + if (typeof value == "string") { + value = [value]; + } + for (let v of value) { + seq.children[1].children.push(new StringValue(v)); + } + attributeSequence.children.push(seq); + } + + this.write(message.getBytes()); + }, + /** + * Sends a SearchResultDone to the application. + * See RFC 4511 section 4.5.2. + */ + writeSearchResultDone() { + let message = new Sequence(0x30, new IntegerValue(this._lastMessageID)); + let person = new Sequence( + 0x65, + new EnumeratedValue(0), + new StringValue(""), + new StringValue("") + ); + message.children.push(person); + this.write(message.getBytes()); + }, + + /** + * nsIServerSocketListener.onSocketAccepted + */ + onSocketAccepted(socket, transport) { + let inputStream = transport + .openInputStream(0, 8192, 1024) + .QueryInterface(Ci.nsIAsyncInputStream); + + let outputStream = transport.openOutputStream(0, 0, 0); + this._outputStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + this._outputStream.setOutputStream(outputStream); + + if (this._socketConnectedResolve) { + this._socketConnectedResolve(); + delete this._socketConnectedResolve; + } + inputStream.asyncWait(this, 0, 0, Services.tm.mainThread); + }, + /** + * nsIServerSocketListener.onStopListening + */ + onStopListening(socket, status) { + console.log(`socket closed with status ${status.toString(16)}`); + }, + + /** + * nsIInputStreamCallback.onInputStreamReady + */ + onInputStreamReady(stream) { + let available; + try { + available = stream.available(); + } catch (ex) { + if ( + [Cr.NS_BASE_STREAM_CLOSED, Cr.NS_ERROR_NET_RESET].includes(ex.result) + ) { + return; + } + throw ex; + } + + let binaryInputStream = Cc[ + "@mozilla.org/binaryinputstream;1" + ].createInstance(Ci.nsIBinaryInputStream); + binaryInputStream.setInputStream(stream); + let data = binaryInputStream.readByteArray(available); + if (PRINT_DEBUG) { + console.log( + "<<< " + data.map(b => b.toString(16).padStart(2, 0)).join(" ") + ); + } + + if (this._inputStreamReadyResolve) { + this._inputStreamReadyResolve(data); + delete this._inputStreamReadyResolve; + } else { + this._data = data; + } + + stream.asyncWait(this, 0, 0, Services.tm.mainThread); + }, +}; + +/** + * Helper classes to convert primitives to LDAP byte sequences. + */ + +class Sequence { + constructor(number, ...children) { + this.number = number; + this.children = children; + } + getBytes() { + let bytes = []; + for (let c of this.children) { + bytes = bytes.concat(c.getBytes()); + } + return [this.number].concat(getLengthBytes(bytes.length), bytes); + } +} +class IntegerValue { + constructor(int) { + this.int = int; + this.number = 0x02; + } + getBytes() { + let temp = this.int; + let bytes = []; + + while (temp >= 128) { + bytes.unshift(temp & 255); + temp >>= 8; + } + bytes.unshift(temp); + return [this.number].concat(getLengthBytes(bytes.length), bytes); + } +} +class StringValue { + constructor(str) { + this.str = str; + } + getBytes() { + return [0x04].concat( + getLengthBytes(this.str.length), + Array.from(this.str, c => c.charCodeAt(0)) + ); + } +} +class EnumeratedValue extends IntegerValue { + constructor(int) { + super(int); + this.number = 0x0a; + } +} + +function getLengthBytes(int) { + if (int < 128) { + return [int]; + } + + let temp = int; + let bytes = []; + + while (temp >= 128) { + bytes.unshift(temp & 255); + temp >>= 8; + } + bytes.unshift(temp); + bytes.unshift(0x80 | bytes.length); + return bytes; +} diff --git a/comm/mailnews/addrbook/test/moz.build b/comm/mailnews/addrbook/test/moz.build new file mode 100644 index 0000000000..c513212222 --- /dev/null +++ b/comm/mailnews/addrbook/test/moz.build @@ -0,0 +1,14 @@ +# 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/. + +TESTING_JS_MODULES += [ + "CardDAVServer.jsm", + "LDAPServer.jsm", +] + +XPCSHELL_TESTS_MANIFESTS += [ + "unit/xpcshell.ini", + "unit/xpcshell_cardDAV.ini", +] diff --git a/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js b/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js new file mode 100644 index 0000000000..4d810d8fc0 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/bug534822prefs.js @@ -0,0 +1,7 @@ +/* globals user_pref */ +user_pref("ldap_2.servers.extension.description", "extension"); +user_pref("ldap_2.servers.extension.filename", "ldap1.mab"); +user_pref( + "ldap_2.servers.extension.uri", + "ldap://test.invalid:389/o=invalid??sub" +); diff --git a/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql b/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql new file mode 100644 index 0000000000..68a226c325 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/cardForEmail.sql @@ -0,0 +1,95 @@ +-- Address book data for use in various tests. +PRAGMA user_version = 1; + +CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER); +CREATE TABLE properties (card TEXT, name TEXT, value TEXT); +CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT); +CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card)); + +INSERT INTO cards (uid, localId) VALUES + ('85f4ad83-38fd-4d17-9364-038d11da77e6', 1), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 2), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 3), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 4); + +INSERT INTO properties (card, name, value) VALUES + ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'LastName', 'Email'), + ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'DisplayName', 'Empty Email'), + ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'FirstName', 'Empty'), + ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'AllowRemoteContent', '0'), + ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'PopularityIndex', '0'), + ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'PreferMailFormat', '0'), + ('85f4ad83-38fd-4d17-9364-038d11da77e6', 'LastModifiedDate', '0'), + + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LastName', 'LastName1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom4', 'Custom41'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LastModifiedDate', '1237281794'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WebPage2', 'http://WebPage11'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'NickName', 'NickName1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'DisplayName', 'DisplayName1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkZipCode', 'WorkZipCode1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', '_AimScreenName', 'ScreenName1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkAddress', 'WorkAddress1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeCountry', 'HomeCountry1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkPhone', 'WorkPhone1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PrimaryEmail', 'PrimaryEmail1@test.invalid'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeAddress', 'HomeAddress11'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'LowercasePrimaryEmail', 'primaryemail1@test.invalid'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkCity', 'WorkCity1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'SecondEmail', 'SecondEmail1Ð@test.invalid'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeZipCode', 'HomeZipCode1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom3', 'Custom31'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'FaxNumber', 'FaxNumber1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom1', 'Custom11'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomePhone', 'HomePhone1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'FirstName', 'FirstName1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeCity', 'HomeCity1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PagerNumber', 'PagerNumber1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'CellularNumber', 'CellularNumber1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkAddress2', 'WorkAddress21'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkState', 'WorkState1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeAddress2', 'HomeAddress21'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WebPage1', 'http://WebPage21'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Notes', 'Notes1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Custom2', 'Custom21'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Department', 'Department1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'WorkCountry', 'WorkCountry1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'HomeState', 'HomeState1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'JobTitle', 'JobTitle1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'Company', 'Organization1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PopularityIndex', '0'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'AllowRemoteContent', '1'), + ('fdcb9131-38ec-4daf-a4a7-2ef115f562a7', 'PreferMailFormat', '0'), + + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LastModifiedDate', '1245128765'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'NickName', 'johnd'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'DisplayName', 'John Doe'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LastName', 'Doe'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PrimaryEmail', 'john.doe@mailinator.invalid'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'FirstName', 'John'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'LowercasePrimaryEmail', 'john.doe@mailinator.invalid'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'AllowRemoteContent', '0'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PopularityIndex', '0'), + ('61c3b8fe-69d0-4a11-a970-ff381ae82d95', 'PreferMailFormat', '0'), + + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'NickName', 'janed'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'DisplayName', 'Jane Doe'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LastName', 'Doe'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PrimaryEmail', 'jane.doe@mailinator.invalid'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'FirstName', 'Jane'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LowercasePrimaryEmail', 'jane.doe@mailinator.invalid'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'LastModifiedDate', '0'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'AllowRemoteContent', '0'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PopularityIndex', '0'), + ('b73bffd5-850d-4a59-8c72-12272d2616a6', 'PreferMailFormat', '0'), + + ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'DisplayName', 'A vCard!'), + ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'PrimaryEmail', 'first@something.invalid'), + ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', 'SecondEmail', 'second@something.invalid'), + ('f68fbac4-158b-4bdc-95c6-592a5f93cfa1', '_vCard', 'BEGIN:VCARD +FN:A vCard! +EMAIL:first@something.invalid +EMAIL:second@something.invalid +EMAIL:third@something.invalid +EMAIL:fourth@something.invalid +END:VCARD'); diff --git a/comm/mailnews/addrbook/test/unit/data/collect.sql b/comm/mailnews/addrbook/test/unit/data/collect.sql new file mode 100644 index 0000000000..dba17a7392 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/collect.sql @@ -0,0 +1,21 @@ +-- Collection address book for use in test_collection_2.js. +PRAGMA user_version = 1; + +CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER); +CREATE TABLE properties (card TEXT, name TEXT, value TEXT); +CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT); +CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card)); + +INSERT INTO cards (uid, localId) VALUES + ('28fd662c-1662-4b02-8950-12dd131a1116', 1); + +INSERT INTO properties (card, name, value) VALUES + ('28fd662c-1662-4b02-8950-12dd131a1116', 'DisplayName', 'Other Book'), + ('28fd662c-1662-4b02-8950-12dd131a1116', 'LastName', 'Book'), + ('28fd662c-1662-4b02-8950-12dd131a1116', 'PrimaryEmail', 'other@book.invalid'), + ('28fd662c-1662-4b02-8950-12dd131a1116', 'FirstName', 'Other'), + ('28fd662c-1662-4b02-8950-12dd131a1116', 'LowercasePrimaryEmail', 'other@book.invalid'), + ('28fd662c-1662-4b02-8950-12dd131a1116', 'LastModifiedDate', '0'), + ('28fd662c-1662-4b02-8950-12dd131a1116', 'AllowRemoteContent', '0'), + ('28fd662c-1662-4b02-8950-12dd131a1116', 'PopularityIndex', '0'), + ('28fd662c-1662-4b02-8950-12dd131a1116', 'PreferMailFormat', '0'); diff --git a/comm/mailnews/addrbook/test/unit/data/export.csv b/comm/mailnews/addrbook/test/unit/data/export.csv new file mode 100644 index 0000000000..92da47c7be --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/export.csv @@ -0,0 +1,4 @@ +First Name,Last Name,Display Name,Nickname,Primary Email,Secondary Email,Screen Name,Work Phone,Home Phone,Fax Number,Pager Number,Mobile Number,Home Address,Home Address 2,Home City,Home State,Home ZipCode,Home Country,Work Address,Work Address 2,Work City,Work State,Work ZipCode,Work Country,Job Title,Department,Organization,Web Page 1,Web Page 2,Birth Year,Birth Month,Birth Day,Custom 1,Custom 2,Custom 3,Custom 4,Notes
+contact,one,contact number one,,contact1@invalid,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
+contact,two,contact number two,,contact2@invalid,,,,,,,,,,,,,,,,,,,,"""worker""",,,,,,,,"custom, 1","custom 2","custom
3","custom +4",here's some unicode text…
diff --git a/comm/mailnews/addrbook/test/unit/data/export.ldif b/comm/mailnews/addrbook/test/unit/data/export.ldif new file mode 100644 index 0000000000..669b63f6a4 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/export.ldif @@ -0,0 +1,36 @@ +dn: cn=new list
+objectclass: top
+objectclass: groupOfNames
+cn: new list
+member: cn=contact number one,mail=contact1@invalid
+
+dn: cn=contact number one,mail=contact1@invalid
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+objectclass: mozillaAbPersonAlpha
+givenName: contact
+sn: one
+cn: contact number one
+mail: contact1@invalid
+modifytimestamp: 12345
+
+dn: cn=contact number two,mail=contact2@invalid
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+objectclass: mozillaAbPersonAlpha
+givenName: contact
+sn: two
+cn: contact number two
+mail: contact2@invalid
+modifytimestamp: 12345
+title: "worker"
+mozillaCustom1: custom, 1
+mozillaCustom2: custom 2
+mozillaCustom3:: Y3VzdG9tDTM=
+mozillaCustom4:: Y3VzdG9tCjQ=
+description:: aGVyZSdzIHNvbWUgdW5pY29kZSB0ZXh04oCm
+
diff --git a/comm/mailnews/addrbook/test/unit/data/export.txt b/comm/mailnews/addrbook/test/unit/data/export.txt new file mode 100644 index 0000000000..82f9c468ae --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/export.txt @@ -0,0 +1,4 @@ +First Name Last Name Display Name Nickname Primary Email Secondary Email Screen Name Work Phone Home Phone Fax Number Pager Number Mobile Number Home Address Home Address 2 Home City Home State Home ZipCode Home Country Work Address Work Address 2 Work City Work State Work ZipCode Work Country Job Title Department Organization Web Page 1 Web Page 2 Birth Year Birth Month Birth Day Custom 1 Custom 2 Custom 3 Custom 4 Notes
+contact one contact number one contact1@invalid
+contact two contact number two contact2@invalid """worker""" "custom, 1" "custom 2" "custom
3" "custom +4" here's some unicode text…
diff --git a/comm/mailnews/addrbook/test/unit/data/export.vcf b/comm/mailnews/addrbook/test/unit/data/export.vcf new file mode 100644 index 0000000000..91cc8b7016 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/export.vcf @@ -0,0 +1,20 @@ +BEGIN:VCARD
+VERSION:4.0
+EMAIL;PREF=1:contact1@invalid
+FN:contact number one
+N:one;contact;;;
+UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+END:VCARD
+BEGIN:VCARD
+VERSION:4.0
+EMAIL;PREF=1:contact2@invalid
+FN:contact number two
+NOTE:here's some unicode text…
+TITLE:"worker"
+N:two;contact;;;
+X-CUSTOM1;VALUE=TEXT:custom\, 1
+X-CUSTOM2;VALUE=TEXT:custom 2
+X-CUSTOM3;VALUE=TEXT:custom
3
+X-CUSTOM4;VALUE=TEXT:custom\n4
+UID:yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy
+END:VCARD
diff --git a/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json b/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json new file mode 100644 index 0000000000..c239820d51 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/ldap_contacts.json @@ -0,0 +1,104 @@ +{ + "eurus": { + "dn": "uid=eurus,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Eurus Holmes", + "givenName": "Eurus", + "mail": "eurus@bakerstreet.invalid", + "sn": "Holmes" + } + }, + "irene": { + "dn": "uid=irene,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Irene Adler", + "givenName": "irene", + "mail": "irene@bakerstreet.invalid", + "sn": "Adler" + } + }, + "john": { + "dn": "uid=john,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "John Watson", + "givenName": "John", + "mail": "john@bakerstreet.invalid", + "sn": "Watson" + } + }, + "lestrade": { + "dn": "uid=lestrade,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Greg Lestrade", + "givenName": "Greg", + "mail": "lestrade@bakerstreet.invalid", + "o": "New Scotland Yard", + "sn": "Lestrade" + } + }, + "mary": { + "dn": "uid=mary,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Mary Watson", + "givenName": "Mary", + "mail": "mary@bakerstreet.invalid", + "sn": "Watson" + } + }, + "molly": { + "dn": "uid=molly,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Molly Hooper", + "givenName": "Molly", + "mail": "molly@bakerstreet.invalid", + "o": "St. Bartholomew's Hospital", + "sn": "Hooper" + } + }, + "moriarty": { + "dn": "uid=moriarty,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Jim Moriarty", + "givenName": "Jim", + "mail": "moriarty@bakerstreet.invalid", + "sn": "Moriarty" + } + }, + "mrs_hudson": { + "dn": "uid=mrs_hudson,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Mrs Hudson", + "givenName": "Martha", + "mail": "mrs_hudson@bakerstreet.invalid", + "sn": "Hudson" + } + }, + "mycroft": { + "dn": "uid=mycroft,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Mycroft Holmes", + "givenName": "Mycroft", + "mail": "mycroft@bakerstreet.invalid", + "sn": "Holmes" + } + }, + "sherlock": { + "dn": "uid=sherlock,dc=bakerstreet,dc=invalid", + "attributes": { + "objectClass": "person", + "cn": "Sherlock Holmes", + "givenName": "Sherlock", + "mail": "sherlock@bakerstreet.invalid", + "sn": "Holmes" + } + } +} diff --git a/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat b/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat new file mode 100644 index 0000000000..7621d1e76d --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/msgFilterRules.dat @@ -0,0 +1,17 @@ +version="9" +logging="no" +name="From is in book 7" +enabled="yes" +type="17" +action="Mark flagged" +condition="AND (from,is in ab,moz-abmdbdirectory://abook-7.mab) AND (subject,contains,nothing)" +name="From is not in book 8" +enabled="yes" +type="17" +action="Mark flagged" +condition="AND (from,isn't in ab,moz-abmdbdirectory://abook-8.na2.mab)" +name="Not related" +enabled="yes" +type="17" +action="Mark read" +condition="AND (subject,contains,unrelated)" diff --git a/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf new file mode 100644 index 0000000000..f877f281ec --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/v3-binary-jpeg.vcf @@ -0,0 +1,101 @@ +BEGIN:VCARD +VERSION:3.0 +FN:Binary JPEG +NOTE:This v3.0 card has a JPEG photo as binary data and binary valuetype. +PHOTO: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 +END:VCARD diff --git a/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf new file mode 100644 index 0000000000..720a5a009c --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/v3-binary-png.vcf @@ -0,0 +1,204 @@ +BEGIN:VCARD +VERSION:3.0 +FN:Binary PNG +NOTE:This v3.0 card has a PNG photo as binary data and binary valuetype. +PHOTO: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 +END:VCARD diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf new file mode 100644 index 0000000000..c856944ae6 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-jpeg.vcf @@ -0,0 +1,102 @@ +BEGIN:VCARD +VERSION:3.0 +FN:URI JPEG, version 3.0 +NOTE:This v3.0 card has a JPEG photo as a URI and binary valuetype. +PHOTO:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYEBQYFBAYGBQ + YHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/ + 2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKC + goKCgoKCgoKCgoKCj/wgARCACAAIADAREAAhEBAxEB/8QAGwAAAQUBAQAAAAAAAAAAAAAAAAIE + BQYHAwH/xAAZAQEAAwEBAAAAAAAAAAAAAAAAAgMEAQX/2gAMAwEAAhADEAAAAdUAAAr+rHE2wk + 67TnXtdstTYAAAAAR841jd50PpytpSbS627Ll2TqErRk0XPBqdQkAAFT10Uj0MMrzO2l1v2TKc + kOgAOI90fyt9gzXgFU10Z56mCdrobOJ71pO1t2ZwrsfXFCuc6caV5HqTdFrKcch9rzJjNoTdhg + vO9SR9XzWMp8o3c42qJCeDoivkePZda9GueL6dF35oLRTo3kb6P6vlRkpWbNqn817aXIezsPZL + jf5/l3m80265XO3jzvQpu7Pacl9nw6IPXjm8+x5CQAAMrKM09jw+XL/eS952Qo2V/XRd8Oi7YN + kYSIAAANHMw9nxVcCXHlnJoVOu/wDn32TLqQMR+BxGR4OXM39bxefZcO2N+2JSk4d1fydwA1GQ + s7D4BlOqi7/Pg7+oS8F8lZM992w3uoyAAAABLlM3ZKhrznXnExTdfvP2ZRthZMl9uqjJnoAAHH + sc19PFBWyddzhoHnb7LmupWqvH79Fip4/hzUs8VD09ADM/Qrr2nKtGaplqPmbvQKPojRLLNnyR + 78LGIkUNbY5J6dKepGENX8zU/h0ACPlx3HvUAAibI5h6NLCzkvTPTvPtfw6AAAAABG2RgroJde + R7Zc8wAP/EACYQAAEEAgEEAgMBAQAAAAAAAAQAAQIDBRETEBIgIQYUFTAxIiP/2gAIAQEAAQUC + 8SsjGtQPkvy9LJswNurIi2Jvf6SzKRY3nzIhJ1J1J079KSLaELm5shyKiIeOUyrUqqMr7ZOpOp + J5eNNk6Z4zIxKbrmj+CLNtRbshKaZTffTfXS0m3F8WaxdKMvYYec5W2Y8CwtWelXPkItkn6xnp + My0tJ39iXSGvqm1lfyO/dmOG+0SO/ExdNnJHudCYfa/HCasxAk1bgIqzBlRTBkjwU/Un9vpYG/ + cMjZyn/HI+ia+9oUOVIcaodvEyHILan/0+k/pY63iye+6Xx2ftT/4W+Vt9Vaf+6Uv8xvUJ6vFh + syjHOMWpM0o0O8JdJ2QrX2ZTXBdYoD1VN/XZ92XS27+3REeDLdb6+SLEzdcZFirGqh1MnxjEDz + qFZ9da499nyOjtvFs5h/0O21n7dzfriKuU88ZihWOJAkL8ig6oPHubyunx15CF0Ghuc39dMGPx + jrP4z7EZs8XxYMjKMfkLxjNkQUSa9/3w+Q38pfI0Y1M7vjhvtks2m6ZrCsSsTdLG5UgWgmNVca + q5RaTOLFlsitRJr3kjIiDSd5SQ9dpdoQsBKPAsMcuNUOOrwyBVI1WrDL5aZwcdcW4YlQlf6iXK + kmxEO64QgiImHHp8f//EACgRAAEDAwMEAgIDAAAAAAAAAAEAAhEDEiEEEDETICIwQVEyUhRhcf + /aAAgBAwEBPwHtqaocMQrxyZX8piGpYUKrHcH1Fwbyq+ou8W9we5vBTNT+ya4OyO6pVtwFUqbj + ua4tyFSrB/8AvZWqW4CLlysoN2AVh7JQdBkKlUvGznWiU90lUtODlyAzCbQYGElBAIBAJ9G7hH + ClXIZgpj7DKBnK1DvhUGXOlcJwIdKuqVPGcKnpwPyXTb9LptViAWupx5hTKp+QKGBG2mdLIVYy + 4rS/incJtLqmTwmsDeO7UNupOCoCcpjbRGwErSp60jsEeoNtwNmiSqI+VT8cKPKCmUDTfc3j1k + Q1UxCGNniH+qs61hKFEgXOXO7clV2/KaZE+rUu+EM70RLk5twhUP1Kt9DjaJXSe83OVSKbYCAn + aiy1uzhBuCa64SESo+u9xXSLzLlVIaLGqjSnJ7OMhTd3uMbH+kyiJk+wuhZcdgJQEesz8Lp/aI + JwgwDt/8QAJxEAAgECBQMFAQEAAAAAAAAAAQIAAxEEEiAhMRATMBQiQUJRMmH/2gAIAQIBAT8B + 008MTu0NC/AtPRvPR1I2HqL8eJKbPxKWHCbnU1NX5EqYIfSOjIbNqoYbPu3EsFFh4WUOLNK+HN + PccaMNQz+48dOeo6X0kA7GV6Pab/OlNM7ZYqhRYStiQmwnxKuKZagVf20A0BraaqCouUwixsZg + k5aYip21nMpupW07VND3Lb/sq4z4Sd+p+wYmoPmDFn5E9ShlKsrbDReYtfdmmHWyCY07gdM4oj + /Y9Rn/AK1UWyuD1vLyuMyRRYTGjg+K8v0EYXFozewkR8R3Eytz4/iDrTOal4qYuwiOGbKNDGwv + MG+2WOuViPFhF2LaMQ2VJTfI15WpB/cIaJEtbWq5jaU3X+F0Yl8zW/OlCrb2NCLR2sd+I1MEXX + Xhkst+tarkGinW+rSouZYCRxqpU+4eruEFzHcubnSrFeNVOmah2gC0lt0qVlSO5c3PjXJ9p6k8 + ILRair7m3MfEM3Gn/8QANRAAAQMBBAgEBQMFAAAAAAAAAQACAxESIUFRBBAgIjEyYXETIzBSQm + KBscEzQ5FygqHR4f/aAAgBAQAGPwLZIi3jmrcjnOdg3gAuWQnsrxIPorpgD81yu9Gsrr/aOJXL + YYcMdrypHN7FU0lloe5vFWoXhw2jFo9DJickXyEuzJx9EPicWu6Kw/dm++x4MJ808T7dQA1VPo + WmmhF4K3v1W8w/Op8rsOCc95q43lWuSH3Zoqxavs2rsO6psX8NhwTZGYYZpr2crhVRwDgN4psf + w8XdkYX3WBd1CebPG9COIEk4BWtLP9gX6DVyFvZy8qcj+oLdMb+xXnx2W51rqbrfAfh3h2U7vm + op3Y3BBw5mqtqzGMRivKYB1x2pG9FTYhydun6ok4qZnZ2q3+2/m6HPb33fTXVAJsmRqomP4WwD + /KEsD9zFpy1EHgV4L8OU5jXvuAXkRud1NwXnS0HtYqhv1OqioFXU6uEtf87F1zxe0qjYXF+OS3 + 3iMZNVbNo5uv1yHpReK/dqaAK7W1oxNEyYcHCh7qOT3Nr6N6jiGF52I8m7yfHjxHdWRwBvCpMK + KrXjbLqE9BijpGkNoXuuCqcNZldzSfbUZoRv/EM1QqY6NIW6TGa0rzBCHTXOjHAkjgr2tlHS4q + jqsdk65XbAiBuj+6o1WnKz+2295/CoOGsy6NdJi3NM8cFgO4+qpPEyQdQmxs5WigvVHAEdVWJz + oz0V4bKOlxVHVY7J1ytVBe7kCJdeSr0IoR/xCNn1OZ2aaRGHdcU1gJIaKX7PnUcTwZmiWt/00I + 0NRmqgWY/eVZiHc4n07OjtY353n8Iy6XLJM7oF4MEbdF0brxcqyea75uH8bP8A/8QAKBABAAIB + AwEJAAMBAAAAAAAAAQARITFBUXEQIGGBkaGxwdEw4fDx/9oACAEBAAE/Ie5pKwgb9PLmGBVcL5 + +tekRZ5AAPvFU8RT+xeuKfpEAUI7n8NWYtMnkSqHizI8fzvATXl+D0iITpHo0+JzKDk6neZSPG + qftmQsWi7doqi2MvdGiN4BUgabdP53Nsjg2fsRQNhS7SaJWmLASpUIJrlY41GVqhj5PB2ZeK4c + uxLgxsY/nU6jPRG1upiJlrPWqu2HT1m0iixZdqFhZpCSKkcYmvdq4NyLlYBLsYfOcH36y2LDmI + xgWZ7ssbBttpxFjIr0j1aeL8v5AqYPOBYby33GW8UW+5NOjo/MeX2Yo+DKDWOTJeYRcPPqWvv8 + zqDHQx9Q/GvlnipitzcibIq3Oj9lEZddS6ve88p1MzSDrL2sIoReamp/8AhPeovUStgDdUD4fr + s0/Wod8GIN8MsFtYQ4Ho9YulJnyYYK0X6GDYczU22vfsNOwpIra3Jv8AGO0y+os0M2Poke8rhs + 8RlWLvMQ0CcGEay1ZUoej0rbuVt7PgGG1bg6B6z7GX1inuI7eRcHVxNjjZrvn2iW5Y7Ki6iIec + q97af18QRthdd/4QpQabL2gItPraShrK7NEXl+WnvUZNDy8DSZXjBcUyptuXHuf1CY18uPXSCJ + Y2d4W6GgWrgmCBSuf+E4rhFL37KocHw/3TsyOgx6jk8Yr1CFWdohfHmyPF7gC9lNyUeoXBVvhM + CC1Y7ncPBXT1a/UKJvxZqK4iEfgP2YABQwB212NfQOjxjZolKofxphLZDWOjCZSoKaOrPCzQuK + dUuPSeqTAro0woOAcjz0ihKrV3YVfCUD714HKwn9M73J3eFXNA84ZZALto5e7fh4tMPj9IY6zY + gCqHFKuHuVjjy5ms66vufxgPokdB9y0MNo2flmS25F5zURCT3EAAoKDuf//aAAwDAQACAAMAAA + AQkkjRaEkkki6RagMkkMw122ZkkVsEe43UnzMsoIVRiqelxLXtd8QAAdbM9EAAAOYYbEAEkOpo + QAAkAtCZDAAAA5w91SgAAeBgklMAAE/8kYCAArvskhUklhwkkkkkLUEk/8QAJREBAAICAQMDBQ + EAAAAAAAAAAQARITFBECBRMGGRcYGhsfDB/9oACAEDAQE/EO0TVb54gsbntgPjc4qf77xTNnp4 + GWo98Rz7wIECBBrXTBTA+IBbfcfkRKc5YEsJlmB2q21CacdinydJVWxtqZMwJbEMnS5cYYkzBv + 5N9CRRGXbBNs2kGnILf95gl3XzN7RKpjLAoEcTAANM1ffMk0RoRkzm5ULnwvEymTPZTwEAagEC + vPh/yXwIIDRFzNOJaIMmC2EfR/tA6Nd304v4zGS9QKkuM6I6H3YrVlni+illMA8RLmCmxSlRxM + z5JaXLY+itZi3mZR3KVu2EFEuZr39L6AfuafeCIbS5cFBNUUfokHcyhgcCXLlW+ISKZDuIoiV3 + iilxgueV2NZOJczDt6KG8/MCBQ51EJfezURtevafOCO+PsBf4ZQYg13U5dxNeUNsMCiu2ue4jm + Xalx4AUenwwC7dzCCibjPb/8QAJxEBAAICAAUEAgMBAAAAAAAAAQARITEQIEFRYTBxkbGB8KHR + 4cH/2gAIAQIBAT8Q5RNJ26zSIfK/OoJ1P38RIwj+f8mWfjn6iV6LtGIbGBAgQOGumKyq8MpRTz + J0X2lQFQIEC4HLXKyO7Pp78lzov5gBFtcsIZgqLUByXAaLGPg21/XBCHWE9BOsDL2IEwiPLLlP + Yz7F8DUWLOoly5c8xB+jHbYTAvtMqbirt2w+HNVB1A6qF/MTT+Yu3ebS34n9ahuWRG7MuDLjFI + HWUfx95jMhuFQF/T3i9q+b3Egy6jDF74zCAJn9FMTIuMXcyzLXuIh2pf4gsPuPRC5dYji0NFsC + scKOu1f89KnTayog742XZLEumZ4pfRFNSlHXHJY+cRyMTG3NVmKVPOgCbLdEMvHCdfbgIsw68R + FTDINr0KOa9fqXUO8LHt1N8aA09Ht/k1Ge0yCi228r09DcACjjgY8ttkW2+Wmad5+mLC0tmNcv + aX59MjLXwf3GioInZ9Z8zH4H715f/8QAJxABAAEDAwQDAQADAQAAAAAAAREAITFBUWFxgZGhEC + CxwTDh8PH/2gAIAQEAAT8Q+igVQC6tHWFGSLwL/wDM1JWcuSbMUrXRDeXBcIBK41SZhUQNuh6T + RVmtA+4B80JQpEkf8ML5T4i/pg5pfnQvaKenR5ay/Dlqdpu3oYgDMybzg9ysagohcqu7UgIVgW + XZLnf7KsZWS9jT0Gu1NVM/NAl/6PhyXq/vQpFFilnPzFY+3tk2TCWw2aOOfRsZl579xN4+VeLL + dddnRei+pW4FdahygXd3VqQmO9G/DkP7U8cOlEYpDCw80UPmDVlSMAuI0+QQBaOnA6mj2mjuh1 + Xt3GO0tNRf1Jf5QwW0zHMh/uOuKMs50bOKJBUWYTBILxcKWa2qEE5an+ehZFeqMoFEia/Hw1o+ + 9kFNmyxNA+XhPDDpUnmDwk32eKbBzd5g7BQrsJywEscqh3mjuqREA4BxhpLoLzLFXNOQksncgU + DOly/7f55UUMIiVT5WaSjzffCT1XAgN5oflSToSRege6iVoI9LNrm1JRsFCpgaKvSgtmU1x009 + 293IBwQao5huUzZvgUNYKM7AJ8yeKWciqZNblk90unOYFtp2I1U7BmH5Qj3ku9MfYmCVXiHspx + lllxTQXAfHoBR5VmAkAc3A9naoo5sW6y051kdwn9PP4OVACxpdDhouW+yLMYfzDHeneSrmIrjo + E1SBQjkF38rRuscQJ+UW+b2yID9qZxVqDIANjCTGCXX4GO4jUad2PwWdcPmIu0Lj0MtMgLgD2O + fVI5l8hDp/tNNlxlDFrfHal5Elan3lk8tqAEnX3avkpLTRMCB0sD6T6O3+fEeHDWXZ5Aef/UUZ + PMnPmw9Ky0brSc3t6oAILHwaTCfLYe2hQrB18iUaW1X9SXNCJOsUlNB0U6NVAftLkdIYwr1hRC + J6ERZ2ZP8ACk1LQmW5zU6qQzewdYF7lIRUSgctNEppKUqaa3nSJsJfXOmjwtRkROUayMW3yND4 + /EMu4eqBDsEvQvtRMSEiMj9l5JumiAarR1M8LZcaABe+Kcw1gMDTgV1Ab0lPCkLPQ/0q9Pg2E8 + NkWLURjU6QiXHsNyg7QpDTYGAmbMgxmtprml2I3VMMzBe8QS06N0bPaj+6zXnHujJBSJI/QSTN + TFt8AHUaCNRmwLvT5lYn7oLmcFpoDuW4JdKLUUAgAwHyOnZtE6ugWphzZmX6E5Zy7MRA06DRZR + kD7A7NBY06QsEisFrtLUDQD3SdMvCreVZpwuaveRt2KPrvmvOPdDGZmSReiZexrTbRPSoyrzNG + GC6gy1NRZDqdAH+r1NeypCsr+GgB9bZOSDsBD2xUmNbuEG4xr9XogwCN4cHL+2otZKzFyZVYCb + v7U5SCRBvDcpnc38Ayult0o7Ggb9TVfhg/xom2zLhuRl6jpVogiHSxJ2iOlQEARHQJz0Xq7X9h + QAvGHlaJiBABAH0//9k= +END:VCARD diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf new file mode 100644 index 0000000000..bea653f3d5 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-binary-png.vcf @@ -0,0 +1,204 @@ +BEGIN:VCARD +VERSION:3.0 +FN:URI PNG, version 3.0 +NOTE:This v3.0 card has a PNG photo as a URI and binary valuetype. +PHOTO:data:image/png;base64,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 +END:VCARD diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf new file mode 100644 index 0000000000..be583a9bfd --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-jpeg.vcf @@ -0,0 +1,102 @@ +BEGIN:VCARD +VERSION:3.0 +FN:URI JPEG, version 3.0 with correct value type +NOTE:This v3.0 card has a JPEG photo as a URI and URI valuetype. +PHOTO;VALUE=URI:data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAYE + 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 + QJz0Xq7X9hQAvGHlaJiBABAH0//9k= +END:VCARD diff --git a/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf new file mode 100644 index 0000000000..a56eb3ed97 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/v3-uri-uri-png.vcf @@ -0,0 +1,204 @@ +BEGIN:VCARD +VERSION:3.0 +FN:URI PNG, version 3.0 with correct value type +NOTE:This v3.0 card has a PNG photo as a URI and URI valuetype. +PHOTO;VALUE=URI:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAA + 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 +END:VCARD diff --git a/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf b/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf new file mode 100644 index 0000000000..f93a68969b --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/v4-uri-jpeg.vcf @@ -0,0 +1,102 @@ +BEGIN:VCARD +VERSION:4.0 +FN:URI JPEG, version 4.0 +NOTE:This v4.0 card has a JPEG photo as a URI and URI valuetype. +PHOTO:data:image/jpeg;base64,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 + QAvGHlaJiBABAH0//9k= +END:VCARD diff --git a/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf b/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf new file mode 100644 index 0000000000..058d5fbbf2 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/data/v4-uri-png.vcf @@ -0,0 +1,204 @@ +BEGIN:VCARD +VERSION:4.0 +FN:URI PNG, version 4.0 +NOTE:This v4.0 card has a PNG photo as a URI and URI valuetype. +PHOTO:data:image/png;base64,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 +END:VCARD diff --git a/comm/mailnews/addrbook/test/unit/head.js b/comm/mailnews/addrbook/test/unit/head.js new file mode 100644 index 0000000000..7a5155ea26 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/head.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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" +); +var { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// Ensure the profile directory is set up +do_get_profile(); + +// Import the required setup scripts. +/* import-globals-from ../../../test/resources/abSetup.js */ +load("../../../resources/abSetup.js"); + +registerCleanupFunction(function () { + load("../../../resources/mailShutdown.js"); +}); + +function promiseDirectoryRemoved(uri) { + let removePromise = TestUtils.topicObserved("addrbook-directory-deleted"); + MailServices.ab.deleteAddressBook(uri); + return removePromise; +} + +function acObserver() {} +acObserver.prototype = { + _search: null, + _result: null, + _resolve: null, + + onSearchResult(aSearch, aResult) { + this._search = aSearch; + this._result = aResult; + this._resolve(); + }, + + waitForResult() { + return new Promise(resolve => { + this._resolve = resolve; + }); + }, +}; + +function formatVCard(strings, ...values) { + let arr = []; + for (let str of strings) { + arr.push(str); + arr.push(values.shift()); + } + let lines = arr.join("").split("\n"); + let indent = lines[1].length - lines[1].trimLeft().length; + let outLines = []; + for (let line of lines) { + if (line.length > 0) { + outLines.push(line.substring(indent) + "\r\n"); + } + } + return outLines.join(""); +} diff --git a/comm/mailnews/addrbook/test/unit/head_cardDAV.js b/comm/mailnews/addrbook/test/unit/head_cardDAV.js new file mode 100644 index 0000000000..5c6ac62f61 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/head_cardDAV.js @@ -0,0 +1,149 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { CardDAVDirectory } = ChromeUtils.import( + "resource:///modules/CardDAVDirectory.jsm" +); +const { CardDAVServer } = ChromeUtils.import( + "resource://testing-common/CardDAVServer.jsm" +); +const { TestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TestUtils.sys.mjs" +); +Cu.importGlobalProperties(["fetch"]); + +do_get_profile(); + +registerCleanupFunction(function () { + load("../../../resources/mailShutdown.js"); +}); + +function initDirectory() { + // Set up a new directory and get the cards from the server. Do this by + // creating an instance of CardDAVDirectory rather than through the address + // book manager, so that we can access the internals of the directory. + + Services.prefs.setIntPref("ldap_2.servers.carddav.carddav.syncinterval", 0); + Services.prefs.setStringPref( + "ldap_2.servers.carddav.carddav.url", + CardDAVServer.url + ); + Services.prefs.setStringPref( + "ldap_2.servers.carddav.carddav.username", + "bob" + ); + Services.prefs.setStringPref( + "ldap_2.servers.carddav.description", + "CardDAV Test" + ); + Services.prefs.setIntPref( + "ldap_2.servers.carddav.dirType", + Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE + ); + Services.prefs.setStringPref( + "ldap_2.servers.carddav.filename", + "carddav.sqlite" + ); + + if (!Services.logins.findLogins(CardDAVServer.origin, null, "test").length) { + // Save a username and password to the login manager. + let loginInfo = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + loginInfo.init(CardDAVServer.origin, null, "test", "bob", "bob", "", ""); + Services.logins.addLogin(loginInfo); + } + + let directory = new CardDAVDirectory(); + directory.init("jscarddav://carddav.sqlite"); + return directory; +} + +async function clearDirectory(directory) { + await directory.cleanUp(); + + let database = do_get_profile(); + database.append("carddav.sqlite"); + database.remove(false); +} + +async function checkCardsOnServer(expectedCards) { + // Send a request to the server. When the server responds, we know it has + // completed all earlier requests. + await fetch(`${CardDAVServer.origin}/ping`); + + info("Checking cards on server are correct."); + let actualCards = [...CardDAVServer.cards]; + Assert.equal(actualCards.length, Object.keys(expectedCards).length); + + for (let [href, { etag, vCard }] of actualCards) { + let baseName = href + .substring(CardDAVServer.path.length) + .replace(/\.vcf$/, ""); + info(baseName); + Assert.equal(etag, expectedCards[baseName].etag); + Assert.equal(href, expectedCards[baseName].href); + // Decode the vCard which is stored as UTF-8 on the server. + vCard = new TextDecoder().decode( + Uint8Array.from(vCard, c => c.charCodeAt(0)) + ); + vCardEqual(vCard, expectedCards[baseName].vCard); + } +} + +let observer = { + notifications: { + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": [], + }, + pendingPromise: null, + init() { + if (this.isInited) { + return; + } + this.isInited = true; + + for (let key of Object.keys(this.notifications)) { + Services.obs.addObserver(observer, key); + } + }, + checkAndClearNotifications(expected) { + Assert.deepEqual(this.notifications, expected); + for (let array of Object.values(this.notifications)) { + array.length = 0; + } + }, + observe(subject, topic) { + let uid = subject.QueryInterface(Ci.nsIAbCard).UID; + info(`${topic}: ${uid}`); + if (this.pendingPromise && this.pendingPromise.topic == topic) { + let promise = this.pendingPromise; + this.pendingPromise = null; + promise.resolve(uid); + return; + } + this.notifications[topic].push(uid); + }, + waitFor(topic) { + return new Promise(resolve => { + this.pendingPromise = { resolve, topic }; + }); + }, +}; + +add_task(async () => { + CardDAVServer.open("bob", "bob"); + registerCleanupFunction(async () => { + await CardDAVServer.close(); + }); +}); + +// Checks two vCard strings have the same lines, in any order. +// Not very smart but smart enough. +function vCardEqual(lhs, rhs, message) { + let lhsLines = lhs.split("\r\n").sort(); + let rhsLines = rhs.split("\r\n").sort(); + Assert.deepEqual(lhsLines, rhsLines, message); +} diff --git a/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js b/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js new file mode 100644 index 0000000000..ebcb746f21 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_LDAPMessage.js @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests for LDAPMessage.jsm. + */ + +var { CommonUtils } = ChromeUtils.importESModule( + "resource://services-common/utils.sys.mjs" +); +var { LDAPResponse, SearchRequest } = ChromeUtils.import( + "resource:///modules/LDAPMessage.jsm" +); + +/** + * Test filter string is converted to asn1 blocks correctly. + */ +add_task(function test_SearchRequest_filter() { + let req = new SearchRequest( + "ou=people,dc=planetexpress,dc=com", + Ci.nsILDAPURL.SCOPE_SUBTREE, + "(memberof=cn=ship_crew,ou=people,dc=planetexpress,dc=com)", + "", + 0, + 0 + ); + let filterBlock = req.protocolOp.valueBlock.value[6]; + let [filterKeyBlock, filterValueBlock] = filterBlock.valueBlock.value; + let filterKey = new TextDecoder().decode(filterKeyBlock.valueBlock.valueHex); + let filterValue = new TextDecoder().decode( + filterValueBlock.valueBlock.valueHex + ); + Assert.equal(filterKey, "memberof", "Filter key should be correct"); + Assert.equal( + filterValue, + "cn=ship_crew,ou=people,dc=planetexpress,dc=com", + "Filter value should be correct" + ); +}); + +/** + * Test extensibleMatch filter is encoded correctly. + */ +add_task(function test_extensibleMatchFilter() { + // Test data is from https://ldap.com/ldapv3-wire-protocol-reference-search/. + // filter string, BER payload, description + let filterBER = [ + [ + "(uid:dn:caseIgnoreMatch:=jdoe)", + "a91f810f6361736549676e6f72654d61746368820375696483046a646f658401ff", + "<type>:dn:<rule>:=<value>", + ], + ["(uid:=jdoe)", "a90b820375696483046a646f65", "<type>:=<value>"], + [ + "(:caseIgnoreMatch:=foo)", + "a916810f6361736549676e6f72654d617463688303666f6f", + ":<rule>:=<value>", + ], + // This one is not directly from ldap.com, but assembled from the above cases. + [ + "(uid:caseIgnoreMatch:=jdoe)", + "a91c810f6361736549676e6f72654d61746368820375696483046a646f65", + "<type>:<rule>:=<value>", + ], + ]; + for (let [filter, ber, description] of filterBER) { + let req = new SearchRequest( + "ou=people,dc=planetexpress,dc=com", + Ci.nsILDAPURL.SCOPE_SUBTREE, + filter, + "", + 0, + 0 + ); + let filterBlock = req.protocolOp.valueBlock.value[6]; + Assert.equal( + CommonUtils.bufferToHex(new Uint8Array(filterBlock.toBER())), + ber, + description + ); + } +}); + +/** + * Test parsing to SearchResultReference works. + */ +add_task(function test_SearchResultReference() { + // A BER payload representing a SearchResultReference with two urls, test data + // is from https://ldap.com/ldapv3-wire-protocol-reference-search/. + let hex = + "306d020102736804326c6461703a2f2f6473312e6578616d706c652e636f6d3a3338392f64633d6578616d706c652c64633d636f6d3f3f7375623f04326c6461703a2f2f6473322e6578616d706c652e636f6d3a3338392f64633d6578616d706c652c64633d636f6d3f3f7375623f"; + let res = LDAPResponse.fromBER(CommonUtils.hexToArrayBuffer(hex).buffer); + + // Should be correctly parsed. + Assert.equal(res.constructor.name, "SearchResultReference"); + Assert.deepEqual(res.result, [ + "ldap://ds1.example.com:389/dc=example,dc=com??sub?", + "ldap://ds2.example.com:389/dc=example,dc=com??sub?", + ]); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js b/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js new file mode 100644 index 0000000000..9e2d1cc97e --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_LDAPSyncQuery.js @@ -0,0 +1,66 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +/** + * Test suite for nsILDAPSyncQuery. + */ + +const { LDAPDaemon, LDAPHandlerFn } = ChromeUtils.import( + "resource://testing-common/mailnews/Ldapd.jsm" +); +const { BinaryServer } = ChromeUtils.import( + "resource://testing-common/mailnews/Binaryd.jsm" +); + +const nsILDAPSyncQuery = Ci.nsILDAPSyncQuery; +const LDAPSyncQueryContractID = "@mozilla.org/ldapsyncquery;1"; + +function getLDAPAttributes(urlSpec) { + let url = Services.io.newURI(urlSpec).QueryInterface(Ci.nsILDAPURL); + let ldapquery = Cc[LDAPSyncQueryContractID].createInstance(nsILDAPSyncQuery); + let payload = ldapquery.getQueryResults(url, Ci.nsILDAPConnection.VERSION3); + // Returns a string with one attr per line. + return payload; +} + +add_task(async function test_LDAPSyncQuery() { + // Set up fake LDAP server, loaded with some contacts. + let daemon = new LDAPDaemon(); + let raw = await IOUtils.readUTF8( + do_get_file( + "../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json" + ).path + ); + let testContacts = JSON.parse(raw); + daemon.add(...Object.values(testContacts)); + // daemon.setDebug(true); + + let server = new BinaryServer(LDAPHandlerFn, daemon); + server.start(); + + // Fetch only the Holmes family. + let out = getLDAPAttributes( + `ldap://localhost:${server.port}/??sub?(sn=Holmes)` + ); + if (daemon.debug) { + dump(`--- getLDAPAttributes() ---\n${out}\n--------------------\n`); + } + + // Make sure we got the contacts we expected: + Assert.ok(out.includes("cn=Eurus Holmes")); + Assert.ok(out.includes("cn=Mycroft Holmes")); + Assert.ok(out.includes("cn=Sherlock Holmes")); + + // Sanity check: make sure some non-Holmes people were excluded. + Assert.ok(!out.includes("cn=John Watson")); + Assert.ok(!out.includes("cn=Jim Moriarty")); + + // Fetch again but this time the filter is without parens. + out = getLDAPAttributes(`ldap://localhost:${server.port}/??sub?sn=Holmes`); + + // Make sure we got the contacts we expected: + Assert.ok(out.includes("cn=Eurus Holmes")); + Assert.ok(out.includes("cn=Mycroft Holmes")); + Assert.ok(out.includes("cn=Sherlock Holmes")); + + server.stop(); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_abCardProperty.js b/comm/mailnews/addrbook/test/unit/test_abCardProperty.js new file mode 100644 index 0000000000..5feae230dc --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_abCardProperty.js @@ -0,0 +1,178 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for basic nsIAbCard functions. + */ + +// Intersperse these with UTF-8 values to check we handle them correctly. +var kFNValue = "testFirst\u00D0"; +var kLNValue = "testLast"; +var kDNValue = "testDisplay\u00D1"; +var kEmailValue = "testEmail\u00D2@foo.invalid"; +var kEmailValueLC = "testemail\u00D2@foo.invalid"; +var kEmailValue2 = "test@test.foo.invalid"; +// Email without the @ or anything after it. +var kEmailReducedValue = "testEmail\u00D2"; +var kCompanyValue = "Test\u00D0 Company"; + +add_task(function testAbCardProperty() { + let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + + // Test - Set First, Last and Display Names and Email Address + // via setProperty, and check correctly saved via their + // attributes. We're using firstName to check UTF-8 values. + card.setProperty("FirstName", kFNValue); + card.setProperty("LastName", kLNValue); + card.setProperty("DisplayName", kDNValue); + card.setProperty("PrimaryEmail", kEmailValue); + + Assert.equal(card.firstName, kFNValue); + Assert.equal(card.lastName, kLNValue); + Assert.equal(card.displayName, kDNValue); + Assert.equal(card.primaryEmail, kEmailValue); + + // Repeat in the opposite order. + card.firstName = kFNValue; + card.lastName = kLNValue; + card.displayName = kDNValue; + card.primaryEmail = kEmailValue; + + Assert.equal(card.getProperty("FirstName", "BAD"), kFNValue); + Assert.equal(card.getProperty("LastName", "BAD"), kLNValue); + Assert.equal(card.getProperty("DisplayName", "BAD"), kDNValue); + Assert.equal(card.getProperty("PrimaryEmail", "BAD"), kEmailValue); + + // Test - generateName. Note: if the addressBook.properties + // value changes, this will affect these tests. + + const { + GENERATE_DISPLAY_NAME, + GENERATE_LAST_FIRST_ORDER, + GENERATE_FIRST_LAST_ORDER, + } = Ci.nsIAbCard; + + // Add a company name, so we can test fallback to company name. + card.setProperty("Company", kCompanyValue); + + Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kDNValue); + Assert.equal( + card.generateName(GENERATE_LAST_FIRST_ORDER), + kLNValue + ", " + kFNValue + ); + Assert.equal( + card.generateName(GENERATE_FIRST_LAST_ORDER), + kFNValue + " " + kLNValue + ); + + // Test - generateName, with missing items. + + card.displayName = ""; + Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kCompanyValue); + + card.deleteProperty("Company"); + Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kEmailReducedValue); + + // Reset company name for the first/last name tests. + card.setProperty("Company", kCompanyValue); + + card.firstName = ""; + Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kLNValue); + Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kLNValue); + + card.firstName = kFNValue; + card.lastName = ""; + Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kFNValue); + Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kFNValue); + + card.firstName = ""; + Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kCompanyValue); + Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kCompanyValue); + + card.deleteProperty("Company"); + Assert.equal( + card.generateName(GENERATE_LAST_FIRST_ORDER), + kEmailReducedValue + ); + Assert.equal( + card.generateName(GENERATE_FIRST_LAST_ORDER), + kEmailReducedValue + ); + + card.primaryEmail = ""; + Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), ""); + Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), ""); + + // Test - generateNameWithBundle, most of this will have + // been tested above. + + card.firstName = kFNValue; + card.lastName = kLNValue; + + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + + Assert.equal(card.generateName(1, bundle), kLNValue + ", " + kFNValue); + + // Test - generatePhoneticName + + card.setProperty("PhoneticFirstName", kFNValue); + card.setProperty("PhoneticLastName", kLNValue); + Assert.equal(card.generatePhoneticName(false), kFNValue + kLNValue); + Assert.equal(card.generatePhoneticName(true), kLNValue + kFNValue); + + card.setProperty("PhoneticLastName", ""); + Assert.equal(card.generatePhoneticName(false), kFNValue); + Assert.equal(card.generatePhoneticName(true), kFNValue); + + card.setProperty("PhoneticFirstName", ""); + card.setProperty("PhoneticLastName", kLNValue); + Assert.equal(card.generatePhoneticName(false), kLNValue); + Assert.equal(card.generatePhoneticName(true), kLNValue); + + // Test - emailAddresses + + card.deleteProperty("PrimaryEmail"); + card.deleteProperty("SecondEmail"); + Assert.deepEqual(card.emailAddresses, []); + + card.primaryEmail = kEmailValue; + Assert.deepEqual(card.emailAddresses, [kEmailValue]); + + card.setProperty("SecondEmail", kEmailValue2); + Assert.deepEqual(card.emailAddresses, [kEmailValue, kEmailValue2]); + + card.primaryEmail = ""; + Assert.deepEqual(card.emailAddresses, [kEmailValue2]); + + card.deleteProperty("SecondEmail"); + Assert.deepEqual(card.emailAddresses, []); + + // Test - hasEmailAddress + + card.deleteProperty("PrimaryEmail"); + card.deleteProperty("SecondEmail"); + + Assert.equal(card.hasEmailAddress(kEmailValue), false); + Assert.equal(card.hasEmailAddress(kEmailValueLC), false); + Assert.equal(card.hasEmailAddress(kEmailValue2), false); + + card.setProperty("PrimaryEmail", kEmailValue); + + Assert.equal(card.hasEmailAddress(kEmailValue), true); + Assert.equal(card.hasEmailAddress(kEmailValueLC), true); + Assert.equal(card.hasEmailAddress(kEmailValue2), false); + + card.setProperty("SecondEmail", kEmailValue2); + + Assert.equal(card.hasEmailAddress(kEmailValue), true); + Assert.equal(card.hasEmailAddress(kEmailValueLC), true); + Assert.equal(card.hasEmailAddress(kEmailValue2), true); + + card.deleteProperty("PrimaryEmail"); + + Assert.equal(card.hasEmailAddress(kEmailValue), false); + Assert.equal(card.hasEmailAddress(kEmailValueLC), false); + Assert.equal(card.hasEmailAddress(kEmailValue2), true); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_addrBookCard.js b/comm/mailnews/addrbook/test/unit/test_addrBookCard.js new file mode 100644 index 0000000000..bf5a12b1dd --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_addrBookCard.js @@ -0,0 +1,260 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for basic nsIAbCard functions. + */ + +const { AddrBookCard } = ChromeUtils.import( + "resource:///modules/AddrBookCard.jsm" +); +const { VCardPropertyEntry } = ChromeUtils.import( + "resource:///modules/VCardUtils.jsm" +); + +// Intersperse these with UTF-8 values to check we handle them correctly. +var kFNValue = "testFirst\u00D0"; +var kLNValue = "testLast"; +var kDNValue = "testDisplay\u00D1"; +var kEmailValue = "testEmail\u00D2@foo.invalid"; +var kEmailValueLC = "testemail\u00D2@foo.invalid"; +var kEmailValue2 = "test@test.foo.invalid"; +// Email without the @ or anything after it. +var kEmailReducedValue = "testEmail\u00D2"; + +add_task(function testAddrBookCard() { + let card = new AddrBookCard(); + + // Test - Set First, Last and Display Names and Email Address + // via setProperty, and check correctly saved via their + // attributes. We're using firstName to check UTF-8 values. + card.vCardProperties.addValue("n", [kLNValue, kFNValue, "", "", ""]); + card.vCardProperties.addValue("fn", kDNValue); + card.vCardProperties.addValue("email", kEmailValue); + + Assert.equal(card.firstName, kFNValue); + Assert.equal(card.lastName, kLNValue); + Assert.equal(card.displayName, kDNValue); + Assert.equal(card.primaryEmail, kEmailValue); + + // Repeat in the opposite order. + card.firstName = kFNValue; + card.lastName = kLNValue; + card.displayName = kDNValue; + card.primaryEmail = kEmailValue; + + Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [ + kLNValue, + kFNValue, + "", + "", + "", + ]); + Assert.equal(card.vCardProperties.getFirstValue("fn"), kDNValue); + Assert.equal(card.vCardProperties.getFirstValue("email"), kEmailValue); + + // Test - generateName. Note: if the addressBook.properties + // value changes, this will affect these tests. + + const { + GENERATE_DISPLAY_NAME, + GENERATE_LAST_FIRST_ORDER, + GENERATE_FIRST_LAST_ORDER, + } = Ci.nsIAbCard; + + Assert.equal(card.generateName(GENERATE_DISPLAY_NAME), kDNValue); + Assert.equal( + card.generateName(GENERATE_LAST_FIRST_ORDER), + kLNValue + ", " + kFNValue + ); + Assert.equal( + card.generateName(GENERATE_FIRST_LAST_ORDER), + kFNValue + " " + kLNValue + ); + + // Test - generateName, with missing items. + + card.displayName = ""; + Assert.equal( + card.generateName(GENERATE_DISPLAY_NAME), + kFNValue + " " + kLNValue + ); + + card.firstName = ""; + Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kLNValue); + Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kLNValue); + + card.firstName = kFNValue; + card.lastName = ""; + Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), kFNValue); + Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), kFNValue); + + card.firstName = ""; + Assert.equal( + card.generateName(GENERATE_LAST_FIRST_ORDER), + kEmailReducedValue + ); + Assert.equal( + card.generateName(GENERATE_FIRST_LAST_ORDER), + kEmailReducedValue + ); + + card.vCardProperties.clearValues("email"); + Assert.equal(card.generateName(GENERATE_LAST_FIRST_ORDER), ""); + Assert.equal(card.generateName(GENERATE_FIRST_LAST_ORDER), ""); + + // Test - generateNameWithBundle, most of this will have + // been tested above. + + card.firstName = kFNValue; + card.lastName = kLNValue; + + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/addressbook/addressBook.properties" + ); + + Assert.equal(card.generateName(1, bundle), kLNValue + ", " + kFNValue); + + // Test - generatePhoneticName + + card.setProperty("PhoneticFirstName", kFNValue); + card.setProperty("PhoneticLastName", kLNValue); + Assert.equal(card.generatePhoneticName(false), kFNValue + kLNValue); + Assert.equal(card.generatePhoneticName(true), kLNValue + kFNValue); + + card.setProperty("PhoneticLastName", ""); + Assert.equal(card.generatePhoneticName(false), kFNValue); + Assert.equal(card.generatePhoneticName(true), kFNValue); + + card.setProperty("PhoneticFirstName", ""); + card.setProperty("PhoneticLastName", kLNValue); + Assert.equal(card.generatePhoneticName(false), kLNValue); + Assert.equal(card.generatePhoneticName(true), kLNValue); + + // Test - emailAddresses + + Assert.deepEqual(card.emailAddresses, []); + + card.primaryEmail = kEmailValue; + Assert.deepEqual(card.emailAddresses, [kEmailValue]); + + card.vCardProperties.addEntry( + new VCardPropertyEntry("email", {}, "text", kEmailValue2) + ); + Assert.deepEqual(card.emailAddresses, [kEmailValue, kEmailValue2]); + + card.primaryEmail = ""; + Assert.deepEqual(card.emailAddresses, [kEmailValue2]); + + card.primaryEmail = ""; + Assert.deepEqual(card.emailAddresses, []); + + // Test - primaryEmail + + card.vCardProperties.addEntry( + new VCardPropertyEntry("email", {}, "text", "three@invalid") + ); + card.vCardProperties.addEntry( + new VCardPropertyEntry("email", { pref: 2 }, "text", "two@invalid") + ); + card.vCardProperties.addEntry( + new VCardPropertyEntry("email", {}, "text", "four@invalid") + ); + card.vCardProperties.addEntry( + new VCardPropertyEntry("email", { pref: 1 }, "text", "one@invalid") + ); + Assert.deepEqual(card.emailAddresses, [ + "one@invalid", + "two@invalid", + "three@invalid", + "four@invalid", + ]); + Assert.equal(card.primaryEmail, "one@invalid"); + + // Setting primaryEmail to the existing value changes nothing. + card.primaryEmail = "one@invalid"; + Assert.deepEqual(card.emailAddresses, [ + "one@invalid", + "two@invalid", + "three@invalid", + "four@invalid", + ]); + Assert.equal(card.primaryEmail, "one@invalid"); + Assert.deepEqual( + card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref), + ["1", "2", undefined, undefined] + ); + + // Setting primaryEmail to another existing address replaces the address with the new one. + card.primaryEmail = "four@invalid"; + Assert.deepEqual(card.emailAddresses, [ + "four@invalid", + "two@invalid", + "three@invalid", + ]); + Assert.equal(card.primaryEmail, "four@invalid"); + Assert.deepEqual( + card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref), + ["1", "2", undefined] + ); + + // Setting primaryEmail to null promotes the next address. + card.primaryEmail = null; + Assert.deepEqual(card.emailAddresses, ["two@invalid", "three@invalid"]); + Assert.equal(card.primaryEmail, "two@invalid"); + Assert.deepEqual( + card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref), + ["1", undefined] + ); + + // Setting primaryEmail to a new address replaces the address with the new one. + card.primaryEmail = "five@invalid"; + Assert.deepEqual(card.emailAddresses, ["five@invalid", "three@invalid"]); + Assert.equal(card.primaryEmail, "five@invalid"); + Assert.deepEqual( + card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref), + ["1", undefined] + ); + + // Setting primaryEmail to an empty string promotes the next address. + card.primaryEmail = ""; + Assert.deepEqual(card.emailAddresses, ["three@invalid"]); + Assert.equal(card.primaryEmail, "three@invalid"); + Assert.deepEqual( + card.vCardProperties.getAllEntriesSorted("email").map(e => e.params.pref), + ["1"] + ); + + // Setting primaryEmail to null clears the only address. + card.primaryEmail = null; + Assert.deepEqual(card.emailAddresses, []); + Assert.equal(card.primaryEmail, ""); + + // Test - hasEmailAddress + + Assert.equal(card.hasEmailAddress(kEmailValue), false); + Assert.equal(card.hasEmailAddress(kEmailValueLC), false); + Assert.equal(card.hasEmailAddress(kEmailValue2), false); + + card.vCardProperties.addEntry( + new VCardPropertyEntry("email", {}, "text", kEmailValue) + ); + + Assert.equal(card.hasEmailAddress(kEmailValue), true); + Assert.equal(card.hasEmailAddress(kEmailValueLC), true); + Assert.equal(card.hasEmailAddress(kEmailValue2), false); + + card.vCardProperties.addEntry( + new VCardPropertyEntry("email", {}, "text", kEmailValue2) + ); + + Assert.equal(card.hasEmailAddress(kEmailValue), true); + Assert.equal(card.hasEmailAddress(kEmailValueLC), true); + Assert.equal(card.hasEmailAddress(kEmailValue2), true); + + card.vCardProperties.removeEntry( + card.vCardProperties.getAllEntries("email")[0] + ); + + Assert.equal(card.hasEmailAddress(kEmailValue), false); + Assert.equal(card.hasEmailAddress(kEmailValueLC), false); + Assert.equal(card.hasEmailAddress(kEmailValue2), true); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js b/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js new file mode 100644 index 0000000000..1b76099227 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_basic_nsIAbDirectory.js @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for basic address book functions - tests obtaining the (default) + * personal address book and getting its details from the nsIAbDirectory. + * + * Functions/attributes not currently tested: + * - lastModifiedDate + * - childNodes + * - childCards + * - deleteDirectory + * - hasCard + * - hasDirectory + * - addCard + * - modifyCard + * - deleteCards + * - dropCard + * - addressLists + * - addMailList + * - listNickName + * - description + * - editMailListToDatabase + * - copyMailList + */ + +// Main function for the this test so we can check both personal and +// collected books work correctly in an easy manner. +function check_ab(abConfig) { + // Test - Get the directory + + let AB = MailServices.ab.getDirectory(abConfig.URI); + + // Test - Is it the right type? + + if (abConfig.dirType == 2) { + Assert.ok(AB instanceof Ci.nsIAbMDBDirectory); + } + + // Test - Check attributes + + Assert.equal(AB.propertiesChromeURI, kNormalPropertiesURI); + Assert.equal(AB.readOnly, abConfig.readOnly); + Assert.equal(AB.dirName, abConfig.dirName); + Assert.equal(AB.dirType, abConfig.dirType); + Assert.equal(AB.fileName, abConfig.fileName); + Assert.equal(AB.URI, abConfig.URI); + Assert.equal(AB.position, abConfig.position); + Assert.equal(AB.isMailList, false); + Assert.equal(AB.isRemote, false); + Assert.equal(AB.isSecure, false); + Assert.equal(AB.supportsMailingLists, true); + Assert.equal(AB.dirPrefId, abConfig.dirPrefID); + + // Test - autocomplete enable/disable + + // enable is the default + Assert.equal(AB.useForAutocomplete(""), true); + + Services.prefs.setBoolPref("mail.enable_autocomplete", false); + Assert.equal(AB.useForAutocomplete(""), false); + + Services.prefs.setBoolPref("mail.enable_autocomplete", true); + Assert.equal(AB.useForAutocomplete(""), true); + + AB.setBoolValue("enable_autocomplete", false); + Assert.equal(AB.useForAutocomplete(""), false); + + AB.setBoolValue("enable_autocomplete", true); + Assert.equal(AB.useForAutocomplete(""), true); + + // Test - check getting default preferences + + Assert.equal(AB.getIntValue("random", 54321), 54321); + Assert.equal(AB.getBoolValue("random", false), false); + Assert.equal(AB.getStringValue("random", "abc"), "abc"); + Assert.equal(AB.getLocalizedStringValue("random", "xyz"), "xyz"); + + // Test - check get/set int preferences on nsIAbDirectory + + AB.setIntValue("inttest", 12345); + Assert.equal( + Services.prefs.getIntPref(abConfig.dirPrefID + ".inttest"), + 12345 + ); + Assert.equal(AB.getIntValue("inttest", -1), 12345); + + AB.setIntValue("inttest", 123456); + Assert.equal( + Services.prefs.getIntPref(abConfig.dirPrefID + ".inttest"), + 123456 + ); + Assert.equal(AB.getIntValue("inttest", -2), 123456); + + // Test - check get/set bool preferences on nsIAbDirectory + + AB.setBoolValue("booltest", true); + Assert.equal( + Services.prefs.getBoolPref(abConfig.dirPrefID + ".booltest"), + true + ); + Assert.equal(AB.getBoolValue("booltest", false), true); + + AB.setBoolValue("booltest", false); + Assert.equal( + Services.prefs.getBoolPref(abConfig.dirPrefID + ".booltest"), + false + ); + Assert.equal(AB.getBoolValue("booltest", true), false); + + // Test - check get/set string preferences on nsIAbDirectory + + AB.setStringValue("stringtest", "tyu"); + Assert.equal( + Services.prefs.getCharPref(abConfig.dirPrefID + ".stringtest"), + "tyu" + ); + Assert.equal(AB.getStringValue("stringtest", ""), "tyu"); +} + +function run_test() { + // Check the default personal address book + check_ab(kPABData); + + // Check the default collected address book + check_ab(kCABData); +} diff --git a/comm/mailnews/addrbook/test/unit/test_bug1522453.js b/comm/mailnews/addrbook/test/unit/test_bug1522453.js new file mode 100644 index 0000000000..b2da2eba12 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_bug1522453.js @@ -0,0 +1,72 @@ +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +add_task(async function () { + do_get_profile(); + MailServices.ab.directories; + let book = MailServices.ab.getDirectory(kPABData.URI); + + let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance( + Ci.nsIAbDirectory + ); + list.isMailList = true; + list.dirName = "list"; + list = book.addMailList(list); + + let contact1 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + contact1.firstName = "contact"; + contact1.lastName = "1"; + contact1.primaryEmail = "contact1@invalid"; + contact1 = book.addCard(contact1); + list.addCard(contact1); + + let contact2 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + contact2.firstName = "contact"; + contact2.lastName = "2"; + // No email address! + contact2 = book.addCard(contact2); + list.addCard(contact2); + + let contact3 = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + contact3.firstName = "contact"; + contact3.lastName = "3"; + contact3.primaryEmail = "contact3@invalid"; + contact3 = book.addCard(contact3); + list.addCard(contact3); + + // book.childCards should contain the list and all three contacts. + let bookCards = book.childCards; + equal(bookCards.length, 1 + 3); + equal(list.UID, bookCards[0].UID); + equal(contact1.UID, bookCards[1].UID); + equal(contact2.UID, bookCards[2].UID); + equal(contact3.UID, bookCards[3].UID); + + // list.childCards should contain contacts 1 and 3, and crucially, not die at 2. + let listCards = list.childCards; + equal(listCards.length, 2); + equal(contact1.UID, listCards[0].UID); + equal(contact3.UID, listCards[1].UID); + + // Reload the address book manager. + let reloadPromise = TestUtils.topicObserved("addrbook-reloaded"); + Services.obs.notifyObservers(null, "addrbook-reload"); + await reloadPromise; + + // Renew our references. + book = MailServices.ab.getDirectory(kPABData.URI); + list = book.childNodes[0]; + + // list.childCards should contain contacts 1 and 3. + listCards = list.childCards; + equal(listCards.length, 2); + equal(contact1.UID, listCards[0].UID); + equal(contact3.UID, listCards[1].UID); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_bug1769889.js b/comm/mailnews/addrbook/test/unit/test_bug1769889.js new file mode 100644 index 0000000000..37cc91fa45 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_bug1769889.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that complex names are correctly flattened when stored in the + * database as FirstName/LastName, and when returned from the + * firstName/lastName getters. + */ + +var { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm"); + +add_task(async function testMultiValueLast() { + // Multiple last names. + let vCard = formatVCard` + BEGIN:VCARD + N:second-last,last;first;;; + END:VCARD + `; + + let book = MailServices.ab.getDirectory(kPABData.URI); + let card = book.addCard(VCardUtils.vCardToAbCard(vCard)); + + Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [ + ["second-last", "last"], + "first", + "", + "", + "", + ]); + Assert.equal(card.firstName, "first"); + Assert.equal(card.getProperty("FirstName", "WRONG"), "first"); + Assert.equal(card.lastName, "second-last last"); + Assert.equal(card.getProperty("LastName", "WRONG"), "second-last last"); +}); + +add_task(async function testMultiValueFirst() { + // Multiple first names. + let vCard = formatVCard` + BEGIN:VCARD + N:last;first,second;;; + END:VCARD + `; + + let book = MailServices.ab.getDirectory(kPABData.URI); + let card = book.addCard(VCardUtils.vCardToAbCard(vCard)); + + Assert.deepEqual(card.vCardProperties.getFirstValue("n"), [ + "last", + ["first", "second"], + "", + "", + "", + ]); + Assert.equal(card.firstName, "first second"); + Assert.equal(card.getProperty("FirstName", "WRONG"), "first second"); + Assert.equal(card.lastName, "last"); + Assert.equal(card.getProperty("LastName", "WRONG"), "last"); +}); + +add_task(async function testNotEnoughValues() { + // The name field doesn't have enough components. That's okay. + let vCard = formatVCard` + BEGIN:VCARD + N:last;first + END:VCARD + `; + + let book = MailServices.ab.getDirectory(kPABData.URI); + let card = book.addCard(VCardUtils.vCardToAbCard(vCard)); + + Assert.deepEqual(card.vCardProperties.getFirstValue("n"), ["last", "first"]); + Assert.equal(card.firstName, "first"); + Assert.equal(card.getProperty("FirstName", "WRONG"), "first"); + Assert.equal(card.lastName, "last"); + Assert.equal(card.getProperty("LastName", "WRONG"), "last"); +}); + +add_task(async function testStringValue() { + // This is a bad value. Let's just ignore it for first/last name purposes. + let vCard = formatVCard` + BEGIN:VCARD + N:first last + END:VCARD + `; + + let book = MailServices.ab.getDirectory(kPABData.URI); + let card = book.addCard(VCardUtils.vCardToAbCard(vCard)); + + Assert.deepEqual(card.vCardProperties.getFirstValue("n"), "first last"); + Assert.equal(card.firstName, ""); + Assert.equal(card.getProperty("FirstName", "RIGHT"), "RIGHT"); + Assert.equal(card.lastName, ""); + Assert.equal(card.getProperty("LastName", "RIGHT"), "RIGHT"); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_bug387403.js b/comm/mailnews/addrbook/test/unit/test_bug387403.js new file mode 100644 index 0000000000..9f8621c705 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_bug387403.js @@ -0,0 +1,16 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Test for bug 387403 crash when opening e-mail with broken vcard. + */ + +function run_test() { + // Before bug 387403 this would hang, eating up all the memory until it + // crashed. + try { + Cc["@mozilla.org/addressbook/msgvcardservice;1"] + .getService(Ci.nsIMsgVCardService) + .escapedVCardToAbCard( + "begin:vcard\nfn;quoted-printable:Xxxx=C5=82xx Xxx\nn;quoted-printable:Xxx;Xxxx=C5=82xx \nadr;quoted-printable;quoted-printable;dom:;;xx. Xxxxxxxxxxxx X;Xxxxxx=C3=3" + ); + } catch (ex) {} +} diff --git a/comm/mailnews/addrbook/test/unit/test_bug448165.js b/comm/mailnews/addrbook/test/unit/test_bug448165.js new file mode 100644 index 0000000000..57337f7fa1 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_bug448165.js @@ -0,0 +1,18 @@ +/** + * A simple test to check for a regression of bug 448165: Mailnews crashes in + * nsAbMDBDirectory::DeleteCards if aCards is null + */ +function run_test() { + // get the Personal Address Book + let pab = MailServices.ab.getDirectory(kPABData.URI); + Assert.ok(pab instanceof Ci.nsIAbDirectory); + try { + pab.deleteCards(null); // this should throw an error + do_throw( + "Error, deleteCards should throw an error when null is passed to it" + ); + } catch (e) { + // make sure the correct error message was thrown + Assert.equal(e.result, Cr.NS_ERROR_XPC_CANT_CONVERT_PRIMITIVE_TO_ARRAY); + } +} diff --git a/comm/mailnews/addrbook/test/unit/test_bug534822.js b/comm/mailnews/addrbook/test/unit/test_bug534822.js new file mode 100644 index 0000000000..4c18f64b5d --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_bug534822.js @@ -0,0 +1,38 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Tests for bug 534822 - non-built-in address books specified in preferences + * don't appear in address book lists. + */ + +function run_test() { + // Read in the prefs that will be default. + let specialPrefs = do_get_file("data/bug534822prefs.js"); + + var profileDir = do_get_profile(); + specialPrefs.copyTo(profileDir, ""); + + specialPrefs = profileDir; + specialPrefs.append("bug534822prefs.js"); + + Services.prefs.readUserPrefsFromFile(specialPrefs); + + // Now load the ABs and check we've got all of them. + let results = [ + { name: "extension", result: false }, + { name: kPABData.dirName, result: false }, + { name: kCABData.dirName, result: false }, + ]; + + for (let dir of MailServices.ab.directories) { + for (let i = 0; i < results.length; ++i) { + if (results[i].name == dir.dirName) { + Assert.ok(!results[i].result); + results[i].result = true; + } + } + } + + results.forEach(function (result) { + Assert.ok(result.result); + }); +} diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js new file mode 100644 index 0000000000..1e8e476c7a --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_copyCard.js @@ -0,0 +1,148 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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" +); + +const vCardTemplate = + "BEGIN:VCARD\r\nUID:{}\r\nFN:Move me around\r\nEND:VCARD\r\n"; +const initialVCard = vCardTemplate.replace("{}", "copyme"); + +let cardDAVDirectory, localDirectory; +let initialCard, localCard; + +add_task(async () => { + // Put some cards on the server. + + CardDAVServer.putCardInternal("copyme.vcf", initialVCard); + + localDirectory = MailServices.ab.getDirectoryFromId("ldap_2.servers.pab"); + cardDAVDirectory = initDirectory(); + await cardDAVDirectory.fetchAllFromServer(); + + observer.init(); + + // Check we have the initial version of the card. + + Assert.equal(cardDAVDirectory.childCards.length, 1); + + initialCard = cardDAVDirectory.childCards[0]; + Assert.equal(initialCard.UID, "copyme"); + Assert.equal(initialCard.getProperty("_etag", ""), "55"); + Assert.equal( + initialCard.getProperty("_href", ""), + `${CardDAVServer.path}copyme.vcf` + ); + vCardEqual(initialCard.getProperty("_vCard", ""), initialVCard); +}); + +/** Copy the card to the local directory. */ +add_task(async function copyCardToLocal() { + localDirectory.dropCard(initialCard, true); + Assert.equal(localDirectory.childCards.length, 1); + + localCard = localDirectory.childCards[0]; + // The UID must change, since this is a copy. + Assert.notEqual(localCard.UID, "copyme"); + Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY"); + Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY"); + vCardEqual( + localCard.getProperty("_vCard", "EMPTY"), + vCardTemplate.replace("{}", localCard.UID) + ); +}); + +/** Remove the card from the local directory for the next step. */ +add_task(async function () { + localDirectory.deleteCards(localDirectory.childCards); + Assert.equal(localDirectory.childCards.length, 0); +}); + +/** This time, move the card to the local directory. */ +add_task(async function moveCardToLocal() { + localDirectory.addCard(initialCard); + Assert.equal(localDirectory.childCards.length, 1); + + localCard = localDirectory.childCards[0]; + // UID should not change + Assert.equal(localCard.UID, "copyme"); + Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY"); + Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY"); + vCardEqual( + localCard.getProperty("_vCard", "EMPTY"), + vCardTemplate.replace("{}", localCard.UID) + ); +}); + +/** + * Okay, let's go back again. First we'll need to remove the card from the + * CardDAV directory. + */ +add_task(async function () { + let deletedPromise = observer.waitFor("addrbook-contact-deleted"); + cardDAVDirectory.deleteCards(cardDAVDirectory.childCards); + await deletedPromise; + Assert.equal(cardDAVDirectory.childCards.length, 0); +}); + +/** Copy the card back to the CardDAV directory. */ +add_task(async function copyCardToCardDAV() { + cardDAVDirectory.dropCard(localCard, true); + Assert.equal(cardDAVDirectory.childCards.length, 1); + + let newCard = cardDAVDirectory.childCards[0]; + Assert.notEqual(newCard.UID, "copyme"); + Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY"); + Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY"); + vCardEqual( + localCard.getProperty("_vCard", "EMPTY"), + vCardTemplate.replace("{}", localCard.UID) + ); + + await observer.waitFor("addrbook-contact-updated"); + let newCardAfterSync = cardDAVDirectory.childCards[0]; + Assert.equal(newCardAfterSync.getProperty("_etag", "EMPTY"), "85"); + Assert.equal( + newCardAfterSync.getProperty("_href", "EMPTY"), + `${CardDAVServer.path}${newCard.UID}.vcf` + ); + vCardEqual( + newCardAfterSync.getProperty("_vCard", "EMPTY"), + vCardTemplate.replace("{}", newCard.UID) + ); +}); + +/** Remove the card from the CardDAV directory again. */ +add_task(async function () { + let deletedPromise = observer.waitFor("addrbook-contact-deleted"); + cardDAVDirectory.deleteCards(cardDAVDirectory.childCards); + await deletedPromise; + Assert.equal(cardDAVDirectory.childCards.length, 0); +}); + +/** This time, move the card to the CardDAV directory. */ +add_task(async function moveCardToCardDAV() { + cardDAVDirectory.addCard(localCard); + Assert.equal(cardDAVDirectory.childCards.length, 1); + + let newCard = cardDAVDirectory.childCards[0]; + // UID should not change + Assert.equal(newCard.UID, "copyme"); + Assert.equal(localCard.getProperty("_etag", "EMPTY"), "EMPTY"); + Assert.equal(localCard.getProperty("_href", "EMPTY"), "EMPTY"); + // _vCard property won't change until we send this card to the server. + vCardEqual(localCard.getProperty("_vCard", "EMPTY"), initialVCard); + + await observer.waitFor("addrbook-contact-updated"); + let newCardAfterSync = cardDAVDirectory.childCards[0]; + Assert.equal(newCardAfterSync.getProperty("_etag", "EMPTY"), "55"); + Assert.equal( + newCardAfterSync.getProperty("_href", "EMPTY"), + `${CardDAVServer.path}copyme.vcf` + ); + vCardEqual(newCardAfterSync.getProperty("_vCard", "EMPTY"), initialVCard); + + await clearDirectory(cardDAVDirectory); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js new file mode 100644 index 0000000000..fa32260328 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_offline.js @@ -0,0 +1,550 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests that changes in a CardDAV directory when offline or unable to reach +// the server are (a) visible in the client immediately, and (b) sent to the +// server when it's next available. +// +// Note that we close the server rather than using Services.io.offline, as +// the server is localhost and therefore not affected by the offline setting. + +var { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +var directory, restart, useSyncV1; + +async function subtest() { + // Put some cards on the server. + + CardDAVServer.putCardInternal( + "change-me.vcf", + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I shall be changed.\r\nEND:VCARD\r\n" + ); + CardDAVServer.putCardInternal( + "delete-me.vcf", + "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n" + ); + + directory = initDirectory(); + + info("Initial sync with server."); + await directory.fetchAllFromServer(); + + if (useSyncV1) { + directory._syncToken = null; + } + + await subtestCreateCard(); + await subtestUpdateCard(); + await subtestDeleteCard(); + await subtestCreateDeleteCard(); + await subtestStillOffline(); + + // Check everything is still correct at the end. + + info("Checking cards on client are correct."); + Assert.deepEqual( + directory.childCards.map(c => c.UID).sort(), + ["another-new-card", "change-me"], + "right cards remain on client" + ); + + await clearDirectory(directory); + CardDAVServer.reset(); +} + +function promiseSyncFailed() { + return TestUtils.topicObserved("addrbook-directory-sync-failed"); +} + +function promiseSyncSucceeded() { + return TestUtils.topicObserved("addrbook-directory-synced"); +} + +/** + * The behaviour should remain the same even if Thunderbird restarts. + * If `restart` is true, simulate restarting. + */ +async function pretendToRestart() { + // Ensure we've finished any async stuff. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 125)); + + if (!restart) { + return; + } + + await directory.cleanUp(); + + info("Shutdown simulated, now restarting."); + directory = new CardDAVDirectory(); + directory.init("jscarddav://carddav.sqlite"); +} + +/** Creating a new card while "offline". */ +async function subtestCreateCard() { + Assert.equal( + directory.childCards.length, + 2, + "card count on client before test" + ); + Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test"); + + info("Going offline, creating a new card."); + await CardDAVServer.close(); + + let contactPromise = TestUtils.topicObserved("addrbook-contact-created"); + let syncFailedPromise = promiseSyncFailed(); + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + newCard.displayName = "A New Card"; + newCard.UID = "a-new-card"; + newCard = directory.addCard(newCard); + await contactPromise; + await syncFailedPromise; + + Assert.equal( + directory.childCards.length, + 3, + "card should have been added on client while offline" + ); + Assert.ok( + directory.childCards.find(c => c.UID == "a-new-card"), + "card should have been added on client" + ); + Assert.equal( + CardDAVServer.cards.size, + 2, + "card should NOT have been added on server while offline" + ); + + info("Going online and syncing."); + await pretendToRestart(directory); + CardDAVServer.reopen(); + + Assert.equal( + CardDAVServer.cards.size, + 2, + "card should NOT have been added on server before syncing" + ); + + contactPromise = TestUtils.topicObserved("addrbook-contact-updated"); + let syncSucceededPromise = promiseSyncSucceeded(); + await directory.syncWithServer(); + await syncSucceededPromise; + let [notificationCard] = await contactPromise; + notificationCard.QueryInterface(Ci.nsIAbCard); + Assert.equal( + notificationCard.UID, + "a-new-card", + "correct card should have been updated" + ); + + Assert.equal( + notificationCard.getProperty("_href", "WRONG"), + `${CardDAVServer.path}a-new-card.vcf`, + "card should have been given _href property" + ); + Assert.equal( + notificationCard.getProperty("_etag", "WRONG"), + "68", + "card should have been given _etag property" + ); + vCardEqual( + notificationCard.getProperty("_vCard", "WRONG"), + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n", + "card should have been given _vCard property" + ); + + await checkCardsOnServer({ + ["change-me"]: { + etag: "63", + href: `${CardDAVServer.path}change-me.vcf`, + vCard: + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I shall be changed.\r\nEND:VCARD\r\n", + }, + ["delete-me"]: { + etag: "61", + href: `${CardDAVServer.path}delete-me.vcf`, + vCard: + "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n", + }, + ["a-new-card"]: { + etag: "68", + href: `${CardDAVServer.path}a-new-card.vcf`, + vCard: + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n", + }, + }); +} + +/** Changing an existing card while "offline". */ +async function subtestUpdateCard() { + Assert.equal( + directory.childCards.length, + 3, + "card count on client before test" + ); + Assert.equal(CardDAVServer.cards.size, 3, "card count on server before test"); + + info("Going offline, changing a card."); + await CardDAVServer.close(); + + let contactPromise = TestUtils.topicObserved("addrbook-contact-updated"); + let syncFailedPromise = promiseSyncFailed(); + let cardToChange = directory.childCards.find(c => c.UID == "change-me"); + cardToChange.displayName = "I'm a new man!"; + cardToChange = directory.modifyCard(cardToChange); + await contactPromise; + await syncFailedPromise; + + Assert.equal( + directory.childCards.find(c => c.UID == "change-me").displayName, + "I'm a new man!", + "card should have been changed on client while offline" + ); + Assert.stringContains( + CardDAVServer.cards.get(`${CardDAVServer.path}change-me.vcf`).vCard, + "I shall be changed.", + "card should NOT have been changed on server while offline" + ); + + info("Going online and syncing."); + await pretendToRestart(directory); + CardDAVServer.reopen(); + + Assert.stringContains( + CardDAVServer.cards.get(`${CardDAVServer.path}change-me.vcf`).vCard, + "I shall be changed.", + "card should NOT have been changed on server before syncing" + ); + + contactPromise = TestUtils.topicObserved("addrbook-contact-updated"); + let syncSucceededPromise = promiseSyncSucceeded(); + await directory.syncWithServer(); + await syncSucceededPromise; + let [notificationCard] = await contactPromise; + notificationCard.QueryInterface(Ci.nsIAbCard); + Assert.equal( + notificationCard.UID, + "change-me", + "correct card should have been updated" + ); + + Assert.equal( + notificationCard.getProperty("_href", "WRONG"), + `${CardDAVServer.path}change-me.vcf`, + "card _href property didn't change" + ); + Assert.equal( + notificationCard.getProperty("_etag", "WRONG"), + "58", + "card _etag property did change" + ); + vCardEqual( + notificationCard.getProperty("_vCard", "WRONG"), + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n", + "card _vCard property did change" + ); + + await checkCardsOnServer({ + ["change-me"]: { + etag: "58", + href: `${CardDAVServer.path}change-me.vcf`, + vCard: + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n", + }, + ["delete-me"]: { + etag: "61", + href: `${CardDAVServer.path}delete-me.vcf`, + vCard: + "BEGIN:VCARD\r\nUID:delete-me\r\nFN:Please delete me.\r\nEND:VCARD\r\n", + }, + ["a-new-card"]: { + etag: "68", + href: `${CardDAVServer.path}a-new-card.vcf`, + vCard: + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n", + }, + }); +} + +/** Deleting an existing card while "offline". */ +async function subtestDeleteCard() { + Assert.equal( + directory.childCards.length, + 3, + "card count on client before test" + ); + Assert.equal(CardDAVServer.cards.size, 3, "card count on server before test"); + + info("Going offline, deleting a card."); + await CardDAVServer.close(); + + let contactPromise = TestUtils.topicObserved("addrbook-contact-deleted"); + let syncFailedPromise = promiseSyncFailed(); + let cardToDelete = directory.childCards.find(c => c.UID == "delete-me"); + directory.deleteCards([cardToDelete]); + await contactPromise; + await syncFailedPromise; + + Assert.equal( + directory.childCards.length, + 2, + "card should have been removed on client while offline" + ); + Assert.ok( + !directory.childCards.find(c => c.UID == "delete-me"), + "card should have been removed on client while offline" + ); + Assert.equal( + CardDAVServer.cards.size, + 3, + "card should NOT have been removed on server while offline" + ); + + info("Going online and syncing."); + await pretendToRestart(directory); + CardDAVServer.reopen(); + + Assert.equal( + CardDAVServer.cards.size, + 3, + "card should NOT have been removed on server before syncing" + ); + + let syncSucceededPromise = promiseSyncSucceeded(); + await directory.syncWithServer(); + await syncSucceededPromise; + + await checkCardsOnServer({ + ["change-me"]: { + etag: "58", + href: `${CardDAVServer.path}change-me.vcf`, + vCard: + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n", + }, + ["a-new-card"]: { + etag: "68", + href: `${CardDAVServer.path}a-new-card.vcf`, + vCard: + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n", + }, + }); +} + +/** Adding a new card and deleting it again while "offline". */ +async function subtestCreateDeleteCard() { + Assert.equal( + directory.childCards.length, + 2, + "card count on client before test" + ); + Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test"); + + info("Going offline, adding a card."); + await CardDAVServer.close(); + + let contactPromise = TestUtils.topicObserved("addrbook-contact-created"); + let syncFailedPromise = promiseSyncFailed(); + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + newCard.displayName = "A Temporary Card"; + newCard.UID = "a-temporary-card"; + newCard = directory.addCard(newCard); + await contactPromise; + await syncFailedPromise; + + // Ensure we've finished any async stuff. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 125)); + + Assert.equal( + directory.childCards.length, + 3, + "card should have been added on client while offline" + ); + Assert.ok( + directory.childCards.find(c => c.UID == "a-temporary-card"), + "card should have been added on client while offline" + ); + Assert.equal( + CardDAVServer.cards.size, + 2, + "card should NOT have been added on server while offline" + ); + + info("Deleting the same card before syncing."); + contactPromise = TestUtils.topicObserved("addrbook-contact-deleted"); + directory.deleteCards([newCard]); + await contactPromise; + // No addrbook-directory-sync-failed notification here, we didn't attempt to + // delete a card that wasn't on the server (it had no _href property). + + Assert.equal( + directory.childCards.length, + 2, + "card should have been removed on client while offline" + ); + Assert.ok( + !directory.childCards.find(c => c.UID == "a-temporary-card"), + "card should have been removed on client while offline" + ); + Assert.equal( + CardDAVServer.cards.size, + 2, + "card should NOT have been on server while offline" + ); + + info("Going online and syncing."); + await pretendToRestart(directory); + CardDAVServer.reopen(); + + let syncSucceededPromise = promiseSyncSucceeded(); + await directory.syncWithServer(); + await syncSucceededPromise; + + await checkCardsOnServer({ + ["change-me"]: { + etag: "58", + href: `${CardDAVServer.path}change-me.vcf`, + vCard: + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n", + }, + ["a-new-card"]: { + etag: "68", + href: `${CardDAVServer.path}a-new-card.vcf`, + vCard: + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:A New Card\r\nUID:a-new-card\r\nEND:VCARD\r\n", + }, + }); +} + +/** + * Check that doing a sync while offline does nothing crazy. First make both + * kinds of changes, then sync while offline. + */ +async function subtestStillOffline() { + Assert.equal( + directory.childCards.length, + 2, + "card count on client before test" + ); + Assert.equal(CardDAVServer.cards.size, 2, "card count on server before test"); + + info("Going offline, adding a card."); + await CardDAVServer.close(); + + let contactPromise = TestUtils.topicObserved("addrbook-contact-created"); + let syncFailedPromise = promiseSyncFailed(); + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + newCard.displayName = "Another New Card"; + newCard.UID = "another-new-card"; + newCard = directory.addCard(newCard); + await contactPromise; + await syncFailedPromise; + + // Ensure we've finished any async stuff. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 125)); + + Assert.equal( + directory.childCards.length, + 3, + "card should have been added on client while offline" + ); + Assert.ok( + directory.childCards.find(c => c.UID == "another-new-card"), + "card should have been added on client while offline" + ); + Assert.equal( + CardDAVServer.cards.size, + 2, + "card should NOT have been added on server while offline" + ); + + info("Still offline, deleting a card."); + let cardToDelete = directory.childCards.find(c => c.UID == "a-new-card"); + contactPromise = TestUtils.topicObserved("addrbook-contact-deleted"); + syncFailedPromise = promiseSyncFailed(); + directory.deleteCards([cardToDelete]); + await contactPromise; + await syncFailedPromise; + + // Ensure we've finished any async stuff. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(r => setTimeout(r, 125)); + + info("Still offline, attempting to sync."); + syncFailedPromise = promiseSyncFailed(); + // Assert.rejects eats the thrown exception, so we don't see it logged here. + await Assert.rejects( + directory.syncWithServer(), + /NS_ERROR_CONNECTION_REFUSED/, + "Attempt to sync threw an exception" + ); + await syncFailedPromise; + + await pretendToRestart(); + syncFailedPromise = promiseSyncFailed(); + // Assert.rejects eats the thrown exception, so we don't see it logged here. + await Assert.rejects( + directory.syncWithServer(), + /NS_ERROR_CONNECTION_REFUSED/, + "Attempt to sync threw an exception" + ); + await syncFailedPromise; + + info("Going online and syncing."); + await pretendToRestart(directory); + CardDAVServer.reopen(); + + let syncSucceededPromise = promiseSyncSucceeded(); + await directory.syncWithServer(); + await syncSucceededPromise; + + await checkCardsOnServer({ + ["change-me"]: { + etag: "58", + href: `${CardDAVServer.path}change-me.vcf`, + vCard: + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm a new man!\r\nEND:VCARD\r\n", + }, + ["another-new-card"]: { + etag: "80", + href: `${CardDAVServer.path}another-new-card.vcf`, + vCard: + "BEGIN:VCARD\r\nVERSION:4.0\r\nFN:Another New Card\r\nUID:another-new-card\r\nEND:VCARD\r\n", + }, + }); +} + +add_task(async function test_syncV1_noRestart() { + restart = false; + useSyncV1 = true; + await subtest(); +}); + +add_task(async function test_syncV1_restart() { + restart = true; + useSyncV1 = true; + await subtest(); +}); + +add_task(async function test_syncV2_noRestart() { + restart = false; + useSyncV1 = false; + await subtest(); +}); + +add_task(async function test_syncV2_restart() { + restart = true; + useSyncV1 = false; + await subtest(); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js new file mode 100644 index 0000000000..244e27617e --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_serverModified.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Tests what happens if a server modifies a card when it first arrives. + * In this test the server changes the card's UID and path, which Google's + * CardDAV server does, and also adds a new property. All changes should be + * reflected in the client. */ + +add_task(async () => { + CardDAVServer.modifyCardOnPut = true; + + let directory = initDirectory(); + await directory.fetchAllFromServer(); + + observer.init(); + + // Create a new card, and check it has the right UID. + + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + newCard.displayName = "A New Card"; + newCard.UID = "a-new-card"; + newCard = directory.addCard(newCard); + observer.checkAndClearNotifications({ + "addrbook-contact-created": ["a-new-card"], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": [], + }); + + Assert.equal(directory.childCards.length, 1); + Assert.equal(directory.childCards[0].UID, "a-new-card"); + + // Wait for notifications. Both arrive at once so we listen for the first. + + let newUID = await observer.waitFor("addrbook-contact-created"); + Assert.equal(newUID, "drac-wen-a"); + + // Check the original card was deleted. + + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": ["a-new-card"], + }); + + // Check we have the card as modified by the server. + + Assert.equal(directory.childCards.length, 1); + let modifiedCard = directory.childCards[0]; + Assert.equal(modifiedCard.UID, "drac-wen-a"); + Assert.equal(modifiedCard.getProperty("_etag", ""), "92"); + Assert.equal( + modifiedCard.getProperty("_href", ""), + "/addressbooks/me/test/drac-wen-a.vcf" + ); + Assert.stringContains( + modifiedCard.getProperty("_vCard", ""), + "UID:drac-wen-a\r\n" + ); + Assert.stringContains( + modifiedCard.getProperty("_vCard", ""), + "X-MODIFIED-BY-SERVER:1\r\n" + ); + + await clearDirectory(directory); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js new file mode 100644 index 0000000000..0ec5a65ae0 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV1.js @@ -0,0 +1,282 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +async function subtest() { + // Put some cards on the server. + CardDAVServer.putCardInternal( + "keep-me.vcf", + "BEGIN:VCARD\r\nUID:keep-me\r\nFN:I'm going to stay.\r\nEND:VCARD\r\n" + ); + CardDAVServer.putCardInternal( + "change-me.vcf", + // This one includes a character encoded with UTF-8. + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm going to be changed. \xCF\x9E\r\nEND:VCARD\r\n" + ); + CardDAVServer.putCardInternal( + "delete-me.vcf", + "BEGIN:VCARD\r\nUID:delete-me\r\nFN:I'm going to be deleted.\r\nEND:VCARD\r\n" + ); + + let directory = initDirectory(); + + // We'll only use this for the initial sync, so I think it's okay to use + // bulkAddCards and not get a notification for every contact. + info("Initial sync with server."); + await directory.fetchAllFromServer(); + + info("Cards:"); + let cardMap = new Map(); + let oldETags = new Map(); + for (let card of directory.childCards) { + info(card.displayName); + info(card.getProperty("_href", "")); + info(card.getProperty("_etag", "")); + + cardMap.set(card.UID, card); + oldETags.set(card.UID, card.getProperty("_etag", "")); + } + + Assert.equal(cardMap.size, 3); + Assert.deepEqual([...cardMap.keys()].sort(), [ + "change-me", + "delete-me", + "keep-me", + ]); + Assert.equal( + cardMap.get("change-me").displayName, + "I'm going to be changed. Ϟ" + ); + + // Make some changes on the server. + + CardDAVServer.putCardInternal( + "change-me.vcf", + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I've been changed.\r\nEND:VCARD\r\n" + ); + CardDAVServer.deleteCardInternal("delete-me.vcf"); + CardDAVServer.putCardInternal( + "new.vcf", + "BEGIN:VCARD\r\nUID:new\r\nFN:I'm new!\r\nEND:VCARD\r\n" + ); + + // Sync with the server. + + info("Second sync with server."); + + observer.init(); + await directory.updateAllFromServerV1(); + observer.checkAndClearNotifications({ + "addrbook-contact-created": ["new"], + "addrbook-contact-updated": ["change-me"], + "addrbook-contact-deleted": ["delete-me"], + }); + + info("Cards:"); + cardMap.clear(); + for (let card of directory.childCards) { + info(card.displayName); + info(card.getProperty("_href", "")); + info(card.getProperty("_etag", "")); + + cardMap.set(card.UID, card); + } + + Assert.equal(cardMap.size, 3); + Assert.deepEqual([...cardMap.keys()].sort(), ["change-me", "keep-me", "new"]); + + Assert.equal( + cardMap.get("keep-me").getProperty("_etag", ""), + oldETags.get("keep-me") + ); + + Assert.equal(cardMap.get("change-me").displayName, "I've been changed."); + Assert.notEqual( + cardMap.get("change-me").getProperty("_etag", ""), + oldETags.get("change-me") + ); + oldETags.set("change-me", cardMap.get("change-me").getProperty("_etag", "")); + + Assert.equal(cardMap.get("new").displayName, "I'm new!"); + oldETags.set("new", cardMap.get("new").getProperty("_etag", "")); + + oldETags.delete("delete-me"); + + // Double-check that what we have matches what's on the server. + + await checkCardsOnServer({ + "change-me": { + etag: cardMap.get("change-me").getProperty("_etag", ""), + href: cardMap.get("change-me").getProperty("_href", ""), + vCard: cardMap.get("change-me").getProperty("_vCard", ""), + }, + "keep-me": { + etag: cardMap.get("keep-me").getProperty("_etag", ""), + href: cardMap.get("keep-me").getProperty("_href", ""), + vCard: cardMap.get("keep-me").getProperty("_vCard", ""), + }, + new: { + etag: cardMap.get("new").getProperty("_etag", ""), + href: cardMap.get("new").getProperty("_href", ""), + vCard: cardMap.get("new").getProperty("_vCard", ""), + }, + }); + + info("Third sync with server. No changes expected."); + + await directory.updateAllFromServerV1(); + + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": [], + }); + + // Delete a card on the client. + + info("Deleting a card on the client."); + + try { + directory.deleteCards([cardMap.get("new")]); + Assert.ok(!directory.readOnly, "read-only directory should throw"); + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": ["new"], + }); + + await checkCardsOnServer({ + "change-me": { + etag: cardMap.get("change-me").getProperty("_etag", ""), + href: cardMap.get("change-me").getProperty("_href", ""), + vCard: cardMap.get("change-me").getProperty("_vCard", ""), + }, + "keep-me": { + etag: cardMap.get("keep-me").getProperty("_etag", ""), + href: cardMap.get("keep-me").getProperty("_href", ""), + vCard: cardMap.get("keep-me").getProperty("_vCard", ""), + }, + }); + } catch (ex) { + Assert.ok(directory.readOnly, "read-write directory should not throw"); + } + + // Change a card on the client. + + info("Changing a card on the client."); + + try { + let changeMeCard = cardMap.get("change-me"); + changeMeCard.displayName = "I've been changed again!"; + + directory.modifyCard(changeMeCard); + Assert.ok(!directory.readOnly, "read-only directory should throw"); + Assert.equal( + await observer.waitFor("addrbook-contact-updated"), + "change-me" + ); + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": ["change-me"], + "addrbook-contact-deleted": [], + }); + + changeMeCard = directory.childCards.find(c => c.UID == "change-me"); + cardMap.set("change-me", changeMeCard); + + await checkCardsOnServer({ + "change-me": { + etag: changeMeCard.getProperty("_etag", ""), + href: changeMeCard.getProperty("_href", ""), + vCard: changeMeCard.getProperty("_vCard", ""), + }, + "keep-me": { + etag: cardMap.get("keep-me").getProperty("_etag", ""), + href: cardMap.get("keep-me").getProperty("_href", ""), + vCard: cardMap.get("keep-me").getProperty("_vCard", ""), + }, + }); + } catch (ex) { + Assert.ok(directory.readOnly, "read-write directory should not throw"); + } + + // Add a new card on the client. + + info("Adding a new card on the client."); + + try { + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + newCard.displayName = "I'm another new contact. ϔ"; + newCard.UID = "another-new"; + newCard = directory.addCard(newCard); + Assert.ok(!directory.readOnly, "read-only directory should throw"); + observer.checkAndClearNotifications({ + "addrbook-contact-created": ["another-new"], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": [], + }); + + Assert.equal( + await observer.waitFor("addrbook-contact-updated"), + "another-new" + ); + + newCard = directory.childCards.find(c => c.UID == "another-new"); + Assert.equal( + newCard.displayName, + "I'm another new contact. ϔ", + "non-ascii character survived the trip to the server" + ); + + await checkCardsOnServer({ + "another-new": { + etag: newCard.getProperty("_etag", ""), + href: newCard.getProperty("_href", ""), + vCard: newCard.getProperty("_vCard", ""), + }, + "change-me": { + etag: cardMap.get("change-me").getProperty("_etag", ""), + href: cardMap.get("change-me").getProperty("_href", ""), + vCard: cardMap.get("change-me").getProperty("_vCard", ""), + }, + "keep-me": { + etag: cardMap.get("keep-me").getProperty("_etag", ""), + href: cardMap.get("keep-me").getProperty("_href", ""), + vCard: cardMap.get("keep-me").getProperty("_vCard", ""), + }, + }); + } catch (ex) { + Assert.ok(directory.readOnly, "read-write directory should not throw"); + } + + info("Fourth sync with server. No changes expected."); + + await directory.updateAllFromServerV1(); + + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": [], + }); + + await clearDirectory(directory); + CardDAVServer.reset(); +} + +add_task(async function testNormal() { + await subtest(); +}); + +add_task(async function testYahoo() { + CardDAVServer.mimicYahoo = true; + await subtest(); + CardDAVServer.mimicYahoo = false; +}); + +add_task(async function testReadOnly() { + Services.prefs.setBoolPref("ldap_2.servers.carddav.readOnly", true); + await subtest(); + Services.prefs.clearUserPref("ldap_2.servers.carddav.readOnly"); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js new file mode 100644 index 0000000000..74f9c5ac88 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_cardDAV_syncV2.js @@ -0,0 +1,408 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +async function subtest() { + // Put some cards on the server. + CardDAVServer.putCardInternal( + "keep-me.vcf", + "BEGIN:VCARD\r\nUID:keep-me\r\nFN:I'm going to stay.\r\nEND:VCARD\r\n" + ); + CardDAVServer.putCardInternal( + "change-me.vcf", + // This one includes a character encoded with UTF-8. + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I'm going to be changed. \xCF\x9E\r\nEND:VCARD\r\n" + ); + CardDAVServer.putCardInternal( + "delete-me.vcf", + "BEGIN:VCARD\r\nUID:delete-me\r\nFN:I'm going to be deleted.\r\nEND:VCARD\r\n" + ); + + let directory = initDirectory(); + + // We'll only use this for the initial sync, so I think it's okay to use + // bulkAddCards and not get a notification for every contact. + info("Initial sync with server."); + await directory.fetchAllFromServer(); + + let lastSyncToken = directory._syncToken; + info(`Token is: ${lastSyncToken}`); + + info("Cards:"); + let cardMap = new Map(); + let oldETags = new Map(); + for (let card of directory.childCards) { + info( + ` ${card.displayName} [${card.getProperty( + "_href", + "" + )}, ${card.getProperty("_etag", "")}]` + ); + + cardMap.set(card.UID, card); + oldETags.set(card.UID, card.getProperty("_etag", "")); + } + + Assert.equal(cardMap.size, 3); + Assert.deepEqual([...cardMap.keys()].sort(), [ + "change-me", + "delete-me", + "keep-me", + ]); + Assert.equal( + cardMap.get("change-me").displayName, + "I'm going to be changed. Ϟ" + ); + + // Make some changes on the server. + + CardDAVServer.putCardInternal( + "change-me.vcf", + "BEGIN:VCARD\r\nUID:change-me\r\nFN:I've been changed.\r\nEND:VCARD\r\n" + ); + CardDAVServer.deleteCardInternal("delete-me.vcf"); + CardDAVServer.putCardInternal( + "new.vcf", + "BEGIN:VCARD\r\nUID:new\r\nFN:I'm new!\r\nEND:VCARD\r\n" + ); + + // Sync with the server. + + info("Second sync with server."); + + observer.init(); + await directory.updateAllFromServerV2(); + Assert.notEqual(directory._syncToken, lastSyncToken); + lastSyncToken = directory._syncToken; + observer.checkAndClearNotifications({ + "addrbook-contact-created": ["new"], + "addrbook-contact-updated": ["change-me"], + "addrbook-contact-deleted": ["delete-me"], + }); + + info("Cards:"); + cardMap.clear(); + for (let card of directory.childCards) { + info( + ` ${card.displayName} [${card.getProperty( + "_href", + "" + )}, ${card.getProperty("_etag", "")}]` + ); + + cardMap.set(card.UID, card); + } + + Assert.equal(cardMap.size, 3); + Assert.deepEqual([...cardMap.keys()].sort(), ["change-me", "keep-me", "new"]); + + Assert.equal( + cardMap.get("keep-me").getProperty("_etag", ""), + oldETags.get("keep-me") + ); + + Assert.equal(cardMap.get("change-me").displayName, "I've been changed."); + Assert.notEqual( + cardMap.get("change-me").getProperty("_etag", ""), + oldETags.get("change-me") + ); + oldETags.set("change-me", cardMap.get("change-me").getProperty("_etag", "")); + + Assert.equal(cardMap.get("new").displayName, "I'm new!"); + oldETags.set("new", cardMap.get("new").getProperty("_etag", "")); + + oldETags.delete("delete-me"); + + // Double-check that what we have matches what's on the server. + + await checkCardsOnServer({ + "change-me": { + etag: cardMap.get("change-me").getProperty("_etag", ""), + href: cardMap.get("change-me").getProperty("_href", ""), + vCard: cardMap.get("change-me").getProperty("_vCard", ""), + }, + "keep-me": { + etag: cardMap.get("keep-me").getProperty("_etag", ""), + href: cardMap.get("keep-me").getProperty("_href", ""), + vCard: cardMap.get("keep-me").getProperty("_vCard", ""), + }, + new: { + etag: cardMap.get("new").getProperty("_etag", ""), + href: cardMap.get("new").getProperty("_href", ""), + vCard: cardMap.get("new").getProperty("_vCard", ""), + }, + }); + + info("Third sync with server. No changes expected."); + + await directory.updateAllFromServerV2(); + // This time the token should NOT change, there's been no contact with the + // server since last time. + Assert.equal(directory._syncToken, lastSyncToken); + lastSyncToken = directory._syncToken; + + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": [], + }); + + // Delete a card on the client. + + info("Deleting a card on the client."); + + try { + directory.deleteCards([cardMap.get("new")]); + Assert.ok(!directory.readOnly, "read-only directory should throw."); + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": ["new"], + }); + + await checkCardsOnServer({ + "change-me": { + etag: cardMap.get("change-me").getProperty("_etag", ""), + href: cardMap.get("change-me").getProperty("_href", ""), + vCard: cardMap.get("change-me").getProperty("_vCard", ""), + }, + "keep-me": { + etag: cardMap.get("keep-me").getProperty("_etag", ""), + href: cardMap.get("keep-me").getProperty("_href", ""), + vCard: cardMap.get("keep-me").getProperty("_vCard", ""), + }, + }); + } catch (ex) { + Assert.ok(directory.readOnly, "read-write directory should not throw"); + } + + // Change a card on the client. + + info("Changing a card on the client."); + + try { + let changeMeCard = cardMap.get("change-me"); + changeMeCard.displayName = "I've been changed again!"; + directory.modifyCard(changeMeCard); + Assert.ok(!directory.readOnly, "read-only directory should throw."); + + Assert.equal( + await observer.waitFor("addrbook-contact-updated"), + "change-me" + ); + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": ["change-me"], + "addrbook-contact-deleted": [], + }); + + changeMeCard = directory.childCards.find(c => c.UID == "change-me"); + cardMap.set("change-me", changeMeCard); + + await checkCardsOnServer({ + "change-me": { + etag: changeMeCard.getProperty("_etag", ""), + href: changeMeCard.getProperty("_href", ""), + vCard: changeMeCard.getProperty("_vCard", ""), + }, + "keep-me": { + etag: cardMap.get("keep-me").getProperty("_etag", ""), + href: cardMap.get("keep-me").getProperty("_href", ""), + vCard: cardMap.get("keep-me").getProperty("_vCard", ""), + }, + }); + } catch (ex) { + Assert.ok(directory.readOnly, "read-write directory should not throw"); + } + + // Add a new card on the client. + + info("Adding a new card on the client."); + + try { + let newCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + newCard.displayName = "I'm another new contact. ϔ"; + newCard.UID = "another-new"; + newCard = directory.addCard(newCard); + Assert.ok(!directory.readOnly, "read-only directory should throw."); + observer.checkAndClearNotifications({ + "addrbook-contact-created": ["another-new"], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": [], + }); + + Assert.equal( + await observer.waitFor("addrbook-contact-updated"), + "another-new" + ); + + newCard = directory.childCards.find(c => c.UID == "another-new"); + Assert.equal( + newCard.displayName, + "I'm another new contact. ϔ", + "non-ascii character survived the trip to the server" + ); + + await checkCardsOnServer({ + "another-new": { + etag: newCard.getProperty("_etag", ""), + href: newCard.getProperty("_href", ""), + vCard: newCard.getProperty("_vCard", ""), + }, + "change-me": { + etag: cardMap.get("change-me").getProperty("_etag", ""), + href: cardMap.get("change-me").getProperty("_href", ""), + vCard: cardMap.get("change-me").getProperty("_vCard", ""), + }, + "keep-me": { + etag: cardMap.get("keep-me").getProperty("_etag", ""), + href: cardMap.get("keep-me").getProperty("_href", ""), + vCard: cardMap.get("keep-me").getProperty("_vCard", ""), + }, + }); + } catch (ex) { + Assert.ok(directory.readOnly, "read-write directory should not throw"); + } + + info("Fourth sync with server. No changes expected."); + + await directory.updateAllFromServerV2(); + if (directory.readOnly) { + Assert.equal(directory._syncToken, lastSyncToken); + } else { + Assert.notEqual(directory._syncToken, lastSyncToken); + } + + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": [], + }); + + await clearDirectory(directory); + CardDAVServer.reset(); +} + +add_task(async function testNormal() { + await subtest(); +}); + +add_task(async function testGoogle() { + CardDAVServer.mimicGoogle = true; + Services.prefs.setBoolPref("ldap_2.servers.carddav.carddav.vcard3", true); + await subtest(); + Services.prefs.clearUserPref("ldap_2.servers.carddav.carddav.vcard3"); + CardDAVServer.mimicGoogle = false; +}); + +add_task(async function testReadOnly() { + Services.prefs.setBoolPref("ldap_2.servers.carddav.readOnly", true); + await subtest(); + Services.prefs.clearUserPref("ldap_2.servers.carddav.readOnly"); +}); + +add_task(async function testExpiredToken() { + // Put some cards on the server. + CardDAVServer.putCardInternal( + "first.vcf", + "BEGIN:VCARD\r\nUID:first\r\nFN:First Person\r\nEND:VCARD\r\n" + ); + CardDAVServer.putCardInternal( + "second.vcf", + "BEGIN:VCARD\r\nUID:second\r\nFN:Second Person\r\nEND:VCARD\r\n" + ); + CardDAVServer.putCardInternal( + "third.vcf", + "BEGIN:VCARD\r\nUID:third\r\nFN:Third Person\r\nEND:VCARD\r\n" + ); + + let directory = initDirectory(); + + info("Initial sync with server."); + await directory.fetchAllFromServer(); + + info(`Token is: ${directory._syncToken}`); + + info("Cards:"); + for (let card of directory.childCards) { + info( + ` ${card.displayName} [${card.getProperty( + "_href", + "" + )}, ${card.getProperty("_etag", "")}]` + ); + } + + Assert.equal(directory.childCardCount, 3); + Assert.deepEqual(Array.from(directory.childCards, c => c.UID).sort(), [ + "first", + "second", + "third", + ]); + + // Corrupt the sync token. This will cause a 400 Bad Request response and a + // complete resync should happen. + + directory._syncToken = "wrong token"; + + // Make some changes on the server. + + CardDAVServer.putCardInternal( + "fourth.vcf", + "BEGIN:VCARD\r\nUID:fourth\r\nFN:Fourth\r\nEND:VCARD\r\n" + ); + CardDAVServer.putCardInternal( + "second.vcf", + "BEGIN:VCARD\r\nUID:second\r\nFN:Second Person, but different\r\nEND:VCARD\r\n" + ); + CardDAVServer.deleteCardInternal("first.vcf"); + + // Sync with the server. + + info("Sync with server."); + + let notificationPromise = TestUtils.topicObserved( + "addrbook-directory-invalidated" + ); + observer.init(); + await directory.updateAllFromServerV2(); + // Check what notifications were fired. There should be an "invalidated" + // notification, making the others redundant, but the "deleted" + // notification is hard to avoid. + observer.checkAndClearNotifications({ + "addrbook-contact-created": [], + "addrbook-contact-updated": [], + "addrbook-contact-deleted": ["first"], + }); + await notificationPromise; + + info(`Token is now: ${directory._syncToken}`); + + info("Cards:"); + for (let card of directory.childCards) { + info( + ` ${card.displayName} [${card.getProperty( + "_href", + "" + )}, ${card.getProperty("_etag", "")}]` + ); + } + + // Check that the changes were synced. + + Assert.equal(directory.childCardCount, 3); + Assert.deepEqual(Array.from(directory.childCards, c => c.UID).sort(), [ + "fourth", + "second", + "third", + ]); + Assert.equal( + directory.childCards.find(c => c.UID == "second").displayName, + "Second Person, but different" + ); + + await clearDirectory(directory); + CardDAVServer.reset(); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_cardForEmail.js b/comm/mailnews/addrbook/test/unit/test_cardForEmail.js new file mode 100644 index 0000000000..3e7a53f339 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_cardForEmail.js @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Tests nsIAbDirectory::cardForEmailAddress + * - checks correct return when no email address supplied + * - checks correct return when no matching email address supplied + * - checks correct return when matching email address supplied. + * + * Uses: cardForEmail.mab + */ + +function check_correct_card(card) { + Assert.ok(!!card); + + Assert.equal(card.firstName, "FirstName1"); + Assert.equal(card.lastName, "LastName1"); + Assert.equal(card.displayName, "DisplayName1"); + Assert.deepEqual(card.emailAddresses, [ + "PrimaryEmail1@test.invalid", + "SecondEmail1\u00D0@test.invalid", + ]); +} + +function run_test() { + loadABFile("data/cardForEmail", kPABData.fileName); + + // Test - Get the directory + let AB = MailServices.ab.getDirectory(kPABData.URI); + + // Test - Check that a null string succeeds and does not + // return a card (bug 404264) + Assert.ok(AB.cardForEmailAddress(null) == null); + + // Test - Check that an empty string succeeds and does not + // return a card (bug 404264) + Assert.ok(AB.cardForEmailAddress("") == null); + + // Test - Check that we don't match an email that doesn't exist + Assert.ok(AB.cardForEmailAddress("nocard@this.email.invalid") == null); + + // Test - Check that we match this email and some of the fields + // of the card are correct. + var card = AB.cardForEmailAddress("PrimaryEmail1@test.invalid"); + + check_correct_card(card); + + // Test - Check that we match with the primary email with insensitive case. + card = AB.cardForEmailAddress("pRimaryemAIL1@teST.invalid"); + + check_correct_card(card); + + // Test - Check that we match with the second email. + card = AB.cardForEmailAddress("SecondEmail1\u00D0@test.invalid"); + + check_correct_card(card); + + // Test - Check that we match with the second email with insensitive case. + card = AB.cardForEmailAddress("SECondEMail1\u00D0@TEST.inValid"); + + check_correct_card(card); + + // Check that we match cards that have more than two email addresses. + card = AB.cardForEmailAddress("first@SOMETHING.invalid"); + Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1"); + Assert.equal(card.displayName, "A vCard!"); + + card = AB.cardForEmailAddress("second@something.INVALID"); + Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1"); + Assert.equal(card.displayName, "A vCard!"); + + card = AB.cardForEmailAddress("THIRD@something.invalid"); + Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1"); + Assert.equal(card.displayName, "A vCard!"); + + card = AB.cardForEmailAddress("FOURTH@SOMETHING.INVALID"); + Assert.equal(card.UID, "f68fbac4-158b-4bdc-95c6-592a5f93cfa1"); + Assert.equal(card.displayName, "A vCard!"); + + card = AB.cardForEmailAddress("A vCard!"); + Assert.equal(card, null); + + // Check getCardFromProperty returns null correctly for non-extant properties + Assert.equal(AB.getCardFromProperty("NickName", "", false), null); + Assert.equal(AB.getCardFromProperty("NickName", "NickName", false), null); + + // Check case-insensitive searching works + card = AB.getCardFromProperty("NickName", "NickName1", true); + check_correct_card(card); + card = AB.getCardFromProperty("NickName", "NickName1", false); + check_correct_card(card); + + Assert.equal(AB.getCardFromProperty("NickName", "nickName1", true), null); + + card = AB.getCardFromProperty("NickName", "nickName1", false); + check_correct_card(card); + + var cards = AB.getCardsFromProperty("LastName", "DOE", true); + Assert.equal(cards.length, 0); + + cards = AB.getCardsFromProperty("LastName", "Doe", true); + var i = 0; + var data = ["John", "Jane"]; + + for (card of cards) { + i++; + Assert.equal(card.lastName, "Doe"); + var index = data.indexOf(card.firstName); + Assert.notEqual(index, -1); + delete data[index]; + } + Assert.equal(i, 2); +} diff --git a/comm/mailnews/addrbook/test/unit/test_collection.js b/comm/mailnews/addrbook/test/unit/test_collection.js new file mode 100644 index 0000000000..720f28c246 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_collection.js @@ -0,0 +1,404 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test suite for the Address Collector Service. + * + * This tests the main collection functions for adding new cards and modifying + * existing ones. + * + * Tests against cards in different ABs are done in test_collection_2.js. + */ + +// Source fields (emailHeader) and expected results for use for +// testing the addition of new addresses to the database. +// +// Note: these email addresses should be different to allow collecting an +// address to add a different card each time. +var addEmailChecks = + // First 3 items aimed at basic collection and mail format. + [ + { + emailHeader: "test0@foo.invalid", + primaryEmail: "test0@foo.invalid", + displayName: "", + firstName: "", + lastName: "", + screenName: "", + }, + { + emailHeader: "test1@foo.invalid", + primaryEmail: "test1@foo.invalid", + displayName: "", + firstName: "", + lastName: "", + screenName: "", + }, + { + emailHeader: "test2@foo.invalid", + primaryEmail: "test2@foo.invalid", + displayName: "", + firstName: "", + lastName: "", + screenName: "", + }, + // UTF-8 based addresses (bug 407564) + { + emailHeader: "test0@\u00D0.invalid", + primaryEmail: "test0@\u00D0.invalid", + displayName: "", + firstName: "", + lastName: "", + screenName: "", + }, + { + emailHeader: "test0\u00D0@foo.invalid", + primaryEmail: "test0\u00D0@foo.invalid", + displayName: "", + firstName: "", + lastName: "", + screenName: "", + }, + // Screen names + { + emailHeader: "invalid\u00D00@aol.com", + primaryEmail: "invalid\u00D00@aol.com", + displayName: "", + firstName: "", + lastName: "", + screenName: "invalid\u00D00", + }, + { + emailHeader: "invalid1\u00D00@cs.com", + primaryEmail: "invalid1\u00D00@cs.com", + displayName: "", + firstName: "", + lastName: "", + screenName: "invalid1\u00D00", + }, + { + emailHeader: "invalid2\u00D00@netscape.net", + primaryEmail: "invalid2\u00D00@netscape.net", + displayName: "", + firstName: "", + lastName: "", + screenName: "invalid2\u00D00", + }, + // Collection of names + { + emailHeader: "Test User <test3@foo.invalid>", + primaryEmail: "test3@foo.invalid", + displayName: "Test User", + firstName: "Test", + lastName: "User", + screenName: "", + }, + { + emailHeader: "Test <test4@foo.invalid>", + primaryEmail: "test4@foo.invalid", + displayName: "Test", + firstName: "", + lastName: "", + screenName: "", + }, + // Collection of names with UTF-8 specific items + { + emailHeader: "Test\u00D0 User <test5@foo.invalid>", + primaryEmail: "test5@foo.invalid", + displayName: "Test\u00D0 User", + firstName: "Test\u00D0", + lastName: "User", + screenName: "", + }, + { + emailHeader: "Test\u00D0 <test6@foo.invalid>", + primaryEmail: "test6@foo.invalid", + displayName: "Test\u00D0", + firstName: "", + lastName: "", + screenName: "", + }, + ]; + +// Source fields (emailHeader) and expected results for use for +// testing the modification of cards in the database. +// +// Note: these sets re-use some of the ones for ease of definition. +var modifyEmailChecks = + // No display name/other details. Add details and modify mail format. + [ + { + emailHeader: "Modify User\u00D0 <test0@\u00D0.invalid>", + primaryEmail: "test0@\u00D0.invalid", + displayName: "Modify User\u00D0", + firstName: "Modify", + lastName: "User\u00D0", + screenName: "", + }, + { + emailHeader: "Modify <test0\u00D0@foo.invalid>", + primaryEmail: "test0\u00D0@foo.invalid", + displayName: "Modify", + firstName: "", + lastName: "", + screenName: "", + }, + // No modification of existing cards with display names + { + emailHeader: "Modify2 User\u00D02 <test0@\u00D0.invalid>", + primaryEmail: "test0@\u00D0.invalid", + displayName: "Modify User\u00D0", + firstName: "Modify", + lastName: "User\u00D0", + screenName: "", + }, + { + emailHeader: "Modify3 <test0\u00D0@foo.invalid>", + primaryEmail: "test0\u00D0@foo.invalid", + displayName: "Modify", + firstName: "", + lastName: "", + screenName: "", + }, + // Check no modification of cards for mail format where format is not + // "unknown". + { + emailHeader: "Modify User\u00D0 <test0@\u00D0.invalid>", + primaryEmail: "test0@\u00D0.invalid", + displayName: "Modify User\u00D0", + firstName: "Modify", + lastName: "User\u00D0", + screenName: "", + }, + { + emailHeader: "Modify <test0\u00D0@foo.invalid>", + primaryEmail: "test0\u00D0@foo.invalid", + displayName: "Modify", + firstName: "", + lastName: "", + screenName: "", + }, + // No modification of cards with email in second email address. + { + emailHeader: "Modify Secondary <usersec\u00D0@foo.invalid>", + primaryEmail: "userprim\u00D0@foo.invalid", + secondEmail: "usersec\u00D0@foo.invalid", + displayName: "", + firstName: "", + lastName: "", + screenName: "", + }, + { + emailHeader: "Modify <usersec\u00D0@foo.invalid>", + primaryEmail: "userprim\u00D0@foo.invalid", + secondEmail: "usersec\u00D0@foo.invalid", + displayName: "", + firstName: "", + lastName: "", + screenName: "", + }, + ]; + +var collectChecker = { + addressCollect: null, + AB: null, + part: 0, + + checkAddress(aDetails) { + info("checkAddress: " + aDetails.emailHeader); + try { + this.addressCollect.collectAddress(aDetails.emailHeader, true); + + this.checkCardResult(aDetails, false); + } catch (e) { + throw new Error( + "FAILED in checkAddress emailHeader: " + + aDetails.emailHeader + + " part: " + + this.part + + " : " + + e + ); + } + ++this.part; + }, + + checkAll(aDetailsArray) { + try { + // Formulate the string to add. + var emailHeader = ""; + var i; + + for (i = 0; i < aDetailsArray.length - 1; ++i) { + emailHeader += aDetailsArray[i].emailHeader + ", "; + } + + emailHeader += aDetailsArray[aDetailsArray.length - 1].emailHeader; + + // Now add it. In this case we just set the Mail format Type to unknown. + this.addressCollect.collectAddress(emailHeader, true); + + for (i = 0; i < aDetailsArray.length; ++i) { + this.checkCardResult(aDetailsArray[i], true); + } + } catch (e) { + throw new Error("FAILED in checkAll item: " + i + " : " + e); + } + }, + + checkCardResult(aDetails) { + info("checkCardResult: " + aDetails.emailHeader); + try { + var card = this.AB.cardForEmailAddress(aDetails.primaryEmail); + + Assert.ok(card != null); + + if ("secondEmail" in aDetails) { + Assert.equal(card.emailAddresses[1], aDetails.secondEmail); + } + + Assert.equal(card.displayName, aDetails.displayName); + Assert.equal(card.firstName, aDetails.firstName); + Assert.equal(card.lastName, aDetails.lastName); + Assert.equal(card.getProperty("_AimScreenName", ""), aDetails.screenName); + } catch (e) { + throw new Error( + "FAILED in checkCardResult emailHeader: " + + aDetails.emailHeader + + " : " + + e + ); + } + }, +}; + +function run_test() { + // Test - Get the address collecter + + // XXX Getting all directories ensures we create all ABs because the + // address collecter can't currently create ABs itself (bug 314448). + MailServices.ab.directories; + + // Get the actual AB for the collector so we can check cards have been + // added. + collectChecker.AB = MailServices.ab.getDirectory( + Services.prefs.getCharPref("mail.collect_addressbook") + ); + + // Get the actual collecter + collectChecker.addressCollect = Cc[ + "@mozilla.org/addressbook/services/addressCollector;1" + ].getService(Ci.nsIAbAddressCollector); + + // Test - Addition of header without email address. + + collectChecker.addressCollect.collectAddress("MyTest <>", true); + + // Address book should have no cards present. + Assert.equal(collectChecker.AB.childCards.length, 0); + + // Test - Email doesn't exist, but don't add it. + + // As we've just set everything up, we know we haven't got anything in the + // AB, so just try and collect without adding. + collectChecker.addressCollect.collectAddress( + addEmailChecks[0].emailHeader, + false + ); + + var card = collectChecker.AB.cardForEmailAddress( + addEmailChecks[0].emailHeader + ); + + Assert.ok(card == null); + + // Test - Try and collect various emails and formats. + + collectChecker.part = 0; + + addEmailChecks.forEach(collectChecker.checkAddress, collectChecker); + + // Test - Do all emails at the same time. + + // First delete all existing cards + collectChecker.AB.deleteCards(collectChecker.AB.childCards); + + // Address book should have no cards present. + Assert.equal(collectChecker.AB.childCards.length, 0); + + Assert.equal( + collectChecker.AB.cardForEmailAddress(addEmailChecks[0].emailHeader), + null + ); + + // Now do all emails at the same time. + collectChecker.checkAll(addEmailChecks); + + // Test - Try and modify various emails and formats. + + // Add a basic card with just primary and second email to allow testing + // of the case where we don't modify when second email is matching. + card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + + card.primaryEmail = "userprim\u00D0@foo.invalid"; + card.setProperty("SecondEmail", "usersec\u00D0@foo.invalid"); + + collectChecker.AB.addCard(card); + + collectChecker.part = 0; + + modifyEmailChecks.forEach(collectChecker.checkAddress, collectChecker); + + // Test collectSingleAddress - Note: because the above tests test + // collectAddress which we know calls collectSingleAddress, we only need to + // test the case where aSkipCheckExisting is true. + + // Add an email that is already there and check we get two instances of it in + // the AB. + + const kSingleAddress = + modifyEmailChecks[modifyEmailChecks.length - 1].primaryEmail; + const kSingleDisplayName = "Test Single"; + + collectChecker.addressCollect.collectSingleAddress( + kSingleAddress, + kSingleDisplayName, + true, + true + ); + + // Try collecting the same address in another case. This shouldn't create any + // new card. + collectChecker.addressCollect.collectSingleAddress( + kSingleAddress.toUpperCase(), + kSingleDisplayName, + true, + true + ); + + var foundCards = []; + + for (card of collectChecker.AB.childCards) { + if (card.primaryEmail == kSingleAddress) { + foundCards.push(card); + } + } + + Assert.equal(foundCards.length, 2); + + if ( + foundCards[0].displayName != kSingleDisplayName && + foundCards[1].displayName != kSingleDisplayName + ) { + do_throw("Error, collectSingleCard didn't create a new card"); + } + + if (foundCards[0].displayName != "" && foundCards[1].displayName != "") { + do_throw( + "Error, collectSingleCard created ok, but other card does not exist" + ); + } +} diff --git a/comm/mailnews/addrbook/test/unit/test_collection_2.js b/comm/mailnews/addrbook/test/unit/test_collection_2.js new file mode 100644 index 0000000000..bff3d6e916 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_collection_2.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Test suite for the Address Collector Service part 2. + * + * This test checks that we don't collect addresses when they already exist + * in other address books. + */ + +function run_test() { + // Test - Get the address collector + loadABFile("data/collect", kPABData.fileName); + + // Get the actual collector + var addressCollect = Cc[ + "@mozilla.org/addressbook/services/addressCollector;1" + ].getService(Ci.nsIAbAddressCollector); + + // Set the new pref afterwards to ensure we change correctly + Services.prefs.setCharPref("mail.collect_addressbook", kCABData.URI); + + // XXX Getting all directories ensures we create all ABs because the + // address collector can't currently create ABs itself (bug 314448). + MailServices.ab.directories; + + addressCollect.collectAddress("Other Book <other@book.invalid>", true); + + let PAB = MailServices.ab.getDirectory(kPABData.URI); + + var cards = PAB.childCards; + + Assert.equal(cards.length, 1); + + Assert.equal(cards[0].displayName, "Other Book"); + Assert.equal(cards[0].primaryEmail, "other@book.invalid"); + + // Check the CAB has no cards. + let CAB = MailServices.ab.getDirectory(kCABData.URI); + Assert.equal(CAB.childCards.length, 0); +} diff --git a/comm/mailnews/addrbook/test/unit/test_convertOnSave.js b/comm/mailnews/addrbook/test/unit/test_convertOnSave.js new file mode 100644 index 0000000000..da26ffe56c --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_convertOnSave.js @@ -0,0 +1,329 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Tests that any card added to an AddrBookDirectory is stored as a vCard. +// Some properties are also recorded outside the vCard for performance reasons +// and/or searching. + +// Each type of card is saved and checked twice: once with its own UID and +// again with a new UID. This ensures that UIDs are appropriately stored. + +var { AddrBookCard } = ChromeUtils.import( + "resource:///modules/AddrBookCard.jsm" +); +var { SQLiteDirectory } = ChromeUtils.import( + "resource:///modules/SQLiteDirectory.jsm" +); +var { VCardPropertyEntry } = ChromeUtils.import( + "resource:///modules/VCardUtils.jsm" +); + +Services.prefs.setStringPref( + "ldap_2.servers.conversion.filename", + "conversion.sqlite" +); + +var book = new SQLiteDirectory(); +book.init("jsaddrbook://conversion.sqlite"); + +/** Tests an instance of nsAbCardProperty. */ +add_task(function testCardProperty() { + let cardProperty = Cc[ + "@mozilla.org/addressbook/cardproperty;1" + ].createInstance(Ci.nsIAbCard); + cardProperty.UID = "99999999-8888-7777-6666-555555555555"; + cardProperty.displayName = "display name"; + cardProperty.firstName = "first"; + cardProperty.lastName = "last"; + cardProperty.primaryEmail = "primary@email"; + cardProperty.setProperty("SecondEmail", "second@email"); + cardProperty.setProperty("NickName", "nick"); + cardProperty.setProperty("FaxNumber", "1234567"); + cardProperty.setProperty("BirthYear", 2001); + cardProperty.setProperty("BirthMonth", 1); + cardProperty.setProperty("BirthDay", 1); + cardProperty.setProperty("FakeProperty", "fake value"); + + saveCardAndTest(cardProperty, false); + saveCardAndTest(cardProperty, true); +}); + +/** + * Tests an instance of AddrBookCard, populated in the same way that card are + * created from storage. This instance *doesn't* contain a vCard, and + * is therefore the same as a card that hasn't yet been migrated to vCard. + */ +add_task(function testABCard() { + let abCard = new AddrBookCard(); + abCard._uid = "99999999-8888-7777-6666-555555555555"; + abCard._properties = new Map([ + ["PopularityIndex", 0], // NO + ["DisplayName", "display name"], + ["FirstName", "first"], + ["LastName", "last"], + ["PrimaryEmail", "primary@email"], + ["SecondEmail", "second@email"], + ["NickName", "nick"], + ["FaxNumber", "1234567"], + ["BirthYear", 2001], + ["BirthMonth", 1], + ["BirthDay", 1], + ["FakeProperty", "fake value"], + ]); + + saveCardAndTest(abCard, false); + saveCardAndTest(abCard, true); +}); + +/** + * Tests an instance of AddrBookCard, populated in the same way that card are + * created from storage. This instance *does* contain a vCard. + */ +add_task(function testABCardWithVCard() { + let abCard = new AddrBookCard(); + abCard._uid = "99999999-8888-7777-6666-555555555555"; + abCard._properties = new Map([ + ["PopularityIndex", 0], // NO + ["DisplayName", "display name"], + ["FirstName", "first"], + ["LastName", "last"], + ["PrimaryEmail", "primary@email"], + ["SecondEmail", "second@email"], + ["NickName", "nick"], + ["FakeProperty", "fake value"], + [ + "_vCard", + formatVCard` + BEGIN:VCARD + VERSION:4.0 + EMAIL;PREF=1:primary@email + EMAIL:second@email + FN:display name + NICKNAME:nick + BDAY;VALUE=DATE:20010101 + N:last;first;;; + TEL;TYPE=fax;VALUE=TEXT:1234567 + UID:99999999-8888-7777-6666-555555555555 + END:VCARD + `, + ], + ]); + + saveCardAndTest(abCard, false); + saveCardAndTest(abCard, true); +}); + +/** + * Tests an instance of AddrBookCard, populated in the same way that card are + * created from storage. This instance *does* contain a vCard. + */ +add_task(function testABCardWithVCardOnly() { + let abCard = new AddrBookCard(); + abCard._uid = "99999999-8888-7777-6666-555555555555"; + abCard._properties = new Map([ + ["FakeProperty", "fake value"], // NO + ["PopularityIndex", 0], // NO + [ + "_vCard", + formatVCard` + BEGIN:VCARD + VERSION:4.0 + EMAIL;PREF=1:primary@email + EMAIL:second@email + FN:display name + NICKNAME:nick + BDAY;VALUE=DATE:20010101 + N:last;first;;; + TEL;TYPE=fax;VALUE=TEXT:1234567 + UID:99999999-8888-7777-6666-555555555555 + END:VCARD + `, + ], + ]); + + saveCardAndTest(abCard, false); + saveCardAndTest(abCard, true); +}); + +/** + * Tests an instance of AddrBookCard, populated in the same way that card are + * created from storage. This instance *does* contain a vCard, but also some + * properties that shouldn't exist because their value is stored in the vCard. + */ +add_task(function testABCardWithVCardAndExtraProps() { + let abCard = new AddrBookCard(); + abCard._uid = "99999999-8888-7777-6666-555555555555"; + abCard._properties = new Map([ + ["PopularityIndex", 0], // NO + ["DisplayName", "display name"], + ["FirstName", "first"], + ["LastName", "last"], + ["PrimaryEmail", "primary@email"], + ["SecondEmail", "second@email"], + ["NickName", "nick"], + ["FaxNumber", "1234567"], + ["BirthYear", 2001], + ["BirthMonth", 1], + ["BirthDay", 1], + ["FakeProperty", "fake value"], + [ + "_vCard", + formatVCard` + BEGIN:VCARD + VERSION:4.0 + EMAIL;PREF=1:primary@email + EMAIL:second@email + FN:display name + NICKNAME:nick + BDAY;VALUE=DATE:20010101 + N:last;first;;; + TEL;TYPE=fax;VALUE=TEXT:1234567 + UID:99999999-8888-7777-6666-555555555555 + END:VCARD + `, + ], + ]); + + saveCardAndTest(abCard, false); + saveCardAndTest(abCard, true); +}); + +/** Tests an instance of AddrBookCard, created from scratch. */ +add_task(function testABCardConstructed() { + let abCard = new AddrBookCard(); + abCard.UID = "99999999-8888-7777-6666-555555555555"; + abCard.displayName = "display name"; + abCard.firstName = "first"; + abCard.lastName = "last"; + abCard.primaryEmail = "primary@email"; + abCard.vCardProperties.addValue("email", "second@email"); + abCard.vCardProperties.addValue("nickname", "nick"); + abCard.vCardProperties.addEntry( + new VCardPropertyEntry("tel", { type: "fax" }, "text", "1234567") + ); + abCard.vCardProperties.addEntry( + new VCardPropertyEntry("bday", {}, "date", "20010101") + ); + abCard.setProperty("FakeProperty", "fake value"); + + saveCardAndTest(abCard, false); + saveCardAndTest(abCard, true); +}); + +/** Tests an instance of AddrBookCard, created from scratch. */ +add_task(function testABCardConstructionThrows() { + let abCard = new AddrBookCard(); + abCard.UID = "99999999-8888-7777-6666-555555555555"; + abCard.displayName = "display name"; + abCard.firstName = "first"; + abCard.lastName = "last"; + abCard.primaryEmail = "primary@email"; + // these properties will be forgotten + Assert.throws( + () => abCard.setProperty("SecondEmail", "second@email"), + /Unable to set SecondEmail as a property/ + ); + Assert.throws( + () => abCard.setProperty("NickName", "nick"), + /Unable to set NickName as a property/ + ); + Assert.throws( + () => abCard.setProperty("FaxNumber", "1234567"), + /Unable to set FaxNumber as a property/ + ); + Assert.throws( + () => abCard.setProperty("BirthYear", 2001), + /Unable to set BirthYear as a property/ + ); + Assert.throws( + () => abCard.setProperty("BirthMonth", 1), + /Unable to set BirthMonth as a property/ + ); + Assert.throws( + () => abCard.setProperty("BirthDay", 1), + /Unable to set BirthDay as a property/ + ); + abCard.setProperty("FakeProperty", "fake value"); +}); + +function saveCardAndTest(card, useNewUID) { + info(`Saving the card ${useNewUID ? "with" : "without"} a new UID`); + + Assert.equal(book.childCardCount, 0); + + let savedCard = book.dropCard(card, useNewUID); + Assert.deepEqual(Array.from(savedCard.properties, p => p.name).sort(), [ + "DisplayName", + "FakeProperty", + "FirstName", + "LastModifiedDate", + "LastName", + "NickName", + "PopularityIndex", + "PrimaryEmail", + "SecondEmail", + "_vCard", + ]); + + if (useNewUID) { + Assert.notEqual(savedCard.UID, "99999999-8888-7777-6666-555555555555"); + } else { + Assert.equal(savedCard.UID, "99999999-8888-7777-6666-555555555555"); + } + + Assert.equal(savedCard.getProperty("DisplayName", "WRONG"), "display name"); + Assert.equal(savedCard.getProperty("FirstName", "WRONG"), "first"); + Assert.equal(savedCard.getProperty("LastName", "WRONG"), "last"); + Assert.equal(savedCard.getProperty("PrimaryEmail", "WRONG"), "primary@email"); + Assert.equal(savedCard.getProperty("SecondEmail", "WRONG"), "second@email"); + Assert.equal(savedCard.getProperty("NickName", "WRONG"), "nick"); + Assert.equal(savedCard.getProperty("FakeProperty", "WRONG"), "fake value"); + Assert.equal(savedCard.getProperty("PopularityIndex", "WRONG"), "0"); + + let vCard = savedCard.getProperty("_vCard", "WRONG"); + Assert.stringContains(vCard, "\r\nEMAIL;PREF=1:primary@email\r\n"); + Assert.stringContains(vCard, "\r\nEMAIL:second@email\r\n"); + Assert.stringContains(vCard, "\r\nFN:display name\r\n"); + Assert.stringContains(vCard, "\r\nNICKNAME:nick\r\n"); + Assert.stringContains(vCard, "\r\nBDAY;VALUE=DATE:20010101\r\n"); + Assert.stringContains(vCard, "\r\nN:last;first;;;\r\n"); + Assert.stringContains(vCard, "\r\nTEL;TYPE=fax;VALUE=TEXT:1234567\r\n"); + Assert.stringContains(vCard, `\r\nUID:${savedCard.UID}\r\n`); + + let modifiedDate = parseInt( + savedCard.getProperty("LastModifiedDate", ""), + 10 + ); + Assert.lessOrEqual(modifiedDate, Date.now() / 1000); + Assert.greater(modifiedDate, Date.now() / 1000 - 10); + + Assert.equal(savedCard.displayName, "display name"); + Assert.equal(savedCard.firstName, "first"); + Assert.equal(savedCard.lastName, "last"); + Assert.equal(savedCard.primaryEmail, "primary@email"); + Assert.deepEqual(savedCard.emailAddresses, ["primary@email", "second@email"]); + + Assert.ok(savedCard.supportsVCard); + Assert.ok(savedCard.vCardProperties); + + Assert.deepEqual(savedCard.vCardProperties.getAllValues("fn"), [ + "display name", + ]); + Assert.deepEqual(savedCard.vCardProperties.getAllValues("email"), [ + "primary@email", + "second@email", + ]); + Assert.deepEqual(savedCard.vCardProperties.getAllValues("nickname"), [ + "nick", + ]); + Assert.deepEqual(savedCard.vCardProperties.getAllValues("bday"), [ + "2001-01-01", + ]); + Assert.deepEqual(savedCard.vCardProperties.getAllValues("n"), [ + ["last", "first", "", "", ""], + ]); + Assert.deepEqual(savedCard.vCardProperties.getAllValues("tel"), ["1234567"]); + + book.deleteCards(book.childCards); +} diff --git a/comm/mailnews/addrbook/test/unit/test_db_enumerator.js b/comm/mailnews/addrbook/test/unit/test_db_enumerator.js new file mode 100644 index 0000000000..50fe8b7d06 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_db_enumerator.js @@ -0,0 +1,89 @@ +/** + * This test verifies that we don't crash if we have an enumerator on an + * addr database and delete the underlying directory, which forces the ab + * closed. + */ +var ab_prefix = "test-537815-"; +var card_properties = { + FirstName: "01-first-3", + LastName: "02-last", + PrimaryEmail: "08-email-1@zindus.invalid", +}; +var max_addressbooks = 10; + +function bug_537815_fixture_setup() { + let i, key; + + for (i = 1; i <= max_addressbooks; i++) { + let ab_name = ab_prefix + i; + MailServices.ab.newAddressBook( + ab_name, + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + dump("created: " + ab_name + "\n"); + + for (var j = 1; j < 2; j++) { + for (let elem of MailServices.ab.directories) { + let uri = elem.URI; + let dir = MailServices.ab.getDirectory(uri); + + dump("considering: j: " + j + " " + elem.dirName + "\n"); + + if (j == 1 && elem.dirName.startsWith(ab_prefix)) { + for (i = 1; i <= 1000; i++) { + let abCard = Cc["@mozilla.org/addressbook/cardproperty;1"] + .createInstance() + .QueryInterface(Ci.nsIAbCard); + + for (key in card_properties) { + abCard.setProperty(key, card_properties[key]); + } + + abCard = dir.addCard(abCard); + } + dump("populated: " + elem.dirName + "\n"); + } + } + } + } +} + +function bug_537815_test() { + for (let elem of MailServices.ab.directories) { + let uri = elem.URI; + let dir = MailServices.ab.getDirectory(uri); + if (elem.dirName.startsWith(ab_prefix)) { + for (let abCard of dir.childCards) { + for (let key in card_properties) { + abCard.getProperty(key, null); + } + } + dump("visited all cards in: " + elem.dirName + "\n"); + } + } +} + +function test_bug_537815() { + bug_537815_fixture_setup(); + bug_537815_test(); + bug_537815_fixture_tear_down(); +} + +function bug_537815_fixture_tear_down() { + let a_uri = {}; + for (let elem of MailServices.ab.directories) { + if (elem.dirName.startsWith(ab_prefix)) { + a_uri[elem.URI] = true; + dump("to be deleted: " + elem.dirName + "\n"); + } + } + + for (let uri in a_uri) { + MailServices.ab.deleteAddressBook(uri); + } +} + +function run_test() { + test_bug_537815(); +} diff --git a/comm/mailnews/addrbook/test/unit/test_delete_book.js b/comm/mailnews/addrbook/test/unit/test_delete_book.js new file mode 100644 index 0000000000..8c63bb43b0 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_delete_book.js @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +function getExistingDirectories() { + return MailServices.ab.directories.map(d => d.dirPrefId); +} + +add_task(async function clearPref() { + Assert.deepEqual(getExistingDirectories(), [ + "ldap_2.servers.pab", + "ldap_2.servers.history", + ]); + equal( + Services.prefs.getStringPref("mail.collect_addressbook"), + "jsaddrbook://history.sqlite" + ); + + let dirPrefId = MailServices.ab.newAddressBook( + "delete me", + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + let book = MailServices.ab.getDirectoryFromId(dirPrefId); + + Assert.deepEqual(getExistingDirectories(), [ + "ldap_2.servers.pab", + "ldap_2.servers.deleteme", + "ldap_2.servers.history", + ]); + Services.prefs.setStringPref("mail.collect_addressbook", book.URI); + + await promiseDirectoryRemoved(book.URI); + + Assert.deepEqual(getExistingDirectories(), [ + "ldap_2.servers.pab", + "ldap_2.servers.history", + ]); + equal( + Services.prefs.getStringPref("mail.collect_addressbook"), + "jsaddrbook://history.sqlite" + ); +}); + +add_task(async function protectBuiltIns() { + Assert.deepEqual(getExistingDirectories(), [ + "ldap_2.servers.pab", + "ldap_2.servers.history", + ]); + equal( + Services.prefs.getStringPref("mail.collect_addressbook"), + "jsaddrbook://history.sqlite" + ); + + Assert.throws(() => { + MailServices.ab.deleteAddressBook("this is completely wrong"); + }, /NS_ERROR_MALFORMED_URI/); + Assert.throws(() => { + MailServices.ab.deleteAddressBook("jsaddrbook://bad.sqlite"); + }, /NS_ERROR_UNEXPECTED/); + Assert.throws(() => { + MailServices.ab.deleteAddressBook("jsaddrbook://history.sqlite"); + }, /NS_ERROR_FAILURE/); + Assert.throws(() => { + MailServices.ab.deleteAddressBook("jsaddrbook://abook.sqlite"); + }, /NS_ERROR_FAILURE/); + + Assert.deepEqual(getExistingDirectories(), [ + "ldap_2.servers.pab", + "ldap_2.servers.history", + ]); + equal( + Services.prefs.getStringPref("mail.collect_addressbook"), + "jsaddrbook://history.sqlite" + ); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_export.js b/comm/mailnews/addrbook/test/unit/test_export.js new file mode 100644 index 0000000000..34874e2f69 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_export.js @@ -0,0 +1,156 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { AddrBookCard } = ChromeUtils.import( + "resource:///modules/AddrBookCard.jsm" +); +var { AddrBookUtils } = ChromeUtils.import( + "resource:///modules/AddrBookUtils.jsm" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { VCardPropertyEntry } = ChromeUtils.import( + "resource:///modules/VCardUtils.jsm" +); + +async function subtest(cardConstructor) { + let dirPrefId = MailServices.ab.newAddressBook( + "new book", + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + let book = MailServices.ab.getDirectoryFromId(dirPrefId); + + let contact1 = cardConstructor(); + contact1.UID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; + contact1.displayName = "contact number one"; + contact1.firstName = "contact"; + contact1.lastName = "one"; + contact1.primaryEmail = "contact1@invalid"; + contact1 = book.addCard(contact1); + + let contact2 = cardConstructor(); + contact2.UID = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"; + contact2.displayName = "contact number two"; + contact2.firstName = "contact"; + contact2.lastName = "two"; + contact2.primaryEmail = "contact2@invalid"; + if (contact2.supportsVCard) { + contact2.vCardProperties.addValue("title", `"worker"`); + contact2.vCardProperties.addValue("note", "here's some unicode text…"); + contact2.vCardProperties.addEntry( + new VCardPropertyEntry("x-custom1", {}, "text", "custom, 1") + ); + contact2.vCardProperties.addEntry( + new VCardPropertyEntry("x-custom2", {}, "text", "custom\t2") + ); + contact2.vCardProperties.addEntry( + new VCardPropertyEntry("x-custom3", {}, "text", "custom\r3") + ); + contact2.vCardProperties.addEntry( + new VCardPropertyEntry("x-custom4", {}, "text", "custom\n4") + ); + } else { + contact2.setProperty("JobTitle", `"worker"`); + contact2.setProperty("Notes", "here's some unicode text…"); + contact2.setProperty("Custom1", "custom, 1"); + contact2.setProperty("Custom2", "custom\t2"); + contact2.setProperty("Custom3", "custom\r3"); + contact2.setProperty("Custom4", "custom\n4"); + } + contact2 = book.addCard(contact2); + + let list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance( + Ci.nsIAbDirectory + ); + list.isMailList = true; + list.dirName = "new list"; + list = book.addMailList(list); + list.addCard(contact1); + + await compareAgainstFile( + "export.csv", + AddrBookUtils.exportDirectoryToDelimitedText(book, ",") + ); + await compareAgainstFile( + "export.txt", + AddrBookUtils.exportDirectoryToDelimitedText(book, "\t") + ); + await compareAgainstFile( + "export.vcf", + AddrBookUtils.exportDirectoryToVCard(book) + ); + // modifytimestamp is always changing, replace it with a fixed value. + await compareAgainstFile( + "export.ldif", + AddrBookUtils.exportDirectoryToLDIF(book).replace( + /modifytimestamp: \d+/g, + "modifytimestamp: 12345" + ) + ); +} + +async function compareAgainstFile(fileName, actual) { + info(`checking against ${fileName}`); + + // The test files are UTF-8 encoded and have Windows line endings. The + // exportDirectoryTo* functions are platform-dependent, except for VCard + // which always uses Windows line endings. + + let file = do_get_file(`data/${fileName}`); + let expected = await IOUtils.readUTF8(file.path); + + if (AppConstants.platform != "win" && fileName != "export.vcf") { + expected = expected.replace(/\r\n/g, "\n"); + } + + // From here on, \r is just another character. It will be the last character + // on lines where Windows line endings exist. + let expectedLines = expected.split("\n"); + let actualLines = actual.split("\n"); + info(actual); + Assert.deepEqual(actualLines.sort(), expectedLines.sort()); + // equal(actualLines.length, expectedLines.length, "correct number of lines"); + + // for (let l = 0; l < expectedLines.length; l++) { + // let expectedLine = expectedLines[l]; + // let actualLine = actualLines[l]; + // if (actualLine == expectedLine) { + // ok(true, `line ${l + 1} matches`); + // } else { + // for (let c = 0; c < expectedLine.length && c < actualLine.length; c++) { + // if (actualLine[c] != expectedLine[c]) { + // // This call to equal automatically prints some extra characters of + // // context. Hopefully that helps with debugging. + // equal( + // actualLine.substring(c - 10, c + 10), + // expectedLine.substring(c - 10, c + 10), + // `line ${l + 1} does not match at character ${c + 1}` + // ); + // } + // } + // equal( + // expectedLine.length, + // actualLine.length, + // `line ${l + 1} lengths differ` + // ); + // } + // } +} + +add_task(async function addrBookCard() { + return subtest(() => new AddrBookCard()); +}); + +add_task(async function cardProperty() { + return subtest(() => + Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(Ci.nsIAbCard) + ); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js b/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js new file mode 100644 index 0000000000..957285bbba --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_jsaddrbook.js @@ -0,0 +1,420 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var FILE_NAME = "abook-1.sqlite"; +var SCHEME = "jsaddrbook"; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +var book, contact, list, listCard; +var observer = { + topics: [ + "addrbook-directory-created", + "addrbook-directory-updated", + "addrbook-directory-deleted", + "addrbook-contact-created", + "addrbook-contact-updated", + "addrbook-contact-properties-updated", + "addrbook-contact-deleted", + "addrbook-list-created", + "addrbook-list-updated", + "addrbook-list-deleted", + "addrbook-list-member-added", + "addrbook-list-member-removed", + ], + setUp() { + for (let topic of this.topics) { + Services.obs.addObserver(observer, topic); + } + }, + cleanUp() { + for (let topic of this.topics) { + Services.obs.removeObserver(observer, topic); + } + }, + + events: [], + observe(subject, topic, data) { + this.events.push([topic, subject, data]); + }, + checkEvents(...events) { + info( + "Actual events: " + + JSON.stringify( + observer.events.map(e => + e.map(a => { + if (a instanceof Ci.nsIAbDirectory) { + return `[nsIAbDirectory]`; + } + if (a instanceof Ci.nsIAbCard) { + return `[nsIAbCard]`; + } + return a; + }) + ) + ) + ); + equal(observer.events.length, events.length); + + let actualEvents = observer.events.slice(); + observer.events.length = 0; + + for (let j = 0; j < events.length; j++) { + let expectedEvent = events[j]; + let actualEvent = actualEvents[j]; + + for (let i = 0; i < expectedEvent.length; i++) { + try { + expectedEvent[i].QueryInterface(Ci.nsIAbCard); + ok(actualEvent[i].equals(expectedEvent[i])); + } catch (ex) { + if (expectedEvent[i] instanceof Ci.nsIAbDirectory) { + equal(actualEvent[i].UID, expectedEvent[i].UID); + } else if (expectedEvent[i] === null) { + ok(!actualEvent[i]); + } else if (expectedEvent[i] !== undefined) { + equal(actualEvent[i], expectedEvent[i]); + } + } + } + } + + return actualEvents; + }, +}; + +var baseAddressBookCount; + +add_setup(function () { + let profileDir = do_get_profile(); + observer.setUp(); + + let dirs = MailServices.ab.directories; + // On Mac we might be loading the OS X Address Book. If we are, then we + // need to take acccount of that here, so that the test still pass on + // development machines. + if ( + AppConstants.platform == "macosx" && + dirs[0].URI == "moz-abosxdirectory:///" + ) { + equal(dirs.length, 3); + equal(dirs[1].fileName, kPABData.fileName); + equal(dirs[2].fileName, kCABData.fileName); + } else { + equal(dirs.length, 2); + equal(dirs[0].fileName, kPABData.fileName); + equal(dirs[1].fileName, kCABData.fileName); + } + // Also record the address book counts so that we get the expected counts + // correct further down in the test. + baseAddressBookCount = dirs.length; + + // Check the PAB file was created. + let pabFile = profileDir.clone(); + pabFile.append(kPABData.fileName); + ok(pabFile.exists()); + + // Check the CAB file was created. + let cabFile = profileDir.clone(); + cabFile.append(kCABData.fileName); + ok(cabFile.exists()); +}); + +add_task(async function createAddressBook() { + let dirPrefId = MailServices.ab.newAddressBook( + "new book", + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + book = MailServices.ab.getDirectoryFromId(dirPrefId); + observer.checkEvents(["addrbook-directory-created", book]); + + // Check nsIAbDirectory properties. + ok(!book.readOnly); + ok(!book.isRemote); + ok(!book.isSecure); + equal(book.dirName, "new book"); + equal(book.dirType, Ci.nsIAbManager.JS_DIRECTORY_TYPE); + equal(book.fileName, FILE_NAME); + equal(book.UID.length, 36); + equal(book.URI, `${SCHEME}://${FILE_NAME}`); + equal(book.isMailList, false); + equal(book.supportsMailingLists, true); + equal(book.dirPrefId, "ldap_2.servers.newbook"); + + // Check enumerations. + equal(Array.from(book.childNodes).length, 0); + equal(Array.from(book.childCards).length, 0); + + // Check prefs. + equal( + Services.prefs.getStringPref("ldap_2.servers.newbook.description"), + "new book" + ); + equal( + Services.prefs.getIntPref("ldap_2.servers.newbook.dirType"), + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + equal( + Services.prefs.getStringPref("ldap_2.servers.newbook.filename"), + FILE_NAME + ); + equal(Services.prefs.getStringPref("ldap_2.servers.newbook.uid"), book.UID); + equal(MailServices.ab.directories.length, baseAddressBookCount + 1); + + // Check the file was created. + let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + dbFile.append(FILE_NAME); + ok(dbFile.exists()); +}); + +add_task(async function editAddressBook() { + book.dirName = "updated book"; + observer.checkEvents(["addrbook-directory-updated", book, "DirName"]); + equal(book.dirName, "updated book"); + equal( + Services.prefs.getStringPref("ldap_2.servers.newbook.description"), + "updated book" + ); +}); + +add_task(async function createContact() { + contact = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + contact.displayName = "a new contact"; + contact.firstName = "new"; + contact.lastName = "contact"; + contact.primaryEmail = "test@invalid"; + contact.setProperty("Foo", "This will be deleted later."); + contact = book.addCard(contact); + observer.checkEvents(["addrbook-contact-created", contact, book.UID]); + + let cards = book.childCards; + equal(cards.length, 1); + ok(cards[0].equals(contact)); + + // Check nsIAbCard properties. + equal(contact.directoryUID, book.UID); + equal(contact.UID.length, 36); + equal(contact.firstName, "new"); + equal(contact.lastName, "contact"); + equal(contact.displayName, "a new contact"); + equal(contact.primaryEmail, "test@invalid"); + equal(contact.getProperty("Foo", ""), "This will be deleted later."); + equal(contact.isMailList, false); + let modifiedDate = parseInt(contact.getProperty("LastModifiedDate", ""), 10); + Assert.lessOrEqual(modifiedDate, Date.now() / 1000); + Assert.greater(modifiedDate, Date.now() / 1000 - 10); + + // Check nsIAbCard methods. + equal( + contact.generateName(Ci.nsIAbCard.GENERATE_DISPLAY_NAME), + "a new contact" + ); + equal( + contact.generateName(Ci.nsIAbCard.GENERATE_LAST_FIRST_ORDER), + "contact, new" + ); + equal( + contact.generateName(Ci.nsIAbCard.GENERATE_FIRST_LAST_ORDER), + "new contact" + ); +}); + +add_task(async function editContact() { + contact.firstName = "updated"; + contact.lastName = "contact"; + contact.displayName = "updated contact"; + contact.setProperty("Foo", null); + contact.setProperty("Bar1", "a new property"); + contact.setProperty("Bar2", ""); + contact.setProperty("LastModifiedDate", 0); + book.modifyCard(contact); + let [, propertyEvent] = observer.checkEvents( + ["addrbook-contact-updated", contact, book.UID], + ["addrbook-contact-properties-updated", contact] + ); + Assert.deepEqual(JSON.parse(propertyEvent[2]), { + DisplayName: { + oldValue: "a new contact", + newValue: "updated contact", + }, + Foo: { + oldValue: "This will be deleted later.", + newValue: null, + }, + Bar1: { + oldValue: null, + newValue: "a new property", + }, + FirstName: { + oldValue: "new", + newValue: "updated", + }, + _vCard: { + oldValue: formatVCard` + BEGIN:VCARD + VERSION:4.0 + FN:a new contact + EMAIL;PREF=1:test@invalid + N:contact;new;;; + UID:${contact.UID} + END:VCARD`, + newValue: formatVCard` + BEGIN:VCARD + VERSION:4.0 + FN:updated contact + EMAIL;PREF=1:test@invalid + N:contact;updated;;; + UID:${contact.UID} + END:VCARD`, + }, + }); + contact = book.childCards[0]; + equal(contact.firstName, "updated"); + equal(contact.lastName, "contact"); + equal(contact.displayName, "updated contact"); + equal(contact.getProperty("Foo", "empty"), "empty"); + equal(contact.getProperty("Bar1", ""), "a new property"); + equal(contact.getProperty("Bar2", "no value"), "no value"); + let modifiedDate = parseInt(contact.getProperty("LastModifiedDate", ""), 10); + Assert.lessOrEqual(modifiedDate, Date.now() / 1000); + Assert.greater(modifiedDate, Date.now() / 1000 - 10); +}); + +add_task(async function createMailingList() { + list = Cc["@mozilla.org/addressbook/directoryproperty;1"].createInstance( + Ci.nsIAbDirectory + ); + list.isMailList = true; + list.dirName = "new list"; + list = book.addMailList(list); + // Skip checking events temporarily, until listCard is defined. + + // Check enumerations. + let childNodes = book.childNodes; + equal(childNodes.length, 1); + equal(childNodes[0].UID, list.UID); // TODO Object equality doesn't work because of XPCOM. + let childCards = book.childCards; + equal(childCards.length, 2); + if (childCards[0].isMailList) { + listCard = childCards[0]; + ok(childCards[1].equals(contact)); + } else { + ok(childCards[0].equals(contact)); + listCard = childCards[1]; + } + equal(listCard.UID, list.UID); + + observer.checkEvents(["addrbook-list-created", list, book.UID]); + + // Check nsIAbDirectory properties. + equal(list.dirName, "new list"); + equal(list.UID.length, 36); + equal(list.URI, `${SCHEME}://${FILE_NAME}/${list.UID}`); + equal(list.isMailList, true); + equal(list.supportsMailingLists, false); + + // Check list enumerations. + equal(Array.from(list.childNodes).length, 0); + equal(Array.from(list.childCards).length, 0); + + // Check nsIAbCard properties. + equal(listCard.firstName, ""); + equal(listCard.lastName, "new list"); + equal(listCard.primaryEmail, ""); + equal(listCard.displayName, "new list"); +}); + +add_task(async function editMailingList() { + list.dirName = "updated list"; + list.editMailListToDatabase(null); + observer.checkEvents(["addrbook-list-updated", list, book.UID]); + equal("updated list", list.dirName); +}); + +add_task(async function addMailingListMember() { + list.addCard(contact); + observer.checkEvents(["addrbook-list-member-added", contact, list.UID]); + + // Check list enumerations. + equal(Array.from(list.childNodes).length, 0); + let childCards = list.childCards; + equal(childCards.length, 1); + ok(childCards[0].equals(contact)); +}); + +add_task(async function removeMailingListMember() { + list.deleteCards([contact]); + observer.checkEvents(["addrbook-list-member-removed", contact, list.UID]); + + // Check list enumerations. + equal(Array.from(list.childNodes).length, 0); + equal(Array.from(list.childCards).length, 0); +}); + +add_task(async function deleteMailingList() { + book.deleteDirectory(list); + observer.checkEvents(["addrbook-list-deleted", list, book.UID]); +}); + +add_task(async function deleteContact() { + book.deleteCards([contact]); + observer.checkEvents(["addrbook-contact-deleted", contact, book.UID]); + + // Check enumerations. + equal(Array.from(book.childNodes).length, 0); + equal(Array.from(book.childCards).length, 0); +}); + +// Tests that the UID on a new contact can be set. +add_task(async function createContactWithUID() { + let contactWithUID = Cc[ + "@mozilla.org/addressbook/cardproperty;1" + ].createInstance(Ci.nsIAbCard); + contactWithUID.UID = "I'm a UID!"; + contactWithUID = book.addCard(contactWithUID); + equal("I'm a UID!", contactWithUID.UID, "New contact has the UID we set"); + + Assert.throws(() => { + // Set the UID after it already exists. + contactWithUID.UID = "This should not be possible"; + }, /NS_ERROR_UNEXPECTED/); + + // Setting the UID to it's existing value should not fail. + contactWithUID.UID = contactWithUID.UID; // eslint-disable-line no-self-assign + + book.deleteCards([contactWithUID]); + observer.events.length = 0; +}); + +add_task(async function deleteAddressBook() { + await promiseDirectoryRemoved(book.URI); + + observer.checkEvents(["addrbook-directory-deleted", book, null]); + ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.dirType")); + ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.description")); + ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.filename")); + ok(!Services.prefs.prefHasUserValue("ldap_2.servers.newbook.uid")); + let dbFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + dbFile.append(FILE_NAME); + ok(!dbFile.exists()); + equal(MailServices.ab.directories.length, baseAddressBookCount); + Assert.throws(() => { + MailServices.ab.getDirectory(`${SCHEME}://${FILE_NAME}`); + }, /NS_ERROR_FAILURE/); +}); + +add_task(async function cleanUp() { + observer.checkEvents(); + observer.cleanUp(); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_ldap1.js b/comm/mailnews/addrbook/test/unit/test_ldap1.js new file mode 100644 index 0000000000..e323d71386 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_ldap1.js @@ -0,0 +1,205 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for basic LDAP address book functions + */ + +var kLDAPUriPrefix = "moz-abldapdirectory://"; +var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)"; + +function run_test() { + // If nsIAbLDAPDirectory doesn't exist in our build options, someone has + // specified --disable-ldap + if (!("nsIAbLDAPDirectory" in Ci)) { + return; + } + + let abCountBeforeStart = MailServices.ab.directories.length; + + // Test - Create an LDAP directory + let abUri = MailServices.ab.newAddressBook( + "test", + kLDAPTestSpec, + Ci.nsIAbManager.LDAP_DIRECTORY_TYPE + ); + + let abCountAfterCreate = MailServices.ab.directories.length; + Assert.equal(abCountAfterCreate, abCountBeforeStart + 1); + + // Test - Check we have the directory. + let abDir = MailServices.ab + .getDirectory(kLDAPUriPrefix + abUri) + .QueryInterface(Ci.nsIAbLDAPDirectory); + + // Test - Check various fields + Assert.equal(abDir.dirName, "test"); + Assert.equal(abDir.lDAPURL.spec, kLDAPTestSpec); + Assert.ok(abDir.readOnly); + + // Test - Write a UTF-8 Auth DN and check it + abDir.authDn = "test\u00D0"; + + Assert.equal(abDir.authDn, "test\u00D0"); + + // Test - searchDuringLocalAutocomplete + + // Set up an account and identity in the account manager + let identity = MailServices.accounts.createIdentity(); + + const localAcTests = [ + // Online checks + { + useDir: false, + dirSer: "", + idOver: false, + idSer: "", + idKey: "", + offline: false, + result: false, + }, + { + useDir: true, + dirSer: abDir.dirPrefId, + idOver: false, + idSer: "", + idKey: "", + offline: false, + result: false, + }, + // Offline checks with and without global prefs set, no identity key + { + useDir: false, + dirSer: "", + idOver: false, + idSer: "", + idKey: "", + offline: true, + result: false, + }, + { + useDir: true, + dirSer: "", + idOver: false, + idSer: "", + idKey: "", + offline: true, + result: false, + }, + { + useDir: true, + dirSer: abDir.dirPrefId, + idOver: false, + idSer: "", + idKey: "", + offline: true, + result: true, + }, + // Offline checks with and without global prefs set, with identity key + { + useDir: false, + dirSer: "", + idOver: false, + idSer: "", + idKey: identity.key, + offline: true, + result: false, + }, + { + useDir: true, + dirSer: "", + idOver: false, + idSer: "", + idKey: identity.key, + offline: true, + result: false, + }, + { + useDir: true, + dirSer: abDir.dirPrefId, + idOver: false, + idSer: "", + idKey: identity.key, + offline: true, + result: true, + }, + // Offline checks, no global prefs, identity ones only + { + useDir: false, + dirSer: "", + idOver: true, + idSer: "", + idKey: identity.key, + offline: true, + result: false, + }, + { + useDir: false, + dirSer: "", + idOver: true, + idSer: kPABData.dirPrefID, + idKey: identity.key, + offline: true, + result: false, + }, + { + useDir: false, + dirSer: "", + idOver: true, + idSer: abDir.dirPrefId, + idKey: identity.key, + offline: true, + result: true, + }, + { + useDir: false, + dirSer: "", + idOver: false, + idSer: abDir.dirPrefId, + idKey: identity.key, + offline: true, + result: false, + }, + // Offline checks, global prefs and identity ones + { + useDir: true, + dirSer: kPABData.dirPrefID, + idOver: true, + idSer: abDir.dirPrefId, + idKey: identity.key, + offline: true, + result: true, + }, + { + useDir: true, + dirSer: abDir.dirPrefId, + idOver: true, + idSer: kPABData.dirPrefID, + idKey: identity.key, + offline: true, + result: false, + }, + ]; + + function checkAc(element, index, array) { + dump("Testing index " + index + "\n"); + Services.prefs.setBoolPref( + "ldap_2.autoComplete.useDirectory", + element.useDir + ); + Services.prefs.setCharPref( + "ldap_2.autoComplete.directoryServer", + element.dirSer + ); + identity.overrideGlobalPref = element.idOver; + identity.directoryServer = element.idSer; + Services.io.offline = element.offline; + + Assert.equal(abDir.useForAutocomplete(element.idKey), element.result); + } + + localAcTests.forEach(checkAc); + + MailServices.ab.deleteAddressBook(abDir.URI); + + let abCountAfterDelete = MailServices.ab.directories.length; + Assert.equal(abCountAfterDelete, abCountBeforeStart); +} diff --git a/comm/mailnews/addrbook/test/unit/test_ldap2.js b/comm/mailnews/addrbook/test/unit/test_ldap2.js new file mode 100644 index 0000000000..2dc39c4a86 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_ldap2.js @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for bug 532170. LDAP address book named with cyrillic/chinese + * letters doesn't work. + */ + +var kLDAPUriPrefix = "moz-abldapdirectory://"; +var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)"; + +function run_test() { + // If nsIAbLDAPDirectory doesn't exist in our build options, someone has + // specified --disable-ldap + if (!("nsIAbLDAPDirectory" in Ci)) { + return; + } + + // Test - Create an LDAP directory + + // Use a UTF-8 based directory name + var abUri = MailServices.ab.newAddressBook( + "\u041C\u0435\u043B\u0435\u043D\u043A\u0438", + kLDAPTestSpec, + Ci.nsIAbManager.LDAP_DIRECTORY_TYPE + ); + + // Test - Check we have the directory. + let abDir = MailServices.ab + .getDirectory(kLDAPUriPrefix + abUri) + .QueryInterface(Ci.nsIAbLDAPDirectory); + + // Test - Check various fields + Assert.equal(abDir.dirName, "\u041C\u0435\u043B\u0435\u043D\u043A\u0438"); + Assert.equal(abDir.lDAPURL.spec, kLDAPTestSpec); + Assert.ok(abDir.readOnly); + + // XXX I'd really like a better check than this, to check that searching + // works correctly. However we haven't got the support for that at the moment + // and this at least ensures that we get a consistent ascii based preference + // for the directory. + Assert.equal(abDir.dirPrefId, "ldap_2.servers._nonascii"); +} diff --git a/comm/mailnews/addrbook/test/unit/test_ldapOffline.js b/comm/mailnews/addrbook/test/unit/test_ldapOffline.js new file mode 100644 index 0000000000..ed81344d03 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_ldapOffline.js @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite to check that we correctly get child cards for LDAP directories + * when offline and that we don't crash. + */ + +var kLDAPUriPrefix = "moz-abldapdirectory://"; +var kLDAPTestSpec = "ldap://invalidhost//dc=intranet??sub?(objectclass=*)"; + +// Main function for the this test so we can check both personal and +// collected books work correctly in an easy manner. +function run_test() { + // If nsIAbLDAPDirectory doesn't exist in our build options, someone has + // specified --disable-ldap + if (!("nsIAbLDAPDirectory" in Ci)) { + return; + } + + // Test set-up + let abUri = MailServices.ab.newAddressBook( + "test", + kLDAPTestSpec, + Ci.nsIAbManager.LDAP_DIRECTORY_TYPE + ); + + let abDir = MailServices.ab + .getDirectory(kLDAPUriPrefix + abUri) + .QueryInterface(Ci.nsIAbLDAPDirectory); + + const kLDAPFileName = "ldap-1.sqlite"; + + // Test setup - copy the data file into place + loadABFile("data/cardForEmail", kLDAPFileName); + + // And tell the ldap directory we want this file. + abDir.replicationFileName = kLDAPFileName; + + // Now go offline + Services.io.offline = true; + + // Make sure we clear any memory that is now loose, so that the crash would + // be triggered. + gc(); + + // Now try and get the card that has been replicated for offline use. + Assert.equal(abDir.childCards.length, 5); +} diff --git a/comm/mailnews/addrbook/test/unit/test_ldapReplication.js b/comm/mailnews/addrbook/test/unit/test_ldapReplication.js new file mode 100644 index 0000000000..220417a095 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_ldapReplication.js @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { LDAPServer } = ChromeUtils.import( + "resource://testing-common/LDAPServer.jsm" +); + +const autocompleteService = Cc[ + "@mozilla.org/autocomplete/search;1?name=addrbook" +].getService(Ci.nsIAutoCompleteSearch); +const jsonFile = do_get_file("data/ldap_contacts.json"); +const replicationService = Cc[ + "@mozilla.org/addressbook/ldap-replication-service;1" +].getService(Ci.nsIAbLDAPReplicationService); + +add_task(async () => { + LDAPServer.open(); + let ldapContacts = await IOUtils.readJSON(jsonFile.path); + + let bookPref = MailServices.ab.newAddressBook( + "XPCShell", + `ldap://localhost:${LDAPServer.port}/people??sub?(objectclass=*)`, + 0 + ); + let book = MailServices.ab.getDirectoryFromId(bookPref); + book.QueryInterface(Ci.nsIAbLDAPDirectory); + equal(book.replicationFileName, "ldap.sqlite"); + + Services.prefs.setCharPref("ldap_2.autoComplete.directoryServer", bookPref); + Services.prefs.setBoolPref("ldap_2.autoComplete.useDirectory", true); + + registerCleanupFunction(async () => { + LDAPServer.close(); + }); + + let progressResolve; + let progressPromise = new Promise(resolve => (progressResolve = resolve)); + let progressListener = { + onStateChange(webProgress, request, stateFlags, status) { + if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { + info("replication started"); + } + if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + info("replication ended"); + progressResolve(); + } + }, + onProgressChange( + webProgress, + request, + currentSelfProgress, + maxSelfProgress, + currentTotalProgress, + maxTotalProgress + ) {}, + onLocationChange(webProgress, request, location, flags) {}, + onStatusChange(webProgress, request, status, message) {}, + onSecurityChange(webProgress, request, state) {}, + onContentBlockingEvent(webProgress, request, event) {}, + }; + + replicationService.startReplication(book, progressListener); + + await LDAPServer.read(LDAPServer.BindRequest); + LDAPServer.writeBindResponse(); + + await LDAPServer.read(LDAPServer.SearchRequest); + for (let contact of Object.values(ldapContacts)) { + LDAPServer.writeSearchResultEntry(contact); + } + LDAPServer.writeSearchResultDone(); + + await progressPromise; + equal(book.replicationFileName, "ldap.sqlite"); + + Services.io.offline = true; + + let cards = book.childCards; + deepEqual(cards.map(c => c.displayName).sort(), [ + "Eurus Holmes", + "Greg Lestrade", + "Irene Adler", + "Jim Moriarty", + "John Watson", + "Mary Watson", + "Molly Hooper", + "Mrs Hudson", + "Mycroft Holmes", + "Sherlock Holmes", + ]); + + await new Promise(resolve => { + autocompleteService.startSearch("molly", '{"type":"addr_to"}', null, { + onSearchResult(search, result) { + equal(result.matchCount, 1); + equal(result.getValueAt(0), "Molly Hooper <molly@bakerstreet.invalid>"); + resolve(); + }, + }); + }); + await new Promise(resolve => { + autocompleteService.startSearch("watson", '{"type":"addr_to"}', null, { + onSearchResult(search, result) { + equal(result.matchCount, 2); + equal(result.getValueAt(0), "John Watson <john@bakerstreet.invalid>"); + equal(result.getValueAt(1), "Mary Watson <mary@bakerstreet.invalid>"); + resolve(); + }, + }); + }); + + // Do it again with different information from the server. Ensure we have the new information. + + progressPromise = new Promise(resolve => (progressResolve = resolve)); + replicationService.startReplication(book, progressListener); + + await LDAPServer.read(LDAPServer.BindRequest); + LDAPServer.writeBindResponse(); + + await LDAPServer.read(LDAPServer.SearchRequest); + LDAPServer.writeSearchResultEntry(ldapContacts.eurus); + LDAPServer.writeSearchResultEntry(ldapContacts.mary); + LDAPServer.writeSearchResultEntry(ldapContacts.molly); + LDAPServer.writeSearchResultDone(); + + await progressPromise; + equal(book.replicationFileName, "ldap.sqlite"); + + cards = book.childCards; + deepEqual(cards.map(c => c.displayName).sort(), [ + "Eurus Holmes", + "Mary Watson", + "Molly Hooper", + ]); + + // Do it again but cancel. Ensure we still have the old information. + + progressPromise = new Promise(resolve => (progressResolve = resolve)); + replicationService.startReplication(book, progressListener); + + await LDAPServer.read(LDAPServer.BindRequest); + LDAPServer.writeBindResponse(); + + await LDAPServer.read(LDAPServer.SearchRequest); + LDAPServer.writeSearchResultEntry(ldapContacts.john); + LDAPServer.writeSearchResultEntry(ldapContacts.sherlock); + LDAPServer.writeSearchResultEntry(ldapContacts.mrs_hudson); + replicationService.cancelReplication(book); + + await progressPromise; + + cards = book.childCards; + deepEqual(cards.map(c => c.displayName).sort(), [ + "Eurus Holmes", + "Mary Watson", + "Molly Hooper", + ]); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_ldapquery.js b/comm/mailnews/addrbook/test/unit/test_ldapquery.js new file mode 100644 index 0000000000..90b1f1673d --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_ldapquery.js @@ -0,0 +1,181 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +/** + * Test basic LDAP querying. + */ + +const { LDAPDaemon, LDAPHandlerFn } = ChromeUtils.import( + "resource://testing-common/mailnews/Ldapd.jsm" +); +const { BinaryServer } = ChromeUtils.import( + "resource://testing-common/mailnews/Binaryd.jsm" +); + +/** + * Adaptor class to implement nsILDAPMessageListener with a promise. + * It should be passed into LDAP functions as a normal listener. The + * caller can then await the promise attribute. + * Based on the pattern used in PromiseTestUtils.jsm. + * + * This base class just rejects all callbacks. Derived classes should + * implement the callbacks they need to handle. + * + * @implements {nsILDAPMessageListener} + */ +class PromiseListener { + constructor() { + this.QueryInterface = ChromeUtils.generateQI(["nsILDAPMessageListener"]); + this.promise = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + } + onLDAPMessage(message) { + this._reject(new Error("Unexpected onLDAPMessage")); + } + onLDAPInit() { + this._reject(new Error("Unexpected onLDAPInit")); + } + onLDAPError(status, secInfo, location) { + this._reject(new Error(`Unexpected onLDAPError (0x${status.toString(16)}`)); + } +} + +/** + * PromiseInitListener resolves the promise when onLDAPInit is called. + * + * @augments {PromiseListener} + */ +class PromiseInitListener extends PromiseListener { + onLDAPInit() { + this._resolve(); + } +} + +/** + * PromiseBindListener resolves when a bind operation completes. + * + * @augments {PromiseListener} + */ +class PromiseBindListener extends PromiseListener { + onLDAPMessage(message) { + if (Ci.nsILDAPErrors.SUCCESS != message.errorCode) { + this._reject( + new Error(`Operation failed (LDAP code ${message.errorCode})`) + ); + } + if (Ci.nsILDAPMessage.RES_BIND == message.type) { + this._resolve(); // All done. + } + } +} + +/** + * PromiseSearchListener collects search results, returning them via promise + * when the search is complete. + * + * @augments {PromiseListener} + */ +class PromiseSearchListener extends PromiseListener { + constructor() { + super(); + this._results = []; + } + onLDAPMessage(message) { + if (Ci.nsILDAPMessage.RES_SEARCH_RESULT == message.type) { + this._resolve(this._results); // All done. + } + if (Ci.nsILDAPMessage.RES_SEARCH_ENTRY == message.type) { + this._results.push(message); + } + } +} + +add_task(async function test_basic_query() { + // Load in some test contact data (characters from Sherlock Holmes). + let raw = await IOUtils.readUTF8( + do_get_file( + "../../../../mailnews/addrbook/test/unit/data/ldap_contacts.json" + ).path + ); + let testContacts = JSON.parse(raw); + + // Set up fake LDAP server, loaded with the test contacts. + let daemon = new LDAPDaemon(); + daemon.add(...Object.values(testContacts)); + // daemon.setDebug(true); + let server = new BinaryServer(LDAPHandlerFn, daemon); + server.start(); + + // Connect to the fake server. + let url = `ldap://localhost:${server.port}`; + let ldapURL = Services.io.newURI(url).QueryInterface(Ci.nsILDAPURL); + let conn = Cc["@mozilla.org/network/ldap-connection;1"] + .createInstance() + .QueryInterface(Ci.nsILDAPConnection); + + // Initialisation is async. + let initListener = new PromiseInitListener(); + conn.init(ldapURL, null, initListener, null, Ci.nsILDAPConnection.VERSION3); + await initListener.promise; + + // Perform bind. + let bindListener = new PromiseBindListener(); + let bindOp = Cc["@mozilla.org/network/ldap-operation;1"].createInstance( + Ci.nsILDAPOperation + ); + bindOp.init(conn, bindListener, null); + bindOp.simpleBind(""); // no password + await bindListener.promise; + + // Run a search. + let searchListener = new PromiseSearchListener(); + let searchOp = Cc["@mozilla.org/network/ldap-operation;1"].createInstance( + Ci.nsILDAPOperation + ); + searchOp.init(conn, searchListener, null); + searchOp.searchExt( + "", // dn + Ci.nsILDAPURL.SCOPE_SUBTREE, + "(sn=Holmes)", // filter: Find the Holmes family members. + "", // wanted_attributes + 0, // timeOut + 100 // maxEntriesWanted + ); + let matches = await searchListener.promise; + + // Make sure we got the contacts we expected (just use cn for comparing): + const holmesCNs = ["Eurus Holmes", "Mycroft Holmes", "Sherlock Holmes"]; + const holmesGivenNames = ["Eurus", "Mycroft", "Sherlock"]; + const nonHolmesCNs = [ + "Greg Lestrade", + "Irene Adler", + "Jim Moriarty", + "John Watson", + "Mary Watson", + "Molly Hooper", + "Mrs Hudson", + ]; + let cns = matches.map(ent => ent.getValues("cn")[0]); + cns.sort(); + Assert.deepEqual(cns, holmesCNs); + + // Test getValues is case insensitive about the attribute name. + let givenNames = matches.map(ent => ent.getValues("givenname")[0]); + givenNames.sort(); + Assert.deepEqual(givenNames, holmesGivenNames); + givenNames = matches.map(ent => ent.getValues("givenName")[0]); + givenNames.sort(); + Assert.deepEqual(givenNames, holmesGivenNames); + givenNames = matches.map(ent => ent.getValues("GIVENNAME")[0]); + givenNames.sort(); + Assert.deepEqual(givenNames, holmesGivenNames); + + // Sanity check: make sure the non-Holmes contacts were excluded. + nonHolmesCNs.forEach(cn => Assert.ok(!cns.includes(cn))); + + server.stop(); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_mailList1.js b/comm/mailnews/addrbook/test/unit/test_mailList1.js new file mode 100644 index 0000000000..0889257da6 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_mailList1.js @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for mailing list functions. + * + * This suite relies on abLists1.mab. checkLists requires that the mailing list + * name be "TestList<n>" where <n> is the number of the list that also matches + * the <n> in the uri: moz-ab???directory://path/MailList<n> + */ + +function checkLists(childNodes, number) { + let count = 0; + // See comment above for matching requirements + for (let list of childNodes) { + if (list.isMailList && list.dirName.startsWith("TestList")) { + Assert.equal(list.URI, `${kPABData.URI}/${list.UID}`); + count++; + } + } + + Assert.equal(count, number); +} + +function run_test() { + loadABFile("../../../data/abLists1", kPABData.fileName); + + // Test - Get the directory. + + // XXX Getting all directories ensures we create all ABs because mailing + // lists need help initialising themselves + MailServices.ab.directories; + + let AB = MailServices.ab.getDirectory(kPABData.URI); + + // Test - Check all the expected mailing lists exist. + + // There are three lists in abLists.mab by default. + checkLists(AB.childNodes, 3); + + // Test - Add a new list. + + var mailList = Cc[ + "@mozilla.org/addressbook/directoryproperty;1" + ].createInstance(Ci.nsIAbDirectory); + + mailList.isMailList = true; + mailList.dirName = "TestList4"; + mailList.listNickName = "test4"; + mailList.description = "test4description"; + + AB.addMailList(mailList); + + // check them + checkLists(AB.childNodes, 4); + + // Test - Remove a list. + + mailList = MailServices.ab.getDirectory( + kPABData.URI + "/46cf4cbf-5945-43e4-a822-30c2f2969db9" + ); + + AB.deleteDirectory(mailList); + + // check them + checkLists(AB.childNodes, 3); +} diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js new file mode 100644 index 0000000000..8faf6e064a --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteMyDomain.js @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsAbAutoCompleteSearch + */ + +var ACR = Ci.nsIAutoCompleteResult; + +function acObserver() {} + +acObserver.prototype = { + _search: null, + _result: null, + + onSearchResult(aSearch, aResult) { + this._search = aSearch; + this._result = aResult; + }, +}; + +function run_test() { + // Test - Create a new search component + + var acs = Cc["@mozilla.org/autocomplete/search;1?name=mydomain"].getService( + Ci.nsIAutoCompleteSearch + ); + + var obs = new acObserver(); + let obsNews = new acObserver(); + let obsFollowup = new acObserver(); + + // Set up an identity in the account manager with the default settings + let identity = MailServices.accounts.createIdentity(); + + // Initially disable autocomplete + identity.autocompleteToMyDomain = false; + identity.email = "myemail@foo.invalid"; + + // Set up autocomplete parameters + let params = JSON.stringify({ idKey: identity.key, type: "addr_to" }); + let paramsNews = JSON.stringify({ + idKey: identity.key, + type: "addr_newsgroups", + }); + let paramsFollowup = JSON.stringify({ + idKey: identity.key, + type: "addr_followup", + }); + + // Test - Valid search - this should return no results (autocomplete disabled) + acs.startSearch("test", params, null, obs); + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "test"); + Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 0); + + // Now enable autocomplete for this identity + identity.autocompleteToMyDomain = true; + + // Test - Search with empty string + + acs.startSearch(null, params, null, obs); + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, null); + Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 0); + + acs.startSearch("", params, null, obs); + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, ""); + Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 0); + + // Test - Check ignoring result with comma + + acs.startSearch("a,b", params, null, obs); + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "a,b"); + Assert.equal(obs._result.searchResult, ACR.RESULT_FAILURE); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 0); + + // Test - Check returning search string with @ sign + + acs.startSearch("a@b", params, null, obs); + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "a@b"); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 1); + + Assert.equal(obs._result.getValueAt(0), "a@b"); + Assert.equal(obs._result.getLabelAt(0), "a@b"); + Assert.equal(obs._result.getCommentAt(0), null); + Assert.equal(obs._result.getStyleAt(0), "default-match"); + Assert.equal(obs._result.getImageAt(0), null); + + // No autocomplete for addr_newsgroups! + acs.startSearch("a@b", paramsNews, null, obsNews); + Assert.ok(obsNews._result == null || obsNews._result.matchCount == 0); + + // No autocomplete for addr_followup! + acs.startSearch("a@b", paramsFollowup, null, obsFollowup); + Assert.ok(obsFollowup._result == null || obsFollowup._result.matchCount == 0); + + // Test - Add default domain + + acs.startSearch("test1", params, null, obs); + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "test1"); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 1); + + Assert.equal(obs._result.getValueAt(0), "test1@foo.invalid"); + Assert.equal(obs._result.getLabelAt(0), "test1@foo.invalid"); + Assert.equal(obs._result.getCommentAt(0), null); + Assert.equal(obs._result.getStyleAt(0), "default-match"); + Assert.equal(obs._result.getImageAt(0), null); +} diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js new file mode 100644 index 0000000000..ffe48506ce --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch1.js @@ -0,0 +1,468 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * First test suite for nsAbAutoCompleteSearch - tests searching in address + * books for autocomplete matches, and checks sort order is correct (without + * popularity checks). + */ + +var ACR = Ci.nsIAutoCompleteResult; + +// Input and results arrays for the autocomplete tests. This are potentially +// more complicated than really required, but it was easier to do them +// on a pattern rather just doing the odd spot check. +// +// Note the expected arrays are in expected sort order as well. +var results = [ + { email: "d <ema@foo.invalid>", dirName: kPABData.dirName }, // 0 + { email: "di <emai@foo.invalid>", dirName: kPABData.dirName }, // 1 + { email: "dis <email@foo.invalid>", dirName: kPABData.dirName }, // 2 + { email: "disp <e@foo.invalid>", dirName: kPABData.dirName }, // 3 + { email: "displ <em@foo.invalid>", dirName: kPABData.dirName }, // 4 + { + email: "DisplayName1 <PrimaryEmail1@test.invalid>", // 5 + dirName: kCABData.dirName, + }, + { email: "t <list>", dirName: kPABData.dirName }, // 6 + { email: "te <lis>", dirName: kPABData.dirName }, // 7 + { email: "tes <li>", dirName: kPABData.dirName }, // 8 + // this contact has a nickname of "abcdef" + { email: "test <l>", dirName: kPABData.dirName }, // 9 + { email: "doh, james <DohJames@foo.invalid>", dirName: kPABData.dirName }, // 10 +]; +var firstNames = [ + { search: "f", expected: [0, 1, 2, 3, 4, 5, 10, 9] }, + { search: "fi", expected: [0, 1, 3, 4, 5] }, + { search: "fir", expected: [0, 1, 4, 5] }, + { search: "firs", expected: [0, 1, 5] }, + { search: "first", expected: [1, 5] }, + { search: "firstn", expected: [5] }, +]; + +var lastNames = [ + { search: "l", expected: [6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 10] }, + { search: "la", expected: [0, 2, 3, 4, 5] }, + { search: "las", expected: [0, 3, 4, 5] }, + { search: "last", expected: [0, 4, 5] }, + { search: "lastn", expected: [0, 5] }, + { search: "lastna", expected: [5] }, +]; + +var displayNames = [ + { search: "d", expected: [0, 1, 2, 3, 4, 5, 10, 9] }, + { search: "di", expected: [1, 2, 3, 4, 5] }, + { search: "dis", expected: [2, 3, 4, 5] }, + { search: "disp", expected: [3, 4, 5] }, + { search: "displ", expected: [4, 5] }, + { search: "displa", expected: [5] }, + { search: "doh,", expected: [10] }, +]; + +var nickNames = [ + { search: "n", expected: [4, 0, 1, 2, 3, 5, 10] }, + { search: "ni", expected: [0, 1, 2, 3, 5] }, + { search: "nic", expected: [1, 2, 3, 5] }, + { search: "nick", expected: [2, 3, 5] }, + { search: "nickn", expected: [3, 5] }, + { search: "nickna", expected: [5] }, +]; + +var emails = [ + { search: "e", expected: [0, 1, 2, 3, 4, 5, 10, 7, 8, 9] }, + { search: "em", expected: [0, 1, 2, 4, 5] }, + { search: "ema", expected: [0, 1, 2, 5] }, + { search: "emai", expected: [1, 2, 5] }, + { search: "email", expected: [2, 5] }, +]; + +// "l" case tested above +var lists = [ + { search: "li", expected: [6, 7, 8, 0, 1, 2, 3, 4, 5, 10] }, + { search: "lis", expected: [6, 7] }, + { search: "list", expected: [6] }, + { search: "t", expected: [6, 7, 8, 9, 5, 0, 1, 4] }, + { search: "te", expected: [7, 8, 9, 5] }, + { search: "tes", expected: [8, 9, 5] }, + { search: "test", expected: [9, 5] }, + { search: "abcdef", expected: [9] }, // Bug 441586 +]; + +var bothNames = [ + { search: "f l", expected: [0, 1, 2, 3, 4, 5, 10, 9] }, + { search: "l f", expected: [0, 1, 2, 3, 4, 5, 10, 9] }, + { search: "firstn lastna", expected: [5] }, + { search: "lastna firstna", expected: [5] }, +]; + +var inputs = [ + firstNames, + lastNames, + displayNames, + nickNames, + emails, + lists, + bothNames, +]; + +var PAB_CARD_DATA = [ + { + FirstName: "firs", + LastName: "lastn", + DisplayName: "d", + NickName: "ni", + PrimaryEmail: "ema@foo.invalid", + PreferDisplayName: true, + PopularityIndex: 0, + }, + { + FirstName: "first", + LastName: "l", + DisplayName: "di", + NickName: "nic", + PrimaryEmail: "emai@foo.invalid", + PreferDisplayName: true, + PopularityIndex: 0, + }, + { + FirstName: "f", + LastName: "la", + DisplayName: "dis", + NickName: "nick", + PrimaryEmail: "email@foo.invalid", + PreferDisplayName: true, + PopularityIndex: 0, + }, + { + FirstName: "fi", + LastName: "las", + DisplayName: "disp", + NickName: "nickn", + PrimaryEmail: "e@foo.invalid", + PreferDisplayName: true, + PopularityIndex: 0, + }, + { + FirstName: "fir", + LastName: "last", + DisplayName: "displ", + NickName: "n", + PrimaryEmail: "em@foo.invalid", + PreferDisplayName: true, + PopularityIndex: 0, + }, + { + FirstName: "Doh", + LastName: "James", + DisplayName: "doh, james", + NickName: "j", + PrimaryEmail: "DohJames@foo.invalid", + PreferDisplayName: true, + PopularityIndex: 0, + }, +]; + +var PAB_LIST_DATA = [ + { + dirName: "t", + listNickName: null, + description: "list", + }, + { + dirName: "te", + listNickName: null, + description: "lis", + }, + { + dirName: "tes", + listNickName: null, + description: "li", + }, + { + dirName: "test", + listNickName: "abcdef", + description: "l", + }, +]; + +var CAB_CARD_DATA = [ + { + FirstName: "FirstName1", + LastName: "LastName1", + DisplayName: "DisplayName1", + NickName: "NickName1", + PrimaryEmail: "PrimaryEmail1@test.invalid", + PreferDisplayName: true, + PopularityIndex: 0, + }, + { + FirstName: "Empty", + LastName: "Email", + DisplayName: "Empty Email", + PreferDisplayName: true, + PopularityIndex: 0, + }, +]; + +var CAB_LIST_DATA = []; + +function setupAddressBookData(aDirURI, aCardData, aMailListData) { + let ab = MailServices.ab.getDirectory(aDirURI); + + // Getting all directories ensures we create all ABs because mailing + // lists need help initialising themselves + MailServices.ab.directories; + + for (let card of ab.childCards) { + ab.dropCard(card, false); + } + + aCardData.forEach(function (cd) { + let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + for (var prop in cd) { + card.setProperty(prop, cd[prop]); + } + ab.addCard(card); + }); + + aMailListData.forEach(function (ld) { + let list = Cc[ + "@mozilla.org/addressbook/directoryproperty;1" + ].createInstance(Ci.nsIAbDirectory); + list.isMailList = true; + for (var prop in ld) { + list[prop] = ld[prop]; + } + ab.addMailList(list); + }); +} + +add_task(async () => { + // Set up addresses for in the personal address book. + setupAddressBookData(kPABData.URI, PAB_CARD_DATA, PAB_LIST_DATA); + // ... and collected addresses address book. + setupAddressBookData(kCABData.URI, CAB_CARD_DATA, CAB_LIST_DATA); + + // Test - Create a new search component + + var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService( + Ci.nsIAutoCompleteSearch + ); + + var obs = new acObserver(); + let obsNews = new acObserver(); + let obsFollowup = new acObserver(); + + // Test - Check disabling of autocomplete + + Services.prefs.setBoolPref("mail.enable_autocomplete", false); + + let param = JSON.stringify({ type: "addr_to" }); + let paramNews = JSON.stringify({ type: "addr_newsgroups" }); + let paramFollowup = JSON.stringify({ type: "addr_followup" }); + + let resultPromise = obs.waitForResult(); + acs.startSearch("abc", param, null, obs); + await resultPromise; + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "abc"); + Assert.equal(obs._result.searchResult, ACR.RESULT_NOMATCH); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 0); + + // Test - Check Enabling of autocomplete, but with empty string. + + Services.prefs.setBoolPref("mail.enable_autocomplete", true); + + resultPromise = obs.waitForResult(); + acs.startSearch(null, param, null, obs); + await resultPromise; + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, null); + Assert.equal(obs._result.searchResult, ACR.RESULT_IGNORED); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 0); + Assert.equal(obs._result.defaultIndex, -1); + + // Test - No matches + + resultPromise = obs.waitForResult(); + acs.startSearch("asjdkljdgfjglkfg", param, null, obs); + await resultPromise; + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "asjdkljdgfjglkfg"); + Assert.equal(obs._result.searchResult, ACR.RESULT_NOMATCH); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 0); + Assert.equal(obs._result.defaultIndex, -1); + + // Test - Matches + + // Basic quick-check + resultPromise = obs.waitForResult(); + acs.startSearch("email", param, null, obs); + await resultPromise; + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "email"); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 2); + Assert.equal(obs._result.defaultIndex, 0); + + Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>"); + Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>"); + Assert.equal(obs._result.getCommentAt(0), ""); + Assert.equal(obs._result.getStyleAt(0), "local-abook"); + Assert.equal(obs._result.getImageAt(0), ""); + + // quick-check that nothing is found for addr_newsgroups + resultPromise = obsNews.waitForResult(); + acs.startSearch("email", paramNews, null, obsNews); + await resultPromise; + Assert.ok(obsNews._result == null || obsNews._result.matchCount == 0); + + // quick-check that nothing is found for addr_followup + resultPromise = obsFollowup.waitForResult(); + acs.startSearch("a@b", paramFollowup, null, obsFollowup); + await resultPromise; + Assert.ok(obsFollowup._result == null || obsFollowup._result.matchCount == 0); + + // Now quick-check with the address book name in the comment column. + Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1); + + resultPromise = obs.waitForResult(); + acs.startSearch("email", param, null, obs); + await resultPromise; + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "email"); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 2); + Assert.equal(obs._result.defaultIndex, 0); + + Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>"); + Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>"); + Assert.equal(obs._result.getCommentAt(0), kPABData.dirName); + Assert.equal(obs._result.getStyleAt(0), "local-abook"); + Assert.equal(obs._result.getImageAt(0), ""); + + // Check input with different case + resultPromise = obs.waitForResult(); + acs.startSearch("EMAIL", param, null, obs); + await resultPromise; + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, "EMAIL"); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, 2); + Assert.equal(obs._result.defaultIndex, 0); + + Assert.equal(obs._result.getValueAt(0), "dis <email@foo.invalid>"); + Assert.equal(obs._result.getLabelAt(0), "dis <email@foo.invalid>"); + Assert.equal(obs._result.getCommentAt(0), kPABData.dirName); + Assert.equal(obs._result.getStyleAt(0), "local-abook"); + Assert.equal(obs._result.getImageAt(0), ""); + + // Now check multiple matches + async function checkInputItem(element, index) { + let prevRes = obs._result; + print("Search #" + index + ": search=" + element.search); + resultPromise = obs.waitForResult(); + acs.startSearch(element.search, param, prevRes, obs); + await resultPromise; + + for (let i = 0; i < obs._result.matchCount; i++) { + print("... got " + i + ": " + obs._result.getValueAt(i)); + } + + for (let i = 0; i < element.expected.length; i++) { + print( + "... expected " + + i + + " (result " + + element.expected[i] + + "): " + + results[element.expected[i]].email + ); + } + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, element.search); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, element.expected.length); + Assert.equal(obs._result.defaultIndex, 0); + + for (let i = 0; i < element.expected.length; ++i) { + Assert.equal( + obs._result.getValueAt(i), + results[element.expected[i]].email + ); + Assert.equal( + obs._result.getLabelAt(i), + results[element.expected[i]].email + ); + Assert.equal( + obs._result.getCommentAt(i), + results[element.expected[i]].dirName + ); + Assert.equal(obs._result.getStyleAt(i), "local-abook"); + Assert.equal(obs._result.getImageAt(i), ""); + } + } + + for (let inputSet of inputs) { + for (let i = 0; i < inputSet.length; i++) { + await checkInputItem(inputSet[i], i); + } + } + + // Test - Popularity Index + print("Checking by popularity index:"); + let pab = MailServices.ab.getDirectory(kPABData.URI); + + for (let card of pab.childCards) { + if (card.isMailList) { + continue; + } + + switch (card.displayName) { + case "dis": // 2 + case "disp": // 3 + card.setProperty("PopularityIndex", 4); + break; + case "displ": // 4 + card.setProperty("PopularityIndex", 5); + break; + case "d": // 0 + card.setProperty("PopularityIndex", 1); + break; + case "di": // 1 + card.setProperty("PopularityIndex", 20); + break; + default: + break; + } + + pab.modifyCard(card); + } + + const popularitySearch = [ + { search: "d", expected: [1, 4, 2, 3, 0, 5, 10, 9] }, + { search: "di", expected: [1, 4, 2, 3, 5] }, + { search: "dis", expected: [4, 2, 3, 5] }, + { search: "disp", expected: [4, 3, 5] }, + { search: "displ", expected: [4, 5] }, + { search: "displa", expected: [5] }, + ]; + + for (let i = 0; i < popularitySearch.length; i++) { + await checkInputItem(popularitySearch[i], i); + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js new file mode 100644 index 0000000000..b2dafd41e8 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch2.js @@ -0,0 +1,194 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Second Test suite for nsAbAutoCompleteSearch - test follow-on lookup after + * a previous search. + * + * We run this test without address books, constructing manually ourselves, + * so that we can ensure that we're not getting the data out of the address + * books. + */ + +var { getModelQuery } = ChromeUtils.import( + "resource:///modules/ABQueryUtils.jsm" +); + +// taken from nsAbAutoCompleteSearch.js +var ACR = Ci.nsIAutoCompleteResult; +var nsIAbAutoCompleteResult = Ci.nsIAbAutoCompleteResult; + +function nsAbAutoCompleteResult(aSearchString) { + // Can't create this in the prototype as we'd get the same array for + // all instances + this._searchResults = []; + this.searchString = aSearchString; + this.modelQuery = getModelQuery("mail.addr_book.autocompletequery.format"); + this.asyncDirectories = []; +} + +nsAbAutoCompleteResult.prototype = { + _searchResults: null, + + // nsIAutoCompleteResult + + modelQuery: null, + searchString: null, + searchResult: ACR.RESULT_NOMATCH, + defaultIndex: -1, + errorDescription: null, + + get matchCount() { + return this._searchResults.length; + }, + + getValueAt: function getValueAt(aIndex) { + return this._searchResults[aIndex].value; + }, + + getLabelAt: function getLabelAt(aIndex) { + return this.getValueAt(aIndex); + }, + + getCommentAt: function getCommentAt(aIndex) { + return this._searchResults[aIndex].comment; + }, + + getStyleAt: function getStyleAt(aIndex) { + return "local-abook"; + }, + + getImageAt: function getImageAt(aIndex) { + return ""; + }, + + getFinalCompleteValueAt(aIndex) { + return this.getValueAt(aIndex); + }, + + removeValueAt: function removeValueAt(aRowIndex, aRemoveFromDB) {}, + + // nsIAbAutoCompleteResult + + getCardAt: function getCardAt(aIndex) { + return this._searchResults[aIndex].card; + }, + + getEmailToUse: function getEmailToUse(aIndex) { + // For this test we can just use the primary email here. + return this._searchResults[aIndex].card.primaryEmail; + }, + + isCompleteResult: function isCompleteResult(aIndex) { + // For this test we claim all results are complete. + return true; + }, + + // nsISupports + + QueryInterface: ChromeUtils.generateQI([ + "nsIAutoCompleteResult", + "nsIAbAutoCompleteResult", + ]), +}; + +function createCard(chars, popularity) { + var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + + card.firstName = "firstName".slice(0, chars); + card.lastName = "lastName".slice(0, chars); + card.displayName = "displayName".slice(0, chars); + card.primaryEmail = "email".slice(0, chars) + "@foo.invalid"; + card.setProperty("NickName", "nickName".slice(0, chars)); + + return card; +} + +var results = [ + { email: "d <e@foo.invalid>", dirName: kPABData.dirName }, + { email: "di <em@foo.invalid>", dirName: kPABData.dirName }, + { email: "dis <ema@foo.invalid>", dirName: kPABData.dirName }, +]; + +var firstNames = [ + { search: "fi", expected: [1, 2] }, + { search: "fir", expected: [2] }, +]; + +var lastNames = [ + { search: "la", expected: [1, 2] }, + { search: "las", expected: [2] }, +]; + +var inputs = [firstNames, lastNames]; + +add_task(async () => { + // Test - Create a new search component + + var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService( + Ci.nsIAutoCompleteSearch + ); + + var obs = new acObserver(); + + // Ensure we've got the comment column set up for extra checking. + Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1); + + // Make up the last autocomplete result + var lastResult = new nsAbAutoCompleteResult(); + + lastResult.searchString = ""; + lastResult.searchResult = ACR.RESULT_SUCCESS; + lastResult.defaultIndex = 0; + lastResult.errorDescription = null; + for (let i = 0; i < results.length; ++i) { + lastResult._searchResults.push({ + value: results[i].email, + comment: results[i].dirName, + card: createCard(i + 1, 0), + }); + } + + // Test - Matches + + // Now check multiple matches + async function checkInputItem(element, index) { + let resultPromise = obs.waitForResult(); + acs.startSearch( + element.search, + JSON.stringify({ type: "addr_to", idKey: "" }), + lastResult, + obs + ); + await resultPromise; + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, element.search); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, element.expected.length); + + for (let i = 0; i < element.expected.length; ++i) { + Assert.equal( + obs._result.getValueAt(i), + results[element.expected[i]].email + ); + Assert.equal( + obs._result.getLabelAt(i), + results[element.expected[i]].email + ); + Assert.equal( + obs._result.getCommentAt(i), + results[element.expected[i]].dirName + ); + Assert.equal(obs._result.getStyleAt(i), "local-abook"); + Assert.equal(obs._result.getImageAt(i), ""); + } + } + + for (let inputSet of inputs) { + for (let i = 0; i < inputSet.length; i++) { + await checkInputItem(inputSet[i], i); + } + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js new file mode 100644 index 0000000000..4916c30bc5 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch3.js @@ -0,0 +1,164 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Third Test suite for nsAbAutoCompleteSearch - test for duplicate elimination + */ + +var ACR = Ci.nsIAutoCompleteResult; + +var cards = [ + { + email: "test@foo.invalid", + displayName: "", + popularityIndex: 0, + firstName: "test0", + value: "test@foo.invalid", + }, + { + email: "test@foo.invalid", + displayName: "", + popularityIndex: 1, + firstName: "test1", + value: "test@foo.invalid", + }, + { + email: "abc@foo.invalid", + displayName: "", + popularityIndex: 1, + firstName: "test2", + value: "abc@foo.invalid", + }, + { + email: "foo1@foo.invalid", + displayName: "d", + popularityIndex: 0, + firstName: "first1", + value: "d <foo1@foo.invalid>", + }, + { + email: "foo2@foo.invalid", + displayName: "di", + popularityIndex: 1, + firstName: "first1", + value: "di <foo2@foo.invalid>", + }, + { + email: "foo3@foo.invalid", + displayName: "dis", + popularityIndex: 2, + firstName: "first2", + value: "dis <foo3@foo.invalid>", + }, + { + email: "foo2@foo.invalid", + displayName: "di", + popularityIndex: 3, + firstName: "first2", + value: "di <foo2@foo.invalid>", + }, + // this just tests we can search for the special chars '(' and ')', bug 749097 + { + email: "bracket@not.invalid", + secondEmail: "h@not.invalid", + firstName: "Mr.", + displayName: "Mr. (Bracket)", + value: "Mr. (Bracket) <bracket@not.invalid>", + popularityIndex: 2, + }, + { + email: "mr@(bracket).not.invalid", + secondEmail: "bracket@not.invalid", + firstName: "Mr.", + displayName: "Mr. Bracket", + value: "Mr. Bracket <mr@(bracket).not.invalid>", + popularityIndex: 1, + }, +]; + +var duplicates = [ + { search: "test", expected: [1, 2] }, + { search: "first", expected: [6, 5, 3] }, + { search: "(bracket)", expected: [7, 8] }, +]; + +add_task(async () => { + // We set up the cards for this test manually as it is easier to set the + // popularity index and we don't need many. + + // Ensure all the directories are initialised. + MailServices.ab.directories; + + let ab = MailServices.ab.getDirectory(kPABData.URI); + + function createAndAddCard(element) { + var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + + card.primaryEmail = element.email; + card.displayName = element.displayName; + card.setProperty("PopularityIndex", element.popularityIndex); + card.firstName = element.firstName; + + ab.addCard(card); + } + + cards.forEach(createAndAddCard); + + // Test - duplicate elements + + var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService( + Ci.nsIAutoCompleteSearch + ); + + var obs = new acObserver(); + + async function checkInputItem(element, index) { + print("Search #" + index + ": search=" + element.search); + let resultPromise = obs.waitForResult(); + acs.startSearch( + element.search, + JSON.stringify({ type: "addr_to" }), + null, + obs + ); + await resultPromise; + + for (let i = 0; i < obs._result.matchCount; i++) { + print("... got " + i + ": " + obs._result.getValueAt(i)); + } + + for (let i = 0; i < element.expected.length; i++) { + print( + "... expected " + + i + + " (card " + + element.expected[i] + + "): " + + cards[element.expected[i]].value + ); + } + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, element.search); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, element.expected.length); + + for (let i = 0; i < element.expected.length; ++i) { + Assert.equal(obs._result.getValueAt(i), cards[element.expected[i]].value); + Assert.equal(obs._result.getLabelAt(i), cards[element.expected[i]].value); + Assert.equal(obs._result.getCommentAt(i), ""); + Assert.equal(obs._result.getStyleAt(i), "local-abook"); + Assert.equal(obs._result.getImageAt(i), ""); + obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult); + Assert.equal( + obs._result.getCardAt(i).firstName, + cards[element.expected[i]].firstName + ); + } + } + + for (let i = 0; i < duplicates.length; i++) { + await checkInputItem(duplicates[i], i); + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js new file mode 100644 index 0000000000..e1de6f1bbd --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch4.js @@ -0,0 +1,258 @@ +/* + * Fourth Test suite for nsAbAutoCompleteSearch - test for second email address. + */ + +var ACR = Ci.nsIAutoCompleteResult; + +var cards = [ + // Basic tests for primary and secondary emails. + { + email: "primary@test.invalid", + secondEmail: "second@test.invalid", + firstName: "", + }, + { + email: "test1@test.invalid", + secondEmail: "test2@test.invalid", + firstName: "firstName", + }, + { + email: "bar1@test.invalid", + secondEmail: "bar2@test.invalid", + firstName: "sweet", + }, + { + email: "boo1@test.invalid", + secondEmail: "boo2@test.invalid", + firstName: "sample", + }, + { + email: "name@test.invalid", + secondEmail: "thename@test.invalid", + firstName: "thename", + }, + // Test to check correct sorting of primary and secondary emails. + { + email: "foo_b@test.invalid", + secondEmail: "foo_a@test.invalid", + displayName: "sortbasic", + }, + { + email: "d@test.invalid", + secondEmail: "e@test.invalid", + displayName: "testsort", + }, + { + email: "c@test.invalid", + secondEmail: "a@test.invalid", + displayName: "testsort", + }, + // "2testsort" does the same as "testsort" but turns the cards around to + // ensure the order is always consistent. + { + email: "c@test.invalid", + secondEmail: "a@test.invalid", + displayName: "2testsort", + }, + { + email: "d@test.invalid", + secondEmail: "e@test.invalid", + displayName: "2testsort", + }, + { + email: "g@test.invalid", + secondEmail: "f@test.invalid", + displayName: "3testsort", + popularityIndex: 3, + }, + { + email: "j@test.invalid", + secondEmail: "h@test.invalid", + displayName: "3testsort", + popularityIndex: 5, + }, + // Add a contact that matches, but has no email. Should not show up. + { displayName: "primaryX" }, +]; + +// These are for the initial search +var searches = [ + "primary", + "second", + "firstName", + "thename", + "sortbasic", + "testsort", + "2testsort", + "3testsort", +]; + +var expectedResults = [ + ["primary@test.invalid", "second@test.invalid"], // searching for primary/second returns + [ + "second@test.invalid", // both the emails as the new search query + "primary@test.invalid", + ], // looks in both the fields. + ["test1@test.invalid", "test2@test.invalid"], + ["thename@test.invalid", "name@test.invalid"], + ["sortbasic <foo_b@test.invalid>", "sortbasic <foo_a@test.invalid>"], + [ + "testsort <c@test.invalid>", + "testsort <a@test.invalid>", + "testsort <d@test.invalid>", + "testsort <e@test.invalid>", + "3testsort <j@test.invalid>", + "3testsort <h@test.invalid>", + "3testsort <g@test.invalid>", + "3testsort <f@test.invalid>", + "2testsort <c@test.invalid>", + "2testsort <a@test.invalid>", + "2testsort <d@test.invalid>", + "2testsort <e@test.invalid>", + ], + [ + "2testsort <c@test.invalid>", + "2testsort <a@test.invalid>", + "2testsort <d@test.invalid>", + "2testsort <e@test.invalid>", + ], + [ + "3testsort <j@test.invalid>", + "3testsort <h@test.invalid>", + "3testsort <g@test.invalid>", + "3testsort <f@test.invalid>", + ], +]; + +// These are for subsequent searches - reducing the number of results. +var reductionSearches = ["b", "bo", "boo2"]; + +var reductionExpectedResults = [ + [ + "bar1@test.invalid", + "bar2@test.invalid", + "boo1@test.invalid", + "boo2@test.invalid", + "sortbasic <foo_b@test.invalid>", + "sortbasic <foo_a@test.invalid>", + ], + ["boo1@test.invalid", "boo2@test.invalid"], + ["boo2@test.invalid"], +]; + +add_task(async () => { + // We set up the cards for this test manually as it is easier to set the + // popularity index and we don't need many. + + // Ensure all the directories are initialised. + MailServices.ab.directories; + + let ab = MailServices.ab.getDirectory(kPABData.URI); + + function createAndAddCard(element) { + var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + + card.primaryEmail = element.email; + if ("secondEmail" in element) { + card.setProperty("SecondEmail", element.secondEmail); + } + card.displayName = element.displayName; + if ("popularityIndex" in element) { + card.setProperty("PopularityIndex", element.popularityIndex); + } + card.firstName = element.firstName; + + ab.addCard(card); + } + + cards.forEach(createAndAddCard); + + var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService( + Ci.nsIAutoCompleteSearch + ); + + var obs = new acObserver(); + + print("Checking Initial Searches"); + + async function checkSearch(element, index) { + print("Search #" + index + ": search=" + element); + let resultPromise = obs.waitForResult(); + acs.startSearch( + element, + JSON.stringify({ type: "addr_to", idKey: "" }), + null, + obs + ); + await resultPromise; + + for (let i = 0; i < obs._result.matchCount; i++) { + print("... got " + i + ": " + obs._result.getValueAt(i)); + } + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, element); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, expectedResults[index].length); + + for (let i = 0; i < expectedResults[index].length; ++i) { + Assert.equal(obs._result.getValueAt(i), expectedResults[index][i]); + Assert.equal(obs._result.getLabelAt(i), expectedResults[index][i]); + Assert.equal(obs._result.getCommentAt(i), ""); + Assert.equal(obs._result.getStyleAt(i), "local-abook"); + Assert.equal(obs._result.getImageAt(i), ""); + obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult); + } + } + + for (let i = 0; i < searches.length; i++) { + await checkSearch(searches[i], i); + } + + print("Checking Reduction of Search Results"); + + var lastResult = null; + + async function checkReductionSearch(element, index) { + let resultPromise = obs.waitForResult(); + acs.startSearch( + element, + JSON.stringify({ type: "addr_to", idKey: "" }), + lastResult, + obs + ); + await resultPromise; + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, element); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal( + obs._result.matchCount, + reductionExpectedResults[index].length + ); + + for (var i = 0; i < reductionExpectedResults[index].length; ++i) { + Assert.equal( + obs._result.getValueAt(i), + reductionExpectedResults[index][i] + ); + Assert.equal( + obs._result.getLabelAt(i), + reductionExpectedResults[index][i] + ); + Assert.equal(obs._result.getCommentAt(i), ""); + Assert.equal(obs._result.getStyleAt(i), "local-abook"); + Assert.equal(obs._result.getImageAt(i), ""); + obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult); + } + lastResult = obs._result; + } + + for (let i = 0; i < reductionSearches.length; i++) { + await checkReductionSearch(reductionSearches[i], i); + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js new file mode 100644 index 0000000000..a10ac5e4b4 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch5.js @@ -0,0 +1,120 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * This suite ensures that we can correctly read and re-set the popularity + * indexes on a + */ + +var ACR = Ci.nsIAutoCompleteResult; + +var results = [ + { email: "d <ema@test.invalid>", dirName: kPABData.dirName }, + { email: "di <emai@test.invalid>", dirName: kPABData.dirName }, + { email: "dis <email@test.invalid>", dirName: kPABData.dirName }, + { email: "disp <e@test.invalid>", dirName: kPABData.dirName }, + { email: "displ <em@test.invalid>", dirName: kPABData.dirName }, + { email: "t <list>", dirName: kPABData.dirName }, + { email: "te <lis>", dirName: kPABData.dirName }, + { email: "tes <li>", dirName: kPABData.dirName }, + // this contact has a nickname of "abcdef" + { email: "test <l>", dirName: kPABData.dirName }, +]; + +var firstNames = [ + { search: "f", expected: [4, 0, 1, 2, 3, 8] }, + { search: "fi", expected: [4, 0, 1, 3] }, + { search: "fir", expected: [4, 0, 1] }, + { search: "firs", expected: [0, 1] }, + { search: "first", expected: [1] }, +]; + +var lastNames = [ + { search: "l", expected: [5, 6, 7, 8, 4, 0, 1, 2, 3] }, + { search: "la", expected: [4, 0, 2, 3] }, + { search: "las", expected: [4, 0, 3] }, + { search: "last", expected: [4, 0] }, + { search: "lastn", expected: [0] }, +]; + +var inputs = [firstNames, lastNames]; + +add_task(async () => { + loadABFile("../../../data/tb2hexpopularity", kPABData.fileName); + + // Test - Create a new search component + + let acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService( + Ci.nsIAutoCompleteSearch + ); + + let obs = new acObserver(); + + // Ensure we've got the comment column set up for extra checking. + Services.prefs.setIntPref("mail.autoComplete.commentColumn", 1); + + // Test - Matches + + // Now check multiple matches + async function checkInputItem(element, index) { + print("Search #" + index + ": search=" + element.search); + let resultPromise = obs.waitForResult(); + acs.startSearch( + element.search, + JSON.stringify({ type: "addr_to" }), + null, + obs + ); + await resultPromise; + + for (let i = 0; i < obs._result.matchCount; i++) { + print("... got " + i + ": " + obs._result.getValueAt(i)); + } + + for (let i = 0; i < element.expected.length; i++) { + print( + "... expected " + + i + + " (card " + + element.expected[i] + + "): " + + results[element.expected[i]].email + ); + } + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, element.search); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, element.expected.length); + Assert.equal(obs._result.defaultIndex, 0); + + for (let i = 0; i < element.expected.length; ++i) { + Assert.equal( + obs._result.getValueAt(i), + results[element.expected[i]].email + ); + Assert.equal( + obs._result.getCommentAt(i), + results[element.expected[i]].dirName + ); + Assert.equal(obs._result.getStyleAt(i), "local-abook"); + Assert.equal(obs._result.getImageAt(i), ""); + + // Card at result number 4 is the one with the TB 2 popularity set as "a" + // in the file, so check that we're now setting the popularity to 10 + // and hence future tests don't have to convert it. + if (element.expected[i] == 4) { + let result = obs._result.QueryInterface(Ci.nsIAbAutoCompleteResult); + Assert.equal( + result.getCardAt(i).getProperty("PopularityIndex", -1), + 10 + ); + } + } + } + + for (let inputSet of inputs) { + for (let i = 0; i < inputSet.length; i++) { + await checkInputItem(inputSet[i], i); + } + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js new file mode 100644 index 0000000000..08b38de7c3 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch6.js @@ -0,0 +1,248 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/** + * Tests for for nsAbAutoCompleteSearch scoring. + */ + +var ACR = Ci.nsIAutoCompleteResult; + +var cards = [ + { + // 0 + email: "jd.who@example.com", + displayName: "John Doe (:xx)", + popularityIndex: 0, + firstName: "John", + value: "John Doe (:xx) <jd.who@example.com>", + }, + + { + // 1 + email: "janey_who@example.com", + displayName: "Jane Doe", + popularityIndex: 0, + value: "Jane Doe <janey_who@example.com>", + }, + + { + // 2 + email: "pf@example.com", + displayName: 'Paul "Shitbreak" Finch', + popularityIndex: 0, + value: 'Paul "Shitbreak" Finch <pf@example.com>', + }, + + { + // 3 + email: "js@example.com", + displayName: "Janine (Stifflers Mom)", + popularityIndex: 0, + value: "Janine (Stifflers Mom) <js@example.com>", + }, + + { + // 4 + email: "ex0@example.com", + displayName: "Ajden", + popularityIndex: 0, + value: "Ajden <ex0@example.com>", + }, + + { + // 5 + email: "5@example.com", + displayName: "Foxx", + popularityIndex: 0, + value: "Foxx <5@example.com>", + }, + + { + // 6 + email: "6@example.com", + displayName: "thewho", + popularityIndex: 0, + value: "thewho <6@example.com>", + }, + + { + // 7 + email: "7@example.com", + displayName: "fakeshit", + popularityIndex: 0, + value: "fakeshit <7@example.com>", + }, + + { + // 8 + email: "8@example.com", + displayName: "mastiff", + popularityIndex: 0, + value: "mastiff <8@example.com>", + }, + + { + // 9 + email: "9@example.com", + displayName: "anyjohn", + popularityIndex: 0, + value: "anyjohn <9@example.com>", + }, + + { + // 10 + email: "10@example.com", + displayName: "däsh l18n", + popularityIndex: 0, + value: "däsh l18n <10@example.com>", + }, + + { + // 11 + email: "11@example.com", + displayName: "paul mary", + popularityIndex: 0, + firstName: "paul", + lastName: "mary meyer", + value: "paul mary <11@example.com>", + }, + + { + // 12 + email: "12@example.com", + displayName: "paul meyer", + popularityIndex: 0, + firstName: "paul", + lastName: "mary meyer", + value: "paul meyer <12@example.com>", + }, + + { + // 13 + email: "13@example.com", + displayName: "mr iron man (exp dev)", + popularityIndex: 0, + firstName: "iron", + lastName: "man", + value: "mr iron man (exp dev) <13@example.com>", + }, + + { + // 14 + email: "14@example.com", + displayName: "michael", + popularityIndex: 0, + nickName: "short", + value: "michael <14@example.com>", + }, + + { + // 15 + email: "15@example.com", + displayName: "good boy", + popularityIndex: 0, + nickName: "sh", + value: "good boy <15@example.com>", + }, + + { + // 16 + email: "16@example.com", + displayName: "sherlock holmes", + popularityIndex: 0, + value: "sherlock holmes <16@example.com>", + }, +]; + +var inputs = [ + { search: "john", expected: [0, 9] }, + { search: "doe", expected: [1, 0] }, + { search: "jd", expected: [0, 4] }, + { search: "who", expected: [1, 0, 6] }, + { search: "xx", expected: [0, 5] }, + { search: "jan", expected: [1, 3] }, + // expecting nickname to score highest. + { search: "sh", expected: [15, 14, 2, 16, 10, 7] }, + { search: "st", expected: [3, 8] }, + { search: "paul mary", expected: [11, 12] }, + { search: '"paul mary"', expected: [11] }, + { search: '"iron man" mr "exp dev"', expected: [13] }, + { search: "short", expected: [14] }, +]; + +add_task(async () => { + // We set up the cards for this test manually as it is easier to set the + // popularity index and we don't need many. + + // Ensure all the directories are initialised. + MailServices.ab.directories; + + let ab = MailServices.ab.getDirectory(kPABData.URI); + + function createAndAddCard(element) { + var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + + card.primaryEmail = element.email; + card.displayName = element.displayName; + card.setProperty("PopularityIndex", element.popularityIndex); + card.firstName = element.firstName; + card.lastName = element.lastName; + if ("nickName" in element) { + card.setProperty("NickName", element.nickName); + } + + ab.addCard(card); + } + + cards.forEach(createAndAddCard); + + // Test - duplicate elements + + var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService( + Ci.nsIAutoCompleteSearch + ); + + var obs = new acObserver(); + + async function checkInputItem(element, index) { + print("Search #" + index + ": search=" + element.search); + let resultPromise = obs.waitForResult(); + acs.startSearch( + element.search, + JSON.stringify({ type: "addr_to" }), + null, + obs + ); + await resultPromise; + + for (let i = 0; i < obs._result.matchCount; i++) { + print("... got " + i + ": " + obs._result.getValueAt(i)); + } + + for (let i = 0; i < element.expected.length; i++) { + print( + "... expected " + + i + + " (card " + + element.expected[i] + + "): " + + cards[element.expected[i]].value + ); + } + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, element.search); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, element.expected.length); + + for (let i = 0; i < element.expected.length; ++i) { + Assert.equal(obs._result.getValueAt(i), cards[element.expected[i]].value); + Assert.equal(obs._result.getLabelAt(i), cards[element.expected[i]].value); + } + } + + for (let i = 0; i < inputs.length; i++) { + await checkInputItem(inputs[i], i); + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js new file mode 100644 index 0000000000..28bd2d1836 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbAutoCompleteSearch7.js @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Tests for nsAbAutoCompleteSearch - tests searching in address + * books for autocomplete matches, and checks sort order is correct + * according to scores. + */ + +var ACR = Ci.nsIAutoCompleteResult; + +// Input and results arrays for the autocomplete tests. + +// Note the expected arrays are in expected sort order as well. + +var results = [ + { email: "Tomas Doe <tomez.doe@foo.invalid>" }, // 0 + { email: "Tomas Doe <tomez.doe@foo2.invalid>" }, // 1 + { email: "Tomas Doe <tomez.doe@b.example.com>" }, // 2 + { email: "Tomas Doe <tomez.doe@a.example.com>" }, // 3 + { email: "Tomek Smith <tomek@example.com>" }, // 4 +]; + +var inputs = [ + [ + { search: "t", expected: [2, 3, 0, 1, 4] }, + { search: "tom", expected: [0, 1, 2, 3, 4] }, + { search: "tomek", expected: [4] }, + ], +]; + +var PAB_CARD_DATA = [ + { + FirstName: "Tomas", + LastName: "Doe", + DisplayName: "Tomas Doe", + NickName: "tom", + PrimaryEmail: "tomez.doe@foo.invalid", + SecondEmail: "tomez.doe@foo2.invalid", + PreferDisplayName: true, + PopularityIndex: 10, + // Poison the card data with an unparseable birthday. This will cause the + // vCard parser to throw an exception, but it should be caught and the + // search should carry on as normal. + BirthDay: 25, + BirthMonth: 9, + BirthYear: "NaN", + }, + { + FirstName: "Tomas", + LastName: "Doe", + DisplayName: "Tomas Doe", + PrimaryEmail: "tomez.doe@b.example.com", + SecondEmail: "tomez.doe@a.example.com", + PreferDisplayName: true, + PopularityIndex: 200, + }, + { + FirstName: "Tomek", + LastName: "Smith", + DisplayName: "Tomek Smith", + PrimaryEmail: "tomek@example.com", + PreferDisplayName: true, + PopularityIndex: 3, + }, +]; + +function setupAddressBookData(aDirURI, aCardData, aMailListData) { + let ab = MailServices.ab.getDirectory(aDirURI); + + // Getting all directories ensures we create all ABs because mailing + // lists need help initialising themselves + MailServices.ab.directories; + + for (let card of ab.childCards) { + ab.dropCard(card, false); + } + + aCardData.forEach(function (cd) { + let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + for (var prop in cd) { + card.setProperty(prop, cd[prop]); + } + ab.addCard(card); + }); + + aMailListData.forEach(function (ld) { + let list = Cc[ + "@mozilla.org/addressbook/directoryproperty;1" + ].createInstance(Ci.nsIAbDirectory); + list.isMailList = true; + for (var prop in ld) { + list[prop] = ld[prop]; + } + ab.addMailList(list); + }); +} + +add_task(async () => { + // Set up addresses for in the personal address book. + setupAddressBookData(kPABData.URI, PAB_CARD_DATA, []); + + // Test - Create a new search component + + var acs = Cc["@mozilla.org/autocomplete/search;1?name=addrbook"].getService( + Ci.nsIAutoCompleteSearch + ); + + var obs = new acObserver(); + + let param = JSON.stringify({ type: "addr_to" }); + + // Now check multiple matches + async function checkInputItem(element, index) { + let prevRes = obs._result; + print("Search #" + index + ": search=" + element.search); + let resultPromise = obs.waitForResult(); + acs.startSearch(element.search, param, prevRes, obs); + await resultPromise; + + for (let i = 0; i < obs._result.matchCount; i++) { + print("... got " + i + ": " + obs._result.getValueAt(i)); + } + for (let i = 0; i < element.expected.length; i++) { + print( + "... expected " + + i + + " (result " + + element.expected[i] + + "): " + + results[element.expected[i]].email + ); + } + + Assert.equal(obs._search, acs); + Assert.equal(obs._result.searchString, element.search); + Assert.equal(obs._result.searchResult, ACR.RESULT_SUCCESS); + Assert.equal(obs._result.errorDescription, null); + Assert.equal(obs._result.matchCount, element.expected.length); + Assert.equal(obs._result.defaultIndex, 0); + + for (let i = 0; i < element.expected.length; ++i) { + Assert.equal( + obs._result.getValueAt(i), + results[element.expected[i]].email + ); + Assert.equal( + obs._result.getLabelAt(i), + results[element.expected[i]].email + ); + Assert.equal(obs._result.getCommentAt(i), ""); + Assert.equal(obs._result.getStyleAt(i), "local-abook"); + Assert.equal(obs._result.getImageAt(i), ""); + } + } + + for (let inputSet of inputs) { + for (let i = 0; i < inputSet.length; i++) { + await checkInputItem(inputSet[i], i); + } + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js new file mode 100644 index 0000000000..37238c51e8 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager2.js @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsAbManager functions relating to add/delete directories and + * getting the list of directories.. + */ + +function checkDirs(aDirs, aDirArray) { + // Don't modify the passed in array. + var dirArray = aDirArray.concat(); + + for (let dir of aDirs) { + var loc = dirArray.indexOf(dir.URI); + + Assert.equal(MailServices.ab.getDirectory(dir.URI), dir); + + if (loc == -1) { + do_throw( + "Unexpected directory " + dir.URI + " found in address book list" + ); + } else { + dirArray[loc] = null; + } + } + + dirArray.forEach(function (value) { + Assert.equal(value, null); + }); +} + +function addDirectory(dirName) { + // Add the directory + let dirPrefId = MailServices.ab.newAddressBook(dirName, "", kPABData.dirType); + return MailServices.ab.getDirectoryFromId(dirPrefId); +} + +async function run_test() { + var expectedABs = [kPABData.URI, kCABData.URI]; + + // Test - Check initial directories + + checkDirs(MailServices.ab.directories, expectedABs); + + // Test - Add a directory + + var newDirectory1 = addDirectory("testAb1"); + + // Test - Check new directory list + expectedABs.push(newDirectory1.URI); + + checkDirs(MailServices.ab.directories, expectedABs); + + // Test - Repeat for a second directory + + var newDirectory2 = addDirectory("testAb2"); + + // Test - Check new directory list + expectedABs.push(newDirectory2.URI); + + checkDirs(MailServices.ab.directories, expectedABs); + + // Test - Remove a directory + + var pos = expectedABs.indexOf(newDirectory1.URI); + + expectedABs.splice(pos, 1); + + await promiseDirectoryRemoved(newDirectory1.URI); + newDirectory1 = null; + + // Test - Check new directory list + + checkDirs(MailServices.ab.directories, expectedABs); + + // Test - Repeat the removal + + await promiseDirectoryRemoved(newDirectory2.URI); + newDirectory2 = null; + + expectedABs.pop(); + + // Test - Check new directory list + checkDirs(MailServices.ab.directories, expectedABs); +} diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js new file mode 100644 index 0000000000..851017a593 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager3.js @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests that an address book, once renamed, is not deleted when a sibling address book is deleted. + */ + +function addDirectory(dirName) { + let dirPrefId = MailServices.ab.newAddressBook(dirName, "", kPABData.dirType); + return MailServices.ab.getDirectoryFromId(dirPrefId); +} + +function renameDirectory(directory, newName) { + directory.dirName = newName; +} + +/** + * Create 4 addressbooks (directories). Rename the second one and delete + * the third one. Check if their names are still correct. (bug 745664) + */ +async function run_test() { + let dirNames = ["testAb0", "testAb1", "testAb2", "testAb3"]; + let directories = []; + + for (let dirName of dirNames) { + directories.push(addDirectory(dirName)); + } + + dirNames[1] = "newTestAb1"; + renameDirectory(directories[1], dirNames[1]); + for (let dir in dirNames) { + Assert.equal(dirNames[dir], directories[dir].dirName); + } + await promiseDirectoryRemoved(directories[2].URI); + dirNames.splice(2, 1); + directories.splice(2, 1); + + for (let dir in dirNames) { + Assert.equal(dirNames[dir], directories[dir].dirName); + } +} diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js new file mode 100644 index 0000000000..9b9d5a124d --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager4.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Creating a new address book with the same name as an existing one should + * always produce a unique preference branch. Check that it does. + */ +add_task(function testSameName() { + let name0 = MailServices.ab.newAddressBook("name", null, kPABData.dirType); + equal(name0, "ldap_2.servers.name"); + + let name1 = MailServices.ab.newAddressBook("name", null, kPABData.dirType); + equal(name1, "ldap_2.servers.name_1"); + + let name2 = MailServices.ab.newAddressBook("name", null, kPABData.dirType); + equal(name2, "ldap_2.servers.name_2"); + + let name3 = MailServices.ab.newAddressBook("name", null, kPABData.dirType); + equal(name3, "ldap_2.servers.name_3"); +}); + +/** + * Tests that creating a new book with the UID argument assigns the UID to + * that book and stores it in the preferences. + */ +function subtestCreateWithUID(type, uidValue) { + let prefID = MailServices.ab.newAddressBook( + "Got a UID", + null, + type, + uidValue + ); + Assert.equal( + Services.prefs.getStringPref(`${prefID}.uid`, ""), + uidValue, + "UID is saved to the preferences" + ); + + let book = MailServices.ab.getDirectoryFromId(prefID); + Assert.equal(book.UID, uidValue, "created book has the right UID"); +} + +add_task(function testCreateWithUID_JS() { + subtestCreateWithUID( + Ci.nsIAbManager.JS_DIRECTORY_TYPE, + "01234567-89ab-cdef-0123-456789abcdef" + ); + + Assert.throws( + () => + MailServices.ab.newAddressBook( + "Should fail", + null, + Ci.nsIAbManager.JS_DIRECTORY_TYPE, + "01234567-89ab-cdef-0123-456789abcdef" + ), + /NS_ERROR_ABORT/, + "reusing a UID should throw an exception" + ); +}); + +add_task(function testCreateWithUID_CardDAV() { + subtestCreateWithUID( + Ci.nsIAbManager.CARDDAV_DIRECTORY_TYPE, + "456789ab-cdef-0123-4567-89abcdef0123" + ); +}); + +add_task(function testCreateWithUID_LDAP() { + subtestCreateWithUID( + Ci.nsIAbManager.LDAP_DIRECTORY_TYPE, + "89abcdef-0123-4567-89ab-cdef01234567" + ); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js new file mode 100644 index 0000000000..42eb370f7b --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager5.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function createAddressBook() { + Assert.ok(!MailServices.ab.getDirectoryFromUID("nonsense")); + + let pabFromURI = MailServices.ab.getDirectory(kPABData.URI); + let pabFromId = MailServices.ab.getDirectoryFromId(kPABData.dirPrefID); + let pabFromUID = MailServices.ab.getDirectoryFromUID(pabFromURI.UID); + + Assert.equal(pabFromId, pabFromURI); + Assert.equal(pabFromUID, pabFromURI); + + let historyFromURI = MailServices.ab.getDirectory(kCABData.URI); + let historyFromId = MailServices.ab.getDirectoryFromId(kCABData.dirPrefID); + let historyFromUID = MailServices.ab.getDirectoryFromUID(historyFromURI.UID); + + Assert.equal(historyFromId, historyFromURI); + Assert.equal(historyFromUID, historyFromURI); + Assert.notEqual(historyFromUID, pabFromUID); + + let newPrefId = MailServices.ab.newAddressBook( + "new book", + "", + kPABData.dirType + ); + let newFromId = MailServices.ab.getDirectoryFromId(newPrefId); + + let newFromURI = MailServices.ab.getDirectory(newFromId.URI); + let newFromUID = MailServices.ab.getDirectoryFromUID(newFromId.UID); + + Assert.equal(newFromId, newFromURI); + Assert.equal(newFromUID, newFromURI); + Assert.notEqual(newFromUID, pabFromUID); + Assert.notEqual(newFromUID, historyFromUID); + + await promiseDirectoryRemoved(newFromId.URI); + + Assert.ok(!MailServices.ab.getDirectoryFromUID(newFromId.UID)); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js b/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js new file mode 100644 index 0000000000..05beb37a9e --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsAbManager6.js @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Tests getMailListFromName() and mailListNameExists() which relies on it. + */ +add_task(function testGetMailListFromName() { + loadABFile("../../../data/abLists1", kPABData.fileName); + + for (let listName of ["TestList1", "TestList2", "TestList3"]) { + Assert.ok( + MailServices.ab.mailListNameExists(listName), + `AddrBookManager has ${listName}` + ); + + let list = MailServices.ab.getMailListFromName(listName); + Assert.ok(list, `"${listName}" is not null`); + Assert.equal( + list.dirName, + listName, + `"${listName}" dirName is "${listName}"` + ); + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js b/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js new file mode 100644 index 0000000000..7d51cecee1 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsIAbDirectory_getMailListFromName.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Test suite for the getMailListFromName() function. + */ + +"use strict"; + +/** + * Tests the getMailListFromName function returns the correct nsIAbDirectory, + * also tests the hasMailListWithName function as it uses the same code. + */ +add_task(function testGetMailListFromName() { + loadABFile("../../../data/abLists1", kPABData.fileName); + + // Test all top level lists are returned. + let root = MailServices.ab.getDirectory(kPABData.URI); + for (let listName of ["TestList1", "TestList2", "TestList3"]) { + Assert.ok(root.hasMailListWithName(listName), `parent has "${listName}"`); + + let list = root.getMailListFromName(listName); + Assert.ok(list, `"${listName}" is not null`); + Assert.equal( + list.dirName, + listName, + `"${listName}" dirName is "${listName}"` + ); + } + + Assert.ok( + !root.hasMailListWithName("Non existent"), + "hasMailListWithName() returns false for non-existent list name" + ); + Assert.ok( + !root.getMailListFromName("Non existent"), + "getMailListFromName() returns null for non-existent list name" + ); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js b/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js new file mode 100644 index 0000000000..b24b9ca20e --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_nsLDAPURL.js @@ -0,0 +1,428 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for nsLDAPURL functions. + */ + +// If we are still using the wallet service, then default port numbers +// are still visible in the password manager, and therefore we need to have +// them in the url. The toolkit login manager doesn't do this. +const usingWallet = "nsIWalletService" in Ci; +const portAdpt = usingWallet ? ":389" : ""; + +const ldapURLs = [ + { + url: "ldap://localhost/dc=test", + spec: "ldap://localhost/dc=test", + asciiSpec: "ldap://localhost/dc=test", + host: "localhost", + asciiHost: "localhost", + port: -1, + scheme: "ldap", + path: "/dc=test", + prePath: "ldap://localhost", + hostPort: "localhost", + displaySpec: "ldap://localhost/dc=test", + displayPrePath: "ldap://localhost", + displayHost: "localhost", + displayHostPort: "localhost", + dn: "dc=test", + scope: Ci.nsILDAPURL.SCOPE_BASE, + filter: "(objectclass=*)", + options: 0, + }, + { + url: "ldap://localhost:389/dc=test,dc=abc??sub?(objectclass=*)", + spec: + "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)", + asciiSpec: + "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)", + host: "localhost", + asciiHost: "localhost", + port: usingWallet ? 389 : -1, + scheme: "ldap", + path: "/dc=test,dc=abc??sub?(objectclass=*)", + prePath: "ldap://localhost" + portAdpt, + hostPort: "localhost" + portAdpt, + displaySpec: + "ldap://localhost" + portAdpt + "/dc=test,dc=abc??sub?(objectclass=*)", + displayPrePath: "ldap://localhost", + displayHost: "localhost", + displayHostPort: "localhost" + portAdpt, + dn: "dc=test,dc=abc", + scope: Ci.nsILDAPURL.SCOPE_SUBTREE, + filter: "(objectclass=*)", + options: 0, + }, + { + url: "ldap://\u65e5\u672c\u8a93.jp:389/dc=tes\u65e5t??one?(oc=xyz)", + spec: + "ldap://xn--wgv71a309e.jp" + portAdpt + "/dc=tes%E6%97%A5t??one?(oc=xyz)", + asciiSpec: + "ldap://xn--wgv71a309e.jp" + portAdpt + "/dc=tes%E6%97%A5t??one?(oc=xyz)", + host: "xn--wgv71a309e.jp", + asciiHost: "xn--wgv71a309e.jp", + port: usingWallet ? 389 : -1, + scheme: "ldap", + path: "/dc=tes%E6%97%A5t??one?(oc=xyz)", + prePath: "ldap://xn--wgv71a309e.jp" + portAdpt, + hostPort: "xn--wgv71a309e.jp" + portAdpt, + displaySpec: + "ldap://\u65e5\u672c\u8a93.jp" + + portAdpt + + "/dc=tes%E6%97%A5t??one?(oc=xyz)", + displayPrePath: "ldap://\u65e5\u672c\u8a93.jp" + portAdpt, + displayHost: "\u65e5\u672c\u8a93.jp", + displayHostPort: "\u65e5\u672c\u8a93.jp" + portAdpt, + dn: "dc=tes\u65e5t", + scope: Ci.nsILDAPURL.SCOPE_ONELEVEL, + filter: "(oc=xyz)", + options: 0, + }, + { + url: "ldaps://localhost/dc=test", + spec: "ldaps://localhost/dc=test", + asciiSpec: "ldaps://localhost/dc=test", + host: "localhost", + asciiHost: "localhost", + port: -1, + scheme: "ldaps", + path: "/dc=test", + prePath: "ldaps://localhost", + hostPort: "localhost", + displaySpec: "ldaps://localhost/dc=test", + displayPrePath: "ldaps://localhost", + displayHost: "localhost", + displayHostPort: "localhost", + dn: "dc=test", + scope: Ci.nsILDAPURL.SCOPE_BASE, + filter: "(objectclass=*)", + options: Ci.nsILDAPURL.OPT_SECURE, + }, + { + url: "ldaps://127.0.0.1/dc=test", + spec: "ldaps://127.0.0.1/dc=test", + asciiSpec: "ldaps://127.0.0.1/dc=test", + host: "127.0.0.1", + asciiHost: "127.0.0.1", + port: -1, + scheme: "ldaps", + path: "/dc=test", + prePath: "ldaps://127.0.0.1", + hostPort: "127.0.0.1", + displaySpec: "ldaps://127.0.0.1/dc=test", + displayPrePath: "ldaps://127.0.0.1", + displayHost: "127.0.0.1", + displayHostPort: "127.0.0.1", + dn: "dc=test", + scope: Ci.nsILDAPURL.SCOPE_BASE, + filter: "(objectclass=*)", + options: Ci.nsILDAPURL.OPT_SECURE, + }, + { + url: "ldaps://[::1]/dc=test", + spec: "ldaps://[::1]/dc=test", + asciiSpec: "ldaps://[::1]/dc=test", + host: "::1", + asciiHost: "::1", + port: -1, + scheme: "ldaps", + path: "/dc=test", + prePath: "ldaps://[::1]", + hostPort: "[::1]", + displaySpec: "ldaps://[::1]/dc=test", + displayPrePath: "ldaps://[::1]", + displayHost: "::1", + displayHostPort: "[::1]", + dn: "dc=test", + scope: Ci.nsILDAPURL.SCOPE_BASE, + filter: "(objectclass=*)", + options: Ci.nsILDAPURL.OPT_SECURE, + }, +]; + +function run_test() { + var url; + + // Test - get and check urls. + + for (let part = 0; part < ldapURLs.length; ++part) { + dump("url: " + ldapURLs[part].url + "\n"); + url = Services.io.newURI(ldapURLs[part].url); + + Assert.equal(url.spec, ldapURLs[part].spec); + Assert.equal(url.asciiSpec, ldapURLs[part].asciiSpec); + Assert.equal(url.scheme, ldapURLs[part].scheme); + Assert.equal(url.host, ldapURLs[part].host); + Assert.equal(url.asciiHost, ldapURLs[part].asciiHost); + Assert.equal(url.port, ldapURLs[part].port); + Assert.equal(url.pathQueryRef, ldapURLs[part].path); + Assert.equal(url.prePath, ldapURLs[part].prePath); + Assert.equal(url.hostPort, ldapURLs[part].hostPort); + Assert.equal(url.displaySpec, ldapURLs[part].displaySpec); + Assert.equal(url.displayPrePath, ldapURLs[part].displayPrePath); + Assert.equal(url.displayHost, ldapURLs[part].displayHost); + Assert.equal(url.displayHostPort, ldapURLs[part].displayHostPort); + // XXX nsLDAPURL ought to have classinfo. + url = url.QueryInterface(Ci.nsILDAPURL); + Assert.equal(url.dn, ldapURLs[part].dn); + Assert.equal(url.scope, ldapURLs[part].scope); + Assert.equal(url.filter, ldapURLs[part].filter); + Assert.equal(url.options, ldapURLs[part].options); + } + + // Test - Check changing ldap values + dump("Other Tests\n"); + + // Start off with a base url + const kBaseURL = "ldap://localhost:389/dc=test,dc=abc??sub?(objectclass=*)"; + + url = Services.io.newURI(kBaseURL).QueryInterface(Ci.nsILDAPURL); + + // Test - dn + + url.dn = "dc=short"; + + Assert.equal(url.dn, "dc=short"); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??sub?(objectclass=*)" + ); + + // Test - scope + + url.scope = Ci.nsILDAPURL.SCOPE_BASE; + + Assert.equal(url.scope, Ci.nsILDAPURL.SCOPE_BASE); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short???(objectclass=*)" + ); + + url.scope = Ci.nsILDAPURL.SCOPE_ONELEVEL; + + Assert.equal(url.scope, Ci.nsILDAPURL.SCOPE_ONELEVEL); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + // Test - filter + + url.filter = "(&(oc=ygh)(l=Ереван))"; + + Assert.equal(url.filter, "(&(oc=ygh)(l=Ереван))"); + Assert.equal( + url.spec, + "ldap://localhost" + + portAdpt + + "/dc=short??one?(&(oc=ygh)(l=%D0%95%D1%80%D0%B5%D0%B2%D0%B0%D0%BD))" + ); + + url.filter = ""; + + Assert.equal(url.filter, "(objectclass=*)"); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + // Test - scheme + + // An old version used to have a bug whereby if you set the scheme to the + // same thing twice, you'd get the options set wrongly. + url = url + .mutate() + .setScheme("ldaps") + .finalize() + .QueryInterface(Ci.nsILDAPURL); + Assert.equal(url.options, 1); + Assert.equal( + url.spec, + "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + url = url + .mutate() + .setScheme("ldaps") + .finalize() + .QueryInterface(Ci.nsILDAPURL); + Assert.equal(url.options, 1); + Assert.equal( + url.spec, + "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + Assert.ok(url.schemeIs("ldaps")); + Assert.ok(!url.schemeIs("ldap")); + + url = url.mutate().setScheme("ldap").finalize().QueryInterface(Ci.nsILDAPURL); + Assert.equal(url.options, 0); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + url = url.mutate().setScheme("ldap").finalize().QueryInterface(Ci.nsILDAPURL); + Assert.equal(url.options, 0); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + Assert.ok(url.schemeIs("ldap")); + Assert.ok(!url.schemeIs("ldaps")); + + // Test - Options + + url.options = Ci.nsILDAPURL.OPT_SECURE; + + Assert.equal(url.options, Ci.nsILDAPURL.OPT_SECURE); + Assert.equal( + url.spec, + "ldaps://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + url.options = 0; + + Assert.equal(url.options, 0); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + // Test - Equals + + var url2 = Services.io + .newURI("ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)") + .QueryInterface(Ci.nsILDAPURL); + + Assert.ok(url.equals(url2)); + + url2 = url2 + .mutate() + .setSpec("ldap://localhost:389/dc=short??sub?(objectclass=*)") + .finalize(); + + Assert.ok(!url.equals(url2)); + + // Test Attributes + + Assert.equal(url.attributes.length, 0); + + // Nothing should happen if the attribute doesn't exist + url.removeAttribute("abc"); + + Assert.equal(url.attributes.length, 0); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + url.addAttribute("dn"); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short?dn?one?(objectclass=*)" + ); + + Assert.equal(url.attributes, "dn"); + + url.removeAttribute("dn"); + + Assert.equal(url.attributes.length, 0); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + var newAttrs = "abc,def,ghi,jkl"; + url.attributes = newAttrs; + + Assert.equal(url.attributes, newAttrs); + Assert.equal( + url.spec, + "ldap://localhost" + + portAdpt + + "/dc=short?" + + newAttrs + + "?one?(objectclass=*)" + ); + + // Try adding an existing attribute - should do nothing + url.addAttribute("def"); + Assert.equal(url.attributes, newAttrs); + + // url.addAttribute("jk"); + + Assert.ok(url.hasAttribute("jkl")); + Assert.ok(url.hasAttribute("def")); + Assert.ok(url.hasAttribute("ABC")); + Assert.ok(!url.hasAttribute("cde")); + Assert.ok(!url.hasAttribute("3446")); + Assert.ok(!url.hasAttribute("kl")); + Assert.ok(!url.hasAttribute("jk")); + + // Sub-string of an attribute, so this shouldn't change anything. + url.removeAttribute("kl"); + url.removeAttribute("jk"); + url.removeAttribute("ef"); + Assert.equal(url.attributes, newAttrs); + + url.removeAttribute("abc"); + newAttrs = newAttrs.substring(4); + + Assert.equal(url.attributes, newAttrs); + Assert.equal( + url.spec, + "ldap://localhost" + + portAdpt + + "/dc=short?" + + newAttrs + + "?one?(objectclass=*)" + ); + + // This shouldn't fail, just clear the list + url.attributes = ""; + + Assert.equal(url.attributes.length, 0); + Assert.equal( + url.spec, + "ldap://localhost" + portAdpt + "/dc=short??one?(objectclass=*)" + ); + + // Set attributes via the url spec + + newAttrs = "abc,def,ghi,jkl"; + url = url + .mutate() + .setSpec("ldap://localhost/dc=short?" + newAttrs + "?one?(objectclass=*)") + .finalize() + .QueryInterface(Ci.nsILDAPURL); + + Assert.equal(url.attributes, newAttrs); + Assert.equal( + url.spec, + "ldap://localhost/dc=short?" + newAttrs + "?one?(objectclass=*)" + ); + + url = url + .mutate() + .setSpec("ldap://localhost/dc=short??one?(objectclass=*)") + .finalize() + .QueryInterface(Ci.nsILDAPURL); + + var attrs = url.attributes; + Assert.equal(attrs.length, 0); + Assert.equal(url.spec, "ldap://localhost/dc=short??one?(objectclass=*)"); + + // Test - clone + + url = url + .mutate() + .setSpec("ldap://localhost/dc=short?abc,def,ghi,jkl?one?(objectclass=*)") + .finalize(); + + var newUrl = url.mutate().finalize(); + + Assert.equal( + newUrl.spec, + "ldap://localhost/dc=short?abc,def,ghi,jkl?one?(objectclass=*)" + ); +} diff --git a/comm/mailnews/addrbook/test/unit/test_photoURL.js b/comm/mailnews/addrbook/test/unit/test_photoURL.js new file mode 100644 index 0000000000..a6e7796264 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_photoURL.js @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm"); + +/** + * Tests that vCard photo data is correctly translated into a URL for display. + */ +add_task(async function testVCardPhotoURL() { + let jpegPrefix = "data:image/jpeg;base64,/9j/4AAQSkZJRgABA"; + let pngPrefix = "data:image/png;base64,iVBORw0KGgoAAAANSU"; + + for (let [fileName, expectedURL] of [ + // Version 3, binary data, binary value type + ["v3-binary-jpeg.vcf", jpegPrefix], + ["v3-binary-png.vcf", pngPrefix], + // Version 3, URI data, binary value type (mismatch) + ["v3-uri-binary-jpeg.vcf", jpegPrefix], + ["v3-uri-binary-png.vcf", pngPrefix], + // Version 3, URI data, URI value type + ["v3-uri-uri-jpeg.vcf", jpegPrefix], + ["v3-uri-uri-png.vcf", pngPrefix], + // Version 4, URI data, URI value type + ["v4-uri-jpeg.vcf", jpegPrefix], + ["v4-uri-png.vcf", pngPrefix], + ]) { + info(`testing ${fileName}`); + let file = do_get_file(`data/${fileName}`); + let vCard = await IOUtils.readUTF8(file.path); + let card = VCardUtils.vCardToAbCard(vCard); + + Assert.equal(card.photoURL.substring(0, 40), expectedURL); + } +}); diff --git a/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js b/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js new file mode 100644 index 0000000000..e879b05cbc --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_preferDisplayName.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 { AddrBookCard } = ChromeUtils.import( + "resource:///modules/AddrBookCard.jsm" +); + +/** + * Tests that the mail.displayname.version preference is correctly incremented + * if a card's DisplayName or PreferDisplayName properties change. + */ +add_task(async function () { + function getPrefValue() { + return Services.prefs.getIntPref("mail.displayname.version", -999); + } + + /** + * Effectively the same as the function of the same name in nsMsgDBView.cpp. + * This proves the cardForEmailAddress cache in AddrBookManager is correctly + * cleared when the preference changes. + */ + function getDisplayNameInAddressBook() { + let card = MailServices.ab.cardForEmailAddress("first.last@invalid"); + if (!card) { + return null; + } + + let preferDisplayName = card.getPropertyAsBool("PreferDisplayName", true); + return preferDisplayName ? card.displayName : card.primaryEmail; + } + + Assert.equal(getPrefValue(), -999, "pref has no initial value"); + Assert.equal(getDisplayNameInAddressBook(), null, "card doesn't exist yet"); + + let book = MailServices.ab.getDirectory(kPABData.URI); + let card = new AddrBookCard(); + card.firstName = "first"; + card.lastName = "last"; + card.displayName = "first last"; + card.primaryEmail = "first.last@invalid"; + book.addCard(card); + + Assert.equal(getPrefValue(), 1, "pref created by adding card"); + Assert.equal(getDisplayNameInAddressBook(), "first last"); + + [card] = book.childCards; + card.displayName = "display"; + book.modifyCard(card); + + Assert.equal(getPrefValue(), 2, "pref updated by changing display name"); + Assert.equal(getDisplayNameInAddressBook(), "display"); + + [card] = book.childCards; + card.setPropertyAsBool("PreferDisplayName", true); + book.modifyCard(card); + + Assert.equal(getPrefValue(), 3, "pref updated by adding flag"); + Assert.equal(getDisplayNameInAddressBook(), "display"); + + [card] = book.childCards; + card.displayName = "display name"; + book.modifyCard(card); + + Assert.equal(getPrefValue(), 4, "pref updated by changing display name"); + Assert.equal(getDisplayNameInAddressBook(), "display name"); + + [card] = book.childCards; + card.setPropertyAsBool("PreferDisplayName", false); + book.modifyCard(card); + + Assert.equal(getPrefValue(), 5, "pref updated by clearing flag"); + Assert.equal(getDisplayNameInAddressBook(), "first.last@invalid"); + + book.deleteCards([card]); + + Assert.equal(getPrefValue(), 6, "pref updated by deleting card"); + Assert.equal(getDisplayNameInAddressBook(), null, "card no longer exists"); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_search.js b/comm/mailnews/addrbook/test/unit/test_search.js new file mode 100644 index 0000000000..c25ba17b96 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_search.js @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { getModelQuery, generateQueryURI } = ChromeUtils.import( + "resource:///modules/ABQueryUtils.jsm" +); + +const jsonFile = do_get_file("data/ldap_contacts.json"); + +add_task(async () => { + let contacts = await IOUtils.readJSON(jsonFile.path); + + let dirPrefId = MailServices.ab.newAddressBook( + "new book", + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + let book = MailServices.ab.getDirectoryFromId(dirPrefId); + + for (let [name, { attributes }] of Object.entries(contacts)) { + let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + card.displayName = attributes.cn; + card.firstName = attributes.givenName; + card.lastName = attributes.sn; + card.primaryEmail = attributes.mail; + contacts[name] = book.addCard(card); + } + + let doSearch = async function (searchString, ...expectedContacts) { + let foundCards = await new Promise(resolve => { + let listener = { + cards: [], + onSearchFoundCard(card) { + this.cards.push(card); + }, + onSearchFinished(status, complete, secInfo, location) { + resolve(this.cards); + }, + }; + book.search(searchString, "", listener); + }); + + Assert.equal(foundCards.length, expectedContacts.length); + for (let name of expectedContacts) { + Assert.ok(foundCards.find(c => c.equals(contacts[name]))); + } + }; + + await doSearch("(DisplayName,c,watson)", "john", "mary"); + + let modelQuery = getModelQuery("mail.addr_book.autocompletequery.format"); + await doSearch( + generateQueryURI(modelQuery, ["holmes"]), + "eurus", + "mycroft", + "sherlock" + ); + await doSearch(generateQueryURI(modelQuery, ["adler"]), "irene"); + await doSearch(generateQueryURI(modelQuery, ["redbeard"])); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_vCard.js b/comm/mailnews/addrbook/test/unit/test_vCard.js new file mode 100644 index 0000000000..328be1c8cd --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_vCard.js @@ -0,0 +1,474 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +let { VCardProperties, VCardUtils } = ChromeUtils.import( + "resource:///modules/VCardUtils.jsm" +); + +const ANY_UID = "UID:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"; + +add_task(function testVCardToPropertyMap() { + function check(vCardLine, expectedProps) { + let vCard = `BEGIN:VCARD\r\n${vCardLine}\r\nEND:VCARD\r\n`; + info(vCard); + let properties = VCardProperties.fromVCard(vCard).toPropertyMap(); + // Check that every property in expectedProps is present in `properties`. + // No other property can be present unless it is in `propWhitelist`. + for (let [name, value] of properties) { + if (name in expectedProps) { + Assert.equal(value, expectedProps[name], `expected ${name}`); + delete expectedProps[name]; + } else { + Assert.ok(false, `card should not have property '${name}'`); + } + } + + for (let name of Object.keys(expectedProps)) { + Assert.ok(false, `expected ${name} not found`); + } + } + + // Name + check("N:Last;First", { FirstName: "First", LastName: "Last" }); + check("N:Last;First;;;", { FirstName: "First", LastName: "Last" }); + check("N:Last;First;Middle;Prefix;Suffix", { + FirstName: "First", + LastName: "Last", + AdditionalNames: "Middle", + NamePrefix: "Prefix", + NameSuffix: "Suffix", + }); + check("N:Stevenson;John;Philip,Paul;Dr.;Jr.,M.D.,A.C.P.", { + FirstName: "John", + LastName: "Stevenson", + AdditionalNames: "Philip Paul", + NamePrefix: "Dr.", + NameSuffix: "Jr. M.D. A.C.P.", + }); + + // Address + check( + "ADR:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.", + { + WorkPOBox: "PO Box 3.14", + WorkAddress2: "Apartment 4", + WorkAddress: "123 Main Street", + WorkCity: "Any Town", + WorkState: "CA", + WorkZipCode: "91921-1234", + WorkCountry: "U.S.A.", + } + ); + check( + "ADR;TYPE=work:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.", + { + WorkPOBox: "PO Box 3.14", + WorkAddress2: "Apartment 4", + WorkAddress: "123 Main Street", + WorkCity: "Any Town", + WorkState: "CA", + WorkZipCode: "91921-1234", + WorkCountry: "U.S.A.", + } + ); + check( + "ADR;TYPE=home:PO Box 3.14;Apartment 4;123 Main Street;Any Town;CA;91921-1234;U.S.A.", + { + HomePOBox: "PO Box 3.14", + HomeAddress2: "Apartment 4", + HomeAddress: "123 Main Street", + HomeCity: "Any Town", + HomeState: "CA", + HomeZipCode: "91921-1234", + HomeCountry: "U.S.A.", + } + ); + + // Phone + check("TEL:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); + check("TEL;TYPE=work:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); + check("TEL;TYPE=home:11-2358-13-21", { HomePhone: "11-2358-13-21" }); + check("TEL;TYPE=cell:11-2358-13-21", { CellularNumber: "11-2358-13-21" }); + check("TEL;TYPE=pager:11-2358-13-21", { PagerNumber: "11-2358-13-21" }); + check("TEL;TYPE=fax:11-2358-13-21", { FaxNumber: "11-2358-13-21" }); + + check("TEL;TYPE=work;PREF:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); + check("TEL;TYPE=work,cell:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); + check("TEL;TYPE=work;TYPE=cell:11-2358-13-21", { + WorkPhone: "11-2358-13-21", + }); + check("TEL;TYPE=work;VALUE=TEXT:11-2358-13-21", { + WorkPhone: "11-2358-13-21", + }); + check("TEL;TYPE=home;VALUE=TEXT:011-2358-13-21", { + HomePhone: "011-2358-13-21", + }); + check( + "TEL;TYPE=work;VALUE=TEXT:11-2358-13-21\r\nTEL;TYPE=home;VALUE=TEXT:011-2358-13-21", + { + WorkPhone: "11-2358-13-21", + HomePhone: "011-2358-13-21", + } + ); + check("TEL;TYPE=cell:11-2358-13-21\r\nTEL;TYPE=cell:011-2358-13-21", { + CellularNumber: "11-2358-13-21", + }); + check("TEL;TYPE=cell;PREF=1:11-2358-13-21\r\nTEL;TYPE=cell:011-2358-13-21", { + CellularNumber: "11-2358-13-21", + }); + check("TEL;TYPE=cell:11-2358-13-21\r\nTEL;TYPE=cell;PREF=1:011-2358-13-21", { + CellularNumber: "011-2358-13-21", + }); + + // Birthday + check("BDAY;VALUE=DATE:19830403", { + BirthDay: "3", + BirthMonth: "4", + BirthYear: "1983", + }); + check("BDAY:--0415", { BirthDay: "15", BirthMonth: "4" }); + check("BDAY:2001", { BirthYear: "2001" }); + check("BDAY:2006-06", { BirthYear: "2006", BirthMonth: "6" }); + check("BDAY:--12", { BirthMonth: "12" }); + check("BDAY:---30", { BirthDay: "30" }); + // These are error cases, testing that it doesn't throw. + check("BDAY;VALUE=DATE:NaN-NaN-NaN", {}); + check("BDAY;VALUE=TEXT:07/07/1949", {}); + + // Anniversary + check("ANNIVERSARY;VALUE=DATE:20041207", { + AnniversaryDay: "7", + AnniversaryMonth: "12", + AnniversaryYear: "2004", + }); + + // Organization: any number of values is valid here. + check("ORG:Acme Widgets, Inc.", { + Company: "Acme Widgets, Inc.", + }); + check("ORG:Acme Widgets, Inc.;Manufacturing", { + Company: "Acme Widgets, Inc.", + Department: "Manufacturing", + }); + check("ORG:Acme Widgets, Inc.;Manufacturing;Thingamies", { + Company: "Acme Widgets, Inc.", + Department: "Manufacturing", + }); + + // URL + // If no type is given assume its WebPage1 (work). + check("URL:https://www.thunderbird.net/", { + WebPage1: "https://www.thunderbird.net/", + }); + + check("URL;TYPE=work:https://developer.thunderbird.net/", { + WebPage1: "https://developer.thunderbird.net/", + }); + + check("URL;TYPE=home:https://addons.thunderbird.net/", { + WebPage2: "https://addons.thunderbird.net/", + }); + + check( + formatVCard` + URL;TYPE=home:https://addons.thunderbird.net/ + URL;TYPE=work:https://developer.thunderbird.net/`, + { + WebPage1: "https://developer.thunderbird.net/", + WebPage2: "https://addons.thunderbird.net/", + } + ); + + // If a URL without a type is given and a Work Web Page do not import the URL without type. + check( + formatVCard` + URL:https://www.thunderbird.net/ + URL;TYPE=home:https://addons.thunderbird.net/ + URL;TYPE=work:https://developer.thunderbird.net/`, + { + WebPage1: "https://developer.thunderbird.net/", + WebPage2: "https://addons.thunderbird.net/", + } + ); + // Email: just to be difficult, email is stored by priority, not type. + check("EMAIL:first@invalid", { PrimaryEmail: "first@invalid" }); + check("EMAIL;PREF=1:first@invalid", { PrimaryEmail: "first@invalid" }); + + check("EMAIL;PREF=1:first@invalid\r\nEMAIL:second@invalid", { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + }); + check("EMAIL:second@invalid\r\nEMAIL;PREF=1:first@invalid", { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + }); + + check("EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid", { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + }); + check("EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid", { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + }); + + check( + "EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=3:third@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=1:first@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=2:second@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=2:second@invalid\r\nEMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=3:third@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + check( + "EMAIL;PREF=1:first@invalid\r\nEMAIL;PREF=3:third@invalid\r\nEMAIL;PREF=2:second@invalid", + { + PrimaryEmail: "first@invalid", + SecondEmail: "second@invalid", + } + ); + + // Group-prefixed properties. + check( + formatVCard` + item1.EMAIL:first@invalid + item1.X-ABLabel:First`, + { + PrimaryEmail: "first@invalid", + } + ); + check( + formatVCard` + item1.EMAIL:first@invalid + item1.X-ABLabel:First + item2.EMAIL:second@invalid + item2.X-ABLabel:Second`, + { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" } + ); + check( + formatVCard` + foo-bar.EMAIL:first@invalid + foo-bar.X-ABLabel:First + EMAIL:second@invalid`, + { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" } + ); + check( + formatVCard` + EMAIL:first@invalid + abc.EMAIL:second@invalid + abc.X-ABLabel:Second`, + { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" } + ); + check("xyz.TEL:11-2358-13-21", { WorkPhone: "11-2358-13-21" }); +}); + +add_task(function testAbCardToVCard() { + function check(abCardProps, ...expectedLines) { + let abCard = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + for (let [name, value] of Object.entries(abCardProps)) { + if (name == "UID") { + abCard.UID = abCardProps.UID; + continue; + } + abCard.setProperty(name, value); + } + + let vCard = VCardUtils.abCardToVCard(abCard); + info(vCard); + let vCardLines = vCard.split("\r\n"); + if (expectedLines.includes(ANY_UID)) { + for (let i = 0; i < vCardLines.length; i++) { + if (vCardLines[i].startsWith("UID:")) { + vCardLines[i] = ANY_UID; + } + } + } + + for (let line of expectedLines) { + Assert.ok(vCardLines.includes(line), line); + } + } + + // UID + check( + { + UID: "12345678-1234-1234-1234-123456789012", + }, + "UID:12345678-1234-1234-1234-123456789012" + ); + + // Name + check( + { + FirstName: "First", + LastName: "Last", + }, + "N:Last;First;;;", + ANY_UID + ); + check( + { + FirstName: "First", + LastName: "Last", + AdditionalNames: "Middle", + NamePrefix: "Prefix", + NameSuffix: "Suffix", + }, + "N:Last;First;Middle;Prefix;Suffix", + ANY_UID + ); + check( + { + FirstName: "First", + LastName: "Last", + NameSuffix: "Suffix", + }, + "N:Last;First;;;Suffix", + ANY_UID + ); + + // Address + check( + { + WorkAddress: "123 Main Street", + WorkCity: "Any Town", + WorkState: "CA", + WorkZipCode: "91921-1234", + WorkCountry: "U.S.A.", + }, + "ADR:;;123 Main Street;Any Town;CA;91921-1234;U.S.A.", + ANY_UID + ); + check( + { + HomeAddress: "123 Main Street", + HomeCity: "Any Town", + HomeState: "CA", + HomeZipCode: "91921-1234", + HomeCountry: "U.S.A.", + }, + "ADR:;;123 Main Street;Any Town;CA;91921-1234;U.S.A.", + ANY_UID + ); + + // Phone + check( + { + WorkPhone: "11-2358-13-21", + }, + "TEL;VALUE=TEXT:11-2358-13-21", + ANY_UID + ); + check( + { + HomePhone: "011-2358-13-21", + }, + "TEL;VALUE=TEXT:011-2358-13-21", + ANY_UID + ); + check( + { + WorkPhone: "11-2358-13-21", + HomePhone: "011-2358-13-21", + }, + "TEL;TYPE=work;VALUE=TEXT:11-2358-13-21", + "TEL;TYPE=home;VALUE=TEXT:011-2358-13-21", + ANY_UID + ); + + // Birthday + check( + { + BirthDay: "3", + BirthMonth: "4", + BirthYear: "1983", + }, + "BDAY;VALUE=DATE:19830403", + ANY_UID + ); + check( + { + BirthDay: "3", + BirthMonth: "4", + BirthYear: "", // No value. + }, + "BDAY;VALUE=DATE:--0403", + ANY_UID + ); + check( + { + BirthDay: "3", + BirthMonth: "4", + // BirthYear missing altogether. + }, + "BDAY;VALUE=DATE:--0403", + ANY_UID + ); + check( + { + BirthDay: "", // No value. + BirthMonth: "", // No value. + BirthYear: "1983", + }, + "BDAY;VALUE=DATE:1983", + ANY_UID + ); + check( + { + BirthDay: "", // No value. + BirthMonth: "", // No value. + BirthYear: "", // No value. + }, + ANY_UID + ); + + // Anniversary + check( + { + AnniversaryDay: "7", + AnniversaryMonth: "12", + AnniversaryYear: "2004", + }, + "ANNIVERSARY;VALUE=DATE:20041207", + ANY_UID + ); + + // Email + check({ PrimaryEmail: "first@invalid" }, "EMAIL;PREF=1:first@invalid"); + check({ SecondEmail: "second@invalid" }, "EMAIL:second@invalid"); + check( + { PrimaryEmail: "first@invalid", SecondEmail: "second@invalid" }, + "EMAIL;PREF=1:first@invalid", + "EMAIL:second@invalid" + ); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_vCard21.js b/comm/mailnews/addrbook/test/unit/test_vCard21.js new file mode 100644 index 0000000000..28fb1b21d4 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_vCard21.js @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +let { VCardUtils } = ChromeUtils.import("resource:///modules/VCardUtils.jsm"); + +add_task(async () => { + function check(vCardLines, expectedProps) { + checkWithCase(vCardLines, expectedProps.slice(), false); + checkWithCase( + vCardLines, + expectedProps.map(p => { + if (p.params?.type) { + p.params.type = p.params.type.toLowerCase(); + } + return p; + }), + true + ); + } + + function checkWithCase(vCardLines, expectedProps, lowerCase) { + let vCard = `BEGIN:VCARD\r\nVERSION:2.1\r\n${vCardLines}\r\nEND:VCARD\r\n`; + if (lowerCase) { + vCard = vCard.toLowerCase(); + } + info(vCard); + let abCard = VCardUtils.vCardToAbCard(vCard); + for (let propertyEntry of abCard.vCardProperties.entries) { + let index = expectedProps.findIndex( + p => + p.name == propertyEntry.name && + p.value.toString() == propertyEntry.value.toString() + ); + Assert.greater(index, -1); + let [prop] = expectedProps.splice(index, 1); + Assert.deepEqual(propertyEntry.params, prop.params ?? {}); + } + + for (let { name, value } of expectedProps) { + ok(false, `expected ${name}=${value} not found`); + } + } + + // Different types of phone number. + check("TEL:1234567", [{ name: "tel", value: "1234567" }]); + check("TEL;PREF:1234567", [ + { name: "tel", value: "1234567", params: { pref: 1 } }, + ]); + check("TEL;CELL:1234567", [ + { name: "tel", value: "1234567", params: { type: "CELL" } }, + ]); + check("TEL;CELL;PREF:1234567", [ + { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } }, + ]); + check("TEL;HOME:1234567", [ + { name: "tel", value: "1234567", params: { type: "HOME" } }, + ]); + check("TEL;HOME;PREF:1234567", [ + { name: "tel", value: "1234567", params: { type: "HOME", pref: 1 } }, + ]); + check("TEL;VOICE:1234567", [{ name: "tel", value: "1234567" }]); + check("TEL;VOICE;PREF:1234567", [ + { name: "tel", value: "1234567", params: { pref: 1 } }, + ]); + check("TEL;WORK:1234567", [ + { name: "tel", value: "1234567", params: { type: "WORK" } }, + ]); + check("TEL;WORK;PREF:1234567", [ + { name: "tel", value: "1234567", params: { type: "WORK", pref: 1 } }, + ]); + + // Combinations of phone number types. + check("TEL;CELL:1234567\r\nTEL;HOME:9876543", [ + { name: "tel", value: "1234567", params: { type: "CELL" } }, + { name: "tel", value: "9876543", params: { type: "HOME" } }, + ]); + check("TEL;CELL;PREF:1234567\r\nTEL;HOME:9876543", [ + { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } }, + { name: "tel", value: "9876543", params: { type: "HOME" } }, + ]); + + // Phone number preference. + check("TEL;CELL;PREF:1234567\r\nTEL;CELL:9876543", [ + { name: "tel", value: "1234567", params: { type: "CELL", pref: 1 } }, + { name: "tel", value: "9876543", params: { type: "CELL" } }, + ]); + check("TEL;CELL:1234567\r\nTEL;CELL;PREF:9876543", [ + { name: "tel", value: "9876543", params: { type: "CELL", pref: 1 } }, + { name: "tel", value: "1234567", params: { type: "CELL" } }, + ]); + + // Different types of email. + check("EMAIL:pref@invalid", [{ name: "email", value: "pref@invalid" }]); + check("EMAIL;PREF:pref@invalid", [ + { name: "email", value: "pref@invalid", params: { pref: 1 } }, + ]); + check("EMAIL;WORK:work@invalid", [ + { name: "email", value: "work@invalid", params: { type: "WORK" } }, + ]); + check("EMAIL;WORK;PREF:work@invalid", [ + { name: "email", value: "work@invalid", params: { type: "WORK", pref: 1 } }, + ]); + check("EMAIL;HOME:home@invalid", [ + { name: "email", value: "home@invalid", params: { type: "HOME" } }, + ]); + check("EMAIL;HOME;PREF:home@invalid", [ + { name: "email", value: "home@invalid", params: { type: "HOME", pref: 1 } }, + ]); + check("EMAIL;INTERNET:mail@invalid", [ + { name: "email", value: "mail@invalid" }, + ]); + + // Email preference. + check("EMAIL;PREF:pref@invalid\r\nEMAIL:other@invalid", [ + { name: "email", value: "pref@invalid", params: { pref: 1 } }, + { name: "email", value: "other@invalid" }, + ]); + check("EMAIL:other@invalid\r\nEMAIL;PREF:pref@invalid", [ + { name: "email", value: "pref@invalid", params: { pref: 1 } }, + { name: "email", value: "other@invalid" }, + ]); + + // Address types. Multiple types are allowed, some we don't care about. + check("ADR:;;street;town;state", [ + { name: "adr", value: ["", "", "street", "town", "state"] }, + ]); + check("ADR;WORK:;;street;town;state", [ + { + name: "adr", + value: ["", "", "street", "town", "state"], + params: { type: "WORK" }, + }, + ]); + check("ADR;HOME:;;street;town;state", [ + { + name: "adr", + value: ["", "", "street", "town", "state"], + params: { type: "HOME" }, + }, + ]); + check("ADR;DOM:;;street;town;state", [ + { name: "adr", value: ["", "", "street", "town", "state"] }, + ]); + check("ADR;POSTAL;WORK:;;street;town;state", [ + { + name: "adr", + value: ["", "", "street", "town", "state"], + params: { type: "WORK" }, + }, + ]); + check("ADR;PARCEL;HOME:;;street;town;state", [ + { + name: "adr", + value: ["", "", "street", "town", "state"], + params: { type: "HOME" }, + }, + ]); + + // Quoted-printable handling. + check("FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=74=C3=A9=24=74=20=23=31", [ + { name: "fn", value: "té$t #1" }, + ]); + check( + "FN;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=74=65=73=74=20=F0=9F=92=A9", + [{ name: "fn", value: "test 💩" }] + ); + check("ORG;QUOTED-PRINTABLE:=74=65=73=74 #3", [ + { name: "org", value: "test #3" }, + ]); + check("N;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=C5=82ast;=C6=92irst", [ + { name: "n", value: ["łast", "ƒirst"] }, + ]); + check( + "NOTE;QUOTED-PRINTABLE:line 1=0D=0A=\nline 2=0D=0A=\nline 3\r\nNICKNAME:foo=\r\nTITLE:bar=", + [ + { name: "note", value: "line 1\r\nline 2\r\nline 3" }, + { name: "nickname", value: "foo=" }, + { name: "title", value: "bar=" }, + ] + ); + check( + "NOTE;QUOTED-PRINTABLE:line 1=0D=0A=\r\nline 2=0D=0A=\r\nline 3\r\nNICKNAME:foo=\r\nTITLE:bar=", + [ + { name: "note", value: "line 1\r\nline 2\r\nline 3" }, + { name: "nickname", value: "foo=" }, + { name: "title", value: "bar=" }, + ] + ); +}); diff --git a/comm/mailnews/addrbook/test/unit/test_vCardProperties.js b/comm/mailnews/addrbook/test/unit/test_vCardProperties.js new file mode 100644 index 0000000000..cf2c28a634 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/test_vCardProperties.js @@ -0,0 +1,899 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Tests of VCardProperties and VCardPropertyEntry. */ + +var { AddrBookCard } = ChromeUtils.import( + "resource:///modules/AddrBookCard.jsm" +); +var { VCardProperties, VCardPropertyEntry } = ChromeUtils.import( + "resource:///modules/VCardUtils.jsm" +); + +function propertyEqual(actual, expected, message) { + let actualAsObject = { + name: actual.name, + params: actual.params, + type: actual.type, + value: actual.value, + }; + Assert.deepEqual(actualAsObject, expected, message); +} + +function propertyArrayEqual(actual, expected, message) { + Assert.deepEqual( + actual.map(a => { + return { + name: a.name, + params: a.params, + type: a.type, + value: a.value, + }; + }), + expected, + message + ); +} + +/** + * Tests that AddrBookCard supports vCard. + */ +add_task(function testAddrBookCard() { + let card = new AddrBookCard(); + Assert.equal(card.supportsVCard, true, "AddrBookCard supports vCard"); + Assert.ok(card.vCardProperties, "AddrBookCard has vCardProperties"); + Assert.equal(card.vCardProperties.constructor.name, "VCardProperties"); +}); + +/** + * Tests that nsAbCardProperty does not support vCard. + */ +add_task(function testABCardProperty() { + let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + Assert.equal( + card.supportsVCard, + false, + "nsAbCardProperty does not support vCard" + ); + Assert.strictEqual( + card.vCardProperties, + null, + "nsAbCardProperty has no vCardProperties" + ); +}); + +/** + * Tests the `clone` and `equals` functions of VCardPropertyEntry, with a + * simple value type. + */ +add_task(function testPropertyEntrySingleValue() { + let entry = new VCardPropertyEntry("fn", {}, "text", "Juliet"); + let clone = entry.clone(); + + Assert.ok(entry.equals(entry), "original is equal to itself"); + Assert.ok(entry.equals(clone), "original is equal to cloned object"); + Assert.ok(clone.equals(entry), "cloned object is equal to original"); + Assert.ok(clone.equals(clone), "cloned object is equal to itself"); + + Assert.equal(clone.value, entry.value, "values are identical"); + + let other = new VCardPropertyEntry("n", {}, "text", "Romeo"); + Assert.ok(!entry.equals(other), "original is not equal to another object"); + Assert.ok(!other.equals(entry), "another object is not equal to original"); +}); + +/** + * Tests the `clone` and `equals` functions of VCardPropertyEntry, with a + * complex value type. + */ +add_task(function testPropertyEntryMultiValue() { + // A name entry for somebody named "Mr One Two Three Four Senior". + let entry = new VCardPropertyEntry("n", {}, "text", [ + "Four", + "One", + ["Two", "Three"], + "Mr", + "Senior", + ]); + let clone = entry.clone(); + + Assert.ok(entry.equals(entry), "original is equal to itself"); + Assert.ok(entry.equals(clone), "original is equal to cloned object"); + Assert.ok(clone.equals(entry), "cloned object is equal to original"); + Assert.ok(clone.equals(clone), "cloned object is equal to itself"); + + Assert.deepEqual(clone.value, entry.value, "values are identical"); + + Assert.notEqual( + clone.value, + entry.value, + "value arrays are separate objects" + ); + Assert.notEqual( + clone.value[2], + entry.value[2], + "subvalue arrays are separate objects" + ); + + // A name entry for somebody named "Mr One Two Three Four Junior". + let other = new VCardPropertyEntry("n", {}, "text", [ + "Four", + "One", + ["Two", "Three"], + "Mr", + "Junior", + ]); + Assert.ok(!entry.equals(other), "original is not equal to another object"); + Assert.ok(!other.equals(entry), "another object is not equal to original"); +}); + +/** + * Tests creating a VCardProperties from a vCard string, + * then recreating the vCard. + */ +add_task(function testFromToVCard() { + let inVCard = formatVCard` + BEGIN:VCARD + VERSION:3.0 + FN:Mike Test + N:Test;Mike;;; + EMAIL;PREF=1:mike@test.invalid + NICKNAME:Testing Mike + CATEGORIES:testers,quality control,QA + END:VCARD`; + let properties = VCardProperties.fromVCard(inVCard); + + Assert.equal(properties.entries.length, 6, "entry count"); + propertyEqual( + properties.getFirstEntry("version"), + { + name: "version", + params: {}, + type: "text", + value: "3.0", + }, + "version entry" + ); + propertyEqual( + properties.getFirstEntry("fn"), + { + name: "fn", + params: {}, + type: "text", + value: "Mike Test", + }, + "fn entry" + ); + propertyEqual( + properties.getFirstEntry("n"), + { + name: "n", + params: {}, + type: "text", + value: ["Test", "Mike", "", "", ""], + }, + "n entry" + ); + propertyEqual( + properties.getFirstEntry("email"), + { + name: "email", + params: { pref: 1 }, + type: "text", + value: "mike@test.invalid", + }, + "email entry" + ); + propertyEqual( + properties.getFirstEntry("nickname"), + { + name: "nickname", + params: {}, + type: "text", + value: "Testing Mike", + }, + "multivalue entry with one value" + ); + propertyEqual( + properties.getFirstEntry("categories"), + { + name: "categories", + params: {}, + type: "text", + value: ["testers", "quality control", "QA"], + }, + "multivalue entry with multiple values" + ); + + let outVCard = properties.toVCard(); + Assert.equal(outVCard, inVCard, "vCard reproduction"); +}); + +/** + * Tests creating a VCardProperties from a Map of old-style address book + * properties, then recreating the Map. + */ +add_task(function testFromToPropertyMap() { + let inProperties = [ + ["DisplayName", "Mike Test"], + ["LastName", "Test"], + ["FirstName", "Mike"], + ["PrimaryEmail", "mike@test.invalid"], + ["Custom1", "custom one"], + ["Custom2", "custom two"], + ["Custom3", "custom three"], + ["Custom4", "custom four"], + ]; + let properties = VCardProperties.fromPropertyMap( + new Map(inProperties), + "3.0" + ); + + Assert.equal(properties.entries.length, 8, "entry count"); + propertyEqual( + properties.getFirstEntry("version"), + { + name: "version", + params: {}, + type: "text", + value: "3.0", + }, + "version entry" + ); + propertyEqual( + properties.getFirstEntry("fn"), + { + name: "fn", + params: {}, + type: "text", + value: "Mike Test", + }, + "fn entry" + ); + propertyEqual( + properties.getFirstEntry("n"), + { + name: "n", + params: {}, + type: "text", + value: ["Test", "Mike", "", "", ""], + }, + "n entry" + ); + propertyEqual( + properties.getFirstEntry("email"), + { + name: "email", + params: { pref: 1 }, + type: "text", + value: "mike@test.invalid", + }, + "email entry" + ); + propertyEqual( + properties.getFirstEntry("x-custom1"), + { + name: "x-custom1", + params: {}, + type: "text", + value: "custom one", + }, + "custom1 entry" + ); + propertyEqual( + properties.getFirstEntry("x-custom2"), + { + name: "x-custom2", + params: {}, + type: "text", + value: "custom two", + }, + "custom2 entry" + ); + propertyEqual( + properties.getFirstEntry("x-custom3"), + { + name: "x-custom3", + params: {}, + type: "text", + value: "custom three", + }, + "custom3 entry" + ); + propertyEqual( + properties.getFirstEntry("x-custom4"), + { + name: "x-custom4", + params: {}, + type: "text", + value: "custom four", + }, + "custom4 entry" + ); + + let outProperties = properties.toPropertyMap(); + Assert.equal(outProperties.size, 8, "property count"); + for (let [key, value] of inProperties) { + Assert.equal(outProperties.get(key), value, `${key} property`); + } + + // Tests that `toPropertyMap` doesn't break multi-value entries, which could + // happen if `toAbCard` operates on the original entry instead of a clone. + properties = new VCardProperties(); + properties.addEntry( + new VCardPropertyEntry("org", {}, "text", ["one", "two", "three", "four"]) + ); + properties.toPropertyMap(); + Assert.deepEqual(properties.getFirstValue("org"), [ + "one", + "two", + "three", + "four", + ]); +}); + +/** + * Tests adding to and removing from VCardProperties using VCardPropertyEntry. + */ +add_task(function testEntryMethods() { + // Sanity check. + + let props = new VCardProperties(); + Assert.deepEqual(props.entries, [], "props has no entries"); + + // Add property entries. + + // Real VCardPropertyEntry objects. + let charlie = new VCardPropertyEntry( + "email", + { type: "home" }, + "text", + "charlie@invalid" + ); + let delta = new VCardPropertyEntry( + "email", + { type: "work" }, + "text", + "delta@invalid" + ); + + // Ordinary objects for Assert.deepEqual comparison. Use these objects to be + // sure of the values being tested. + let data = { + charlie: { + name: "email", + params: { type: "home" }, + type: "text", + value: "charlie@invalid", + }, + delta: { + name: "email", + params: { type: "work" }, + type: "text", + value: "delta@invalid", + }, + juliet: { + name: "email", + params: { type: "home" }, + type: "text", + value: "juliet@invalid", + }, + }; + + Assert.ok(props.addEntry(charlie)); + propertyArrayEqual( + props.getAllEntries("email"), + [data.charlie], + "props.email has one entry" + ); + Assert.deepEqual( + props.getAllValues("email"), + ["charlie@invalid"], + "props.email has one value" + ); + Assert.equal( + props.getFirstValue("email"), + "charlie@invalid", + "props.email has a first value" + ); + propertyArrayEqual(props.entries, [data.charlie], "props has one entry"); + + Assert.ok(props.addEntry(delta)); + propertyArrayEqual( + props.getAllEntries("email"), + [data.charlie, data.delta], + "props.email has two entries" + ); + Assert.deepEqual( + props.getAllValues("email"), + ["charlie@invalid", "delta@invalid"], + "props.email has two values" + ); + Assert.equal( + props.getFirstValue("email"), + "charlie@invalid", + "props.email has a first value" + ); + propertyArrayEqual( + props.entries, + [data.charlie, data.delta], + "props has two entries" + ); + + Assert.ok(!props.addEntry(charlie)); + propertyArrayEqual( + props.entries, + [data.charlie, data.delta], + "props still has two entries" + ); + + // Update a property entry. + + charlie.value = "juliet@invalid"; + propertyArrayEqual( + props.getAllEntries("email"), + [data.juliet, data.delta], + "props.email has two entries" + ); + Assert.deepEqual( + props.getAllValues("email"), + ["juliet@invalid", "delta@invalid"], + "props.email has two values" + ); + Assert.equal( + props.getFirstValue("email"), + "juliet@invalid", + "props.email has a first value" + ); + propertyArrayEqual( + props.entries, + [data.juliet, data.delta], + "props has two entries" + ); + + // Clone a property entry. + + let juliet = charlie.clone(); + Assert.notEqual( + juliet, + charlie, + "cloned VCardPropertyEntry is not the same object" + ); + propertyEqual( + juliet, + data.juliet, + "cloned VCardPropertyEntry has the same properties" + ); + + // Delete a property entry. + + Assert.ok(props.removeEntry(delta)); + propertyArrayEqual( + props.getAllEntries("email"), + [data.juliet], + "props.email has one entry" + ); + Assert.deepEqual( + props.getAllValues("email"), + ["juliet@invalid"], + "props.email has one value" + ); + Assert.equal( + props.getFirstValue("email"), + "juliet@invalid", + "props.email has a first value" + ); + propertyArrayEqual(props.entries, [data.juliet], "props has one entry"); + + // Delete a property entry using a clone of it. + + Assert.ok(props.removeEntry(juliet)); + propertyArrayEqual(props.entries, [], "all entries removed"); +}); + +/** + * Tests adding to and removing from VCardProperties using names and values. + * Uses the vCard 3 default entry types. + */ +add_task(function testValueMethods3() { + let props = new VCardProperties(); + + // Add a value. + + let first = props.addValue("tel", "1234567"); + propertyEqual(first, { + name: "tel", + params: {}, + type: "phone-number", + value: "1234567", + }); + propertyArrayEqual(props.entries, [ + { name: "tel", params: {}, type: "phone-number", value: "1234567" }, + ]); + + // Add a second value. + + let second = props.addValue("tel", "2345678"); + propertyEqual(second, { + name: "tel", + params: {}, + type: "phone-number", + value: "2345678", + }); + propertyArrayEqual(props.entries, [ + { name: "tel", params: {}, type: "phone-number", value: "1234567" }, + { name: "tel", params: {}, type: "phone-number", value: "2345678" }, + ]); + + // Add a value that already exists. The existing property should be returned. + + let secondCopy = props.addValue("tel", "2345678"); + Assert.equal(secondCopy, second); + propertyArrayEqual(props.entries, [ + { name: "tel", params: {}, type: "phone-number", value: "1234567" }, + { name: "tel", params: {}, type: "phone-number", value: "2345678" }, + ]); + + // Add a third value. + + let third = props.addValue("tel", "3456789"); + propertyEqual(third, { + name: "tel", + params: {}, + type: "phone-number", + value: "3456789", + }); + propertyArrayEqual(props.entries, [ + { name: "tel", params: {}, type: "phone-number", value: "1234567" }, + { name: "tel", params: {}, type: "phone-number", value: "2345678" }, + { name: "tel", params: {}, type: "phone-number", value: "3456789" }, + ]); + + // Remove the second value. + + props.removeValue("tel", "2345678"); + propertyArrayEqual(props.entries, [ + { name: "tel", params: {}, type: "phone-number", value: "1234567" }, + { name: "tel", params: {}, type: "phone-number", value: "3456789" }, + ]); + + // Remove a value that's already been removed. + + props.removeValue("tel", "2345678"); + propertyArrayEqual(props.entries, [ + { name: "tel", params: {}, type: "phone-number", value: "1234567" }, + { name: "tel", params: {}, type: "phone-number", value: "3456789" }, + ]); + + // Remove a value that never existed. + + props.removeValue("tel", "4567890"); + propertyArrayEqual(props.entries, [ + { name: "tel", params: {}, type: "phone-number", value: "1234567" }, + { name: "tel", params: {}, type: "phone-number", value: "3456789" }, + ]); + + // Remove the first value. + + props.removeValue("tel", "1234567"); + propertyArrayEqual(props.entries, [ + { name: "tel", params: {}, type: "phone-number", value: "3456789" }, + ]); + + // Remove the last value. + + props.removeValue("tel", "3456789"); + propertyArrayEqual(props.entries, []); +}); + +/** + * Tests adding to and removing from VCardProperties using names and values. + * Uses the vCard 4 default entry types. + */ +add_task(function testValueMethods4() { + let props = new VCardProperties("4.0"); + + // Add a value. + + let first = props.addValue("tel", "tel:1234567"); + propertyEqual(first, { + name: "tel", + params: {}, + type: "uri", + value: "tel:1234567", + }); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + { name: "tel", params: {}, type: "uri", value: "tel:1234567" }, + ]); + + // Add a second value. + + let second = props.addValue("tel", "tel:2345678"); + propertyEqual(second, { + name: "tel", + params: {}, + type: "uri", + value: "tel:2345678", + }); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + { name: "tel", params: {}, type: "uri", value: "tel:1234567" }, + { name: "tel", params: {}, type: "uri", value: "tel:2345678" }, + ]); + + // Add a value that already exists. The existing property should be returned. + + let secondCopy = props.addValue("tel", "tel:2345678"); + Assert.equal(secondCopy, second); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + { name: "tel", params: {}, type: "uri", value: "tel:1234567" }, + { name: "tel", params: {}, type: "uri", value: "tel:2345678" }, + ]); + + // Add a third value. + + let third = props.addValue("tel", "tel:3456789"); + propertyEqual(third, { + name: "tel", + params: {}, + type: "uri", + value: "tel:3456789", + }); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + { name: "tel", params: {}, type: "uri", value: "tel:1234567" }, + { name: "tel", params: {}, type: "uri", value: "tel:2345678" }, + { name: "tel", params: {}, type: "uri", value: "tel:3456789" }, + ]); + + // Remove the second value. + + props.removeValue("tel", "tel:2345678"); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + { name: "tel", params: {}, type: "uri", value: "tel:1234567" }, + { name: "tel", params: {}, type: "uri", value: "tel:3456789" }, + ]); + + // Remove a value that's already been removed. + + props.removeValue("tel", "tel:2345678"); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + { name: "tel", params: {}, type: "uri", value: "tel:1234567" }, + { name: "tel", params: {}, type: "uri", value: "tel:3456789" }, + ]); + + // Remove a value that never existed. + + props.removeValue("tel", "tel:4567890"); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + { name: "tel", params: {}, type: "uri", value: "tel:1234567" }, + { name: "tel", params: {}, type: "uri", value: "tel:3456789" }, + ]); + + // Remove the first value. + + props.removeValue("tel", "tel:1234567"); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + { name: "tel", params: {}, type: "uri", value: "tel:3456789" }, + ]); + + // Remove the last value. + + props.removeValue("tel", "tel:3456789"); + propertyArrayEqual(props.entries, [ + { name: "version", params: {}, type: "text", value: "4.0" }, + ]); +}); + +/** + * Tests retrieving entries and values in preference order. + */ +add_task(function testSortMethods() { + let props = new VCardProperties(); + props.addEntry(new VCardPropertyEntry("email", {}, "text", "third@invalid")); + props.addEntry( + new VCardPropertyEntry("email", { pref: 2 }, "text", "second@invalid") + ); + props.addEntry(new VCardPropertyEntry("email", {}, "text", "fourth@invalid")); + props.addEntry( + new VCardPropertyEntry("email", { pref: 1 }, "text", "first@invalid") + ); + + propertyArrayEqual(props.getAllEntriesSorted("email"), [ + { + name: "email", + params: { pref: 1 }, + type: "text", + value: "first@invalid", + }, + { + name: "email", + params: { pref: 2 }, + type: "text", + value: "second@invalid", + }, + { name: "email", params: {}, type: "text", value: "third@invalid" }, + { name: "email", params: {}, type: "text", value: "fourth@invalid" }, + ]); + + Assert.deepEqual(props.getAllValuesSorted("email"), [ + "first@invalid", + "second@invalid", + "third@invalid", + "fourth@invalid", + ]); +}); + +/** + * Tests the `clone` method of VCardProperties. + */ +add_task(function testClone() { + let properties = VCardProperties.fromVCard( + formatVCard` + BEGIN:VCARD + FN:this is a test + N:test;this;is,a;; + EMAIL;PREF=1;TYPE=WORK:test@invalid + EMAIL:test@test.invalid + END:VCARD` + ); + let clone = properties.clone(); + + Assert.deepEqual(clone.entries, properties.entries); + Assert.notEqual(clone.entries, properties.entries); + + for (let i = 0; i < 4; i++) { + Assert.deepEqual(clone.entries[i].value, properties.entries[i].value); + Assert.notEqual(clone.entries[i], properties.entries[i]); + Assert.ok(clone.entries[i].equals(properties.entries[i])); + } + + Assert.equal(clone.toVCard(), properties.toVCard()); +}); + +/** + * Tests that entries with a group prefix are correctly handled, and the + * `getGroupedEntries` method of VCardProperties. + */ +add_task(function testGroupEntries() { + let vCard = formatVCard` + BEGIN:VCARD + GROUP1.FN:test + GROUP1.X-FOO:bar + NOTE:this doesn't have a group + END:VCARD`; + + let properties = VCardProperties.fromVCard(vCard); + + let data = [ + { + name: "fn", + params: { + group: "group1", + }, + type: "text", + value: "test", + }, + { + name: "x-foo", + params: { + group: "group1", + }, + type: "unknown", + value: "bar", + }, + { + name: "note", + params: {}, + type: "text", + value: "this doesn't have a group", + }, + ]; + + propertyArrayEqual(properties.entries, data); + Assert.equal(properties.toVCard(), vCard); + propertyArrayEqual(properties.getGroupedEntries("group1"), data.slice(0, 2)); + + let clone = properties.clone(); + propertyArrayEqual(clone.entries, data); + Assert.equal(clone.toVCard(), vCard); + propertyArrayEqual(clone.getGroupedEntries("group1"), data.slice(0, 2)); +}); + +/** + * Tests that we correctly fix Google's bad escaping of colons in values, and + * other characters in URI values. + */ +add_task(function testGoogleEscaping() { + let vCard = formatVCard` + BEGIN:VCARD + VERSION:3.0 + N:test;en\\\\c\\:oding;;; + FN:en\\\\c\\:oding test + TITLE:title\\:title\\;title\\,title\\\\title\\\\\\:title\\\\\\;title\\\\\\,title\\\\\\\\ + TEL:tel\\:0123\\\\4567 + EMAIL:test\\\\test@invalid + NOTE:notes\\:\\nnotes\\;\\nnotes\\,\\nnotes\\\\ + URL:http\\://host/url\\:url\\;url\\,url\\\\url + END:VCARD`; + + let goodVCard = formatVCard` + BEGIN:VCARD + VERSION:3.0 + N:test;en\\\\c:oding;;; + FN:en\\\\c:oding test + TITLE:title:title\\;title\\,title\\\\title\\\\:title\\\\\\;title\\\\\\,title\\\\\\\\ + TEL:tel:01234567 + EMAIL:test\\\\test@invalid + NOTE:notes:\\nnotes\\;\\nnotes\\,\\nnotes\\\\ + URL:http://host/url:url;url,url\\url + END:VCARD`; + + let data = [ + { + name: "version", + params: {}, + type: "text", + value: "3.0", + }, + { + name: "n", + params: {}, + type: "text", + value: ["test", "en\\c:oding", "", "", ""], + }, + { + name: "fn", + params: {}, + type: "text", + value: "en\\c:oding test", + }, + { + name: "title", + params: {}, + type: "text", + value: "title:title;title,title\\title\\:title\\;title\\,title\\\\", + }, + { + name: "tel", + params: {}, + type: "phone-number", + value: "tel:01234567", + }, + { + name: "email", + params: {}, + type: "text", + value: "test\\test@invalid", + }, + { + name: "note", + params: {}, + type: "text", + value: "notes:\nnotes;\nnotes,\nnotes\\", + }, + { + name: "url", + params: {}, + type: "uri", + value: "http://host/url:url;url,url\\url", + }, + ]; + + let properties = VCardProperties.fromVCard(vCard, { isGoogleCardDAV: true }); + propertyArrayEqual(properties.entries, data); + Assert.equal(properties.toVCard(), goodVCard); + + let goodProperties = VCardProperties.fromVCard(goodVCard); + propertyArrayEqual(goodProperties.entries, data); + Assert.equal(goodProperties.toVCard(), goodVCard); +}); diff --git a/comm/mailnews/addrbook/test/unit/xpcshell.ini b/comm/mailnews/addrbook/test/unit/xpcshell.ini new file mode 100644 index 0000000000..2701be7a7a --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/xpcshell.ini @@ -0,0 +1,60 @@ +[DEFAULT] +head = head.js +support-files = data/* +tags = addrbook + +[test_abCardProperty.js] +[test_addrBookCard.js] +[test_basic_nsIAbDirectory.js] +[test_bug387403.js] +tags = addrbook vcard +[test_bug448165.js] +[test_bug534822.js] +[test_bug1522453.js] +[test_bug1769889.js] +tags = addrbook vcard +[test_cardForEmail.js] +[test_collection.js] +[test_collection_2.js] +[test_convertOnSave.js] +tags = addrbook vcard +[test_db_enumerator.js] +[test_delete_book.js] +[test_export.js] +tags = addrbook vcard +[test_jsaddrbook.js] +[test_LDAPMessage.js] +[test_LDAPSyncQuery.js] +[test_ldap1.js] +[test_ldap2.js] +[test_ldapOffline.js] +[test_ldapquery.js] +[test_ldapReplication.js] +skip-if = debug # Fails for unknown reasons. +[test_mailList1.js] +[test_nsAbAutoCompleteMyDomain.js] +[test_nsAbAutoCompleteSearch1.js] +[test_nsAbAutoCompleteSearch2.js] +[test_nsAbAutoCompleteSearch3.js] +[test_nsAbAutoCompleteSearch4.js] +[test_nsAbAutoCompleteSearch5.js] +[test_nsAbAutoCompleteSearch6.js] +[test_nsAbAutoCompleteSearch7.js] +[test_nsAbManager2.js] +[test_nsAbManager3.js] +[test_nsAbManager4.js] +[test_nsAbManager5.js] +[test_nsAbManager6.js] +[test_nsIAbCard.js] +tags = addrbook vcard +[test_nsIAbDirectory_getMailListFromName.js] +[test_nsLDAPURL.js] +[test_photoURL.js] +[test_preferDisplayName.js] +[test_search.js] +[test_vCard.js] +tags = addrbook vcard +[test_vCard21.js] +tags = addrbook vcard +[test_vCardProperties.js] +tags = addrbook vcard diff --git a/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini b/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini new file mode 100644 index 0000000000..720607fbe0 --- /dev/null +++ b/comm/mailnews/addrbook/test/unit/xpcshell_cardDAV.ini @@ -0,0 +1,12 @@ +[DEFAULT] +head = head_cardDAV.js +tags = addrbook carddav vcard +prefs = + carddav.setup.loglevel=Debug + carddav.sync.loglevel=Debug + +[test_cardDAV_copyCard.js] +[test_cardDAV_offline.js] +[test_cardDAV_serverModified.js] +[test_cardDAV_syncV1.js] +[test_cardDAV_syncV2.js] |