diff options
Diffstat (limited to 'comm/mailnews/addrbook/modules/AddrBookUtils.jsm')
-rw-r--r-- | comm/mailnews/addrbook/modules/AddrBookUtils.jsm | 522 |
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, +}; |