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/content | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.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 'comm/mailnews/import/content')
-rw-r--r-- | comm/mailnews/import/content/aboutImport.js | 1511 | ||||
-rw-r--r-- | comm/mailnews/import/content/aboutImport.xhtml | 477 | ||||
-rw-r--r-- | comm/mailnews/import/content/csv-field-map.js | 280 | ||||
-rw-r--r-- | comm/mailnews/import/content/fieldMapImport.js | 259 | ||||
-rw-r--r-- | comm/mailnews/import/content/fieldMapImport.xhtml | 104 | ||||
-rw-r--r-- | comm/mailnews/import/content/importDialog.js | 1184 | ||||
-rw-r--r-- | comm/mailnews/import/content/importDialog.xhtml | 225 |
7 files changed, 4040 insertions, 0 deletions
diff --git a/comm/mailnews/import/content/aboutImport.js b/comm/mailnews/import/content/aboutImport.js new file mode 100644 index 0000000000..1f0d06d38d --- /dev/null +++ b/comm/mailnews/import/content/aboutImport.js @@ -0,0 +1,1511 @@ +/* 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/. */ + +/* global MozElements */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + MailServices: "resource:///modules/MailServices.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", + AddrBookFileImporter: "resource:///modules/AddrBookFileImporter.jsm", + CalendarFileImporter: "resource:///modules/CalendarFileImporter.jsm", + ProfileExporter: "resource:///modules/ProfileExporter.jsm", + cal: "resource:///modules/calendar/calUtils.jsm", +}); + +/** + * 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. + */ + +/** + * @typedef {object} Step + * @property {Function} returnTo - Function that resets to this step. Should end + * up calling |updateSteps()| with this step again. + */ + +const Steps = { + _pastSteps: [], + /** + * Toggle visibility of the navigation steps. + * + * @param {boolean} visible - If the navigation steps should be shown. + */ + toggle(visible) { + document.getElementById("stepNav").hidden = !visible; + }, + /** + * Update the currently displayed steps by adding a new step and updating the + * forecast of remaining steps. + * + * @param {Step} currentStep + * @param {number} plannedSteps - Amount of steps to follow this step, + * including summary. + */ + updateSteps(currentStep, plannedSteps) { + this._pastSteps.push(currentStep); + let confirm = document.getElementById("navConfirm"); + const isConfirmStep = plannedSteps === 0; + confirm.classList.toggle("current", isConfirmStep); + confirm.toggleAttribute("disabled", isConfirmStep); + confirm.removeAttribute("aria-current"); + document.getElementById("stepNav").replaceChildren( + ...this._pastSteps.map((step, index) => { + const li = document.createElement("li"); + const button = document.createElement("button"); + if (step === currentStep) { + if (isConfirmStep) { + confirm.setAttribute("aria-current", "step"); + return confirm; + } + li.classList.add("current"); + li.setAttribute("aria-current", "step"); + button.setAttribute("disabled", "disabled"); + } else { + li.classList.add("completed"); + button.addEventListener("click", () => { + this.backTo(index); + }); + } + document.l10n.setAttributes(button, "step-count", { + number: index + 1, + }); + li.append(button); + //TODO tooltips + return li; + }), + ...new Array(Math.max(plannedSteps - 1, 0)) + .fill(null) + .map((item, index) => { + const li = document.createElement("li"); + const button = document.createElement("button"); + document.l10n.setAttributes(button, "step-count", { + number: this._pastSteps.length + index + 1, + }); + button.setAttribute("disabled", "disabled"); + li.append(button); + //TODO tooltips + return li; + }), + isConfirmStep ? "" : confirm + ); + }, + /** + * Return to a previous step. + * + * @param {number} [stepIndex=-1] - The absolute index of the step to return + * to. By default goes back one step. + * @returns {boolean} if a previous step was recalled. + */ + backTo(stepIndex = -1) { + if (!this._pastSteps.length || stepIndex >= this._pastSteps.length) { + return false; + } + if (stepIndex < 0) { + // Make relative step index absolute + stepIndex = this._pastSteps.length + stepIndex - 1; + } + let targetStep = this._pastSteps[stepIndex]; + this._pastSteps = this._pastSteps.slice(0, stepIndex); + targetStep.returnTo(); + return true; + }, + /** + * If any previous steps have been recorded. + * + * @returns {boolean} If there are steps preceding the current state. + */ + hasStepHistory() { + return this._pastSteps.length > 0; + }, + /** + * Reset step state. + */ + reset() { + this._pastSteps = []; + }, +}; + +/** + * The base controller for an importing process. + */ +class ImporterController { + _logger = console.createInstance({ + prefix: "mail.import", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.import.loglevel", + }); + + /** + * @param {string} elementId - The root element id. + * @param {string} paneIdPrefix - The prefix of sub pane id. + */ + constructor(elementId, paneIdPrefix) { + this._el = document.getElementById(elementId); + this._paneIdPrefix = paneIdPrefix; + } + + /** + * Show a specific pane, hide all the others. + * + * @param {string} id - The pane id to show. + */ + showPane(id) { + this._currentPane = id; + id = `${this._paneIdPrefix}-${id}`; + for (let pane of this._el.querySelectorAll(":scope > section")) { + pane.hidden = pane.id != id; + } + } + + /** + * Show the previous pane. + */ + back() { + ImporterController.notificationBox.removeAllNotifications(); + Steps.backTo(); + } + + /** + * Show the next pane. + */ + next() { + if (this._restartOnOk) { + window.close(); + MailUtils.restartApplication(); + return; + } + ImporterController.notificationBox.removeAllNotifications(); + } + + /** + * Show the first pane. + */ + reset() { + this._el.classList.remove( + "restart-only", + "progress", + "complete", + "final-step" + ); + this._toggleBackButton(true); + } + + /** + * Show the progress bar. + * + * @param {string} progressL10nId - Fluent ID to use for the progress + * description. Should have a |progressPercent| variable expecting the + * current progress like "50%". + */ + showProgress(progressL10nId) { + this._progressL10nId = progressL10nId; + this.updateProgress(0); + this._el.classList.add("progress"); + this._toggleBackButton(false); + this._inProgress = true; + } + + /** + * Update the progress bar. + * + * @param {number} value - A number between 0 and 1 to represent the progress. + */ + updateProgress(value) { + this._el.querySelector(".progressPaneProgressBar").value = value; + document.l10n.setAttributes( + this._el.querySelector(".progressPaneDesc"), + this._progressL10nId, + { + progressPercent: ImporterController.percentFormatter.format(value), + } + ); + } + + /** + * Show the finish text. + * + * @param {boolean} [restartNeeded=false] - Whether restart is needed to + * finish the importing. + */ + finish(restartNeeded = false) { + this._restartOnOk = restartNeeded; + this._el.classList.toggle("restart-required", restartNeeded); + this._el.classList.add("complete"); + document.l10n.setAttributes( + this._el.querySelector(".progressPaneDesc"), + "progress-pane-finished-desc2" + ); + this._inProgress = false; + } + + /** + * Show the error pane, with an error message. + * + * @param {string} msgId - The error message fluent id. + */ + showError(msgId) { + if (this._inProgress) { + this._toggleBackButton(true); + this._el.classList.remove("progress"); + this._restartOnOk = false; + this._inProgress = false; + } + ImporterController.notificationBox.removeAllNotifications(); + let notification = ImporterController.notificationBox.appendNotification( + "error", + { + label: { + "l10n-id": msgId, + }, + priority: ImporterController.notificationBox.PRIORITY_CRITICAL_HIGH, + }, + null + ); + notification.removeAttribute("dismissable"); + } + + /** + * Disable/enable the back button. + * + * @param {boolean} enable - If the back button should be enabled + */ + _toggleBackButton(enable) { + if (this._el.querySelector(".buttons-container")) { + this._el.querySelector(".back").disabled = !enable; + } + } +} + +XPCOMUtils.defineLazyGetter( + ImporterController, + "percentFormatter", + () => + new Intl.NumberFormat(undefined, { + style: "percent", + }) +); +XPCOMUtils.defineLazyGetter( + ImporterController, + "notificationBox", + () => + new MozElements.NotificationBox(element => { + element.setAttribute("notificationside", "bottom"); + document.getElementById("errorNotifications").append(element); + }) +); + +/** + * Control the #tabPane-app element, to support importing from an application. + */ +class ProfileImporterController extends ImporterController { + constructor() { + super("tabPane-app", "app"); + document.getElementById("appItemsList").addEventListener( + "input", + () => { + let state = this._getItemsChecked(true); + document.getElementById("profileNextButton").disabled = Object.values( + state + ).every(isChecked => !isChecked); + }, + { + capture: true, + passive: true, + } + ); + } + + /** + * A map from radio input value to the importer module name. + */ + _sourceModules = { + Thunderbird: "ThunderbirdProfileImporter", + Seamonkey: "SeamonkeyProfileImporter", + Outlook: "OutlookProfileImporter", + Becky: "BeckyProfileImporter", + AppleMail: "AppleMailProfileImporter", + }; + + /** + * Maps app radio input values to their respective representations in l10n + * ids. + */ + _sourceL10nIds = { + Thunderbird: "thunderbird", + Seamonkey: "seamonkey", + Outlook: "outlook", + Becky: "becky", + AppleMail: "apple-mail", + }; + _sourceAppName = "thunderbird"; + + next() { + super.next(); + switch (this._currentPane) { + case "profiles": + this._onSelectProfile(); + break; + case "items": + this._onSelectItems(); + break; + case "summary": + window.close(); + break; + } + } + + /** + * Handler for the Continue button on the sources pane. + * + * @param {string} source - Profile source to import. + */ + async _onSelectSource(source) { + this._sourceAppName = this._sourceL10nIds[source]; + let sourceModule = this._sourceModules[source]; + + let module = ChromeUtils.import(`resource:///modules/${sourceModule}.jsm`); + this._importer = new module[sourceModule](); + + let sourceProfiles = await this._importer.getSourceProfiles(); + if (sourceProfiles.length > 1 || this._importer.USE_FILE_PICKER) { + // Let the user pick a profile if there are multiple options. + this._showProfiles(sourceProfiles, this._importer.USE_FILE_PICKER); + } else if (sourceProfiles.length == 1) { + // Let the user pick what to import. + this._showItems(sourceProfiles[0]); + } else { + this.showError("error-message-no-profile"); + throw new Error("No profile found, do not advance to app flow."); + } + } + + /** + * Show the profiles pane, with a list of profiles and optional file pickers. + * + * @param {SourceProfile[]} profiles - An array of profiles. + * @param {boolean} useFilePicker - Whether to render file pickers. + */ + _showProfiles(profiles, useFilePicker) { + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + this._showProfiles(profiles, useFilePicker); + }, + }, + 2 + ); + this._sourceProfiles = profiles; + document.l10n.setAttributes( + document.getElementById("profilesPaneTitle"), + `from-app-${this._sourceAppName}` + ); + document.l10n.setAttributes( + document.getElementById("profilesPaneSubtitle"), + `profiles-pane-title-${this._sourceAppName}` + ); + let elProfileList = document.getElementById("profileList"); + elProfileList.hidden = !profiles.length; + elProfileList.innerHTML = ""; + document.getElementById("filePickerList").hidden = !useFilePicker; + + for (let profile of profiles) { + let label = document.createElement("label"); + label.className = "toggle-container-with-text"; + + let input = document.createElement("input"); + input.type = "radio"; + input.name = "appProfile"; + input.value = profile.dir.path; + label.append(input); + + let name = document.createElement("p"); + if (profile.name) { + document.l10n.setAttributes(name, "profile-source-named", { + profileName: profile.name, + }); + } else { + document.l10n.setAttributes(name, "profile-source"); + } + label.append(name); + + let profileDetails = document.createElement("dl"); + profileDetails.className = "result-indent tip-caption"; + let profilePathLabel = document.createElement("dt"); + document.l10n.setAttributes(profilePathLabel, "items-pane-directory"); + let profilePath = document.createElement("dd"); + profilePath.textContent = profile.dir.path; + profileDetails.append(profilePathLabel, profilePath); + label.append(profileDetails); + + elProfileList.append(label); + } + document.querySelector("input[name=appProfile]").checked = true; + document.getElementById("profileNextButton").disabled = false; + + this.showPane("profiles"); + } + + /** + * Handler for the Continue button on the profiles pane. + */ + _onSelectProfile() { + let index = [ + ...document.querySelectorAll("input[name=appProfile]"), + ].findIndex(el => el.checked); + if (this._sourceProfiles[index]) { + this._showItems(this._sourceProfiles[index]); + } else { + this._openFilePicker( + index == this._sourceProfiles.length ? "dir" : "zip" + ); + } + } + + /** + * Open a file picker to select a folder or a zip file. + * + * @param {'dir' | 'zip'} type - Whether to pick a folder or a zip file. + */ + async _openFilePicker(type) { + let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance( + Ci.nsIFilePicker + ); + let [filePickerTitleDir, filePickerTitleZip] = + await document.l10n.formatValues([ + "profile-file-picker-directory", + "profile-file-picker-archive-title", + ]); + if (type == "zip") { + filePicker.init(window, filePickerTitleZip, filePicker.modeOpen); + filePicker.appendFilter("", "*.zip"); + } else { + filePicker.init(window, filePickerTitleDir, filePicker.modeGetFolder); + } + let rv = await new Promise(resolve => filePicker.open(resolve)); + if (rv != Ci.nsIFilePicker.returnOK) { + return; + } + let selectedFile = filePicker.file; + if (!selectedFile.isDirectory()) { + if (selectedFile.fileSize > 2147483647) { + // nsIZipReader only supports zip file less than 2GB. + this.showError("error-message-zip-file-too-big2"); + return; + } + this._importingFromZip = true; + } + this._showItems({ dir: selectedFile }); + } + + /** + * Show the items pane, with a list of items to import. + * + * @param {SourceProfile} profile - The profile to import from. + */ + _showItems(profile) { + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + this._showItems(profile); + }, + }, + 1 + ); + this._el.classList.remove("final-step", "progress"); + this._sourceProfile = profile; + document.l10n.setAttributes( + this._el.querySelector("#app-items h1"), + `from-app-${this._sourceAppName}` + ); + document.getElementById("appSourceProfilePath").textContent = + profile.dir.path; + document.getElementById("appSourceProfilePath").textContent = + this._sourceProfile.dir.path; + document.getElementById("appSourceProfileNameWrapper").hidden = + !this._sourceProfile.name; + if (this._sourceProfile.name) { + document.getElementById("appSourceProfileName").textContent = + this._sourceProfile.name; + } + this._setItemsChecked(this._importer.SUPPORTED_ITEMS); + document.getElementById("profileNextButton").disabled = Object.values( + this._importer.SUPPORTED_ITEMS + ).every(isChecked => !isChecked); + + this.showPane("items"); + } + + /** A map from checkbox id to ImportItems field */ + _itemCheckboxes = { + checkAccounts: "accounts", + checkAddressBooks: "addressBooks", + checkCalendars: "calendars", + checkMailMessages: "mailMessages", + }; + + /** + * Map of fluent IDs from ImportItems if they differ. + * + * @type {Object<string>} + */ + _importItemFluentId = { + addressBooks: "address-books", + mailMessages: "mail-messages", + }; + + /** + * Set checkbox states according to an ImportItems object. + * + * @param {ImportItems} items. + */ + _setItemsChecked(items) { + for (let [id, field] of Object.entries(this._itemCheckboxes)) { + let supported = items[field]; + let checkbox = document.getElementById(id); + checkbox.checked = supported; + checkbox.disabled = !supported; + } + } + + /** + * Construct an ImportItems object from the checkbox states. + * + * @param {boolean} [onlySupported=false] - Only return supported ImportItems. + * @returns {ImportItems} + */ + _getItemsChecked(onlySupported = false) { + let items = {}; + for (let id in this._itemCheckboxes) { + let checkbox = document.getElementById(id); + if (!onlySupported || !checkbox.disabled) { + items[this._itemCheckboxes[id]] = checkbox.checked; + } + } + return items; + } + + /** + * Handler for the Continue button on the items pane. + */ + _onSelectItems() { + let checkedItems = this._getItemsChecked(true); + if (Object.values(checkedItems).some(isChecked => isChecked)) { + this._showSummary(); + } + } + + _showSummary() { + Steps.updateSteps({}, 0); + this._el.classList.add("final-step"); + document.l10n.setAttributes( + this._el.querySelector("#app-summary h1"), + `from-app-${this._sourceAppName}` + ); + document.getElementById("appSummaryProfilePath").textContent = + this._sourceProfile.dir.path; + document.getElementById("appSummaryProfileNameWrapper").hidden = + !this._sourceProfile.name; + if (this._sourceProfile.name) { + document.getElementById("appSummaryProfileName").textContent = + this._sourceProfile.name; + } + document.getElementById("appSummaryItems").replaceChildren( + ...Object.entries(this._getItemsChecked(true)) + .filter(([item, checked]) => checked) + .map(([item]) => { + let li = document.createElement("li"); + let fluentId = this._importItemFluentId[item] ?? item; + document.l10n.setAttributes(li, `items-pane-checkbox-${fluentId}`); + return li; + }) + ); + this.showPane("summary"); + } + + /** + * Extract the zip file to a tmp dir, set _sourceProfile.dir to the tmp dir. + */ + async _extractZipFile() { + // Extract the zip file to a tmp dir. + let targetDir = Services.dirsvc.get("TmpD", Ci.nsIFile); + targetDir.append("tmp-profile"); + targetDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + let ZipReader = Components.Constructor( + "@mozilla.org/libjar/zip-reader;1", + "nsIZipReader", + "open" + ); + let zip = ZipReader(this._sourceProfile.dir); + for (let entry of zip.findEntries(null)) { + let parts = entry.split("/"); + if ( + this._importer.IGNORE_DIRS.includes(parts[1]) || + entry.endsWith("/") + ) { + continue; + } + // Folders can not be unzipped recursively, have to iterate and + // extract all file entries one by one. + let target = targetDir.clone(); + for (let part of parts.slice(1)) { + // Drop the root folder name in the zip file. + target.append(part); + } + if (!target.parent.exists()) { + target.parent.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + try { + this._logger.debug(`Extracting ${entry} to ${target.path}`); + zip.extract(entry, target); + this._extractedFileCount++; + if (this._extractedFileCount % 10 == 0) { + let progress = Math.min((this._extractedFileCount / 200) * 0.2, 0.2); + this.updateProgress(progress); + await new Promise(resolve => setTimeout(resolve)); + } + } catch (e) { + this._logger.error(e); + } + } + // Use the tmp dir as source profile dir. + this._sourceProfile = { dir: targetDir }; + this.updateProgress(0.2); + } + + async startImport() { + this.showProgress("progress-pane-importing2"); + if (this._importingFromZip) { + this._extractedFileCount = 0; + try { + await this._extractZipFile(); + } catch (e) { + this.showError("error-message-extract-zip-file-failed2"); + throw e; + } + } + this._importer.onProgress = (current, total) => { + this.updateProgress( + this._importingFromZip ? 0.2 + (0.8 * current) / total : current / total + ); + }; + try { + this.finish( + await this._importer.startImport( + this._sourceProfile.dir, + this._getItemsChecked() + ) + ); + } catch (e) { + this.showError("error-message-failed"); + throw e; + } finally { + if (this._importingFromZip) { + IOUtils.remove(this._sourceProfile.dir.path, { recursive: true }); + } + } + } +} + +/** + * Control the #tabPane-addressBook element, to support importing from an + * address book file. + */ +class AddrBookImporterController extends ImporterController { + constructor() { + super("tabPane-addressBook", "addr-book"); + } + + /** + * Show the next pane. + */ + next() { + super.next(); + switch (this._currentPane) { + case "sources": + this._onSelectSource(); + break; + case "csvFieldMap": + this._onSubmitCsvFieldMap(); + break; + case "directories": + this._onSelectDirectory(); + break; + case "summary": + window.close(); + break; + } + } + + showInitialStep() { + this._showSources(); + } + + /** + * Show the sources pane. + */ + _showSources() { + document.getElementById("addrBookBackButton").hidden = + !Steps.hasStepHistory(); + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + this._showSources(); + }, + }, + 2 + ); + this.showPane("sources"); + } + + /** + * Handler for the Continue button on the sources pane. + */ + async _onSelectSource() { + this._fileType = document.querySelector( + "input[name=addrBookSource]:checked" + ).value; + this._importer = new AddrBookFileImporter(this._fileType); + + let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance( + Ci.nsIFilePicker + ); + let [filePickerTitle] = await document.l10n.formatValues([ + "addr-book-file-picker", + ]); + filePicker.init(window, filePickerTitle, filePicker.modeOpen); + let filter = { + csv: "*.csv; *.tsv; *.tab", + ldif: "*.ldif", + vcard: "*.vcf", + sqlite: "*.sqlite", + mab: "*.mab", + }[this._fileType]; + if (filter) { + filePicker.appendFilter("", filter); + } + filePicker.appendFilters(Ci.nsIFilePicker.filterAll); + let rv = await new Promise(resolve => filePicker.open(resolve)); + if (rv != Ci.nsIFilePicker.returnOK) { + return; + } + + this._sourceFile = filePicker.file; + document.getElementById("addrBookSourcePath").textContent = + filePicker.file.path; + + if (this._fileType == "csv") { + let unmatchedRows = await this._importer.parseCsvFile(filePicker.file); + if (unmatchedRows.length) { + document.getElementById("csvFieldMap").data = unmatchedRows; + this._showCsvFieldMap(); + return; + } + } + this._showDirectories(); + } + + /** + * Show the csvFieldMap pane, user can map source CSV fields to address book + * fields. + */ + _showCsvFieldMap() { + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + this._showCsvFieldMap(); + }, + }, + 2 + ); + document.getElementById("addrBookBackButton").hidden = false; + this.showPane("csvFieldMap"); + } + + /** + * Handler for the Continue button on the csvFieldMap pane. + */ + async _onSubmitCsvFieldMap() { + this._importer.setCsvFields(document.getElementById("csvFieldMap").value); + this._showDirectories(); + } + + /** + * Show the directories pane, with a list of existing directories and an + * option to create a new directory. + */ + async _showDirectories() { + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + this._showDirectories(); + }, + }, + 1 + ); + document.getElementById("addrBookBackButton").hidden = false; + this._el.classList.remove("final-step", "progress"); + let sourceFileName = this._sourceFile.leafName; + this._fallbackABName = sourceFileName.slice( + 0, + sourceFileName.lastIndexOf(".") == -1 + ? Infinity + : sourceFileName.lastIndexOf(".") + ); + document.l10n.setAttributes( + document.getElementById("newDirectoryLabel"), + "addr-book-import-into-new-directory2", + { + addressBookName: this._fallbackABName, + } + ); + let elList = document.getElementById("directoryList"); + elList.innerHTML = ""; + this._directories = MailServices.ab.directories.filter( + dir => dir.dirType == Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + for (let directory of this._directories) { + let label = document.createElement("label"); + label.className = "toggle-container-with-text"; + + let input = document.createElement("input"); + input.type = "radio"; + input.name = "addrBookDirectory"; + input.value = directory.dirPrefId; + label.append(input); + + let name = document.createElement("div"); + name.className = "strong"; + name.textContent = directory.dirName; + label.append(name); + + elList.append(label); + } + document.querySelector("input[name=addrBookDirectory]").checked = true; + + this.showPane("directories"); + } + + /** + * Handler for the Continue button on the directories pane. + */ + _onSelectDirectory() { + let index = [ + ...document.querySelectorAll("input[name=addrBookDirectory]"), + ].findIndex(el => el.checked); + this._selectedAddressBook = this._directories[index]; + this._showSummary(); + } + + _showSummary() { + Steps.updateSteps({}, 0); + this._el.classList.add("final-step"); + document.getElementById("addrBookSummaryPath").textContent = + this._sourceFile.path; + let targetAddressBook = this._selectedAddressBook?.dirName; + let newAddressBook = false; + if (!targetAddressBook) { + targetAddressBook = this._fallbackABName; + newAddressBook = true; + } + let description = this._el.querySelector("#addr-book-summary .description"); + description.hidden = !newAddressBook; + if (newAddressBook) { + document.l10n.setAttributes( + description, + "addr-book-summary-description", + { + addressBookName: targetAddressBook, + } + ); + } + document.l10n.setAttributes( + document.getElementById("addrBookSummarySubtitle"), + "addr-book-summary-title", + { + addressBookName: targetAddressBook, + } + ); + this.showPane("summary"); + } + + async startImport() { + let targetDirectory = this._selectedAddressBook; + if (!targetDirectory) { + // User selected to create a new address book and import into it. Create + // one based on the file name. + let dirId = MailServices.ab.newAddressBook( + this._fallbackABName, + "", + Ci.nsIAbManager.JS_DIRECTORY_TYPE + ); + targetDirectory = MailServices.ab.getDirectoryFromId(dirId); + } + + this.showProgress("progress-pane-importing2"); + this._importer.onProgress = (current, total) => { + this.updateProgress(current / total); + }; + try { + this.finish( + await this._importer.startImport(this._sourceFile, targetDirectory) + ); + } catch (e) { + this.showError("error-message-failed"); + throw e; + } + } +} + +/** + * Control the #tabPane-calendar element, to support importing from a calendar + * file. + */ +class CalendarImporterController extends ImporterController { + constructor() { + super("tabPane-calendar", "calendar"); + } + + next() { + super.next(); + switch (this._currentPane) { + case "sources": + this._onSelectSource(); + break; + case "items": + this._onSelectItems(); + break; + case "calendars": + this._onSelectCalendar(); + break; + case "summary": + window.close(); + break; + } + } + + showInitialStep() { + this._showSources(); + } + + /** + * When filter changes, re-render the item list. + * + * @param {HTMLInputElement} filterInput - The filter input. + */ + onFilterChange(filterInput) { + let term = filterInput.value.toLowerCase(); + this._filteredItems = []; + for (let item of this._items) { + let element = this._itemElements[item.id]; + if (item.title.toLowerCase().includes(term)) { + element.hidden = false; + this._filteredItems.push(item); + } else { + element.hidden = true; + } + } + } + + /** + * Select or deselect all visible items. + * + * @param {boolean} selected - Select all if true, otherwise deselect all. + */ + selectAllItems(selected) { + for (let item of this._filteredItems) { + let element = this._itemElements[item.id]; + element.querySelector("input").checked = selected; + if (selected) { + this._selectedItems.add(item); + } else { + this._selectedItems.delete(item); + } + } + document.getElementById("calendarNextButton").disabled = + this._selectedItems.size == 0; + } + + /** + * Show the sources pane. + */ + _showSources() { + document.getElementById("calendarBackButton").hidden = + !Steps.hasStepHistory(); + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + this._showSources(); + }, + }, + 3 + ); + this.showPane("sources"); + } + + /** + * Handler for the Continue button on the sources pane. + */ + async _onSelectSource() { + let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance( + Ci.nsIFilePicker + ); + filePicker.appendFilter("", "*.ics"); + filePicker.appendFilters(Ci.nsIFilePicker.filterAll); + filePicker.init( + window, + await document.l10n.formatValue("file-calendar-description"), + filePicker.modeOpen + ); + let rv = await new Promise(resolve => filePicker.open(resolve)); + if (rv != Ci.nsIFilePicker.returnOK) { + return; + } + + this._sourceFile = filePicker.file; + this._importer = new CalendarFileImporter(); + + document.getElementById("calendarSourcePath").textContent = + filePicker.file.path; + + this._showItems(); + } + + /** + * Show the sources pane. + */ + async _showItems() { + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + this._showItems(); + }, + }, + 2 + ); + document.getElementById("calendarBackButton").hidden = false; + let elItemList = document.getElementById("calendar-item-list"); + document.getElementById("calendarItemsTools").hidden = true; + document.l10n.setAttributes(elItemList, "calendar-items-loading"); + this.showPane("items"); + + // Give the UI a chance to render. + await new Promise(resolve => setTimeout(resolve, 100)); + + try { + this._items = await this._importer.parseIcsFile(this._sourceFile); + } catch (e) { + this.showError("error-failed-to-parse-ics-file"); + throw e; + } + + document.getElementById("calendarItemsTools").hidden = + this._items.length < 2; + elItemList.innerHTML = ""; + this._filteredItems = this._items; + this._selectedItems = new Set(this._items); + this._itemElements = {}; + + for (let item of this._items) { + let wrapper = document.createElement("div"); + wrapper.className = "calendar-item-wrapper"; + elItemList.appendChild(wrapper); + this._itemElements[item.id] = wrapper; + + let summary = document.createXULElement("calendar-item-summary"); + wrapper.appendChild(summary); + summary.item = item; + summary.updateItemDetails(); + + let input = document.createElement("input"); + input.type = "checkbox"; + input.checked = true; + wrapper.appendChild(input); + + wrapper.addEventListener("click", e => { + if (e.target != input) { + input.checked = !input.checked; + } + if (input.checked) { + this._selectedItems.add(item); + } else { + this._selectedItems.delete(item); + } + document.getElementById("calendarNextButton").disabled = + this._selectedItems.size == 0; + }); + } + } + + /** + * Handler for the Continue button on the items pane. + */ + _onSelectItems() { + this._showCalendars(); + } + + /** + * Show the calendars pane, with a list of existing writable calendars and an + * option to create a new calendar. + */ + _showCalendars() { + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + this._showCalendars(); + }, + }, + 1 + ); + this._el.classList.remove("final-step", "progress"); + document.getElementById("calendarCalPath").textContent = + this._sourceFile.path; + let elList = document.getElementById("calendarList"); + elList.innerHTML = ""; + + let sourceFileName = this._sourceFile.leafName; + this._fallbackCalendarName = sourceFileName.slice( + 0, + sourceFileName.lastIndexOf(".") == -1 + ? Infinity + : sourceFileName.lastIndexOf(".") + ); + + document.l10n.setAttributes( + document.getElementById("newCalendarLabel"), + "calendar-import-into-new-calendar2", + { + targetCalendar: this._fallbackCalendarName, + } + ); + + this._calendars = this._importer.getTargetCalendars(); + for (let calendar of this._calendars) { + let label = document.createElement("label"); + label.className = "toggle-container-with-text"; + + let input = document.createElement("input"); + input.type = "radio"; + input.name = "targetCalendar"; + input.value = calendar.id; + label.append(input); + + let name = document.createElement("div"); + name.className = "strong"; + name.textContent = calendar.name; + label.append(name); + + elList.append(label); + } + document.querySelector("input[name=targetCalendar]").checked = true; + + this.showPane("calendars"); + } + + _onSelectCalendar() { + let index = [ + ...document.querySelectorAll("input[name=targetCalendar]"), + ].findIndex(el => el.checked); + this._selectedCalendar = this._calendars[index]; + this._showSummary(); + } + + _showSummary() { + Steps.updateSteps({}, 0); + this._el.classList.add("final-step"); + document.getElementById("calendarSummaryPath").textContent = + this._sourceFile.path; + let targetCalendar = this._selectedCalendar?.name; + let newCalendar = false; + if (!targetCalendar) { + targetCalendar = this._fallbackCalendarName; + newCalendar = true; + } + let description = this._el.querySelector("#calendar-summary .description"); + description.hidden = !newCalendar; + if (newCalendar) { + document.l10n.setAttributes(description, "calendar-summary-description", { + targetCalendar, + }); + } + document.l10n.setAttributes( + document.getElementById("calendarSummarySubtitle"), + "calendar-summary-title", + { + itemCount: this._selectedItems.size, + targetCalendar, + } + ); + this.showPane("summary"); + } + + /** + * Handler for the Continue button on the calendars pane. + */ + async startImport() { + let targetCalendar = this._selectedCalendar; + if (!targetCalendar) { + // Create a new calendar. + targetCalendar = cal.manager.createCalendar( + "storage", + Services.io.newURI("moz-storage-calendar://") + ); + targetCalendar.name = this._fallbackCalendarName; + cal.manager.registerCalendar(targetCalendar); + } + this.showProgress("progress-pane-importing2"); + this._importer.onProgress = (current, total) => { + this.updateProgress(current / total); + }; + try { + await this._importer.startImport( + [...this._selectedItems], + targetCalendar + ); + this.finish(); + } catch (e) { + this.showError("error-message-failed"); + throw e; + } + } +} + +/** + * Control the #tabPane-export element, to support exporting the current profile + * to a zip file. + */ +class ExportController extends ImporterController { + constructor() { + super("tabPane-export", ""); + } + + back() { + window.close(); + } + + async next() { + super.next(); + let [filePickerTitle, brandName] = await document.l10n.formatValues([ + "export-file-picker2", + "export-brand-name", + ]); + let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance( + Ci.nsIFilePicker + ); + filePicker.init(window, filePickerTitle, Ci.nsIFilePicker.modeSave); + filePicker.defaultString = `${brandName}_profile_backup.zip`; + filePicker.defaultExtension = "zip"; + filePicker.appendFilter("", "*.zip"); + let rv = await new Promise(resolve => filePicker.open(resolve)); + if ( + ![Ci.nsIFilePicker.returnOK, Ci.nsIFilePicker.returnReplace].includes(rv) + ) { + return; + } + + let exporter = new ProfileExporter(); + this.showProgress("progress-pane-exporting2"); + exporter.onProgress = (current, total) => { + this.updateProgress(current / total); + }; + try { + await exporter.startExport(filePicker.file); + this.finish(); + } catch (e) { + this.showError("error-export-failed"); + throw e; + } + } + + openProfileFolder() { + Services.dirsvc.get("ProfD", Ci.nsIFile).reveal(); + } +} + +class StartController extends ImporterController { + constructor() { + super("tabPane-start", "start"); + } + + next() { + super.next(); + switch (this._currentPane) { + case "sources": + this._onSelectSource(); + break; + case "file": + this._onSelectFile(); + break; + } + } + + showInitialStep() { + this._showSources(); + } + + /** + * Show the sources pane. + */ + _showSources() { + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + showTab("tab-start"); + //showTab will always call showInitialStep + }, + }, + 3 + ); + document.getElementById("startBackButton").hidden = true; + this.showPane("sources"); + } + + /** + * Handler for the Continue button on the sources pane. + */ + async _onSelectSource() { + let checkedInput = document.querySelector("input[name=appSource]:checked"); + + switch (checkedInput.value) { + case "file": + this._showFile(); + break; + default: + await profileController._onSelectSource(checkedInput.value); + showTab("tab-app"); + // Don't change back button state, since we switch to app flow. + return; + } + + document.getElementById("startBackButton").hidden = false; + } + + _showFile() { + Steps.updateSteps( + { + returnTo: () => { + this.reset(); + showTab("tab-start"); + this._showFile(); + }, + }, + 3 + ); + this.showPane("file"); + } + + async _onSelectFile() { + let checkedInput = document.querySelector("input[name=startFile]:checked"); + switch (checkedInput.value) { + case "profile": + // Go to the import profile from zip file step in profile flow for TB. + profileController.reset(); + await profileController._onSelectSource("Thunderbird"); + document.getElementById("appFilePickerZip").checked = true; + await profileController._onSelectProfile(); + showTab("tab-app"); + break; + case "calendar": + calendarController.reset(); + showTab("tab-calendar"); + calendarController.showInitialStep(); + await calendarController._onSelectSource(); + break; + case "addressbook": + addrBookController.reset(); + showTab("tab-addressBook"); + addrBookController.showInitialStep(); + break; + } + } +} + +/** + * Show a specific importing tab. + * + * @param {"tab-app"|"tab-addressBook"|"tab-calendar"|"tab-export"|"tab-start"} tabId - + * Tab to show. + * @param {boolean} [reset=false] - If the state should be reset as if this was + * the initial tab shown. + */ +function showTab(tabId, reset = false) { + if (reset) { + Steps.reset(); + restart(); + } + let selectedPaneId = `tabPane-${tabId.split("-")[1]}`; + let isExport = tabId === "tab-export"; + document.getElementById("importDocs").hidden = isExport; + document.getElementById("exportDocs").hidden = !isExport; + Steps.toggle(!isExport); + document.l10n.setAttributes( + document.querySelector("title"), + isExport ? "export-page-title" : "import-page-title" + ); + document.querySelector("link[rel=icon]").href = isExport + ? "chrome://messenger/skin/icons/new/compact/export.svg" + : "chrome://messenger/skin/icons/new/compact/import.svg"; + location.hash = isExport ? "export" : ""; + for (let tabPane of document.querySelectorAll("[id^=tabPane-]")) { + tabPane.hidden = tabPane.id != selectedPaneId; + } + for (let el of document.querySelectorAll("[id^=tab-]")) { + el.classList.toggle("is-selected", el.id == tabId); + } + if (!Steps.hasStepHistory()) { + switch (tabId) { + case "tab-start": + startController.showInitialStep(); + break; + case "tab-addressBook": + addrBookController.showInitialStep(); + break; + case "tab-calendar": + calendarController.showInitialStep(); + break; + default: + } + } +} + +/** + * Restart the import wizard. Resets all previous choices. + */ +function restart() { + startController.reset(); + profileController.reset(); + addrBookController.reset(); + calendarController.reset(); + Steps.backTo(0); +} + +let profileController; +let addrBookController; +let calendarController; +let exportController; +let startController; + +document.addEventListener("DOMContentLoaded", () => { + profileController = new ProfileImporterController(); + addrBookController = new AddrBookImporterController(); + calendarController = new CalendarImporterController(); + exportController = new ExportController(); + startController = new StartController(); + showTab(location.hash === "#export" ? "tab-export" : "tab-start", true); +}); diff --git a/comm/mailnews/import/content/aboutImport.xhtml b/comm/mailnews/import/content/aboutImport.xhtml new file mode 100644 index 0000000000..e4a259d1f5 --- /dev/null +++ b/comm/mailnews/import/content/aboutImport.xhtml @@ -0,0 +1,477 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<head> + <title data-l10n-id="import-page-title"></title> + + <link rel="stylesheet" href="chrome://messenger/skin/messenger.css"/> + <link rel="stylesheet" href="chrome://global/skin/global.css"/> + <link rel="stylesheet" href="chrome://calendar/skin/shared/calendar-attendees.css"/> + <link rel="stylesheet" href="chrome://calendar/skin/shared/calendar-item-summary.css"/> + <link rel="stylesheet" href="chrome://messenger/skin/accountSetup.css"/> + <link rel="stylesheet" href="chrome://messenger/skin/aboutImport.css"/> + + <link rel="localization" href="branding/brand.ftl" /> + <link rel="localization" href="messenger/aboutImport.ftl"/> + + <link rel="icon" href="chrome://messenger/skin/icons/new/compact/import.svg" sizes="any"/> + + <script defer="" src="chrome://messenger/content/aboutImport.js"></script> + <script defer="" src="chrome://messenger/content/csv-field-map.js"></script> + <script defer="" src="chrome://calendar/content/widgets/calendar-item-summary.js"></script> +</head> +<body> + <main id="main"> + <nav> + <ol id="stepNav" data-l10n-id="step-list"> + <li id="navConfirm"> + <button data-l10n-id="step-confirm" disabled="disabled"></button> + </li> + </ol> + </nav> + <section id="errorNotifications"></section> + <div id="tabPane-start" class="tabPane"> + <section id="start-sources"> + <h1 id="startSource" data-l10n-id="import-start"></h1> + <h2 data-l10n-id="import-start-title"></h2> + <p data-l10n-id="import-start-description" class="description"></p> + <div class="source-list indent"> + <label class="toggle-container-with-text"> + <input type="radio" + value="Thunderbird" + name="appSource" + checked="checked"/> + <p data-l10n-id="source-thunderbird"></p> + <p class="tip-caption" data-l10n-id="source-thunderbird-description"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="Seamonkey" + name="appSource"/> + <p data-l10n-id="source-seamonkey"></p> + <p class="tip-caption" data-l10n-id="source-seamonkey-description"></p> + </label> +#ifdef XP_WIN + <label class="toggle-container-with-text"> + <input type="radio" + value="Outlook" + name="appSource"/> + <p data-l10n-id="source-outlook"></p> + <p class="tip-caption" data-l10n-id="source-outlook-description"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="Becky" + name="appSource"/> + <p data-l10n-id="source-becky"></p> + <p class="tip-caption" data-l10n-id="source-becky-description"></p> + </label> +#endif +#ifdef XP_MACOSX + <label class="toggle-container-with-text"> + <input type="radio" + value="AppleMail" + name="appSource"/> + <p data-l10n-id="source-apple-mail"></p> + <p class="tip-caption" data-l10n-id="source-apple-mail-description"></p> + </label> +#endif + <label class="toggle-container-with-text"> + <input type="radio" + value="file" + name="appSource"/> + <p data-l10n-id="source-file2"></p> + <p class="tip-caption" data-l10n-id="source-file-description"></p> + </label> + </div> + </section> + <section id="start-file"> + <h1 id="startFile" data-l10n-id="import-file"></h1> + <h2 data-l10n-id="import-file-title"></h2> + <p data-l10n-id="import-file-description" class="description"></p> + <div class="option-list indent"> + <label class="toggle-container-with-text"> + <input type="radio" + value="profile" + name="startFile" + checked="checked"/> + <p data-l10n-id="file-profile2"></p> + <p data-l10n-id="file-profile-description" class="tip-caption"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="calendar" + name="startFile"/> + <p data-l10n-id="file-calendar"></p> + <p data-l10n-id="file-calendar-description" class="tip-caption"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="addressbook" + name="startFile"/> + <p data-l10n-id="file-addressbook"></p> + <p data-l10n-id="file-addressbook-description" class="tip-caption"></p> + </label> + </div> + </section> + <footer class="buttons-container"> + <button id="startBackButton" + class="back" + onclick="startController.back()" + data-l10n-id="button-back"></button> + <button class="primary continue" + onclick="startController.next()" + data-l10n-id="button-continue"></button> + </footer> + </div> + <div id="tabPane-app" class="tabPane restart-required"> + <section id="app-profiles"> + <h1 id="profilesPaneTitle" data-l10n-id="import-from-app"></h1> + <h2 id="profilesPaneSubtitle"></h2> + <div class="profile-list indent" id="profileList"></div> + <div class="profile-list indent" id="filePickerList"> + <label class="toggle-container-with-text"> + <input type="radio" + value="file-picker-dir" + name="appProfile"/> + <p data-l10n-id="profile-file-picker-directory"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="file-picker-zip" + name="appProfile" + id="appFilePickerZip"/> + <p data-l10n-id="profile-file-picker-archive"></p> + <p data-l10n-id="profile-file-picker-archive-description" class="tip-caption"></p> + </label> + </div> + </section> + <section id="app-items"> + <h1 data-l10n-id="import-from-app"></h1> + <dl> + <div id="appSourceProfileNameWrapper"> + <dt data-l10n-id="items-pane-profile-name"></dt> + <dd id="appSourceProfileName"></dd> + </div> + <dt data-l10n-id="items-pane-directory"></dt> + <dd id="appSourceProfilePath"></dd> + </dl> + <h2 data-l10n-id="items-pane-title2" class="light-heading"></h2> + <p> + <img src="chrome://messenger/skin/icons/new/compact/info.svg" + class="info icon" + alt=""/> + <span data-l10n-id="items-pane-override"></span> + </p> + <div class="option-list indent" id="appItemsList"> + <label class="toggle-container-with-text"> + <input type="checkbox" id="checkAccounts"/> + <span data-l10n-id="items-pane-checkbox-accounts"></span> + </label> + <label class="toggle-container-with-text"> + <input type="checkbox" id="checkAddressBooks"/> + <span data-l10n-id="items-pane-checkbox-address-books"></span> + </label> + <label class="toggle-container-with-text"> + <input type="checkbox" id="checkCalendars"/> + <span data-l10n-id="items-pane-checkbox-calendars"></span> + </label> + <label class="toggle-container-with-text"> + <input type="checkbox" id="checkMailMessages"/> + <span data-l10n-id="items-pane-checkbox-mail-messages"></span> + </label> + </div> + <p class="center"> + <img src="chrome://messenger/skin/icons/new/compact/warning.svg" + class="icon warn" + alt=""/> + <span data-l10n-id="summary-pane-warning"></span> + </p> + </section> + <section id="app-summary"> + <h1 data-l10n-id="import-from-app"></h1> + <dl> + <div id="appSummaryProfileNameWrapper"> + <dt data-l10n-id="items-pane-profile-name"></dt> + <dd id="appSummaryProfileName"></dd> + </div> + <dt data-l10n-id="items-pane-directory"></dt> + <dd id="appSummaryProfilePath"></dd> + </dl> + <h2 data-l10n-id="summary-pane-title" class="light-heading"></h2> + <ul id="appSummaryItems" class="summary-items indent"> + </ul> + <button id="appStartImport" + onclick="profileController.startImport()" + class="primary before-progress center-button" + data-l10n-id="summary-pane-start"></button> + <div class="progressPane"> + <section class="progressPane-progress"> + <progress class="progressPaneProgressBar"></progress> + <p class="progressPaneDesc tip-caption"></p> + </section> + </div> + <button class="progressFinish primary center-button" + onclick="profileController.next()" + data-l10n-id="button-finish"></button> + <button data-l10n-id="summary-pane-start-over" + class="progressFinish no-restart center-button btn-link" + onclick="restart()"></button> + <p class="restart-only center"> + <img src="chrome://messenger/skin/icons/new/compact/warning.svg" + class="icon warn" + alt=""/> + <span data-l10n-id="summary-pane-warning"></span> + </p> + </section> + <footer class="buttons-container"> + <button id="profileBackButton" + class="back" + onclick="profileController.back()" + data-l10n-id="button-back"></button> + <button id="profileNextButton" + class="primary next-button" + onclick="profileController.next()" + data-l10n-id="button-continue"></button> + </footer> + </div> + + <div id="tabPane-addressBook" class="tabPane"> + <section id="addr-book-sources"> + <h1 id="importAddressBook" data-l10n-id="import-address-book-title"></h1> + <h2 data-l10n-id="import-file-title"></h2> + <p data-l10n-id="import-from-addr-book-file-description" class="description"></p> + <div class="source-list indent"> + <label class="toggle-container-with-text"> + <input type="radio" + value="csv" + name="addrBookSource" + checked=""/> + <p data-l10n-id="addr-book-csv-file"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="ldif" + name="addrBookSource"/> + <p data-l10n-id="addr-book-ldif-file"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="vcard" + name="addrBookSource"/> + <p data-l10n-id="addr-book-vcard-file"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="sqlite" + name="addrBookSource"/> + <p data-l10n-id="addr-book-sqlite-file"></p> + </label> + <label class="toggle-container-with-text"> + <input type="radio" + value="mab" + name="addrBookSource"/> + <p data-l10n-id="addr-book-mab-file"></p> + </label> + </div> + </section> + <section id="addr-book-csvFieldMap"> + <h1 data-l10n-id="import-address-book-title"></h1> + <h2 data-l10n-id="addr-book-csv-field-map-title"></h2> + <p data-l10n-id="addr-book-csv-field-map-desc" class="description"></p> + <csv-field-map id="csvFieldMap"/> + </section> + <section id="addr-book-directories"> + <h1 data-l10n-id="import-address-book-title"></h1> + <h2 data-l10n-id="addr-book-directories-title"></h2> + <dl> + <dt data-l10n-id="addr-book-directories-pane-source"></dt> + <dd id="addrBookSourcePath"></dd> + </dl> + <div class="profile-list indent" id="directoryList"></div> + <label class="toggle-container-with-text indent"> + <input type="radio" + value=".new" + name="addrBookDirectory"/> + <p id="newDirectoryLabel"></p> + </label> + </section> + <section id="addr-book-summary"> + <h1 data-l10n-id="import-address-book-title"></h1> + <h2 id="addrBookSummarySubtitle"></h2> + <p class="description"></p> + <dl> + <dt data-l10n-id="addr-book-directories-pane-source"></dt> + <dd id="addrBookSummaryPath"></dd> + </dl> + <h2 data-l10n-id="summary-pane-title" class="light-heading"></h2> + <ul class="summary-items indent"> + <li data-l10n-id="items-pane-checkbox-address-books"></li> + </ul> + <button id="addrBookStartImport" + onclick="addrBookController.startImport()" + class="primary before-progress center-button" + data-l10n-id="summary-pane-start"></button> + <div class="progressPane"> + <section class="progressPane-progress"> + <progress class="progressPaneProgressBar"></progress> + <p class="progressPaneDesc tip-caption"></p> + </section> + </div> + <button class="progressFinish primary center-button" + onclick="addrBookController.next()" + data-l10n-id="button-finish"></button> + <button data-l10n-id="summary-pane-start-over" + class="progressFinish no-restart center-button btn-link" + onclick="restart()"></button> + <p class="restart-only center"> + <img src="chrome://messenger/skin/icons/new/compact/warning.svg" + class="icon warn" + alt=""/> + <span data-l10n-id="summary-pane-warning"></span> + </p> + </section> + <footer class="buttons-container"> + <button id="addrBookBackButton" + class="back" + onclick="addrBookController.back()" + data-l10n-id="button-back"></button> + <button class="primary next-button continue" + id="addrBookNextButton" + onclick="addrBookController.next()" + data-l10n-id="button-continue"></button> + </footer> + </div> + + <div id="tabPane-calendar" class="tabPane"> + <section id="calendar-sources"> + <h1 data-l10n-id="import-calendar-title"></h1> + <h2 data-l10n-id="import-from-calendar-file-desc"></h2> + </section> + <section id="calendar-items"> + <h1 data-l10n-id="import-calendar-title"></h1> + <dl> + <dt data-l10n-id="addr-book-directories-pane-source"></dt> + <dd id="calendarSourcePath"></dd> + </dl> + <h2 data-l10n-id="calendar-items-title"></h2> + <div id="calendarItemsTools"> + <input type="search" + data-l10n-id="calendar-items-filter-input" + oninput="calendarController.onFilterChange(this)"/> + <button data-l10n-id="calendar-deselect-all-items" + onclick="calendarController.selectAllItems(false)"></button> + <button data-l10n-id="calendar-select-all-items" + onclick="calendarController.selectAllItems(true)"></button> + </div> + <div id="calendar-item-list"></div> + </section> + <section id="calendar-calendars"> + <h1 data-l10n-id="import-calendar-title"></h1> + <dl> + <dt data-l10n-id="addr-book-directories-pane-source"></dt> + <dd id="calendarCalPath"></dd> + </dl> + <h2 data-l10n-id="calendar-target-title"></h2> + <div class="profile-list indent" id="calendarList"></div> + <label class="toggle-container-with-text indent"> + <input type="radio" + value=".new" + name="targetCalendar"/> + <p id="newCalendarLabel"></p> + </label> + </section> + <section id="calendar-summary"> + <h1 data-l10n-id="import-calendar-title"></h1> + <h2 id="calendarSummarySubtitle"></h2> + <p class="description"></p> + <dl> + <dt data-l10n-id="addr-book-directories-pane-source"></dt> + <dd id="calendarSummaryPath"></dd> + </dl> + <h2 data-l10n-id="summary-pane-title" class="light-heading"></h2> + <ul class="summary-items indent"> + <li data-l10n-id="items-pane-checkbox-calendars"></li> + </ul> + <button id="calendarStartImport" + onclick="calendarController.startImport()" + class="primary before-progress center-button" + data-l10n-id="summary-pane-start"></button> + <div class="progressPane"> + <section class="progressPane-progress"> + <progress class="progressPaneProgressBar"></progress> + <p class="progressPaneDesc tip-caption"></p> + </section> + </div> + <button class="progressFinish primary center-button" + onclick="calendarController.next()" + data-l10n-id="button-finish"></button> + <button data-l10n-id="summary-pane-start-over" + class="progressFinish no-restart center-button btn-link" + onclick="restart()"></button> + <p class="restart-only center"> + <img src="chrome://messenger/skin/icons/new/compact/warning.svg" + class="icon warn" + alt=""/> + <span data-l10n-id="summary-pane-warning"></span> + </p> + </section> + <footer class="buttons-container"> + <button id="calendarBackButton" + class="back" + onclick="calendarController.back()" + data-l10n-id="button-back"></button> + <button class="primary next-button continue" + id="calendarNextButton" + onclick="calendarController.next()" + data-l10n-id="button-continue"></button> + </footer> + </div> + + <div id="tabPane-export" class="tabPane"> + <section> + <h1 data-l10n-id="export-profile"></h1> + <h2 data-l10n-id="export-profile-title"></h2> + <p class="description"> + <img src="chrome://messenger/skin/icons/new/compact/info.svg" + class="info icon" + alt=""/> + <span data-l10n-id="export-profile-description"></span> + <button data-l10n-id="export-open-profile-folder" + onclick="exportController.openProfileFolder()" + class="btn-link"></button> + </p> + <button id="exportButton" + onclick="exportController.next()" + class="primary before-progress center-button" + data-l10n-id="button-export"></button> + <div class="progressPane"> + <section class="progressPane-progress"> + <progress class="progressPaneProgressBar"></progress> + <p class="progressPaneDesc tip-caption"></p> + </section> + </div> + <button class="progressFinish primary center-button" + onclick="exportController.back()" + data-l10n-id="button-finish"></button> + </section> + </div> + + <footer id="importFooter" class="tip-caption"> + <p data-l10n-id="footer-help"></p> + <a id="importDocs" + data-l10n-id="footer-import-documentation" + href="https://support.mozilla.org/kb/thunderbird-import"></a> + <a id="exportDocs" + data-l10n-id="footer-export-documentation" + href="https://support.mozilla.org/kb/thunderbird-export"></a> + - + <a data-l10n-id="footer-support-forum" + href="https://support.mozilla.org/products/thunderbird"></a> + </footer> + </main> +</body> +</html> diff --git a/comm/mailnews/import/content/csv-field-map.js b/comm/mailnews/import/content/csv-field-map.js new file mode 100644 index 0000000000..544a9cf50c --- /dev/null +++ b/comm/mailnews/import/content/csv-field-map.js @@ -0,0 +1,280 @@ +/* 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/. */ + +var { exportAttributes } = ChromeUtils.import( + "resource:///modules/AddrBookUtils.jsm" +); + +/** + * A component to config the mapping between CSV fields and address book fields. + * For each CSV field, there is a <select> with address book fields as options. + * If an address book field is selected for one CSV field, it can't be used for + * another CSV field. + */ +class CsvFieldMap extends HTMLElement { + /** Render the first two rows from the source CSV data. */ + DATA_ROWS_LIMIT = 2; + + /** @type {string[]} - The indexes of target address book fields. */ + get value() { + return [...this._elTbody.querySelectorAll("select")].map( + select => select.value + ); + } + + /** @type {string[][]} - An array of rows, each row is an array of columns. */ + set data(rows) { + this._init(); + this._rows = rows.slice(0, this.DATA_ROWS_LIMIT); + this._render(); + } + + /** + * Init internal states. + */ + _init() { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/importMsgs.properties" + ); + this._supportedFields = []; + for (let [, stringId] of exportAttributes) { + if (stringId) { + this._supportedFields.push(bundle.GetStringFromID(stringId)); + } + } + // Create an index array ["0", "1", "2", ..., "<length -1>"]. + this._allFieldIndexes = Array.from({ + length: this._supportedFields.length, + }).map((_, index) => index.toString()); + } + + /** + * Init <option> list for all <select> elements. + */ + _initSelectOptions() { + let fields; + let fieldIndexes = Services.prefs.getCharPref("mail.import.csv.fields", ""); + if (fieldIndexes) { + // If the user has done CSV importing before, show the same field mapping. + fieldIndexes = fieldIndexes.split(","); + fields = fieldIndexes.map(i => (i == "" ? i : this._supportedFields[+i])); + } else { + // Show the same field orders as in an exported CSV file. + fields = this._supportedFields; + fieldIndexes = this._allFieldIndexes; + } + + let i = 0; + for (let select of this.querySelectorAll("select")) { + if (fields[i]) { + let option = document.createElement("option"); + option.value = fieldIndexes[i]; + option.textContent = fields[i]; + select.add(option); + } else { + select.disabled = true; + select + .closest("tr") + .querySelector("input[type=checkbox]").checked = false; + } + i++; + } + + this._updateSelectOptions(); + } + + /** + * When a <select> is disabled, we remove all its options. This function is to + * add all available options back. + * + * @param {HTMLSelectElement} select - The <select> element. + */ + _enableSelect(select) { + let selects = [...this._elTbody.querySelectorAll("select")]; + let selectedFieldIndexes = selects.map(select => select.value); + let availableFieldIndexes = this._allFieldIndexes.filter( + index => !selectedFieldIndexes.includes(index) + ); + for (let i = 0; i < availableFieldIndexes.length; i++) { + let option = document.createElement("option"); + option.value = availableFieldIndexes[i]; + option.textContent = this._supportedFields[option.value]; + select.add(option); + } + } + + /** + * Update the options of all <select> elements. The result is if an option is + * selected by a <select>, this option should no longer be shown as an option + * for other <select>. + * + * @param {HTMLSelectElement} [changedSelect] - This param is present only + * when an option is selected, we don't need to update the options of this + * <select> element. + */ + _updateSelectOptions(changedSelect) { + let selects = [...this._elTbody.querySelectorAll("select")]; + let selectedFieldIndexes = selects.map(select => select.value); + let availableFieldIndexes = this._allFieldIndexes.filter( + index => !selectedFieldIndexes.includes(index) + ); + + for (let select of selects) { + if (select.disabled || select == changedSelect) { + continue; + } + for (let i = select.options.length - 1; i >= 0; i--) { + // Remove unselected options first. + if (i != select.selectedIndex) { + select.remove(i); + } + } + for (let i = 0; i < availableFieldIndexes.length; i++) { + // Add all available options. + let option = document.createElement("option"); + option.value = availableFieldIndexes[i]; + option.textContent = this._supportedFields[option.value]; + select.add(option); + } + } + } + + /** + * Handle the change event of <select> and <input type="checkbox">. + */ + _bindEvents() { + this._elTbody.addEventListener("change", e => { + let el = e.target; + if (el.tagName == "select") { + this._updateSelectOptions(el); + } else if (el.tagName == "input" && el.type == "checkbox") { + let select = el.closest("tr").querySelector("select"); + select.disabled = !el.checked; + if (select.disabled) { + // Because it's disabled, remove all the options. + for (let i = select.options.length - 1; i >= 0; i--) { + select.remove(i); + } + } else { + this._enableSelect(select); + } + this._updateSelectOptions(); + } + }); + } + + /** + * Render the table structure. + */ + async _renderLayout() { + this.innerHTML = ""; + let [ + firstRowContainsHeaders, + sourceField, + sourceFirstRecord, + sourceSecondRecord, + targetField, + ] = await document.l10n.formatValues([ + "csv-first-row-contains-headers", + "csv-source-field", + "csv-source-first-record", + "csv-source-second-record", + "csv-target-field", + ]); + + let label = document.createElement("label"); + label.className = "toggle-container-with-text"; + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = Services.prefs.getBoolPref( + "mail.import.csv.skipfirstrow", + true + ); + let labelText = document.createElement("span"); + labelText.textContent = firstRowContainsHeaders; + label.appendChild(checkbox); + label.appendChild(labelText); + this.appendChild(label); + + let table = document.createElement("table"); + + let thead = document.createElement("thead"); + let tr = document.createElement("tr"); + let headers = []; + for (let colName of [sourceField, sourceFirstRecord, targetField, ""]) { + let th = document.createElement("th"); + th.textContent = colName; + tr.appendChild(th); + headers.push(th); + } + thead.appendChild(tr); + table.appendChild(thead); + + this._elTbody = document.createElement("tbody"); + table.appendChild(this._elTbody); + + this.appendChild(table); + this._bindEvents(); + + checkbox.addEventListener("change", () => { + if (checkbox.checked) { + headers[0].textContent = sourceField; + headers[1].textContent = sourceFirstRecord; + } else { + headers[0].textContent = sourceFirstRecord; + headers[1].textContent = sourceSecondRecord; + } + Services.prefs.setBoolPref( + "mail.import.csv.skipfirstrow", + checkbox.checked + ); + }); + } + + /** + * Render the table content. Each row contains four columns: + * Source field | Source Data | Address book field | <checkbox> + */ + _renderTable() { + let colCount = this._rows[0].length; + for (let i = 0; i < colCount; i++) { + let tr = document.createElement("tr"); + + // Render the source field name and source data. + for (let j = 0; j < this.DATA_ROWS_LIMIT; j++) { + let td = document.createElement("td"); + td.textContent = this._rows[j]?.[i] || ""; + tr.appendChild(td); + } + + // Render a <select> for target field name. + let td = document.createElement("td"); + let select = document.createElement("select"); + td.appendChild(select); + tr.appendChild(td); + + // Render a checkbox. + td = document.createElement("td"); + let checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.checked = true; + td.appendChild(checkbox); + tr.appendChild(td); + + this._elTbody.appendChild(tr); + } + + this._initSelectOptions(); + } + + /** + * Render the table layout and content. + */ + async _render() { + await this._renderLayout(); + this._renderTable(); + } +} + +customElements.define("csv-field-map", CsvFieldMap); diff --git a/comm/mailnews/import/content/fieldMapImport.js b/comm/mailnews/import/content/fieldMapImport.js new file mode 100644 index 0000000000..3f777c0cb6 --- /dev/null +++ b/comm/mailnews/import/content/fieldMapImport.js @@ -0,0 +1,259 @@ +/* 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/. */ + +var importService; +var fieldMap = null; +var gRecordNum = 0; +var addInterface = null; +var dialogResult = null; +var gPreviousButton; +var gNextButton; +var gMoveUpButton; +var gMoveDownButton; +var gListbox; +var gSkipFirstRecordButton; + +window.addEventListener("DOMContentLoaded", event => { + OnLoadFieldMapImport(); +}); +window.addEventListener("load", resizeColumns, { once: true }); +window.addEventListener("resize", resizeColumns); + +document.addEventListener("dialogaccept", FieldImportOKButton); + +function resizeColumns() { + let list = document.getElementById("fieldList"); + let cols = list.getElementsByTagName("treecol"); + list.style.setProperty( + "--column1width", + cols[0].getBoundingClientRect().width + "px" + ); + list.style.setProperty( + "--column2width", + cols[1].getBoundingClientRect().width + "px" + ); +} + +function OnLoadFieldMapImport() { + top.importService = Cc["@mozilla.org/import/import-service;1"].getService( + Ci.nsIImportService + ); + + // We need a field map object... + // assume we have one passed in? or just make one? + if (window.arguments && window.arguments[0]) { + top.fieldMap = window.arguments[0].fieldMap; + top.addInterface = window.arguments[0].addInterface; + top.dialogResult = window.arguments[0].result; + } + if (top.fieldMap == null) { + top.fieldMap = top.importService.CreateNewFieldMap(); + top.fieldMap.DefaultFieldMap(top.fieldMap.numMozFields); + } + + gMoveUpButton = document.getElementById("upButton"); + gMoveDownButton = document.getElementById("downButton"); + gPreviousButton = document.getElementById("previous"); + gNextButton = document.getElementById("next"); + gListbox = document.getElementById("fieldList"); + gSkipFirstRecordButton = document.getElementById("skipFirstRecord"); + + // Set the state of the skip first record button + gSkipFirstRecordButton.checked = top.fieldMap.skipFirstRecord; + + ListFields(); + browseDataPreview(1); + gListbox.selectedItem = gListbox.getItemAtIndex(0); + disableMoveButtons(); +} + +function IndexInMap(index) { + var count = top.fieldMap.mapSize; + for (var i = 0; i < count; i++) { + if (top.fieldMap.GetFieldMap(i) == index) { + return true; + } + } + + return false; +} + +function ListFields() { + if (top.fieldMap == null) { + return; + } + + // Add rows for every mapped field. + let count = top.fieldMap.mapSize; + for (let i = 0; i < count; i++) { + let index = top.fieldMap.GetFieldMap(i); + if (index == -1) { + continue; + } + AddFieldToList( + top.fieldMap.GetFieldDescription(index), + index, + top.fieldMap.GetFieldActive(i) + ); + } + + // Add rows every possible field we don't already have a row for. + count = top.fieldMap.numMozFields; + for (let i = 0; i < count; i++) { + if (!IndexInMap(i)) { + AddFieldToList(top.fieldMap.GetFieldDescription(i), i, false); + } + } + + // Add dummy rows if the data has more fields than Thunderbird does. + let data = top.addInterface.GetData("sampleData-0"); + if (!(data instanceof Ci.nsISupportsString)) { + return; + } + count = data.data.split("\n").length; + for (let i = gListbox.itemCount; i < count; i++) { + AddFieldToList(null, -1, false); + } +} + +function CreateField(name, index, on) { + var item = document.createXULElement("richlistitem"); + item.setAttribute("align", "center"); + item.setAttribute("field-index", index); + item.setAttribute("allowevents", "true"); + + var checkboxCell = document.createXULElement("hbox"); + checkboxCell.setAttribute("style", "width: var(--column1width)"); + let checkbox = document.createXULElement("checkbox"); + if (!name) { + checkbox.disabled = true; + } else if (on) { + checkbox.setAttribute("checked", "true"); + } + checkboxCell.appendChild(checkbox); + + var firstCell = document.createXULElement("label"); + firstCell.setAttribute("style", "width: var(--column2width)"); + firstCell.setAttribute("value", name || ""); + + var secondCell = document.createXULElement("label"); + secondCell.setAttribute("class", "importsampledata"); + secondCell.setAttribute("flex", "1"); + + item.appendChild(checkboxCell); + item.appendChild(firstCell); + item.appendChild(secondCell); + return item; +} + +function AddFieldToList(name, index, on) { + var item = CreateField(name, index, on); + gListbox.appendChild(item); +} + +// The "Move Up/Move Down" buttons should move the items in the left column +// up/down but the values in the right column should not change. +function moveItem(up) { + var selectedItem = gListbox.selectedItem; + var swapPartner = up + ? gListbox.getPreviousItem(selectedItem, 1) + : gListbox.getNextItem(selectedItem, 1); + + var tmpLabel = swapPartner.lastElementChild.getAttribute("value"); + swapPartner.lastElementChild.setAttribute( + "value", + selectedItem.lastElementChild.getAttribute("value") + ); + selectedItem.lastElementChild.setAttribute("value", tmpLabel); + + var newItemPosition = up ? selectedItem.nextElementSibling : selectedItem; + gListbox.insertBefore(swapPartner, newItemPosition); + gListbox.ensureElementIsVisible(selectedItem); + disableMoveButtons(); +} + +function disableMoveButtons() { + var selectedIndex = gListbox.selectedIndex; + gMoveUpButton.disabled = selectedIndex == 0; + gMoveDownButton.disabled = selectedIndex == gListbox.getRowCount() - 1; +} + +function ShowSampleData(data) { + var fields = data.split("\n"); + for (var i = 0; i < gListbox.getRowCount(); i++) { + gListbox + .getItemAtIndex(i) + .lastElementChild.setAttribute( + "value", + i < fields.length ? fields[i] : "" + ); + } +} + +function FetchSampleData(num) { + if (!top.addInterface) { + return false; + } + + var data = top.addInterface.GetData("sampleData-" + num); + if (!(data instanceof Ci.nsISupportsString)) { + return false; + } + ShowSampleData(data.data); + return true; +} + +/** + * Handle the command event of #next and #previous buttons. + * + * @param {Event} event - The command event of #next or #previous button. + */ +function nextPreviousOnCommand(event) { + browseDataPreview(event.target.id == "next" ? 1 : -1); +} + +/** + * Browse the import data preview by moving to the next or previous record. + * Also handle the disabled status of the #next and #previous buttons at the + * first or last record. + * + * @param {integer} step - How many records to move forwards or backwards. + * Used by the #next and #previous buttons with step of 1 or -1. + */ +function browseDataPreview(step) { + gRecordNum += step; + if (FetchSampleData(gRecordNum - 1)) { + document.l10n.setAttributes( + document.getElementById("labelRecordNumber"), + "import-ab-csv-preview-record-number", + { + recordNumber: gRecordNum, + } + ); + } + + gPreviousButton.disabled = gRecordNum == 1; + gNextButton.disabled = + addInterface.GetData("sampleData-" + gRecordNum) == null; +} + +function FieldImportOKButton() { + var max = gListbox.getRowCount(); + // Ensure field map is the right size + top.fieldMap.SetFieldMapSize(max); + + for (let i = 0; i < max; i++) { + let fIndex = gListbox.getItemAtIndex(i).getAttribute("field-index"); + let on = gListbox + .getItemAtIndex(i) + .querySelector("checkbox") + .getAttribute("checked"); + top.fieldMap.SetFieldMap(i, fIndex); + top.fieldMap.SetFieldActive(i, on == "true"); + } + + top.fieldMap.skipFirstRecord = gSkipFirstRecordButton.checked; + + top.dialogResult.ok = true; +} diff --git a/comm/mailnews/import/content/fieldMapImport.xhtml b/comm/mailnews/import/content/fieldMapImport.xhtml new file mode 100644 index 0000000000..8278436321 --- /dev/null +++ b/comm/mailnews/import/content/fieldMapImport.xhtml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<!-- 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/. --> +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/shared/fieldMapImport.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE html SYSTEM "chrome://messenger/locale/fieldMapImport.dtd"> + +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + width="640" + height="480" + scrolling="false" +> + <head> + <link rel="localization" href="messenger/addressbook/fieldMapImport.ftl" /> + <title data-l10n-id="import-ab-csv-dialog-title"></title> + <script + defer="defer" + src="chrome://messenger/content/fieldMapImport.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/dialogShadowDom.js" + ></script> + </head> + <html:body + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + > + <dialog + buttons="accept,cancel" + data-l10n-id="import-ab-csv-dialog" + data-l10n-attrs="buttonlabelaccept,buttonaccesskeyaccept" + > + <hbox align="center"> + <label id="labelRecordNumber" /> + <spacer flex="1" /> + <button + id="previous" + oncommand="nextPreviousOnCommand(event);" + label="&fieldMapImport.previous.label;" + accesskey="&fieldMapImport.previous.accesskey;" + /> + <button + id="next" + oncommand="nextPreviousOnCommand(event);" + label="&fieldMapImport.next.label;" + accesskey="&fieldMapImport.next.accesskey;" + /> + </hbox> + + <hbox align="center"> + <checkbox + id="skipFirstRecord" + label="&fieldMapImport.skipFirstRecord.label;" + accesskey="&fieldMapImport.skipFirstRecord.accessKey;" + /> + </hbox> + + <separator class="thin" /> + <label control="fieldList">&fieldMapImport.text;</label> + <separator class="thin" /> + + <!-- field list --> + <hbox flex="1"> + <richlistbox id="fieldList" flex="1" onselect="disableMoveButtons();"> + <treecols> + <treecol id="checkedHeader" /> + <treecol + id="fieldNameHeader" + label="&fieldMapImport.fieldListTitle;" + /> + <treecol id="sampleDataHeader" label="&fieldMapImport.dataTitle;" /> + </treecols> + </richlistbox> + + <vbox> + <spacer flex="1" /> + <button + id="upButton" + class="up" + label="&fieldMapImport.up.label;" + accesskey="&fieldMapImport.up.accesskey;" + oncommand="moveItem(true);" + /> + <button + id="downButton" + class="down" + label="&fieldMapImport.down.label;" + accesskey="&fieldMapImport.down.accesskey;" + oncommand="moveItem(false);" + /> + <spacer flex="1" /> + </vbox> + </hbox> + </dialog> + </html:body> +</html> diff --git a/comm/mailnews/import/content/importDialog.js b/comm/mailnews/import/content/importDialog.js new file mode 100644 index 0000000000..cf029d4989 --- /dev/null +++ b/comm/mailnews/import/content/importDialog.js @@ -0,0 +1,1184 @@ +/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +"use strict"; + +/* import-globals-from ../../extensions/newsblog/feed-subscriptions.js */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); + +var gImportType = null; +var gImportMsgsBundle; +var gFeedsBundle; +var gImportService = null; +var gSuccessStr = null; +var gErrorStr = null; +var gInputStr = null; +var gProgressInfo = null; +var gSelectedModuleName = null; +var gAddInterface = null; +var gNewFeedAcctCreated = false; + +window.addEventListener("DOMContentLoaded", OnLoadImportDialog); +window.addEventListener("unload", OnUnloadImportDialog); + +function OnLoadImportDialog() { + gImportMsgsBundle = document.getElementById("bundle_importMsgs"); + gFeedsBundle = document.getElementById("bundle_feeds"); + gImportService = Cc["@mozilla.org/import/import-service;1"].getService( + Ci.nsIImportService + ); + + gProgressInfo = {}; + gProgressInfo.progressWindow = null; + gProgressInfo.importInterface = null; + gProgressInfo.mainWindow = window; + gProgressInfo.intervalState = 0; + gProgressInfo.importSuccess = false; + gProgressInfo.importType = null; + gProgressInfo.localFolderExists = false; + + gSuccessStr = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + gErrorStr = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + gInputStr = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + + // look in arguments[0] for parameters + if ( + "arguments" in window && + window.arguments.length >= 1 && + "importType" in window.arguments[0] && + window.arguments[0].importType + ) { + // keep parameters in global for later + gImportType = window.arguments[0].importType; + gProgressInfo.importType = gImportType; + } else { + gImportType = "all"; + gProgressInfo.importType = "all"; + } + + SetUpImportType(); + + // on startup, set the focus to the control element + // for accessibility reasons. + // if we used the wizardOverlay, we would get this for free. + // see bug #101874 + document.getElementById("importFields").focus(); +} + +/** + * After importing, need to restart so that imported address books and mail + * accounts can show up. + */ +function OnUnloadImportDialog() { + let nextButton = document.getElementById("forward"); + if ( + gImportType == "settings" && + !gErrorStr.data && + nextButton.label == nextButton.getAttribute("finishedval") + ) { + MailUtils.restartApplication(); + } +} + +function SetUpImportType() { + // set dialog title + document.getElementById("importFields").value = gImportType; + + // Mac migration not working right now, so disable it. + if (Services.appinfo.OS == "Darwin") { + document.getElementById("allRadio").setAttribute("disabled", "true"); + if (gImportType == "all") { + document.getElementById("importFields").value = "addressbook"; + } + } + + let fileLabel = document.getElementById("fileLabel"); + let accountLabel = document.getElementById("accountLabel"); + if (gImportType == "feeds") { + accountLabel.hidden = false; + fileLabel.hidden = true; + ListFeedAccounts(); + } else { + accountLabel.hidden = true; + fileLabel.hidden = false; + ListModules(); + } +} + +function SetDivText(id, text) { + var div = document.getElementById(id); + + if (div) { + if (!div.hasChildNodes()) { + var textNode = document.createTextNode(text); + div.appendChild(textNode); + } else if (div.childNodes.length == 1) { + div.firstChild.nodeValue = text; + } + } +} + +function CheckIfLocalFolderExists() { + try { + if (MailServices.accounts.localFoldersServer) { + gProgressInfo.localFolderExists = true; + } + } catch (ex) { + gProgressInfo.localFolderExists = false; + } +} + +function showWizardBox(index) { + let stateBox = document.getElementById("stateBox"); + for (let i = 0; i < stateBox.children.length; i++) { + stateBox.children[i].hidden = i != index; + } +} + +function getWizardBoxIndex() { + let selectedIndex = 0; + for (let element of document.getElementById("stateBox").children) { + if (!element.hidden) { + return selectedIndex; + } + selectedIndex++; + } + return selectedIndex - 1; +} + +async function ImportDialogOKButton() { + var listbox = document.getElementById("moduleList"); + var header = document.getElementById("header"); + var progressMeterEl = document.getElementById("progressMeter"); + progressMeterEl.value = 0; + var progressStatusEl = document.getElementById("progressStatus"); + var progressTitleEl = document.getElementById("progressTitle"); + + // better not mess around with navigation at this point + var nextButton = document.getElementById("forward"); + nextButton.setAttribute("disabled", "true"); + var backButton = document.getElementById("back"); + backButton.setAttribute("disabled", "true"); + + if (listbox && listbox.selectedCount == 1) { + let module = ""; + let name = ""; + gImportType = document.getElementById("importFields").value; + let index = listbox.selectedItem.getAttribute("list-index"); + if (index == -1) { + return false; + } + if (gImportType == "feeds") { + module = "Feeds"; + } else { + module = gImportService.GetModule(gImportType, index); + name = gImportService.GetModuleName(gImportType, index); + } + gSelectedModuleName = name; + if (module) { + // Fix for Bug 57839 & 85219 + // We use localFoldersServer(in nsIMsgAccountManager) to check if Local Folder exists. + // We need to check localFoldersServer before importing "mail", "settings", or "filters". + // Reason: We will create an account with an incoming server of type "none" after + // importing "mail", so the localFoldersServer is valid even though the Local Folder + // is not created. + if ( + gImportType == "mail" || + gImportType == "settings" || + gImportType == "filters" + ) { + CheckIfLocalFolderExists(); + } + + let meterText = ""; + let error = {}; + switch (gImportType) { + case "mail": + if (await ImportMail(module, gSuccessStr, gErrorStr)) { + // We think it was a success, either, we need to + // wait for the import to finish + // or we are done! + if (gProgressInfo.importInterface == null) { + ShowImportResults(true, "Mail"); + return true; + } + + meterText = gImportMsgsBundle.getFormattedString( + "MailProgressMeterText", + [name] + ); + header.setAttribute("description", meterText); + + progressStatusEl.setAttribute("label", ""); + progressTitleEl.setAttribute("label", meterText); + + showWizardBox(2); + gProgressInfo.progressWindow = window; + gProgressInfo.intervalState = setInterval( + ContinueImportCallback, + 100 + ); + return true; + } + + ShowImportResults(false, "Mail"); + // Re-enable the next button, as we are here, because the user cancelled the picking. + // Enable next, so they can try again. + nextButton.removeAttribute("disabled"); + // Also enable back button so that users can pick other import options. + backButton.removeAttribute("disabled"); + return false; + + case "feeds": + if (await ImportFeeds()) { + // Successful completion of pre processing and launch of async import. + meterText = document.getElementById("description").textContent; + header.setAttribute("description", meterText); + + progressStatusEl.setAttribute("label", ""); + progressTitleEl.setAttribute("label", meterText); + progressMeterEl.removeAttribute("value"); + + showWizardBox(2); + return true; + } + + // Re-enable the next button, as we are here, because the user cancelled the picking. + // Enable next, so they can try again. + nextButton.removeAttribute("disabled"); + // Also enable back button so that users can pick other import options. + backButton.removeAttribute("disabled"); + return false; + + case "addressbook": + if (await ImportAddress(module, gSuccessStr, gErrorStr)) { + // We think it was a success, either, we need to + // wait for the import to finish + // or we are done! + if (gProgressInfo.importInterface == null) { + ShowImportResults(true, "Address"); + return true; + } + + meterText = gImportMsgsBundle.getFormattedString( + "AddrProgressMeterText", + [name] + ); + header.setAttribute("description", meterText); + + progressStatusEl.setAttribute("label", ""); + progressTitleEl.setAttribute("label", meterText); + + showWizardBox(2); + gProgressInfo.progressWindow = window; + gProgressInfo.intervalState = setInterval( + ContinueImportCallback, + 100 + ); + + return true; + } + + ShowImportResults(false, "Address"); + // Re-enable the next button, as we are here, because the user cancelled the picking. + // Enable next, so they can try again. + nextButton.removeAttribute("disabled"); + // Also enable back button so that users can pick other import options. + backButton.removeAttribute("disabled"); + return false; + + case "settings": + error.value = null; + let newAccount = {}; + if (!(await ImportSettings(module, newAccount, error))) { + if (error.value) { + ShowImportResultsRaw( + gImportMsgsBundle.getString("ImportSettingsFailed"), + null, + false + ); + } + // Re-enable the next button, as we are here, because the user cancelled the picking. + // Enable next, so they can try again. + nextButton.removeAttribute("disabled"); + // Also enable back button so that users can pick other import options. + backButton.removeAttribute("disabled"); + return false; + } + ShowImportResultsRaw( + gImportMsgsBundle.getFormattedString("ImportSettingsSuccess", [ + name, + ]), + null, + true + ); + break; + + case "filters": + error.value = null; + if (!ImportFilters(module, error)) { + if (error.value) { + ShowImportResultsRaw( + gImportMsgsBundle.getFormattedString("ImportFiltersFailed", [ + name, + ]), + error.value, + false + ); + } + // Re-enable the next button, as we are here, because the user cancelled the picking. + // Enable next, so they can try again. + nextButton.removeAttribute("disabled"); + // Also enable back button so that users can pick other import options. + backButton.removeAttribute("disabled"); + return false; + } + + if (error.value) { + ShowImportResultsRaw( + gImportMsgsBundle.getFormattedString("ImportFiltersPartial", [ + name, + ]), + error.value, + true + ); + } else { + ShowImportResultsRaw( + gImportMsgsBundle.getFormattedString("ImportFiltersSuccess", [ + name, + ]), + null, + true + ); + } + + break; + } + } + } + + return true; +} + +function SetStatusText(val) { + var progressStatus = document.getElementById("progressStatus"); + progressStatus.setAttribute("label", val); +} + +function SetProgress(val) { + var progressMeter = document.getElementById("progressMeter"); + progressMeter.value = val; +} + +function ContinueImportCallback() { + gProgressInfo.mainWindow.ContinueImport(gProgressInfo); +} + +function ImportSelectionChanged() { + let listbox = document.getElementById("moduleList"); + let acctNameBox = document.getElementById("acctName-box"); + if (listbox && listbox.selectedCount == 1) { + let index = listbox.selectedItem.getAttribute("list-index"); + if (index == -1) { + return; + } + acctNameBox.setAttribute("style", "visibility: hidden;"); + if (gImportType == "feeds") { + if (index == 0) { + SetDivText( + "description", + gFeedsBundle.getString("ImportFeedsNewAccount") + ); + let defaultName = gFeedsBundle.getString("feeds-accountname"); + document.getElementById("acctName").value = defaultName; + acctNameBox.removeAttribute("style"); + } else { + SetDivText( + "description", + gFeedsBundle.getString("ImportFeedsExistingAccount") + ); + } + } else { + SetDivText( + "description", + gImportService.GetModuleDescription(gImportType, index) + ); + } + } +} + +function CompareImportModuleName(a, b) { + if (a.name > b.name) { + return 1; + } + if (a.name < b.name) { + return -1; + } + return 0; +} + +function ListModules() { + if (gImportService == null) { + return; + } + + var body = document.getElementById("moduleList"); + while (body.hasChildNodes()) { + body.lastChild.remove(); + } + + var count = gImportService.GetModuleCount(gImportType); + var i; + + var moduleArray = new Array(count); + for (i = 0; i < count; i++) { + moduleArray[i] = { + name: gImportService.GetModuleName(gImportType, i), + index: i, + }; + } + + // sort the array of modules by name, so that they'll show up in the right order + moduleArray.sort(CompareImportModuleName); + + for (i = 0; i < count; i++) { + AddModuleToList(moduleArray[i].name, moduleArray[i].index); + } +} + +function AddModuleToList(moduleName, index) { + var body = document.getElementById("moduleList"); + + let item = document.createXULElement("richlistitem"); + let label = document.createXULElement("label"); + label.setAttribute("value", moduleName); + item.appendChild(label); + item.setAttribute("list-index", index); + body.appendChild(item); +} + +function ListFeedAccounts() { + let body = document.getElementById("moduleList"); + while (body.hasChildNodes()) { + body.lastChild.remove(); + } + + // Add item to allow for new account creation. + let item = document.createXULElement("richlistitem"); + let label = document.createXULElement("label"); + label.setAttribute( + "value", + gFeedsBundle.getString("ImportFeedsCreateNewListItem") + ); + item.appendChild(label); + item.setAttribute("list-index", 0); + body.appendChild(item); + + let index = 0; + let feedRootFolders = FeedUtils.getAllRssServerRootFolders(); + + feedRootFolders.forEach(function (rootFolder) { + item = document.createXULElement("richlistitem"); + let label = document.createXULElement("label"); + label.setAttribute("value", rootFolder.prettyName); + item.appendChild(label); + item.setAttribute("list-index", ++index); + item.server = rootFolder.server; + body.appendChild(item); + }, this); + + if (index) { + // If there is an existing feed account, select the first one. + body.selectedIndex = 1; + } +} + +function ContinueImport(info) { + var isMail = info.importType == "mail"; + var clear = true; + var pcnt; + + if (info.importInterface) { + if (!info.importInterface.ContinueImport()) { + info.importSuccess = false; + clearInterval(info.intervalState); + if (info.progressWindow != null) { + showWizardBox(3); + info.progressWindow = null; + } + + ShowImportResults(false, isMail ? "Mail" : "Address"); + } else if ((pcnt = info.importInterface.GetProgress()) < 100) { + clear = false; + if (info.progressWindow != null) { + if (pcnt < 5) { + pcnt = 5; + } + SetProgress(pcnt); + if (isMail) { + let mailName = info.importInterface.GetData("currentMailbox"); + if (mailName) { + mailName = mailName.QueryInterface(Ci.nsISupportsString); + if (mailName) { + SetStatusText(mailName.data); + } + } + } + } + } else { + dump("*** WARNING! sometimes this shows results too early. \n"); + dump(" something screwy here. this used to work fine.\n"); + clearInterval(info.intervalState); + info.importSuccess = true; + if (info.progressWindow) { + showWizardBox(3); + info.progressWindow = null; + } + + ShowImportResults(true, isMail ? "Mail" : "Address"); + } + } + if (clear) { + info.intervalState = null; + info.importInterface = null; + } +} + +function ShowResults(doesWantProgress, result) { + if (result) { + if (doesWantProgress) { + let header = document.getElementById("header"); + let progressStatusEl = document.getElementById("progressStatus"); + let progressTitleEl = document.getElementById("progressTitle"); + + let meterText = gImportMsgsBundle.getFormattedString( + "AddrProgressMeterText", + [name] + ); + header.setAttribute("description", meterText); + + progressStatusEl.setAttribute("label", ""); + progressTitleEl.setAttribute("label", meterText); + + showWizardBox(2); + gProgressInfo.progressWindow = window; + gProgressInfo.intervalState = setInterval(ContinueImportCallback, 100); + } else { + ShowImportResults(true, "Address"); + } + } else { + ShowImportResults(false, "Address"); + } + + return true; +} + +function ShowImportResults(good, module) { + // String keys for ImportSettingsSuccess, ImportSettingsFailed, + // ImportMailSuccess, ImportMailFailed, ImportAddressSuccess, + // ImportAddressFailed, ImportFiltersSuccess, and ImportFiltersFailed. + var modSuccess = "Import" + module + "Success"; + var modFailed = "Import" + module + "Failed"; + + // The callers seem to set 'good' to true even if there's something + // in the error log. So we should only make it a success case if + // error log/str is empty. + var results, title; + var moduleName = gSelectedModuleName ? gSelectedModuleName : ""; + if (good && !gErrorStr.data) { + title = gImportMsgsBundle.getFormattedString(modSuccess, [moduleName]); + results = gSuccessStr.data; + } else if (gErrorStr.data) { + title = gImportMsgsBundle.getFormattedString(modFailed, [moduleName]); + results = gErrorStr.data; + } + + if (results && title) { + ShowImportResultsRaw(title, results, good); + } +} + +function ShowImportResultsRaw(title, results, good) { + SetDivText("status", title); + var header = document.getElementById("header"); + header.setAttribute("description", title); + dump("*** results = " + results + "\n"); + attachStrings("results", results); + showWizardBox(3); + var nextButton = document.getElementById("forward"); + nextButton.label = nextButton.getAttribute("finishedval"); + nextButton.removeAttribute("disabled"); + var cancelButton = document.getElementById("cancel"); + cancelButton.setAttribute("disabled", "true"); + var backButton = document.getElementById("back"); + backButton.setAttribute("disabled", "true"); + + // If the Local Folder doesn't exist, create it after successfully + // importing "mail" and "settings" + var checkLocalFolder = + gProgressInfo.importType == "mail" || + gProgressInfo.importType == "settings"; + if (good && checkLocalFolder && !gProgressInfo.localFolderExists) { + MailServices.accounts.createLocalMailAccount(); + } +} + +function attachStrings(aNode, aString) { + var attachNode = document.getElementById(aNode); + if (!aString) { + attachNode.parentNode.setAttribute("hidden", "true"); + return; + } + var strings = aString.split("\n"); + for (let string of strings) { + if (string) { + let currNode = document.createTextNode(string); + attachNode.appendChild(currNode); + let br = document.createElementNS("http://www.w3.org/1999/xhtml", "br"); + attachNode.appendChild(br); + } + } +} + +/** + * Show the file picker. + * + * @returns {Promise} the selected file, or null + */ +function promptForFile(fp) { + return new Promise(resolve => { + fp.open(rv => { + if (rv != Ci.nsIFilePicker.returnOK || !fp.file) { + resolve(null); + return; + } + resolve(fp.file); + }); + }); +} + +/* + Import Settings from a specific module, returns false if it failed + and true if successful. A "local mail" account is returned in newAccount. + This is only useful in upgrading - import the settings first, then + import mail into the account returned from ImportSettings, then + import address books. + An error string is returned as error.value +*/ +async function ImportSettings(module, newAccount, error) { + var setIntf = module.GetImportInterface("settings"); + if (!(setIntf instanceof Ci.nsIImportSettings)) { + error.value = gImportMsgsBundle.getString("ImportSettingsBadModule"); + return false; + } + + // determine if we can auto find the settings or if we need to ask the user + var location = {}; + var description = {}; + var result = setIntf.AutoLocate(description, location); + if (!result) { + // In this case, we couldn't find the settings + if (location.value != null) { + // Settings were not found, however, they are specified + // in a file, so ask the user for the settings file. + let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(); + if (filePicker instanceof Ci.nsIFilePicker) { + let file = null; + try { + filePicker.init( + window, + gImportMsgsBundle.getString("ImportSelectSettings"), + filePicker.modeOpen + ); + filePicker.appendFilters(filePicker.filterAll); + + file = await promptForFile(filePicker); + } catch (ex) { + console.error(ex); + error.value = null; + return false; + } + if (file != null) { + setIntf.SetLocation(file); + } else { + error.value = null; + return false; + } + } else { + error.value = gImportMsgsBundle.getString("ImportSettingsNotFound"); + return false; + } + } else { + error.value = gImportMsgsBundle.getString("ImportSettingsNotFound"); + return false; + } + } + + // interesting, we need to return the account that new + // mail should be imported into? + // that's really only useful for "Upgrade" + result = setIntf.Import(newAccount); + if (!result) { + error.value = gImportMsgsBundle.getString("ImportSettingsFailed"); + } + return result; +} + +async function ImportMail(module, success, error) { + if (gProgressInfo.importInterface || gProgressInfo.intervalState) { + error.data = gImportMsgsBundle.getString("ImportAlreadyInProgress"); + return false; + } + + gProgressInfo.importSuccess = false; + + var mailInterface = module.GetImportInterface("mail"); + if (!(mailInterface instanceof Ci.nsIImportGeneric)) { + error.data = gImportMsgsBundle.getString("ImportMailBadModule"); + return false; + } + + var loc = mailInterface.GetData("mailLocation"); + + if (loc == null) { + // No location found, check to see if we can ask the user. + if (mailInterface.GetStatus("canUserSetLocation") != 0) { + let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(); + if (filePicker instanceof Ci.nsIFilePicker) { + try { + filePicker.init( + window, + gImportMsgsBundle.getString("ImportSelectMailDir"), + filePicker.modeGetFolder + ); + filePicker.appendFilters(filePicker.filterAll); + let file = await promptForFile(filePicker); + if (!file) { + return false; + } + mailInterface.SetData("mailLocation", file); + } catch (ex) { + console.error(ex); + // don't show an error when we return! + return false; + } + } else { + error.data = gImportMsgsBundle.getString("ImportMailNotFound"); + return false; + } + } else { + error.data = gImportMsgsBundle.getString("ImportMailNotFound"); + return false; + } + } + + if (mailInterface.WantsProgress()) { + if (mailInterface.BeginImport(success, error)) { + gProgressInfo.importInterface = mailInterface; + // intervalState = setInterval(ContinueImport, 100); + return true; + } + return false; + } + return mailInterface.BeginImport(success, error); +} + +// The address import! A little more complicated than the mail import +// due to field maps... +async function ImportAddress(module, success, error) { + if (gProgressInfo.importInterface || gProgressInfo.intervalState) { + error.data = gImportMsgsBundle.getString("ImportAlreadyInProgress"); + return false; + } + + gProgressInfo.importSuccess = false; + + gAddInterface = module.GetImportInterface("addressbook"); + if (!(gAddInterface instanceof Ci.nsIImportGeneric)) { + error.data = gImportMsgsBundle.getString("ImportAddressBadModule"); + return false; + } + + var loc = gAddInterface.GetStatus("autoFind"); + if (loc == 0) { + loc = gAddInterface.GetData("addressLocation"); + if (loc instanceof Ci.nsIFile && !loc.exists) { + loc = null; + } + } + + if (loc == null) { + // Couldn't find the address book, see if we can + // as the user for the location or not? + if (gAddInterface.GetStatus("canUserSetLocation") == 0) { + // an autofind address book that could not be found! + error.data = gImportMsgsBundle.getString("ImportAddressNotFound"); + return false; + } + + let filePicker = Cc["@mozilla.org/filepicker;1"].createInstance(); + if (!(filePicker instanceof Ci.nsIFilePicker)) { + error.data = gImportMsgsBundle.getString("ImportAddressNotFound"); + return false; + } + + // The address book location was not found. + // Determine if we need to ask for a directory + // or a single file. + let file = null; + let fileIsDirectory = false; + if (gAddInterface.GetStatus("supportsMultiple") != 0) { + // ask for dir + try { + filePicker.init( + window, + gImportMsgsBundle.getString("ImportSelectAddrDir"), + filePicker.modeGetFolder + ); + filePicker.appendFilters(filePicker.filterAll); + file = await promptForFile(filePicker); + if (file && file.path) { + fileIsDirectory = true; + } + } catch (ex) { + console.error(ex); + file = null; + } + } else { + // ask for file + try { + filePicker.init( + window, + gImportMsgsBundle.getString("ImportSelectAddrFile"), + filePicker.modeOpen + ); + let addressbookBundle = document.getElementById("bundle_addressbook"); + if ( + gSelectedModuleName == + document + .getElementById("bundle_vcardImportMsgs") + .getString("vCardImportName") + ) { + filePicker.appendFilter( + addressbookBundle.getString("VCFFiles"), + "*.vcf" + ); + } else if ( + gSelectedModuleName == + document + .getElementById("bundle_morkImportMsgs") + .getString("morkImportName") + ) { + filePicker.appendFilter( + document + .getElementById("bundle_morkImportMsgs") + .getString("MABFiles"), + "*.mab" + ); + } else { + filePicker.appendFilter( + addressbookBundle.getString("LDIFFiles"), + "*.ldi; *.ldif" + ); + filePicker.appendFilter( + addressbookBundle.getString("CSVFiles"), + "*.csv" + ); + filePicker.appendFilter( + addressbookBundle.getString("TABFiles"), + "*.tab; *.txt" + ); + filePicker.appendFilter( + addressbookBundle.getString("SupportedABFiles"), + "*.csv; *.ldi; *.ldif; *.tab; *.txt" + ); + filePicker.appendFilters(filePicker.filterAll); + // Use "Supported Address Book Files" as default file filter. + filePicker.filterIndex = 3; + } + + file = await promptForFile(filePicker); + } catch (ex) { + console.error(ex); + file = null; + } + } + + if (!file) { + return false; + } + + if (!fileIsDirectory && file.fileSize == 0) { + let errorText = gImportMsgsBundle.getFormattedString( + "ImportEmptyAddressBook", + [file.leafName] + ); + + Services.prompt.alert(window, document.title, errorText); + return false; + } + gAddInterface.SetData("addressLocation", file); + } + + var map = gAddInterface.GetData("fieldMap"); + if (map instanceof Ci.nsIImportFieldMap) { + let result = {}; + result.ok = false; + window.openDialog( + "chrome://messenger/content/fieldMapImport.xhtml", + "", + "chrome,modal,titlebar", + { + fieldMap: map, + addInterface: gAddInterface, + result, + } + ); + + if (!result.ok) { + return false; + } + } + + if (gAddInterface.WantsProgress()) { + if (gAddInterface.BeginImport(success, error)) { + gProgressInfo.importInterface = gAddInterface; + // intervalState = setInterval(ContinueImport, 100); + return true; + } + return false; + } + + return gAddInterface.BeginImport(success, error); +} + +/* + Import filters from a specific module. + Returns false if it failed and true if it succeeded. + An error string is returned as error.value. +*/ +function ImportFilters(module, error) { + if (gProgressInfo.importInterface || gProgressInfo.intervalState) { + error.data = gImportMsgsBundle.getString("ImportAlreadyInProgress"); + return false; + } + + gProgressInfo.importSuccess = false; + + var filtersInterface = module.GetImportInterface("filters"); + if (!(filtersInterface instanceof Ci.nsIImportFilters)) { + error.data = gImportMsgsBundle.getString("ImportFiltersBadModule"); + return false; + } + + return filtersInterface.Import(error); +} + +/* + Import feeds. +*/ +async function ImportFeeds() { + // Get file and file url to open from filepicker. + let [openFile, openFileUrl] = await FeedSubscriptions.opmlPickOpenFile(); + + let acctName; + let acctNewExist = gFeedsBundle.getString("ImportFeedsExisting"); + let fileName = openFile.path; + let server = document.getElementById("moduleList").selectedItem.server; + gNewFeedAcctCreated = false; + + if (!server) { + // Create a new Feeds account. + acctName = document.getElementById("acctName").value; + server = FeedUtils.createRssAccount(acctName).incomingServer; + acctNewExist = gFeedsBundle.getString("ImportFeedsNew"); + gNewFeedAcctCreated = true; + } + + acctName = server.rootFolder.prettyName; + + let callback = function (aStatusReport, aLastFolder, aFeedWin) { + let message = gFeedsBundle.getFormattedString("ImportFeedsDone", [ + fileName, + acctNewExist, + acctName, + ]); + ShowImportResultsRaw(message + " " + aStatusReport, null, true); + document.getElementById("back").removeAttribute("disabled"); + + let subscriptionsWindow = Services.wm.getMostRecentWindow( + "Mail:News-BlogSubscriptions" + ); + if (subscriptionsWindow) { + let feedWin = subscriptionsWindow.FeedSubscriptions; + if (aLastFolder) { + feedWin.FolderListener.folderAdded(aLastFolder); + } + + feedWin.mActionMode = null; + feedWin.updateButtons(feedWin.mView.currentItem); + feedWin.clearStatusInfo(); + feedWin.updateStatusItem("statusText", aStatusReport); + } + }; + + if ( + !(await FeedSubscriptions.importOPMLFile( + openFile, + openFileUrl, + server, + callback + )) + ) { + return false; + } + + let subscriptionsWindow = Services.wm.getMostRecentWindow( + "Mail:News-BlogSubscriptions" + ); + if (subscriptionsWindow) { + let feedWin = subscriptionsWindow.FeedSubscriptions; + feedWin.mActionMode = feedWin.kImportingOPML; + feedWin.updateButtons(null); + let statusReport = gFeedsBundle.getString("subscribe-loading"); + feedWin.updateStatusItem("statusText", statusReport); + feedWin.updateStatusItem("progressMeter", "?"); + } + + return true; +} + +function SwitchType(newType) { + if (gImportType == newType) { + return; + } + + gImportType = newType; + gProgressInfo.importType = newType; + + SetUpImportType(); + + SetDivText("description", ""); +} + +function next() { + switch (getWizardBoxIndex()) { + case 0: + let backButton = document.getElementById("back"); + backButton.removeAttribute("disabled"); + let radioGroup = document.getElementById("importFields"); + + if (radioGroup.value == "all") { + let args = { closeMigration: true }; + let SEAMONKEY_ID = "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}"; + if (Services.appinfo.ID == SEAMONKEY_ID) { + window.openDialog( + "chrome://communicator/content/migration/migration.xhtml", + "", + "chrome,dialog,modal,centerscreen" + ); + } else { + // Running as Thunderbird or its clone. + window.openDialog( + "chrome://messenger/content/migration/migration.xhtml", + "", + "chrome,dialog,modal,centerscreen", + null, + null, + null, + args + ); + } + if (args.closeMigration) { + close(); + } + } else { + SwitchType(radioGroup.value); + showWizardBox(1); + let moduleBox = document.getElementById("moduleBox"); + let noModuleLabel = document.getElementById("noModuleLabel"); + if (document.getElementById("moduleList").itemCount > 0) { + moduleBox.hidden = false; + noModuleLabel.hidden = true; + } else { + moduleBox.hidden = true; + noModuleLabel.hidden = false; + } + SelectFirstItem(); + enableAdvance(); + } + break; + case 1: + ImportDialogOKButton(); + break; + case 3: + close(); + break; + } +} + +function SelectFirstItem() { + var listbox = document.getElementById("moduleList"); + if (listbox.selectedIndex == -1 && listbox.itemCount > 0) { + listbox.selectedIndex = 0; + } + ImportSelectionChanged(); +} + +function enableAdvance() { + var listbox = document.getElementById("moduleList"); + var nextButton = document.getElementById("forward"); + if (listbox.selectedCount > 0) { + nextButton.removeAttribute("disabled"); + } else { + nextButton.setAttribute("disabled", "true"); + } +} + +function back() { + var backButton = document.getElementById("back"); + var nextButton = document.getElementById("forward"); + switch (getWizardBoxIndex()) { + case 1: + backButton.setAttribute("disabled", "true"); + nextButton.label = nextButton.getAttribute("nextval"); + nextButton.removeAttribute("disabled"); + showWizardBox(0); + break; + case 3: + // Clear out the results box. + let results = document.getElementById("results"); + while (results.hasChildNodes()) { + results.lastChild.remove(); + } + + // Reset the next button. + nextButton.label = nextButton.getAttribute("nextval"); + nextButton.removeAttribute("disabled"); + + // Enable the cancel button again. + document.getElementById("cancel").removeAttribute("disabled"); + + // If a new Feed account has been created, rebuild the list. + if (gNewFeedAcctCreated) { + ListFeedAccounts(); + } + + // Now go back to the second page. + showWizardBox(1); + break; + } +} diff --git a/comm/mailnews/import/content/importDialog.xhtml b/comm/mailnews/import/content/importDialog.xhtml new file mode 100644 index 0000000000..c3147d8c99 --- /dev/null +++ b/comm/mailnews/import/content/importDialog.xhtml @@ -0,0 +1,225 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?> + +<!DOCTYPE html [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> +%brandDTD; +<!ENTITY % importDTD SYSTEM "chrome://messenger/locale/importDialog.dtd" > +%importDTD; ]> + +<html + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + lightweightthemes="true" + width="720" + height="520" + scrolling="false" +> + <head> + <title>&importDialog.windowTitle;</title> + <script + defer="defer" + src="chrome://messenger/content/globalOverlay.js" + ></script> + <script + defer="defer" + src="chrome://global/content/editMenuOverlay.js" + ></script> + <script + defer="defer" + src="chrome://messenger-newsblog/content/feed-subscriptions.js" + ></script> + <script + defer="defer" + src="chrome://messenger/content/importDialog.js" + ></script> + </head> + <html:body + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + > + <stringbundle + id="bundle_importMsgs" + src="chrome://messenger/locale/importMsgs.properties" + /> + <stringbundle + id="bundle_addressbook" + src="chrome://messenger/locale/addressbook/addressBook.properties" + /> + <stringbundle + id="bundle_morkImportMsgs" + src="chrome://messenger/locale/morkImportMsgs.properties" + /> + <stringbundle + id="bundle_vcardImportMsgs" + src="chrome://messenger/locale/vCardImportMsgs.properties" + /> + <stringbundle + id="bundle_feeds" + src="chrome://messenger-newsblog/locale/newsblog.properties" + /> + + <hbox + class="box-header" + id="header" + title="&importTitle.label;" + description="&importShortDesc.label;" + /> + + <vbox id="stateBox" flex="1" style="min-height: 30em"> + <vbox class="wizard-box"> + <description>&importDescription1.label;</description> + <description>&importDescription2.label;</description> + <separator /> + <radiogroup id="importFields"> + <radio + id="allRadio" + value="all" + label="&importAll.label;" + accesskey="&importAll.accesskey;" + /> + <separator /> + <label control="importFields">&select.label;</label> + <separator class="thin" /> + <vbox class="indent"> + <radio + id="addressbookRadio" + value="addressbook" + label="&importAddressbook.label;" + accesskey="&importAddressbook.accesskey;" + /> + <radio + id="mailRadio" + value="mail" + label="&importMail.label;" + accesskey="&importMail.accesskey;" + /> + <radio + id="feedsRadio" + value="feeds" + label="&importFeeds.label;" + accesskey="&importFeeds.accesskey;" + /> + <radio + id="settingsRadio" + value="settings" + label="&importSettings.label;" + accesskey="&importSettings.accesskey;" + /> + <radio + id="filtersRadio" + value="filters" + label="&importFilters.label;" + accesskey="&importFilters.accesskey;" + /> + </vbox> + </radiogroup> + </vbox> + <vbox class="wizard-box" hidden="true"> + <vbox> + <vbox id="moduleBox"> + <vbox> + <label + id="fileLabel" + control="moduleList" + value="&selectDescription.label;" + accesskey="&selectDescription.accesskey;" + /> + <label + id="accountLabel" + control="moduleList" + value="&selectDescriptionB.label;" + accesskey="&selectDescription.accesskey;" + hidden="true" + /> + </vbox> + <richlistbox + id="moduleList" + height="200px" + onselect="ImportSelectionChanged(); enableAdvance();" + /> + </vbox> + <label id="noModuleLabel" hidden="true">&noModulesFound.label;</label> + </vbox> + <vbox> + <hbox flex="1"> + <description + flex="1" + control="moduleList" + id="description" + class="box-padded" + /> + </hbox> + <hbox + id="acctName-box" + flex="1" + class="input-container" + style="visibility: hidden" + > + <label + control="acctName" + class="box-padded" + accesskey="&acctName.accesskey;" + value="&acctName.label;" + /> + <html:input + id="acctName" + type="text" + class="input-inline" + aria-labelledby="acctName" + /> + </hbox> + </vbox> + </vbox> + <vbox class="wizard-box" hidden="true"> + <spacer flex="1" /> + <html:fieldset> + <label id="progressTitle" class="header">&title.label;</label> + <label + class="indent" + id="progressStatus" + value="&processing.label;" + /> + <vbox class="box-padded"> + <html:progress id="progressMeter" value="5" max="100" /> + </vbox> + </html:fieldset> + </vbox> + <vbox class="wizard-box" flex="1" hidden="true"> + <description id="status" /> + <hbox style="overflow: auto" class="inset" flex="1"> + <description id="results" flex="1" /> + </hbox> + </vbox> + </vbox> + + <separator /> + + <separator class="groove" /> + + <hbox class="box-padded"> + <spacer flex="1" /> + <button + id="back" + label="&back.label;" + disabled="true" + oncommand="back();" + /> + <button + id="forward" + label="&forward.label;" + nextval="&forward.label;" + finishedval="&finish.label;" + oncommand="next();" + /> + <separator orient="vertical" /> + <button id="cancel" label="&cancel.label;" oncommand="window.close();" /> + </hbox> + </html:body> +</html> |