summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/addrbook/modules/AddrBookUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/addrbook/modules/AddrBookUtils.jsm')
-rw-r--r--comm/mailnews/addrbook/modules/AddrBookUtils.jsm522
1 files changed, 522 insertions, 0 deletions
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,
+};