summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/import/modules
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/import/modules')
-rw-r--r--comm/mailnews/import/modules/AddrBookFileImporter.jsm356
-rw-r--r--comm/mailnews/import/modules/AppleMailProfileImporter.jsm92
-rw-r--r--comm/mailnews/import/modules/BaseProfileImporter.jsm100
-rw-r--r--comm/mailnews/import/modules/BeckyProfileImporter.jsm125
-rw-r--r--comm/mailnews/import/modules/CalendarFileImporter.jsm127
-rw-r--r--comm/mailnews/import/modules/OutlookProfileImporter.jsm143
-rw-r--r--comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm75
-rw-r--r--comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm1024
-rw-r--r--comm/mailnews/import/modules/moz.build22
9 files changed, 2064 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);
+ }
+}
diff --git a/comm/mailnews/import/modules/AppleMailProfileImporter.jsm b/comm/mailnews/import/modules/AppleMailProfileImporter.jsm
new file mode 100644
index 0000000000..c6c6e5b4ba
--- /dev/null
+++ b/comm/mailnews/import/modules/AppleMailProfileImporter.jsm
@@ -0,0 +1,92 @@
+/* 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 = ["AppleMailProfileImporter"];
+
+var { BaseProfileImporter } = ChromeUtils.import(
+ "resource:///modules/BaseProfileImporter.jsm"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/**
+ * A module to import things from an apple mail profile dir into the current
+ * profile.
+ */
+class AppleMailProfileImporter extends BaseProfileImporter {
+ USE_FILE_PICKER = true;
+
+ SUPPORTED_ITEMS = {
+ accounts: false,
+ addressBooks: false,
+ calendars: false,
+ mailMessages: true,
+ };
+
+ async getSourceProfiles() {
+ this._importModule = Cc[
+ "@mozilla.org/import/import-applemail;1"
+ ].createInstance(Ci.nsIImportModule);
+ this._importMailGeneric = this._importModule
+ .GetImportInterface("mail")
+ .QueryInterface(Ci.nsIImportGeneric);
+ let importMail = this._importMailGeneric
+ .GetData("mailInterface")
+ .QueryInterface(Ci.nsIImportMail);
+ let outLocation = {};
+ let outFound = {};
+ let outUserVerify = {};
+ importMail.GetDefaultLocation(outLocation, outFound, outUserVerify);
+ if (outLocation.value) {
+ return [{ dir: outLocation.value }];
+ }
+ return [];
+ }
+
+ async startImport(sourceProfileDir, items) {
+ this._logger.debug(
+ `Start importing from ${sourceProfileDir.path}, items=${JSON.stringify(
+ items
+ )}`
+ );
+ this._itemsTotalCount = Object.values(items).filter(Boolean).length;
+ this._itemsImportedCount = 0;
+
+ let successStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ let errorStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+
+ if (items.mailMessages) {
+ // @see nsIImportGeneric.
+ this._importMailGeneric.SetData("mailLocation", sourceProfileDir);
+ let wantsProgress = this._importMailGeneric.WantsProgress();
+ this._importMailGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (this._importMailGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import mail messages progress:",
+ this._importMailGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ this._importMailGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing mail messages:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import mail messages:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+ }
+}
diff --git a/comm/mailnews/import/modules/BaseProfileImporter.jsm b/comm/mailnews/import/modules/BaseProfileImporter.jsm
new file mode 100644
index 0000000000..18c5dce16b
--- /dev/null
+++ b/comm/mailnews/import/modules/BaseProfileImporter.jsm
@@ -0,0 +1,100 @@
+/* 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 = ["BaseProfileImporter"];
+
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/**
+ * An object to represent a source profile to import from.
+ *
+ * @typedef {object} SourceProfile
+ * @property {string} name - The profile name.
+ * @property {nsIFile} dir - The profile location.
+ *
+ * An object to represent items to import.
+ * @typedef {object} ImportItems
+ * @property {boolean} accounts - Whether to import accounts and settings.
+ * @property {boolean} addressBooks - Whether to import address books.
+ * @property {boolean} calendars - Whether to import calendars.
+ * @property {boolean} mailMessages - Whether to import mail messages.
+ */
+
+/**
+ * Common interfaces shared by profile importers.
+ *
+ * @abstract
+ */
+class BaseProfileImporter {
+ /** @type boolean - Whether to allow importing from a user picked dir. */
+ USE_FILE_PICKER = true;
+
+ /** @type ImportItems */
+ SUPPORTED_ITEMS = {
+ accounts: true,
+ addressBooks: true,
+ calendars: true,
+ mailMessages: true,
+ };
+
+ /** When importing from a zip file, ignoring these folders. */
+ IGNORE_DIRS = [];
+
+ /**
+ * Callback for progress updates.
+ *
+ * @param {number} current - Current imported items count.
+ * @param {number} total - Total items count.
+ */
+ onProgress = () => {};
+
+ /**
+ * @returns {SourceProfile[]} Profiles found on this machine.
+ */
+ async getSourceProfiles() {
+ throw Components.Exception(
+ `getSourceProfiles not implemented in ${this.constructor.name}`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Actually start importing things to the current profile.
+ *
+ * @param {nsIFile} sourceProfileDir - The source location to import from.
+ * @param {ImportItems} items - The items to import.
+ * @returns {boolean} Returns true when accounts have been imported, which
+ * means a restart is needed. Otherwise, no restart is needed.
+ */
+ async startImport(sourceProfileDir, items) {
+ throw Components.Exception(
+ `startImport not implemented in ${this.constructor.name}`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Reset use_without_mail_account, so that imported accounts are correctly
+ * rendered in the folderPane.
+ */
+ _onImportAccounts() {
+ Services.prefs.setBoolPref("app.use_without_mail_account", false);
+ }
+
+ /**
+ * Increase _itemsImportedCount by one, and call onProgress.
+ */
+ async _updateProgress() {
+ this.onProgress(++this._itemsImportedCount, this._itemsTotalCount);
+ return new Promise(resolve => setTimeout(resolve));
+ }
+
+ _logger = console.createInstance({
+ prefix: "mail.import",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.import.loglevel",
+ });
+}
diff --git a/comm/mailnews/import/modules/BeckyProfileImporter.jsm b/comm/mailnews/import/modules/BeckyProfileImporter.jsm
new file mode 100644
index 0000000000..7c81ca7b66
--- /dev/null
+++ b/comm/mailnews/import/modules/BeckyProfileImporter.jsm
@@ -0,0 +1,125 @@
+/* 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 = ["BeckyProfileImporter"];
+
+var { BaseProfileImporter } = ChromeUtils.import(
+ "resource:///modules/BaseProfileImporter.jsm"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/**
+ * A module to import things from an becky profile dir into the current
+ * profile.
+ */
+class BeckyProfileImporter extends BaseProfileImporter {
+ SUPPORTED_ITEMS = {
+ accounts: false,
+ addressBooks: true,
+ calendars: false,
+ mailMessages: true,
+ };
+
+ async getSourceProfiles() {
+ this._importModule = Cc[
+ "@mozilla.org/import/import-becky;1"
+ ].createInstance(Ci.nsIImportModule);
+ this._importMailGeneric = this._importModule
+ .GetImportInterface("mail")
+ .QueryInterface(Ci.nsIImportGeneric);
+ let importMail = this._importMailGeneric
+ .GetData("mailInterface")
+ .QueryInterface(Ci.nsIImportMail);
+ let outLocation = {};
+ let outFound = {};
+ let outUserVerify = {};
+ importMail.GetDefaultLocation(outLocation, outFound, outUserVerify);
+ if (outLocation.value) {
+ return [{ dir: outLocation.value }];
+ }
+ return [];
+ }
+
+ async startImport(sourceProfileDir, items) {
+ this._logger.debug(
+ `Start importing from ${sourceProfileDir.path}, items=${JSON.stringify(
+ items
+ )}`
+ );
+ this._itemsTotalCount = Object.values(items).filter(Boolean).length;
+ this._itemsImportedCount = 0;
+
+ let successStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ let errorStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+
+ if (items.mailMessages) {
+ // @see nsIImportGeneric.
+ this._importMailGeneric.SetData("mailLocation", sourceProfileDir);
+ let wantsProgress = this._importMailGeneric.WantsProgress();
+ this._importMailGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (this._importMailGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import mail messages progress:",
+ this._importMailGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ this._importMailGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing mail messages:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import mail messages:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+
+ if (items.addressBooks) {
+ successStr.data = "";
+ errorStr.data = "";
+
+ let importABGeneric = this._importModule
+ .GetImportInterface("addressbook")
+ .QueryInterface(Ci.nsIImportGeneric);
+ importABGeneric.SetData("addressLocation", sourceProfileDir);
+
+ // @see nsIImportGeneric.
+ let wantsProgress = importABGeneric.WantsProgress();
+ importABGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (importABGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import address books progress:",
+ importABGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ importABGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing address books:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import address books:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+ }
+}
diff --git a/comm/mailnews/import/modules/CalendarFileImporter.jsm b/comm/mailnews/import/modules/CalendarFileImporter.jsm
new file mode 100644
index 0000000000..ffa69feafb
--- /dev/null
+++ b/comm/mailnews/import/modules/CalendarFileImporter.jsm
@@ -0,0 +1,127 @@
+/* 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 = ["CalendarFileImporter"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ cal: "resource:///modules/calendar/calUtils.jsm",
+});
+
+/**
+ * A module to import iCalendar (.ics) file.
+ */
+class CalendarFileImporter {
+ /**
+ * 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",
+ });
+
+ /**
+ * Parse an ics file to an array of items.
+ *
+ * @param {string} file - The file path of an ics file.
+ * @returns {calIItemBase[]}
+ */
+ async parseIcsFile(file) {
+ this._logger.debug(`Getting items from ${file.path}`);
+ let importer = Cc["@mozilla.org/calendar/import;1?type=ics"].getService(
+ Ci.calIImporter
+ );
+
+ let inputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ let items = [];
+
+ try {
+ // 0x01 means MODE_RDONLY.
+ inputStream.init(file, 0x01, 0o444, {});
+ items = importer.importFromStream(inputStream);
+ if (!items.length) {
+ throw new Error("noItemsFound");
+ }
+ } catch (e) {
+ this._logger.error(e);
+ throw e;
+ } finally {
+ inputStream.close();
+ }
+
+ return items;
+ }
+
+ /**
+ * Get all calendars that the current user can import items to.
+ *
+ * @returns {calICalendar[]}
+ */
+ getTargetCalendars() {
+ let calendars = lazy.cal.manager
+ .getCalendars()
+ .filter(
+ calendar =>
+ !calendar.getProperty("disabled") &&
+ !calendar.readOnly &&
+ lazy.cal.acl.userCanAddItemsToCalendar(calendar)
+ );
+ let sortOrderPref = Services.prefs.getCharPref(
+ "calendar.list.sortOrder",
+ ""
+ );
+ let sortOrder = sortOrderPref ? sortOrderPref.split(" ") : [];
+ return calendars.sort(
+ (x, y) => sortOrder.indexOf(x.id) - sortOrder.indexOf(y.id)
+ );
+ }
+
+ /**
+ * Actually start importing items into a calendar.
+ *
+ * @param {nsIFile} sourceFile - The source file to import from.
+ * @param {calICalendar} targetCalendar - The calendar to import into.
+ */
+ async startImport(items, targetCalendar) {
+ let count = 0;
+ let total = items.length;
+
+ this._logger.debug(`Importing ${total} items into ${targetCalendar.name}`);
+
+ for (let item of items) {
+ try {
+ await targetCalendar.addItem(item);
+ } catch (e) {
+ this._logger.error(e);
+ throw e;
+ }
+
+ count++;
+
+ if (count % 10 == 0) {
+ this.onProgress(count, total);
+ // Give the UI a chance to update the progress bar.
+ await new Promise(resolve => lazy.setTimeout(resolve));
+ }
+ }
+ this.onProgress(total, total);
+ }
+}
diff --git a/comm/mailnews/import/modules/OutlookProfileImporter.jsm b/comm/mailnews/import/modules/OutlookProfileImporter.jsm
new file mode 100644
index 0000000000..6c2b5932f2
--- /dev/null
+++ b/comm/mailnews/import/modules/OutlookProfileImporter.jsm
@@ -0,0 +1,143 @@
+/* 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 = ["OutlookProfileImporter"];
+
+var { BaseProfileImporter } = ChromeUtils.import(
+ "resource:///modules/BaseProfileImporter.jsm"
+);
+var { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+/**
+ * A module to import things from an outlook profile dir into the current
+ * profile.
+ */
+class OutlookProfileImporter extends BaseProfileImporter {
+ USE_FILE_PICKER = false;
+
+ SUPPORTED_ITEMS = {
+ accounts: true,
+ addressBooks: true,
+ calendars: false,
+ mailMessages: true,
+ };
+
+ async getSourceProfiles() {
+ this._importModule = Cc[
+ "@mozilla.org/import/import-outlook;1"
+ ].createInstance(Ci.nsIImportModule);
+ this._importMailGeneric = this._importModule
+ .GetImportInterface("mail")
+ .QueryInterface(Ci.nsIImportGeneric);
+ let importMail = this._importMailGeneric
+ .GetData("mailInterface")
+ .QueryInterface(Ci.nsIImportMail);
+ let outLocation = {};
+ let outFound = {};
+ let outUserVerify = {};
+ importMail.GetDefaultLocation(outLocation, outFound, outUserVerify);
+ if (outLocation.value) {
+ return [{ dir: outLocation.value }];
+ }
+ return [];
+ }
+
+ async startImport(sourceProfileDir, items) {
+ this._logger.debug(
+ `Start importing from ${sourceProfileDir.path}, items=${JSON.stringify(
+ items
+ )}`
+ );
+ this._itemsTotalCount = Object.values(items).filter(Boolean).length;
+ this._itemsImportedCount = 0;
+
+ if (items.accounts) {
+ let importSettings = this._importModule
+ .GetImportInterface("settings")
+ .QueryInterface(Ci.nsIImportSettings);
+ let outLocalAccount = {};
+ importSettings.Import(outLocalAccount);
+ await this._updateProgress();
+ this._onImportAccounts();
+ }
+
+ let successStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+ let errorStr = Cc["@mozilla.org/supports-string;1"].createInstance(
+ Ci.nsISupportsString
+ );
+
+ if (items.mailMessages) {
+ // @see nsIImportGeneric.
+ let wantsProgress = this._importMailGeneric.WantsProgress();
+ this._importMailGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (this._importMailGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import mail messages progress:",
+ this._importMailGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ this._importMailGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing mail messages:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import mail messages:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+
+ if (items.addressBooks) {
+ successStr.data = "";
+ errorStr.data = "";
+
+ let importABGeneric = this._importModule
+ .GetImportInterface("addressbook")
+ .QueryInterface(Ci.nsIImportGeneric);
+ // Set import destination to the personal address book.
+ let addressDestination = Cc[
+ "@mozilla.org/supports-string;1"
+ ].createInstance(Ci.nsISupportsString);
+ addressDestination.data = "jsaddrbook://abooks.sqlite";
+ importABGeneric.SetData("addressDestination", addressDestination);
+
+ // @see nsIImportGeneric.
+ let wantsProgress = importABGeneric.WantsProgress();
+ importABGeneric.BeginImport(successStr, errorStr);
+ if (wantsProgress) {
+ while (importABGeneric.GetProgress() < 100) {
+ this._logger.debug(
+ "Import address books progress:",
+ importABGeneric.GetProgress()
+ );
+ await new Promise(resolve => setTimeout(resolve, 50));
+ importABGeneric.ContinueImport();
+ }
+ }
+ if (successStr.data) {
+ this._logger.debug(
+ "Finished importing address books:",
+ successStr.data
+ );
+ }
+ if (errorStr.data) {
+ this._logger.error("Failed to import address books:", errorStr.data);
+ throw new Error(errorStr.data);
+ }
+ await this._updateProgress();
+ }
+
+ return items.accounts;
+ }
+}
diff --git a/comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm b/comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm
new file mode 100644
index 0000000000..e96cc01a2d
--- /dev/null
+++ b/comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm
@@ -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/. */
+
+const EXPORTED_SYMBOLS = ["SeamonkeyProfileImporter"];
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { ThunderbirdProfileImporter } = ChromeUtils.import(
+ "resource:///modules/ThunderbirdProfileImporter.jsm"
+);
+
+/**
+ * A module to import things from a seamonkey profile dir into the current
+ * profile.
+ */
+class SeamonkeyProfileImporter extends ThunderbirdProfileImporter {
+ NAME = "SeaMonkey";
+
+ /** @see BaseProfileImporter */
+ async getSourceProfiles() {
+ let slugs = {
+ win: ["AppData", "Mozilla", "SeaMonkey"],
+ macosx: ["ULibDir", "Application Support", "SeaMonkey"],
+ linux: ["Home", ".mozilla", "seamonkey"],
+ }[AppConstants.platform];
+ if (!slugs) {
+ // We don't recognize this OS.
+ return [];
+ }
+
+ let seamonkeyRoot = Services.dirsvc.get(slugs[0], Ci.nsIFile);
+ slugs.slice(1).forEach(seamonkeyRoot.append);
+ let profilesIni = seamonkeyRoot.clone();
+ profilesIni.append("profiles.ini");
+ if (!profilesIni.exists()) {
+ // No Seamonkey profile found in the well known location.
+ return [];
+ }
+
+ let profiles = [];
+ let ini = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
+ .getService(Ci.nsIINIParserFactory)
+ .createINIParser(profilesIni);
+ for (let section of ini.getSections()) {
+ let keys = [...ini.getKeys(section)];
+ if (!keys.includes("Path")) {
+ // Not a profile section.
+ continue;
+ }
+
+ let name = keys.includes("Name") ? ini.getString(section, "Name") : null;
+ let path = ini.getString(section, "Path");
+ let isRelative = keys.includes("IsRelative")
+ ? ini.getString(section, "IsRelative") == "1"
+ : false;
+
+ let dir;
+ if (isRelative) {
+ dir = seamonkeyRoot.clone();
+ dir.append(path);
+ } else {
+ dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dir.initWithPath(path);
+ }
+ if (!dir.exists()) {
+ // Not a valid profile.
+ continue;
+ }
+ profiles.push({ name, dir });
+ }
+ return profiles;
+ }
+}
diff --git a/comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm b/comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm
new file mode 100644
index 0000000000..082813309e
--- /dev/null
+++ b/comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm
@@ -0,0 +1,1024 @@
+/* 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 = ["ThunderbirdProfileImporter"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { BaseProfileImporter } = ChromeUtils.import(
+ "resource:///modules/BaseProfileImporter.jsm"
+);
+var { AddrBookFileImporter } = ChromeUtils.import(
+ "resource:///modules/AddrBookFileImporter.jsm"
+);
+
+/**
+ * A pref is represented as [type, name, value].
+ *
+ * @typedef {["Bool"|"Char"|"Int", string, number|string|boolean]} PrefItem
+ *
+ * A map from source smtp server key to target smtp server key.
+ * @typedef {Map<string, string>} SmtpServerKeyMap
+ *
+ * A map from source identity key to target identity key.
+ * @typedef {Map<string, string>} IdentityKeyMap
+ *
+ * A map from source IM account key to target IM account key.
+ * @typedef {Map<string, string>} IMAccountKeyMap
+ *
+ * A map from source incoming server key to target incoming server key.
+ * @typedef {Map<string, string>} IncomingServerKeyMap
+ */
+
+// Pref branches that need special handling.
+const ACCOUNT_MANAGER = "mail.accountmanager.";
+const MAIL_IDENTITY = "mail.identity.";
+const MAIL_SERVER = "mail.server.";
+const MAIL_ACCOUNT = "mail.account.";
+const IM_ACCOUNT = "messenger.account.";
+const MAIL_SMTP = "mail.smtp.";
+const SMTP_SERVER = "mail.smtpserver.";
+const ADDRESS_BOOK = "ldap_2.servers.";
+const LDAP_AUTO_COMPLETE = "ldap_2.autoComplete.";
+const CALENDAR = "calendar.registry.";
+const CALENDAR_LIST = "calendar.list.";
+
+// Prefs (branches) that we do not want to copy directly.
+const IGNORE_PREFS = [
+ "app.update.",
+ "browser.",
+ "calendar.timezone",
+ "devtools.",
+ "extensions.",
+ "mail.cloud_files.accounts.",
+ "mail.newsrc_root",
+ "mail.root.",
+ "mail.smtpservers",
+ "messenger.accounts",
+ "print.",
+ "services.",
+ "toolkit.telemetry.",
+];
+
+/**
+ * A module to import things from another thunderbird profile dir into the
+ * current profile.
+ */
+class ThunderbirdProfileImporter extends BaseProfileImporter {
+ NAME = "Thunderbird";
+
+ IGNORE_DIRS = [
+ "chrome_debugger_profile",
+ "crashes",
+ "datareporting",
+ "extensions",
+ "extension-store",
+ "logs",
+ "minidumps",
+ "saved-telemetry-pings",
+ "security_state",
+ "storage",
+ "xulstore",
+ ];
+
+ async getSourceProfiles() {
+ let profileService = Cc[
+ "@mozilla.org/toolkit/profile-service;1"
+ ].getService(Ci.nsIToolkitProfileService);
+ let sourceProfiles = [];
+ for (let profile of profileService.profiles) {
+ if (profile == profileService.currentProfile) {
+ continue;
+ }
+ sourceProfiles.push({
+ name: profile.name,
+ dir: profile.rootDir,
+ });
+ }
+ return sourceProfiles;
+ }
+
+ async startImport(sourceProfileDir, items) {
+ this._logger.debug(
+ `Start importing from ${sourceProfileDir.path}, items=${JSON.stringify(
+ items
+ )}`
+ );
+
+ this._sourceProfileDir = sourceProfileDir;
+ this._items = items;
+ this._itemsTotalCount = Object.values(items).filter(Boolean).length;
+ this._itemsImportedCount = 0;
+
+ try {
+ this._localServer = MailServices.accounts.localFoldersServer;
+ } catch (e) {}
+
+ if (items.accounts || items.addressBooks || items.calendars) {
+ await this._loadPreferences();
+ }
+
+ if (this._items.accounts) {
+ await this._importServersAndAccounts();
+ this._importOtherPrefs(this._otherPrefs);
+ await this._updateProgress();
+ }
+
+ if (this._items.addressBooks) {
+ await this._importAddressBooks(
+ this._branchPrefsMap.get(ADDRESS_BOOK),
+ this._collectPrefsToObject(this._branchPrefsMap.get(LDAP_AUTO_COMPLETE))
+ );
+ await this._updateProgress();
+ }
+
+ if (this._items.calendars) {
+ this._importCalendars(
+ this._branchPrefsMap.get(CALENDAR),
+ this._collectPrefsToObject(this._branchPrefsMap.get(CALENDAR_LIST))
+ );
+ await this._updateProgress();
+ }
+
+ if (!this._items.accounts && this._items.mailMessages) {
+ this._importMailMessagesToLocal();
+ }
+
+ await this._updateProgress();
+
+ return true;
+ }
+
+ /**
+ * Collect interested prefs from this._sourceProfileDir.
+ */
+ async _loadPreferences() {
+ // A Map to collect all prefs in interested pref branches.
+ // @type {Map<string, PrefItem[]>}
+ this._branchPrefsMap = new Map([
+ [ACCOUNT_MANAGER, []],
+ [MAIL_IDENTITY, []],
+ [MAIL_SERVER, []],
+ [MAIL_ACCOUNT, []],
+ [IM_ACCOUNT, []],
+ [MAIL_SMTP, []],
+ [SMTP_SERVER, []],
+ [ADDRESS_BOOK, []],
+ [LDAP_AUTO_COMPLETE, []],
+ [CALENDAR, []],
+ [CALENDAR_LIST, []],
+ ]);
+ this._otherPrefs = [];
+
+ let sourcePrefsFile = this._sourceProfileDir.clone();
+ sourcePrefsFile.append("prefs.js");
+ let sourcePrefsBuffer = await IOUtils.read(sourcePrefsFile.path);
+
+ let savePref = (type, name, value) => {
+ for (let [branchName, branchPrefs] of this._branchPrefsMap) {
+ if (name.startsWith(branchName)) {
+ branchPrefs.push([type, name.slice(branchName.length), value]);
+ return;
+ }
+ }
+ if (IGNORE_PREFS.some(ignore => name.startsWith(ignore))) {
+ return;
+ }
+ // Collect all the other prefs.
+ this._otherPrefs.push([type, name, value]);
+ };
+
+ Services.prefs.parsePrefsFromBuffer(sourcePrefsBuffer, {
+ onStringPref: (kind, name, value) => savePref("Char", name, value),
+ onIntPref: (kind, name, value) => savePref("Int", name, value),
+ onBoolPref: (kind, name, value) => savePref("Bool", name, value),
+ onError: msg => {
+ throw new Error(msg);
+ },
+ });
+ }
+
+ /**
+ * Import all the servers and accounts.
+ */
+ async _importServersAndAccounts() {
+ // Import SMTP servers first, the importing order is important.
+ let smtpServerKeyMap = this._importSmtpServers(
+ this._branchPrefsMap.get(SMTP_SERVER),
+ this._collectPrefsToObject(this._branchPrefsMap.get(MAIL_SMTP))
+ .defaultserver
+ );
+
+ // mail.identity.idN.smtpServer depends on transformed smtp server key.
+ let identityKeyMap = this._importIdentities(
+ this._branchPrefsMap.get(MAIL_IDENTITY),
+ smtpServerKeyMap
+ );
+ let imAccountKeyMap = await this._importIMAccounts(
+ this._branchPrefsMap.get(IM_ACCOUNT)
+ );
+
+ let accountManager = this._collectPrefsToObject(
+ this._branchPrefsMap.get(ACCOUNT_MANAGER)
+ );
+ // Officially we only support one Local Folders account, if we already have
+ // one, do not import a new one.
+ this._sourceLocalServerKeyToSkip = this._localServer
+ ? accountManager.localfoldersserver
+ : null;
+ this._sourceLocalServerAttrs = {};
+
+ // mail.server.serverN.imAccount depends on transformed im account key.
+ let incomingServerKeyMap = await this._importIncomingServers(
+ this._branchPrefsMap.get(MAIL_SERVER),
+ imAccountKeyMap
+ );
+
+ // mail.account.accountN.{identities, server} depends on previous steps.
+ this._importAccounts(
+ this._branchPrefsMap.get(MAIL_ACCOUNT),
+ accountManager.accounts,
+ accountManager.defaultaccount,
+ identityKeyMap,
+ incomingServerKeyMap
+ );
+
+ await this._importMailMessages(incomingServerKeyMap);
+ if (this._sourceLocalServerKeyToSkip) {
+ this._mergeLocalFolders();
+ }
+
+ if (accountManager.accounts) {
+ this._onImportAccounts();
+ }
+ }
+
+ /**
+ * Collect an array of prefs to an object.
+ *
+ * @param {PrefItem[]} prefs - An array of prefs.
+ * @returns {object} An object mapping pref name to pref value.
+ */
+ _collectPrefsToObject(prefs) {
+ let obj = {};
+ for (let [, name, value] of prefs) {
+ obj[name] = value;
+ }
+ return obj;
+ }
+
+ /**
+ * Import SMTP servers.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the SMTP_SERVER branch.
+ * @param {string} sourceDefaultServer - The value of mail.smtp.defaultserver
+ * in the source profile.
+ * @returns {smtpServerKeyMap} A map from source server key to new server key.
+ */
+ _importSmtpServers(prefs, sourceDefaultServer) {
+ let smtpServerKeyMap = new Map();
+ let branch = Services.prefs.getBranch(SMTP_SERVER);
+ for (let [type, name, value] of prefs) {
+ let key = name.split(".")[0];
+ let newServerKey = smtpServerKeyMap.get(key);
+ if (!newServerKey) {
+ // For every smtp server, create a new one to avoid conflicts.
+ let server = MailServices.smtp.createServer();
+ newServerKey = server.key;
+ smtpServerKeyMap.set(key, newServerKey);
+ this._logger.debug(
+ `Mapping SMTP server from ${key} to ${newServerKey}`
+ );
+ }
+
+ let newName = `${newServerKey}${name.slice(key.length)}`;
+ branch[`set${type}Pref`](newName, value);
+ }
+
+ // Set defaultserver if it doesn't already exist.
+ let defaultServer = Services.prefs.getCharPref(
+ "mail.smtp.defaultserver",
+ ""
+ );
+ if (sourceDefaultServer && !defaultServer) {
+ Services.prefs.setCharPref(
+ "mail.smtp.defaultserver",
+ smtpServerKeyMap.get(sourceDefaultServer)
+ );
+ }
+ return smtpServerKeyMap;
+ }
+
+ /**
+ * Import mail identites.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the MAIL_IDENTITY branch.
+ * @param {SmtpServerKeyMap} smtpServerKeyMap - A map from the source SMTP
+ * server key to new SMTP server key.
+ * @returns {IdentityKeyMap} A map from the source identity key to new identity
+ * key.
+ */
+ _importIdentities(prefs, smtpServerKeyMap) {
+ let identityKeyMap = new Map();
+ let branch = Services.prefs.getBranch(MAIL_IDENTITY);
+ for (let [type, name, value] of prefs) {
+ let key = name.split(".")[0];
+ let newIdentityKey = identityKeyMap.get(key);
+ if (!newIdentityKey) {
+ // For every identity, create a new one to avoid conflicts.
+ let identity = MailServices.accounts.createIdentity();
+ newIdentityKey = identity.key;
+ identityKeyMap.set(key, newIdentityKey);
+ this._logger.debug(`Mapping identity from ${key} to ${newIdentityKey}`);
+ }
+
+ let newName = `${newIdentityKey}${name.slice(key.length)}`;
+ let newValue = value;
+ if (name.endsWith(".smtpServer")) {
+ newValue = smtpServerKeyMap.get(value) || newValue;
+ }
+ branch[`set${type}Pref`](newName, newValue);
+ }
+ return identityKeyMap;
+ }
+
+ /**
+ * Import IM accounts.
+ *
+ * @param {Array<[string, string, number|string|boolean]>} prefs - All source
+ * prefs in the IM_ACCOUNT branch.
+ * @returns {IMAccountKeyMap} A map from the source account key to new account
+ * key.
+ */
+ async _importIMAccounts(prefs) {
+ let imAccountKeyMap = new Map();
+ let branch = Services.prefs.getBranch(IM_ACCOUNT);
+
+ let lastKey = 1;
+ function _getUniqueAccountKey() {
+ let key = `account${lastKey++}`;
+ if (Services.prefs.getCharPref(`messenger.account.${key}.name`, "")) {
+ return _getUniqueAccountKey();
+ }
+ return key;
+ }
+
+ for (let [type, name, value] of prefs) {
+ let key = name.split(".")[0];
+ let newAccountKey = imAccountKeyMap.get(key);
+ if (!newAccountKey) {
+ // For every account, create a new one to avoid conflicts.
+ newAccountKey = _getUniqueAccountKey();
+ imAccountKeyMap.set(key, newAccountKey);
+ this._logger.debug(
+ `Mapping IM account from ${key} to ${newAccountKey}`
+ );
+ }
+
+ let newName = `${newAccountKey}${name.slice(key.length)}`;
+ branch[`set${type}Pref`](newName, value);
+ }
+
+ return imAccountKeyMap;
+ }
+
+ /**
+ * Import incoming servers.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the MAIL_SERVER branch.
+ * @param {IMAccountKeyMap} imAccountKeyMap - A map from the source account
+ * key to new account key.
+ * @returns {IncomingServerKeyMap} A map from the source server key to new
+ * server key.
+ */
+ async _importIncomingServers(prefs, imAccountKeyMap) {
+ let incomingServerKeyMap = new Map();
+ let branch = Services.prefs.getBranch(MAIL_SERVER);
+
+ let lastKey = 1;
+ function _getUniqueIncomingServerKey() {
+ let key = `server${lastKey++}`;
+ if (branch.getCharPref(`${key}.type`, "")) {
+ return _getUniqueIncomingServerKey();
+ }
+ return key;
+ }
+
+ for (let [type, name, value] of prefs) {
+ let [key, attr] = name.split(".");
+ if (key == this._sourceLocalServerKeyToSkip) {
+ if (["directory", "directory-rel"].includes(attr)) {
+ this._sourceLocalServerAttrs[attr] = value;
+ }
+ // We already have a Local Folders account.
+ continue;
+ }
+ if (attr == "deferred_to_account") {
+ // Handling deferred account is a bit complicated, to prevent potential
+ // problems, just skip this pref so it becomes a normal account.
+ continue;
+ }
+ let newServerKey = incomingServerKeyMap.get(key);
+ if (!newServerKey) {
+ // For every incoming server, create a new one to avoid conflicts.
+ newServerKey = _getUniqueIncomingServerKey();
+ incomingServerKeyMap.set(key, newServerKey);
+ this._logger.debug(`Mapping server from ${key} to ${newServerKey}`);
+ }
+
+ let newName = `${newServerKey}${name.slice(key.length)}`;
+ let newValue = value;
+ if (newName.endsWith(".imAccount")) {
+ newValue = imAccountKeyMap.get(value);
+ }
+ branch[`set${type}Pref`](newName, newValue || value);
+ }
+ return incomingServerKeyMap;
+ }
+
+ /**
+ * Import mail accounts.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the MAIL_ACCOUNT branch.
+ * @param {string} sourceAccounts - The value of mail.accountmanager.accounts
+ * in the source profile.
+ * @param {string} sourceDefaultAccount - The value of
+ * mail.accountmanager.defaultaccount in the source profile.
+ * @param {IdentityKeyMap} identityKeyMap - A map from the source identity key
+ * to new identity key.
+ * @param {IncomingServerKeyMap} incomingServerKeyMap - A map from the source
+ * server key to new server key.
+ */
+ _importAccounts(
+ prefs,
+ sourceAccounts,
+ sourceDefaultAccount,
+ identityKeyMap,
+ incomingServerKeyMap
+ ) {
+ let accountKeyMap = new Map();
+ let branch = Services.prefs.getBranch(MAIL_ACCOUNT);
+ for (let [type, name, value] of prefs) {
+ let key = name.split(".")[0];
+ if (key == "lastKey" || value == this._sourceLocalServerKeyToSkip) {
+ continue;
+ }
+ let newAccountKey = accountKeyMap.get(key);
+ if (!newAccountKey) {
+ // For every account, create a new one to avoid conflicts.
+ newAccountKey = MailServices.accounts.getUniqueAccountKey();
+ accountKeyMap.set(key, newAccountKey);
+ }
+
+ let newName = `${newAccountKey}${name.slice(key.length)}`;
+ let newValue = value;
+ if (name.endsWith(".identities")) {
+ // An account can have multiple identities.
+ newValue = value
+ .split(",")
+ .map(v => identityKeyMap.get(v))
+ .filter(Boolean)
+ .join(",");
+ } else if (name.endsWith(".server")) {
+ newValue = incomingServerKeyMap.get(value);
+ }
+ branch[`set${type}Pref`](newName, newValue || value);
+ }
+
+ // Append newly create accounts to mail.accountmanager.accounts.
+ let accounts = Services.prefs
+ .getCharPref("mail.accountmanager.accounts", "")
+ .split(",");
+ if (sourceAccounts) {
+ for (let sourceAccountKey of sourceAccounts.split(",")) {
+ accounts.push(accountKeyMap.get(sourceAccountKey));
+ }
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ accounts.filter(Boolean).join(",")
+ );
+ }
+
+ // Set defaultaccount if it doesn't already exist.
+ let defaultAccount = Services.prefs.getCharPref(
+ "mail.accountmanager.defaultaccount",
+ ""
+ );
+ if (sourceDefaultAccount && !defaultAccount) {
+ Services.prefs.setCharPref(
+ "mail.accountmanager.defaultaccount",
+ accountKeyMap.get(sourceDefaultAccount)
+ );
+ }
+ }
+
+ /**
+ * Try to locate a file specified by the relative path, if not possible, use
+ * the absolute path.
+ *
+ * @param {string} relValue - The pref value for the relative file path.
+ * @param {string} absValue - The pref value for the absolute file path.
+ * @returns {nsIFile}
+ */
+ _getSourceFileFromPaths(relValue, absValue) {
+ let relPath = relValue.slice("[ProfD]".length);
+ let parts = relPath.split("/");
+ if (!relValue.startsWith("[ProfD]") || parts.includes("..")) {
+ // If we don't recognize this path or if it's a path outside the ProfD,
+ // use absValue instead.
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ try {
+ file.initWithPath(absValue);
+ } catch (e) {
+ this._logger.warn("nsIFile.initWithPath failed for path=", absValue);
+ return null;
+ }
+ return file;
+ }
+
+ let sourceFile = this._sourceProfileDir.clone();
+ for (let part of parts) {
+ sourceFile.append(part);
+ }
+ return sourceFile;
+ }
+
+ /**
+ * Copy mail folders from this._sourceProfileDir to the current profile dir.
+ *
+ * @param {PrefKeyMap} incomingServerKeyMap - A map from the source server key
+ * to new server key.
+ */
+ async _importMailMessages(incomingServerKeyMap) {
+ for (let key of incomingServerKeyMap.values()) {
+ let branch = Services.prefs.getBranch(`${MAIL_SERVER}${key}.`);
+ if (!branch) {
+ continue;
+ }
+ let type = branch.getCharPref("type", "");
+ let hostname = branch.getCharPref("hostname", "");
+ if (!type || !hostname) {
+ continue;
+ }
+
+ let targetDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ if (type == "imap") {
+ targetDir.append("ImapMail");
+ } else if (type == "nntp") {
+ targetDir.append("News");
+ } else if (["none", "pop3", "rss"].includes(type)) {
+ targetDir.append("Mail");
+ } else {
+ continue;
+ }
+
+ this._logger.debug("Importing mail messages for", key);
+
+ let sourceDir = this._getSourceFileFromPaths(
+ branch.getCharPref("directory-rel", ""),
+ branch.getCharPref("directory", "")
+ );
+ if (sourceDir?.exists()) {
+ // Use the hostname as mail folder name and ensure it's unique.
+ targetDir.append(hostname);
+ targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ this._recursivelyCopyMsgFolder(sourceDir, targetDir);
+ branch.setCharPref("directory", targetDir.path);
+ // .directory-rel may be outdated, it will be created when first needed.
+ branch.clearUserPref("directory-rel");
+ }
+
+ if (type == "nntp") {
+ let targetNewsrc = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ targetNewsrc.append("News");
+ targetNewsrc.append(`newsrc-${hostname}`);
+ targetNewsrc.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ let sourceNewsrc = this._getSourceFileFromPaths(
+ branch.getCharPref("newsrc.file-rel", ""),
+ branch.getCharPref("newsrc.file", "")
+ );
+ if (sourceNewsrc?.exists()) {
+ this._logger.debug(
+ `Copying ${sourceNewsrc.path} to ${targetNewsrc.path}`
+ );
+ sourceNewsrc.copyTo(targetNewsrc.parent, targetNewsrc.leafName);
+ }
+
+ branch.setCharPref("newsrc.file", targetNewsrc.path);
+ // .file-rel may be outdated, it will be created when first needed.
+ branch.clearUserPref("newsrc.file-rel");
+ }
+ }
+ }
+
+ /**
+ * Merge Local Folders from the source profile into the current profile.
+ * Source Local Folders become a subfoler of the current Local Folders.
+ */
+ _mergeLocalFolders() {
+ let sourceDir = this._getSourceFileFromPaths(
+ this._sourceLocalServerAttrs["directory-rel"],
+ this._sourceLocalServerAttrs.directory
+ );
+ if (!sourceDir?.exists()) {
+ return;
+ }
+ let rootMsgFolder = this._localServer.rootMsgFolder;
+ let folderName = rootMsgFolder.generateUniqueSubfolderName(
+ "Local Folders",
+ null
+ );
+ rootMsgFolder.createSubfolder(folderName, null);
+ let targetDir = rootMsgFolder.filePath;
+ targetDir.append(folderName + ".sbd");
+ this._logger.debug(
+ `Copying ${sourceDir.path} to ${targetDir.path} in Local Folders`
+ );
+ this._recursivelyCopyMsgFolder(sourceDir, targetDir, true);
+ }
+
+ /**
+ * Copy a source msg folder to a destination.
+ *
+ * @param {nsIFile} sourceDir - The source msg folder location.
+ * @param {nsIFile} targetDir - The target msg folder location.
+ * @param {boolean} isTargetLocal - Whether the targetDir is a subfolder in
+ * the Local Folders.
+ */
+ _recursivelyCopyMsgFolder(sourceDir, targetDir, isTargetLocal) {
+ this._logger.debug(`Copying ${sourceDir.path} to ${targetDir.path}`);
+
+ // Copy the whole sourceDir.
+ if (!isTargetLocal && this._items.accounts && this._items.mailMessages) {
+ // Remove the folder so that nsIFile.copyTo doesn't copy into targetDir.
+ targetDir.remove(false);
+ sourceDir.copyTo(targetDir.parent, targetDir.leafName);
+ return;
+ }
+
+ for (let entry of sourceDir.directoryEntries) {
+ if (entry.isDirectory()) {
+ let newFolder = targetDir.clone();
+ newFolder.append(entry.leafName);
+ newFolder.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ this._recursivelyCopyMsgFolder(entry, newFolder);
+ } else {
+ let leafName = entry.leafName;
+ let extName = leafName.slice(leafName.lastIndexOf(".") + 1);
+ if (isTargetLocal) {
+ // When copying to Local Folders, drop database files so that special
+ // folders (Inbox, Trash) become normal folders. Otherwise, imported
+ // special folders can't be deleted.
+ if (extName != "msf") {
+ entry.copyTo(targetDir, leafName);
+ }
+ } else if (
+ this._items.accounts &&
+ extName != leafName &&
+ ["msf", "dat"].includes(extName)
+ ) {
+ // Copy only the folder structure, databases and filter rules.
+ // Ignore the messages themselves.
+ entry.copyTo(targetDir, leafName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Import msg folders from this._sourceProfileDir into the Local Folders of
+ * the current profile.
+ */
+ _importMailMessagesToLocal() {
+ // Make sure Local Folders exist first.
+ if (!this._localServer) {
+ MailServices.accounts.createLocalMailAccount();
+ this._localServer = MailServices.accounts.localFoldersServer;
+ }
+ let localMsgFolder = this._localServer.rootMsgFolder;
+ let localRootDir = this._localServer.rootMsgFolder.filePath;
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/importMsgs.properties"
+ );
+
+ // Create a "Thunderbird Import" folder, and import into it.
+ let wrapFolderName = localMsgFolder.generateUniqueSubfolderName(
+ bundle.formatStringFromName("ImportModuleFolderName", [this.NAME]),
+ null
+ );
+ localMsgFolder.createSubfolder(wrapFolderName, null);
+ let targetRootMsgFolder = localMsgFolder.getChildNamed(wrapFolderName);
+
+ // Import mail folders.
+ for (let name of ["ImapMail", "News", "Mail"]) {
+ let sourceDir = this._sourceProfileDir.clone();
+ sourceDir.append(name);
+ if (!sourceDir.exists()) {
+ continue;
+ }
+
+ for (let entry of sourceDir.directoryEntries) {
+ if (entry.isDirectory()) {
+ if (name == "Mail" && entry.leafName == "Feeds") {
+ continue;
+ }
+ let targetDir = localRootDir.clone();
+ let folderName = targetRootMsgFolder.generateUniqueSubfolderName(
+ entry.leafName,
+ null
+ );
+ targetRootMsgFolder.createSubfolder(folderName, null);
+ targetDir.append(wrapFolderName + ".sbd");
+ targetDir.append(folderName + ".sbd");
+ targetDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ this._recursivelyCopyMsgFolder(entry, targetDir, true);
+ }
+ }
+ }
+ }
+
+ /**
+ * Import a pref from source only when this pref has no user value in the
+ * current profile.
+ *
+ * @param {PrefItem[]} prefs - All source prefs to try to import.
+ */
+ _importOtherPrefs(prefs) {
+ let tags = {};
+ for (let [type, name, value] of prefs) {
+ if (name.startsWith("mailnews.tags.")) {
+ let [, , key, attr] = name.split(".");
+ if (!tags[key]) {
+ tags[key] = {};
+ }
+ tags[key][attr] = value;
+ continue;
+ }
+ if (!Services.prefs.prefHasUserValue(name)) {
+ Services.prefs[`set${type}Pref`](name, value);
+ }
+ }
+
+ // Import tags, but do not overwrite existing customized tags.
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ for (let [key, { color, tag }] of Object.entries(tags)) {
+ if (!color || !tag) {
+ continue;
+ }
+ let currentTagColor, currentTagTag;
+ try {
+ currentTagColor = MailServices.tags.getColorForKey(key);
+ currentTagTag = MailServices.tags.getTagForKey(key);
+ } catch (e) {
+ // No tag exists for this key in the current profile, safe to write.
+ Services.prefs.setCharPref(`mailnews.tags.${key}.color`, color);
+ Services.prefs.setCharPref(`mailnews.tags.${key}.tag`, tag);
+ }
+ if (currentTagColor == color && currentTagTag == tag) {
+ continue;
+ }
+ if (
+ ["$label1", "$label2", "$label3", "$label4", "$label5"].includes(key)
+ ) {
+ let seq = key.at(-1);
+ let defaultColor = Services.prefs.getCharPref(
+ `mailnews.labels.color.${seq}`
+ );
+ let defaultTag = bundle.GetStringFromName(
+ `mailnews.labels.description.${seq}`
+ );
+ if (currentTagColor == defaultColor && currentTagTag == defaultTag) {
+ // The existing tag is in default state, safe to write.
+ Services.prefs.setCharPref(`mailnews.tags.${key}.color`, color);
+ Services.prefs.setCharPref(`mailnews.tags.${key}.tag`, tag);
+ }
+ }
+ }
+ }
+
+ /**
+ * Import address books.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the ADDRESS_BOOK branch.
+ * @param {object} ldapAutoComplete - Pref values of LDAP_AUTO_COMPLETE branch.
+ * @param {boolean} ldapAutoComplete.useDirectory
+ * @param {string} ldapAutoComplete.directoryServer
+ */
+ async _importAddressBooks(prefs, ldapAutoComplete) {
+ let keyMap = new Map();
+ let branch = Services.prefs.getBranch(ADDRESS_BOOK);
+ for (let [type, name, value] of prefs) {
+ let [key, attr] = name.split(".");
+ if (["pab", "history"].includes(key)) {
+ continue;
+ }
+ if (attr == "uid") {
+ // Prevent duplicated uids when importing back, uid will be created when
+ // first used.
+ continue;
+ }
+ let newKey = keyMap.get(key);
+ if (!newKey) {
+ // For every address book, create a new one to avoid conflicts.
+ let uniqueCount = 0;
+ newKey = key;
+ while (true) {
+ if (!branch.getCharPref(`${newKey}.filename`, "")) {
+ break;
+ }
+ newKey = `${key}${++uniqueCount}`;
+ }
+ keyMap.set(key, newKey);
+ }
+
+ let newName = `${newKey}${name.slice(key.length)}`;
+ if (newName.endsWith(".dirType") && value == 2) {
+ // dirType=2 is a Mab file, we will migrate it in _copyAddressBookDatabases.
+ value = Ci.nsIAbManager.JS_DIRECTORY_TYPE;
+ }
+ branch[`set${type}Pref`](newName, value);
+ }
+
+ // Transform the value of ldap_2.autoComplete.directoryServer if needed.
+ if (
+ ldapAutoComplete.useDirectory &&
+ ldapAutoComplete.directoryServer &&
+ !Services.prefs.getBoolPref(`${LDAP_AUTO_COMPLETE}useDirectory`, false)
+ ) {
+ let key = ldapAutoComplete.directoryServer.split("/").slice(-1)[0];
+ let newKey = keyMap.get(key);
+ if (newKey) {
+ Services.prefs.setBoolPref(`${LDAP_AUTO_COMPLETE}useDirectory`, true);
+ Services.prefs.setCharPref(
+ `${LDAP_AUTO_COMPLETE}directoryServer`,
+ `ldap_2.servers.${newKey}`
+ );
+ }
+ }
+
+ await this._copyAddressBookDatabases(keyMap);
+ }
+
+ /**
+ * Copy sqlite files from this._sourceProfileDir to the current profile dir.
+ *
+ * @param {Map<string, string>} keyMap - A map from the source address
+ * book key to new address book key.
+ */
+ async _copyAddressBookDatabases(keyMap) {
+ let hasMabFile = false;
+
+ // Copy user created address books.
+ for (let key of keyMap.values()) {
+ let branch = Services.prefs.getBranch(`${ADDRESS_BOOK}${key}.`);
+ let filename = branch.getCharPref("filename", "");
+ if (!filename) {
+ continue;
+ }
+ let sourceFile = this._sourceProfileDir.clone();
+ sourceFile.append(filename);
+ if (!sourceFile.exists()) {
+ this._logger.debug(
+ `Ignoring non-existing address book file ${sourceFile.path}`
+ );
+ continue;
+ }
+
+ let leafName = sourceFile.leafName;
+ let isMabFile = leafName.endsWith(".mab");
+ if (isMabFile) {
+ leafName = leafName.slice(0, -4) + ".sqlite";
+ hasMabFile = true;
+ }
+ let targetFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ targetFile.append(leafName);
+ targetFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ branch.setCharPref("filename", targetFile.leafName);
+ this._logger.debug(`Copying ${sourceFile.path} to ${targetFile.path}`);
+ if (isMabFile) {
+ await this._migrateMabToSqlite(sourceFile, targetFile);
+ } else {
+ sourceFile.copyTo(targetFile.parent, targetFile.leafName);
+ // Write-Ahead Logging file contains changes not written to .sqlite file
+ // yet.
+ let sourceWalFile = this._sourceProfileDir.clone();
+ sourceWalFile.append(filename + "-wal");
+ if (sourceWalFile.exists()) {
+ sourceWalFile.copyTo(targetFile.parent, targetFile.leafName + "-wal");
+ }
+ }
+ }
+
+ if (hasMabFile) {
+ await this._importMorkDatabase("abook");
+ await this._importMorkDatabase("history");
+ } else {
+ // Copy or import Personal Address Book.
+ await this._importAddressBookDatabase("abook.sqlite");
+ // Copy or import Collected Addresses.
+ await this._importAddressBookDatabase("history.sqlite");
+ }
+ }
+
+ /**
+ * Copy a sqlite file from this._sourceProfileDir to the current profile dir.
+ *
+ * @param {string} filename - The name of the sqlite file.
+ */
+ async _importAddressBookDatabase(filename) {
+ let sourceFile = this._sourceProfileDir.clone();
+ sourceFile.append(filename);
+ if (!sourceFile.exists()) {
+ return;
+ }
+
+ let targetDirectory = MailServices.ab.getDirectory(
+ `jsaddrbook://${filename}`
+ );
+ if (!targetDirectory) {
+ sourceFile.copyTo(Services.dirsvc.get("ProfD", Ci.nsIFile), "");
+ return;
+ }
+
+ let importer = new AddrBookFileImporter("sqlite");
+ await importer.startImport(sourceFile, targetDirectory);
+ }
+
+ /**
+ * Migrate an address book .mab file to a .sqlite file.
+ *
+ * @param {nsIFile} sourceMabFile - The source .mab file.
+ * @param {nsIFile} targetSqliteFile - The target .sqlite file, should already
+ * exists in the profile dir.
+ */
+ async _migrateMabToSqlite(sourceMabFile, targetSqliteFile) {
+ // It's better to use MailServices.ab.getDirectory, but we need to refresh
+ // AddrBookManager first.
+ let targetDirectory = Cc[
+ "@mozilla.org/addressbook/directory;1?type=jsaddrbook"
+ ].createInstance(Ci.nsIAbDirectory);
+ targetDirectory.init(`jsaddrbook://${targetSqliteFile.leafName}`);
+
+ let importer = new AddrBookFileImporter("mab");
+ await importer.startImport(sourceMabFile, targetDirectory);
+ }
+
+ /**
+ * Import pab/history address book from mab file into the corresponding sqlite
+ * file.
+ *
+ * @param {string} basename - The filename without extension, e.g. "abook".
+ */
+ async _importMorkDatabase(basename) {
+ this._logger.debug(`Importing ${basename}.mab into ${basename}.sqlite`);
+
+ let sourceMabFile = this._sourceProfileDir.clone();
+ sourceMabFile.append(`${basename}.mab`);
+ if (!sourceMabFile.exists()) {
+ return;
+ }
+
+ let targetDirectory;
+ try {
+ targetDirectory = MailServices.ab.getDirectory(
+ `jsaddrbook://${basename}.sqlite`
+ );
+ } catch (e) {
+ this._logger.warn(`Failed to open ${basename}.sqlite`, e);
+ return;
+ }
+
+ let importer = new AddrBookFileImporter("mab");
+ await importer.startImport(sourceMabFile, targetDirectory);
+ }
+
+ /**
+ * Import calendars.
+ *
+ * For storage calendars, we need to import everything from the source
+ * local.sqlite to the target local.sqlite, which is not implemented yet, see
+ * bug 1719582.
+ *
+ * @param {PrefItem[]} prefs - All source prefs in the CALENDAR branch.
+ * @param {object} calendarList - Pref values of CALENDAR_LIST branch.
+ */
+ _importCalendars(prefs, calendarList) {
+ let branch = Services.prefs.getBranch(CALENDAR);
+ for (let [type, name, value] of prefs) {
+ branch[`set${type}Pref`](name, value);
+ }
+
+ if (calendarList.sortOrder) {
+ let prefName = `${CALENDAR_LIST}sortOrder`;
+ let prefValue =
+ Services.prefs.getCharPref(prefName, "") + " " + calendarList.sortOrder;
+ Services.prefs.setCharPref(prefName, prefValue.trim());
+ }
+ }
+}
diff --git a/comm/mailnews/import/modules/moz.build b/comm/mailnews/import/modules/moz.build
new file mode 100644
index 0000000000..5173f10d46
--- /dev/null
+++ b/comm/mailnews/import/modules/moz.build
@@ -0,0 +1,22 @@
+# 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/.
+
+EXTRA_JS_MODULES += [
+ "AddrBookFileImporter.jsm",
+ "BaseProfileImporter.jsm",
+ "CalendarFileImporter.jsm",
+ "SeamonkeyProfileImporter.jsm",
+ "ThunderbirdProfileImporter.jsm",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ EXTRA_JS_MODULES += [
+ "BeckyProfileImporter.jsm",
+ "OutlookProfileImporter.jsm",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ EXTRA_JS_MODULES += [
+ "AppleMailProfileImporter.jsm",
+ ]