diff options
Diffstat (limited to 'comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm')
-rw-r--r-- | comm/mailnews/import/modules/ThunderbirdProfileImporter.jsm | 1024 |
1 files changed, 1024 insertions, 0 deletions
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()); + } + } +} |