summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/import/modules/AddrBookFileImporter.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/import/modules/AddrBookFileImporter.jsm')
-rw-r--r--comm/mailnews/import/modules/AddrBookFileImporter.jsm356
1 files changed, 356 insertions, 0 deletions
diff --git a/comm/mailnews/import/modules/AddrBookFileImporter.jsm b/comm/mailnews/import/modules/AddrBookFileImporter.jsm
new file mode 100644
index 0000000000..c9cea94893
--- /dev/null
+++ b/comm/mailnews/import/modules/AddrBookFileImporter.jsm
@@ -0,0 +1,356 @@
+/* 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 = ["AddrBookFileImporter"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailStringUtils: "resource:///modules/MailStringUtils.jsm",
+ exportAttributes: "resource:///modules/AddrBookUtils.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "d3", () => {
+ let d3Scope = Cu.Sandbox(null);
+ Services.scriptloader.loadSubScript(
+ "chrome://global/content/third_party/d3/d3.js",
+ d3Scope
+ );
+ return Cu.waiveXrays(d3Scope.d3);
+});
+
+/**
+ * A module to import address book files.
+ */
+class AddrBookFileImporter {
+ /**
+ * @param {string} type - Source file type, currently supporting "csv",
+ * "ldif", "vcard" and "mab".
+ */
+ constructor(type) {
+ this._type = type;
+ }
+
+ /**
+ * Callback for progress updates.
+ *
+ * @param {number} current - Current imported items count.
+ * @param {number} total - Total items count.
+ */
+ onProgress = () => {};
+
+ _logger = console.createInstance({
+ prefix: "mail.import",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.import.loglevel",
+ });
+
+ /**
+ * Actually start importing records into a directory.
+ *
+ * @param {nsIFile} sourceFile - The source file to import from.
+ * @param {nsIAbDirectory} targetDirectory - The directory to import into.
+ */
+ async startImport(sourceFile, targetDirectory) {
+ this._logger.debug(
+ `Importing ${this._type} file from ${sourceFile.path} into ${targetDirectory.dirName}`
+ );
+ this._sourceFile = sourceFile;
+ this._targetDirectory = targetDirectory;
+
+ switch (this._type) {
+ case "csv":
+ await this._importCsvFile();
+ break;
+ case "ldif":
+ await this._importLdifFile();
+ break;
+ case "vcard":
+ await this._importVCardFile();
+ break;
+ case "sqlite":
+ await this._importSqliteFile();
+ break;
+ case "mab":
+ await this._importMabFile();
+ break;
+ default:
+ throw Components.Exception(
+ `Importing ${this._type} file is not supported`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+ }
+
+ /**
+ * Parse a CSV/TSV file to an array of rows, each row is an array of columns.
+ * The first row is expected to contain field names. If we recognize all the
+ * field names, return an empty array, which means everything is parsed fine.
+ * Otherwise, return all the rows.
+ *
+ * @param {nsIFile} sourceFile - The source file to import from.
+ * @returns {string[][]}
+ */
+ async parseCsvFile(sourceFile) {
+ let content = await lazy.MailStringUtils.readEncoded(sourceFile.path);
+
+ let csvRows = lazy.d3.csv.parseRows(content);
+ let tsvRows = lazy.d3.tsv.parseRows(content);
+ let dsvRows = lazy.d3.dsv(";").parseRows(content);
+ if (!csvRows.length && !tsvRows.length && !dsvRows.length) {
+ this._csvRows = [];
+ return [];
+ }
+ // If we have more CSV columns, then it's a CSV file, otherwise a TSV file.
+ this._csvRows = csvRows[0]?.length > tsvRows[0]?.length ? csvRows : tsvRows;
+ // See if it's semicolon separated.
+ if (this._csvRows[0]?.length < dsvRows[0]?.length) {
+ this._csvRows = dsvRows;
+ }
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/importMsgs.properties"
+ );
+ let supportedFieldNames = [];
+ this._supportedCsvProperties = [];
+ // Collect field names in an exported CSV file, and their corresponding
+ // nsIAbCard property names.
+ for (let [property, stringId] of lazy.exportAttributes) {
+ if (stringId) {
+ this._supportedCsvProperties.push(property);
+ supportedFieldNames.push(
+ bundle.GetStringFromID(stringId).toLowerCase()
+ );
+ }
+ }
+ this._csvSkipFirstRow = true;
+ this._csvProperties = [];
+ // Get the nsIAbCard properties corresponding to the user supplied file.
+ for (let field of this._csvRows[0]) {
+ if (
+ !field &&
+ this._csvRows[0].length > 1 &&
+ field == this._csvRows[0].at(-1)
+ ) {
+ // This is the last field and empty, caused by a trailing comma, which
+ // is OK.
+ return [];
+ }
+ let index = supportedFieldNames.indexOf(field.toLowerCase());
+ if (index == -1) {
+ return this._csvRows;
+ }
+ this._csvProperties.push(this._supportedCsvProperties[index]);
+ }
+ return [];
+ }
+
+ /**
+ * Set the address book properties to use when importing.
+ *
+ * @param {number[]} fieldIndexes - An array of indexes representing the
+ * mapping between the source fields and nsIAbCard fields. For example, [2,
+ * 4] means the first field maps to the 2nd property, the second field maps
+ * to the 4th property.
+ */
+ setCsvFields(fieldIndexes) {
+ Services.prefs.setCharPref(
+ "mail.import.csv.fields",
+ fieldIndexes.join(",")
+ );
+ this._csvProperties = fieldIndexes.map(
+ i => this._supportedCsvProperties[i]
+ );
+ this._csvSkipFirstRow = Services.prefs.getBoolPref(
+ "mail.import.csv.skipfirstrow",
+ true
+ );
+ }
+
+ /**
+ * Import the .csv/.tsv source file into the target directory.
+ */
+ async _importCsvFile() {
+ let totalLines = this._csvRows.length - 1;
+ let currentLine = 0;
+
+ let startRow = this._csvSkipFirstRow ? 1 : 0;
+ for (let row of this._csvRows.slice(startRow)) {
+ currentLine++;
+ let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+ for (let i = 0; i < row.length; i++) {
+ let property = this._csvProperties[i];
+ if (!property) {
+ continue;
+ }
+ // Set the field value to the property.
+ card.setProperty(property, row[i]);
+ }
+ this._targetDirectory.addCard(card);
+ if (currentLine % 10 == 0) {
+ this.onProgress(currentLine, totalLines);
+ // Give UI a chance to update the progress bar.
+ await new Promise(resolve => lazy.setTimeout(resolve));
+ }
+ }
+ this.onProgress(totalLines, totalLines);
+ }
+
+ /**
+ * Import the .ldif source file into the target directory.
+ */
+ async _importLdifFile() {
+ this.onProgress(2, 10);
+ let ldifService = Cc["@mozilla.org/addressbook/abldifservice;1"].getService(
+ Ci.nsIAbLDIFService
+ );
+ let progress = {};
+ ldifService.importLDIFFile(
+ this._targetDirectory,
+ this._sourceFile,
+ false,
+ progress
+ );
+ this.onProgress(10, 10);
+ }
+
+ /**
+ * Import the .vcf source file into the target directory.
+ */
+ async _importVCardFile() {
+ let vcardService = Cc[
+ "@mozilla.org/addressbook/msgvcardservice;1"
+ ].getService(Ci.nsIMsgVCardService);
+
+ let content = await IOUtils.readUTF8(this._sourceFile.path);
+ // According to rfc6350, \r\n should be used as line break.
+ let sep = content.includes("\r\n") ? "\r\n" : "\n";
+ let lines = content.trim().split(sep);
+
+ let totalLines = lines.length;
+ let currentLine = 0;
+ let record = [];
+
+ for (let line of lines) {
+ currentLine++;
+ if (!line) {
+ continue;
+ }
+
+ if (line.toLowerCase().trimEnd() == "begin:vcard") {
+ if (record.length) {
+ throw Components.Exception(
+ "Expecting END:VCARD but got BEGIN:VCARD",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ record.push(line);
+ continue;
+ } else if (!record.length) {
+ throw Components.Exception(
+ `Expecting BEGIN:VCARD but got ${line}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+
+ record.push(line);
+
+ if (line.toLowerCase().trimEnd() == "end:vcard") {
+ this._targetDirectory.addCard(
+ vcardService.vCardToAbCard(record.join("\n") + "\n")
+ );
+ record = [];
+ this.onProgress(currentLine, totalLines);
+ // Give UI a chance to update the progress bar.
+ await new Promise(resolve => lazy.setTimeout(resolve));
+ }
+ }
+ this.onProgress(totalLines, totalLines);
+ }
+
+ /**
+ * Import the .sqlite source file into the target directory.
+ */
+ async _importSqliteFile() {
+ this.onProgress(2, 10);
+ // Create a temporary address book.
+ let dirId = MailServices.ab.newAddressBook(
+ "tmp",
+ "",
+ Ci.nsIAbManager.JS_DIRECTORY_TYPE
+ );
+ let tmpDirectory = MailServices.ab.getDirectoryFromId(dirId);
+
+ try {
+ // Close the connection to release the file handler.
+ await tmpDirectory.cleanUp();
+ // Overwrite the sqlite database file.
+ this._sourceFile.copyTo(
+ Services.dirsvc.get("ProfD", Ci.nsIFile),
+ tmpDirectory.fileName
+ );
+ // Write-Ahead Logging file contains changes not written to .sqlite file
+ // yet.
+ let sourceWalFile = this._sourceFile.parent.clone();
+ sourceWalFile.append(this._sourceFile.leafName + "-wal");
+ if (sourceWalFile.exists()) {
+ sourceWalFile.copyTo(
+ Services.dirsvc.get("ProfD", Ci.nsIFile),
+ tmpDirectory.fileName + "-wal"
+ );
+ }
+ // Open a new connection to use the new database file.
+ let uri = tmpDirectory.URI;
+ tmpDirectory = Cc[
+ "@mozilla.org/addressbook/directory;1?type=jsaddrbook"
+ ].createInstance(Ci.nsIAbDirectory);
+ tmpDirectory.init(uri);
+
+ for (let card of tmpDirectory.childCards) {
+ this._targetDirectory.addCard(card);
+ }
+ this.onProgress(8, 10);
+
+ for (let sourceList of tmpDirectory.childNodes) {
+ let targetList = this._targetDirectory.getMailListFromName(
+ sourceList.dirName
+ );
+ if (!targetList) {
+ targetList = this._targetDirectory.addMailList(sourceList);
+ }
+ for (let card of sourceList.childCards) {
+ targetList.addCard(card);
+ }
+ }
+ this.onProgress(10, 10);
+ } finally {
+ MailServices.ab.deleteAddressBook(tmpDirectory.URI);
+ }
+ }
+
+ /**
+ * Import the .mab source file into the target directory.
+ */
+ async _importMabFile() {
+ this.onProgress(2, 10);
+ let importMab = Cc[
+ "@mozilla.org/import/import-ab-file;1?type=mab"
+ ].createInstance(Ci.nsIImportABFile);
+ importMab.readFileToDirectory(this._sourceFile, this._targetDirectory);
+ this.onProgress(10, 10);
+ }
+}