summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/accountcreation/content/accountSetup.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/accountcreation/content/accountSetup.js')
-rw-r--r--comm/mail/components/accountcreation/content/accountSetup.js3023
1 files changed, 3023 insertions, 0 deletions
diff --git a/comm/mail/components/accountcreation/content/accountSetup.js b/comm/mail/components/accountcreation/content/accountSetup.js
new file mode 100644
index 0000000000..3a214f2292
--- /dev/null
+++ b/comm/mail/components/accountcreation/content/accountSetup.js
@@ -0,0 +1,3023 @@
+/* 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 */
+
+/* import-globals-from ../../../../mailnews/base/prefs/content/accountUtils.js */
+var { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+var { fetchConfigFromExchange, getAddonsList } = ChromeUtils.import(
+ "resource:///modules/accountcreation/ExchangeAutoDiscover.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ AccountConfig: "resource:///modules/accountcreation/AccountConfig.jsm",
+ cal: "resource:///modules/calendar/calUtils.jsm",
+ CardDAVUtils: "resource:///modules/CardDAVUtils.jsm",
+ ConfigVerifier: "resource:///modules/accountcreation/ConfigVerifier.jsm",
+ CreateInBackend: "resource:///modules/accountcreation/CreateInBackend.jsm",
+ FetchConfig: "resource:///modules/accountcreation/FetchConfig.jsm",
+ GuessConfig: "resource:///modules/accountcreation/GuessConfig.jsm",
+ OAuth2Providers: "resource:///modules/OAuth2Providers.jsm",
+ Sanitizer: "resource:///modules/accountcreation/Sanitizer.jsm",
+ UIDensity: "resource:///modules/UIDensity.jsm",
+ UIFontSize: "resource:///modules/UIFontSize.jsm",
+});
+
+var {
+ Abortable,
+ AddonInstaller,
+ alertPrompt,
+ assert,
+ CancelledException,
+ Exception,
+ gAccountSetupLogger,
+ NotReached,
+ PriorityOrderAbortable,
+ UserCancelledException,
+} = AccountCreationUtils;
+
+/**
+ * This is the dialog opened by menu File | New account | Mail... .
+ *
+ * It gets the user's realname, email address and password,
+ * and tries to automatically configure the account from that,
+ * using various mechanisms. If all fails, the user can enter/edit
+ * the config, then we create the account.
+ *
+ * Steps:
+ * - User enters realname, email address and password
+ * - check for config files on disk
+ * (shipping with Thunderbird, for enterprise deployments)
+ * - (if fails) try to get the config file from the ISP via a
+ * fixed URL on the domain of the email address
+ * - (if fails) try to get the config file from our own database
+ * at MoMo servers, maintained by the community
+ * - (if fails) try to guess the config, by guessing hostnames,
+ * probing ports, checking config via server's CAPS line etc..
+ * - verify the setup, by trying to login to the configured servers
+ * - let user verify and maybe edit the server names and ports
+ * - If user clicks OK, create the account
+ */
+
+// Keep track of the prefers-reduce-motion media query for JS based animations.
+var gReducedMotion;
+
+// The main 3 Pane Window that we need to define on load in order to properly
+// update the UI when a new account is created.
+var gMainWindow;
+
+// Define standard incoming port numbers.
+var gStandardPorts = {
+ imap: [143, 993],
+ pop3: [110, 995],
+ smtp: [587, 25, 465], // order matters
+ exchange: [443],
+};
+
+// Store all ports into a flat array for greppability.
+var gAllStandardPorts = gStandardPorts.smtp
+ .concat(gStandardPorts.imap)
+ .concat(gStandardPorts.pop3)
+ .concat(gStandardPorts.exchange);
+
+// Define window event listeners.
+window.addEventListener("load", () => {
+ gAccountSetup.onLoad();
+});
+window.addEventListener("unload", () => {
+ gAccountSetup.onUnload();
+});
+
+function onSetupComplete() {
+ // Post a message to the main window at the end of a successful account setup.
+ gMainWindow.postMessage("account-created", "*");
+}
+
+/**
+ * Prompt a native HTML confirmation dialog for the Exchange auto discover.
+ *
+ * @param {string} domain - Text with the question.
+ * @param {Function} okCallback - Called when the user clicks OK.
+ * @param {function(ex)} cancelCallback - Called when the user clicks Cancel
+ * or if you call `Abortable.cancel()`.
+ * @returns {Abortable} - If `Abortable.cancel()` is called,
+ * the dialog is closed and the `cancelCallback()` is called.
+ */
+function confirmExchange(domain, okCallback, cancelCallback) {
+ let dialog = document.getElementById("exchangeDialog");
+
+ document.l10n.setAttributes(
+ document.getElementById("exchangeDialogQuestion"),
+ "exchange-dialog-question",
+ {
+ domain,
+ }
+ );
+
+ document.getElementById("exchangeDialogConfirmButton").onclick = () => {
+ dialog.close();
+ okCallback();
+ };
+
+ document.getElementById("exchangeDialogCancelButton").onclick = () => {
+ dialog.close();
+ cancelCallback(new UserCancelledException());
+ };
+
+ // Show the dialog.
+ dialog.showModal();
+
+ let abortable = new Abortable();
+ abortable.cancel = ex => {
+ dialog.close();
+ cancelCallback(ex);
+ };
+ return abortable;
+}
+
+/**
+ * This is our controller for the entire account setup workflow.
+ */
+var gAccountSetup = {
+ // Boolean attribute to keep track of the initialization status of the wizard.
+ isInited: false,
+ // Attribute to store methods to interrupt abortable operations like testing
+ // a server configuration or installing an add-on.
+ _abortable: null,
+
+ tabMonitor: {
+ monitorName: "accountSetupMonitor",
+
+ onTabTitleChanged() {},
+ onTabOpened() {},
+ onTabPersist() {},
+ onTabRestored() {},
+ onTabClosing(tab) {
+ if (tab?.urlbar?.value == "about:accountsetup") {
+ gMainWindow?.postMessage("account-setup-dismissed", "*");
+ }
+ },
+ onTabSwitched() {},
+ },
+
+ /**
+ * Initialize the main notification box for the account setup process.
+ */
+ get notificationBox() {
+ if (!this._notificationBox) {
+ this._notificationBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "bottom");
+ document.getElementById("accountSetupNotifications").append(element);
+ });
+ }
+ return this._notificationBox;
+ },
+
+ /**
+ * Initialize the notification box for the calendar and address book sync
+ * process at the end of the account setup.
+ */
+ get syncingBox() {
+ if (!this._syncingBox) {
+ this._syncingBox = new MozElements.NotificationBox(element => {
+ element.setAttribute("notificationside", "bottom");
+ document.getElementById("syncNotifications").append(element);
+ });
+ }
+ return this._syncingBox;
+ },
+
+ clearNotifications() {
+ this.notificationBox.removeAllNotifications();
+ },
+
+ onLoad() {
+ // Bail out if it was already initialized.
+ if (this.isInited) {
+ return;
+ }
+
+ gAccountSetupLogger.debug("Initializing setup wizard");
+ gReducedMotion = window.matchMedia(
+ "(prefers-reduced-motion: reduce)"
+ ).matches;
+
+ // Store the main window.
+ gMainWindow = Services.wm.getMostRecentWindow("mail:3pane");
+
+ // this._currentConfig is the config we got either from the XML file or from
+ // guessing or from the user. Unless it's from the user, it contains
+ // placeholders like %EMAILLOCALPART% in username and other fields.
+ //
+ // The config here must retain these placeholders, to be able to adapt when
+ // the user enters a different realname, or password or email local part.
+ // A change of the domain name will trigger a new detection anyways. That
+ // means, before you actually use the config (e.g. to create an account or
+ // to show it to the user), you need to run replaceVariables().
+ this._currentConfig = null;
+ this._domain = "";
+ this._hostname = "";
+ this._email = "";
+ this._realname = "";
+ if ("@mozilla.org/userinfo;1" in Cc) {
+ let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo);
+ // Assume that it's a genuine full name if it includes a space.
+ if (userInfo.fullname.includes(" ")) {
+ this._realname = userInfo.fullname;
+ document.getElementById("realname").value = this._realname;
+ }
+ }
+
+ this._password = "";
+ // Keep track of the state of the password field, if password or clear text.
+ this._showPassword = false;
+ // This is used only for Exchange AutoDiscover and only if needed.
+ this._exchangeUsername = "";
+ // Store the successful callback in this attribute so we can send it around
+ // the various validation methods.
+ this._okCallback = onSetupComplete;
+ this._msgWindow = gMainWindow.msgWindow;
+
+ // If the account provisioner is preffed off, don't display the account
+ // provisioner button.
+ if (!Services.prefs.getBoolPref("mail.provider.enabled")) {
+ document.getElementById("provisionerButton").hidden = true;
+ }
+
+ // Disable the remember password checkbox if the pref is false.
+ if (!Services.prefs.getBoolPref("signon.rememberSignons")) {
+ let passwordCheckbox = document.getElementById("rememberPassword");
+ passwordCheckbox.checked = false;
+ passwordCheckbox.disabled = true;
+ }
+
+ // Ensure the cursor is on the first input field.
+ document.getElementById("realname").focus();
+
+ // In a new profile, the first request to live.thunderbird.net is much
+ // slower because of one-time overheads like DNS and OCSP. Let's create some
+ // dummy requests to prime the connections.
+ let autoconfigURL = Services.prefs.getCharPref("mailnews.auto_config_url");
+ fetch(autoconfigURL, { method: "OPTIONS" }).catch(console.error);
+
+ let addonsURL = Services.prefs.getCharPref(
+ "mailnews.auto_config.addons_url"
+ );
+ if (new URL(autoconfigURL).origin != new URL(addonsURL).origin) {
+ fetch(addonsURL, { method: "OPTIONS" }).catch(console.error);
+ }
+
+ // The tab monitor will inform us when this tab is getting closed.
+ gMainWindow.document
+ .getElementById("tabmail")
+ .registerTabMonitor(this.tabMonitor);
+
+ // We did everything, now we can update the variable.
+ this.isInited = true;
+ gAccountSetupLogger.debug("Account setup tab loaded.");
+
+ UIDensity.registerWindow(window);
+ UIFontSize.registerWindow(window);
+ },
+
+ /**
+ * Changes the window configuration to the different modes we have.
+ * Shows/hides various window parts and buttons.
+ *
+ * @param {string} modename
+ * "start" : Just the realname, email address, password fields
+ * "find-config" : detection step, adds the loading notification
+ * "result" : We found a config and display it to the user.
+ * The user may create the account.
+ * "manual-edit" : The user wants (or needs) to manually enter their
+ * the server hostname and other settings. We'll use them as provided.
+ * Additionally, there are the following sub-modes which can be entered after
+ * you entered the main mode:
+ * "manual-edit-have-hostname" : user entered a hostname for both servers
+ * that we can use
+ * "manual-edit-testing" : User pressed the [Re-test] button and
+ * we're currently detecting the "Auto" values
+ * "manual-edit-complete" : user entered (or we tested) all necessary
+ * values, and we're ready to create to account
+ * Currently, this doesn't cover the warning dialogs etc.. It may later.
+ */
+ switchToMode(modename) {
+ // Bail out if we requested the same mode we're currently viewing.
+ if (modename == this._currentModename) {
+ return;
+ }
+
+ this._currentModename = modename;
+ gAccountSetupLogger.debug(`switching to UI mode ${modename}`);
+
+ let continueButton = document.getElementById("continueButton");
+ let createButton = document.getElementById("createButton");
+ let reTestButton = document.getElementById("reTestButton");
+ let autoconfigDesc = document.getElementById("manualConfigDescription");
+ let setupTitle = document.getElementById("accountSetupTitle");
+
+ switch (modename) {
+ case "start":
+ this.clearNotifications();
+ document.getElementById("setupView").hidden = false;
+ document.getElementById("successView").hidden = true;
+
+ document.l10n.setAttributes(setupTitle, "account-setup-title");
+ setupTitle.classList.remove("success");
+ document.l10n.setAttributes(
+ document.getElementById("accountSetupDescription"),
+ "account-setup-description"
+ );
+
+ document.getElementById("resultsArea").hidden = true;
+ document.getElementById("manualConfigArea").hidden = true;
+ document.getElementById("manualConfigButton").hidden = true;
+ document.getElementById("stopButton").hidden = true;
+
+ reTestButton.hidden = true;
+ autoconfigDesc.hidden = true;
+ createButton.hidden = true;
+ continueButton.disabled = true;
+ continueButton.hidden = false;
+ break;
+ case "find-config":
+ document.getElementById("resultsArea").hidden = true;
+ document.getElementById("manualConfigArea").hidden = true;
+ document.getElementById("manualConfigButton").hidden = true;
+ document.getElementById("stopButton").hidden = false;
+
+ reTestButton.hidden = true;
+ autoconfigDesc.hidden = true;
+ createButton.hidden = true;
+ continueButton.disabled = true;
+ continueButton.hidden = false;
+ this.onStop = this.onStopFindConfig;
+ break;
+ case "result":
+ document.getElementById("manualConfigArea").hidden = true;
+ document.getElementById("stopButton").hidden = true;
+ document.getElementById("resultsArea").hidden = false;
+ document.getElementById("manualConfigButton").hidden = false;
+
+ reTestButton.hidden = true;
+ autoconfigDesc.hidden = true;
+ continueButton.hidden = true;
+ createButton.hidden = false;
+ createButton.disabled = false;
+ break;
+ case "manual-edit":
+ document.getElementById("resultsArea").hidden = true;
+ document.getElementById("stopButton").hidden = true;
+ document.getElementById("manualConfigButton").hidden = true;
+ document.getElementById("manualConfigArea").hidden = false;
+
+ continueButton.hidden = true;
+ reTestButton.hidden = false;
+ autoconfigDesc.hidden = false;
+ reTestButton.disabled = true;
+ createButton.hidden = false;
+ createButton.disabled = true;
+ break;
+ case "manual-edit-have-hostname":
+ document.getElementById("resultsArea").hidden = true;
+ document.getElementById("stopButton").hidden = true;
+ document.getElementById("manualConfigButton").hidden = true;
+ document.getElementById("manualConfigArea").hidden = false;
+
+ reTestButton.hidden = false;
+ autoconfigDesc.hidden = false;
+ reTestButton.disabled = false;
+ continueButton.hidden = true;
+ createButton.hidden = false;
+ createButton.disabled = true;
+ break;
+ case "manual-edit-testing":
+ document.getElementById("resultsArea").hidden = true;
+ document.getElementById("manualConfigArea").hidden = false;
+ document.getElementById("manualConfigButton").hidden = true;
+ document.getElementById("stopButton").hidden = false;
+
+ reTestButton.hidden = false;
+ autoconfigDesc.hidden = false;
+ reTestButton.disabled = true;
+ continueButton.hidden = true;
+ createButton.hidden = false;
+ createButton.disabled = true;
+
+ this.onStop = this.onStopHalfManualTesting;
+ break;
+ case "manual-edit-complete":
+ document.getElementById("resultsArea").hidden = true;
+ document.getElementById("manualConfigArea").hidden = false;
+ document.getElementById("manualConfigButton").hidden = true;
+ document.getElementById("stopButton").hidden = true;
+
+ reTestButton.hidden = false;
+ autoconfigDesc.hidden = false;
+ reTestButton.disabled = false;
+ continueButton.hidden = true;
+ createButton.disabled = false;
+ createButton.hidden = false;
+
+ document.getElementById("incomingProtocol").focus();
+ break;
+ case "success":
+ document.getElementById("setupView").hidden = true;
+ document.getElementById("successView").hidden = false;
+
+ document.l10n.setAttributes(setupTitle, "account-setup-success-title");
+ setupTitle.classList.add("success");
+ document.l10n.setAttributes(
+ document.getElementById("accountSetupDescription"),
+ "account-setup-success-description"
+ );
+ document.l10n.setAttributes(
+ document.getElementById("accountSetupDescriptionSecondary"),
+ "account-setup-success-secondary-description"
+ );
+ break;
+ default:
+ throw new NotReached("Unknown mode requested");
+ }
+
+ // If we're offline, we're going to disable the create button, but enable
+ // the advanced config button if we have a current config.
+ if (Services.io.offline && !this._currentConfig) {
+ document.getElementById("manualConfigButton").hidden = true;
+ reTestButton.hidden = true;
+ autoconfigDesc.hidden = true;
+ createButton.hidden = true;
+ }
+ },
+
+ /**
+ * Reset the form and the entire UI of the account setup.
+ */
+ resetSetup() {
+ document.getElementById("form").reset();
+ document.getElementById("realname").focus();
+ // Call onStartOver only after resetting the form in order to properly
+ // update the form buttons.
+ this.onStartOver();
+ },
+
+ /**
+ * Start from beginning with possibly new email address.
+ */
+ onStartOver() {
+ this._currentConfig = null;
+ if (this._abortable) {
+ this.onStop();
+ }
+ this.switchToMode("start");
+ this.checkValidForm();
+ },
+
+ getConcreteConfig() {
+ let result = this._currentConfig.copy();
+
+ AccountConfig.replaceVariables(
+ result,
+ this._realname,
+ this._email,
+ this._password
+ );
+ result.rememberPassword =
+ document.getElementById("rememberPassword").checked && !!this._password;
+
+ if (result.incoming.addonAccountType) {
+ result.incoming.type = result.incoming.addonAccountType;
+ }
+
+ return result;
+ },
+
+ /**
+ * onInputEmail and onInputRealname are called on input = keypresses, and
+ * enable/disable the next button based on whether there's a semi-proper
+ * e-mail address and non-blank realname to start with.
+ *
+ * A change to the email address also automatically restarts the
+ * whole process.
+ */
+ onInputEmail() {
+ this._email = document.getElementById("email").value;
+ this.onStartOver();
+ },
+
+ onInputRealname() {
+ this._realname = document.getElementById("realname").value;
+ this.checkValidForm();
+ },
+
+ onInputUsername() {
+ this._exchangeUsername = document.getElementById("usernameEx").value;
+ this.onStartOver();
+ },
+
+ onInputPassword() {
+ this._password = document.getElementById("password").value;
+ this.onStartOver();
+
+ // Show the password toggle button only if the field is not empty.
+ let toggleButton = document.getElementById("passwordToggleButton");
+ toggleButton.hidden = !this._password;
+
+ if (!this._password) {
+ // Always reset the field to the proper type.
+ this.hidePassword();
+ }
+ },
+
+ /**
+ * Toggle the type of the password field between password and text to allow
+ * users reading their own password.
+ */
+ passwordToggle(event) {
+ // Prevent the form submission if the user presses Enter.
+ event.preventDefault();
+
+ // The password field is in plain text, change it back to the proper type.
+ if (this._showPassword) {
+ this.hidePassword();
+ return;
+ }
+
+ // Change the password field to plain text to make the text visible.
+ this.showPassword();
+ },
+
+ /**
+ * Convert the password field into a plain text field allowing users and
+ * assistive technologies to read the typed text.
+ */
+ showPassword() {
+ document.getElementById("password").type = "text";
+ document.l10n.setAttributes(
+ document.getElementById("passwordToggleButton"),
+ "account-setup-password-toggle-hide"
+ );
+
+ let toggleImage = document.getElementById("passwordInfo");
+ toggleImage.src = "chrome://messenger/skin/icons/new/compact/eye.svg";
+ toggleImage.classList.add("password-toggled");
+
+ this._showPassword = true;
+ },
+
+ /**
+ * Convert the password field back to its default password type.
+ */
+ hidePassword() {
+ // No need to reset anything if the password was never shown.
+ if (!this._showPassword) {
+ return;
+ }
+
+ document.getElementById("password").type = "password";
+ document.l10n.setAttributes(
+ document.getElementById("passwordToggleButton"),
+ "account-setup-password-toggle-show"
+ );
+
+ let toggleImage = document.getElementById("passwordInfo");
+ toggleImage.src = "chrome://messenger/skin/icons/new/compact/hidden.svg";
+ toggleImage.classList.remove("password-toggled");
+
+ this._showPassword = false;
+ },
+
+ /**
+ * Check whether the user entered the minimum amount of information needed to
+ * leave the "start" mode (name and email) and is allowed to proceed to the
+ * detection step.
+ */
+ checkValidForm() {
+ let email = document.getElementById("email");
+ let isValidForm =
+ email.checkValidity() &&
+ document.getElementById("realname").checkValidity();
+ this._domain = isValidForm ? this._email.split("@")[1].toLowerCase() : "";
+
+ document.getElementById("continueButton").disabled = !isValidForm;
+ document.getElementById("manualConfigButton").hidden = !isValidForm;
+ document.getElementById("provisionerButton").hidden = email.value;
+ },
+
+ /**
+ * When the [Continue] button is clicked, we move from the initial account
+ * information stage to using that information to configure account details.
+ */
+ onContinue() {
+ this.findConfig(this._domain, this._email);
+ },
+
+ // --------------
+ // Detection step
+
+ /**
+ * Try to find an account configuration for this email address.
+ * This is the function which runs the autoconfig.
+ */
+ findConfig(domain, emailAddress) {
+ gAccountSetupLogger.debug("findConfig()");
+ if (this._abortable) {
+ this.onStop();
+ }
+ this.switchToMode("find-config");
+ this.startLoadingState("account-setup-looking-up-settings");
+
+ let self = this;
+ let call = null;
+ let fetch = null;
+
+ let priority = (this._abortable = new PriorityOrderAbortable(
+ function (config, call) {
+ // success
+ self._abortable = null;
+ self.stopLoadingState(call.foundMsg);
+ self.foundConfig(config);
+ },
+ function (e, allErrors) {
+ // all failed
+ if (e instanceof CancelledException) {
+ self.onStartOver();
+ return;
+ }
+
+ // guess config
+ let initialConfig = new AccountConfig();
+ self._prefillConfig(initialConfig);
+ self._guessConfig(domain, initialConfig);
+ }
+ ));
+
+ try {
+ call = priority.addCall();
+ gAccountSetupLogger.debug(
+ "Looking up configuration: Thunderbird installation…"
+ );
+ call.foundMsg = "account-setup-success-settings-disk";
+ fetch = FetchConfig.fromDisk(
+ domain,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+
+ call = priority.addCall();
+ gAccountSetupLogger.debug("Looking up configuration: Email provider…");
+ call.foundMsg = "account-setup-success-settings-isp";
+ fetch = FetchConfig.fromISP(
+ domain,
+ emailAddress,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+
+ call = priority.addCall();
+ gAccountSetupLogger.debug(
+ "Looking up configuration: Thunderbird installation…"
+ );
+ call.foundMsg = "account-setup-success-settings-db";
+ fetch = FetchConfig.fromDB(
+ domain,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+
+ call = priority.addCall();
+ gAccountSetupLogger.debug(
+ "Looking up configuration: Incoming mail domain…"
+ );
+ // "account-setup-success-settings-db" is correct.
+ // We display the same message for both db and mx cases.
+ call.foundMsg = "account-setup-success-settings-db";
+ fetch = FetchConfig.forMX(
+ domain,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+
+ call = priority.addCall();
+ gAccountSetupLogger.debug("Looking up configuration: Exchange server…");
+ call.foundMsg = "account-setup-success-settings-exchange";
+ fetch = fetchConfigFromExchange(
+ domain,
+ emailAddress,
+ this._exchangeUsername,
+ this._password,
+ confirmExchange,
+ call.successCallback(),
+ (e, allErrors) => {
+ // Must call error callback in any case to stop the discover mode.
+ let errorCallback = call.errorCallback();
+ if (e instanceof CancelledException) {
+ errorCallback(e);
+ } else if (allErrors && allErrors.some(e => e.code == 401)) {
+ // Auth failed.
+ // Ask user for username.
+ this.onStartOver();
+ this.stopLoadingState(); // clears status message
+ document.getElementById("usernameRow").hidden = false;
+
+ this.showErrorNotification(
+ !this._exchangeUsername
+ ? "account-setup-credentials-incomplete"
+ : "account-setup-credentials-wrong"
+ );
+ document.getElementById("manualConfigButton").hidden = false;
+ errorCallback(new CancelledException());
+ } else {
+ errorCallback(e);
+ }
+ }
+ );
+ call.setAbortable(fetch);
+ } catch (e) {
+ this.onStop();
+ // e.g. when entering an invalid domain like "c@c.-com"
+ this.showErrorNotification(e, true);
+ }
+ },
+
+ /**
+ * Just a continuation of findConfig()
+ */
+ _guessConfig(domain, initialConfig) {
+ this.startLoadingState("account-setup-looking-up-settings-guess");
+ let self = this;
+ self._abortable = GuessConfig.guessConfig(
+ domain,
+ function (type, hostname, port, socketType, done, config) {
+ // progress
+ gAccountSetupLogger.debug(
+ `${hostname}:${port} socketType=${socketType} ${type}: progress callback`
+ );
+ },
+ function (config) {
+ // success
+ self._abortable = null;
+ self.foundConfig(config);
+ self.stopLoadingState(
+ Services.io.offline
+ ? "account-setup-success-guess-offline"
+ : "account-setup-success-guess"
+ );
+ },
+ function (e, config) {
+ // guessconfig failed
+ if (e instanceof CancelledException) {
+ return;
+ }
+ self._abortable = null;
+ gAccountSetupLogger.warn(`guessConfig failed: ${e}`);
+ self.showErrorNotification("account-setup-find-settings-failed");
+ self.editConfigDetails();
+ },
+ initialConfig,
+ "both"
+ );
+ },
+
+ /**
+ * Called after findConfig() is successful and displays the data to the user.
+ *
+ * @param {AccountConfig} config - The config to present to the user.
+ */
+ foundConfig(config) {
+ gAccountSetupLogger.debug("found config:\n" + config);
+ assert(
+ config instanceof AccountConfig,
+ "BUG: Arg 'config' needs to be an AccountConfig object"
+ );
+
+ this._haveValidConfigForDomain = this._email.split("@")[1];
+
+ // Bail out if the name and email fields are empty.
+ if (!this._realname || !this._email) {
+ return;
+ }
+
+ config.addons = [];
+ let successCallback = () => {
+ this._abortable = null;
+ this.displayConfigResult(config);
+ this.switchToMode("result");
+ this.ensureVisibleButtons();
+ };
+ this._abortable = getAddonsList(config, successCallback, e => {
+ successCallback();
+ this.showErrorNotification(e, true);
+ });
+ },
+
+ /**
+ * [Stop] button click handler.
+ * This allows the user to abort any longer operation, esp. network activity.
+ * We currently have 3 such cases here:
+ * 1. findConfig(), i.e. fetch config from DB, guessConfig etc.
+ * 2. testManualConfig(), i.e. the [Retest] button in manual config.
+ * 3. verifyConfig() - We can't stop this yet, so irrelevant here currently.
+ * Given that these need slightly different actions, this function will be set
+ * to a function (i.e. overwritten) by whoever enables the stop button.
+ *
+ * We also call this from the code when the user started a different action
+ * without explicitly clicking [Stop] for the old one first.
+ */
+ onStop() {
+ throw new NotReached("onStop should be overridden by now");
+ },
+
+ _onStopCommon() {
+ if (!this._abortable) {
+ throw new NotReached("onStop called although there's nothing to stop");
+ }
+ gAccountSetupLogger.debug("onStop cancelled _abortable");
+ this._abortable.cancel(new UserCancelledException());
+ this._abortable = null;
+ this.stopLoadingState();
+ },
+
+ onStopFindConfig() {
+ this._onStopCommon();
+ this.switchToMode("start");
+ this.checkValidForm();
+ },
+
+ onStopHalfManualTesting() {
+ this._onStopCommon();
+ this.validateManualEditComplete();
+ },
+
+ // ----------- Loading area -----------
+ /**
+ * Disable all the input fields of the main form to prevent editing and show
+ * a notification while a loading or fetching state.
+ *
+ * @param {string} stringName - The name of the fluent string that needs to be
+ * attached to the notification.
+ */
+ async startLoadingState(stringName) {
+ gAccountSetupLogger.debug(`Loading start: ${stringName}`);
+
+ this.showHelperImage("step2");
+
+ // Disable all input fields.
+ for (let input of document.querySelectorAll("#form input")) {
+ input.disabled = true;
+ }
+
+ let notificationMessage = await document.l10n.formatValue(stringName);
+
+ gAccountSetupLogger.debug(`Status msg: ${notificationMessage}`);
+
+ let notification = this.notificationBox.getNotificationWithValue(
+ "accountSetupLoading"
+ );
+
+ // If a notification already exists, simply update the message.
+ if (notification) {
+ notification.label = notificationMessage;
+ this.ensureVisibleNotification();
+ return;
+ }
+
+ notification = this.notificationBox.appendNotification(
+ "accountSetupLoading",
+ {
+ label: notificationMessage,
+ priority: this.notificationBox.PRIORITY_INFO_LOW,
+ },
+ null
+ );
+ notification.setAttribute("align", "center");
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.ensureVisibleNotification();
+ },
+
+ /**
+ * Update the text of a currently visible loading notification
+ *
+ * @param {string} stringName - The name of the fluent string that needs to be
+ * attached to the notification.
+ */
+ async updateLoadingState(stringName) {
+ let notification = this.notificationBox.getNotificationWithValue(
+ "accountSetupLoading"
+ );
+ // If a notification doesn't already exist, create one.
+ if (!notification) {
+ this.startLoadingState(stringName);
+ return;
+ }
+
+ let notificationMessage = await document.l10n.formatValue(stringName);
+ notification.label = notificationMessage;
+ this.ensureVisibleNotification();
+
+ gAccountSetupLogger.debug(`Status msg: ${notificationMessage}`);
+ },
+
+ /**
+ * Clear the loading notification and show a successful notification if
+ * needed.
+ *
+ * @param {?string} stringName - The name of the fluent string that needs to
+ * be attached to the notification, or null if nothing needs to be showed.
+ */
+ async stopLoadingState(stringName) {
+ // Re-enable all form input fields.
+ for (let input of document.querySelectorAll("#form input")) {
+ input.removeAttribute("disabled");
+ }
+
+ // Always remove any leftover notification.
+ this.clearNotifications();
+
+ // Bail out if we don't need to show anything else.
+ if (!stringName) {
+ gAccountSetupLogger.debug("Loading stopped");
+ this.showHelperImage("step1");
+ return;
+ }
+
+ gAccountSetupLogger.debug(`Loading stopped: ${stringName}`);
+
+ let notificationMessage = await document.l10n.formatValue(stringName);
+
+ let notification = this.notificationBox.appendNotification(
+ "accountSetupSuccess",
+ {
+ label: notificationMessage,
+ priority: this.notificationBox.PRIORITY_INFO_HIGH,
+ },
+ null
+ );
+ notification.setAttribute("type", "success");
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.showHelperImage("step3");
+ },
+
+ /**
+ * Show an error notification in case something went wrong.
+ *
+ * @param {string} stringName - The name of the fluent string that needs to
+ * be attached to the notification.
+ * @param {boolean} isMsgError - True if the message comes from a server error
+ * response or try/catch.
+ */
+ async showErrorNotification(stringName, isMsgError) {
+ gAccountSetupLogger.debug(`Status error: ${stringName}`);
+
+ this.showHelperImage("step4");
+
+ // Re-enable all form input fields.
+ for (let input of document.querySelectorAll("#form input")) {
+ input.removeAttribute("disabled");
+ }
+
+ // Always remove any leftover notification before creating a new one.
+ this.clearNotifications();
+
+ // Fetch the fluent string only if this is not an error message coming from
+ // a previous method.
+ let notificationMessage = isMsgError
+ ? stringName
+ : await document.l10n.formatValue(stringName);
+
+ let notification = this.notificationBox.appendNotification(
+ "accountSetupError",
+ {
+ label: notificationMessage,
+ priority: this.notificationBox.PRIORITY_WARNING_MEDIUM,
+ },
+ null
+ );
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ this.ensureVisibleNotification();
+ },
+
+ /**
+ * Hide all the helper images and show the requested one.
+ *
+ * @param {string} id - The string ID of the element to show.
+ */
+ showHelperImage(id) {
+ // Hide all currently visible articles containing helper images in the
+ // second column.
+ for (let article of document.querySelectorAll(
+ ".second-column article:not([hidden])"
+ )) {
+ article.hidden = true;
+ }
+
+ // Simply show the requested helper image if the user specified a reduced
+ // motion preference.
+ if (gReducedMotion) {
+ document.getElementById(id).hidden = false;
+ return;
+ }
+
+ // Handle a nice cross fade between steps.
+ let stepToShow = document.getElementById(id);
+ // Add the class to let the revealing element start from a proper state.
+ stepToShow.classList.add("hide");
+ stepToShow.hidden = false;
+ // Timeout to animate after the hidden attribute has been removed.
+ setTimeout(() => {
+ stepToShow.classList.remove("hide");
+ });
+ },
+
+ /**
+ * Always ensure the primary button is visible by scrolling the page until the
+ * button is above the fold.
+ */
+ ensureVisibleButtons() {
+ // We use the #footDescription element to ensure the buttons are properly
+ // scrolled above the fold.
+ document.getElementById("footDescription").scrollIntoView({
+ behavior: gReducedMotion ? "auto" : "smooth",
+ block: "end",
+ inline: "nearest",
+ });
+ },
+
+ /**
+ * Always ensure the notification area is visible when a new notification is
+ * created.
+ */
+ ensureVisibleNotification() {
+ document.getElementById("accountSetupNotifications").scrollIntoView({
+ behavior: gReducedMotion ? "auto" : "smooth",
+ block: "start",
+ inline: "nearest",
+ });
+ },
+
+ /**
+ * Populate the results config details area.
+ *
+ * @param {AccountConfig} config - The config to present to user.
+ */
+ displayConfigResult(config) {
+ assert(config instanceof AccountConfig);
+ this._currentConfig = config;
+ let configFilledIn = this.getConcreteConfig();
+
+ // Filter out Protcols we don't currently support
+ let protocols = config.incomingAlternatives.filter(protocol =>
+ ["imap", "pop3", "exchange"].includes(protocol.type)
+ );
+ protocols.unshift(config.incoming);
+ protocols = protocols.reduce((found, nextEl) => {
+ if (!found.some(prevEl => prevEl.type == nextEl.type)) {
+ found.push(nextEl);
+ }
+ return found;
+ }, []);
+
+ // Hide all the available options in order to start with a clean slate.
+ for (let row of document.querySelectorAll(".content-blocking-category")) {
+ row.classList.remove("selected");
+ row.hidden = true;
+ }
+
+ // Remove all previously generated protocol types.
+ for (let type of document.querySelectorAll(".config-type")) {
+ type.remove();
+ }
+
+ // Reveal all the matching protocols.
+ for (let protocol of protocols) {
+ let row = document.getElementById(`resultsOption-${protocol.type}`);
+ row.hidden = false;
+ // Attach the protocol to the radio input for later usage.
+ row.querySelector(`input[type="radio"]`).configIncoming = protocol;
+ }
+
+ // Preselect the default protocol type.
+ let selected = document.getElementById(
+ `resultSelect-${config.incoming.type}`
+ );
+ selected.closest(".content-blocking-category").classList.add("selected");
+ selected.checked = true;
+
+ // Update the results area title to match the protocols choice.
+ document.l10n.setAttributes(
+ document.getElementById("resultAreaTitle"),
+ "account-setup-results-area-title",
+ {
+ count: protocols.length,
+ }
+ );
+
+ // Ensure by default the "Done" button is enabled.
+ document.getElementById("createButton").disabled = false;
+
+ // Thunderbird can't handle Exchange server independently, therefore we
+ // need to prompt the user with the installation of the Owl add-on.
+ if (config.incoming.type == "exchange") {
+ let addonsInstallRows = document.getElementById("resultAddonInstallRows");
+
+ // Remove any pre-existing child element.
+ while (addonsInstallRows.hasChildNodes()) {
+ addonsInstallRows.lastChild.remove();
+ }
+
+ let container = document.getElementById("resultExchangeHostname");
+ _makeHostDisplayString(config.incoming, container);
+ document
+ .getElementById("incomingTitle-exchange")
+ .appendChild(_socketTypeSpan(config.incoming.socketType));
+
+ (async () => {
+ try {
+ for (let addon of config.addons) {
+ let installer = new AddonInstaller(addon);
+ addon.isInstalled = await installer.isInstalled();
+ addon.isDisabled = await installer.isDisabled();
+ }
+
+ let addonInfoArea = document.getElementById("installAddonInfo");
+ let installedAddon = config.addons.find(addon => addon.isInstalled);
+
+ // The needed add-on is already installed, no need to show anything.
+ if (installedAddon) {
+ config.incoming.addonAccountType =
+ installedAddon.useType.addonAccountType;
+ addonInfoArea.hidden = true;
+ return;
+ }
+ // Disable "Done" until add-on is installed, or some other protocol
+ // is selected.
+ document.getElementById("createButton").disabled = true;
+
+ addonInfoArea.hidden = false;
+
+ document.l10n.setAttributes(
+ document.getElementById("resultAddonIntro"),
+ !config.incomingAlternatives.find(alt =>
+ ["imap", "pop3"].includes(alt.type)
+ )
+ ? "account-setup-addon-install-intro"
+ : "account-setup-addon-no-protocol"
+ );
+
+ for (let addon of config.addons) {
+ // Creates and addon install section.
+ // <div><img/><a></a><button></button></div>
+ let container = document.createElement("div");
+ container.classList.add("addon-container");
+
+ let img = document.createElement("img");
+ img.alt = "";
+ img.classList.add("icon");
+ if (addon.icon32) {
+ img.setAttribute("src", addon.icon32);
+ }
+
+ let link = document.createElement("a");
+ link.classList.add("link");
+ link.setAttribute("href", addon.websiteURL);
+ link.textContent = addon.description;
+
+ let button = document.createElement("button");
+ document.l10n.setAttributes(
+ button,
+ "account-setup-addon-install-title"
+ );
+ button.addEventListener("click", () => {
+ gAccountSetup.addonInstall(addon);
+ });
+ if (addon.isDisabled) {
+ // If the add on is disabled by user, or due to incompatibility
+ // - disable install (it won't help, it's already installed)
+ // - link to the addons manager instead (so they can fix it)
+ button.disabled = true;
+ link.setAttribute("href", "about:addons");
+ link.setAttribute("target", "_blank");
+
+ // Trigger an add-on update check. If an update is available,
+ // enable the install button to (re)install.
+ AddonManager.getAddonByID(addon.id).then(a => {
+ if (!a) {
+ return;
+ }
+ let listener = {
+ onUpdateAvailable(addon, install) {
+ button.disabled = false;
+ },
+ onNoUpdateAvailable() {},
+ };
+ a.findUpdates(
+ listener,
+ AddonManager.UPDATE_WHEN_USER_REQUESTED
+ );
+ });
+ }
+
+ container.appendChild(img);
+ container.appendChild(link);
+ container.appendChild(button);
+
+ addonsInstallRows.appendChild(container);
+ }
+ } catch (e) {
+ this.showErrorNotification(e, true);
+ }
+ })();
+ return;
+ }
+
+ function _makeHostDisplayString(server, container) {
+ // Clean up any existing element.
+ while (container.hasChildNodes()) {
+ container.lastChild.remove();
+ }
+
+ let cert = container.parentNode.querySelector(".cert-status");
+ if (cert != null) {
+ cert.remove();
+ }
+
+ let domain = server.hostname;
+ try {
+ domain = Services.eTLD.getBaseDomainFromHost(server.hostname);
+ } catch (ex) {
+ gAccountSetupLogger.warn(ex);
+ }
+
+ let hostSpan = document.createElement("span");
+ hostSpan.classList.add("host-without-domain");
+ hostSpan.textContent = server.hostname.substr(
+ 0,
+ server.hostname.length - domain.length
+ );
+ container.appendChild(hostSpan);
+
+ let domainSpan = document.createElement("span");
+ domainSpan.classList.add("domain");
+ domainSpan.textContent = domain;
+ container.appendChild(domainSpan);
+
+ if (!gAllStandardPorts.includes(server.port)) {
+ let portSpan = document.createElement("span");
+ portSpan.classList.add("port");
+ portSpan.textContent = `:${server.port}`;
+ container.appendChild(portSpan);
+ }
+
+ if (server.badCert) {
+ container.parentNode
+ .querySelector(".cert-status")
+ .classList.add("insecure");
+ }
+ }
+
+ /**
+ * Helper method to create the span protocol element.
+ *
+ * @returns {HTMLElement} - The newly created span label.
+ */
+ function _protocolTypeSpan() {
+ let span = document.createElement("span");
+ span.classList.add("protocol-type", "config-type");
+ return span;
+ }
+
+ /**
+ * Helper method to create the span socket element.
+ *
+ * @param {nsMsgSocketType} socket - The value representing the server
+ * socket type.
+ * @returns {HTMLElement} - The newly created span label.
+ */
+ function _socketTypeSpan(socket) {
+ let ssl = Sanitizer.translate(socket, {
+ 0: "no-encryption",
+ 2: "starttls",
+ 3: "ssl",
+ });
+ let span = _protocolTypeSpan();
+ document.l10n.setAttributes(span, `account-setup-result-${ssl}`);
+ span.classList.add("ssl");
+ if (socket != 2 && socket != 3) {
+ // Not an SSL or STARTTLS socket.
+ span.classList.add("insecure");
+ }
+ return span;
+ }
+
+ let protocolType = config.incoming.type;
+ if (configFilledIn.incoming.hostname) {
+ _makeHostDisplayString(
+ configFilledIn.incoming,
+ document.getElementById(`incomingInfo-${protocolType}`)
+ );
+
+ let container = document.getElementById(`incomingTitle-${protocolType}`);
+
+ // No need to show the protocol type if it's exchange, and the socket span
+ // is generated somewhere else specifically for exchange.
+ if (protocolType != "exchange") {
+ let span = _protocolTypeSpan();
+ span.textContent = configFilledIn.incoming.type;
+ container.appendChild(span);
+ container.appendChild(_socketTypeSpan(config.incoming.socketType));
+ }
+ }
+
+ let outgoingInfo = document.getElementById(`outgoingInfo-${protocolType}`);
+ if (!config.outgoing.existingServerKey) {
+ if (configFilledIn.outgoing.hostname) {
+ _makeHostDisplayString(configFilledIn.outgoing, outgoingInfo);
+ }
+ let container = document.getElementById(`outgoingTitle-${protocolType}`);
+ // No need to show the protocol type if it's exchange, and the socket span
+ // is generated somewhere else specifically for exchange.
+ if (protocolType != "exchange") {
+ let span = _protocolTypeSpan();
+ span.textContent = configFilledIn.outgoing.type;
+ container.appendChild(span);
+ container.appendChild(_socketTypeSpan(config.outgoing.socketType));
+ }
+ } else {
+ let span = document.createElement("span");
+ document.l10n.setAttributes(
+ span,
+ "account-setup-result-outgoing-existing"
+ );
+ outgoingInfo.appendChild(span);
+ }
+
+ let usernameInfo = document.getElementById(
+ `usernameInfo-${config.incoming.type}`
+ );
+ if (configFilledIn.incoming.username == configFilledIn.outgoing.username) {
+ usernameInfo.textContent = configFilledIn.incoming.username;
+ } else {
+ document.l10n.setAttributes(
+ usernameInfo,
+ "account-setup-result-username-different",
+ {
+ incoming: configFilledIn.incoming.username,
+ outgoing: configFilledIn.outgoing.username,
+ }
+ );
+ }
+ },
+
+ /**
+ * Handle the user switching between IMAP and POP3 settings using the
+ * radio buttons.
+ */
+ onResultServerTypeChanged() {
+ let config = this._currentConfig;
+ // Add current server as best alternative to start of array.
+ config.incomingAlternatives.unshift(config.incoming);
+
+ // Clear the visually selected radio container.
+ document
+ .querySelector(".content-blocking-category.selected")
+ .classList.remove("selected");
+
+ // Use selected server (stored as special property on the <input> node).
+ let selected = document.querySelector(
+ 'input[name="resultsServerType"]:checked'
+ );
+ selected.closest(".content-blocking-category").classList.add("selected");
+ config.incoming = selected.configIncoming;
+
+ // Remove newly selected server from list of alternatives.
+ config.incomingAlternatives = config.incomingAlternatives.filter(
+ alt => alt != config.incoming
+ );
+ this.displayConfigResult(config);
+ },
+
+ /**
+ * Install the addon
+ * Called when user clicks [Install] button.
+ *
+ * @param {AddonInfo} addon - @see AccountConfig.addons
+ */
+ async addonInstall(addon) {
+ let addonInfoArea = document.getElementById("installAddonInfo");
+ let createButton = document.getElementById("createButton");
+ addonInfoArea.hidden = true;
+ createButton.disabled = true;
+
+ this.clearNotifications();
+ await this.startLoadingState("account-setup-installing-addon");
+
+ try {
+ let installer = (this._abortable = new AddonInstaller(addon));
+ await installer.install();
+
+ this._abortable = null;
+ this.stopLoadingState("account-setup-success-addon");
+ createButton.disabled = false;
+
+ this._currentConfig.incoming.addonAccountType =
+ addon.useType.addonAccountType;
+ // Remove the note about having to install an add-on.
+ let rows = document.getElementById("resultAddonInstallRows");
+ while (rows.lastChild) {
+ rows.lastChild.remove();
+ }
+ } catch (e) {
+ console.error(e);
+ this.showErrorNotification(e, true);
+ addonInfoArea.hidden = false;
+ }
+ },
+
+ // ----------------
+ // Manual Edit area
+
+ /**
+ * Gets the values from the user in the manual edit area. Realname and
+ * password are not part of that area and still placeholders, but hostname and
+ * username are concrete and no placeholders anymore.
+ */
+ getUserConfig() {
+ let config = this.getConcreteConfig() || new AccountConfig();
+ config.source = AccountConfig.kSourceUser;
+
+ // Incoming server
+ try {
+ let inHostnameField = document.getElementById("incomingHostname");
+ config.incoming.hostname = Sanitizer.hostname(inHostnameField.value);
+ inHostnameField.value = config.incoming.hostname;
+ } catch (e) {
+ gAccountSetupLogger.warn(e);
+ }
+
+ try {
+ config.incoming.port = Sanitizer.integerRange(
+ document.getElementById("incomingPort").value,
+ 1,
+ 65535
+ );
+ } catch (e) {
+ config.incoming.port = undefined; // incl. default "Auto"
+ }
+
+ config.incoming.type = Sanitizer.translate(
+ document.getElementById("incomingProtocol").value,
+ {
+ 1: "imap",
+ 2: "pop3",
+ 3: "exchange",
+ 0: null,
+ }
+ );
+ config.incoming.socketType = Sanitizer.integer(
+ document.getElementById("incomingSsl").value
+ );
+ config.incoming.auth = Sanitizer.integer(
+ document.getElementById("incomingAuthMethod").value
+ );
+ config.incoming.username =
+ document.getElementById("incomingUsername").value;
+
+ // Outgoing server
+
+ config.outgoing.username =
+ document.getElementById("outgoingUsername").value;
+
+ // The user specified a custom SMTP server.
+ config.outgoing.existingServerKey = null;
+ config.outgoing.addThisServer = true;
+ config.outgoing.useGlobalPreferredServer = false;
+
+ try {
+ let input = document.getElementById("outgoingHostname");
+ config.outgoing.hostname = Sanitizer.hostname(input.value);
+ input.value = config.outgoing.hostname;
+ } catch (e) {
+ gAccountSetupLogger.warn(e);
+ }
+
+ try {
+ config.outgoing.port = Sanitizer.integerRange(
+ document.getElementById("outgoingPort").value,
+ 1,
+ 65535
+ );
+ } catch (e) {
+ config.outgoing.port = undefined; // incl. default "Auto"
+ }
+
+ config.outgoing.socketType = Sanitizer.integer(
+ document.getElementById("outgoingSsl").value
+ );
+ config.outgoing.auth = Sanitizer.integer(
+ document.getElementById("outgoingAuthMethod").value
+ );
+
+ return config;
+ },
+
+ /**
+ * [Manual Config] button click handler. This turns the config details area
+ * into an editable form and makes the (Go) button appear. The edit button
+ * should only be available after the config probing is completely finished,
+ * replacing what was the (Stop) button.
+ */
+ onManualEdit() {
+ if (this._abortable) {
+ this.onStop();
+ }
+ this.editConfigDetails();
+ this.showHelperImage("step3");
+ },
+
+ /**
+ * Setting the config details form so it can be edited. We also disable
+ * (and hide) the create button during this time because we don't know what
+ * might have changed. The function called from the button that restarts
+ * the config check should be enabling the config button as needed.
+ */
+ editConfigDetails() {
+ gAccountSetupLogger.debug("manual edit");
+
+ if (!this._currentConfig) {
+ this._currentConfig = new AccountConfig();
+ this._currentConfig.incoming.type = "imap";
+ this._currentConfig.incoming.username = "%EMAILADDRESS%";
+ this._currentConfig.outgoing.username = "%EMAILADDRESS%";
+ this._currentConfig.incoming.hostname = ".%EMAILDOMAIN%";
+ this._currentConfig.outgoing.hostname = ".%EMAILDOMAIN%";
+ }
+ // Although we go manual, and we need to display the concrete username,
+ // however the realname and password is not part of manual config and
+ // must stay a placeholder in _currentConfig. @see getUserConfig()
+
+ this._fillManualEditFields(this.getConcreteConfig());
+
+ // _fillManualEditFields() indirectly calls validateManualEditComplete(),
+ // but it's important to not forget it in case the code is rewritten,
+ // so calling it explicitly again. Doesn't do harm, speed is irrelevant.
+ this.validateManualEditComplete();
+ },
+
+ /**
+ * Fills the manual edit textfields with the provided config.
+ *
+ * @param {AccountConfig} config - The config to present to the user.
+ */
+ _fillManualEditFields(config) {
+ assert(config instanceof AccountConfig);
+
+ let isExchange = config.incoming.type == "exchange";
+
+ // Incoming server.
+ document.getElementById("incomingProtocolExchange").hidden = !isExchange;
+ document.getElementById("incomingProtocol").value = Sanitizer.translate(
+ config.incoming.type,
+ { imap: 1, pop3: 2, exchange: 3 },
+ 1
+ );
+ document.getElementById("incomingHostname").value =
+ config.incoming.hostname;
+ document.getElementById("incomingSsl").value = Sanitizer.enum(
+ config.incoming.socketType,
+ [0, 1, 2, 3],
+ 0
+ );
+ document.getElementById("incomingAuthMethod").value = Sanitizer.enum(
+ config.incoming.auth,
+ [0, 3, 4, 5, 6, 10],
+ 0
+ );
+ document.getElementById("incomingUsername").value =
+ config.incoming.username;
+
+ // If a port number was specified other than "Auto"
+ if (config.incoming.port) {
+ document.getElementById("incomingPort").value = config.incoming.port;
+ } else {
+ this.adjustIncomingPortToSSLAndProtocol(config);
+ }
+
+ // Outgoing server.
+
+ document.getElementById("outgoingHostname").value =
+ config.outgoing.hostname;
+ document.getElementById("outgoingUsername").value =
+ config.outgoing.username;
+
+ // While sameInOutUsernames is true we synchronize values of incoming
+ // and outgoing username.
+ this.sameInOutUsernames = true;
+ document.getElementById("outgoingSsl").value = Sanitizer.enum(
+ config.outgoing.socketType,
+ [0, 1, 2, 3],
+ 0
+ );
+ document.getElementById("outgoingAuthMethod").value = Sanitizer.enum(
+ config.outgoing.auth,
+ [0, 1, 3, 4, 5, 6, 10],
+ 0
+ );
+
+ // If a port number was specified other than "Auto"
+ if (config.outgoing.port) {
+ document.getElementById("outgoingPort").value = config.outgoing.port;
+ } else {
+ this.adjustOutgoingPortToSSLAndProtocol(config);
+ }
+
+ this.adjustOAuth2Visibility(config);
+ },
+
+ /**
+ * Make OAuth2 visible as an authentication method when a hostname that
+ * OAuth2 can be used with is entered.
+ *
+ * @param {AccountConfig} config - The account configuration.
+ */
+ async adjustOAuth2Visibility(config) {
+ // If the incoming server hostname supports OAuth2, enable it.
+ let iDetails = OAuth2Providers.getHostnameDetails(config.incoming.hostname);
+ document.getElementById("in-authMethod-oauth2").hidden = !iDetails;
+ if (iDetails) {
+ gAccountSetupLogger.debug(
+ `OAuth2 details for incoming server ${config.incoming.hostname} is ${iDetails}`
+ );
+ config.incoming.oauthSettings = {};
+ [
+ config.incoming.oauthSettings.issuer,
+ config.incoming.oauthSettings.scope,
+ ] = iDetails;
+ this._currentConfig.incoming.oauthSettings =
+ config.incoming.oauthSettings;
+ }
+
+ // If the smtp hostname supports OAuth2, enable it.
+ let oDetails = OAuth2Providers.getHostnameDetails(config.outgoing.hostname);
+ document.getElementById("out-authMethod-oauth2").hidden = !oDetails;
+ if (oDetails) {
+ gAccountSetupLogger.debug(
+ `OAuth2 details for outgoing server ${config.outgoing.hostname} is ${oDetails}`
+ );
+ config.outgoing.oauthSettings = {};
+ [
+ config.outgoing.oauthSettings.issuer,
+ config.outgoing.oauthSettings.scope,
+ ] = oDetails;
+ this._currentConfig.outgoing.oauthSettings =
+ config.outgoing.oauthSettings;
+ }
+ },
+
+ /**
+ * Automatically fill port field in manual edit, unless the user entered a
+ * non-standard port.
+ *
+ * @param {AccountConfig} config - The account configuration.
+ */
+ async adjustIncomingPortToSSLAndProtocol(config) {
+ let incoming = config.incoming;
+
+ // Bail out if a port number is already defined and it's not part of the
+ // known ports array.
+ if (!gAllStandardPorts.includes(incoming.port)) {
+ return;
+ }
+
+ let input = document.getElementById("incomingPort");
+
+ switch (incoming.type) {
+ case "imap":
+ input.value = incoming.socketType == Ci.nsMsgSocketType.SSL ? 993 : 143;
+ break;
+
+ case "pop3":
+ input.value = incoming.socketType == Ci.nsMsgSocketType.SSL ? 995 : 110;
+ break;
+
+ case "exchange":
+ input.value = 443;
+ break;
+ }
+ },
+
+ /**
+ * Automatically fill port field in manual edit, unless the user entered a
+ * non-standard port.
+ *
+ * @param {AccountConfig} config - The account configuration.
+ */
+ async adjustOutgoingPortToSSLAndProtocol(config) {
+ let outgoing = config.outgoing;
+
+ // Bail out if a port number is already defined and it's not part of the
+ // known ports array.
+ if (!gAllStandardPorts.includes(outgoing.port)) {
+ return;
+ }
+
+ // Implicit TLS for SMTP is on port 465.
+ if (outgoing.socketType == Ci.nsMsgSocketType.SSL) {
+ document.getElementById("outgoingPort").value = 465;
+ return;
+ }
+
+ // Implicit TLS for SMTP is on port 465. STARTTLS won't work there.
+ if (
+ outgoing.port == 465 &&
+ outgoing.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS
+ ) {
+ document.getElementById("outgoingPort").value = 587;
+ }
+ },
+
+ /**
+ * If the user changed the port manually, adjust the SSL value,
+ * (only) if the new port is impossible with the old SSL value.
+ *
+ * @param config {AccountConfig}
+ */
+ adjustIncomingSSLToPort(config) {
+ let incoming = config.incoming;
+ if (!gAllStandardPorts.includes(incoming.port)) {
+ return;
+ }
+
+ if (incoming.type == "imap") {
+ // Implicit TLS for IMAP is on port 993.
+ if (
+ incoming.port == 993 &&
+ incoming.socketType != Ci.nsMsgSocketType.SSL
+ ) {
+ document.getElementById("incomingSsl").value = Ci.nsMsgSocketType.SSL;
+ return;
+ }
+ if (
+ incoming.port == 143 &&
+ incoming.socketType == Ci.nsMsgSocketType.SSL
+ ) {
+ document.getElementById("incomingSsl").value =
+ Ci.nsMsgSocketType.alwaysSTARTTLS;
+ return;
+ }
+ }
+
+ if (incoming.type == "pop3") {
+ // Implicit TLS for POP3 is on port 995.
+ if (
+ incoming.port == 995 &&
+ incoming.socketType != Ci.nsMsgSocketType.SSL
+ ) {
+ document.getElementById("incomingSsl").value = Ci.nsMsgSocketType.SSL;
+ return;
+ }
+ if (
+ incoming.port == 110 &&
+ incoming.socketType == Ci.nsMsgSocketType.SSL
+ ) {
+ document.getElementById("incomingSsl").value =
+ Ci.nsMsgSocketType.alwaysSTARTTLS;
+ }
+ }
+ },
+
+ /**
+ * @see adjustIncomingSSLToPort()
+ */
+ adjustOutgoingSSLToPort(config) {
+ let outgoing = config.outgoing;
+ if (!gAllStandardPorts.includes(outgoing.port)) {
+ return;
+ }
+
+ // Implicit TLS for SMTP is on port 465.
+ if (outgoing.port == 465 && outgoing.socketType != Ci.nsMsgSocketType.SSL) {
+ document.getElementById("outgoingSsl").value = Ci.nsMsgSocketType.SSL;
+ return;
+ }
+
+ // Port 587 and port 25 are for plain or STARTTLS. Not for Implicit TLS.
+ if (
+ (outgoing.port == 587 || outgoing.port == 25) &&
+ outgoing.socketType == Ci.nsMsgSocketType.SSL
+ ) {
+ document.getElementById("outgoingSsl").value =
+ Ci.nsMsgSocketType.alwaysSTARTTLS;
+ }
+ },
+
+ onChangedProtocolIncoming() {
+ let config = this.getUserConfig();
+ this.adjustIncomingPortToSSLAndProtocol(config);
+ this.onChangedManualEdit();
+ },
+
+ onChangedPortIncoming() {
+ gAccountSetupLogger.debug(
+ "incoming port changed: " + document.getElementById("incomingPort").value
+ );
+ this.adjustIncomingSSLToPort(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+
+ onChangedPortOutgoing() {
+ gAccountSetupLogger.debug(
+ "outgoing port changed: " + document.getElementById("outgoingPort").value
+ );
+ this.adjustOutgoingSSLToPort(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+
+ onChangedSSLIncoming() {
+ this.adjustIncomingPortToSSLAndProtocol(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+
+ onChangedSSLOutgoing() {
+ this.adjustOutgoingPortToSSLAndProtocol(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+
+ onChangedInAuth() {
+ this.onChangedManualEdit();
+ },
+
+ onChangedOutAuth(event) {
+ // Disable the outgoing username field if the "No Authentication" option is
+ // selected.
+ document.getElementById("outgoingUsername").disabled =
+ event.target.selectedOptions[0].id == "outNoAuth";
+ this.onChangedManualEdit();
+ },
+
+ onInputInUsername() {
+ if (this.sameInOutUsernames) {
+ document.getElementById("outgoingUsername").value =
+ document.getElementById("incomingUsername").value;
+ }
+ this.onChangedManualEdit();
+ },
+
+ onInputOutUsername() {
+ this.sameInOutUsernames = false;
+ this.onChangedManualEdit();
+ },
+
+ onChangeHostname() {
+ this.adjustOAuth2Visibility(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+
+ /**
+ * A value in the manual configuration area was changed.
+ */
+ onChangedManualEdit() {
+ // If there's a current operation in progress and is abortable.
+ if (this._abortable) {
+ this.onStop();
+ }
+ this.validateManualEditComplete();
+ },
+
+ /**
+ * The user interacted with an input field in the manual configuration area
+ * therefore we need to clear previous notifications and disable the "Done"
+ * button as the current config is not valid until we run again the
+ * validateManualEditComplete() method, which happens on input blur.
+ */
+ manualConfigChanged() {
+ this.clearNotifications();
+ document.getElementById("createButton").disabled = true;
+ },
+
+ /**
+ * This enables the buttons which allow the user to proceed
+ * once he has entered enough information.
+ *
+ * We can easily and fairly surely autodetect everything apart from the
+ * hostname (and username). So, once the user has entered
+ * proper hostnames, change to "manual-edit-have-hostname" mode
+ * which allows to press [Re-test], which starts the detection
+ * of the other values.
+ * Once the user has entered (or we detected) all values, he may
+ * do [Create Account] (tests login and if successful creates the account)
+ * or [Advanced Setup] (goes to Account Manager). Esp. in the latter case,
+ * we will not second-guess his setup and just to as told, so here we make
+ * sure that he at least entered all values.
+ */
+ validateManualEditComplete() {
+ // getUserConfig() is expensive, but still OK, not a problem.
+ let manualConfig = this.getUserConfig();
+ this._currentConfig = manualConfig;
+
+ if (manualConfig.isComplete()) {
+ this.switchToMode("manual-edit-complete");
+ return;
+ }
+
+ if (!!manualConfig.incoming.hostname && !!manualConfig.outgoing.hostname) {
+ this.switchToMode("manual-edit-have-hostname");
+ return;
+ }
+
+ this.switchToMode("manual-edit");
+ },
+
+ /**
+ * [Advanced Setup...] button click handler
+ * Only active in manual edit mode, and goes straight into
+ * Account Settings (pref UI) dialog. Requires a backend account,
+ * which requires proper hostname, port and protocol.
+ */
+ async onAdvancedSetup() {
+ assert(this._currentConfig instanceof AccountConfig);
+ let configFilledIn = this.getConcreteConfig();
+
+ if (CreateInBackend.checkIncomingServerAlreadyExists(configFilledIn)) {
+ let [title, description] = await document.l10n.formatValues([
+ "account-setup-creation-error-title",
+ "account-setup-error-server-exists",
+ ]);
+ Services.prompt.alert(null, title, description);
+ return;
+ }
+
+ let [title, description] = await document.l10n.formatValues([
+ "account-setup-confirm-advanced-title",
+ "account-setup-confirm-advanced-description",
+ ]);
+
+ if (!Services.prompt.confirm(null, title, description)) {
+ return;
+ }
+
+ gAccountSetupLogger.debug("creating account in backend");
+ let newAccount = CreateInBackend.createAccountInBackend(configFilledIn);
+
+ window.close();
+ gMainWindow.postMessage("account-created-in-backend", "*");
+ MsgAccountManager("am-server.xhtml", newAccount.incomingServer);
+ },
+
+ /**
+ * [Re-test] button click handler.
+ * Restarts the config guessing process after a person editing the server
+ * fields.
+ * It's called "half-manual", because we take the user-entered values
+ * as given and will not second-guess them, to respect the user wishes.
+ * (Yes, Sir! Will do as told!)
+ * The values that the user left empty or on "Auto" will be guessed/probed
+ * here. We will also check that the user-provided values work.
+ */
+ async testManualConfig() {
+ this.clearNotifications();
+ await this.startLoadingState(
+ "account-setup-looking-up-settings-half-manual"
+ );
+
+ let newConfig = this.getUserConfig();
+ gAccountSetupLogger.debug("manual config to test:\n" + newConfig);
+
+ this.switchToMode("manual-edit-testing");
+ // if (this._userPickedOutgoingServer) TODO
+ let self = this;
+ this._abortable = GuessConfig.guessConfig(
+ this._domain,
+ function (type, hostname, port, ssl, done, config) {
+ // Progress.
+ gAccountSetupLogger.debug(
+ `progress callback host: ${hostname}, port: ${port}, type: ${type}`
+ );
+ },
+ function (config) {
+ // Success.
+ self._abortable = null;
+ self._fillManualEditFields(config);
+ self.stopLoadingState("account-setup-success-half-manual");
+ self.validateManualEditComplete();
+ },
+ function (e, config) {
+ // guessConfig failed.
+ if (e instanceof CancelledException) {
+ return;
+ }
+ self._abortable = null;
+ gAccountSetupLogger.warn(`guessConfig failed: ${e}`);
+ self.showErrorNotification("account-setup-find-settings-failed");
+ self.switchToMode("manual-edit-have-hostname");
+ },
+ newConfig,
+ newConfig.outgoing.existingServerKey ? "incoming" : "both"
+ );
+ },
+
+ // -------------------
+ // UI helper functions
+
+ _prefillConfig(initialConfig) {
+ let emailsplit = this._email.split("@");
+ assert(emailsplit.length > 1);
+ let emaillocal = Sanitizer.nonemptystring(emailsplit[0]);
+ initialConfig.incoming.username = emaillocal;
+ initialConfig.outgoing.username = emaillocal;
+ return initialConfig;
+ },
+
+ clearError(which) {
+ document.getElementById(`${which}Warning`).hidden = true;
+ document.getElementById(`${which}Info`).hidden = false;
+ },
+
+ setError(which, msg_name) {
+ try {
+ document.getElementById(`${which}Info`).hidden = true;
+ document.getElementById(`${which}Warning`).hidden = false;
+ } catch (ex) {
+ alertPrompt("Missing error string", msg_name);
+ }
+ },
+
+ onFormSubmit(event) {
+ // Prevent the actual form submission.
+ event.preventDefault();
+
+ // Select the only primary button that is visible and enabled.
+ let currentButton = document.querySelector(
+ ".buttons-container-last button.primary:not([disabled],[hidden])"
+ );
+ if (currentButton) {
+ currentButton.click();
+ }
+ },
+
+ // -------------------------------
+ // Finish & dialog close functions
+
+ onCancel() {
+ // Some tests might close the account setup before it finishes loading,
+ // therefore the gMainWindow might still be null. If that's the case, do an
+ // early return since we don't need to run any condition.
+ if (!gMainWindow) {
+ window.close();
+ return;
+ }
+
+ // Ask for confirmation if the user never set Thunderbrid to be used without
+ // an email account, and no account has been configured.
+ if (
+ !Services.prefs.getBoolPref("app.use_without_mail_account", false) &&
+ !MailServices.accounts.accounts.length
+ ) {
+ // Abort any possible process before showing the confirmation dialog.
+ this.checkIfAbortable();
+ this.confirmExitDialog();
+ return;
+ }
+
+ window.close();
+ },
+
+ /**
+ * Ask for confirmation when the account setup is dismissed and the user
+ * doesn't have any configured account.
+ */
+ confirmExitDialog() {
+ let dialog = document.getElementById("confirmExitDialog");
+
+ document.getElementById("exitDialogConfirmButton").onclick = () => {
+ // Update the pref only if the checkbox was checked since it's FALSE by
+ // default. We won't expose this checkbox in the UI anymore afterward.
+ if (document.getElementById("useWithoutAccount").checked) {
+ Services.prefs.setBoolPref("app.use_without_mail_account", true);
+ }
+
+ dialog.close();
+ window.close();
+ };
+
+ document.getElementById("exitDialogCancelButton").onclick = () => {
+ dialog.close();
+ };
+
+ dialog.showModal();
+ },
+
+ /**
+ * Disable the exit dialog button if the user checks the "Use without an email
+ * account" checkbox.
+ *
+ * @param {DOMEvent} event - The checkbox change event.
+ */
+ toggleExitDialogButton(event) {
+ document.getElementById("exitDialogCancelButton").disabled =
+ event.target.checked;
+ },
+
+ checkIfAbortable() {
+ if (this._abortable) {
+ this._abortable.cancel(new UserCancelledException());
+ }
+ },
+
+ onUnload() {
+ gMainWindow.document
+ .getElementById("tabmail")
+ .unregisterTabMonitor(this.tabMonitor);
+ this.checkIfAbortable();
+ gAccountSetupLogger.debug("Shutting down email config dialog");
+ },
+
+ async onCreate() {
+ gAccountSetupLogger.debug("Create button clicked");
+
+ let configFilledIn = this.getConcreteConfig();
+ let self = this;
+ // If the dialog is not needed, it will go straight to OK callback
+ gSecurityWarningDialog.open(
+ this._currentConfig,
+ configFilledIn,
+ true,
+ async function () {
+ // on OK
+ await self.validateAndFinish(configFilledIn).catch(async ex => {
+ let errorMessage = await document.l10n.formatValue(
+ "account-setup-creation-error-title"
+ );
+ gAccountSetupLogger.error(errorMessage + ". " + ex);
+
+ self.clearNotifications();
+ let notification = self.notificationBox.appendNotification(
+ "accountSetupError",
+ {
+ label: errorMessage,
+ priority: self.notificationBox.PRIORITY_CRITICAL_HIGH,
+ },
+ null
+ );
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+ });
+ },
+ function () {
+ // on cancel, do nothing
+ }
+ );
+ },
+
+ // called by onCreate()
+ async validateAndFinish(configFilled) {
+ let configFilledIn = configFilled || this.getConcreteConfig();
+ if (
+ configFilledIn.incoming.type == "exchange" &&
+ "addonAccountType" in configFilledIn.incoming
+ ) {
+ configFilledIn.incoming.type = configFilledIn.incoming.addonAccountType;
+ }
+
+ if (CreateInBackend.checkIncomingServerAlreadyExists(configFilledIn)) {
+ let [title, description] = await document.l10n.formatValues([
+ "account-setup-creation-error-title",
+ "account-setup-error-server-exists",
+ ]);
+ Services.prompt.alert(null, title, description);
+ return;
+ }
+
+ if (configFilledIn.outgoing.addThisServer) {
+ let existingServer =
+ CreateInBackend.checkOutgoingServerAlreadyExists(configFilledIn);
+ if (existingServer) {
+ configFilledIn.outgoing.addThisServer = false;
+ configFilledIn.outgoing.existingServerKey = existingServer.key;
+ }
+ }
+
+ let createButton = document.getElementById("createButton");
+ let reTestButton = document.getElementById("reTestButton");
+ createButton.disabled = true;
+ reTestButton.disabled = true;
+
+ this.clearNotifications();
+ this.startLoadingState("account-setup-checking-password");
+ let telemetryKey =
+ this._currentConfig.source == AccountConfig.kSourceXML ||
+ this._currentConfig.source == AccountConfig.kSourceExchange
+ ? this._currentConfig.subSource
+ : this._currentConfig.source;
+
+ let self = this;
+ let verifier = new ConfigVerifier(this._msgWindow);
+ window.addEventListener("unload", event => {
+ verifier.cleanup();
+ });
+ verifier
+ .verifyConfig(
+ configFilledIn,
+ // guess login config?
+ configFilledIn.source != AccountConfig.kSourceXML
+ // TODO Instead, the following line would be correct, but I cannot use it,
+ // because some other code doesn't adhere to the expectations/specs.
+ // Find out what it was and fix it.
+ // concreteConfig.source == AccountConfig.kSourceGuess,
+ )
+ .then(successfulConfig => {
+ // success
+ self.stopLoadingState(
+ successfulConfig.incoming.password
+ ? "account-setup-success-password"
+ : null
+ );
+
+ // The auth might have changed, so we should update the current config.
+ self._currentConfig.incoming.auth = successfulConfig.incoming.auth;
+ self._currentConfig.outgoing.auth = successfulConfig.outgoing.auth;
+ self._currentConfig.incoming.username =
+ successfulConfig.incoming.username;
+ self._currentConfig.outgoing.username =
+ successfulConfig.outgoing.username;
+
+ // We loaded dynamic client registration, fill this data back in to the
+ // config set.
+ if (successfulConfig.incoming.oauthSettings) {
+ self._currentConfig.incoming.oauthSettings =
+ successfulConfig.incoming.oauthSettings;
+ }
+ if (successfulConfig.outgoing.oauthSettings) {
+ self._currentConfig.outgoing.oauthSettings =
+ successfulConfig.outgoing.oauthSettings;
+ }
+ self.finish(configFilledIn);
+
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.successful_email_account_setup",
+ telemetryKey,
+ 1
+ );
+ })
+ .catch(e => {
+ // failed
+ // Could be a wrong password, but there are 1000 other
+ // reasons why this failed. Only the backend knows.
+ // If we got no message, then something other than VerifyLogon failed.
+
+ // For an Exchange server, some known configurations can
+ // be disabled (per user or domain or server).
+ // Warn the user if the open protocol we tried didn't work.
+ if (
+ ["imap", "pop3"].includes(configFilledIn.incoming.type) &&
+ configFilledIn.incomingAlternatives.some(i => i.type == "exchange")
+ ) {
+ self.showErrorNotification(
+ "account-setup-exchange-config-unverifiable"
+ );
+ } else {
+ let msg = e.message || e.toString();
+ self.showErrorNotification(msg, true);
+ }
+
+ // give user something to proceed after fixing
+ createButton.disabled = false;
+ // hidden in non-manual mode, so it's fine to enable
+ reTestButton.disabled = false;
+
+ Services.telemetry.keyedScalarAdd(
+ "tb.account.failed_email_account_setup",
+ telemetryKey,
+ 1
+ );
+ });
+ },
+
+ /**
+ * @param {AccountConfig} concreteConfig - The config to use.
+ */
+ finish(concreteConfig) {
+ gAccountSetupLogger.debug("creating account in backend");
+ let newAccount = CreateInBackend.createAccountInBackend(concreteConfig);
+
+ // Trigger the first login to download the folder structure and messages.
+ newAccount.incomingServer.getNewMessages(
+ newAccount.incomingServer.rootFolder,
+ this._msgWindow,
+ null
+ );
+
+ if (this._okCallback) {
+ this._okCallback();
+ }
+
+ this.showSuccessView(newAccount);
+ },
+
+ /**
+ * Toggle the visibility of the list of available services to configure.
+ */
+ toggleSetupContainer(event) {
+ let container = event.target.closest(".linked-services-section");
+ container.classList.toggle("opened");
+ container
+ .querySelector(".linked-services-container")
+ .toggleAttribute("hidden");
+ },
+
+ /**
+ * Update the account setup tab to show a successful final view with quick
+ * links and suggested next steps.
+ *
+ * @param {nsIMsgAccount} account - The newly created account.
+ */
+ async showSuccessView(account) {
+ gAccountSetupLogger.debug("Account creation successful");
+
+ // Populate the account recap info.
+ document.getElementById("newAccountName").textContent = this._realname;
+ document.getElementById("newAccountEmail").textContent = this._email;
+ document.getElementById("newAccountProtocol").textContent =
+ account.incomingServer.type;
+
+ // Store the host domain that will be used to look for CardDAV and CalDAV
+ // services.
+ this._hostname = this._email.split("@")[1];
+
+ // Set up event listeners for the quick links.
+ document.getElementById("settingsButton").addEventListener(
+ "click",
+ () => {
+ MsgAccountManager(null, account.incomingServer);
+ },
+ { once: true }
+ );
+
+ // Hide the e2ee button if the current server doesn't support it.
+ let hasEncryption =
+ account.incomingServer.type != "rss" &&
+ account.incomingServer.type != "nntp" &&
+ account.incomingServer.protocolInfo?.canGetMessages;
+ document.getElementById("encryptionButton").hidden = !hasEncryption;
+ if (hasEncryption) {
+ document
+ .getElementById("encryptionButton")
+ .addEventListener("click", () => {
+ MsgAccountManager("am-e2e.xhtml", account.incomingServer);
+ });
+ }
+
+ document.getElementById("signatureButton").addEventListener("click", () => {
+ MsgAccountManager(null, account.incomingServer);
+ });
+
+ // Finally, show the success view.
+ this.switchToMode("success");
+
+ // Initialize the fetching of possible linked services like address books
+ // or calendars.
+ gAccountSetupLogger.debug("Fetching linked address books and calendars");
+
+ let notification = this.syncingBox.appendNotification(
+ "accountSetupLoading",
+ {
+ label: await document.l10n.formatValue(
+ "account-setup-looking-up-address-books"
+ ),
+ priority: this.syncingBox.PRIORITY_INFO_LOW,
+ },
+ null
+ );
+ notification.setAttribute("align", "center");
+
+ // Hide the close button to prevent dismissing the notification.
+ notification.removeAttribute("dismissable");
+
+ // Detect linked address books.
+ await this.fetchAddressBooks();
+
+ // Update the notification and start detecting linked calendars.
+ document.l10n.setAttributes(
+ notification.messageText,
+ "account-setup-looking-up-calendars"
+ );
+ await this.fetchCalendars();
+
+ // Update the connected services description if we have at least one address
+ // book or one calendar we can connect to.
+ document.l10n.setAttributes(
+ document.getElementById("linkedServicesDescription"),
+ !this.addressBooks.length && !this.calendars.size
+ ? "account-setup-no-linked-description"
+ : "account-setup-linked-services-description"
+ );
+
+ // Clear the loading notification.
+ this.syncingBox.removeAllNotifications();
+ this.showHelperImage("step5");
+ },
+
+ /**
+ * Fetch any available CardDAV address books.
+ */
+ async fetchAddressBooks() {
+ this.addressBooks = [];
+ try {
+ this.addressBooks = await CardDAVUtils.detectAddressBooks(
+ this._email,
+ this._password,
+ `https://${this._hostname}`,
+ false
+ );
+ } catch (ex) {
+ gAccountSetupLogger.error(ex);
+ }
+
+ let hideAddressBookUI = !this.addressBooks.length;
+ document.getElementById("linkedAddressBooks").hidden = hideAddressBookUI;
+
+ // Clear the UI from any previous list.
+ let abList = document.querySelector(
+ "#addressBooksSetup .linked-services-list"
+ );
+ while (abList.hasChildNodes()) {
+ abList.lastChild.remove();
+ }
+
+ // Interrupt if we don't have anything to show.
+ if (hideAddressBookUI) {
+ return;
+ }
+
+ document.l10n.setAttributes(
+ document.getElementById("addressBooksCountDescription"),
+ "account-setup-found-address-books-description",
+ { count: this.addressBooks.length }
+ );
+
+ // Collect existing carddav address books to compare with the list of
+ // recently fetched ones.
+ let existing = MailServices.ab.directories.map(d =>
+ d.getStringValue("carddav.url", "")
+ );
+
+ // Populate the list of available address books.
+ for (let book of this.addressBooks) {
+ let provider = document.createElement("span");
+ provider.classList.add("protocol-type");
+ provider.textContent = "CardDAV";
+
+ let name = document.createElement("span");
+ name.classList.add("list-item-name");
+ name.textContent = book.name;
+
+ let button = document.createElement("button");
+ button.setAttribute("type", "button");
+
+ if (existing.includes(book.url.href)) {
+ // This address book aready exists for some reason, so disable the
+ // button and mark it as existing.
+ button.classList.add("existing", "small-button");
+ document.l10n.setAttributes(
+ button,
+ "account-setup-existing-address-book"
+ );
+ button.disabled = true;
+ } else {
+ button.classList.add("small-button");
+ document.l10n.setAttributes(button, "account-setup-connect-link");
+ button.addEventListener("click", () => {
+ this._setupAddressBook(button, book);
+ });
+ }
+
+ let row = document.createElement("li");
+ row.appendChild(provider);
+ row.appendChild(name);
+ row.appendChild(button);
+ abList.appendChild(row);
+ }
+
+ // Show a "connect all" button if we have more than one address book.
+ document.getElementById("addressBooksSetupAll").hidden =
+ this.addressBooks.length <= 1;
+ },
+
+ /**
+ * Connect to the selected address book.
+ *
+ * @param {HTMLElement} button - The clicked button in the list.
+ * @param {foundBook} book - The address book to configure.
+ */
+ _setupAddressBook(button, book) {
+ book.create();
+
+ // Update the button to reflect the creation of the new address book.
+ button.classList.add("existing");
+ document.l10n.setAttributes(button, "account-setup-existing-address-book");
+ button.disabled = true;
+
+ // Check if we have any address book left to set up and hide the
+ // "Connect all" button if not.
+ document.getElementById("addressBooksSetupAll").hidden =
+ !document.querySelectorAll(
+ "#addressBooksSetup .linked-services-list button:not(.existing)"
+ ).length;
+ },
+
+ /**
+ * Loop through all available address books found and click the connect
+ * button to trigger the method attached to the onclick listener.
+ */
+ setupAllAddressBooks() {
+ for (let button of document.querySelectorAll(
+ "#addressBooksSetup .linked-services-list button"
+ )) {
+ button.click();
+ }
+ },
+
+ /**
+ * Fetch any available CalDAV calendars.
+ */
+ async fetchCalendars() {
+ this.calendars = {};
+ try {
+ this.calendars = await cal.provider.detection.detect(
+ this._email,
+ this._password,
+ `https://${this._hostname}`,
+ document.getElementById("rememberPassword").checked,
+ [],
+ {}
+ );
+ } catch (ex) {
+ gAccountSetupLogger.error(ex);
+ }
+
+ let hideCalendarUI = !this.calendars.size;
+ document.getElementById("linkedCalendars").hidden = hideCalendarUI;
+
+ // Clear the UI from any previous list.
+ let calList = document.querySelector(
+ "#calendarsSetup .linked-services-list"
+ );
+ while (calList.hasChildNodes()) {
+ calList.lastChild.remove();
+ }
+
+ // Interrupt if we don't have anything to show.
+ if (hideCalendarUI) {
+ return;
+ }
+
+ // Collect existing calendars to compare with the list of recently fetched
+ // ones.
+ let existing = new Set(
+ cal.manager.getCalendars({}).map(calendar => calendar.uri.spec)
+ );
+
+ let calendarsCount = 0;
+
+ // Populate the list of available calendars.
+ for (let [provider, calendars] of this.calendars.entries()) {
+ for (let calendar of calendars) {
+ let cal_provider = document.createElement("span");
+ cal_provider.classList.add("protocol-type");
+ cal_provider.textContent = provider.shortName;
+
+ let cal_name = document.createElement("span");
+ cal_name.classList.add("list-item-name");
+ cal_name.textContent = calendar.name;
+
+ let button = document.createElement("button");
+ button.setAttribute("type", "button");
+
+ if (existing.has(calendar.uri.spec)) {
+ // This calendar aready exists for some reason, so disable the button
+ // and mark it as existing.
+ button.classList.add("existing", "small-button");
+ document.l10n.setAttributes(
+ button,
+ "account-setup-existing-calendar"
+ );
+ button.disabled = true;
+ } else {
+ button.classList.add("small-button");
+ document.l10n.setAttributes(button, "account-setup-connect-link");
+ button.addEventListener("click", () => {
+ // If the button has a specific data attribute it means we want to
+ // set up the calendar directly without opening the dialog.
+ if (button.hasAttribute("data-setup-calendar")) {
+ this._setupCalendar(button, calendar);
+ return;
+ }
+
+ this._showCalendarDialog(button, calendar);
+ });
+ }
+
+ let row = document.createElement("li");
+ row.appendChild(cal_provider);
+ row.appendChild(cal_name);
+ row.appendChild(button);
+ calList.appendChild(row);
+
+ calendarsCount++;
+ }
+ }
+
+ document.l10n.setAttributes(
+ document.getElementById("calendarsCountDescription"),
+ "account-setup-found-calendars-description",
+ { count: calendarsCount }
+ );
+
+ // Show a "connect all" button if we have more than one calendar.
+ document.getElementById("calendarsSetupAll").hidden = calendarsCount <= 1;
+ },
+
+ /**
+ * Show the dialog to connect the selected calendar. This native HTML dialog
+ * is a streamlined version of the calendar-properties-dialog.xhtml. The two
+ * dialogs should kept in sync if a property of the calendar changes that
+ * requires updating any field.
+ *
+ * @param {HTMLElement} button - The clicked button in the list.
+ * @param {calICalendar} calendar - The calendar to configure.
+ */
+ _showCalendarDialog(button, calendar) {
+ let dialog = document.getElementById("calendarDialog");
+
+ // Update the calendar info in the dialog.
+ let nameInput = document.getElementById("calendarName");
+ nameInput.value = calendar.name;
+
+ // Some servers provide colors as an 8-character hex string, which the color
+ // picker can't handle. Strip the alpha component.
+ let color = calendar.getProperty("color");
+ let alpha = color?.match(/^(#[0-9A-Fa-f]{6})[0-9A-Fa-f]{2}$/);
+ if (alpha) {
+ calendar.setProperty("color", alpha[1]);
+ color = alpha[1];
+ }
+ let colorInput = document.getElementById("calendarColor");
+ colorInput.value = color || "#A8C2E1";
+
+ let readOnlyCheckbox = document.getElementById("calendarReadOnly");
+ readOnlyCheckbox.checked = calendar.readOnly;
+
+ // Hide the "Show reminders" checkbox if the calendar doesn't support it.
+ document.getElementById("calendarShowRemindersRow").hidden =
+ calendar.getProperty("capabilities.alarms.popup.supported") === false;
+ let remindersCheckbox = document.getElementById("calendarShowReminders");
+ remindersCheckbox.checked = !calendar.getProperty("suppressAlarms");
+
+ // Hide the "Offline support" if the calendar doesn't support it.
+ let offlineCheckbox = document.getElementById("calendarOfflineSupport");
+ let canCache = calendar.getProperty("cache.supported") !== false;
+ let alwaysCache = calendar.getProperty("cache.always");
+ if (!canCache || alwaysCache) {
+ offlineCheckbox.hidden = true;
+ offlineCheckbox.disabled = true;
+ }
+ offlineCheckbox.checked =
+ alwaysCache || (canCache && calendar.getProperty("cache.enabled"));
+
+ // Set up the "Refresh calendar" menulist.
+ let calendarRefresh = document.getElementById("calendarRefresh");
+ calendarRefresh.disabled = !calendar.canRefresh;
+ calendarRefresh.value = calendar.getProperty("refreshInterval") || 30;
+
+ // Set up the dialog's action buttons.
+ document.getElementById("calendarDialogConfirmButton").onclick = () => {
+ // Update the attributes of the calendar in case the user changed some
+ // values.
+ calendar.name = nameInput.value;
+ calendar.setProperty("color", colorInput.value);
+ if (calendar.canRefresh) {
+ calendar.setProperty("refreshInterval", calendarRefresh.value);
+ }
+
+ calendar.readOnly = readOnlyCheckbox.checked;
+ calendar.setProperty("suppressAlarms", !remindersCheckbox.checked);
+ if (!alwaysCache) {
+ calendar.setProperty("cache.enabled", offlineCheckbox.checked);
+ }
+
+ this._setupCalendar(button, calendar);
+ dialog.close();
+ };
+
+ document.getElementById("calendarDialogCancelButton").onclick = () => {
+ dialog.close();
+ };
+
+ dialog.showModal();
+ },
+
+ /**
+ * Connect to the selected calendar.
+ *
+ * @param {HTMLElement} button - The clicked button in the list.
+ * @param {calICalendar} calendar - The calendar to configure.
+ */
+ _setupCalendar(button, calendar) {
+ cal.manager.registerCalendar(calendar);
+
+ // Update the button to reflect the creation of the new calendar.
+ button.classList.add("existing");
+ document.l10n.setAttributes(button, "account-setup-existing-calendar");
+ button.disabled = true;
+
+ // Check if we have any calendar left to set up and hide the "Connect all"
+ // button if not.
+ document.getElementById("calendarsSetupAll").hidden =
+ !document.querySelectorAll(
+ "#calendarsSetup .linked-services-list button:not(.existing)"
+ ).length;
+ },
+
+ /**
+ * Loop through all available calendars found and click the connect
+ * button to trigger the method attached to the onclick listener.
+ */
+ setupAllCalendars() {
+ for (let button of document.querySelectorAll(
+ "#calendarsSetup .linked-services-list button:not(.existing)"
+ )) {
+ // Set the attribute to skip the opening of the properties dialog.
+ button.setAttribute("data-setup-calendar", true);
+ button.click();
+ }
+ },
+
+ /**
+ * Called from the very final view of the account setup, when the user decides
+ * to close the wizard.
+ */
+ onFinish() {
+ // Send the message to the mail tab in case the UI didn't load during the
+ // previous setup callback.
+ gMainWindow.postMessage("account-setup-closed", "*");
+ // Close this tab.
+ window.close();
+ },
+};
+
+function serverMatches(a, b) {
+ return (
+ a.type == b.type &&
+ a.hostname == b.hostname &&
+ a.port == b.port &&
+ a.socketType == b.socketType &&
+ a.auth == b.auth
+ );
+}
+
+/**
+ * Warning dialog, warning user about lack of, or inappropriate, encryption.
+ */
+var gSecurityWarningDialog = {
+ /**
+ * {Array of {(incoming or outgoing) server part of {AccountConfig}}
+ * A list of the servers for which we already showed this dialog and the
+ * user approved the configs. For those, we won't show the warning again.
+ * (Make sure to store a copy in case the underlying object is changed.)
+ */
+ _acknowledged: [],
+
+ _inSecurityBad: 0x0001,
+ _inCertBad: 0x0010,
+ _outSecurityBad: 0x0100,
+ _outCertBad: 0x1000,
+
+ /**
+ * Checks whether we need to warn about this config.
+ *
+ * We (currently) warn if
+ * - the mail travels unsecured (no SSL/STARTTLS)
+ * - (We don't warn about unencrypted passwords specifically,
+ * because they'd be encrypted with SSL and without SSL, we'd
+ * warn anyways.)
+ *
+ * We may not warn despite these conditions if we had shown the
+ * warning for that server before and the user acknowledged it.
+ * (Given that this dialog object is static/global and persistent,
+ * we can store that approval state here in this object.)
+ *
+ * @param configSchema @see open()
+ * @param configFilledIn @see open()
+ * @returns {boolean} - True when the dialog should be shown
+ * (call open()). if false, the dialog can and should be skipped.
+ */
+ needed(configSchema, configFilledIn) {
+ assert(configSchema instanceof AccountConfig);
+ assert(configFilledIn instanceof AccountConfig);
+ assert(configSchema.isComplete());
+ assert(configFilledIn.isComplete());
+
+ let incomingBad =
+ (configFilledIn.incoming.socketType > 1 ? 0 : this._inSecurityBad) |
+ (configFilledIn.incoming.badCert ? this._inCertBad : 0);
+ let outgoingBad = 0;
+ if (configFilledIn.outgoing.addThisServer) {
+ outgoingBad =
+ (configFilledIn.outgoing.socketType > 1 ? 0 : this._outSecurityBad) |
+ (configFilledIn.outgoing.badCert ? this._outCertBad : 0);
+ }
+
+ if (incomingBad > 0) {
+ if (
+ this._acknowledged.some(ackServer => {
+ return serverMatches(ackServer, configFilledIn.incoming);
+ })
+ ) {
+ incomingBad = 0;
+ }
+ }
+ if (outgoingBad > 0) {
+ if (
+ this._acknowledged.some(ackServer => {
+ return serverMatches(ackServer, configFilledIn.outgoing);
+ })
+ ) {
+ outgoingBad = 0;
+ }
+ }
+
+ return incomingBad | outgoingBad;
+ },
+
+ /**
+ * Opens the dialog, fills it with values, and shows it to the user.
+ *
+ * The function is async: it returns immediately, and when the user clicks
+ * OK or Cancel, the callbacks are called. There the callers proceed as
+ * appropriate.
+ *
+ * @param configSchema The config, with placeholders not replaced yet.
+ * This object may be modified to store the user's confirmations, but
+ * currently that's not the case.
+ * @param configFilledIn The concrete config with placeholders replaced.
+ * @param onlyIfNeeded {Boolean} - If there is nothing to warn about,
+ * call okCallback() immediately (and sync).
+ * @param okCallback {function(config {AccountConfig})}
+ * Called when the user clicked OK and approved the config including
+ * the warnings. |config| is without placeholders replaced.
+ * @param cancalCallback {function()}
+ * Called when the user decided to heed the warnings and not approve.
+ */
+ open(configSchema, configFilledIn, onlyIfNeeded, okCallback, cancelCallback) {
+ assert(typeof okCallback == "function");
+ assert(typeof cancelCallback == "function");
+
+ // needed() also checks the parameters
+ let needed = this.needed(configSchema, configFilledIn);
+ if (needed == 0 && onlyIfNeeded) {
+ okCallback();
+ return;
+ }
+
+ assert(needed > 0, "security dialog opened needlessly");
+
+ let dialog = document.getElementById("insecureDialog");
+ this._currentConfigFilledIn = configFilledIn;
+ this._okCallback = okCallback;
+ this._cancelCallback = cancelCallback;
+ let incoming = configFilledIn.incoming;
+ let outgoing = configFilledIn.outgoing;
+
+ // Reset the dialog, in case we've shown it before.
+ document.getElementById("acknowledgeWarning").checked = false;
+ document.getElementById("insecureConfirmButton").disabled = true;
+
+ // Incoming security is bad.
+ let insecureIncoming = document.getElementById("insecureSectionIncoming");
+ if (needed & this._inSecurityBad) {
+ document.l10n.setAttributes(
+ document.getElementById("warningIncoming"),
+ "account-setup-warning-cleartext",
+ {
+ server: incoming.hostname,
+ }
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("detailsIncoming"),
+ "account-setup-warning-cleartext-details"
+ );
+
+ insecureIncoming.hidden = false;
+ } else {
+ insecureIncoming.hidden = true;
+ }
+
+ // Outgoing security or certificate is bad.
+ let insecureOutgoing = document.getElementById("insecureSectionOutgoing");
+ if (needed & this._outSecurityBad) {
+ document.l10n.setAttributes(
+ document.getElementById("warningOutgoing"),
+ "account-setup-warning-cleartext",
+ {
+ server: outgoing.hostname,
+ }
+ );
+
+ document.l10n.setAttributes(
+ document.getElementById("detailsOutgoing"),
+ "account-setup-warning-cleartext-details"
+ );
+
+ insecureOutgoing.hidden = false;
+ } else {
+ insecureOutgoing.hidden = true;
+ }
+
+ assert(
+ !insecureIncoming.hidden || !insecureOutgoing.hidden,
+ "warning dialog shown for unknown reason"
+ );
+
+ // Show the dialog.
+ dialog.showModal();
+ },
+
+ /**
+ * User checked checkbox that he understood it and wishes to ignore the
+ * warning.
+ */
+ toggleAcknowledge() {
+ document.getElementById("insecureConfirmButton").disabled =
+ !document.getElementById("acknowledgeWarning").checked;
+ },
+
+ /**
+ * [Cancel] button pressed. Get me out of here!
+ */
+ onCancel() {
+ document.getElementById("insecureDialog").close();
+ document.getElementById("incomingProtocol").focus();
+
+ this._cancelCallback();
+ },
+
+ /**
+ * [OK] button pressed.
+ * Implies that the user toggled the acknowledge checkbox,
+ * i.e. approved the config and ignored the warnings,
+ * otherwise the button would have been disabled.
+ */
+ onOK() {
+ assert(document.getElementById("acknowledgeWarning").checked);
+
+ // Need filled in, in case the hostname is a placeholder.
+ let storeConfig = this._currentConfigFilledIn.copy();
+ this._acknowledged.push(storeConfig.incoming);
+ this._acknowledged.push(storeConfig.outgoing);
+
+ document.getElementById("insecureDialog").close();
+
+ this._okCallback();
+ },
+};
+
+/**
+ * Helper method to open the dictionaries list in a new tab.
+ */
+function openDictionariesTab() {
+ let mailWindow = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mailWindow.document.getElementById("tabmail");
+
+ let url = Services.urlFormatter.formatURLPref(
+ "spellchecker.dictionaries.download.url"
+ );
+
+ // Open the dictionaries URL.
+ tabmail.openTab("contentTab", {
+ url,
+ });
+}