summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/import/content
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/import/content')
-rw-r--r--comm/mailnews/import/content/aboutImport.js1511
-rw-r--r--comm/mailnews/import/content/aboutImport.xhtml477
-rw-r--r--comm/mailnews/import/content/csv-field-map.js280
-rw-r--r--comm/mailnews/import/content/fieldMapImport.js259
-rw-r--r--comm/mailnews/import/content/fieldMapImport.xhtml104
-rw-r--r--comm/mailnews/import/content/importDialog.js1184
-rw-r--r--comm/mailnews/import/content/importDialog.xhtml225
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>