diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/import/modules | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/import/modules/AddrBookFileImporter.jsm | 356 | ||||
-rw-r--r-- | comm/mailnews/import/modules/AppleMailProfileImporter.jsm | 92 | ||||
-rw-r--r-- | comm/mailnews/import/modules/BaseProfileImporter.jsm | 100 | ||||
-rw-r--r-- | comm/mailnews/import/modules/BeckyProfileImporter.jsm | 125 | ||||
-rw-r--r-- | comm/mailnews/import/modules/CalendarFileImporter.jsm | 127 | ||||
-rw-r--r-- | comm/mailnews/import/modules/OutlookProfileImporter.jsm | 143 | ||||
-rw-r--r-- | comm/mailnews/import/modules/SeamonkeyProfileImporter.jsm | 75 | ||||
-rw-r--r-- | comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm | 1024 | ||||
-rw-r--r-- | comm/mailnews/import/modules/moz.build | 22 |
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", + ] |