summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/accountcreation
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/accountcreation')
-rw-r--r--comm/mail/components/accountcreation/AccountConfig.jsm463
-rw-r--r--comm/mail/components/accountcreation/AccountCreationUtils.jsm717
-rw-r--r--comm/mail/components/accountcreation/ConfigVerifier.jsm386
-rw-r--r--comm/mail/components/accountcreation/CreateInBackend.jsm459
-rw-r--r--comm/mail/components/accountcreation/ExchangeAutoDiscover.jsm676
-rw-r--r--comm/mail/components/accountcreation/FetchConfig.jsm299
-rw-r--r--comm/mail/components/accountcreation/FetchHTTP.jsm401
-rw-r--r--comm/mail/components/accountcreation/GuessConfig.jsm1317
-rw-r--r--comm/mail/components/accountcreation/Sanitizer.jsm249
-rw-r--r--comm/mail/components/accountcreation/content/accountHub.js277
-rw-r--r--comm/mail/components/accountcreation/content/accountSetup.js3023
-rw-r--r--comm/mail/components/accountcreation/content/accountSetup.xhtml1333
-rw-r--r--comm/mail/components/accountcreation/jar.mn12
-rw-r--r--comm/mail/components/accountcreation/moz.build23
-rw-r--r--comm/mail/components/accountcreation/readFromXML.jsm352
-rw-r--r--comm/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml158
-rw-r--r--comm/mail/components/accountcreation/test/xpcshell/data/example.com.xml21
-rw-r--r--comm/mail/components/accountcreation/test/xpcshell/test_autoconfigFetchDisk.js76
-rw-r--r--comm/mail/components/accountcreation/test/xpcshell/test_autoconfigUtils.js319
-rw-r--r--comm/mail/components/accountcreation/test/xpcshell/test_autoconfigXML.js266
-rw-r--r--comm/mail/components/accountcreation/test/xpcshell/xpcshell.ini9
-rw-r--r--comm/mail/components/accountcreation/views/container.mjs50
-rw-r--r--comm/mail/components/accountcreation/views/email.mjs185
-rw-r--r--comm/mail/components/accountcreation/views/start.mjs163
24 files changed, 11234 insertions, 0 deletions
diff --git a/comm/mail/components/accountcreation/AccountConfig.jsm b/comm/mail/components/accountcreation/AccountConfig.jsm
new file mode 100644
index 0000000000..59b9604725
--- /dev/null
+++ b/comm/mail/components/accountcreation/AccountConfig.jsm
@@ -0,0 +1,463 @@
+/* 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/. */
+
+/**
+ * This file creates the class AccountConfig, which is a JS object that holds
+ * a configuration for a certain account. It is *not* created in the backend
+ * yet (use aw-createAccount.js for that), and it may be incomplete.
+ *
+ * Several AccountConfig objects may co-exist, e.g. for autoconfig.
+ * One AccountConfig object is used to prefill and read the widgets
+ * in the Wizard UI.
+ * When we autoconfigure, we autoconfig writes the values into a
+ * new object and returns that, and the caller can copy these
+ * values into the object used by the UI.
+ *
+ * See also
+ * <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat>
+ * for values stored.
+ */
+
+const EXPORTED_SYMBOLS = ["AccountConfig"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "AccountCreationUtils",
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+function AccountConfig() {
+ this.incoming = this.createNewIncoming();
+ this.incomingAlternatives = [];
+ this.outgoing = this.createNewOutgoing();
+ this.outgoingAlternatives = [];
+ this.identity = {
+ // displayed real name of user
+ realname: "%REALNAME%",
+ // email address of user, as shown in From of outgoing mails
+ emailAddress: "%EMAILADDRESS%",
+ };
+ this.inputFields = [];
+ this.domains = [];
+}
+AccountConfig.prototype = {
+ // @see createNewIncoming()
+ incoming: null,
+ // @see createNewOutgoing()
+ outgoing: null,
+ /**
+ * Other servers which can be used instead of |incoming|,
+ * in order of decreasing preference.
+ * (|incoming| itself should not be included here.)
+ * { Array of incoming/createNewIncoming() }
+ */
+ incomingAlternatives: null,
+ outgoingAlternatives: null,
+ // just an internal string to refer to this. Do not show to user.
+ id: null,
+ // who created the config.
+ // { one of kSource* }
+ source: null,
+ /**
+ * Used for telemetry purposes.
+ * - for kSourceXML, subSource is one of xml-from-{disk, db, isp-https, isp-http}.
+ * - for kSourceExchange, subSource is one of exchange-from-urlN[-guess].
+ */
+ subSource: null,
+ displayName: null,
+ // { Array of { varname (value without %), displayName, exampleValue } }
+ inputFields: null,
+ // email address domains for which this config is applicable
+ // { Array of Strings }
+ domains: null,
+
+ /**
+ * Factory function for incoming and incomingAlternatives
+ */
+ createNewIncoming() {
+ return {
+ // { String-enum: "pop3", "imap", "nntp", "exchange" }
+ type: null,
+ hostname: null,
+ // { Integer }
+ port: null,
+ // May be a placeholder (starts and ends with %). { String }
+ username: null,
+ password: null,
+ // {nsMsgSocketType} @see MailNewsTypes2.idl. -1 means not inited
+ socketType: -1,
+ /**
+ * true when the cert is invalid (and thus SSL useless), because it's
+ * 1) not from an accepted CA (including self-signed certs)
+ * 2) for a different hostname or
+ * 3) expired.
+ * May go back to false when user explicitly accepted the cert.
+ */
+ badCert: false,
+ /**
+ * How to log in to the server: plaintext or encrypted pw, GSSAPI etc.
+ * Defined by Ci.nsMsgAuthMethod
+ * Same as server pref "authMethod".
+ */
+ auth: 0,
+ /**
+ * Other auth methods that we think the server supports.
+ * They are ordered by descreasing preference.
+ * (|auth| itself is not included in |authAlternatives|)
+ * {Array of Ci.nsMsgAuthMethod} (same as .auth)
+ */
+ authAlternatives: null,
+ // in minutes { Integer }
+ checkInterval: 10,
+ loginAtStartup: true,
+ // POP3 only:
+ // Not yet implemented. { Boolean }
+ useGlobalInbox: false,
+ leaveMessagesOnServer: true,
+ daysToLeaveMessagesOnServer: 14,
+ deleteByAgeFromServer: true,
+ // When user hits delete, delete from local store and from server
+ deleteOnServerWhenLocalDelete: true,
+ downloadOnBiff: true,
+ // Override `addThisServer` for a specific incoming server
+ useGlobalPreferredServer: false,
+
+ // OAuth2 configuration, if needed.
+ oauthSettings: null,
+
+ // for Microsoft Exchange servers. Optional.
+ owaURL: null,
+ ewsURL: null,
+ easURL: null,
+ // for when an addon overrides the account type. Optional.
+ addonAccountType: null,
+ };
+ },
+ /**
+ * Factory function for outgoing and outgoingAlternatives
+ */
+ createNewOutgoing() {
+ return {
+ type: "smtp",
+ hostname: null,
+ port: null, // see incoming
+ username: null, // see incoming. may be null, if auth is 0.
+ password: null, // see incoming. may be null, if auth is 0.
+ socketType: -1, // see incoming
+ badCert: false, // see incoming
+ auth: 0, // see incoming
+ authAlternatives: null, // see incoming
+ addThisServer: true, // if we already have an SMTP server, add this
+ // if we already have an SMTP server, use it.
+ useGlobalPreferredServer: false,
+ // we should reuse an already configured SMTP server.
+ // nsISmtpServer.key
+ existingServerKey: null,
+ // user display value for existingServerKey
+ existingServerLabel: null,
+
+ // OAuth2 configuration, if needed.
+ oauthSettings: null,
+ };
+ },
+
+ /**
+ * The configuration needs an addon to handle the account type.
+ * The addon needs to be installed before the account can be created
+ * in the backend.
+ * You can choose one, if there are several addons in the list.
+ * (Optional)
+ *
+ * Array of:
+ * {
+ * id: "owl@example.com" {string},
+ *
+ * // already localized string
+ * name: "Owl" {string},
+ *
+ * // already localized string
+ * description: "A third party addon that allows you to connect to Exchange servers" {string}
+ *
+ * // Minimal version of the addon. Needed in case the addon is already installed,
+ * // to verify that the installed version is sufficient.
+ * // The XPI URL below must satisfy this.
+ * // Must satisfy <https://developer.mozilla.org/en-US/docs/Mozilla/Toolkit_version_format>
+ * minVersion: "0.2" {string}
+ *
+ * xpiURL: "https://live.thunderbird.net/autoconfig/owl.xpi" {URL},
+ * websiteURL: "https://www.beonex.com/owl/" {URL},
+ * icon32: "https://www.beonex.com/owl/owl-32x32.png" {URL},
+ *
+ * useType : {
+ * // Type shown as radio button to user in the config result.
+ * // Users won't understand OWA vs. EWS vs. EAS etc., so this is an abstraction
+ * // from the end user perspective.
+ * generalType: "exchange" {string},
+ *
+ * // Protocol
+ * // Independent of the addon
+ * protocolType: "owa" {string},
+ *
+ * // Account type in the Thunderbird backend.
+ * // What nsIMsgAccount.type will be set to when creating the account.
+ * // This is specific to the addon.
+ * addonAccountType: "owl-owa" {string},
+ * }
+ * }
+ */
+ addons: null,
+
+ /**
+ * Returns a deep copy of this object,
+ * i.e. modifying the copy will not affect the original object.
+ */
+ copy() {
+ // Workaround: deepCopy() fails to preserve base obj (instanceof)
+ let result = new AccountConfig();
+ for (let prop in this) {
+ result[prop] = lazy.AccountCreationUtils.deepCopy(this[prop]);
+ }
+
+ return result;
+ },
+
+ isComplete() {
+ return (
+ !!this.incoming.hostname &&
+ !!this.incoming.port &&
+ this.incoming.socketType != -1 &&
+ !!this.incoming.auth &&
+ !!this.incoming.username &&
+ (!!this.outgoing.existingServerKey ||
+ this.outgoing.useGlobalPreferredServer ||
+ (!!this.outgoing.hostname &&
+ !!this.outgoing.port &&
+ this.outgoing.socketType != -1 &&
+ !!this.outgoing.auth &&
+ !!this.outgoing.username))
+ );
+ },
+
+ toString() {
+ function sslToString(socketType) {
+ switch (socketType) {
+ case 0:
+ return "plain";
+ case 2:
+ return "STARTTLS";
+ case 3:
+ return "SSL";
+ default:
+ return "invalid";
+ }
+ }
+
+ function authToString(authMethod) {
+ switch (authMethod) {
+ case 0:
+ return "undefined";
+ case 1:
+ return "none";
+ case 2:
+ return "old plain";
+ case 3:
+ return "plain";
+ case 4:
+ return "encrypted";
+ case 5:
+ return "Kerberos";
+ case 6:
+ return "NTLM";
+ case 7:
+ return "external/SSL";
+ case 8:
+ return "any secure";
+ case 10:
+ return "OAuth2";
+ default:
+ return "invalid";
+ }
+ }
+
+ function passwordToString(password) {
+ return password ? "set" : "not set";
+ }
+
+ function configToString(config) {
+ return (
+ config.type +
+ ", " +
+ config.hostname +
+ ":" +
+ config.port +
+ ", " +
+ sslToString(config.socketType) +
+ ", auth: " +
+ authToString(config.auth) +
+ ", username: " +
+ (config.username || "(undefined)") +
+ ", password: " +
+ passwordToString(config.password)
+ );
+ }
+
+ let result = "Incoming: " + configToString(this.incoming) + "\nOutgoing: ";
+ if (
+ this.outgoing.useGlobalPreferredServer ||
+ this.incoming.useGlobalPreferredServer
+ ) {
+ result += "Use global server";
+ } else if (this.outgoing.existingServerKey) {
+ result += "Use existing server " + this.outgoing.existingServerKey;
+ } else {
+ result += configToString(this.outgoing);
+ }
+ for (let config of this.incomingAlternatives) {
+ result += "\nIncoming alt: " + configToString(config);
+ }
+ for (let config of this.outgoingAlternatives) {
+ result += "\nOutgoing alt: " + configToString(config);
+ }
+ return result;
+ },
+
+ /**
+ * Sort the config alternatives such that exchange is the last of the
+ * alternatives.
+ */
+ preferStandardProtocols() {
+ let alternatives = this.incomingAlternatives;
+ // Add default incoming as one alternative.
+ alternatives.unshift(this.incoming);
+ alternatives.sort((a, b) => {
+ if (a.type == "exchange") {
+ return 1;
+ }
+ if (b.type == "exchange") {
+ return -1;
+ }
+ return 0;
+ });
+ this.incomingAlternatives = alternatives;
+ this.incoming = alternatives.shift();
+ },
+};
+
+// enum consts
+
+// .source
+AccountConfig.kSourceUser = "user"; // user manually entered the config
+AccountConfig.kSourceXML = "xml"; // config from XML from ISP or Mozilla DB
+AccountConfig.kSourceGuess = "guess"; // guessConfig()
+AccountConfig.kSourceExchange = "exchange"; // from Microsoft Exchange AutoDiscover
+
+/**
+ * Some fields on the account config accept placeholders (when coming from XML).
+ *
+ * These are the predefined ones
+ * %EMAILADDRESS% (full email address of the user, usually entered by user)
+ * %EMAILLOCALPART% (email address, part before @)
+ * %EMAILDOMAIN% (email address, part after @)
+ * %REALNAME%
+ * as well as those defined in account.inputFields.*.varname, with % added
+ * before and after.
+ *
+ * These must replaced with real values, supplied by the user or app,
+ * before the account is created. This is done here. You call this function once
+ * you have all the data - gathered the standard vars mentioned above as well as
+ * all listed in account.inputFields, and pass them in here. This function will
+ * insert them in the fields, returning a fully filled-out account ready to be
+ * created.
+ *
+ * @param account {AccountConfig}
+ * The account data to be modified. It may or may not contain placeholders.
+ * After this function, it should not contain placeholders anymore.
+ * This object will be modified in-place.
+ *
+ * @param emailfull {String}
+ * Full email address of this account, e.g. "joe@example.com".
+ * Empty of incomplete email addresses will/may be rejected.
+ *
+ * @param realname {String}
+ * Real name of user, as will appear in From of outgoing messages
+ *
+ * @param password {String}
+ * The password for the incoming server and (if necessary) the outgoing server
+ */
+AccountConfig.replaceVariables = function (
+ account,
+ realname,
+ emailfull,
+ password
+) {
+ lazy.Sanitizer.nonemptystring(emailfull);
+ let emailsplit = emailfull.split("@");
+ lazy.AccountCreationUtils.assert(
+ emailsplit.length == 2,
+ "email address not in expected format: must contain exactly one @"
+ );
+ let emaillocal = lazy.Sanitizer.nonemptystring(emailsplit[0]);
+ let emaildomain = lazy.Sanitizer.hostname(emailsplit[1]);
+ lazy.Sanitizer.label(realname);
+ lazy.Sanitizer.nonemptystring(realname);
+
+ let otherVariables = {};
+ otherVariables.EMAILADDRESS = emailfull;
+ otherVariables.EMAILLOCALPART = emaillocal;
+ otherVariables.EMAILDOMAIN = emaildomain;
+ otherVariables.REALNAME = realname;
+
+ if (password) {
+ account.incoming.password = password;
+ account.outgoing.password = password; // set member only if auth required?
+ }
+ account.incoming.username = _replaceVariable(
+ account.incoming.username,
+ otherVariables
+ );
+ account.outgoing.username = _replaceVariable(
+ account.outgoing.username,
+ otherVariables
+ );
+ account.incoming.hostname = _replaceVariable(
+ account.incoming.hostname,
+ otherVariables
+ );
+ if (account.outgoing.hostname) {
+ // will be null if user picked existing server.
+ account.outgoing.hostname = _replaceVariable(
+ account.outgoing.hostname,
+ otherVariables
+ );
+ }
+ account.identity.realname = _replaceVariable(
+ account.identity.realname,
+ otherVariables
+ );
+ account.identity.emailAddress = _replaceVariable(
+ account.identity.emailAddress,
+ otherVariables
+ );
+ account.displayName = _replaceVariable(account.displayName, otherVariables);
+};
+
+function _replaceVariable(variable, values) {
+ let str = variable;
+ if (typeof str != "string") {
+ return str;
+ }
+
+ for (let varname in values) {
+ str = str.replace("%" + varname + "%", values[varname]);
+ }
+
+ return str;
+}
diff --git a/comm/mail/components/accountcreation/AccountCreationUtils.jsm b/comm/mail/components/accountcreation/AccountCreationUtils.jsm
new file mode 100644
index 0000000000..f4efb96b2d
--- /dev/null
+++ b/comm/mail/components/accountcreation/AccountCreationUtils.jsm
@@ -0,0 +1,717 @@
+/* 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/. */
+/**
+ * Some common, generic functions
+ */
+
+const EXPORTED_SYMBOLS = ["AccountCreationUtils"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+const { AddonManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+);
+const { clearInterval, clearTimeout, setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+// --------------------------
+// Low level, basic functions
+
+function assert(test, errorMsg) {
+ if (!test) {
+ throw new NotReached(
+ errorMsg ? errorMsg : "Programming bug. Assertion failed, see log."
+ );
+ }
+}
+
+function makeCallback(obj, func) {
+ return func.bind(obj);
+}
+
+/**
+ * Runs the given function sometime later
+ *
+ * Currently implemented using setTimeout(), but
+ * can later be replaced with an nsITimer impl,
+ * when code wants to use it in a module.
+ *
+ * @see |TimeoutAbortable|
+ */
+function runAsync(func) {
+ return setTimeout(func, 0);
+}
+
+/**
+ * Reads UTF8 data from a URL.
+ *
+ * @param uri {nsIURI} - what you want to read
+ * @returns {Array of String} the contents of the file, one string per line
+ */
+function readURLasUTF8(uri) {
+ assert(uri instanceof Ci.nsIURI, "uri must be an nsIURI");
+ let chan = Services.io.newChannelFromURI(
+ uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ let is = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(
+ Ci.nsIConverterInputStream
+ );
+ is.init(
+ chan.open(),
+ "UTF-8",
+ 1024,
+ Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER
+ );
+
+ let content = "";
+ let strOut = {};
+ try {
+ while (is.readString(1024, strOut) != 0) {
+ content += strOut.value;
+ }
+ } finally {
+ is.close();
+ }
+
+ return content;
+ // TODO this has a numeric error message. We need to ship translations
+ // into human language.
+}
+
+/**
+ * @param bundleURI {String} - chrome URL to properties file
+ * @returns nsIStringBundle
+ */
+function getStringBundle(bundleURI) {
+ try {
+ return Services.strings.createBundle(bundleURI);
+ } catch (e) {
+ throw new Exception(
+ "Failed to get stringbundle URI <" + bundleURI + ">. Error: " + e
+ );
+ }
+}
+
+// ---------
+// Exception
+
+function Exception(msg) {
+ this._message = msg;
+ this.stack = Components.stack.formattedStack;
+}
+Exception.prototype = {
+ get message() {
+ return this._message;
+ },
+ toString() {
+ return this._message;
+ },
+};
+
+function NotReached(msg) {
+ Exception.call(this, msg); // call super constructor
+ console.error(this);
+}
+// Make NotReached extend Exception.
+NotReached.prototype = Object.create(Exception.prototype);
+NotReached.prototype.constructor = NotReached;
+
+// ---------
+// Abortable
+
+/**
+ * A handle for an async function which you can cancel.
+ * The async function will return an object of this type (a subtype)
+ * and you can call cancel() when you feel like killing the function.
+ */
+function Abortable() {}
+Abortable.prototype = {
+ cancel(e) {},
+};
+
+function CancelledException(msg) {
+ Exception.call(this, msg);
+}
+CancelledException.prototype = Object.create(Exception.prototype);
+CancelledException.prototype.constructor = CancelledException;
+
+function UserCancelledException(msg) {
+ // The user knows they cancelled so I don't see a need
+ // for a message to that effect.
+ if (!msg) {
+ msg = "User cancelled";
+ }
+ CancelledException.call(this, msg);
+}
+UserCancelledException.prototype = Object.create(CancelledException.prototype);
+UserCancelledException.prototype.constructor = UserCancelledException;
+
+/**
+ * Utility implementation, for waiting for a promise to resolve,
+ * but allowing its result to be cancelled.
+ */
+function PromiseAbortable(promise, successCallback, errorCallback) {
+ Abortable.call(this); // call super constructor
+ let complete = false;
+ this.cancel = function (e) {
+ if (!complete) {
+ complete = true;
+ errorCallback(e || new CancelledException());
+ }
+ };
+ promise
+ .then(function (result) {
+ if (!complete) {
+ successCallback(result);
+ complete = true;
+ }
+ })
+ .catch(function (e) {
+ if (!complete) {
+ complete = true;
+ errorCallback(e);
+ }
+ });
+}
+PromiseAbortable.prototype = Object.create(Abortable.prototype);
+PromiseAbortable.prototype.constructor = PromiseAbortable;
+
+/**
+ * Utility implementation, for allowing to abort a setTimeout.
+ * Use like: return new TimeoutAbortable(setTimeout(function(){ ... }, 0));
+ *
+ * @param setTimeoutID {Integer} - Return value of setTimeout()
+ */
+function TimeoutAbortable(setTimeoutID) {
+ Abortable.call(this); // call super constructor
+ this._id = setTimeoutID;
+}
+TimeoutAbortable.prototype = Object.create(Abortable.prototype);
+TimeoutAbortable.prototype.constructor = TimeoutAbortable;
+TimeoutAbortable.prototype.cancel = function () {
+ clearTimeout(this._id);
+};
+
+/**
+ * Utility implementation, for allowing to abort a setTimeout.
+ * Use like: return new TimeoutAbortable(setTimeout(function(){ ... }, 0));
+ *
+ * @param setIntervalID {Integer} - Return value of setInterval()
+ */
+function IntervalAbortable(setIntervalID) {
+ Abortable.call(this); // call super constructor
+ this._id = setIntervalID;
+}
+IntervalAbortable.prototype = Object.create(Abortable.prototype);
+IntervalAbortable.prototype.constructor = IntervalAbortable;
+IntervalAbortable.prototype.cancel = function () {
+ clearInterval(this._id);
+};
+
+/**
+ * Allows you to make several network calls,
+ * but return only one |Abortable| object.
+ */
+function SuccessiveAbortable() {
+ Abortable.call(this); // call super constructor
+ this._current = null;
+}
+SuccessiveAbortable.prototype = {
+ __proto__: Abortable.prototype,
+ get current() {
+ return this._current;
+ },
+ set current(abortable) {
+ assert(
+ abortable instanceof Abortable || abortable == null,
+ "need an Abortable object (or null)"
+ );
+ this._current = abortable;
+ },
+ cancel(e) {
+ if (this._current) {
+ this._current.cancel(e);
+ }
+ },
+};
+
+/**
+ * Allows you to make several network calls in parallel.
+ */
+function ParallelAbortable() {
+ Abortable.call(this); // call super constructor
+ // { Array of ParallelCall }
+ this._calls = [];
+ // { Array of Function }
+ this._finishedObservers = [];
+}
+ParallelAbortable.prototype = {
+ __proto__: Abortable.prototype,
+ /**
+ * @returns {Array of ParallelCall}
+ */
+ get results() {
+ return this._calls;
+ },
+ /**
+ * @returns {ParallelCall}
+ */
+ addCall() {
+ let call = new ParallelCall(this);
+ call.position = this._calls.length;
+ this._calls.push(call);
+ return call;
+ },
+ /**
+ * Observers will be called once one of the functions
+ * finishes, i.e. returns successfully or fails.
+ *
+ * @param {Function({ParallelCall} call)} func
+ */
+ addOneFinishedObserver(func) {
+ assert(typeof func == "function");
+ this._finishedObservers.push(func);
+ },
+ /**
+ * Will be called once *all* of the functions finished,
+ * It gives you a list of all functions that succeeded or failed,
+ * respectively.
+ *
+ * @param {Function(
+ * {Array of ParallelCall} succeeded,
+ * {Array of ParallelCall} failed
+ * )} func
+ */
+ addAllFinishedObserver(func) {
+ assert(typeof func == "function");
+ this.addOneFinishedObserver(() => {
+ if (this._calls.some(call => !call.finished)) {
+ return;
+ }
+ let succeeded = this._calls.filter(call => call.succeeded);
+ let failed = this._calls.filter(call => !call.succeeded);
+ func(succeeded, failed);
+ });
+ },
+ _notifyFinished(call) {
+ for (let observer of this._finishedObservers) {
+ try {
+ observer(call);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ },
+ cancel(e) {
+ for (let call of this._calls) {
+ if (!call.finished && call.callerAbortable) {
+ call.callerAbortable.cancel(e);
+ }
+ }
+ },
+};
+
+/**
+ * Returned by ParallelAbortable.addCall().
+ * Do not create this object directly
+ *
+ * @param {ParallelAbortable} parallelAbortable - The controlling ParallelAbortable
+ */
+function ParallelCall(parallelAbortable) {
+ assert(parallelAbortable instanceof ParallelAbortable);
+ // {ParallelAbortable} the parent
+ this._parallelAbortable = parallelAbortable;
+ // {Abortable} Abortable of the caller function that should run in parallel
+ this.callerAbortable = null;
+ // {Integer} the order in which the function was added, and its priority
+ this.position = null;
+ // {boolean} false = running, pending, false = success or failure
+ this.finished = false;
+ // {boolean} if finished: true = returned with success, false = returned with error
+ this.succeeded = false;
+ // {Exception} if failed: the error or exception that the caller function returned
+ this.e = null;
+ // {Object} if succeeded: the result of the caller function
+ this.result = null;
+
+ this._time = Date.now();
+}
+ParallelCall.prototype = {
+ /**
+ * Returns a successCallback(result) function that you pass
+ * to your function that runs in parallel.
+ *
+ * @returns {Function(result)} successCallback
+ */
+ successCallback() {
+ return result => {
+ ddump(
+ "call " +
+ this.position +
+ " took " +
+ (Date.now() - this._time) +
+ "ms and succeeded" +
+ (this.callerAbortable && this.callerAbortable._url
+ ? " at <" + this.callerAbortable._url + ">"
+ : "")
+ );
+ this.result = result;
+ this.finished = true;
+ this.succeeded = true;
+ this._parallelAbortable._notifyFinished(this);
+ };
+ },
+ /**
+ * Returns an errorCallback(e) function that you pass
+ * to your function that runs in parallel.
+ *
+ * @returns {Function(e)} errorCallback
+ */
+ errorCallback() {
+ return e => {
+ ddump(
+ "call " +
+ this.position +
+ " took " +
+ (Date.now() - this._time) +
+ "ms and failed with " +
+ (typeof e.code == "number" ? e.code + " " : "") +
+ (e.toString()
+ ? e.toString()
+ : "unknown error, probably no host connection") +
+ (this.callerAbortable && this.callerAbortable._url
+ ? " at <" + this.callerAbortable._url + ">"
+ : "")
+ );
+ this.e = e;
+ this.finished = true;
+ this.succeeded = false;
+ this._parallelAbortable._notifyFinished(this);
+ };
+ },
+ /**
+ * Call your function that needs to run in parallel
+ * and pass the resulting |Abortable| of your function here.
+ *
+ * @param {Abortable} abortable
+ */
+ setAbortable(abortable) {
+ assert(abortable instanceof Abortable);
+ this.callerAbortable = abortable;
+ },
+};
+
+/**
+ * Runs several calls in parallel.
+ * Returns the result of the "highest" priority call that succeeds.
+ * Unlike Promise.race(), does not return the fastest,
+ * but the first in the order they were added.
+ * So, the order in which the calls were added determines their priority,
+ * with the first to be added being the most desirable.
+ *
+ * E.g. the first failed, the second is pending, the third succeeded, and the forth is pending.
+ * It aborts the forth (because the third succeeded), and it waits for the second to return.
+ * If the second succeeds, it is the result, otherwise the third is the result.
+ *
+ * @param {Function(
+ * {Object} result - Result of winner call
+ * {ParallelCall} call - Winner call info
+ * )} successCallback - A call returned successfully
+ * @param {Function(e, allErrors)} errorCallback - All calls failed.
+ * {Exception} e - The first CancelledException, and otherwise
+ * the exception returned by the first call.
+ * This is just to adhere to the standard API of errorCallback(e).
+ * {Array of Exception} allErrors - The exceptions from all calls.
+ */
+function PriorityOrderAbortable(successCallback, errorCallback) {
+ assert(typeof successCallback == "function");
+ assert(typeof errorCallback == "function");
+ ParallelAbortable.call(this); // call super constructor
+ this._successfulCall = null;
+
+ this.addOneFinishedObserver(finishedCall => {
+ for (let call of this._calls) {
+ if (!call.finished) {
+ if (this._successfulCall) {
+ // abort
+ if (call.callerAbortable) {
+ call.callerAbortable.cancel(
+ new NoLongerNeededException("Another higher call succeeded")
+ );
+ }
+ continue;
+ }
+ // It's pending. do nothing and wait for it.
+ return;
+ }
+ if (!call.succeeded) {
+ // it failed. ignore it.
+ continue;
+ }
+ if (this._successfulCall) {
+ // we already have a winner. ignore it.
+ continue;
+ }
+ try {
+ successCallback(call.result, call);
+ // This is the winner.
+ this._successfulCall = call;
+ } catch (e) {
+ console.error(e);
+ // If the handler failed with this data, treat this call as failed.
+ call.e = e;
+ call.succeeded = false;
+ }
+ }
+ if (!this._successfulCall) {
+ // all failed
+ let allErrors = this._calls.map(call => call.e);
+ let e =
+ allErrors.find(e => e instanceof CancelledException) || allErrors[0];
+ errorCallback(e, allErrors); // see docs above
+ }
+ });
+}
+PriorityOrderAbortable.prototype = Object.create(ParallelAbortable.prototype);
+PriorityOrderAbortable.prototype.constructor = PriorityOrderAbortable;
+
+function NoLongerNeededException(msg) {
+ CancelledException.call(this, msg);
+}
+NoLongerNeededException.prototype = Object.create(CancelledException.prototype);
+NoLongerNeededException.prototype.constructor = NoLongerNeededException;
+
+// -------------------
+// High level features
+
+/**
+ * Allows you to install an addon.
+ *
+ * Example:
+ * var installer = new AddonInstaller({ xpiURL : "https://...xpi", id: "...", ...});
+ * installer.install();
+ *
+ * @param {object} args - Contains parameters:
+ * @param {string} name (Optional) - Name of the addon (not important)
+ * @param {string} id (Optional) - Addon ID
+ * If you pass an ID, and the addon is already installed (and the version matches),
+ * then install() will do nothing.
+ * After the XPI is downloaded, the ID will be verified. If it doesn't match, the
+ * install will fail.
+ * If you don't pass an ID, these checks will be skipped and the addon be installed
+ * unconditionally.
+ * It is recommended to pass at least an ID, because it can confuse some addons
+ * to be reloaded at runtime.
+ * @param {string} minVersion (Optional) - Minimum version of the addon
+ * If you pass a minVersion (in addition to ID), and the installed addon is older than this,
+ * the install will be done anyway. If the downloaded addon has a lower version,
+ * the install will fail.
+ * If you do not pass a minVersion, there will be no version check.
+ * @param {URL} xpiURL - Where to download the XPI from
+ */
+function AddonInstaller(args) {
+ Abortable.call(this);
+ this._name = lazy.Sanitizer.label(args.name);
+ this._id = lazy.Sanitizer.string(args.id);
+ this._minVersion = lazy.Sanitizer.string(args.minVersion);
+ this._url = lazy.Sanitizer.url(args.xpiURL);
+}
+AddonInstaller.prototype = Object.create(Abortable.prototype);
+AddonInstaller.prototype.constructor = AddonInstaller;
+
+/**
+ * Checks whether the passed-in addon matches the
+ * id and minVersion requested by the caller.
+ *
+ * @param {nsIAddon} addon
+ * @returns {boolean} is OK
+ */
+AddonInstaller.prototype.matches = function (addon) {
+ return (
+ !this._id ||
+ (this._id == addon.id &&
+ (!this._minVersion ||
+ Services.vc.compare(addon.version, this._minVersion) >= 0))
+ );
+};
+
+/**
+ * Start the installation
+ *
+ * @throws Exception in case of failure
+ */
+AddonInstaller.prototype.install = async function () {
+ if (await this.isInstalled()) {
+ return;
+ }
+ await this._installDirect();
+};
+
+/**
+ * Checks whether we already have an addon installed that matches the
+ * id and minVersion requested by the caller.
+ *
+ * @returns {boolean} is already installed and enabled
+ */
+AddonInstaller.prototype.isInstalled = async function () {
+ if (!this._id) {
+ return false;
+ }
+ var addon = await AddonManager.getAddonByID(this._id);
+ return addon && this.matches(addon) && addon.isActive;
+};
+
+/**
+ * Checks whether we already have an addon but it is disabled.
+ *
+ * @returns {boolean} is already installed but disabled
+ */
+AddonInstaller.prototype.isDisabled = async function () {
+ if (!this._id) {
+ return false;
+ }
+ let addon = await AddonManager.getAddonByID(this._id);
+ return addon && !addon.isActive;
+};
+
+/**
+ * Downloads and installs the addon.
+ * The downloaded XPI will be checked using prompt().
+ */
+AddonInstaller.prototype._installDirect = async function () {
+ var installer = (this._installer = await AddonManager.getInstallForURL(
+ this._url,
+ { name: this._name }
+ ));
+ installer.promptHandler = makeCallback(this, this.prompt);
+ await installer.install(); // throws, if failed
+
+ var addon = await AddonManager.getAddonByID(this._id);
+ await addon.enable();
+
+ // Wait for addon startup code to finish
+ // Fixes: verify password fails with NOT_AVAILABLE in createIncomingServer()
+ if ("startupPromise" in addon) {
+ await addon.startupPromise;
+ }
+ let wait = ms => new Promise(resolve => setTimeout(resolve, ms));
+ await wait(1000);
+};
+
+/**
+ * Install confirmation. You may override this, if needed.
+ *
+ * @throws Exception If you want to cancel install, then throw an exception.
+ */
+AddonInstaller.prototype.prompt = async function (info) {
+ if (!this.matches(info.addon)) {
+ // happens only when we got the wrong XPI
+ throw new Exception(
+ "The downloaded addon XPI does not match the minimum requirements"
+ );
+ }
+};
+
+AddonInstaller.prototype.cancel = function () {
+ if (this._installer) {
+ try {
+ this._installer.cancel();
+ } catch (e) {
+ // if install failed
+ ddump(e);
+ }
+ }
+};
+
+// ------------
+// Debug output
+
+function deepCopy(org) {
+ if (typeof org == "undefined") {
+ return undefined;
+ }
+ if (org == null) {
+ return null;
+ }
+ if (typeof org == "string") {
+ return org;
+ }
+ if (typeof org == "number") {
+ return org;
+ }
+ if (typeof org == "boolean") {
+ return org;
+ }
+ if (typeof org == "function") {
+ return org;
+ }
+ if (typeof org != "object") {
+ throw new Error("can't copy objects of type " + typeof org + " yet");
+ }
+
+ // TODO still instanceof org != instanceof copy
+ // var result = new org.constructor();
+ var result = {};
+ if (typeof org.length != "undefined") {
+ result = [];
+ }
+ for (var prop in org) {
+ result[prop] = deepCopy(org[prop]);
+ }
+ return result;
+}
+
+var gAccountSetupLogger = new ConsoleAPI({
+ prefix: "mail.setup",
+ maxLogLevel: "warn",
+ maxLogLevelPref: "mail.setup.loglevel",
+});
+
+function ddump(text) {
+ gAccountSetupLogger.info(text);
+}
+
+function alertPrompt(alertTitle, alertMsg) {
+ Services.prompt.alert(
+ Services.wm.getMostRecentWindow(""),
+ alertTitle,
+ alertMsg
+ );
+}
+
+var AccountCreationUtils = {
+ Abortable,
+ AddonInstaller,
+ alertPrompt,
+ assert,
+ CancelledException,
+ ddump,
+ deepCopy,
+ Exception,
+ gAccountSetupLogger,
+ getStringBundle,
+ NotReached,
+ PriorityOrderAbortable,
+ PromiseAbortable,
+ readURLasUTF8,
+ runAsync,
+ SuccessiveAbortable,
+ TimeoutAbortable,
+ UserCancelledException,
+};
diff --git a/comm/mail/components/accountcreation/ConfigVerifier.jsm b/comm/mail/components/accountcreation/ConfigVerifier.jsm
new file mode 100644
index 0000000000..cce934a159
--- /dev/null
+++ b/comm/mail/components/accountcreation/ConfigVerifier.jsm
@@ -0,0 +1,386 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["ConfigVerifier"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { OAuth2Providers } = ChromeUtils.import(
+ "resource:///modules/OAuth2Providers.jsm"
+);
+const { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+
+/**
+ * @implements {nsIUrlListener}
+ * @implements {nsIInterfaceRequestor}
+ */
+class ConfigVerifier {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIInterfaceRequestor",
+ "nsIUrlListener",
+ ]);
+
+ // @see {nsIInterfaceRequestor}
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ }
+
+ constructor(msgWindow) {
+ this.msgWindow = msgWindow;
+ this._log = console.createInstance({
+ prefix: "mail.setup",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.setup.loglevel",
+ });
+ }
+
+ /**
+ * @param {nsIURI} url - The URL being processed.
+ * @see {nsIUrlListener}
+ */
+ OnStartRunningUrl(url) {
+ this._log.debug(`Starting to verify configuration;
+ email as username=${
+ this.config.incoming.username != this.config.identity.emailAddress
+ }
+ savedUsername=${this.config.usernameSaved ? "true" : "false"},
+ authMethod=${this.server.authMethod}`);
+ }
+
+ /**
+ * @param {nsIURI} url - The URL being processed.
+ * @param {nsresult} status - A result code of URL processing.
+ * @see {nsIUrlListener}
+ */
+ OnStopRunningUrl(url, status) {
+ if (Components.isSuccessCode(status)) {
+ this._log.debug(`Configuration verified successfully!`);
+ this.cleanup();
+ this.successCallback(this.config);
+ return;
+ }
+
+ this._log.debug(`Verifying configuration failed; status=${status}`);
+
+ let certError = false;
+ try {
+ let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService(
+ Ci.nsINSSErrorsService
+ );
+ let errorClass = nssErrorsService.getErrorClass(status);
+ if (errorClass == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ certError = true;
+ }
+ } catch (e) {
+ // It's not an NSS error.
+ }
+
+ if (certError) {
+ let mailNewsUrl = url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ let secInfo = mailNewsUrl.failedSecInfo;
+ this.informUserOfCertError(secInfo, url.asciiHostPort);
+ } else if (this.alter) {
+ // Try other variations.
+ this.server.closeCachedConnections();
+ this.tryNextLogon(url);
+ } else {
+ // Logon failed, and we aren't supposed to try other variations.
+ this._failed(url);
+ }
+ }
+
+ tryNextLogon(aPreviousUrl) {
+ this._log.debug("Trying next logon variation");
+ // check if we tried full email address as username
+ if (this.config.incoming.username != this.config.identity.emailAddress) {
+ this._log.debug("Changing username to email address.");
+ this.config.usernameSaved = this.config.incoming.username;
+ this.config.incoming.username = this.config.identity.emailAddress;
+ this.config.outgoing.username = this.config.identity.emailAddress;
+ this.server.username = this.config.incoming.username;
+ this.server.password = this.config.incoming.password;
+ this.verifyLogon();
+ return;
+ }
+
+ if (this.config.usernameSaved) {
+ this._log.debug("Re-setting username.");
+ // If we tried the full email address as the username, then let's go
+ // back to trying just the username before trying the other cases.
+ this.config.incoming.username = this.config.usernameSaved;
+ this.config.outgoing.username = this.config.usernameSaved;
+ this.config.usernameSaved = null;
+ this.server.username = this.config.incoming.username;
+ this.server.password = this.config.incoming.password;
+ }
+
+ // sec auth seems to have failed, and we've tried both
+ // varieties of user name, sadly.
+ // So fall back to non-secure auth, and
+ // again try the user name and email address as username
+ if (this.server.socketType == Ci.nsMsgSocketType.SSL) {
+ this._log.debug("Using SSL");
+ } else if (this.server.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS) {
+ this._log.debug("Using STARTTLS");
+ }
+ if (
+ this.config.incoming.authAlternatives &&
+ this.config.incoming.authAlternatives.length
+ ) {
+ // We may be dropping back to insecure auth methods here,
+ // which is not good. But then again, we already warned the user,
+ // if it is a config without SSL.
+
+ let brokenAuth = this.config.incoming.auth;
+ // take the next best method (compare chooseBestAuthMethod() in guess)
+ this.config.incoming.auth = this.config.incoming.authAlternatives.shift();
+ this.server.authMethod = this.config.incoming.auth;
+ // Assume that SMTP server has same methods working as incoming.
+ // Broken assumption, but we currently have no SMTP verification.
+ // TODO: implement real SMTP verification
+ if (
+ this.config.outgoing.auth == brokenAuth &&
+ this.config.outgoing.authAlternatives.includes(
+ this.config.incoming.auth
+ )
+ ) {
+ this.config.outgoing.auth = this.config.incoming.auth;
+ }
+ this._log.debug(`Trying next auth method: ${this.server.authMethod}`);
+ this.verifyLogon();
+ return;
+ }
+
+ // Tried all variations we can. Give up.
+ this._log.debug("Have tried all variations. Giving up.");
+ this._failed(aPreviousUrl);
+ }
+
+ /**
+ * Clear out the server we had created for use during testing.
+ */
+ cleanup() {
+ try {
+ if (this.server) {
+ MailServices.accounts.removeIncomingServer(this.server, true);
+ this.server = null;
+ }
+ } catch (e) {
+ this._log.error(e);
+ }
+ }
+
+ /**
+ * @param {nsIURI} url - The URL being processed.
+ */
+ _failed(url) {
+ this.cleanup();
+ url = url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ let code = url.errorCode || "login-error-unknown";
+ let msg = url.errorMessage;
+ // *Only* for known (!) username/password errors, show our message.
+ // But there are 1000 other reasons why it could have failed, e.g.
+ // server not reachable, bad auth method, server hiccups, or even
+ // custom server messages that tell the user to do something,
+ // so show the backend error message, unless we are certain
+ // that it's a wrong username or password.
+ if (
+ !msg || // Normal IMAP login error sets no error msg
+ code == "pop3UsernameFailure" ||
+ code == "pop3PasswordFailed" ||
+ code == "imapOAuth2Error"
+ ) {
+ msg = AccountCreationUtils.getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties"
+ ).GetStringFromName("cannot_login.error");
+ }
+ this.errorCallback(new Error(msg));
+ }
+
+ /**
+ * Inform users that we got a certificate error for the specified location.
+ * Allow them to add an exception for it.
+ *
+ * @param {nsITransportSecurityInfo} secInfo
+ * @param {string} location - "host:port" that had the problem.
+ */
+ informUserOfCertError(secInfo, location) {
+ this._log.debug(`Informing user about cert error for ${location}`);
+ let params = {
+ exceptionAdded: false,
+ securityInfo: secInfo,
+ prefetchCert: true,
+ location,
+ };
+ Services.wm
+ .getMostRecentWindow("mail:3pane")
+ .browsingContext.topChromeWindow.openDialog(
+ "chrome://pippki/content/exceptionDialog.xhtml",
+ "exceptionDialog",
+ "chrome,centerscreen,modal",
+ params
+ );
+ if (!params.exceptionAdded) {
+ this._log.debug(`Did not accept exception for ${location}`);
+ this.cleanup();
+ let errorMsg = AccountCreationUtils.getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties"
+ ).GetStringFromName("cannot_login.error");
+ this.errorCallback(new Error(errorMsg));
+ } else {
+ this._log.debug(`Accept exception for ${location} - will retry logon.`);
+ // Retry the logon now that we've added the cert exception.
+ this.verifyLogon();
+ }
+ }
+
+ /**
+ * This checks a given config, by trying a real connection and login,
+ * with username and password.
+ *
+ * @param {AccountConfig} config - The guessed account config.
+ * username, password, realname, emailaddress etc. are not filled out,
+ * but placeholders to be filled out via replaceVariables().
+ * @param alter {boolean} - Try other usernames and login schemes, until
+ * login works. Warning: Modifies |config|.
+ * @returns {Promise<AccountConfig>} the successful configuration.
+ * @throws {Error} when we could guess not the config, either
+ * because we have not found anything or because there was an error
+ * (e.g. no network connection).
+ * The ex.message will contain a user-presentable message.
+ */
+ async verifyConfig(config, alter) {
+ this.alter = alter;
+ return new Promise((resolve, reject) => {
+ this.config = config;
+ this.successCallback = resolve;
+ this.errorCallback = reject;
+ if (
+ MailServices.accounts.findServer(
+ config.incoming.username,
+ config.incoming.hostname,
+ config.incoming.type,
+ config.incoming.port
+ )
+ ) {
+ reject(new Error("Incoming server exists"));
+ return;
+ }
+
+ // incoming server
+ if (!this.server) {
+ this.server = MailServices.accounts.createIncomingServer(
+ config.incoming.username,
+ config.incoming.hostname,
+ config.incoming.type
+ );
+ }
+ this.server.port = config.incoming.port;
+ this.server.password = config.incoming.password;
+ this.server.socketType = config.incoming.socketType;
+
+ this._log.info(
+ "Setting incoming server authMethod to " + config.incoming.auth
+ );
+ this.server.authMethod = config.incoming.auth;
+
+ try {
+ // Lookup OAuth2 issuer if needed.
+ // -- Incoming.
+ if (
+ config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2 &&
+ (!config.incoming.oauthSettings ||
+ !config.incoming.oauthSettings.issuer ||
+ !config.incoming.oauthSettings.scope)
+ ) {
+ let details = OAuth2Providers.getHostnameDetails(
+ config.incoming.hostname
+ );
+ if (!details) {
+ reject(
+ new Error(
+ `Could not get OAuth2 details for hostname=${config.incoming.hostname}.`
+ )
+ );
+ }
+ config.incoming.oauthSettings = {
+ issuer: details[0],
+ scope: details[1],
+ };
+ }
+ // -- Outgoing.
+ if (
+ config.outgoing.auth == Ci.nsMsgAuthMethod.OAuth2 &&
+ (!config.outgoing.oauthSettings ||
+ !config.outgoing.oauthSettings.issuer ||
+ !config.outgoing.oauthSettings.scope)
+ ) {
+ let details = OAuth2Providers.getHostnameDetails(
+ config.outgoing.hostname
+ );
+ if (!details) {
+ reject(
+ new Error(
+ `Could not get OAuth2 details for hostname=${config.outgoing.hostname}.`
+ )
+ );
+ }
+ config.outgoing.oauthSettings = {
+ issuer: details[0],
+ scope: details[1],
+ };
+ }
+ if (config.incoming.owaURL) {
+ this.server.setUnicharValue("owa_url", config.incoming.owaURL);
+ }
+ if (config.incoming.ewsURL) {
+ this.server.setUnicharValue("ews_url", config.incoming.ewsURL);
+ }
+ if (config.incoming.easURL) {
+ this.server.setUnicharValue("eas_url", config.incoming.easURL);
+ }
+
+ if (
+ this.server.password ||
+ this.server.authMethod == Ci.nsMsgAuthMethod.OAuth2
+ ) {
+ this.verifyLogon();
+ } else {
+ this.cleanup();
+ resolve(config);
+ }
+ } catch (e) {
+ this._log.info("verifyConfig failed: " + e);
+ this.cleanup();
+ reject(e);
+ }
+ });
+ }
+
+ /**
+ * Verify that the provided credentials can log in to the incoming server.
+ */
+ verifyLogon() {
+ this._log.info("verifyLogon for server at " + this.server.hostName);
+ // Save away the old callbacks.
+ let saveCallbacks = this.msgWindow.notificationCallbacks;
+ // Set our own callbacks - this works because verifyLogon will
+ // synchronously create the transport and use the notification callbacks.
+ // Our listener listens both for the url and cert errors.
+ this.msgWindow.notificationCallbacks = this;
+ // try to work around bug where backend is clearing password.
+ try {
+ this.server.password = this.config.incoming.password;
+ let uri = this.server.verifyLogon(this, this.msgWindow);
+ // clear msgWindow so url won't prompt for passwords.
+ uri.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = null;
+ } finally {
+ // restore them
+ this.msgWindow.notificationCallbacks = saveCallbacks;
+ }
+ }
+}
diff --git a/comm/mail/components/accountcreation/CreateInBackend.jsm b/comm/mail/components/accountcreation/CreateInBackend.jsm
new file mode 100644
index 0000000000..c254bbb44b
--- /dev/null
+++ b/comm/mail/components/accountcreation/CreateInBackend.jsm
@@ -0,0 +1,459 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["CreateInBackend"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "AccountConfig",
+ "resource:///modules/accountcreation/AccountConfig.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "AccountCreationUtils",
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* eslint-disable complexity */
+/**
+ * Takes an |AccountConfig| JS object and creates that account in the
+ * Thunderbird backend (which also writes it to prefs).
+ *
+ * @param {AccountConfig} config - The account to create
+ * @returns {nsIMsgAccount} - the newly created account
+ */
+function createAccountInBackend(config) {
+ // incoming server
+ let inServer = MailServices.accounts.createIncomingServer(
+ config.incoming.username,
+ config.incoming.hostname,
+ config.incoming.type
+ );
+ inServer.port = config.incoming.port;
+ inServer.authMethod = config.incoming.auth;
+ inServer.password = config.incoming.password;
+ // This new CLIENTID is for the outgoing server, and will be applied to the
+ // incoming only if the incoming username and hostname match the outgoing.
+ // We must generate this unconditionally because we cannot determine whether
+ // the outgoing server has clientid enabled yet or not, and we need to do it
+ // here in order to populate the incoming server if the outgoing matches.
+ let newOutgoingClientid = Services.uuid
+ .generateUUID()
+ .toString()
+ .replace(/[{}]/g, "");
+ // Grab the base domain of both incoming and outgoing hostname in order to
+ // compare the two to detect if the base domain is the same.
+ let incomingBaseDomain;
+ let outgoingBaseDomain;
+ try {
+ incomingBaseDomain = Services.eTLD.getBaseDomainFromHost(
+ config.incoming.hostname
+ );
+ } catch (e) {
+ incomingBaseDomain = config.incoming.hostname;
+ }
+ try {
+ outgoingBaseDomain = Services.eTLD.getBaseDomainFromHost(
+ config.outgoing.hostname
+ );
+ } catch (e) {
+ outgoingBaseDomain = config.outgoing.hostname;
+ }
+ if (
+ config.incoming.username == config.outgoing.username &&
+ incomingBaseDomain == outgoingBaseDomain
+ ) {
+ inServer.clientid = newOutgoingClientid;
+ } else {
+ // If the username/hostname are different then generate a new CLIENTID.
+ inServer.clientid = Services.uuid
+ .generateUUID()
+ .toString()
+ .replace(/[{}]/g, "");
+ }
+
+ if (config.rememberPassword && config.incoming.password) {
+ rememberPassword(inServer, config.incoming.password);
+ }
+
+ if (inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
+ inServer.setUnicharValue(
+ "oauth2.scope",
+ config.incoming.oauthSettings.scope
+ );
+ inServer.setUnicharValue(
+ "oauth2.issuer",
+ config.incoming.oauthSettings.issuer
+ );
+ }
+
+ // SSL
+ inServer.socketType = config.incoming.socketType;
+
+ // If we already have an account with an identical name, generate a unique
+ // name for the new account to avoid duplicates.
+ inServer.prettyName = checkAccountNameAlreadyExists(
+ config.identity.emailAddress
+ )
+ ? generateUniqueAccountName(config)
+ : config.identity.emailAddress;
+
+ inServer.doBiff = true;
+ inServer.biffMinutes = config.incoming.checkInterval;
+ inServer.setBoolValue("login_at_startup", config.incoming.loginAtStartup);
+ if (config.incoming.type == "pop3") {
+ inServer.setBoolValue(
+ "leave_on_server",
+ config.incoming.leaveMessagesOnServer
+ );
+ inServer.setIntValue(
+ "num_days_to_leave_on_server",
+ config.incoming.daysToLeaveMessagesOnServer
+ );
+ inServer.setBoolValue(
+ "delete_mail_left_on_server",
+ config.incoming.deleteOnServerWhenLocalDelete
+ );
+ inServer.setBoolValue(
+ "delete_by_age_from_server",
+ config.incoming.deleteByAgeFromServer
+ );
+ inServer.setBoolValue("download_on_biff", config.incoming.downloadOnBiff);
+ }
+ if (config.incoming.owaURL) {
+ inServer.setUnicharValue("owa_url", config.incoming.owaURL);
+ }
+ if (config.incoming.ewsURL) {
+ inServer.setUnicharValue("ews_url", config.incoming.ewsURL);
+ }
+ if (config.incoming.easURL) {
+ inServer.setUnicharValue("eas_url", config.incoming.easURL);
+ }
+ inServer.valid = true;
+
+ let username =
+ config.outgoing.auth != Ci.nsMsgAuthMethod.none
+ ? config.outgoing.username
+ : null;
+ let outServer = MailServices.smtp.findServer(
+ username,
+ config.outgoing.hostname
+ );
+ lazy.AccountCreationUtils.assert(
+ config.outgoing.addThisServer ||
+ config.outgoing.useGlobalPreferredServer ||
+ config.outgoing.existingServerKey,
+ "No SMTP server: inconsistent flags"
+ );
+
+ if (
+ config.outgoing.addThisServer &&
+ !outServer &&
+ !config.incoming.useGlobalPreferredServer
+ ) {
+ outServer = MailServices.smtp.createServer();
+ outServer.hostname = config.outgoing.hostname;
+ outServer.port = config.outgoing.port;
+ outServer.authMethod = config.outgoing.auth;
+ // Populate the clientid if it is enabled for this outgoing server.
+ if (outServer.clientidEnabled) {
+ outServer.clientid = newOutgoingClientid;
+ }
+ if (config.outgoing.auth != Ci.nsMsgAuthMethod.none) {
+ outServer.username = username;
+ outServer.password = config.outgoing.password;
+ if (config.rememberPassword && config.outgoing.password) {
+ rememberPassword(outServer, config.outgoing.password);
+ }
+ }
+
+ if (outServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
+ let prefBranch = "mail.smtpserver." + outServer.key + ".";
+ Services.prefs.setCharPref(
+ prefBranch + "oauth2.scope",
+ config.outgoing.oauthSettings.scope
+ );
+ Services.prefs.setCharPref(
+ prefBranch + "oauth2.issuer",
+ config.outgoing.oauthSettings.issuer
+ );
+ }
+
+ outServer.socketType = config.outgoing.socketType;
+ outServer.description = config.displayName;
+
+ // If this is the first SMTP server, set it as default
+ if (
+ !MailServices.smtp.defaultServer ||
+ !MailServices.smtp.defaultServer.hostname
+ ) {
+ MailServices.smtp.defaultServer = outServer;
+ }
+ }
+
+ // identity
+ // TODO accounts without identity?
+ let identity = MailServices.accounts.createIdentity();
+ identity.fullName = config.identity.realname;
+ identity.email = config.identity.emailAddress;
+
+ // for new accounts, default to replies being positioned above the quote
+ // if a default account is defined already, take its settings instead
+ if (config.incoming.type == "imap" || config.incoming.type == "pop3") {
+ identity.replyOnTop = 1;
+ // identity.sigBottom = false; // don't set this until Bug 218346 is fixed
+
+ if (
+ MailServices.accounts.accounts.length &&
+ MailServices.accounts.defaultAccount
+ ) {
+ let defAccount = MailServices.accounts.defaultAccount;
+ let defIdentity = defAccount.defaultIdentity;
+ if (
+ defAccount.incomingServer.canBeDefaultServer &&
+ defIdentity &&
+ defIdentity.valid
+ ) {
+ identity.replyOnTop = defIdentity.replyOnTop;
+ identity.sigBottom = defIdentity.sigBottom;
+ }
+ }
+ }
+
+ // due to accepted conventions, news accounts should default to plain text
+ if (config.incoming.type == "nntp") {
+ identity.composeHtml = false;
+ }
+
+ identity.valid = true;
+
+ if (
+ !config.outgoing.useGlobalPreferredServer &&
+ !config.incoming.useGlobalPreferredServer
+ ) {
+ if (config.outgoing.existingServerKey) {
+ identity.smtpServerKey = config.outgoing.existingServerKey;
+ } else {
+ identity.smtpServerKey = outServer.key;
+ }
+ }
+
+ // account and hook up
+ // Note: Setting incomingServer will cause the AccountManager to refresh
+ // itself, which could be a problem if we came from it and we haven't set
+ // the identity (see bug 521955), so make sure everything else on the
+ // account is set up before you set the incomingServer.
+ let account = MailServices.accounts.createAccount();
+ account.addIdentity(identity);
+ account.incomingServer = inServer;
+ if (
+ inServer.canBeDefaultServer &&
+ (!MailServices.accounts.defaultAccount ||
+ !MailServices.accounts.defaultAccount.incomingServer.canBeDefaultServer)
+ ) {
+ MailServices.accounts.defaultAccount = account;
+ }
+
+ verifyLocalFoldersAccount(MailServices.accounts);
+ setFolders(identity, inServer);
+
+ // save
+ MailServices.accounts.saveAccountInfo();
+ try {
+ Services.prefs.savePrefFile(null);
+ } catch (ex) {
+ lazy.AccountCreationUtils.ddump("Could not write out prefs: " + ex);
+ }
+ return account;
+}
+/* eslint-enable complexity */
+
+function setFolders(identity, server) {
+ // TODO: support for local folders for global inbox (or use smart search
+ // folder instead)
+
+ var baseURI = server.serverURI + "/";
+
+ // Names will be localized in UI, not in folder names on server/disk
+ // TODO allow to override these names in the XML config file,
+ // in case e.g. Google or AOL use different names?
+ // Workaround: Let user fix it :)
+ var fccName = "Sent";
+ var draftName = "Drafts";
+ var templatesName = "Templates";
+
+ identity.draftFolder = baseURI + draftName;
+ identity.stationeryFolder = baseURI + templatesName;
+ identity.fccFolder = baseURI + fccName;
+
+ identity.fccFolderPickerMode = 0;
+ identity.draftsFolderPickerMode = 0;
+ identity.tmplFolderPickerMode = 0;
+}
+
+function rememberPassword(server, password) {
+ let passwordURI;
+ if (server instanceof Ci.nsIMsgIncomingServer) {
+ passwordURI = server.localStoreType + "://" + server.hostName;
+ } else if (server instanceof Ci.nsISmtpServer) {
+ passwordURI = "smtp://" + server.hostname;
+ } else {
+ throw new lazy.AccountCreationUtils.NotReached("Server type not supported");
+ }
+
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(passwordURI, null, passwordURI, server.username, password, "", "");
+ try {
+ Services.logins.addLogin(login);
+ } catch (e) {
+ if (e.message.includes("This login already exists")) {
+ // TODO modify
+ } else {
+ throw e;
+ }
+ }
+}
+
+/**
+ * Check whether the user's setup already has an incoming server
+ * which matches (hostname, port, username) the primary one
+ * in the config.
+ * (We also check the email address as username.)
+ *
+ * @param config {AccountConfig} filled in (no placeholders)
+ * @returns {nsIMsgIncomingServer} If it already exists, the server
+ * object is returned.
+ * If it's a new server, |null| is returned.
+ */
+function checkIncomingServerAlreadyExists(config) {
+ lazy.AccountCreationUtils.assert(config instanceof lazy.AccountConfig);
+ let incoming = config.incoming;
+ let existing = MailServices.accounts.findServer(
+ incoming.username,
+ incoming.hostname,
+ incoming.type,
+ incoming.port
+ );
+
+ // if username does not have an '@', also check the e-mail
+ // address form of the name.
+ if (!existing && !incoming.username.includes("@")) {
+ existing = MailServices.accounts.findServer(
+ config.identity.emailAddress,
+ incoming.hostname,
+ incoming.type,
+ incoming.port
+ );
+ }
+ return existing;
+}
+
+/**
+ * Check whether the user's setup already has an outgoing server
+ * which matches (hostname, port, username) the primary one
+ * in the config.
+ *
+ * @param config {AccountConfig} filled in (no placeholders)
+ * @returns {nsISmtpServer} If it already exists, the server
+ * object is returned.
+ * If it's a new server, |null| is returned.
+ */
+function checkOutgoingServerAlreadyExists(config) {
+ lazy.AccountCreationUtils.assert(config instanceof lazy.AccountConfig);
+ for (let existingServer of MailServices.smtp.servers) {
+ // TODO check username with full email address, too, like for incoming
+ if (
+ existingServer.hostname == config.outgoing.hostname &&
+ existingServer.port == config.outgoing.port &&
+ existingServer.username == config.outgoing.username
+ ) {
+ return existingServer;
+ }
+ }
+ return null;
+}
+
+/**
+ * Check whether the user's setup already has an account with the same email
+ * address. This might happen if the user uses the same email for different
+ * protocols (eg. IMAP and POP3).
+ *
+ * @param {string} name - The name or email address of the new account.
+ * @returns {boolean} True if an account with the same name is found.
+ */
+function checkAccountNameAlreadyExists(name) {
+ return MailServices.accounts.accounts.some(
+ a => a.incomingServer.prettyName == name
+ );
+}
+
+/**
+ * Generate a unique account name by appending the incoming protocol type, and
+ * a counter if necessary.
+ *
+ * @param {AccountConfig} config - The config data of the account being created.
+ * @returns {string} - The unique account name.
+ */
+function generateUniqueAccountName(config) {
+ // Generate a potential unique name. e.g. "foo@bar.com (POP3)".
+ let name = `${
+ config.identity.emailAddress
+ } (${config.incoming.type.toUpperCase()})`;
+
+ // If this name already exists, append a counter until we find a unique name.
+ if (checkAccountNameAlreadyExists(name)) {
+ let counter = 2;
+ while (checkAccountNameAlreadyExists(`${name}_${counter}`)) {
+ counter++;
+ }
+ // e.g. "foo@bar.com (POP3)_1".
+ name = `${name}_${counter}`;
+ }
+
+ return name;
+}
+
+/**
+ * Check if there already is a "Local Folders". If not, create it.
+ * Copied from AccountWizard.js with minor updates.
+ */
+function verifyLocalFoldersAccount(am) {
+ let localMailServer;
+ try {
+ localMailServer = am.localFoldersServer;
+ } catch (ex) {
+ localMailServer = null;
+ }
+
+ try {
+ if (!localMailServer) {
+ // creates a copy of the identity you pass in
+ am.createLocalMailAccount();
+ try {
+ localMailServer = am.localFoldersServer;
+ } catch (ex) {
+ lazy.AccountCreationUtils.ddump(
+ "Error! we should have found the local mail server " +
+ "after we created it."
+ );
+ }
+ }
+ } catch (ex) {
+ lazy.AccountCreationUtils.ddump("Error in verifyLocalFoldersAccount " + ex);
+ }
+}
+
+var CreateInBackend = {
+ checkIncomingServerAlreadyExists,
+ checkOutgoingServerAlreadyExists,
+ createAccountInBackend,
+};
diff --git a/comm/mail/components/accountcreation/ExchangeAutoDiscover.jsm b/comm/mail/components/accountcreation/ExchangeAutoDiscover.jsm
new file mode 100644
index 0000000000..5813aa0240
--- /dev/null
+++ b/comm/mail/components/accountcreation/ExchangeAutoDiscover.jsm
@@ -0,0 +1,676 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["fetchConfigFromExchange", "getAddonsList"];
+
+var { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ AccountConfig: "resource:///modules/accountcreation/AccountConfig.jsm",
+ FetchHTTP: "resource:///modules/accountcreation/FetchHTTP.jsm",
+ GuessConfig: "resource:///modules/accountcreation/GuessConfig.jsm",
+ Sanitizer: "resource:///modules/accountcreation/Sanitizer.jsm",
+});
+
+var {
+ Abortable,
+ assert,
+ ddump,
+ deepCopy,
+ Exception,
+ gAccountSetupLogger,
+ getStringBundle,
+ PriorityOrderAbortable,
+ SuccessiveAbortable,
+ TimeoutAbortable,
+} = AccountCreationUtils;
+
+/**
+ * Tries to get a configuration from an MS Exchange server
+ * using Microsoft AutoDiscover protocol.
+ *
+ * Disclaimers:
+ * - To support domain hosters, we cannot use SSL. That means we
+ * rely on insecure DNS and http, which means the results may be
+ * forged when under attack. The same is true for guessConfig(), though.
+ *
+ * @param {string} domain - The domain part of the user's email address
+ * @param {string} emailAddress - The user's email address
+ * @param {string} username - (Optional) The user's login name.
+ * If null, email address will be used.
+ * @param {string} password - The user's password for that email address
+ * @param {Function(domain, okCallback, cancelCallback)} confirmCallback - A
+ * callback that will be called to confirm redirection to another domain.
+ * @param {Function(config {AccountConfig})} successCallback - A callback that
+ * will be called when we could retrieve a configuration.
+ * The AccountConfig object will be passed in as first parameter.
+ * @param {Function(ex)} errorCallback - A callback that
+ * will be called when we could not retrieve a configuration,
+ * for whatever reason. This is expected (e.g. when there's no config
+ * for this domain at this location),
+ * so do not unconditionally show this to the user.
+ * The first parameter will be an exception object or error string.
+ */
+function fetchConfigFromExchange(
+ domain,
+ emailAddress,
+ username,
+ password,
+ confirmCallback,
+ successCallback,
+ errorCallback
+) {
+ assert(typeof successCallback == "function");
+ assert(typeof errorCallback == "function");
+ if (
+ !Services.prefs.getBoolPref(
+ "mailnews.auto_config.fetchFromExchange.enabled",
+ true
+ )
+ ) {
+ errorCallback("Exchange AutoDiscover disabled per user preference");
+ return new Abortable();
+ }
+
+ // <https://technet.microsoft.com/en-us/library/bb124251(v=exchg.160).aspx#Autodiscover%20services%20in%20Outlook>
+ // <https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638(v%3Dexchg.140)>, search for "The Autodiscover service uses one of these four methods"
+ let url1 =
+ "https://autodiscover." +
+ lazy.Sanitizer.hostname(domain) +
+ "/autodiscover/autodiscover.xml";
+ let url2 =
+ "https://" +
+ lazy.Sanitizer.hostname(domain) +
+ "/autodiscover/autodiscover.xml";
+ let url3 =
+ "http://autodiscover." +
+ lazy.Sanitizer.hostname(domain) +
+ "/autodiscover/autodiscover.xml";
+ let body = `<?xml version="1.0" encoding="utf-8"?>
+ <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
+ <Request>
+ <EMailAddress>${emailAddress}</EMailAddress>
+ <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
+ </Request>
+ </Autodiscover>`;
+ let callArgs = {
+ uploadBody: body,
+ post: true,
+ headers: {
+ // outlook.com needs this exact string, with space and lower case "utf".
+ // Compare bug 1454325 comment 15.
+ "Content-Type": "text/xml; charset=utf-8",
+ },
+ username: username || emailAddress,
+ password,
+ allowAuthPrompt: false,
+ };
+ let call;
+ let fetch;
+ let fetch3;
+
+ let successive = new SuccessiveAbortable();
+ let priority = new PriorityOrderAbortable(function (xml, call) {
+ // success
+ readAutoDiscoverResponse(
+ xml,
+ successive,
+ emailAddress,
+ username,
+ password,
+ confirmCallback,
+ config => {
+ config.subSource = `exchange-from-${call.foundMsg}`;
+ return detectStandardProtocols(config, domain, successCallback);
+ },
+ errorCallback
+ );
+ }, errorCallback); // all failed
+
+ call = priority.addCall();
+ call.foundMsg = "url1";
+ fetch = new lazy.FetchHTTP(
+ url1,
+ callArgs,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ fetch.start();
+ call.setAbortable(fetch);
+
+ call = priority.addCall();
+ call.foundMsg = "url2";
+ fetch = new lazy.FetchHTTP(
+ url2,
+ callArgs,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ fetch.start();
+ call.setAbortable(fetch);
+
+ call = priority.addCall();
+ call.foundMsg = "url3";
+ let call3ErrorCallback = call.errorCallback();
+ // url3 is HTTP (not HTTPS), so suppress password. Even MS spec demands so.
+ let call3Args = deepCopy(callArgs);
+ delete call3Args.username;
+ delete call3Args.password;
+ fetch3 = new lazy.FetchHTTP(url3, call3Args, call.successCallback(), ex => {
+ // url3 is an HTTP URL that will redirect to the real one, usually a
+ // HTTPS URL of the hoster. XMLHttpRequest unfortunately loses the call
+ // parameters, drops the auth, drops the body, and turns POST into GET,
+ // which cause the call to fail. For AutoDiscover mechanism to work,
+ // we need to repeat the call with the correct parameters again.
+ let redirectURL = fetch3._request.responseURL;
+ if (!redirectURL.startsWith("https:")) {
+ call3ErrorCallback(ex);
+ return;
+ }
+ let redirectURI = Services.io.newURI(redirectURL);
+ let redirectDomain = Services.eTLD.getBaseDomain(redirectURI);
+ let originalDomain = Services.eTLD.getBaseDomainFromHost(domain);
+
+ function fetchRedirect() {
+ let fetchCall = priority.addCall();
+ let fetch = new lazy.FetchHTTP(
+ redirectURL,
+ callArgs, // now with auth
+ fetchCall.successCallback(),
+ fetchCall.errorCallback()
+ );
+ fetchCall.setAbortable(fetch);
+ fetch.start();
+ }
+
+ const kSafeDomains = ["office365.com", "outlook.com"];
+ if (
+ redirectDomain != originalDomain &&
+ !kSafeDomains.includes(redirectDomain)
+ ) {
+ // Given that we received the redirect URL from an insecure HTTP call,
+ // we ask the user whether he trusts the redirect domain.
+ gAccountSetupLogger.info("AutoDiscover HTTP redirected to other domain");
+ let dialogSuccessive = new SuccessiveAbortable();
+ // Because the dialog implements Abortable, the dialog will cancel and
+ // close automatically, if a slow higher priority call returns late.
+ let dialogCall = priority.addCall();
+ dialogCall.setAbortable(dialogSuccessive);
+ call3ErrorCallback(new Exception("Redirected"));
+ dialogSuccessive.current = new TimeoutAbortable(
+ lazy.setTimeout(() => {
+ dialogSuccessive.current = confirmCallback(
+ redirectDomain,
+ () => {
+ // User agreed.
+ fetchRedirect();
+ // Remove the dialog from the call stack.
+ dialogCall.errorCallback()(new Exception("Proceed to fetch"));
+ },
+ ex => {
+ // User rejected, or action cancelled otherwise.
+ dialogCall.errorCallback()(ex);
+ }
+ );
+ // Account for a slow server response.
+ // This will prevent showing the warning message when not necessary.
+ // The timeout is just for optics. The Abortable ensures that it works.
+ }, 2000)
+ );
+ } else {
+ fetchRedirect();
+ call3ErrorCallback(new Exception("Redirected"));
+ }
+ });
+ fetch3.start();
+ call.setAbortable(fetch3);
+
+ successive.current = priority;
+ return successive;
+}
+
+var gLoopCounter = 0;
+
+/**
+ * @param {JXON} xml - The Exchange server AutoDiscover response
+ * @param {Function(config {AccountConfig})} successCallback - @see accountConfig.js
+ */
+function readAutoDiscoverResponse(
+ autoDiscoverXML,
+ successive,
+ emailAddress,
+ username,
+ password,
+ confirmCallback,
+ successCallback,
+ errorCallback
+) {
+ assert(successive instanceof SuccessiveAbortable);
+ assert(typeof successCallback == "function");
+ assert(typeof errorCallback == "function");
+
+ // redirect to other email address
+ if (
+ "Account" in autoDiscoverXML.Autodiscover.Response &&
+ "RedirectAddr" in autoDiscoverXML.Autodiscover.Response.Account
+ ) {
+ // <https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/49083e77-8dc2-4010-85c6-f40e090f3b17>
+ let redirectEmailAddress = lazy.Sanitizer.emailAddress(
+ autoDiscoverXML.Autodiscover.Response.Account.RedirectAddr
+ );
+ let domain = redirectEmailAddress.split("@").pop();
+ if (++gLoopCounter > 2) {
+ throw new Error("Too many redirects in XML response; domain=" + domain);
+ }
+ successive.current = fetchConfigFromExchange(
+ domain,
+ redirectEmailAddress,
+ // Per spec, need to authenticate with the original email address,
+ // not the redirected address (if not already overridden).
+ username || emailAddress,
+ password,
+ confirmCallback,
+ successCallback,
+ errorCallback
+ );
+ return;
+ }
+
+ let config = readAutoDiscoverXML(autoDiscoverXML, username);
+ if (config.isComplete()) {
+ successCallback(config);
+ } else {
+ errorCallback(new Exception("No valid configs found in AutoDiscover XML"));
+ }
+}
+
+/* eslint-disable complexity */
+/**
+ * @param {JXON} xml - The Exchange server AutoDiscover response
+ * @param {string} username - (Optional) The user's login name
+ * If null, email address placeholder will be used.
+ * @returns {AccountConfig} - @see accountConfig.js
+ *
+ * @see <https://www.msxfaq.de/exchange/autodiscover/autodiscover_xml.htm>
+ */
+function readAutoDiscoverXML(autoDiscoverXML, username) {
+ if (
+ typeof autoDiscoverXML != "object" ||
+ !("Autodiscover" in autoDiscoverXML) ||
+ !("Response" in autoDiscoverXML.Autodiscover) ||
+ !("Account" in autoDiscoverXML.Autodiscover.Response) ||
+ !("Protocol" in autoDiscoverXML.Autodiscover.Response.Account)
+ ) {
+ let stringBundle = getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties"
+ );
+ throw new Exception(
+ stringBundle.GetStringFromName("no_autodiscover.error")
+ );
+ }
+ var xml = autoDiscoverXML.Autodiscover.Response.Account;
+
+ function array_or_undef(value) {
+ return value === undefined ? [] : value;
+ }
+
+ var config = new lazy.AccountConfig();
+ config.source = lazy.AccountConfig.kSourceExchange;
+ config.incoming.username = username || "%EMAILADDRESS%";
+ config.incoming.socketType = Ci.nsMsgSocketType.SSL; // only https supported
+ config.incoming.port = 443;
+ config.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext;
+ config.incoming.authAlternatives = [Ci.nsMsgAuthMethod.OAuth2];
+ config.outgoing.addThisServer = false;
+ config.outgoing.useGlobalPreferredServer = true;
+
+ for (let protocolX of array_or_undef(xml.$Protocol)) {
+ try {
+ let type = lazy.Sanitizer.enum(
+ protocolX.Type,
+ ["WEB", "EXHTTP", "EXCH", "EXPR", "POP3", "IMAP", "SMTP"],
+ "unknown"
+ );
+ if (type == "WEB") {
+ let urlsX;
+ if ("External" in protocolX) {
+ urlsX = protocolX.External;
+ } else if ("Internal" in protocolX) {
+ urlsX = protocolX.Internal;
+ }
+ if (urlsX) {
+ config.incoming.owaURL = lazy.Sanitizer.url(urlsX.OWAUrl.value);
+ if (
+ !config.incoming.ewsURL &&
+ "Protocol" in urlsX &&
+ "ASUrl" in urlsX.Protocol
+ ) {
+ config.incoming.ewsURL = lazy.Sanitizer.url(urlsX.Protocol.ASUrl);
+ }
+ config.incoming.type = "exchange";
+ let parsedURL = new URL(config.incoming.owaURL);
+ config.incoming.hostname = lazy.Sanitizer.hostname(
+ parsedURL.hostname
+ );
+ if (parsedURL.port) {
+ config.incoming.port = lazy.Sanitizer.integer(parsedURL.port);
+ }
+ }
+ } else if (type == "EXHTTP" || type == "EXCH") {
+ config.incoming.ewsURL = lazy.Sanitizer.url(protocolX.EwsUrl);
+ if (!config.incoming.ewsURL) {
+ config.incoming.ewsURL = lazy.Sanitizer.url(protocolX.ASUrl);
+ }
+ config.incoming.type = "exchange";
+ let parsedURL = new URL(config.incoming.ewsURL);
+ config.incoming.hostname = lazy.Sanitizer.hostname(parsedURL.hostname);
+ if (parsedURL.port) {
+ config.incoming.port = lazy.Sanitizer.integer(parsedURL.port);
+ }
+ } else if (type == "POP3" || type == "IMAP" || type == "SMTP") {
+ let server;
+ if (type == "SMTP") {
+ server = config.createNewOutgoing();
+ } else {
+ server = config.createNewIncoming();
+ }
+
+ server.type = lazy.Sanitizer.translate(type, {
+ POP3: "pop3",
+ IMAP: "imap",
+ SMTP: "smtp",
+ });
+ server.hostname = lazy.Sanitizer.hostname(protocolX.Server);
+ server.port = lazy.Sanitizer.integer(protocolX.Port);
+ server.socketType = Ci.nsMsgSocketType.plain;
+ if (
+ "SSL" in protocolX &&
+ protocolX.SSL.toLowerCase() == "on" // "On" or "Off"
+ ) {
+ // SSL is too unspecific. Do they mean STARTTLS or normal TLS?
+ // For now, assume normal TLS, unless it's a standard plain port.
+ switch (server.port) {
+ case 143: // IMAP standard
+ case 110: // POP3 standard
+ case 25: // SMTP standard
+ case 587: // SMTP standard
+ server.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+ break;
+ case 993: // IMAP SSL
+ case 995: // POP3 SSL
+ case 465: // SMTP SSL
+ default:
+ // if non-standard port, assume normal TLS, not STARTTLS
+ server.socketType = Ci.nsMsgSocketType.SSL;
+ break;
+ }
+ }
+ server.auth = Ci.nsMsgAuthMethod.passwordCleartext;
+ if (
+ "SPA" in protocolX &&
+ protocolX.SPA.toLowerCase() == "on" // "On" or "Off"
+ ) {
+ // Secure Password Authentication = NTLM or GSSAPI/Kerberos
+ server.auth = Ci.nsMsgAuthMethod.secure;
+ }
+ if ("LoginName" in protocolX) {
+ server.username = lazy.Sanitizer.nonemptystring(protocolX.LoginName);
+ } else {
+ server.username = username || "%EMAILADDRESS%";
+ }
+
+ if (type == "SMTP") {
+ if (!config.outgoing.hostname) {
+ config.outgoing = server;
+ } else {
+ config.outgoingAlternatives.push(server);
+ }
+ } else if (!config.incoming.hostname) {
+ // eslint-disable-line no-lonely-if
+ config.incoming = server;
+ } else {
+ config.incomingAlternatives.push(server);
+ }
+ }
+
+ // else unknown or unsupported protocol
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ // OAuth2 settings, so that createInBackend() doesn't bail out
+ if (config.incoming.owaURL || config.incoming.ewsURL) {
+ config.incoming.oauthSettings = {
+ issuer: config.incoming.hostname,
+ scope: config.incoming.owaURL || config.incoming.ewsURL,
+ };
+ config.outgoing.oauthSettings = {
+ issuer: config.incoming.hostname,
+ scope: config.incoming.owaURL || config.incoming.ewsURL,
+ };
+ }
+
+ return config;
+}
+/* eslint-enable complexity */
+
+/**
+ * Ask server which addons can handle this config.
+ *
+ * @param {AccountConfig} config
+ * @param {Function(config {AccountConfig})} successCallback
+ * @returns {Abortable}
+ */
+function getAddonsList(config, successCallback, errorCallback) {
+ let incoming = [config.incoming, ...config.incomingAlternatives].find(
+ alt => alt.type == "exchange"
+ );
+ if (!incoming) {
+ successCallback();
+ return new Abortable();
+ }
+ let url = Services.prefs.getCharPref("mailnews.auto_config.addons_url");
+ if (!url) {
+ errorCallback(new Exception("no URL for addons list configured"));
+ return new Abortable();
+ }
+ let fetch = new lazy.FetchHTTP(
+ url,
+ { allowCache: true, timeout: 10000 },
+ function (json) {
+ let addons = readAddonsJSON(json);
+ addons = addons.filter(addon => {
+ // Find types matching the current config.
+ // Pick the first in the list as the preferred one and
+ // tell the UI to use that one.
+ addon.useType = addon.supportedTypes.find(
+ type =>
+ (incoming.owaURL && type.protocolType == "owa") ||
+ (incoming.ewsURL && type.protocolType == "ews") ||
+ (incoming.easURL && type.protocolType == "eas")
+ );
+ return !!addon.useType;
+ });
+ if (addons.length == 0) {
+ errorCallback(
+ new Exception(
+ "Config found, but no addons known to handle the config"
+ )
+ );
+ return;
+ }
+ config.addons = addons;
+ successCallback(config);
+ },
+ errorCallback
+ );
+ fetch.start();
+ return fetch;
+}
+
+/**
+ * This reads the addons list JSON and makes security validations,
+ * e.g. that the URLs are not chrome: URLs, which could lead to exploits.
+ * It also chooses the right language etc..
+ *
+ * @param {JSON} json - the addons.json file contents
+ * @returns {Array of AddonInfo} - @see AccountConfig.addons
+ *
+ * accountTypes are listed in order of decreasing preference.
+ * Languages are 2-letter codes. If a language is not available,
+ * the first name or description will be used.
+ *
+ * Parse e.g.
+[
+ {
+ "id": "owl@beonex.com",
+ "name": {
+ "en": "Owl",
+ "de": "Eule"
+ },
+ "description": {
+ "en": "Owl is a paid third-party addon that allows you to access your email account on Exchange servers. See the website for prices.",
+ "de": "Eule ist eine Erweiterung von einem Drittanbieter, die Ihnen erlaubt, Exchange-Server zu benutzen. Sie ist kostenpflichtig. Die Preise finden Sie auf der Website."
+ },
+ "minVersion": "0.2",
+ "xpiURL": "http://www.beonex.com/owl/latest.xpi",
+ "websiteURL": "http://www.beonex.com/owl/",
+ "icon32": "http://www.beonex.com/owl/owl-32.png",
+ "accountTypes": [
+ {
+ "generalType": "exchange",
+ "protocolType": "owa",
+ "addonAccountType": "owl-owa"
+ },
+ {
+ "generalType": "exchange",
+ "protocolType": "eas",
+ "addonAccountType": "owl-eas"
+ }
+ ]
+ }
+]
+ */
+function readAddonsJSON(json) {
+ let addons = [];
+ function ensureArray(value) {
+ return Array.isArray(value) ? value : [];
+ }
+ let xulLocale = Services.locale.requestedLocale;
+ let locale = xulLocale ? xulLocale.substring(0, 5) : "default";
+ for (let addonJSON of ensureArray(json)) {
+ try {
+ let addon = {
+ id: addonJSON.id,
+ minVersion: addonJSON.minVersion,
+ xpiURL: lazy.Sanitizer.url(addonJSON.xpiURL),
+ websiteURL: lazy.Sanitizer.url(addonJSON.websiteURL),
+ icon32: addonJSON.icon32 ? lazy.Sanitizer.url(addonJSON.icon32) : null,
+ supportedTypes: [],
+ };
+ assert(
+ new URL(addon.xpiURL).protocol == "https:",
+ "XPI download URL needs to be https"
+ );
+ addon.name =
+ locale in addonJSON.name ? addonJSON.name[locale] : addonJSON.name[0];
+ addon.description =
+ locale in addonJSON.description
+ ? addonJSON.description[locale]
+ : addonJSON.description[0];
+ for (let typeJSON of ensureArray(addonJSON.accountTypes)) {
+ try {
+ addon.supportedTypes.push({
+ generalType: lazy.Sanitizer.alphanumdash(typeJSON.generalType),
+ protocolType: lazy.Sanitizer.alphanumdash(typeJSON.protocolType),
+ addonAccountType: lazy.Sanitizer.alphanumdash(
+ typeJSON.addonAccountType
+ ),
+ });
+ } catch (e) {
+ ddump(e);
+ }
+ }
+ addons.push(addon);
+ } catch (e) {
+ ddump(e);
+ }
+ }
+ return addons;
+}
+
+/**
+ * Probe a found Exchange server for IMAP/POP3 and SMTP support.
+ *
+ * @param {AccountConfig} config - The initial detected Exchange configuration.
+ * @param {string} domain - The domain part of the user's email address
+ * @param {Function(config {AccountConfig})} successCallback - A callback that
+ * will be called when we found an appropriate configuration.
+ * The AccountConfig object will be passed in as first parameter.
+ */
+function detectStandardProtocols(config, domain, successCallback) {
+ gAccountSetupLogger.info("Exchange Autodiscover gave some results.");
+ let alts = [config.incoming, ...config.incomingAlternatives];
+ if (alts.find(alt => alt.type == "imap" || alt.type == "pop3")) {
+ // Autodiscover found an exchange server with advertized IMAP and/or
+ // POP3 support. We're done then.
+ config.preferStandardProtocols();
+ successCallback(config);
+ return;
+ }
+
+ // Autodiscover is known not to advertise all that it supports. Let's see
+ // if there really isn't any IMAP/POP3 support by probing the Exchange
+ // server. Use the server hostname already found.
+ let config2 = new lazy.AccountConfig();
+ config2.incoming.hostname = config.incoming.hostname;
+ config2.incoming.username = config.incoming.username || "%EMAILADDRESS%";
+ // For Exchange 2013+ Kerberos/GSSAPI and NTLM options do not work by
+ // default at least for Linux users, even if support is detected.
+ config2.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext;
+
+ config2.outgoing.hostname = config.incoming.hostname;
+ config2.outgoing.username = config.incoming.username || "%EMAILADDRESS%";
+
+ config2.incomingAlternatives = config.incomingAlternatives;
+ config2.incomingAlternatives.push(config.incoming); // type=exchange
+
+ config2.outgoingAlternatives = config.outgoingAlternatives;
+ if (config.outgoing.hostname) {
+ config2.outgoingAlternatives.push(config.outgoing);
+ }
+
+ lazy.GuessConfig.guessConfig(
+ domain,
+ function (type, hostname, port, ssl, done, config) {
+ gAccountSetupLogger.info(
+ `Probing exchange server ${hostname} for ${type} protocol support.`
+ );
+ },
+ function (probedConfig) {
+ // Probing succeeded: found open protocols, yay!
+ successCallback(probedConfig);
+ },
+ function (e, probedConfig) {
+ // Probing didn't find any open protocols.
+ // Let's use the exchange (only) config that was listed then.
+ config.subSource += "-guess";
+ successCallback(config);
+ },
+ config2,
+ "both"
+ );
+}
diff --git a/comm/mail/components/accountcreation/FetchConfig.jsm b/comm/mail/components/accountcreation/FetchConfig.jsm
new file mode 100644
index 0000000000..1bf16ca2ed
--- /dev/null
+++ b/comm/mail/components/accountcreation/FetchConfig.jsm
@@ -0,0 +1,299 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["FetchConfig"];
+
+const { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "FetchHTTP",
+ "resource:///modules/accountcreation/FetchHTTP.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "readFromXML",
+ "resource:///modules/accountcreation/readFromXML.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm");
+const { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm");
+
+const {
+ Abortable,
+ ddump,
+ Exception,
+ PriorityOrderAbortable,
+ PromiseAbortable,
+ readURLasUTF8,
+ runAsync,
+ SuccessiveAbortable,
+ TimeoutAbortable,
+} = AccountCreationUtils;
+
+/**
+ * Tries to find a configuration for this ISP on the local harddisk, in the
+ * application install directory's "isp" subdirectory.
+ * Params @see fetchConfigFromISP()
+ */
+function fetchConfigFromDisk(domain, successCallback, errorCallback) {
+ return new TimeoutAbortable(
+ runAsync(function () {
+ try {
+ // <TB installdir>/isp/example.com.xml
+ var configLocation = Services.dirsvc.get("CurProcD", Ci.nsIFile);
+ configLocation.append("isp");
+ configLocation.append(lazy.Sanitizer.hostname(domain) + ".xml");
+
+ if (!configLocation.exists() || !configLocation.isReadable()) {
+ errorCallback(new Exception("local file not found"));
+ return;
+ }
+ var contents = readURLasUTF8(Services.io.newFileURI(configLocation));
+ let domParser = new DOMParser();
+ const xml = JXON.build(domParser.parseFromString(contents, "text/xml"));
+ successCallback(lazy.readFromXML(xml, "disk"));
+ } catch (e) {
+ errorCallback(e);
+ }
+ })
+ );
+}
+
+/**
+ * Tries to get a configuration from the ISP / mail provider directly.
+ *
+ * Disclaimers:
+ * - To support domain hosters, we cannot use SSL. That means we
+ * rely on insecure DNS and http, which means the results may be
+ * forged when under attack. The same is true for guessConfig(), though.
+ *
+ * @param domain {String} - The domain part of the user's email address
+ * @param emailAddress {String} - The user's email address
+ * @param successCallback {Function(config {AccountConfig}})} A callback that
+ * will be called when we could retrieve a configuration.
+ * The AccountConfig object will be passed in as first parameter.
+ * @param errorCallback {Function(ex)} - A callback that
+ * will be called when we could not retrieve a configuration,
+ * for whatever reason. This is expected (e.g. when there's no config
+ * for this domain at this location),
+ * so do not unconditionally show this to the user.
+ * The first parameter will be an exception object or error string.
+ */
+function fetchConfigFromISP(
+ domain,
+ emailAddress,
+ successCallback,
+ errorCallback
+) {
+ if (
+ !Services.prefs.getBoolPref("mailnews.auto_config.fetchFromISP.enabled")
+ ) {
+ errorCallback(new Exception("ISP fetch disabled per user preference"));
+ return new Abortable();
+ }
+
+ let conf1 =
+ "autoconfig." + lazy.Sanitizer.hostname(domain) + "/mail/config-v1.1.xml";
+ // .well-known/ <http://tools.ietf.org/html/draft-nottingham-site-meta-04>
+ let conf2 =
+ lazy.Sanitizer.hostname(domain) +
+ "/.well-known/autoconfig/mail/config-v1.1.xml";
+ // This list is sorted by decreasing priority
+ var urls = ["https://" + conf1, "https://" + conf2];
+ if (
+ !Services.prefs.getBoolPref("mailnews.auto_config.fetchFromISP.sslOnly")
+ ) {
+ urls.push("http://" + conf1, "http://" + conf2);
+ }
+ let callArgs = {
+ urlArgs: {
+ emailaddress: emailAddress,
+ },
+ };
+ if (
+ !Services.prefs.getBoolPref(
+ "mailnews.auto_config.fetchFromISP.sendEmailAddress"
+ )
+ ) {
+ delete callArgs.urlArgs.emailaddress;
+ }
+ let call;
+ let fetch;
+
+ let priority = new PriorityOrderAbortable(
+ (xml, call) =>
+ successCallback(lazy.readFromXML(xml, `isp-${call.foundMsg}`)),
+ errorCallback
+ );
+ for (let url of urls) {
+ call = priority.addCall();
+ call.foundMsg = url.startsWith("https") ? "https" : "http";
+ fetch = new lazy.FetchHTTP(
+ url,
+ callArgs,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+ fetch.start();
+ }
+
+ return priority;
+}
+
+/**
+ * Tries to get a configuration for this ISP from a central database at
+ * Mozilla servers.
+ * Params @see fetchConfigFromISP()
+ */
+function fetchConfigFromDB(domain, successCallback, errorCallback) {
+ let url = Services.prefs.getCharPref("mailnews.auto_config_url");
+ if (!url) {
+ errorCallback(new Exception("no URL for ISP DB configured"));
+ return new Abortable();
+ }
+ domain = lazy.Sanitizer.hostname(domain);
+
+ // If we don't specify a place to put the domain, put it at the end.
+ if (!url.includes("{{domain}}")) {
+ url = url + domain;
+ } else {
+ url = url.replace("{{domain}}", domain);
+ }
+
+ let fetch = new lazy.FetchHTTP(
+ url,
+ { timeout: 10000 }, // 10 seconds
+ function (result) {
+ successCallback(lazy.readFromXML(result, "db"));
+ },
+ errorCallback
+ );
+ fetch.start();
+ return fetch;
+}
+
+/**
+ * Does a lookup of DNS MX, to get the server that is responsible for
+ * receiving mail for this domain. Then it takes the domain of that
+ * server, and does another lookup (in ISPDB and possibly at ISP autoconfig
+ * server) and if such a config is found, returns that.
+ *
+ * Disclaimers:
+ * - DNS is unprotected, meaning the results could be forged.
+ * The same is true for fetchConfigFromISP() and guessConfig(), though.
+ * - DNS MX tells us the incoming server, not the mailbox (IMAP) server.
+ * They are different. This mechanism is only an approximation
+ * for hosted domains (yourname.com is served by mx.hoster.com and
+ * therefore imap.hoster.com - that "therefore" is exactly the
+ * conclusional jump we make here.) and alternative domains
+ * (e.g. yahoo.de -> yahoo.com).
+ * - We make a look up for the base domain. E.g. if MX is
+ * mx1.incoming.servers.hoster.com, we look up hoster.com.
+ * Thanks to Services.eTLD, we also get bbc.co.uk right.
+ *
+ * Params @see fetchConfigFromISP()
+ */
+function fetchConfigForMX(domain, successCallback, errorCallback) {
+ const sanitizedDomain = lazy.Sanitizer.hostname(domain);
+ const sucAbortable = new SuccessiveAbortable();
+ const time = Date.now();
+
+ sucAbortable.current = getMX(
+ sanitizedDomain,
+ function (mxHostname) {
+ // success
+ ddump("getmx took " + (Date.now() - time) + "ms");
+ let sld = Services.eTLD.getBaseDomainFromHost(mxHostname);
+ ddump("base domain " + sld + " for " + mxHostname);
+ if (sld == sanitizedDomain) {
+ errorCallback(
+ new Exception("MX lookup would be no different from domain")
+ );
+ return;
+ }
+
+ // In addition to just the base domain, also check the full domain of the MX server
+ // to differentiate between Outlook.com/Hotmail and Office365 business domains.
+ let mxDomain;
+ try {
+ mxDomain = Services.eTLD.getNextSubDomain(mxHostname);
+ } catch (ex) {
+ // e.g. hostname doesn't have enough components
+ console.error(ex); // not fatal
+ }
+ let priority = new PriorityOrderAbortable(successCallback, errorCallback);
+ if (mxDomain && sld != mxDomain) {
+ let call = priority.addCall();
+ let fetch = fetchConfigFromDB(
+ mxDomain,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+ }
+ let call = priority.addCall();
+ let fetch = fetchConfigFromDB(
+ sld,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+ sucAbortable.current = priority;
+ },
+ errorCallback
+ );
+ return sucAbortable;
+}
+
+/**
+ * Queries the DNS MX records for a given domain. Calls `successCallback` with
+ * the hostname of the MX server. If there are several entries with different
+ * preference values, only the most preferred (i.e. has the lowest value)
+ * is used. If there are several most preferred servers (i.e. round robin),
+ * only one of them is used.
+ *
+ * @param {string} sanitizedDomain @see fetchConfigFromISP()
+ * @param {function(hostname {string})} - successCallback
+ * Called when we found an MX for the domain.
+ * For |hostname|, see description above.
+ * @param {function({Exception|string})} errorCallback @see fetchConfigFromISP()
+ */
+function getMX(sanitizedDomain, successCallback, errorCallback) {
+ return new PromiseAbortable(
+ DNS.mx(sanitizedDomain),
+ function (records) {
+ const filteredRecs = records.filter(record => record.host);
+
+ if (filteredRecs.length > 0) {
+ const sortedRecs = filteredRecs.sort((a, b) => a.prio > b.prio);
+ const firstHost = sortedRecs[0].host;
+ successCallback(firstHost);
+ } else {
+ errorCallback(
+ new Exception(
+ "No hostname found in MX records for sanitizedDomain=" +
+ sanitizedDomain
+ )
+ );
+ }
+ },
+ errorCallback
+ );
+}
+
+var FetchConfig = {
+ forMX: fetchConfigForMX,
+ fromDB: fetchConfigFromDB,
+ fromISP: fetchConfigFromISP,
+ fromDisk: fetchConfigFromDisk,
+};
diff --git a/comm/mail/components/accountcreation/FetchHTTP.jsm b/comm/mail/components/accountcreation/FetchHTTP.jsm
new file mode 100644
index 0000000000..54b3629906
--- /dev/null
+++ b/comm/mail/components/accountcreation/FetchHTTP.jsm
@@ -0,0 +1,401 @@
+/* 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/. */
+
+/**
+ * This is a small wrapper around XMLHttpRequest, which solves various
+ * inadequacies of the API, e.g. error handling. It is entirely generic and
+ * can be used for purposes outside of even mail.
+ *
+ * It does not provide download progress, but assumes that the
+ * fetched resource is so small (<1 10 KB) that the roundtrip and
+ * response generation is far more significant than the
+ * download time of the response. In other words, it's fine for RPC,
+ * but not for bigger file downloads.
+ */
+
+const EXPORTED_SYMBOLS = ["FetchHTTP"];
+
+const { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+const { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm");
+
+const {
+ Abortable,
+ alertPrompt,
+ assert,
+ ddump,
+ Exception,
+ gAccountSetupLogger,
+ getStringBundle,
+ UserCancelledException,
+} = AccountCreationUtils;
+
+/**
+ * Set up a fetch.
+ *
+ * @param {string} url - URL of the server function.
+ * ATTENTION: The caller needs to make sure that the URL is secure to call.
+ * @param {object} args - Additional parameters as properties, see below
+ *
+ * @param {Function({string} result)} successCallback
+ * Called when the server call worked (no errors).
+ * |result| will contain the body of the HTTP response, as string.
+ * @param {Function(ex)} errorCallback
+ * Called in case of error. ex contains the error
+ * with a user-displayable but not localized |.message| and maybe a
+ * |.code|, which can be either
+ * - an nsresult error code,
+ * - an HTTP result error code (0...1000) or
+ * - negative: 0...-100 :
+ * -2 = can't resolve server in DNS etc.
+ * -4 = response body (e.g. XML) malformed
+ *
+ * The following optional parameters are supported as properties of the |args| object:
+ *
+ * @param {Object, associative array} urlArgs - Parameters to add
+ * to the end of the URL as query string. E.g.
+ * { foo: "bla", bar: "blub blub" } will add "?foo=bla&bar=blub%20blub"
+ * to the URL
+ * (unless the URL already has a "?", then it adds "&foo...").
+ * The values will be urlComponentEncoded, so pass them unencoded.
+ * @param {Object, associative array} headers - HTTP headers to be added
+ * to the HTTP request.
+ * { foo: "blub blub" } will add HTTP header "Foo: Blub blub".
+ * The values will be passed verbatim.
+ * @param {boolean} post - HTTP GET or POST
+ * Only influences the HTTP request method,
+ * i.e. first line of the HTTP request, not the body or parameters.
+ * Use POST when you modify server state,
+ * GET when you only request information.
+ * Default is GET.
+ * @param {Object, associative array} bodyFormArgs - Like urlArgs,
+ * just that the params will be sent x-url-encoded in the body,
+ * like a HTML form post.
+ * The values will be urlComponentEncoded, so pass them unencoded.
+ * This cannot be used together with |uploadBody|.
+ * @param {object} uploadBody - Arbitrary object, which to use as
+ * body of the HTTP request. Will also set the mimetype accordingly.
+ * Only supported object types, currently only JXON is supported
+ * (sending XML).
+ * Usually, you have nothing to upload, so just pass |null|.
+ * Only supported object types, currently supported:
+ * JXON -> sending XML
+ * JS object -> sending JSON
+ * string -> sending text/plain
+ * If you want to override the body mimetype, set header Content-Type below.
+ * Usually, you have nothing to upload, so just leave it at |null|.
+ * Default |null|.
+ * @param {boolean} allowCache (default true)
+ * @param {string} username (default null = no authentication)
+ * @param {string} password (default null = no authentication)
+ * @param {boolean} allowAuthPrompt (default true)
+ * @param {boolean} requireSecureAuth (default false)
+ * Ignore the username and password unless we are using https:
+ * This also applies to both https: to http: and http: to https: redirects.
+ */
+function FetchHTTP(url, args, successCallback, errorCallback) {
+ assert(typeof successCallback == "function", "BUG: successCallback");
+ assert(typeof errorCallback == "function", "BUG: errorCallback");
+ this._url = lazy.Sanitizer.string(url);
+ if (!args) {
+ args = {};
+ }
+ if (!args.urlArgs) {
+ args.urlArgs = {};
+ }
+ if (!args.headers) {
+ args.headers = {};
+ }
+
+ this._args = args;
+ this._args.post = lazy.Sanitizer.boolean(args.post || false); // default false
+ this._args.allowCache =
+ "allowCache" in args ? lazy.Sanitizer.boolean(args.allowCache) : true; // default true
+ this._args.allowAuthPrompt = lazy.Sanitizer.boolean(
+ args.allowAuthPrompt || false
+ ); // default false
+ this._args.requireSecureAuth = lazy.Sanitizer.boolean(
+ args.requireSecureAuth || false
+ ); // default false
+ this._args.timeout = lazy.Sanitizer.integer(args.timeout || 5000); // default 5 seconds
+ this._successCallback = successCallback;
+ this._errorCallback = errorCallback;
+ this._logger = gAccountSetupLogger;
+ this._logger.info("Requesting <" + url + ">");
+}
+FetchHTTP.prototype = {
+ __proto__: Abortable.prototype,
+ _url: null, // URL as passed to ctor, without arguments
+ _args: null,
+ _successCallback: null,
+ _errorCallback: null,
+ _request: null, // the XMLHttpRequest object
+ result: null,
+
+ start() {
+ let url = this._url;
+ for (let name in this._args.urlArgs) {
+ url +=
+ (!url.includes("?") ? "?" : "&") +
+ name +
+ "=" +
+ encodeURIComponent(this._args.urlArgs[name]);
+ }
+ this._request = new XMLHttpRequest();
+ let request = this._request;
+ request.mozBackgroundRequest = !this._args.allowAuthPrompt;
+ let username = null,
+ password = null;
+ if (url.startsWith("https:") || !this._args.requireSecureAuth) {
+ username = this._args.username;
+ password = this._args.password;
+ }
+ request.open(
+ this._args.post ? "POST" : "GET",
+ url,
+ true,
+ username,
+ password
+ );
+ request.channel.loadGroup = null;
+ request.timeout = this._args.timeout;
+ // needs bug 407190 patch v4 (or higher) - uncomment if that lands.
+ // try {
+ // var channel = request.channel.QueryInterface(Ci.nsIHttpChannel2);
+ // channel.connectTimeout = 5;
+ // channel.requestTimeout = 5;
+ // } catch (e) { dump(e + "\n"); }
+
+ if (!this._args.allowCache) {
+ // Disable Mozilla HTTP cache
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ }
+
+ // body
+ let mimetype = null;
+ let body = this._args.uploadBody;
+ if (typeof body == "object" && "nodeType" in body) {
+ // XML
+ mimetype = "text/xml; charset=UTF-8";
+ body = new XMLSerializer().serializeToString(body);
+ } else if (typeof body == "object") {
+ // JSON
+ mimetype = "text/json; charset=UTF-8";
+ body = JSON.stringify(body);
+ } else if (typeof body == "string") {
+ // Plaintext
+ // You can override the mimetype with { headers: {"Content-Type" : "text/foo" } }
+ mimetype = "text/plain; charset=UTF-8";
+ // body already set above
+ } else if (this._args.bodyFormArgs) {
+ mimetype = "application/x-www-form-urlencoded; charset=UTF-8";
+ body = "";
+ for (let name in this._args.bodyFormArgs) {
+ body +=
+ (body ? "&" : "") +
+ name +
+ "=" +
+ encodeURIComponent(this._args.bodyFormArgs[name]);
+ }
+ }
+
+ // Headers
+ if (mimetype && !("Content-Type" in this._args.headers)) {
+ request.setRequestHeader("Content-Type", mimetype);
+ }
+ if (username && password) {
+ // workaround, because open(..., username, password) does not work.
+ request.setRequestHeader(
+ "Authorization",
+ "Basic " +
+ btoa(
+ // btoa() takes a BinaryString.
+ String.fromCharCode(
+ ...new TextEncoder().encode(username + ":" + password)
+ )
+ )
+ );
+ }
+ for (let name in this._args.headers) {
+ request.setRequestHeader(name, this._args.headers[name]);
+ if (name == "Cookie") {
+ // Websites are not allowed to set this, but chrome is.
+ // Nevertheless, the cookie lib later overwrites our header.
+ // request.channel.setCookie(this._args.headers[name]); -- crashes
+ // So, deactivate that Firefox cookie lib.
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
+ }
+ }
+
+ var me = this;
+ request.onload = function () {
+ me._response(true);
+ };
+ request.onerror = function () {
+ me._response(false);
+ };
+ request.ontimeout = function () {
+ me._response(false);
+ };
+ request.send(body);
+ // Store the original stack so we can use it if there is an exception
+ this._callStack = Error().stack;
+ },
+ _response(success, exStored) {
+ try {
+ var errorCode = null;
+ var errorStr = null;
+
+ if (
+ success &&
+ this._request.status >= 200 &&
+ this._request.status < 300
+ ) {
+ // HTTP level success
+ try {
+ // response
+ var mimetype = this._request.getResponseHeader("Content-Type");
+ if (!mimetype) {
+ mimetype = "";
+ }
+ mimetype = mimetype.split(";")[0];
+ if (
+ mimetype == "text/xml" ||
+ mimetype == "application/xml" ||
+ mimetype == "text/rdf"
+ ) {
+ // XML
+ this.result = JXON.build(this._request.responseXML);
+ } else if (
+ mimetype == "text/json" ||
+ mimetype == "application/json"
+ ) {
+ // JSON
+ this.result = JSON.parse(this._request.responseText);
+ } else {
+ // Plaintext (fallback)
+ // ddump("mimetype: " + mimetype + " only supported as text");
+ this.result = this._request.responseText;
+ }
+ } catch (e) {
+ success = false;
+ errorStr = getStringBundle(
+ "chrome://messenger/locale/accountCreationUtil.properties"
+ ).GetStringFromName("bad_response_content.error");
+ errorCode = -4;
+ }
+ } else if (
+ this._args.username &&
+ this._request.responseURL.replace(/\/\/.*@/, "//") != this._url &&
+ this._request.responseURL.startsWith(
+ this._args.requireSecureAuth ? "https" : "http"
+ ) &&
+ !this._isRetry
+ ) {
+ // Redirects lack auth, see <https://stackoverflow.com/a/28411170>
+ this._logger.info(
+ "Call to <" +
+ this._url +
+ "> was redirected to <" +
+ this._request.responseURL +
+ ">, and failed. Re-trying the new URL with authentication again."
+ );
+ this._url = this._request.responseURL;
+ this._isRetry = true;
+ this.start();
+ return;
+ } else {
+ success = false;
+ try {
+ errorCode = this._request.status;
+ errorStr = this._request.statusText;
+ } catch (e) {
+ // In case .statusText throws (it's marked as [Throws] in the webidl),
+ // continue with empty errorStr.
+ }
+ if (!errorStr) {
+ // If we can't resolve the hostname in DNS etc., .statusText is empty.
+ errorCode = -2;
+ errorStr = getStringBundle(
+ "chrome://messenger/locale/accountCreationUtil.properties"
+ ).GetStringFromName("cannot_contact_server.error");
+ ddump(errorStr + " on <" + this._url + ">");
+ }
+ }
+
+ // Callbacks
+ if (success) {
+ try {
+ this._successCallback(this.result);
+ } catch (e) {
+ e.stack = this._callStack;
+ this._error(e);
+ }
+ } else if (exStored) {
+ this._error(exStored);
+ } else {
+ // Put the caller's stack into the exception
+ let e = new ServerException(errorStr, errorCode, this._url);
+ e.stack = this._callStack;
+ this._error(e);
+ }
+
+ if (this._finishedCallback) {
+ try {
+ this._finishedCallback(this);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ } catch (e) {
+ // error in our fetchhttp._response() code
+ this._error(e);
+ }
+ },
+ _error(e) {
+ try {
+ this._errorCallback(e);
+ } catch (e) {
+ // error in errorCallback, too!
+ console.error(e);
+ alertPrompt("Error in errorCallback for fetchhttp", e);
+ }
+ },
+ /**
+ * Call this between start() and finishedCallback fired.
+ */
+ cancel(ex) {
+ assert(!this.result, "Call already returned");
+
+ this._request.abort();
+
+ // Need to manually call error handler
+ // <https://bugzilla.mozilla.org/show_bug.cgi?id=218236#c11>
+ this._response(false, ex ? ex : new UserCancelledException());
+ },
+ /**
+ * Allows caller or lib to be notified when the call is done.
+ * This is useful to enable and disable a Cancel button in the UI,
+ * which allows to cancel the network request.
+ */
+ setFinishedCallback(finishedCallback) {
+ this._finishedCallback = finishedCallback;
+ },
+};
+
+function ServerException(msg, code, uri) {
+ Exception.call(this, msg);
+ this.code = code;
+ this.uri = uri;
+}
+ServerException.prototype = Object.create(Exception.prototype);
+ServerException.prototype.constructor = ServerException;
diff --git a/comm/mail/components/accountcreation/GuessConfig.jsm b/comm/mail/components/accountcreation/GuessConfig.jsm
new file mode 100644
index 0000000000..3d590311d9
--- /dev/null
+++ b/comm/mail/components/accountcreation/GuessConfig.jsm
@@ -0,0 +1,1317 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["GuessConfig"];
+
+const { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "AccountConfig",
+ "resource:///modules/accountcreation/AccountConfig.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const {
+ Abortable,
+ alertPrompt,
+ assert,
+ CancelledException,
+ ddump,
+ deepCopy,
+ Exception,
+ gAccountSetupLogger,
+ getStringBundle,
+ NotReached,
+ UserCancelledException,
+} = AccountCreationUtils;
+
+/**
+ * Try to guess the config, by:
+ * - guessing hostnames (pop3.<domain>, pop.<domain>, imap.<domain>,
+ * mail.<domain> etc.)
+ * - probing known ports (for IMAP, POP3 etc., with SSL, STARTTLS etc.)
+ * - opening a connection via the right protocol and checking the
+ * protocol-specific CAPABILITIES like that the server returns.
+ *
+ * Final verification is not done here, but in verifyConfig().
+ *
+ * This function is async.
+ *
+ * @param domain {String} the domain part of the email address
+ * @param progressCallback {function(type, hostname, port, socketType, done)}
+ * Called when we try a new hostname/port.
+ * type {String-enum} @see AccountConfig type - "imap", "pop3", "smtp"
+ * hostname {String}
+ * port {Integer}
+ * socketType {nsMsgSocketType} @see MailNewsTypes2.idl
+ * 0 = plain, 2 = STARTTLS, 3 = SSL
+ * done {Boolean} false, if we start probing this host/port, true if we're
+ * done and the host is good. (there is no notification when a host is
+ * bad, we'll just tell about the next host tried)
+ * @param successCallback {function(config {AccountConfig})}
+ * Called when we could guess the config.
+ * param accountConfig {AccountConfig} The guessed account config.
+ * username, password, realname, emailaddress etc. are not filled out,
+ * but placeholders to be filled out via replaceVariables().
+ * @param errorCallback function(ex)
+ * Called when we could guess not the config, either
+ * because we have not found anything or
+ * because there was an error (e.g. no network connection).
+ * The ex.message will contain a user-presentable message.
+ * @param resultConfig {AccountConfig} (optional)
+ * A config which may be partially filled in. If so, it will be used as base
+ * for the guess.
+ * @param which {String-enum} (optional) "incoming", "outgoing", or "both".
+ * Default "both". Whether to guess only the incoming or outgoing server.
+ * @result {Abortable} Allows you to cancel the guess
+ */
+function guessConfig(
+ domain,
+ progressCallback,
+ successCallback,
+ errorCallback,
+ resultConfig,
+ which
+) {
+ assert(typeof progressCallback == "function", "need progressCallback");
+ assert(typeof successCallback == "function", "need successCallback");
+ assert(typeof errorCallback == "function", "need errorCallback");
+
+ // Servers that we know enough that they support OAuth2 do not need guessing.
+ if (resultConfig.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) {
+ successCallback(resultConfig);
+ return new Abortable();
+ }
+
+ if (!resultConfig) {
+ resultConfig = new lazy.AccountConfig();
+ }
+ resultConfig.source = lazy.AccountConfig.kSourceGuess;
+
+ if (!which) {
+ which = "both";
+ }
+
+ if (!Services.prefs.getBoolPref("mailnews.auto_config.guess.enabled")) {
+ errorCallback("Guessing config disabled per user preference");
+ return new Abortable();
+ }
+
+ var incomingHostDetector = null;
+ var outgoingHostDetector = null;
+ var incomingEx = null; // if incoming had error, store ex here
+ var outgoingEx = null; // if incoming had error, store ex here
+ var incomingDone = which == "outgoing";
+ var outgoingDone = which == "incoming";
+ // If we're offline, we're going to pick the most common settings.
+ // (Not the "best" settings, but common).
+ if (Services.io.offline) {
+ // TODO: don't do this. Bug 599173.
+ resultConfig.source = lazy.AccountConfig.kSourceUser;
+ resultConfig.incoming.hostname = "mail." + domain;
+ resultConfig.incoming.username = resultConfig.identity.emailAddress;
+ resultConfig.outgoing.username = resultConfig.identity.emailAddress;
+ resultConfig.incoming.type = "imap";
+ resultConfig.incoming.port = 143;
+ resultConfig.incoming.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+ resultConfig.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext;
+ resultConfig.outgoing.hostname = "smtp." + domain;
+ resultConfig.outgoing.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+ resultConfig.outgoing.port = 587;
+ resultConfig.outgoing.auth = Ci.nsMsgAuthMethod.passwordCleartext;
+ resultConfig.incomingAlternatives.push({
+ hostname: "mail." + domain,
+ username: resultConfig.identity.emailAddress,
+ type: "pop3",
+ port: 110,
+ socketType: Ci.nsMsgSocketType.alwaysSTARTTLS,
+ auth: Ci.nsMsgAuthMethod.passwordCleartext,
+ });
+ successCallback(resultConfig);
+ return new Abortable();
+ }
+ var progress = function (thisTry) {
+ progressCallback(
+ protocolToString(thisTry.protocol),
+ thisTry.hostname,
+ thisTry.port,
+ thisTry.socketType,
+ false,
+ resultConfig
+ );
+ };
+
+ var checkDone = function () {
+ if (incomingEx) {
+ try {
+ errorCallback(incomingEx, resultConfig);
+ } catch (e) {
+ console.error(e);
+ alertPrompt("Error in errorCallback for guessConfig()", e);
+ }
+ return;
+ }
+ if (outgoingEx) {
+ try {
+ errorCallback(outgoingEx, resultConfig);
+ } catch (e) {
+ console.error(e);
+ alertPrompt("Error in errorCallback for guessConfig()", e);
+ }
+ return;
+ }
+ if (incomingDone && outgoingDone) {
+ try {
+ successCallback(resultConfig);
+ } catch (e) {
+ try {
+ errorCallback(e);
+ } catch (e) {
+ console.error(e);
+ alertPrompt("Error in errorCallback for guessConfig()", e);
+ }
+ }
+ }
+ };
+
+ var logger = gAccountSetupLogger;
+ var HostTryToAccountServer = function (thisTry, server) {
+ server.type = protocolToString(thisTry.protocol);
+ server.hostname = thisTry.hostname;
+ server.port = thisTry.port;
+ server.socketType = thisTry.socketType;
+ server.auth =
+ thisTry.authMethod || chooseBestAuthMethod(thisTry.authMethods);
+ server.authAlternatives = thisTry.authMethods;
+ // TODO
+ // cert is also bad when targetSite is set. (Same below for incoming.)
+ // Fix SSLErrorHandler and security warning dialog in accountSetup.js.
+ server.badCert = thisTry.selfSignedCert;
+ server.targetSite = thisTry.targetSite;
+ logger.info(
+ "CHOOSING " +
+ server.type +
+ " " +
+ server.hostname +
+ ":" +
+ server.port +
+ ", auth method " +
+ server.auth +
+ (server.authAlternatives.length
+ ? " " + server.authAlternatives.join(",")
+ : "") +
+ ", socketType " +
+ server.socketType +
+ (server.badCert ? " (bad cert!)" : "")
+ );
+ };
+
+ var outgoingSuccess = function (thisTry, alternativeTries) {
+ assert(thisTry.protocol == SMTP, "I only know SMTP for outgoing");
+ // Ensure there are no previously saved outgoing errors, if we've got
+ // success here.
+ outgoingEx = null;
+ HostTryToAccountServer(thisTry, resultConfig.outgoing);
+
+ for (let alternativeTry of alternativeTries) {
+ // resultConfig.createNewOutgoing(); misses username etc., so copy
+ let altServer = deepCopy(resultConfig.outgoing);
+ HostTryToAccountServer(alternativeTry, altServer);
+ assert(resultConfig.outgoingAlternatives);
+ resultConfig.outgoingAlternatives.push(altServer);
+ }
+
+ progressCallback(
+ resultConfig.outgoing.type,
+ resultConfig.outgoing.hostname,
+ resultConfig.outgoing.port,
+ resultConfig.outgoing.socketType,
+ true,
+ resultConfig
+ );
+ outgoingDone = true;
+ checkDone();
+ };
+
+ var incomingSuccess = function (thisTry, alternativeTries) {
+ // Ensure there are no previously saved incoming errors, if we've got
+ // success here.
+ incomingEx = null;
+ HostTryToAccountServer(thisTry, resultConfig.incoming);
+
+ for (let alternativeTry of alternativeTries) {
+ // resultConfig.createNewIncoming(); misses username etc., so copy
+ let altServer = deepCopy(resultConfig.incoming);
+ HostTryToAccountServer(alternativeTry, altServer);
+ assert(resultConfig.incomingAlternatives);
+ resultConfig.incomingAlternatives.push(altServer);
+ }
+
+ progressCallback(
+ resultConfig.incoming.type,
+ resultConfig.incoming.hostname,
+ resultConfig.incoming.port,
+ resultConfig.incoming.socketType,
+ true,
+ resultConfig
+ );
+ incomingDone = true;
+ checkDone();
+ };
+
+ var incomingError = function (ex) {
+ incomingEx = ex;
+ checkDone();
+ incomingHostDetector.cancel(new CancelOthersException());
+ outgoingHostDetector.cancel(new CancelOthersException());
+ };
+
+ var outgoingError = function (ex) {
+ outgoingEx = ex;
+ checkDone();
+ incomingHostDetector.cancel(new CancelOthersException());
+ outgoingHostDetector.cancel(new CancelOthersException());
+ };
+
+ incomingHostDetector = new IncomingHostDetector(
+ progress,
+ incomingSuccess,
+ incomingError
+ );
+ outgoingHostDetector = new OutgoingHostDetector(
+ progress,
+ outgoingSuccess,
+ outgoingError
+ );
+ if (which == "incoming" || which == "both") {
+ incomingHostDetector.start(
+ resultConfig.incoming.hostname ? resultConfig.incoming.hostname : domain,
+ !!resultConfig.incoming.hostname,
+ resultConfig.incoming.type,
+ resultConfig.incoming.port,
+ resultConfig.incoming.socketType,
+ resultConfig.incoming.auth
+ );
+ }
+ if (which == "outgoing" || which == "both") {
+ outgoingHostDetector.start(
+ resultConfig.outgoing.hostname ? resultConfig.outgoing.hostname : domain,
+ !!resultConfig.outgoing.hostname,
+ "smtp",
+ resultConfig.outgoing.port,
+ resultConfig.outgoing.socketType,
+ resultConfig.outgoing.auth
+ );
+ }
+
+ return new GuessAbortable(incomingHostDetector, outgoingHostDetector);
+}
+
+function GuessAbortable(incomingHostDetector, outgoingHostDetector) {
+ Abortable.call(this);
+ this._incomingHostDetector = incomingHostDetector;
+ this._outgoingHostDetector = outgoingHostDetector;
+}
+GuessAbortable.prototype = Object.create(Abortable.prototype);
+GuessAbortable.prototype.constructor = GuessAbortable;
+GuessAbortable.prototype.cancel = function (ex) {
+ this._incomingHostDetector.cancel(ex);
+ this._outgoingHostDetector.cancel(ex);
+};
+
+// --------------
+// Implementation
+
+// Objects, functions and constants that follow are not to be used outside
+// this file.
+var kNotTried = 0;
+var kOngoing = 1;
+var kFailed = 2;
+var kSuccess = 3;
+
+/**
+ * Internal object holding one server that we should try or did try.
+ * Used as |thisTry|.
+ *
+ * Note: The consts it uses for protocol is defined towards the end of this file
+ * and not the same as those used in AccountConfig (type). (fix
+ * this)
+ */
+function HostTry() {}
+HostTry.prototype = {
+ // IMAP, POP or SMTP
+ protocol: UNKNOWN,
+ // {String}
+ hostname: undefined,
+ // {Integer}
+ port: undefined,
+ // {nsMsgSocketType}
+ socketType: UNKNOWN,
+ // {String} what to send to server
+ commands: null,
+ // {Integer-enum} kNotTried, kOngoing, kFailed or kSuccess
+ status: kNotTried,
+ // {Abortable} allows to cancel the socket comm
+ abortable: null,
+
+ // {Array of {Integer-enum}} @see _advertisesAuthMethods() result
+ // Info about the server, from the protocol and SSL chat
+ authMethods: null,
+ // {String} Whether the SSL cert is not from a proper CA
+ selfSignedCert: false,
+ // {String} Which host the SSL cert is made for, if not hostname.
+ // If set, this is an SSL error.
+ targetSite: null,
+};
+
+/**
+ * When the success or errorCallbacks are called to abort the other requests
+ * which happened in parallel, this ex is used as param for cancel(), so that
+ * the cancel doesn't trigger another callback.
+ */
+function CancelOthersException() {
+ CancelledException.call(this, "we're done, cancelling the other probes");
+}
+CancelOthersException.prototype = Object.create(CancelledException.prototype);
+CancelOthersException.prototype.constructor = CancelOthersException;
+
+/**
+ * @param successCallback {function(result {HostTry}, alts {Array of HostTry})}
+ * Called when the config is OK
+ * |result| is the most preferred server.
+ * |alts| currently exists only for |IncomingHostDetector| and contains
+ * some servers of the other type (POP3 instead of IMAP), if available.
+ * @param errorCallback {function(ex)} Called when we could not find a config
+ * @param progressCallback { function(server {HostTry}) } Called when we tried
+ * (will try?) a new hostname and port
+ */
+function HostDetector(progressCallback, successCallback, errorCallback) {
+ this.mSuccessCallback = successCallback;
+ this.mProgressCallback = progressCallback;
+ this.mErrorCallback = errorCallback;
+ this._cancel = false;
+ // {Array of {HostTry}}, ordered by decreasing preference
+ this._hostsToTry = [];
+
+ // init logging
+ this._log = gAccountSetupLogger;
+ this._log.info("created host detector");
+}
+
+HostDetector.prototype = {
+ cancel(ex) {
+ this._cancel = true;
+ // We have to actively stop the network calls, as they may result in
+ // callbacks e.g. to the cert handler. If the dialog is gone by the time
+ // this happens, the javascript stack is horked.
+ for (let i = 0; i < this._hostsToTry.length; i++) {
+ let thisTry = this._hostsToTry[i]; // {HostTry}
+ if (thisTry.abortable) {
+ thisTry.abortable.cancel(ex);
+ }
+ thisTry.status = kFailed; // or don't set? Maybe we want to continue.
+ }
+ if (ex instanceof CancelOthersException) {
+ return;
+ }
+ if (!ex) {
+ ex = new CancelledException();
+ }
+ this.mErrorCallback(ex);
+ },
+
+ /**
+ * Start the detection.
+ *
+ * @param {string} domain - Domain to be used as base for guessing.
+ * Should be a domain (e.g. yahoo.co.uk).
+ * If hostIsPrecise == true, it should be a full hostname.
+ * @param {boolean} hostIsPrecise - If true, use only this hostname,
+ * do not guess hostnames.
+ * @param {"pop3"|"imap"|"exchange"|"smtp"|""} - Account type.
+ * @param {integer} port - The port to use. 0 to autodetect
+ * @param {nsMsgSocketType|-1} socketType - Socket type. -1 to autodetect.
+ * @param {nsMsgAuthMethod|0} authMethod - Authentication method. 0 to autodetect.
+ */
+ start(domain, hostIsPrecise, type, port, socketType, authMethod) {
+ domain = domain.replace(/\s*/g, ""); // Remove whitespace
+ if (!hostIsPrecise) {
+ hostIsPrecise = false;
+ }
+ var protocol = lazy.Sanitizer.translate(
+ type,
+ { imap: IMAP, pop3: POP, smtp: SMTP },
+ UNKNOWN
+ );
+ if (!port) {
+ port = UNKNOWN;
+ }
+ var ssl_only = Services.prefs.getBoolPref(
+ "mailnews.auto_config.guess.sslOnly"
+ );
+ this._cancel = false;
+ this._log.info(
+ `Starting ${protocol} detection on ${
+ !hostIsPrecise ? "~ " : ""
+ }${domain}:${port} with socketType=${socketType} and authMethod=${authMethod}`
+ );
+
+ // fill this._hostsToTry
+ this._hostsToTry = [];
+ var hostnamesToTry = [];
+ // if hostIsPrecise is true, it's because that's what the user input
+ // explicitly, and we'll just try it, nothing else.
+ if (hostIsPrecise) {
+ hostnamesToTry.push(domain);
+ } else {
+ hostnamesToTry = this._hostnamesToTry(protocol, domain);
+ }
+
+ for (let i = 0; i < hostnamesToTry.length; i++) {
+ let hostname = hostnamesToTry[i];
+ let hostEntries = this._portsToTry(hostname, protocol, socketType, port);
+ for (let j = 0; j < hostEntries.length; j++) {
+ let hostTry = hostEntries[j]; // from getHostEntry()
+ if (ssl_only && hostTry.socketType == NONE) {
+ continue;
+ }
+ hostTry.hostname = hostname;
+ hostTry.status = kNotTried;
+ hostTry.desc =
+ hostTry.hostname +
+ ":" +
+ hostTry.port +
+ " socketType=" +
+ hostTry.socketType +
+ " " +
+ protocolToString(hostTry.protocol);
+ hostTry.authMethod = authMethod;
+ this._hostsToTry.push(hostTry);
+ }
+ }
+
+ this._hostsToTry = sortTriesByPreference(this._hostsToTry);
+ this._tryAll();
+ },
+
+ // We make all host/port combinations run in parallel, store their
+ // results in an array, and as soon as one finishes successfully and all
+ // higher-priority ones have failed, we abort all lower-priority ones.
+
+ _tryAll() {
+ if (this._cancel) {
+ return;
+ }
+ var me = this;
+ var timeout = Services.prefs.getIntPref(
+ "mailnews.auto_config.guess.timeout"
+ );
+ // We assume we'll resolve the same proxy for all tries, and
+ // proceed to use the first resolved proxy for all tries. This
+ // assumption is generally sound, but not always: mechanisms like
+ // the pref network.proxy.no_proxies_on can make imap.domain and
+ // pop.domain resolve differently.
+ doProxy(this._hostsToTry[0].hostname, function (proxy) {
+ for (let i = 0; i < me._hostsToTry.length; i++) {
+ let thisTry = me._hostsToTry[i]; // {HostTry}
+ if (thisTry.status != kNotTried) {
+ continue;
+ }
+ me._log.info(thisTry.desc + ": initializing probe...");
+ if (i == 0) {
+ // showing 50 servers at once is pointless
+ me.mProgressCallback(thisTry);
+ }
+
+ thisTry.abortable = SocketUtil(
+ thisTry.hostname,
+ thisTry.port,
+ thisTry.socketType,
+ thisTry.commands,
+ timeout,
+ proxy,
+ new SSLErrorHandler(thisTry, me._log),
+ function (wiredata) {
+ // result callback
+ if (me._cancel) {
+ // Don't use response anymore.
+ return;
+ }
+ me.mProgressCallback(thisTry);
+ me._processResult(thisTry, wiredata);
+ me._checkFinished();
+ },
+ function (e) {
+ // error callback
+ if (me._cancel) {
+ // Who set cancel to true already called mErrorCallback().
+ return;
+ }
+ me._log.warn(thisTry.desc + ": " + e);
+ thisTry.status = kFailed;
+ me._checkFinished();
+ }
+ );
+ thisTry.status = kOngoing;
+ }
+ });
+ },
+
+ /**
+ * @param {HostTry} thisTry
+ * @param {string[]} wiredata - What the server returned in response to our protocol chat.
+ */
+ _processResult(thisTry, wiredata) {
+ if (thisTry._gotCertError) {
+ if (thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_MISMATCH) {
+ thisTry._gotCertError = 0;
+ thisTry.status = kFailed;
+ return;
+ }
+
+ if (
+ thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_UNTRUSTED ||
+ thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_TIME
+ ) {
+ this._log.info(
+ thisTry.desc + ": TRYING AGAIN, hopefully with exception recorded"
+ );
+ thisTry._gotCertError = 0;
+ thisTry.selfSignedCert = true; // _next_ run gets this exception
+ thisTry.status = kNotTried; // try again (with exception)
+ this._tryAll();
+ return;
+ }
+ }
+
+ if (wiredata == null || wiredata === undefined) {
+ this._log.info(thisTry.desc + ": no data");
+ thisTry.status = kFailed;
+ return;
+ }
+ this._log.info(thisTry.desc + ": wiredata: " + wiredata.join(""));
+ thisTry.authMethods = this._advertisesAuthMethods(
+ thisTry.protocol,
+ wiredata
+ );
+ if (
+ thisTry.socketType == STARTTLS &&
+ !this._hasSTARTTLS(thisTry, wiredata)
+ ) {
+ this._log.info(thisTry.desc + ": STARTTLS wanted, but not offered");
+ thisTry.status = kFailed;
+ return;
+ }
+ this._log.info(
+ thisTry.desc +
+ ": success" +
+ (thisTry.selfSignedCert ? " (selfSignedCert)" : "")
+ );
+ thisTry.status = kSuccess;
+
+ if (thisTry.selfSignedCert) {
+ // eh, ERROR_UNTRUSTED or ERROR_TIME
+ // We clear the temporary override now after success. If we clear it
+ // earlier we get into an infinite loop, probably because the cert
+ // remembering is temporary and the next try gets a new connection which
+ // isn't covered by that temporariness.
+ this._log.info(
+ thisTry.desc + ": clearing validity override for " + thisTry.hostname
+ );
+ Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService)
+ .clearValidityOverride(thisTry.hostname, thisTry.port, {});
+ }
+ },
+
+ _checkFinished() {
+ var successfulTry = null;
+ var successfulTryAlternative = null; // POP3
+ var unfinishedBusiness = false;
+ // this._hostsToTry is ordered by decreasing preference
+ for (let i = 0; i < this._hostsToTry.length; i++) {
+ let thisTry = this._hostsToTry[i];
+ if (thisTry.status == kNotTried || thisTry.status == kOngoing) {
+ unfinishedBusiness = true;
+ } else if (thisTry.status == kSuccess && !unfinishedBusiness) {
+ // thisTry is good, and all higher preference tries failed, so use this
+ if (!successfulTry) {
+ successfulTry = thisTry;
+ if (successfulTry.protocol == SMTP) {
+ break;
+ }
+ } else if (successfulTry.protocol != thisTry.protocol) {
+ successfulTryAlternative = thisTry;
+ break;
+ }
+ }
+ }
+ if (successfulTry && (successfulTryAlternative || !unfinishedBusiness)) {
+ this.mSuccessCallback(
+ successfulTry,
+ successfulTryAlternative ? [successfulTryAlternative] : []
+ );
+ this.cancel(new CancelOthersException());
+ } else if (!unfinishedBusiness) {
+ // all failed
+ this._log.info("ran out of options");
+ var errorMsg = getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties"
+ ).GetStringFromName("cannot_find_server.error");
+ this.mErrorCallback(new Exception(errorMsg));
+ // no need to cancel, all failed
+ }
+ // else let ongoing calls continue
+ },
+
+ /**
+ * Which auth mechanism the server claims to support.
+ * That doesn't necessarily reflect reality, it is more an upper bound.
+ *
+ * @param {integer} protocol - IMAP, POP or SMTP
+ * @param {string[]} capaResponse - On the wire data that the server returned.
+ * May be the full exchange or just capa.
+ * @returns {nsMsgAuthMethod[]} Advertised authentication methods,
+ * in decreasing order of preference.
+ * E.g. [ nsMsgAuthMethod.GSSAPI, nsMsgAuthMethod.passwordEncrypted ]
+ * for a server that supports only Kerberos and encrypted passwords.
+ */
+ _advertisesAuthMethods(protocol, capaResponse) {
+ // For IMAP, capabilities include e.g.:
+ // "AUTH=CRAM-MD5", "AUTH=NTLM", "AUTH=GSSAPI", "AUTH=MSN", "AUTH=PLAIN"
+ // for POP3, the auth mechanisms are returned in capa as the following:
+ // "CRAM-MD5", "NTLM", "MSN", "GSSAPI"
+ // For SMTP, EHLO will return AUTH and then a list of the
+ // mechanism(s) supported, e.g.,
+ // AUTH LOGIN NTLM MSN CRAM-MD5 GSSAPI
+ var supported = new Set();
+ var line = capaResponse.join("\n").toUpperCase();
+ var prefix = "";
+ if (protocol == POP) {
+ prefix = "";
+ } else if (protocol == IMAP) {
+ prefix = "AUTH=";
+ } else if (protocol == SMTP) {
+ prefix = "AUTH.*";
+ } else {
+ throw NotReached("must pass protocol");
+ }
+ // add in decreasing order of preference
+ if (new RegExp(prefix + "GSSAPI").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.GSSAPI);
+ }
+ if (new RegExp(prefix + "CRAM-MD5").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.passwordEncrypted);
+ }
+ if (new RegExp(prefix + "(NTLM|MSN)").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.NTLM);
+ }
+ if (new RegExp(prefix + "LOGIN").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.passwordCleartext);
+ }
+ if (new RegExp(prefix + "PLAIN").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.passwordCleartext);
+ }
+ if (protocol != IMAP || !line.includes("LOGINDISABLED")) {
+ supported.add(Ci.nsMsgAuthMethod.passwordCleartext);
+ }
+ // The array elements will be in the Set's order of addition.
+ return Array.from(supported);
+ },
+
+ _hasSTARTTLS(thisTry, wiredata) {
+ var capa = thisTry.protocol == POP ? "STLS" : "STARTTLS";
+ return (
+ thisTry.socketType == STARTTLS &&
+ wiredata.join("").toUpperCase().includes(capa)
+ );
+ },
+};
+
+/**
+ * @param {nsMsgAuthMethod[]} authMethods - Authentication methods to choose from.
+ * See return value of _advertisesAuthMethods()
+ * Note: the returned auth method will be removed from the array.
+ * @returns {nsMsgAuthMethod} one of them, the preferred one
+ * Note: this might be Kerberos, which might not actually work,
+ * so you might need to try the others, too.
+ */
+function chooseBestAuthMethod(authMethods) {
+ if (!authMethods || !authMethods.length) {
+ return Ci.nsMsgAuthMethod.passwordCleartext;
+ }
+ return authMethods.shift(); // take first (= most preferred)
+}
+
+function IncomingHostDetector(
+ progressCallback,
+ successCallback,
+ errorCallback
+) {
+ HostDetector.call(this, progressCallback, successCallback, errorCallback);
+}
+IncomingHostDetector.prototype = {
+ __proto__: HostDetector.prototype,
+ _hostnamesToTry(protocol, domain) {
+ var hostnamesToTry = [];
+ if (protocol != POP) {
+ hostnamesToTry.push("imap." + domain);
+ }
+ if (protocol != IMAP) {
+ hostnamesToTry.push("pop3." + domain);
+ hostnamesToTry.push("pop." + domain);
+ }
+ hostnamesToTry.push("mail." + domain);
+ hostnamesToTry.push(domain);
+ return hostnamesToTry;
+ },
+ _portsToTry: getIncomingTryOrder,
+};
+
+function OutgoingHostDetector(
+ progressCallback,
+ successCallback,
+ errorCallback
+) {
+ HostDetector.call(this, progressCallback, successCallback, errorCallback);
+}
+OutgoingHostDetector.prototype = {
+ __proto__: HostDetector.prototype,
+ _hostnamesToTry(protocol, domain) {
+ var hostnamesToTry = [];
+ hostnamesToTry.push("smtp." + domain);
+ hostnamesToTry.push("mail." + domain);
+ hostnamesToTry.push(domain);
+ return hostnamesToTry;
+ },
+ _portsToTry: getOutgoingTryOrder,
+};
+
+// ---------------------------------------------
+// Encode protocol ports and order of preference
+
+// Protocol Types
+var UNKNOWN = -1;
+var IMAP = 0;
+var POP = 1;
+var SMTP = 2;
+// Security Types
+var NONE = Ci.nsMsgSocketType.plain;
+var STARTTLS = Ci.nsMsgSocketType.alwaysSTARTTLS;
+var SSL = Ci.nsMsgSocketType.SSL;
+
+var IMAP_PORTS = {};
+IMAP_PORTS[NONE] = 143;
+IMAP_PORTS[STARTTLS] = 143;
+IMAP_PORTS[SSL] = 993;
+
+var POP_PORTS = {};
+POP_PORTS[NONE] = 110;
+POP_PORTS[STARTTLS] = 110;
+POP_PORTS[SSL] = 995;
+
+var SMTP_PORTS = {};
+SMTP_PORTS[NONE] = 587;
+SMTP_PORTS[STARTTLS] = 587;
+SMTP_PORTS[SSL] = 465;
+
+var CMDS = {};
+CMDS[IMAP] = ["1 CAPABILITY\r\n", "2 LOGOUT\r\n"];
+CMDS[POP] = ["CAPA\r\n", "QUIT\r\n"];
+CMDS[SMTP] = ["EHLO we-guess.mozilla.org\r\n", "QUIT\r\n"];
+
+/**
+ * Sort by preference of SSL, IMAP etc.
+ *
+ * @param tries {Array of {HostTry}}
+ * @returns {Array of {HostTry}}
+ */
+function sortTriesByPreference(tries) {
+ return tries.sort(function (a, b) {
+ // -1 = a is better; 1 = b is better; 0 = equal
+ // Prefer SSL/STARTTLS above all else
+ if (a.socketType != NONE && b.socketType == NONE) {
+ return -1;
+ }
+ if (b.socketType != NONE && a.socketType == NONE) {
+ return 1;
+ }
+ // Prefer IMAP over POP
+ if (a.protocol == IMAP && b.protocol == POP) {
+ return -1;
+ }
+ if (b.protocol == IMAP && a.protocol == POP) {
+ return 1;
+ }
+ // Prefer SSL/TLS over STARTTLS
+ if (a.socketType == SSL && b.socketType == STARTTLS) {
+ return -1;
+ }
+ if (a.socketType == STARTTLS && b.socketType == SSL) {
+ return 1;
+ }
+ // For hostnames, leave existing sorting, as in _hostnamesToTry()
+ // For ports, leave existing sorting, as in getOutgoingTryOrder()
+ return 0;
+ });
+}
+
+/**
+ * @returns {HostTry[]} Hosts to try.
+ */
+function getIncomingTryOrder(host, protocol, socketType, port) {
+ var lowerCaseHost = host.toLowerCase();
+
+ if (
+ protocol == UNKNOWN &&
+ (lowerCaseHost.startsWith("pop.") || lowerCaseHost.startsWith("pop3."))
+ ) {
+ protocol = POP;
+ } else if (protocol == UNKNOWN && lowerCaseHost.startsWith("imap.")) {
+ protocol = IMAP;
+ }
+
+ if (protocol != UNKNOWN) {
+ if (socketType == UNKNOWN) {
+ return [
+ getHostEntry(protocol, STARTTLS, port),
+ getHostEntry(protocol, SSL, port),
+ getHostEntry(protocol, NONE, port),
+ ];
+ }
+ return [getHostEntry(protocol, socketType, port)];
+ }
+ if (socketType == UNKNOWN) {
+ return [
+ getHostEntry(IMAP, STARTTLS, port),
+ getHostEntry(IMAP, SSL, port),
+ getHostEntry(POP, STARTTLS, port),
+ getHostEntry(POP, SSL, port),
+ getHostEntry(IMAP, NONE, port),
+ getHostEntry(POP, NONE, port),
+ ];
+ }
+ return [
+ getHostEntry(IMAP, socketType, port),
+ getHostEntry(POP, socketType, port),
+ ];
+}
+
+/**
+ * @returns {Array of {HostTry}}
+ */
+function getOutgoingTryOrder(host, protocol, socketType, port) {
+ assert(protocol == SMTP, "need SMTP as protocol for outgoing");
+ if (socketType == UNKNOWN) {
+ if (port == UNKNOWN) {
+ // neither SSL nor port known
+ return [
+ getHostEntry(SMTP, STARTTLS, UNKNOWN),
+ getHostEntry(SMTP, STARTTLS, 25),
+ getHostEntry(SMTP, SSL, UNKNOWN),
+ getHostEntry(SMTP, NONE, UNKNOWN),
+ getHostEntry(SMTP, NONE, 25),
+ ];
+ }
+ // port known, SSL not
+ return [
+ getHostEntry(SMTP, STARTTLS, port),
+ getHostEntry(SMTP, SSL, port),
+ getHostEntry(SMTP, NONE, port),
+ ];
+ }
+ // SSL known, port not
+ if (port == UNKNOWN) {
+ if (socketType == SSL) {
+ return [getHostEntry(SMTP, SSL, UNKNOWN)];
+ }
+ return [
+ getHostEntry(SMTP, socketType, UNKNOWN),
+ getHostEntry(SMTP, socketType, 25),
+ ];
+ }
+ // SSL and port known
+ return [getHostEntry(SMTP, socketType, port)];
+}
+
+/**
+ * @returns {HostTry} with proper default port and commands,
+ * but without hostname.
+ */
+function getHostEntry(protocol, socketType, port) {
+ if (!port || port == UNKNOWN) {
+ switch (protocol) {
+ case POP:
+ port = POP_PORTS[socketType];
+ break;
+ case IMAP:
+ port = IMAP_PORTS[socketType];
+ break;
+ case SMTP:
+ port = SMTP_PORTS[socketType];
+ break;
+ default:
+ throw new NotReached("unsupported protocol " + protocol);
+ }
+ }
+
+ var r = new HostTry();
+ r.protocol = protocol;
+ r.socketType = socketType;
+ r.port = port;
+ r.commands = CMDS[protocol];
+ return r;
+}
+
+// here -> AccountConfig
+function protocolToString(type) {
+ if (type == IMAP) {
+ return "imap";
+ }
+ if (type == POP) {
+ return "pop3";
+ }
+ if (type == SMTP) {
+ return "smtp";
+ }
+ throw new NotReached("unexpected protocol");
+}
+
+// ----------------------
+// SSL cert error handler
+
+/**
+ * @param thisTry {HostTry}
+ * @param logger {ConsoleAPI}
+ */
+function SSLErrorHandler(thisTry, logger) {
+ this._try = thisTry;
+ this._log = logger;
+ // _ gotCertError will be set to an error code (one of those defined in
+ // nsICertOverrideService)
+ this._gotCertError = 0;
+}
+SSLErrorHandler.prototype = {
+ processCertError(secInfo, targetSite) {
+ this._log.error("Got Cert error for " + targetSite);
+
+ if (!secInfo) {
+ return;
+ }
+
+ let cert = secInfo.serverCert;
+
+ let parts = targetSite.split(":");
+ let host = parts[0];
+ let port = parts[1];
+
+ /* The following 2 cert problems are unfortunately common:
+ * 1) hostname mismatch:
+ * user is customer at a domain hoster, he owns yourname.org,
+ * and the IMAP server is imap.hoster.com (but also reachable as
+ * imap.yourname.org), and has a cert for imap.hoster.com.
+ * 2) self-signed:
+ * a company has an internal IMAP server, and it's only for
+ * 30 employees, and they didn't want to buy a cert, so
+ * they use a self-signed cert.
+ *
+ * We would like the above to pass, somehow, with user confirmation.
+ * The following case should *not* pass:
+ *
+ * 1) MITM
+ * User has @gmail.com, and an attacker is between the user and
+ * the Internet and runs a man-in-the-middle (MITM) attack.
+ * Attacker controls DNS and sends imap.gmail.com to his own
+ * imap.attacker.com. He has either a valid, CA-issued
+ * cert for imap.attacker.com, or a self-signed cert.
+ * Of course, attacker.com could also be legit-sounding gmailservers.com.
+ *
+ * What makes it dangerous is that we (!) propose the server to the user,
+ * and he cannot judge whether imap.gmailservers.com is correct or not,
+ * and he will likely approve it.
+ */
+
+ if (secInfo.isDomainMismatch) {
+ this._try._gotCertError = Ci.nsICertOverrideService.ERROR_MISMATCH;
+ } else if (secInfo.isUntrusted) {
+ // e.g. self-signed
+ this._try._gotCertError = Ci.nsICertOverrideService.ERROR_UNTRUSTED;
+ } else if (secInfo.isNotValidAtThisTime) {
+ this._try._gotCertError = Ci.nsICertOverrideService.ERROR_TIME;
+ } else {
+ this._try._gotCertError = -1; // other
+ }
+
+ /* We will add a temporary cert exception here, so that
+ * we can continue and connect and try.
+ * But we will remove it again as soon as we close the
+ * connection, in _processResult().
+ * _gotCertError will serve as the marker that we
+ * have to clear the override later.
+ *
+ * In verifyConfig(), before we send the password, we *must*
+ * get another cert exception, this time with dialog to the user
+ * so that he gets informed about this and can make a choice.
+ */
+ this._try.targetSite = targetSite;
+ Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService)
+ .rememberValidityOverride(host, port, {}, cert, true); // temporary override
+ this._log.warn(`Added temporary override of bad cert for: ${host}:${port}`);
+ },
+};
+
+// -----------
+// Socket Util
+
+/**
+ * @param hostname {String} The DNS hostname to connect to.
+ * @param port {Integer} The numeric port to connect to on the host.
+ * @param socketType {nsMsgSocketType} SSL, STARTTLS or NONE
+ * @param commands {Array of String}: protocol commands
+ * to send to the server.
+ * @param timeout {Integer} seconds to wait for a server response, then cancel.
+ * @param proxy {nsIProxyInfo} The proxy to use (or null to not use any).
+ * @param sslErrorHandler {SSLErrorHandler}
+ * @param resultCallback {function(wiredata)} This function will
+ * be called with the result string array from the server
+ * or null if no communication occurred.
+ * @param errorCallback {function(e)}
+ */
+function SocketUtil(
+ hostname,
+ port,
+ socketType,
+ commands,
+ timeout,
+ proxy,
+ sslErrorHandler,
+ resultCallback,
+ errorCallback
+) {
+ assert(commands && commands.length, "need commands");
+
+ var index = 0; // commands[index] is next to send to server
+ var initialized = false;
+ var aborted = false;
+
+ function _error(e) {
+ if (aborted) {
+ return;
+ }
+ aborted = true;
+ errorCallback(e);
+ }
+
+ function timeoutFunc() {
+ if (!initialized) {
+ _error("timeout");
+ }
+ }
+
+ // In case DNS takes too long or does not resolve or another blocking
+ // issue occurs before the timeout can be set on the socket, this
+ // ensures that the listener callback will be fired in a timely manner.
+ // XXX There might to be some clean up needed after the timeout is fired
+ // for socket and io resources.
+
+ // The timeout value plus 2 seconds
+ setTimeout(timeoutFunc, timeout * 1000 + 2000);
+
+ var transportService = Cc[
+ "@mozilla.org/network/socket-transport-service;1"
+ ].getService(Ci.nsISocketTransportService);
+
+ // @see NS_NETWORK_SOCKET_CONTRACTID_PREFIX
+ var socketTypeName;
+ if (socketType == SSL) {
+ socketTypeName = ["ssl"];
+ } else if (socketType == STARTTLS) {
+ socketTypeName = ["starttls"];
+ } else {
+ socketTypeName = [];
+ }
+ var transport = transportService.createTransport(
+ socketTypeName,
+ hostname,
+ port,
+ proxy,
+ null
+ );
+
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, timeout);
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, timeout);
+
+ var outstream = transport.openOutputStream(0, 0, 0);
+ var stream = transport.openInputStream(0, 0, 0);
+ var instream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ instream.init(stream);
+
+ var dataListener = {
+ data: [],
+ onStartRequest(request) {
+ try {
+ initialized = true;
+ if (!aborted) {
+ // Send the first request
+ let outputData = commands[index++];
+ outstream.write(outputData, outputData.length);
+ }
+ } catch (e) {
+ _error(e);
+ }
+ },
+ async onStopRequest(request, status) {
+ try {
+ instream.close();
+ outstream.close();
+ // Did it fail because of a bad certificate?
+ let isCertError = false;
+ if (!Components.isSuccessCode(status)) {
+ let nssErrorsService = Cc[
+ "@mozilla.org/nss_errors_service;1"
+ ].getService(Ci.nsINSSErrorsService);
+ try {
+ let errorType = nssErrorsService.getErrorClass(status);
+ if (errorType == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ isCertError = true;
+ }
+ } catch (e) {
+ // nsINSSErrorsService.getErrorClass throws if given a non-STARTTLS,
+ // non-cert error, so ignore this.
+ }
+ }
+ if (isCertError) {
+ if (
+ Services.prefs.getBoolPref(
+ "mailnews.auto_config.guess.requireGoodCert",
+ true
+ )
+ ) {
+ gAccountSetupLogger.info(
+ `Bad (overridable) certificate for ${hostname}:${port}. Set mailnews.auto_config.guess.requireGoodCert to false to allow detecting this as a valid SSL/TLS configuration`
+ );
+ } else {
+ let socketTransport = transport.QueryInterface(
+ Ci.nsISocketTransport
+ );
+ let secInfo =
+ await socketTransport.tlsSocketControl?.asyncGetSecurityInfo();
+ sslErrorHandler.processCertError(secInfo, hostname + ":" + port);
+ }
+ }
+ resultCallback(this.data.length ? this.data : null);
+ } catch (e) {
+ _error(e);
+ }
+ },
+ onDataAvailable(request, inputStream, offset, count) {
+ try {
+ if (!aborted) {
+ let inputData = instream.read(count);
+ this.data.push(inputData);
+ if (index < commands.length) {
+ // Send the next request to the server.
+ let outputData = commands[index++];
+ outstream.write(outputData, outputData.length);
+ }
+ }
+ } catch (e) {
+ _error(e);
+ }
+ },
+ };
+
+ try {
+ var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+
+ pump.init(stream, 0, 0, false);
+ pump.asyncRead(dataListener);
+ return new SocketAbortable(transport);
+ } catch (e) {
+ _error(e);
+ }
+ return null;
+}
+
+function SocketAbortable(transport) {
+ Abortable.call(this);
+ assert(transport instanceof Ci.nsITransport, "need transport");
+ this._transport = transport;
+}
+SocketAbortable.prototype = Object.create(Abortable.prototype);
+SocketAbortable.prototype.constructor = UserCancelledException;
+SocketAbortable.prototype.cancel = function (ex) {
+ try {
+ this._transport.close(Cr.NS_ERROR_ABORT);
+ } catch (e) {
+ ddump("canceling socket failed: " + e);
+ }
+};
+
+/**
+ * Resolve a proxy for some domain and expose it via a callback.
+ *
+ * @param hostname {String} The hostname which a proxy will be resolved for
+ * @param resultCallback {function(proxyInfo)}
+ * Called after the proxy has been resolved for hostname.
+ * proxy {nsIProxyInfo} The resolved proxy, or null if none were found
+ * for hostname
+ */
+function doProxy(hostname, resultCallback) {
+ // This implements the nsIProtocolProxyCallback interface:
+ function ProxyResolveCallback() {}
+ ProxyResolveCallback.prototype = {
+ onProxyAvailable(req, uri, proxy, status) {
+ // Anything but a SOCKS proxy will be unusable for email.
+ if (proxy != null && proxy.type != "socks" && proxy.type != "socks4") {
+ proxy = null;
+ }
+ resultCallback(proxy);
+ },
+ };
+ var proxyService = Cc[
+ "@mozilla.org/network/protocol-proxy-service;1"
+ ].getService(Ci.nsIProtocolProxyService);
+ // Use some arbitrary scheme just because it is required...
+ var uri = Services.io.newURI("http://" + hostname);
+ // ... we'll ignore it any way. We prefer SOCKS since that's the
+ // only thing we can use for email protocols.
+ var proxyFlags =
+ Ci.nsIProtocolProxyService.RESOLVE_IGNORE_URI_SCHEME |
+ Ci.nsIProtocolProxyService.RESOLVE_PREFER_SOCKS_PROXY;
+ if (Services.prefs.getBoolPref("network.proxy.socks_remote_dns")) {
+ proxyFlags |= Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL;
+ }
+ proxyService.asyncResolve(uri, proxyFlags, new ProxyResolveCallback());
+}
+
+var GuessConfig = {
+ UNKNOWN,
+ IMAP,
+ POP,
+ SMTP,
+ NONE,
+ STARTTLS,
+ SSL,
+ getHostEntry,
+ getIncomingTryOrder,
+ getOutgoingTryOrder,
+ guessConfig,
+};
diff --git a/comm/mail/components/accountcreation/Sanitizer.jsm b/comm/mail/components/accountcreation/Sanitizer.jsm
new file mode 100644
index 0000000000..d6bc3918bc
--- /dev/null
+++ b/comm/mail/components/accountcreation/Sanitizer.jsm
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["Sanitizer"];
+
+const { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+
+const { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import(
+ "resource:///modules/hostnameUtils.jsm"
+);
+
+/**
+ * This is a generic input validation lib. Use it when you process
+ * data from the network.
+ *
+ * Just a few functions which verify, for security purposes, that the
+ * input variables (strings, if nothing else is noted) are of the expected
+ * type and syntax.
+ *
+ * The functions take a string (unless noted otherwise) and return
+ * the expected datatype in JS types. If the value is not as expected,
+ * they throw exceptions.
+ */
+
+// To debug, set mail.setup.loglevel="All" and kDebug = true.
+var kDebug = false;
+
+var Sanitizer = {
+ integer(unchecked) {
+ if (typeof unchecked == "number" && !isNaN(unchecked)) {
+ return unchecked;
+ }
+
+ var r = parseInt(unchecked);
+ if (isNaN(r)) {
+ throw new MalformedException("no_number.error", unchecked);
+ }
+
+ return r;
+ },
+
+ integerRange(unchecked, min, max) {
+ var int = this.integer(unchecked);
+ if (int < min) {
+ throw new MalformedException("number_too_small.error", unchecked);
+ }
+
+ if (int > max) {
+ throw new MalformedException("number_too_large.error", unchecked);
+ }
+
+ return int;
+ },
+
+ boolean(unchecked) {
+ if (typeof unchecked == "boolean") {
+ return unchecked;
+ }
+
+ if (unchecked == "true") {
+ return true;
+ }
+
+ if (unchecked == "false") {
+ return false;
+ }
+
+ throw new MalformedException("boolean.error", unchecked);
+ },
+
+ string(unchecked) {
+ return String(unchecked);
+ },
+
+ nonemptystring(unchecked) {
+ if (!unchecked) {
+ throw new MalformedException("string_empty.error", unchecked);
+ }
+
+ return this.string(unchecked);
+ },
+
+ /**
+ * Allow only letters, numbers, "-" and "_".
+ *
+ * Empty strings not allowed (good idea?).
+ */
+ alphanumdash(unchecked) {
+ var str = this.nonemptystring(unchecked);
+ if (!/^[a-zA-Z0-9\-\_]*$/.test(str)) {
+ throw new MalformedException("alphanumdash.error", unchecked);
+ }
+
+ return str;
+ },
+
+ /**
+ * DNS hostnames like foo.bar.example.com
+ * Allow only letters, numbers, "-" and "."
+ * Empty strings not allowed.
+ * Currently does not support IDN (international domain names).
+ */
+ hostname(unchecked) {
+ let str = cleanUpHostName(this.nonemptystring(unchecked));
+
+ // Allow placeholders. TODO move to a new hostnameOrPlaceholder()
+ // The regex is "anything, followed by one or more (placeholders than
+ // anything)". This doesn't catch the non-placeholder case, but that's
+ // handled down below.
+ if (/^[a-zA-Z0-9\-\.]*(%[A-Z0-9]+%[a-zA-Z0-9\-\.]*)+$/.test(str)) {
+ return str;
+ }
+
+ if (!isLegalHostNameOrIP(str)) {
+ throw new MalformedException("hostname_syntax.error", unchecked);
+ }
+
+ return str.toLowerCase();
+ },
+
+ /**
+ * A value which resembles an email address.
+ */
+ emailAddress(unchecked) {
+ let str = this.nonemptystring(unchecked);
+ if (!/^[a-z0-9\-%+_\.\*]+@[a-z0-9\-\.]+\.[a-z]+$/i.test(str)) {
+ throw new MalformedException("emailaddress_syntax.error", unchecked);
+ }
+
+ return str.toLowerCase();
+ },
+
+ /**
+ * A non-chrome URL that's safe to request.
+ */
+ url(unchecked) {
+ var str = this.string(unchecked);
+
+ // DANGER ZONE: data:text/javascript or data:text/html can contain
+ // JavaScript code, run in the caller's security context, and might allow
+ // arbitrary code execution, so these must be prevented at all costs.
+ // PNG and JPEG data: URLs are fine. But SVG is again dangerous,
+ // it can contain javascript, so it would create a critical security hole.
+ // Talk to BenB or bz before relaxing *any* of the checks in this function.
+ if (
+ str.startsWith("data:image/png;") ||
+ str.startsWith("data:image/jpeg;")
+ ) {
+ return new URL(str).href;
+ }
+
+ if (!str.startsWith("http:") && !str.startsWith("https:")) {
+ throw new MalformedException("url_scheme.error", unchecked);
+ }
+
+ var uri;
+ try {
+ uri = Services.io.newURI(str);
+ uri = uri.QueryInterface(Ci.nsIURL);
+ } catch (e) {
+ throw new MalformedException("url_parsing.error", unchecked);
+ }
+
+ if (uri.scheme != "http" && uri.scheme != "https") {
+ throw new MalformedException("url_scheme.error", unchecked);
+ }
+
+ return uri.spec;
+ },
+
+ /**
+ * A value which should be shown to the user in the UI as label
+ */
+ label(unchecked) {
+ return this.string(unchecked);
+ },
+
+ /**
+ * Allows only certain values as input, otherwise throw.
+ *
+ * @param unchecked {Any} The value to check
+ * @param allowedValues {Array} List of values that |unchecked| may have.
+ * @param defaultValue {Any} (Optional) If |unchecked| does not match
+ * anything in |mapping|, a |defaultValue| can be returned instead of
+ * throwing an exception. The latter is the default and happens when
+ * no |defaultValue| is passed.
+ * @throws MalformedException
+ */
+ enum(unchecked, allowedValues, defaultValue) {
+ for (let allowedValue of allowedValues) {
+ if (allowedValue == unchecked) {
+ return allowedValue;
+ }
+ }
+ // value is bad
+ if (typeof defaultValue == "undefined") {
+ throw new MalformedException("allowed_value.error", unchecked);
+ }
+ return defaultValue;
+ },
+
+ /**
+ * Like enum, allows only certain (string) values as input, but allows the
+ * caller to specify another value to return instead of the input value. E.g.,
+ * if unchecked == "foo", return 1, if unchecked == "bar", return 2,
+ * otherwise throw. This allows to translate string enums into integer enums.
+ *
+ * @param unchecked {Any} The value to check
+ * @param mapping {Object} Associative array. property name is the input
+ * value, property value is the output value. E.g. the example above
+ * would be: { foo: 1, bar : 2 }.
+ * Use quotes when you need freaky characters: "baz-" : 3.
+ * @param defaultValue {Any} (Optional) If |unchecked| does not match
+ * anything in |mapping|, a |defaultValue| can be returned instead of
+ * throwing an exception. The latter is the default and happens when
+ * no |defaultValue| is passed.
+ * @throws MalformedException
+ */
+ translate(unchecked, mapping, defaultValue) {
+ for (var inputValue in mapping) {
+ if (inputValue == unchecked) {
+ return mapping[inputValue];
+ }
+ }
+ // value is bad
+ if (typeof defaultValue == "undefined") {
+ throw new MalformedException("allowed_value.error", unchecked);
+ }
+ return defaultValue;
+ },
+};
+
+function MalformedException(msgID, uncheckedBadValue) {
+ var stringBundle = AccountCreationUtils.getStringBundle(
+ "chrome://messenger/locale/accountCreationUtil.properties"
+ );
+ var msg = stringBundle.GetStringFromName(msgID);
+ if (typeof kDebug != "undefined" && kDebug) {
+ msg += " (bad value: " + uncheckedBadValue + ")";
+ }
+ AccountCreationUtils.Exception.call(this, msg);
+}
+MalformedException.prototype = Object.create(
+ AccountCreationUtils.Exception.prototype
+);
+MalformedException.prototype.constructor = MalformedException;
diff --git a/comm/mail/components/accountcreation/content/accountHub.js b/comm/mail/components/accountcreation/content/accountHub.js
new file mode 100644
index 0000000000..a703ffce16
--- /dev/null
+++ b/comm/mail/components/accountcreation/content/accountHub.js
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Holds the main controller class.
+ *
+ * @type {?AccountHubControllerClass}
+ */
+var AccountHubController;
+
+/**
+ * Controller class to handle the primary views of the account setup flow.
+ * This class acts as a sort of controller to lazily load the needed views upon
+ * request. It doesn't handle any data and it should only be used to switch
+ * between the different setup flows.
+ * All methods of this class should be private, except for the open() method.
+ */
+class AccountHubControllerClass {
+ /**
+ * The account hub main modal dialog.
+ *
+ * @type {?HTMLElement}
+ */
+ #modal = null;
+
+ /**
+ * The currently visible view inside the dialog.
+ *
+ * @type {?HTMLElement}
+ */
+ #currentView = null;
+
+ /**
+ * Object containing all strings to trigger the needed methods for the various
+ * views.
+ */
+ #accounts = {
+ START: () => this.#viewStart(),
+ MAIL: () => this.#viewEmailSetup(),
+ CALENDAR: () => this.#viewCalendarSetup(),
+ ADDRESS_BOOK: () => this.#viewAddressBookSetup(),
+ CHAT: () => this.#viewChatSetup(),
+ FEED: () => this.#viewFeedSetup(),
+ NNTP: () => this.#viewNNTPSetup(),
+ IMPORT: () => this.#viewImportSetup(),
+ };
+
+ constructor() {
+ this.ready = this.#init();
+ }
+
+ async #init() {
+ await this.#loadScript("container");
+ const element = document.createElement("account-hub-container");
+ document.body.appendChild(element);
+ this.#modal = element.modal;
+
+ let closeButton = this.#modal.querySelector("#closeButton");
+ closeButton.hidden = !MailServices.accounts.accounts.length;
+ closeButton.addEventListener("click", () => this.#modal.close());
+
+ // Listen from closing requests coming from child elements.
+ this.#modal.addEventListener(
+ "request-close",
+ event => {
+ event.stopPropagation();
+ this.#modal.close();
+ },
+ {
+ capture: true,
+ }
+ );
+
+ this.#modal.addEventListener(
+ "open-view",
+ event => {
+ event.stopPropagation();
+ this.open(event.detail.type);
+ },
+ {
+ capture: true,
+ }
+ );
+
+ this.#modal.addEventListener("close", event => {
+ // Don't allow the dialog to be closed if some operations are can't be
+ // aborted or the UI can't be cleared.
+ if (!this.#reset()) {
+ event.preventDefault();
+ }
+ });
+
+ this.#modal.addEventListener("cancel", event => {
+ if (
+ !MailServices.accounts.accounts.length &&
+ !Services.prefs.getBoolPref("app.use_without_mail_account", false)
+ ) {
+ // Prevent closing the modal if no account is currently present and the
+ // user didn't request using Thunderbird without an email account.
+ event.preventDefault();
+ return;
+ }
+
+ // Don't allow the dialog to be canceled via the ESC key if some
+ // operations are in progress and can't be aborted or the UI can't be
+ // cleared.
+ if (!this.#reset()) {
+ event.preventDefault();
+ }
+ });
+ }
+
+ /**
+ * Check if we don't currently have the needed custom element for the
+ * requested view and load the needed script. We do this to avoid loading all
+ * the unnecessary account creation files.
+ *
+ * @param {string} view - The name of the view to load.
+ * @returns {Promise<void>} Resolves when custom element of the view is usable.
+ */
+ #loadScript(view) {
+ if (customElements.get(`account-hub-${view}`)) {
+ return Promise.resolve();
+ }
+ // eslint-disable-next-line no-unsanitized/method
+ return import(
+ `chrome://messenger/content/accountcreation/views/${view}.mjs`
+ );
+ }
+
+ /**
+ * Create a custom element and append it to the modal inner HTML, or simply
+ * show it if it was already loaded.
+ *
+ * @param {string} id - The ID of the template to clone.
+ */
+ #loadView(id) {
+ this.#hideViews();
+
+ let view = this.#modal.querySelector(id);
+ if (view) {
+ view.hidden = false;
+ this.#currentView = view;
+ // Update the UI to make sure we're refreshing old views.
+ this.#currentView.initUI();
+ return;
+ }
+
+ view = document.createElement(id);
+ this.#modal.appendChild(view);
+ this.#currentView = view;
+ }
+
+ /**
+ * Hide all the currently visible views.
+ */
+ #hideViews() {
+ for (let view of this.#modal.querySelectorAll(".account-hub-view")) {
+ view.hidden = true;
+ }
+ }
+
+ /**
+ * Open the main modal dialog and load the requested account setup view, or
+ * fallback to the initial start screen.
+ *
+ * @param {?string} type - Which account flow to load when the modal opens.
+ */
+ open(type = "START") {
+ // Interrupt if something went wrong while cleaning up a previously loaded
+ // view.
+ if (!this.#reset()) {
+ return;
+ }
+
+ this.#accounts[type].call();
+ if (!this.#modal.open) {
+ this.#modal.showModal();
+ }
+ }
+
+ /**
+ * Check if we have a current class and try to trigger the rest in order to
+ * handle abort operations and markup clean up, if possible.
+ *
+ * @returns {boolean} - True if the reset process was successful or we didn't
+ * have anything to reset.
+ */
+ #reset() {
+ let isClean = this.#currentView?.reset() ?? true;
+ // If the reset operation was successful, clear the current class.
+ if (isClean) {
+ this.#hideViews();
+ this.#currentView = null;
+ }
+ return isClean;
+ }
+
+ /**
+ * Show the initial view of the account hub dialog.
+ */
+ async #viewStart() {
+ await this.#loadScript("start");
+ this.#loadView("account-hub-start");
+ }
+
+ /**
+ * Show the email setup view.
+ */
+ async #viewEmailSetup() {
+ await this.#loadScript("email");
+ this.#loadView("account-hub-email");
+ }
+
+ /**
+ * TODO: Show the calendar setup view.
+ */
+ #viewCalendarSetup() {
+ console.log("Calendar setup");
+ }
+
+ /**
+ * TODO: Show the address book setup view.
+ */
+ #viewAddressBookSetup() {
+ console.log("Address Book setup");
+ }
+
+ /**
+ * TODO: Show the chat setup view.
+ */
+ #viewChatSetup() {
+ console.log("Chat setup");
+ }
+
+ /**
+ * TODO: Show the feed setup view.
+ */
+ #viewFeedSetup() {
+ console.log("Feed setup");
+ }
+
+ /**
+ * TODO: Show the newsgroup setup view.
+ */
+ #viewNNTPSetup() {
+ console.log("Newsgroup setup");
+ }
+
+ /**
+ * TODO: Show the import setup view.
+ */
+ #viewImportSetup() {
+ console.log("Import setup");
+ }
+}
+
+/**
+ * Open the account hub dialog and show the requested view.
+ *
+ * @param {?string} type - The type of view that should be loaded when the modal
+ * is showed. See AccountHubController::#accounts for a list references.
+ */
+async function openAccountHub(type) {
+ if (!AccountHubController) {
+ AccountHubController = new AccountHubControllerClass();
+ }
+ await AccountHubController.ready;
+ AccountHubController.open(type);
+}
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,
+ });
+}
diff --git a/comm/mail/components/accountcreation/content/accountSetup.xhtml b/comm/mail/components/accountcreation/content/accountSetup.xhtml
new file mode 100644
index 0000000000..ed1148c561
--- /dev/null
+++ b/comm/mail/components/accountcreation/content/accountSetup.xhtml
@@ -0,0 +1,1333 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html>
+<html
+ id="accountSetup"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mail:accountsetup"
+>
+ <head>
+ <title data-l10n-id="account-setup-tab-title"></title>
+ <meta name="color-scheme" content="light dark" />
+ <link
+ rel="icon"
+ href="chrome://messenger/skin/icons/new/compact/new-mail.svg"
+ />
+
+ <link rel="stylesheet" href="chrome://messenger/skin/messenger.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/menulist.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/inContentDialog.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/accountSetup.css" />
+
+ <link rel="localization" href="branding/brand.ftl" />
+ <link
+ rel="localization"
+ href="messenger/accountcreation/accountSetup.ftl"
+ />
+
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/accountUtils.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/accountcreation/accountSetup.js"
+ ></script>
+ </head>
+
+ <body>
+ <!-- Native HTML dialog used for setup cancel confirmation. -->
+ <dialog id="confirmExitDialog" class="account-setup-dialog">
+ <div class="dialog-container vertical">
+ <h2 class="dialog-title">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/info.svg"
+ alt=""
+ class="dialog-header-image"
+ />
+ <span data-l10n-id="exit-dialog-title"></span>
+ </h2>
+
+ <div class="dialog-container">
+ <p
+ data-l10n-id="exit-dialog-description"
+ class="dialog-description indent"
+ ></p>
+ </div>
+
+ <label class="toggle-container-with-text indent">
+ <input
+ id="useWithoutAccount"
+ type="checkbox"
+ onchange="gAccountSetup.toggleExitDialogButton(event);"
+ />
+ <span
+ data-l10n-id="account-setup-no-account-checkbox"
+ data-l10n-attrs="accesskey"
+ >
+ </span>
+ </label>
+ </div>
+
+ <menu class="dialog-menu-container">
+ <button
+ id="exitDialogConfirmButton"
+ data-l10n-id="exit-dialog-confirm-button"
+ ></button>
+ <button
+ id="exitDialogCancelButton"
+ data-l10n-id="exit-dialog-cancel-button"
+ class="primary"
+ ></button>
+ </menu>
+ </dialog>
+
+ <!-- Native HTML dialog used for Exchange confirmation. -->
+ <dialog id="exchangeDialog" class="account-setup-dialog">
+ <div class="dialog-container">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/question.svg"
+ alt=""
+ class="dialog-header-image"
+ />
+ <p id="exchangeDialogQuestion" class="dialog-description"></p>
+ </div>
+ <menu class="dialog-menu-container">
+ <button
+ id="exchangeDialogCancelButton"
+ data-l10n-id="exchange-dialog-cancel-button"
+ ></button>
+ <button
+ id="exchangeDialogConfirmButton"
+ data-l10n-id="exchange-dialog-confirm-button"
+ class="primary"
+ ></button>
+ </menu>
+ </dialog>
+
+ <!-- Native HTML dialog used for insecure password confirmation. -->
+ <dialog id="insecureDialog" class="account-setup-dialog dialog-critical">
+ <div class="dialog-container vertical">
+ <h2 class="warning-title">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ alt=""
+ class="dialog-header-image warning-icon"
+ />
+ <span data-l10n-id="account-setup-insecure-title"></span>
+ </h2>
+
+ <section
+ id="insecureSectionIncoming"
+ class="insecure-section content-blocking-category"
+ hidden="hidden"
+ >
+ <h3 data-l10n-id="account-setup-insecure-incoming-title"></h3>
+ <p id="warningIncoming"></p>
+ <p
+ id="detailsIncoming"
+ class="insecure-section-description indent"
+ ></p>
+ </section>
+
+ <section
+ id="insecureSectionOutgoing"
+ class="insecure-section content-blocking-category"
+ hidden="hidden"
+ >
+ <h3 data-l10n-id="account-setup-insecure-outgoing-title"></h3>
+ <p id="warningOutgoing"></p>
+ <p
+ id="detailsOutgoing"
+ class="insecure-section-description indent"
+ ></p>
+ </section>
+
+ <p
+ class="dialog-footnote"
+ data-l10n-id="account-setup-insecure-description"
+ >
+ <a
+ href="https://support.mozilla.org/products/thunderbird"
+ data-l10n-name="thunderbird-faq-link"
+ ></a>
+ </p>
+ </div>
+
+ <menu class="dialog-menu-container two-columns">
+ <aside>
+ <label class="toggle-container-with-text">
+ <input
+ id="acknowledgeWarning"
+ type="checkbox"
+ onchange="gSecurityWarningDialog.toggleAcknowledge();"
+ />
+ <span
+ data-l10n-id="account-setup-insecure-server-checkbox"
+ data-l10n-attrs="accesskey"
+ >
+ </span>
+ </label>
+ </aside>
+
+ <aside>
+ <button
+ data-l10n-id="insecure-dialog-cancel-button"
+ onclick="gSecurityWarningDialog.onCancel();"
+ ></button>
+ <button
+ id="insecureConfirmButton"
+ data-l10n-id="insecure-dialog-confirm-button"
+ class="primary"
+ disable="disabled"
+ onclick="gSecurityWarningDialog.onOK();"
+ ></button>
+ </aside>
+ </menu>
+ </dialog>
+
+ <!-- Native HTML dialog for Calendar synchronization. This is a streamlined
+ version of the calendar-properties-dialog.xhtml with fewer properties:
+ - Name
+ - Color
+ - Refresh rate
+ - Read only
+ - Show reminders
+ - Offline support
+ This dialog should be kept synced with the calendar-properties-dialog.xhtml
+ if one of these properties changes.
+ -->
+ <dialog id="calendarDialog" class="account-setup-dialog">
+ <div class="dialog-container vertical">
+ <div class="dialog-container">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/new-event.svg"
+ alt=""
+ class="dialog-header-image small"
+ />
+ <p
+ data-l10n-id="calendar-dialog-title"
+ class="dialog-description"
+ ></p>
+ </div>
+
+ <section class="calendar-dialog-form">
+ <label
+ for="calendarName"
+ data-l10n-id="account-setup-calendar-name-label"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="calendarName"
+ type="text"
+ autocomplete="off"
+ class="input-field input-grow"
+ data-l10n-id="account-setup-calendar-name-input"
+ required="required"
+ />
+ </div>
+
+ <label
+ for="calendarColor"
+ data-l10n-id="account-setup-calendar-color-label"
+ >
+ </label>
+ <div class="input-control">
+ <input id="calendarColor" type="color" />
+ </div>
+
+ <label
+ for="calendarRefresh"
+ data-l10n-id="account-setup-calendar-refresh-label"
+ >
+ </label>
+ <div class="input-control">
+ <select id="calendarRefresh" class="input-grow">
+ <option
+ data-l10n-id="account-setup-calendar-refresh-manual"
+ value="0"
+ ></option>
+ <option
+ data-l10n-id="account-setup-calendar-refresh-interval"
+ data-l10n-args='{ "count": 1 }'
+ value="1"
+ ></option>
+ <option
+ data-l10n-id="account-setup-calendar-refresh-interval"
+ data-l10n-args='{ "count": 5 }'
+ value="5"
+ ></option>
+ <option
+ data-l10n-id="account-setup-calendar-refresh-interval"
+ data-l10n-args='{ "count": 15 }'
+ value="15"
+ ></option>
+ <option
+ data-l10n-id="account-setup-calendar-refresh-interval"
+ data-l10n-args='{ "count": 30 }'
+ value="30"
+ selected="selected"
+ ></option>
+ <option
+ data-l10n-id="account-setup-calendar-refresh-interval"
+ data-l10n-args='{ "count": 60 }'
+ value="60"
+ ></option>
+ </select>
+ </div>
+ </section>
+
+ <section class="indent">
+ <label class="toggle-container-with-text">
+ <input id="calendarReadOnly" type="checkbox" />
+ <span
+ data-l10n-id="account-setup-calendar-read-only"
+ data-l10n-attrs="accesskey"
+ >
+ </span>
+ </label>
+
+ <label
+ id="calendarShowRemindersRow"
+ class="toggle-container-with-text"
+ >
+ <input
+ id="calendarShowReminders"
+ type="checkbox"
+ checked="checked"
+ />
+ <span
+ data-l10n-id="account-setup-calendar-show-reminders"
+ data-l10n-attrs="accesskey"
+ >
+ </span>
+ </label>
+
+ <label class="toggle-container-with-text">
+ <input
+ id="calendarOfflineSupport"
+ type="checkbox"
+ checked="checked"
+ />
+ <span
+ data-l10n-id="account-setup-calendar-offline-support"
+ data-l10n-attrs="accesskey"
+ >
+ </span>
+ </label>
+ </section>
+ </div>
+
+ <menu class="dialog-menu-container">
+ <button
+ id="calendarDialogCancelButton"
+ data-l10n-id="calendar-dialog-cancel-button"
+ ></button>
+ <button
+ id="calendarDialogConfirmButton"
+ data-l10n-id="calendar-dialog-confirm-button"
+ class="primary"
+ ></button>
+ </menu>
+ </dialog>
+
+ <header>
+ <h1
+ id="accountSetupTitle"
+ data-l10n-id="account-setup-title"
+ class="title"
+ ></h1>
+ <p
+ id="accountSetupDescription"
+ data-l10n-id="account-setup-description"
+ class="description"
+ ></p>
+ <p
+ id="accountSetupDescriptionSecondary"
+ data-l10n-id="account-setup-secondary-description"
+ class="description"
+ ></p>
+ </header>
+
+ <section class="main-container">
+ <aside id="setupView" class="column first-column">
+ <form id="form" onsubmit="gAccountSetup.onFormSubmit(event);">
+ <!-- Hidden submit field to enable the natural Enter keypress to
+ submit the form. We do this because we have the Continue and Done
+ button outside the form and we want to only handle the Enter to
+ submit on the primary fields inside the form. -->
+ <input type="submit" hidden="hidden" />
+ <label
+ for="realname"
+ data-l10n-id="account-setup-name-label"
+ data-l10n-attrs="accesskey"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="realname"
+ type="text"
+ autocomplete="off"
+ class="input-field"
+ data-l10n-id="account-setup-name-input"
+ oninput="gAccountSetup.onInputRealname();"
+ required="required"
+ />
+ <img
+ id="realnameInfo"
+ src="chrome://messenger/skin/icons/new/compact/info.svg"
+ data-l10n-id="account-setup-name-info-icon"
+ alt=""
+ class="form-icon"
+ />
+ <img
+ id="realnameWarning"
+ src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ data-l10n-id="account-setup-name-warning-icon"
+ alt=""
+ class="form-icon icon-warning"
+ />
+ </div>
+
+ <label
+ for="email"
+ data-l10n-id="account-setup-email-label"
+ data-l10n-attrs="accesskey"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="email"
+ type="email"
+ autocomplete="off"
+ data-l10n-id="account-setup-email-input"
+ class="input-field"
+ oninput="gAccountSetup.onInputEmail();"
+ required="required"
+ />
+ <img
+ id="emailInfo"
+ src="chrome://messenger/skin/icons/new/compact/info.svg"
+ data-l10n-id="account-setup-email-info-icon"
+ alt=""
+ class="form-icon"
+ />
+ <img
+ id="emailWarning"
+ src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ data-l10n-id="account-setup-email-warning-icon"
+ alt=""
+ class="form-icon icon-warning"
+ />
+ </div>
+
+ <div class="provisioner-button-container">
+ <button
+ id="provisionerButton"
+ type="button"
+ data-l10n-id="account-provisioner-button"
+ data-l10n-attrs="accesskey"
+ class="btn-link btn-link-new-email"
+ onclick="openAccountProvisionerTab();"
+ ></button>
+ </div>
+
+ <label
+ for="password"
+ data-l10n-id="account-setup-password-label"
+ data-l10n-attrs="accesskey"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="password"
+ type="password"
+ autocomplete="off"
+ class="input-field"
+ oninput="gAccountSetup.onInputPassword();"
+ />
+ <button
+ id="passwordToggleButton"
+ type="button"
+ onclick="gAccountSetup.passwordToggle(event);"
+ data-l10n-id="account-setup-password-toggle-show"
+ class="form-toggle-button"
+ hidden="hidden"
+ >
+ <img
+ id="passwordInfo"
+ src="chrome://messenger/skin/icons/new/compact/hidden.svg"
+ class="form-icon"
+ alt=""
+ />
+ </button>
+ </div>
+
+ <div class="remember-button-container">
+ <label class="toggle-container-with-text">
+ <input id="rememberPassword" type="checkbox" checked="checked" />
+ <span
+ data-l10n-id="account-setup-remember-password"
+ data-l10n-attrs="accesskey"
+ >
+ </span>
+ </label>
+ </div>
+
+ <div id="usernameRow" hidden="hidden">
+ <!-- This is only used for Exchange AutoDiscover, and even then
+ only when absolutely necessary and known to be needed. -->
+ <label
+ for="usernameEx"
+ data-l10n-id="account-setup-exchange-label"
+ data-l10n-attrs="accesskey"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="usernameEx"
+ type="text"
+ data-l10n-id="account-setup-exchange-input"
+ class="input-field"
+ oninput="gAccountSetup.onInputUsername();"
+ />
+ <img
+ id="usernameExInfo"
+ src="chrome://messenger/skin/icons/new/compact/info.svg"
+ class="form-icon"
+ data-l10n-id="account-setup-exchange-info-icon"
+ alt=""
+ />
+ </div>
+ </div>
+ </form>
+
+ <section
+ id="accountSetupNotifications"
+ class="account-setup-notifications"
+ >
+ <!-- Notifications will be lazily loaded here. -->
+ </section>
+
+ <!-- Results area -->
+ <section id="resultsArea" hidden="hidden">
+ <h4 id="resultAreaTitle" class="section-title"></h4>
+
+ <!-- IMAP -->
+ <div
+ id="resultsOption-imap"
+ class="content-blocking-category results-option"
+ >
+ <label class="toggle-container-with-text">
+ <input
+ id="resultSelect-imap"
+ type="radio"
+ value="imap"
+ name="resultsServerType"
+ onchange="gAccountSetup.onResultServerTypeChanged();"
+ />
+ <span class="strong">IMAP</span>
+ <p
+ class="result-indent"
+ data-l10n-id="account-setup-result-imap-description"
+ ></p>
+ </label>
+ <aside class="result-details">
+ <section class="result-details-row">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/inbox.svg"
+ alt=""
+ />
+ <div id="incomingTitle-imap" class="result-details-title">
+ <h4 data-l10n-id="account-setup-incoming-title"></h4>
+ </div>
+ <div id="incomingInfo-imap" class="result-host-info"></div>
+ </section>
+
+ <section class="result-details-row">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/outbox.svg"
+ alt=""
+ />
+ <div id="outgoingTitle-imap" class="result-details-title">
+ <h4 data-l10n-id="account-setup-outgoing-title"></h4>
+ </div>
+ <div id="outgoingInfo-imap" class="result-host-info"></div>
+ </section>
+
+ <section class="result-details-row">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/contact.svg"
+ alt=""
+ />
+ <div class="result-details-title">
+ <h4 data-l10n-id="account-setup-username-title"></h4>
+ </div>
+ <div id="usernameInfo-imap" class="result-host-info"></div>
+ </section>
+ </aside>
+ </div>
+
+ <!-- POP3 -->
+ <div
+ id="resultsOption-pop3"
+ class="content-blocking-category results-option"
+ >
+ <label class="toggle-container-with-text">
+ <input
+ id="resultSelect-pop3"
+ type="radio"
+ value="pop3"
+ name="resultsServerType"
+ onchange="gAccountSetup.onResultServerTypeChanged();"
+ />
+ <span class="strong">POP3</span>
+ <p
+ class="result-indent"
+ data-l10n-id="account-setup-result-pop-description"
+ ></p>
+ </label>
+ <aside class="result-details">
+ <section class="result-details-row">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/inbox.svg"
+ alt=""
+ />
+ <div id="incomingTitle-pop3" class="result-details-title">
+ <h4 data-l10n-id="account-setup-incoming-title"></h4>
+ </div>
+ <div id="incomingInfo-pop3" class="result-host-info"></div>
+ </section>
+
+ <section class="result-details-row">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/outbox.svg"
+ alt=""
+ />
+ <div id="outgoingTitle-pop3" class="result-details-title">
+ <h4 data-l10n-id="account-setup-outgoing-title"></h4>
+ </div>
+ <div id="outgoingInfo-pop3" class="result-host-info"></div>
+ </section>
+
+ <section class="result-details-row">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/contact.svg"
+ alt=""
+ />
+ <div class="result-details-title">
+ <h4 data-l10n-id="account-setup-username-title"></h4>
+ </div>
+ <div id="usernameInfo-pop3" class="result-host-info"></div>
+ </section>
+ </aside>
+ </div>
+
+ <!-- EXCHANGE -->
+ <div
+ id="resultsOption-exchange"
+ class="content-blocking-category results-option"
+ >
+ <label class="toggle-container-with-text">
+ <input
+ id="resultSelect-exchange"
+ type="radio"
+ value="exchange"
+ name="resultsServerType"
+ onchange="gAccountSetup.onResultServerTypeChanged();"
+ />
+ <span class="strong"> Exchange/Office365 </span>
+ <p
+ class="result-indent"
+ data-l10n-id="account-setup-result-exchange2-description"
+ ></p>
+ </label>
+ <aside class="result-details">
+ <section class="result-details-row">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/inbox.svg"
+ alt=""
+ />
+ <div id="incomingTitle-exchange" class="result-details-title">
+ <h4 data-l10n-id="account-setup-exchange-title"></h4>
+ </div>
+ <div id="resultExchangeHostname" class="result-host-info"></div>
+ </section>
+ <aside id="installAddonInfo">
+ <p id="resultAddonIntro"></p>
+ <div id="resultAddonInstallRows"></div>
+ </aside>
+ </aside>
+ </div>
+ </section>
+ <!-- END Results area -->
+
+ <!-- Manual edit area -->
+ <section id="manualConfigArea" hidden="hidden">
+ <h4
+ class="section-title"
+ data-l10n-id="account-setup-manual-config-title"
+ ></h4>
+
+ <!-- Incoming server section -->
+ <fieldset
+ class="manual-config-grid content-blocking-category"
+ aria-describedby="manualConfigDescription"
+ >
+ <legend
+ data-l10n-id="account-setup-incoming-server-legend"
+ ></legend>
+
+ <!-- Incoming Protocol -->
+ <aside>
+ <label
+ for="incomingProtocol"
+ data-l10n-id="account-setup-protocol-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <select
+ id="incomingProtocol"
+ onchange="gAccountSetup.onChangedProtocolIncoming();"
+ >
+ <option value="1">IMAP</option>
+ <option value="2">POP3</option>
+ <option
+ id="incomingProtocolExchange"
+ value="3"
+ hidden="hidden"
+ >
+ Exchange
+ </option>
+ </select>
+ </div>
+ </aside>
+
+ <!-- Incoming Server -->
+ <aside>
+ <label
+ for="incomingHostname"
+ data-l10n-id="account-setup-hostname-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="incomingHostname"
+ type="text"
+ placeholder="mail.example.com"
+ onchange="gAccountSetup.onChangeHostname();"
+ oninput="gAccountSetup.manualConfigChanged();"
+ class="host uri-element input-field"
+ />
+ </div>
+ </aside>
+
+ <!-- Incoming Port -->
+ <section>
+ <aside>
+ <label
+ for="incomingPort"
+ data-l10n-id="account-setup-port-label"
+ class="option-label"
+ >
+ </label>
+ <input
+ id="incomingPort"
+ type="number"
+ min="1"
+ max="65535"
+ onchange="gAccountSetup.onChangedPortIncoming();"
+ oninput="gAccountSetup.manualConfigChanged();"
+ class="input-field"
+ />
+ </aside>
+ </section>
+
+ <!-- Incoming SSL -->
+ <aside>
+ <label
+ for="incomingSsl"
+ data-l10n-id="account-setup-ssl-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <select
+ id="incomingSsl"
+ class="security"
+ onchange="gAccountSetup.onChangedSSLIncoming();"
+ >
+ <!-- @see nsMsgSocketType -->
+ <option
+ data-l10n-id="ssl-autodetect-option"
+ value="-1"
+ ></option>
+ <option
+ data-l10n-id="ssl-noencryption-option"
+ value="0"
+ ></option>
+ <option value="2">STARTTLS</option>
+ <option value="3">SSL/TLS</option>
+ </select>
+ </div>
+ </aside>
+
+ <!-- Incoming Authentication -->
+ <aside>
+ <label
+ for="incomingAuthMethod"
+ data-l10n-id="account-setup-auth-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <select
+ id="incomingAuthMethod"
+ class="auth"
+ onchange="gAccountSetup.onChangedInAuth();"
+ >
+ <option
+ data-l10n-id="ssl-autodetect-option"
+ value="0"
+ ></option>
+ <!-- Values defined in nsMsgAuthMethod. -->
+ <option
+ data-l10n-id="ssl-cleartext-password-option"
+ value="3"
+ ></option>
+ <option
+ data-l10n-id="ssl-encrypted-password-option"
+ value="4"
+ ></option>
+ <option value="5">Kerberos / GSSAPI</option>
+ <option value="6">NTLM</option>
+ <option id="in-authMethod-oauth2" value="10" hidden="hidden">
+ OAuth2
+ </option>
+ </select>
+ </div>
+ </aside>
+
+ <!-- Incoming Username -->
+ <aside>
+ <label
+ for="incomingUsername"
+ data-l10n-id="account-setup-username-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="incomingUsername"
+ type="text"
+ data-l10n-id="account-setup-email-input"
+ oninput="gAccountSetup.onInputInUsername();"
+ class="username input-field"
+ />
+ </div>
+ </aside>
+ </fieldset>
+
+ <!-- Outgoing server section -->
+ <fieldset
+ class="manual-config-grid content-blocking-category"
+ aria-describedby="manualConfigDescription"
+ >
+ <legend
+ data-l10n-id="account-setup-outgoing-server-legend"
+ ></legend>
+
+ <!-- Outgoing Server -->
+ <aside>
+ <label
+ for="outgoingHostname"
+ data-l10n-id="account-setup-hostname-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="outgoingHostname"
+ type="text"
+ placeholder="mail.example.com"
+ onchange="gAccountSetup.onChangeHostname();"
+ oninput="gAccountSetup.manualConfigChanged();"
+ class="input-field"
+ />
+ </div>
+ </aside>
+
+ <!-- Outgoing Port -->
+ <section>
+ <aside>
+ <label
+ for="outgoingPort"
+ data-l10n-id="account-setup-port-label"
+ class="option-label"
+ >
+ </label>
+ <input
+ id="outgoingPort"
+ type="number"
+ min="1"
+ max="65535"
+ onchange="gAccountSetup.onChangedPortOutgoing();"
+ oninput="gAccountSetup.manualConfigChanged();"
+ class="input-field"
+ />
+ </aside>
+ </section>
+
+ <!-- Outgoing SSL -->
+ <aside>
+ <label
+ for="outgoingSsl"
+ data-l10n-id="account-setup-ssl-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <select
+ id="outgoingSsl"
+ class="security"
+ onchange="gAccountSetup.onChangedSSLOutgoing();"
+ >
+ <!-- Values defined in nsMsgSocketType. -->
+ <option
+ data-l10n-id="ssl-autodetect-option"
+ value="-1"
+ ></option>
+ <option
+ data-l10n-id="ssl-noencryption-option"
+ value="0"
+ ></option>
+ <option value="2">STARTTLS</option>
+ <option value="3">SSL/TLS</option>
+ </select>
+ </div>
+ </aside>
+
+ <!-- Outgoing Authentication -->
+ <aside>
+ <label
+ for="outgoingAuthMethod"
+ data-l10n-id="account-setup-auth-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <select
+ id="outgoingAuthMethod"
+ class="auth"
+ onchange="gAccountSetup.onChangedOutAuth(event);"
+ >
+ <option
+ data-l10n-id="ssl-autodetect-option"
+ value="0"
+ ></option>
+ <!-- @see incoming -->
+ <option
+ id="outNoAuth"
+ data-l10n-id="ssl-no-authentication-option"
+ value="1"
+ ></option>
+ <option
+ data-l10n-id="ssl-cleartext-password-option"
+ value="3"
+ ></option>
+ <option
+ data-l10n-id="ssl-encrypted-password-option"
+ value="4"
+ ></option>
+ <option value="5">Kerberos / GSSAPI</option>
+ <option value="6">NTLM</option>
+ <option id="out-authMethod-oauth2" value="10" hidden="hidden">
+ OAuth2
+ </option>
+ </select>
+ </div>
+ </aside>
+
+ <!-- Outgoing Username -->
+ <aside>
+ <label
+ for="outgoingUsername"
+ data-l10n-id="account-setup-username-label"
+ class="option-label"
+ >
+ </label>
+ <div class="input-control">
+ <input
+ id="outgoingUsername"
+ type="text"
+ data-l10n-id="account-setup-email-input"
+ oninput="gAccountSetup.onInputOutUsername();"
+ class="username input-field"
+ />
+ </div>
+ </aside>
+ </fieldset>
+
+ <div class="link-row">
+ <button
+ id="advancedSetupButton"
+ class="btn-link"
+ data-l10n-id="account-setup-advanced-setup-button"
+ data-l10n-attrs="accesskey"
+ onclick="gAccountSetup.onAdvancedSetup();"
+ ></button>
+ </div>
+ </section>
+ <!-- END Manual edit area -->
+
+ <div class="action-buttons-container">
+ <aside>
+ <button
+ id="stopButton"
+ type="button"
+ data-l10n-id="account-setup-button-stop"
+ data-l10n-attrs="accesskey"
+ onclick="gAccountSetup.onStop();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="reTestButton"
+ type="button"
+ data-l10n-id="account-setup-button-retest"
+ data-l10n-attrs="accesskey"
+ onclick="gAccountSetup.testManualConfig();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="manualConfigButton"
+ type="button"
+ data-l10n-id="account-setup-button-manual-config"
+ data-l10n-attrs="accesskey"
+ class="btn-link"
+ onclick="gAccountSetup.onManualEdit();"
+ hidden="hidden"
+ ></button>
+ </aside>
+
+ <aside class="buttons-container-last">
+ <button
+ id="cancelButton"
+ type="button"
+ data-l10n-id="account-setup-button-cancel"
+ data-l10n-attrs="accesskey"
+ onclick="gAccountSetup.onCancel();"
+ ></button>
+ <button
+ id="continueButton"
+ type="button"
+ data-l10n-id="account-setup-button-continue"
+ data-l10n-attrs="accesskey"
+ class="primary"
+ onclick="gAccountSetup.onContinue();"
+ disabled="disabled"
+ ></button>
+ <button
+ id="createButton"
+ type="button"
+ data-l10n-id="account-setup-button-done"
+ data-l10n-attrs="accesskey"
+ class="primary"
+ onclick="gAccountSetup.onCreate();"
+ hidden="hidden"
+ disabled="disabled"
+ ></button>
+ </aside>
+ </div>
+
+ <p
+ id="manualConfigDescription"
+ data-l10n-id="account-setup-auto-description"
+ class="autoconfig-note tip-caption"
+ hidden="hidden"
+ ></p>
+
+ <p
+ id="footDescription"
+ data-l10n-id="account-setup-privacy-footnote2"
+ class="foot-note tip-caption"
+ ></p>
+ </aside>
+ <!-- END first column "setupView"-->
+
+ <aside
+ id="successView"
+ class="column first-column success-column"
+ hidden="hidden"
+ >
+ <section class="account-success-block">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/mail-secure.svg"
+ class="account-type-image"
+ alt=""
+ />
+ <aside>
+ <span id="newAccountName" class="account-name"></span>
+ <span id="newAccountEmail" class="account-email"></span>
+ </aside>
+ <span id="newAccountProtocol" class="protocol-type"></span>
+ </section>
+
+ <section class="quick-links">
+ <button
+ id="settingsButton"
+ type="button"
+ data-l10n-id="account-setup-settings-button"
+ class="quick-link"
+ ></button>
+ <button
+ id="encryptionButton"
+ type="button"
+ data-l10n-id="account-setup-encryption-button"
+ class="quick-link"
+ ></button>
+ <button
+ id="signatureButton"
+ type="button"
+ data-l10n-id="account-setup-signature-button"
+ class="quick-link"
+ ></button>
+ <button
+ id="dictionariesButton"
+ type="button"
+ data-l10n-id="account-setup-dictionaries-button"
+ class="quick-link"
+ onclick="openDictionariesTab();"
+ ></button>
+ </section>
+
+ <section id="linkedServices">
+ <h3 data-l10n-id="account-setup-linked-services-title"></h3>
+ <p id="linkedServicesDescription" class="tip-caption"></p>
+
+ <section id="syncNotifications" class="account-setup-notifications">
+ <!-- Notifications will be lazily loaded here. -->
+ </section>
+
+ <aside class="services-buttons-container">
+ <section
+ id="linkedAddressBooks"
+ class="content-blocking-category linked-services-section opened"
+ hidden="hidden"
+ >
+ <button
+ type="button"
+ class="linked-services-button"
+ onclick="gAccountSetup.toggleSetupContainer(event);"
+ >
+ <aside>
+ <span
+ class="account-name"
+ data-l10n-id="account-setup-address-books-button"
+ >
+ </span>
+ <p
+ id="addressBooksCountDescription"
+ class="linked-services-description"
+ ></p>
+ </aside>
+ <span class="linked-service-dropdown">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/nav-right.svg"
+ alt=""
+ />
+ </span>
+ </button>
+
+ <div id="addressBooksSetup" class="linked-services-container">
+ <ul class="linked-services-list"></ul>
+ <button
+ id="addressBooksSetupAll"
+ data-l10n-id="account-setup-connect-all-address-books"
+ class="btn-link self-center"
+ onclick="gAccountSetup.setupAllAddressBooks();"
+ hidden="hidden"
+ ></button>
+ </div>
+ </section>
+
+ <section class="indent">
+ <button
+ id="addressBookCardDAVButton"
+ type="button"
+ data-l10n-id="account-setup-address-book-carddav-button"
+ class="quick-link"
+ onclick="addNewAddressBook('CARDDAV');"
+ ></button>
+
+ <button
+ id="addressBookLDAPButton"
+ type="button"
+ data-l10n-id="account-setup-address-book-ldap-button"
+ class="quick-link"
+ onclick="addNewAddressBook('LDAP');"
+ ></button>
+ </section>
+
+ <section
+ id="linkedCalendars"
+ class="content-blocking-category linked-services-section opened"
+ hidden="hidden"
+ >
+ <button
+ type="button"
+ class="linked-services-button"
+ onclick="gAccountSetup.toggleSetupContainer(event);"
+ >
+ <aside>
+ <span
+ class="account-name"
+ data-l10n-id="account-setup-calendars-button"
+ >
+ </span>
+ <p
+ id="calendarsCountDescription"
+ class="linked-services-description"
+ ></p>
+ </aside>
+ <span class="linked-service-dropdown">
+ <img
+ src="chrome://messenger/skin/icons/new/compact/nav-right.svg"
+ alt=""
+ />
+ </span>
+ </button>
+
+ <div id="calendarsSetup" class="linked-services-container">
+ <ul class="linked-services-list"></ul>
+ <button
+ id="calendarsSetupAll"
+ data-l10n-id="account-setup-connect-all-calendars"
+ class="btn-link self-center"
+ onclick="gAccountSetup.setupAllCalendars();"
+ hidden="hidden"
+ ></button>
+ </div>
+ </section>
+
+ <section class="indent">
+ <button
+ id="createCalendarButton"
+ type="button"
+ data-l10n-id="account-setup-calendar-button"
+ class="quick-link"
+ onclick="showCalendarWizard();"
+ ></button>
+ </section>
+ </aside>
+ </section>
+
+ <section class="final-buttons-container">
+ <button
+ id="finishButton"
+ type="button"
+ data-l10n-id="account-setup-button-finish"
+ data-l10n-attrs="accesskey"
+ class="primary"
+ onclick="gAccountSetup.onFinish();"
+ ></button>
+ </section>
+ </aside>
+ <!-- END first column "successView"-->
+
+ <aside class="column second-column">
+ <article id="step1">
+ <img
+ src="chrome://messenger/skin/illustrations/octopus-setup.svg"
+ data-l10n-id="account-setup-step1-image"
+ alt=""
+ />
+ </article>
+ <article id="step2" hidden="hidden">
+ <img
+ src="chrome://messenger/skin/illustrations/sloth.svg"
+ data-l10n-id="account-setup-step2-image"
+ alt=""
+ />
+ </article>
+ <article id="step3" class="tip-caption" hidden="hidden">
+ <img
+ src="chrome://messenger/skin/illustrations/form.svg"
+ data-l10n-id="account-setup-step3-image"
+ alt=""
+ />
+ <p data-l10n-id="account-setup-selection-help"></p>
+ <a
+ href="https://support.mozilla.org/products/thunderbird/emails-thunderbird/set-up-email-thunderbird"
+ data-l10n-id="account-setup-documentation-help"
+ ></a>
+ -
+ <a
+ href="https://support.mozilla.org/products/thunderbird"
+ data-l10n-id="account-setup-forum-help"
+ ></a>
+ -
+ <a
+ href="https://www.mozilla.org/privacy/thunderbird/"
+ data-l10n-id="account-setup-privacy-help"
+ ></a>
+ </article>
+ <article id="step4" class="tip-caption" hidden="hidden">
+ <img
+ src="chrome://messenger/skin/illustrations/connection-error.svg"
+ data-l10n-id="account-setup-step4-image"
+ alt=""
+ />
+ <p data-l10n-id="account-setup-selection-error"></p>
+ <a
+ href="https://support.mozilla.org/products/thunderbird/emails-thunderbird/set-up-email-thunderbird"
+ data-l10n-id="account-setup-documentation-help"
+ ></a>
+ -
+ <a
+ href="https://support.mozilla.org/products/thunderbird"
+ data-l10n-id="account-setup-forum-help"
+ ></a>
+ </article>
+ <article id="step5" class="tip-caption" hidden="hidden">
+ <img
+ src="chrome://messenger/skin/illustrations/accounts.svg"
+ data-l10n-id="account-setup-step5-image"
+ alt=""
+ />
+ <p data-l10n-id="account-setup-success-help"></p>
+ <a
+ href="https://support.mozilla.org/products/thunderbird/learn-basics-get-started"
+ data-l10n-id="account-setup-getting-started"
+ ></a>
+ -
+ <a
+ href="https://support.mozilla.org/products/thunderbird"
+ data-l10n-id="account-setup-forum-help"
+ ></a>
+ -
+ <a
+ href="https://www.mozilla.org/privacy/thunderbird/"
+ data-l10n-id="account-setup-privacy-help"
+ ></a>
+ </article>
+ </aside>
+ <!-- END second column-->
+ </section>
+ </body>
+</html>
diff --git a/comm/mail/components/accountcreation/jar.mn b/comm/mail/components/accountcreation/jar.mn
new file mode 100644
index 0000000000..0a3389020e
--- /dev/null
+++ b/comm/mail/components/accountcreation/jar.mn
@@ -0,0 +1,12 @@
+# 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/.
+
+messenger.jar:
+ content/messenger/accountcreation/accountHub.js (content/accountHub.js)
+ content/messenger/accountcreation/accountSetup.js (content/accountSetup.js)
+ content/messenger/accountcreation/accountSetup.xhtml (content/accountSetup.xhtml)
+# Custom elements
+ content/messenger/accountcreation/views/container.mjs (views/container.mjs)
+ content/messenger/accountcreation/views/email.mjs (views/email.mjs)
+ content/messenger/accountcreation/views/start.mjs (views/start.mjs)
diff --git a/comm/mail/components/accountcreation/moz.build b/comm/mail/components/accountcreation/moz.build
new file mode 100644
index 0000000000..fa8ce3c258
--- /dev/null
+++ b/comm/mail/components/accountcreation/moz.build
@@ -0,0 +1,23 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ["jar.mn"]
+
+EXTRA_JS_MODULES.accountcreation += [
+ "AccountConfig.jsm",
+ "AccountCreationUtils.jsm",
+ "ConfigVerifier.jsm",
+ "CreateInBackend.jsm",
+ "ExchangeAutoDiscover.jsm",
+ "FetchConfig.jsm",
+ "FetchHTTP.jsm",
+ "GuessConfig.jsm",
+ "readFromXML.jsm",
+ "Sanitizer.jsm",
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "test/xpcshell/xpcshell.ini",
+]
diff --git a/comm/mail/components/accountcreation/readFromXML.jsm b/comm/mail/components/accountcreation/readFromXML.jsm
new file mode 100644
index 0000000000..b853a81117
--- /dev/null
+++ b/comm/mail/components/accountcreation/readFromXML.jsm
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["readFromXML"];
+
+const lazy = {};
+
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "AccountConfig",
+ "resource:///modules/accountcreation/AccountConfig.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "AccountCreationUtils",
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+/* eslint-disable complexity */
+/**
+ * Takes an XML snipplet (as JXON) and reads the values into
+ * a new AccountConfig object.
+ * It does so securely (or tries to), by trying to avoid remote execution
+ * and similar holes which can appear when reading too naively.
+ * Of course it cannot tell whether the actual values are correct,
+ * e.g. it can't tell whether the host name is a good server.
+ *
+ * The XML format is documented at
+ * <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat>
+ *
+ * @param clientConfigXML {JXON} - The <clientConfig> node.
+ * @param source {String} - Used for the subSource field of AccountConfig.
+ * @returns AccountConfig object filled with the data from XML
+ */
+function readFromXML(clientConfigXML, subSource) {
+ function array_or_undef(value) {
+ return value === undefined ? [] : value;
+ }
+ var exception;
+ if (
+ typeof clientConfigXML != "object" ||
+ !("clientConfig" in clientConfigXML) ||
+ !("emailProvider" in clientConfigXML.clientConfig)
+ ) {
+ dump(
+ `client config xml = ${JSON.stringify(clientConfigXML).substr(0, 50)} \n`
+ );
+ let stringBundle = lazy.AccountCreationUtils.getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties"
+ );
+ throw stringBundle.GetStringFromName("no_emailProvider.error");
+ }
+ var xml = clientConfigXML.clientConfig.emailProvider;
+
+ var d = new lazy.AccountConfig();
+ d.source = lazy.AccountConfig.kSourceXML;
+ d.subSource = `xml-from-${subSource}`;
+
+ d.id = lazy.Sanitizer.hostname(xml["@id"]);
+ d.displayName = d.id;
+ try {
+ d.displayName = lazy.Sanitizer.label(xml.displayName);
+ } catch (e) {
+ console.error(e);
+ }
+ for (var domain of xml.$domain) {
+ try {
+ d.domains.push(lazy.Sanitizer.hostname(domain));
+ } catch (e) {
+ console.error(e);
+ exception = e;
+ }
+ }
+ if (d.domains.length == 0) {
+ throw exception ? exception : "need proper <domain> in XML";
+ }
+ exception = null;
+
+ // incoming server
+ for (let iX of array_or_undef(xml.$incomingServer)) {
+ // input (XML)
+ let iO = d.createNewIncoming(); // output (object)
+ try {
+ // throws if not supported
+ iO.type = lazy.Sanitizer.enum(iX["@type"], [
+ "pop3",
+ "imap",
+ "nntp",
+ "exchange",
+ ]);
+ iO.hostname = lazy.Sanitizer.hostname(iX.hostname);
+ iO.port = lazy.Sanitizer.integerRange(iX.port, 1, 65535);
+ // We need a username even for Kerberos, need it even internally.
+ iO.username = lazy.Sanitizer.string(iX.username); // may be a %VARIABLE%
+
+ if ("password" in iX) {
+ d.rememberPassword = true;
+ iO.password = lazy.Sanitizer.string(iX.password);
+ }
+
+ for (let iXsocketType of array_or_undef(iX.$socketType)) {
+ try {
+ iO.socketType = lazy.Sanitizer.translate(iXsocketType, {
+ plain: Ci.nsMsgSocketType.plain,
+ SSL: Ci.nsMsgSocketType.SSL,
+ STARTTLS: Ci.nsMsgSocketType.alwaysSTARTTLS,
+ });
+ break; // take first that we support
+ } catch (e) {
+ exception = e;
+ }
+ }
+ if (iO.socketType == -1) {
+ throw exception ? exception : "need proper <socketType> in XML";
+ }
+ exception = null;
+
+ for (let iXauth of array_or_undef(iX.$authentication)) {
+ try {
+ iO.auth = lazy.Sanitizer.translate(iXauth, {
+ "password-cleartext": Ci.nsMsgAuthMethod.passwordCleartext,
+ // @deprecated TODO remove
+ plain: Ci.nsMsgAuthMethod.passwordCleartext,
+ "password-encrypted": Ci.nsMsgAuthMethod.passwordEncrypted,
+ // @deprecated TODO remove
+ secure: Ci.nsMsgAuthMethod.passwordEncrypted,
+ GSSAPI: Ci.nsMsgAuthMethod.GSSAPI,
+ NTLM: Ci.nsMsgAuthMethod.NTLM,
+ OAuth2: Ci.nsMsgAuthMethod.OAuth2,
+ });
+ break; // take first that we support
+ } catch (e) {
+ exception = e;
+ }
+ }
+ if (!iO.auth) {
+ throw exception ? exception : "need proper <authentication> in XML";
+ }
+ exception = null;
+
+ if (iO.type == "exchange") {
+ try {
+ if ("owaURL" in iX) {
+ iO.owaURL = lazy.Sanitizer.url(iX.owaURL);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ try {
+ if ("ewsURL" in iX) {
+ iO.ewsURL = lazy.Sanitizer.url(iX.ewsURL);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ try {
+ if ("easURL" in iX) {
+ iO.easURL = lazy.Sanitizer.url(iX.easURL);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ iO.oauthSettings = {
+ issuer: iO.hostname,
+ scope: iO.owaURL || iO.ewsURL || iO.easURL,
+ };
+ }
+ // defaults are in accountConfig.js
+ if (iO.type == "pop3" && "pop3" in iX) {
+ try {
+ if ("leaveMessagesOnServer" in iX.pop3) {
+ iO.leaveMessagesOnServer = lazy.Sanitizer.boolean(
+ iX.pop3.leaveMessagesOnServer
+ );
+ }
+ if ("daysToLeaveMessagesOnServer" in iX.pop3) {
+ iO.daysToLeaveMessagesOnServer = lazy.Sanitizer.integer(
+ iX.pop3.daysToLeaveMessagesOnServer
+ );
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ try {
+ if ("downloadOnBiff" in iX.pop3) {
+ iO.downloadOnBiff = lazy.Sanitizer.boolean(iX.pop3.downloadOnBiff);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ try {
+ if ("useGlobalPreferredServer" in iX) {
+ iO.useGlobalPreferredServer = lazy.Sanitizer.boolean(
+ iX.useGlobalPreferredServer
+ );
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ // processed successfully, now add to result object
+ if (!d.incoming.hostname) {
+ // first valid
+ d.incoming = iO;
+ } else {
+ d.incomingAlternatives.push(iO);
+ }
+ } catch (e) {
+ exception = e;
+ }
+ }
+ if (!d.incoming.hostname) {
+ // throw exception for last server
+ throw exception ? exception : "Need proper <incomingServer> in XML file";
+ }
+ exception = null;
+
+ // outgoing server
+ for (let oX of array_or_undef(xml.$outgoingServer)) {
+ // input (XML)
+ let oO = d.createNewOutgoing(); // output (object)
+ try {
+ if (oX["@type"] != "smtp") {
+ let stringBundle = lazy.AccountCreationUtils.getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties"
+ );
+ throw stringBundle.GetStringFromName("outgoing_not_smtp.error");
+ }
+ oO.hostname = lazy.Sanitizer.hostname(oX.hostname);
+ oO.port = lazy.Sanitizer.integerRange(oX.port, 1, 65535);
+
+ for (let oXsocketType of array_or_undef(oX.$socketType)) {
+ try {
+ oO.socketType = lazy.Sanitizer.translate(oXsocketType, {
+ plain: Ci.nsMsgSocketType.plain,
+ SSL: Ci.nsMsgSocketType.SSL,
+ STARTTLS: Ci.nsMsgSocketType.alwaysSTARTTLS,
+ });
+ break; // take first that we support
+ } catch (e) {
+ exception = e;
+ }
+ }
+ if (oO.socketType == -1) {
+ throw exception ? exception : "need proper <socketType> in XML";
+ }
+ exception = null;
+
+ for (let oXauth of array_or_undef(oX.$authentication)) {
+ try {
+ oO.auth = lazy.Sanitizer.translate(oXauth, {
+ // open relay
+ none: Ci.nsMsgAuthMethod.none,
+ // inside ISP or corp network
+ "client-IP-address": Ci.nsMsgAuthMethod.none,
+ // hope for the best
+ "smtp-after-pop": Ci.nsMsgAuthMethod.none,
+ "password-cleartext": Ci.nsMsgAuthMethod.passwordCleartext,
+ // @deprecated TODO remove
+ plain: Ci.nsMsgAuthMethod.passwordCleartext,
+ "password-encrypted": Ci.nsMsgAuthMethod.passwordEncrypted,
+ // @deprecated TODO remove
+ secure: Ci.nsMsgAuthMethod.passwordEncrypted,
+ GSSAPI: Ci.nsMsgAuthMethod.GSSAPI,
+ NTLM: Ci.nsMsgAuthMethod.NTLM,
+ OAuth2: Ci.nsMsgAuthMethod.OAuth2,
+ });
+
+ break; // take first that we support
+ } catch (e) {
+ exception = e;
+ }
+ }
+ if (!oO.auth) {
+ throw exception ? exception : "need proper <authentication> in XML";
+ }
+ exception = null;
+
+ if (
+ "username" in oX ||
+ // if password-based auth, we need a username,
+ // so go there anyways and throw.
+ oO.auth == Ci.nsMsgAuthMethod.passwordCleartext ||
+ oO.auth == Ci.nsMsgAuthMethod.passwordEncrypted
+ ) {
+ oO.username = lazy.Sanitizer.string(oX.username);
+ }
+
+ if ("password" in oX) {
+ d.rememberPassword = true;
+ oO.password = lazy.Sanitizer.string(oX.password);
+ }
+
+ try {
+ // defaults are in accountConfig.js
+ if ("addThisServer" in oX) {
+ oO.addThisServer = lazy.Sanitizer.boolean(oX.addThisServer);
+ }
+ if ("useGlobalPreferredServer" in oX) {
+ oO.useGlobalPreferredServer = lazy.Sanitizer.boolean(
+ oX.useGlobalPreferredServer
+ );
+ }
+ } catch (e) {
+ console.error(e);
+ }
+
+ // processed successfully, now add to result object
+ if (!d.outgoing.hostname) {
+ // first valid
+ d.outgoing = oO;
+ } else {
+ d.outgoingAlternatives.push(oO);
+ }
+ } catch (e) {
+ console.error(e);
+ exception = e;
+ }
+ }
+ if (!d.outgoing.hostname) {
+ // throw exception for last server
+ throw exception ? exception : "Need proper <outgoingServer> in XML file";
+ }
+ exception = null;
+
+ d.inputFields = [];
+ for (let inputField of array_or_undef(xml.$inputField)) {
+ try {
+ let fieldset = {
+ varname: lazy.Sanitizer.alphanumdash(inputField["@key"]).toUpperCase(),
+ displayName: lazy.Sanitizer.label(inputField["@label"]),
+ exampleValue: lazy.Sanitizer.label(inputField.value),
+ };
+ d.inputFields.push(fieldset);
+ } catch (e) {
+ console.error(e);
+ // For now, don't throw,
+ // because we don't support custom fields yet anyways.
+ }
+ }
+
+ return d;
+}
+/* eslint-enable complexity */
diff --git a/comm/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml b/comm/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml
new file mode 100644
index 0000000000..8b8c7c4ada
--- /dev/null
+++ b/comm/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml
@@ -0,0 +1,158 @@
+# 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/.
+
+<!--
+ List of all the account creation templates. Don't load any JavaScript or
+ CSS in here, those files are handled lazily in the controller and in the
+ various shadowRoots of the views.
+-->
+<html:template id="accountHubDialog" xmlns="http://www.w3.org/1999/xhtml">
+ <dialog class="account-hub-dialog">
+ <button id="closeButton" type="button">
+ <img src="" alt="" />
+ </button>
+ </dialog>
+</html:template>
+
+<html:template id="accountHubStart" xmlns="http://www.w3.org/1999/xhtml">
+ <header class="hub-header">
+ <div id="welcomeHeader" class="start-header" hidden="hidden">
+ <img src="chrome://branding/content/logo-gradient.svg" alt="" />
+ <h1 data-l10n-id="account-hub-welcome-line">
+ <span data-l10n-name="brand-name"></span>
+ </h1>
+ </div>
+ <div id="defaultHeader" class="start-header" hidden="hidden">
+ <img src="chrome://branding/content/logo-gradient.svg" alt="" />
+ <h1>
+ <span class="start-header-brand-name"
+ data-l10n-id="account-hub-brand"></span>
+ <span class="start-header-title"
+ data-l10n-id="account-hub-title"></span>
+ </h1>
+ </div>
+ </header>
+
+ <div class="hub-body">
+ <div class="hub-body-grid"></div>
+#ifdef NIGHTLY_BUILD
+ <button id="hubSyncButton"
+ data-l10n-id="account-hub-sync-button"
+ class="button button-flat"
+ type="button"
+ hidden="hidden"></button>
+#endif
+ </div>
+
+ <footer class="hub-footer">
+ <ul class="reset-list footer-links">
+ <li>
+ <a id="hubReleaseNotes"
+ data-l10n-id="account-hub-release-notes"></a>
+ </li>
+ <li>
+ <a href="https://support.mozilla.org/products/thunderbird"
+ data-l10n-id="account-hub-support"
+ onclick="openLinkExternally(this.href);"></a>
+ </li>
+ <li>
+ <a href="https://give.thunderbird.net/?utm_source=thunderbird_account_hub&amp;utm_medium=referral&amp;utm_content=hub_footer"
+ data-l10n-id="account-hub-donate"
+ onclick="openLinkExternally(this.href);"></a>
+ </li>
+ </ul>
+ </footer>
+</html:template>
+
+<html:template id="accountHubEmailSetup" xmlns="http://www.w3.org/1999/xhtml">
+ <form id="emailForm" class="account-hub-form">
+ <header class="hub-header">
+ <h1 class="sub-view-title" data-l10n-id="account-hub-email-title"></h1>
+ </header>
+
+ <div class="hub-body">
+ <label for="realName"
+ data-l10n-id="account-setup-name-label"
+ data-l10n-attrs="accesskey">
+ </label>
+ <div class="input-control">
+ <input id="realName" type="text"
+ class="input-field"
+ data-l10n-id="account-setup-name-input"
+ required="required" />
+ <img src="chrome://messenger/skin/icons/new/compact/info.svg"
+ data-l10n-id="account-setup-name-info-icon"
+ alt=""
+ class="form-icon" />
+ <img src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ data-l10n-id="account-setup-name-warning-icon"
+ alt=""
+ class="form-icon icon-warning" />
+ </div>
+
+ <label for="email"
+ data-l10n-id="account-setup-email-label"
+ data-l10n-attrs="accesskey">
+ </label>
+ <div class="input-control">
+ <input id="email" type="email"
+ data-l10n-id="account-setup-email-input"
+ class="input-field"
+ required="required" />
+ <img id="emailInfo"
+ src="chrome://messenger/skin/icons/new/compact/info.svg"
+ data-l10n-id="account-setup-email-info-icon"
+ alt=""
+ class="form-icon" />
+ <img id="emailWarning"
+ src="chrome://messenger/skin/icons/new/compact/warning.svg"
+ data-l10n-id="account-setup-email-warning-icon"
+ alt=""
+ class="form-icon icon-warning" />
+ </div>
+
+ <label for="password"
+ data-l10n-id="account-setup-password-label"
+ data-l10n-attrs="accesskey">
+ </label>
+ <div class="input-control">
+ <!-- Leave the placeholder empty for CSS visibility toggle of the
+ adjacent button. -->
+ <input id="password" type="password" class="input-field"
+ placeholder="" />
+ <button id="passwordToggleButton"
+ type="button"
+ data-l10n-id="account-setup-password-toggle-show"
+ class="form-toggle-button"
+ aria-pressed="false">
+ <img src="" alt="" class="form-icon" />
+ </button>
+ </div>
+
+ <div class="remember-button-container">
+ <label class="toggle-container-with-text">
+ <input id="rememberPassword" type="checkbox" checked="checked" />
+ <span data-l10n-id="account-setup-remember-password"
+ data-l10n-attrs="accesskey">
+ </span>
+ </label>
+ </div>
+ </div>
+
+ <footer class="hub-footer">
+ <menu class="dialog-menu-container two-columns">
+ <li>
+ <button id="emailGoBackButton" type="button"
+ data-l10n-id="account-hub-email-cancel-button"></button>
+ </li>
+ <li>
+ <button id="emailContinueButton" type="submit"
+ data-l10n-id="account-hub-email-continue-button"
+ class="primary"
+ disabled="disabled"></button>
+ </li>
+ </menu>
+ </footer>
+ </form>
+</html:template>
diff --git a/comm/mail/components/accountcreation/test/xpcshell/data/example.com.xml b/comm/mail/components/accountcreation/test/xpcshell/data/example.com.xml
new file mode 100644
index 0000000000..871ce732e2
--- /dev/null
+++ b/comm/mail/components/accountcreation/test/xpcshell/data/example.com.xml
@@ -0,0 +1,21 @@
+<clientConfig version="1.1">
+ <emailProvider id="example.com">
+ <domain>example.com</domain>
+ <displayName>example.com</displayName>
+ <displayShortName>example.com</displayShortName>
+ <incomingServer type="pop3">
+ <hostname>pop.example.com</hostname>
+ <port>995</port>
+ <socketType>SSL</socketType>
+ <authentication>plain</authentication>
+ <username>%EMAILLOCALPART%</username>
+ </incomingServer>
+ <outgoingServer type="smtp">
+ <hostname>smtp.example.com</hostname>
+ <port>587</port>
+ <socketType>STARTTLS</socketType>
+ <username>%EMAILADDRESS%</username>
+ <authentication>plain</authentication>
+ </outgoingServer>
+ </emailProvider>
+</clientConfig>
diff --git a/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigFetchDisk.js b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigFetchDisk.js
new file mode 100644
index 0000000000..5b57a42ebb
--- /dev/null
+++ b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigFetchDisk.js
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests getting a configuration file from the local isp directory and
+ * reading that file.
+ */
+
+// Globals
+
+var { AccountConfig } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountConfig.jsm"
+);
+var { FetchConfig } = ChromeUtils.import(
+ "resource:///modules/accountcreation/FetchConfig.jsm"
+);
+
+var kXMLFile = "example.com.xml";
+var fetchConfigAbortable;
+var copyLocation;
+
+function onTestSuccess(config) {
+ // Check that we got the expected config.
+ AccountConfig.replaceVariables(
+ config,
+ "Yamato Nadeshiko",
+ "yamato.nadeshiko@example.com",
+ "abc12345"
+ );
+
+ Assert.equal(config.incoming.username, "yamato.nadeshiko");
+ Assert.equal(config.outgoing.username, "yamato.nadeshiko@example.com");
+ Assert.equal(config.incoming.hostname, "pop.example.com");
+ Assert.equal(config.outgoing.hostname, "smtp.example.com");
+ Assert.equal(config.identity.realname, "Yamato Nadeshiko");
+ Assert.equal(config.identity.emailAddress, "yamato.nadeshiko@example.com");
+
+ Assert.equal(config.subSource, "xml-from-disk");
+
+ do_test_finished();
+}
+
+function onTestFailure(e) {
+ do_throw(e);
+}
+
+function run_test() {
+ registerCleanupFunction(finish_test);
+
+ // Copy the xml file into place
+ let file = do_get_file("data/" + kXMLFile);
+
+ copyLocation = Services.dirsvc.get("CurProcD", Ci.nsIFile);
+ copyLocation.append("isp");
+
+ file.copyTo(copyLocation, kXMLFile);
+
+ do_test_pending();
+
+ // Now run the actual test
+ // Note we keep a global copy of this so that the abortable doesn't get
+ // garbage collected before the async operation has finished.
+ fetchConfigAbortable = FetchConfig.fromDisk(
+ "example.com",
+ onTestSuccess,
+ onTestFailure
+ );
+}
+
+function finish_test() {
+ // Remove the test config file
+ copyLocation.append(kXMLFile);
+ copyLocation.remove(false);
+}
diff --git a/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigUtils.js b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigUtils.js
new file mode 100644
index 0000000000..763084f750
--- /dev/null
+++ b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigUtils.js
@@ -0,0 +1,319 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests for GuessConfig.jsm
+ *
+ * Currently tested:
+ * - getHostEntry function.
+ * - getIncomingTryOrder function.
+ * - getOutgoingTryOrder function.
+ *
+ * TODO:
+ * - Test the returned CMDS.
+ * - Figure out what else to test.
+ */
+
+// Globals
+
+var { GuessConfig } = ChromeUtils.import(
+ "resource:///modules/accountcreation/GuessConfig.jsm"
+);
+
+var {
+ UNKNOWN,
+ IMAP,
+ POP,
+ SMTP,
+ NONE,
+ STARTTLS,
+ SSL,
+ getHostEntry,
+ getIncomingTryOrder,
+ getOutgoingTryOrder,
+} = GuessConfig;
+
+/*
+ * UTILITIES
+ */
+
+function assert_equal(aA, aB, aWhy) {
+ if (aA != aB) {
+ do_throw(aWhy);
+ }
+ Assert.equal(aA, aB);
+}
+
+/**
+ * Test that two host entries are the same, ignoring the commands.
+ */
+function assert_equal_host_entries(hostEntry, expected) {
+ assert_equal(hostEntry.protocol, expected[0], "Protocols are different");
+ assert_equal(hostEntry.socketType, expected[1], "SSL values are different");
+ assert_equal(hostEntry.port, expected[2], "Port values are different");
+}
+
+/**
+ * Assert that the list of tryOrders are the same.
+ */
+function assert_equal_try_orders(aA, aB) {
+ assert_equal(aA.length, aB.length, "tryOrders have different length");
+ for (let [i, subA] of aA.entries()) {
+ let subB = aB[i];
+ assert_equal_host_entries(subA, subB);
+ }
+}
+
+/**
+ * Check that the POP calculations are correct for a given host and
+ * protocol.
+ */
+function checkPop(host, protocol) {
+ // The list of protocol+ssl+port configurations should match
+ // getIncomingTryOrder() in guessConfig.js.
+
+ // port == UNKNOWN
+ // [POP, STARTTLS, 110], [POP, SSL, 995], [POP, NONE, 110]
+ // port != UNKNOWN
+ // ssl == UNKNOWN
+ // [POP, STARTTLS, port], [POP, SSL, port], [POP, NONE, port]
+ // ssl != UNKNOWN
+ // [POP, ssl, port]
+ let ssl = UNKNOWN;
+ let port = UNKNOWN;
+ let tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [POP, STARTTLS, 110],
+ [POP, SSL, 995],
+ [POP, NONE, 110],
+ ]);
+
+ ssl = STARTTLS;
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[POP, ssl, 110]]);
+
+ ssl = SSL;
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[POP, ssl, 995]]);
+
+ ssl = NONE;
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[POP, ssl, 110]]);
+
+ ssl = UNKNOWN;
+ port = 31337;
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [POP, STARTTLS, port],
+ [POP, SSL, port],
+ [POP, NONE, port],
+ ]);
+
+ for (ssl in [STARTTLS, SSL, NONE]) {
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[POP, ssl, port]]);
+ }
+}
+
+/**
+ * Check that the IMAP calculations are correct for a given host and
+ * protocol.
+ */
+function checkImap(host, protocol) {
+ // The list of protocol+ssl+port configurations should match
+ // getIncomingTryOrder() in guessConfig.js.
+
+ // port == UNKNOWN
+ // [IMAP, STARTTLS, 143], [IMAP, SSL, 993], [IMAP, NONE, 143]
+ // port != UNKNOWN
+ // ssl == UNKNOWN
+ // [IMAP, STARTTLS, port], [IMAP, SSL, port], [IMAP, NONE, port]
+ // ssl != UNKNOWN
+ // [IMAP, ssl, port];
+
+ let ssl = UNKNOWN;
+ let port = UNKNOWN;
+ let tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [IMAP, STARTTLS, 143],
+ [IMAP, SSL, 993],
+ [IMAP, NONE, 143],
+ ]);
+
+ ssl = STARTTLS;
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[IMAP, ssl, 143]]);
+
+ ssl = SSL;
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[IMAP, ssl, 993]]);
+
+ ssl = NONE;
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[IMAP, ssl, 143]]);
+
+ ssl = UNKNOWN;
+ port = 31337;
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [IMAP, STARTTLS, port],
+ [IMAP, SSL, port],
+ [IMAP, NONE, port],
+ ]);
+
+ for (ssl in [STARTTLS, SSL, NONE]) {
+ tryOrder = getIncomingTryOrder(host, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[IMAP, ssl, port]]);
+ }
+}
+
+/*
+ * TESTS
+ */
+
+/**
+ * Test that getHostEntry returns the correct port numbers.
+ *
+ * TODO:
+ * - Test the returned commands as well.
+ */
+function test_getHostEntry() {
+ // IMAP port numbers.
+ assert_equal_host_entries(getHostEntry(IMAP, STARTTLS, UNKNOWN), [
+ IMAP,
+ STARTTLS,
+ 143,
+ ]);
+ assert_equal_host_entries(getHostEntry(IMAP, SSL, UNKNOWN), [IMAP, SSL, 993]);
+ assert_equal_host_entries(getHostEntry(IMAP, NONE, UNKNOWN), [
+ IMAP,
+ NONE,
+ 143,
+ ]);
+
+ // POP port numbers.
+ assert_equal_host_entries(getHostEntry(POP, STARTTLS, UNKNOWN), [
+ POP,
+ STARTTLS,
+ 110,
+ ]);
+ assert_equal_host_entries(getHostEntry(POP, SSL, UNKNOWN), [POP, SSL, 995]);
+ assert_equal_host_entries(getHostEntry(POP, NONE, UNKNOWN), [POP, NONE, 110]);
+
+ // SMTP port numbers.
+ assert_equal_host_entries(getHostEntry(SMTP, STARTTLS, UNKNOWN), [
+ SMTP,
+ STARTTLS,
+ 587,
+ ]);
+ assert_equal_host_entries(getHostEntry(SMTP, SSL, UNKNOWN), [SMTP, SSL, 465]);
+ assert_equal_host_entries(getHostEntry(SMTP, NONE, UNKNOWN), [
+ SMTP,
+ NONE,
+ 587,
+ ]);
+}
+
+/**
+ * Test the getIncomingTryOrder method.
+ */
+function test_getIncomingTryOrder() {
+ // The list of protocol+ssl+port configurations should match
+ // getIncomingTryOrder() in guessConfig.js.
+
+ // protocol == POP || host starts with pop. || host starts with pop3.
+ checkPop("example.com", POP);
+ checkPop("pop.example.com", UNKNOWN);
+ checkPop("pop3.example.com", UNKNOWN);
+ checkPop("imap.example.com", POP);
+
+ // protocol == IMAP || host starts with imap.
+ checkImap("example.com", IMAP);
+ checkImap("imap.example.com", UNKNOWN);
+ checkImap("pop.example.com", IMAP);
+
+ let domain = "example.com";
+ let protocol = UNKNOWN;
+ let ssl = UNKNOWN;
+ let port = UNKNOWN;
+ let tryOrder = getIncomingTryOrder(domain, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [IMAP, STARTTLS, 143],
+ [IMAP, SSL, 993],
+ [POP, STARTTLS, 110],
+ [POP, SSL, 995],
+ [IMAP, NONE, 143],
+ [POP, NONE, 110],
+ ]);
+
+ ssl = SSL;
+ tryOrder = getIncomingTryOrder(domain, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [IMAP, SSL, 993],
+ [POP, SSL, 995],
+ ]);
+
+ ssl = UNKNOWN;
+ port = 31337;
+ tryOrder = getIncomingTryOrder(domain, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [IMAP, STARTTLS, port],
+ [IMAP, SSL, port],
+ [POP, STARTTLS, port],
+ [POP, SSL, port],
+ [IMAP, NONE, port],
+ [POP, NONE, port],
+ ]);
+
+ ssl = SSL;
+ tryOrder = getIncomingTryOrder(domain, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [IMAP, SSL, port],
+ [POP, SSL, port],
+ ]);
+}
+
+/**
+ * Test the getOutgoingTryOrder method.
+ */
+function test_getOutgoingTryOrder() {
+ // The list of protocol+ssl+port configurations should match
+ // getOutgoingTryOrder() in guessConfig.js.
+ let domain = "example.com";
+ let protocol = SMTP;
+ let ssl = UNKNOWN;
+ let port = UNKNOWN;
+ let tryOrder = getOutgoingTryOrder(domain, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [SMTP, STARTTLS, 587],
+ [SMTP, STARTTLS, 25],
+ [SMTP, SSL, 465],
+ [SMTP, NONE, 587],
+ [SMTP, NONE, 25],
+ ]);
+
+ ssl = SSL;
+ tryOrder = getOutgoingTryOrder(domain, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[SMTP, SSL, 465]]);
+
+ ssl = UNKNOWN;
+ port = 31337;
+ tryOrder = getOutgoingTryOrder(domain, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [
+ [SMTP, STARTTLS, port],
+ [SMTP, SSL, port],
+ [SMTP, NONE, port],
+ ]);
+
+ ssl = SSL;
+ tryOrder = getOutgoingTryOrder(domain, protocol, ssl, port);
+ assert_equal_try_orders(tryOrder, [[SMTP, SSL, port]]);
+}
+
+function run_test() {
+ test_getHostEntry();
+ test_getIncomingTryOrder();
+ test_getOutgoingTryOrder();
+}
diff --git a/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigXML.js b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigXML.js
new file mode 100644
index 0000000000..919b0ffc2f
--- /dev/null
+++ b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigXML.js
@@ -0,0 +1,266 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests accountcreation/readFromXML.js , reading the XML files
+ * containing a mail configuration.
+ *
+ * To allow forwards-compatibility (add new stuff in the future without
+ * breaking old clients on the new files), we are now fairly tolerant when
+ * reading and allow fallback mechanisms. This test checks whether that works,
+ * and of course also whether we can read a normal config and get the proper
+ * values.
+ */
+
+// Globals
+
+var { AccountConfig } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountConfig.jsm"
+);
+var { readFromXML } = ChromeUtils.import(
+ "resource:///modules/accountcreation/readFromXML.jsm"
+);
+
+var { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm");
+
+/*
+ * UTILITIES
+ */
+
+function assert_equal(aA, aB, aWhy) {
+ if (aA != aB) {
+ do_throw(aWhy);
+ }
+ Assert.equal(aA, aB);
+}
+
+/**
+ * Test that two config entries are the same.
+ */
+function assert_equal_config(aA, aB, field) {
+ assert_equal(aA, aB, "Configured " + field + " is incorrect.");
+}
+
+/*
+ * TESTS
+ */
+
+/**
+ * Test that the xml reader returns a proper config and
+ * is also forwards-compatible to new additions to the data format.
+ */
+function test_readFromXML_config1() {
+ var clientConfigXML =
+ "<clientConfig>" +
+ '<emailProvider id="example.com">' +
+ "<domain>example.com</domain>" +
+ "<domain>example.net</domain>" +
+ "<displayName>Example</displayName>" +
+ "<displayShortName>Example Mail</displayShortName>" +
+ // 1. - protocol not supported
+ '<incomingServer type="imap5">' +
+ "<hostname>badprotocol.example.com</hostname>" +
+ "<port>993</port>" +
+ "<socketType>SSL</socketType>" +
+ "<username>%EMAILLOCALPART%</username>" +
+ "<authentication>ssl-client-cert</authentication>" +
+ "</incomingServer>" +
+ // 2. - socket type not supported
+ '<incomingServer type="imap">' +
+ "<hostname>badsocket.example.com</hostname>" +
+ "<port>993</port>" +
+ "<socketType>key-from-DNSSEC</socketType>" +
+ "<username>%EMAILLOCALPART%</username>" +
+ "<authentication>password-cleartext</authentication>" +
+ "</incomingServer>" +
+ // 3. - first supported incoming server
+ '<incomingServer type="imap">' +
+ "<hostname>imapmail.example.com</hostname>" +
+ "<port>993</port>" +
+ "<socketType>SSL</socketType>" +
+ "<username>%EMAILLOCALPART%</username>" +
+ "<authentication>password-cleartext</authentication>" +
+ "</incomingServer>" +
+ // 4. - auth method not supported
+ '<incomingServer type="imap">' +
+ "<hostname>badauth.example.com</hostname>" +
+ "<port>993</port>" +
+ "<socketType>SSL</socketType>" +
+ "<username>%EMAILLOCALPART%</username>" +
+ "<authentication>ssl-client-cert</authentication>" +
+ // Throw in some elements we don"t support yet
+ "<imap>" +
+ '<rootFolder path="INBOX."/>' +
+ '<specialFolder id="sent" path="INBOX.Sent Mail"/>' +
+ "</imap>" +
+ "</incomingServer>" +
+ // 5. - second supported incoming server
+ '<incomingServer type="pop3">' +
+ "<hostname>popmail.example.com</hostname>" +
+ // alternative hostname, not yet supported, should be ignored
+ "<hostname>popbackup.example.com</hostname>" +
+ "<port>110</port>" +
+ "<port>7878</port>" +
+ // unsupported socket type
+ "<socketType>GSSAPI2</socketType>" +
+ // but fall back
+ "<socketType>plain</socketType>" +
+ "<username>%EMAILLOCALPART%</username>" +
+ "<username>%EMAILADDRESS%</username>" +
+ // unsupported auth method
+ "<authentication>GSSAPI2</authentication>" +
+ // but fall back
+ "<authentication>password-encrypted</authentication>" +
+ "<pop3>" +
+ "<leaveMessagesOnServer>true</leaveMessagesOnServer>" +
+ "<daysToLeaveMessagesOnServer>999</daysToLeaveMessagesOnServer>" +
+ "</pop3>" +
+ "</incomingServer>" +
+ // outgoing server with invalid auth method
+ '<outgoingServer type="smtp">' +
+ "<hostname>badauth.example.com</hostname>" +
+ "<port>587</port>" +
+ "<socketType>STARTTLS</socketType>" +
+ "<username>%EMAILADDRESS%</username>" +
+ "<authentication>smtp-after-imap</authentication>" +
+ "</outgoingServer>" +
+ // outgoing server - supported
+ '<outgoingServer type="smtp">' +
+ "<hostname>smtpout.example.com</hostname>" +
+ "<hostname>smtpfallback.example.com</hostname>" +
+ "<port>587</port>" +
+ "<port>7878</port>" +
+ "<socketType>GSSAPI2</socketType>" +
+ "<socketType>STARTTLS</socketType>" +
+ "<username>%EMAILADDRESS%</username>" +
+ "<username>%EMAILLOCALPART%</username>" +
+ "<authentication>GSSAPI2</authentication>" +
+ "<authentication>client-IP-address</authentication>" +
+ "<smtp/>" +
+ "</outgoingServer>" +
+ // Throw in some more elements we don"t support yet
+ '<enableURL url="http://foobar"/>' +
+ '<instructionsURL url="http://foobar"/>' +
+ "</emailProvider>" +
+ "</clientConfig>";
+
+ var domParser = new DOMParser();
+ var config = readFromXML(
+ JXON.build(domParser.parseFromString(clientConfigXML, "text/xml"))
+ );
+
+ Assert.equal(config instanceof AccountConfig, true);
+ Assert.equal("example.com", config.id);
+ Assert.equal("Example", config.displayName);
+ Assert.notEqual(-1, config.domains.indexOf("example.com"));
+ // 1. incoming server skipped because of an unsupported protocol
+ // 2. incoming server skipped because of an so-far unknown auth method
+ // 3. incoming server is fine for us: IMAP, SSL, cleartext password
+ let server = config.incoming;
+ Assert.equal("imapmail.example.com", server.hostname);
+ Assert.equal("imap", server.type);
+ Assert.equal(Ci.nsMsgSocketType.SSL, server.socketType);
+ Assert.equal(3, server.auth); // cleartext password
+ // only one more supported incoming server
+ Assert.equal(1, config.incomingAlternatives.length);
+ // 4. incoming server skipped because of an so-far unknown socketType
+ // 5. server: POP
+ server = config.incomingAlternatives[0];
+ Assert.equal("popmail.example.com", server.hostname);
+ Assert.equal("pop3", server.type);
+ Assert.equal(Ci.nsMsgSocketType.plain, server.socketType);
+ Assert.equal(4, server.auth); // encrypted password
+
+ // SMTP server, most preferred
+ server = config.outgoing;
+ Assert.equal("smtpout.example.com", server.hostname);
+ Assert.equal("smtp", server.type);
+ Assert.equal(Ci.nsMsgSocketType.alwaysSTARTTLS, server.socketType);
+ Assert.equal(1, server.auth); // no auth
+ // no other SMTP servers
+ Assert.equal(0, config.outgoingAlternatives.length);
+}
+
+/**
+ * Test the replaceVariables method.
+ */
+function test_replaceVariables() {
+ var clientConfigXML =
+ "<clientConfig>" +
+ '<emailProvider id="example.com">' +
+ "<domain>example.com</domain>" +
+ "<displayName>example.com</displayName>" +
+ "<displayShortName>example.com</displayShortName>" +
+ '<incomingServer type="pop3">' +
+ "<hostname>pop.%EMAILDOMAIN%</hostname>" +
+ "<port>995</port>" +
+ "<socketType>SSL</socketType>" +
+ "<username>%EMAILLOCALPART%</username>" +
+ "<authentication>plain</authentication>" +
+ "<pop3>" +
+ "<leaveMessagesOnServer>true</leaveMessagesOnServer>" +
+ "<daysToLeaveMessagesOnServer>999</daysToLeaveMessagesOnServer>" +
+ "</pop3>" +
+ "</incomingServer>" +
+ '<outgoingServer type="smtp">' +
+ "<hostname>smtp.example.com</hostname>" +
+ "<port>587</port>" +
+ "<socketType>STARTTLS</socketType>" +
+ "<username>%EMAILADDRESS%</username>" +
+ "<authentication>plain</authentication>" +
+ "<addThisServer>true</addThisServer>" +
+ "<useGlobalPreferredServer>false</useGlobalPreferredServer>" +
+ "</outgoingServer>" +
+ "</emailProvider>" +
+ "</clientConfig>";
+
+ var domParser = new DOMParser();
+ var config = readFromXML(
+ JXON.build(domParser.parseFromString(clientConfigXML, "text/xml"))
+ );
+
+ AccountConfig.replaceVariables(
+ config,
+ "Yamato Nadeshiko",
+ "yamato.nadeshiko@example.com",
+ "abc12345"
+ );
+
+ assert_equal_config(
+ config.incoming.username,
+ "yamato.nadeshiko",
+ "incoming server username"
+ );
+ assert_equal_config(
+ config.outgoing.username,
+ "yamato.nadeshiko@example.com",
+ "outgoing server username"
+ );
+ assert_equal_config(
+ config.incoming.hostname,
+ "pop.example.com",
+ "incoming server hostname"
+ );
+ assert_equal_config(
+ config.outgoing.hostname,
+ "smtp.example.com",
+ "outgoing server hostname"
+ );
+ assert_equal_config(
+ config.identity.realname,
+ "Yamato Nadeshiko",
+ "user real name"
+ );
+ assert_equal_config(
+ config.identity.emailAddress,
+ "yamato.nadeshiko@example.com",
+ "user email address"
+ );
+}
+
+function run_test() {
+ test_readFromXML_config1();
+ test_replaceVariables();
+}
diff --git a/comm/mail/components/accountcreation/test/xpcshell/xpcshell.ini b/comm/mail/components/accountcreation/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..8d42ab2145
--- /dev/null
+++ b/comm/mail/components/accountcreation/test/xpcshell/xpcshell.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+head =
+tail =
+support-files = data/*
+
+[test_autoconfigFetchDisk.js]
+skip-if = os == 'win' && msix # MSIX cannot write to application directory
+[test_autoconfigUtils.js]
+[test_autoconfigXML.js]
diff --git a/comm/mail/components/accountcreation/views/container.mjs b/comm/mail/components/accountcreation/views/container.mjs
new file mode 100644
index 0000000000..3bf8d9b4dc
--- /dev/null
+++ b/comm/mail/components/accountcreation/views/container.mjs
@@ -0,0 +1,50 @@
+/* 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/. */
+
+/**
+ * Custom Element containing the main account hub dialog. Used to append the
+ * needed CSS files to the shadowDom to prevent style leakage.
+ * NOTE: This could directly extend an HTMLDialogElement if it had a shadowRoot.
+ */
+class AccountHubContainer extends HTMLElement {
+ /** @type {HTMLDialogElement} */
+ modal;
+
+ /** @type {DOMLocalization} */
+ l10n;
+
+ connectedCallback() {
+ if (this.shadowRoot) {
+ // Already connected, no need to run it again.
+ return;
+ }
+
+ const shadowRoot = this.attachShadow({ mode: "open" });
+
+ // Load styles in the shadowRoot so we don't leak it.
+ let style = document.createElement("link");
+ style.rel = "stylesheet";
+ style.href = "chrome://messenger/skin/accountHub.css";
+ shadowRoot.appendChild(style);
+
+ let template = document.getElementById("accountHubDialog");
+ let clonedNode = template.content.cloneNode(true);
+ shadowRoot.appendChild(clonedNode);
+ this.modal = shadowRoot.querySelector("dialog");
+
+ // We need to create an internal DOM localization in order to let fluent
+ // see the IDs inside our shadowRoot.
+ this.l10n = new DOMLocalization([
+ "branding/brand.ftl",
+ "messenger/accountcreation/accountHub.ftl",
+ "messenger/accountcreation/accountSetup.ftl",
+ ]);
+ this.l10n.connectRoot(shadowRoot);
+ }
+
+ disconnectedCallback() {
+ this.l10n.disconnectRoot(this.shadowRoot);
+ }
+}
+customElements.define("account-hub-container", AccountHubContainer);
diff --git a/comm/mail/components/accountcreation/views/email.mjs b/comm/mail/components/accountcreation/views/email.mjs
new file mode 100644
index 0000000000..f5a4d628b7
--- /dev/null
+++ b/comm/mail/components/accountcreation/views/email.mjs
@@ -0,0 +1,185 @@
+/* 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/. */
+
+class AccountHubEmail extends HTMLElement {
+ /**
+ * The email setup form.
+ *
+ * @type {HTMLFormElement}
+ */
+ #form;
+
+ /**
+ * The account name field.
+ *
+ * @type {HTMLInputElement}
+ */
+ #realName;
+
+ /**
+ * The email field.
+ *
+ * @type {HTMLInputElement}
+ */
+ #email;
+
+ /**
+ * The password field.
+ *
+ * @type {HTMLInputElement}
+ */
+ #password;
+
+ /**
+ * The password visibility button.
+ *
+ * @type {HTMLButtonElement}
+ */
+ #passwordToggleButton;
+
+ /**
+ * The submit form button.
+ *
+ * @type {HTMLButtonElement}
+ */
+ #continueButton;
+
+ /**
+ * The domain name extrapolated from the email address.
+ *
+ * @type {string}
+ */
+ #domain = "";
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ this.classList.add("account-hub-view");
+
+ let template = document.getElementById("accountHubEmailSetup");
+ this.appendChild(template.content.cloneNode(true));
+
+ this.#form = this.querySelector("form");
+ this.#realName = this.querySelector("#realName");
+ this.#email = this.querySelector("#email");
+ this.#password = this.querySelector("#password");
+ this.#passwordToggleButton = this.querySelector("#passwordToggleButton");
+ this.#continueButton = this.querySelector("#emailContinueButton");
+
+ this.initUI();
+
+ this.setupEventListeners();
+ }
+
+ /**
+ * Initialize the UI of the email setup flow.
+ */
+ initUI() {
+ // Populate the account name if we can get some user info.
+ if ("@mozilla.org/userinfo;1" in Cc) {
+ let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo);
+ this.#realName.value = userInfo.fullname;
+ }
+
+ this.#realName.focus();
+ }
+
+ /**
+ * Set up the event listeners for this workflow only once.
+ */
+ setupEventListeners() {
+ this.#form.addEventListener("submit", event => {
+ event.preventDefault();
+ event.stopPropagation();
+ console.log("submit");
+ });
+
+ this.#realName.addEventListener("input", () => this.#checkValidForm());
+ this.#email.addEventListener("input", () => this.#checkValidForm());
+ this.#password.addEventListener("input", () => this.#onPasswordInput());
+
+ this.#passwordToggleButton.addEventListener("click", event => {
+ this.#togglePasswordInput(
+ event.target.getAttribute("aria-pressed") === "false"
+ );
+ });
+
+ // Set the Cancel/Back button.
+ this.querySelector("#emailGoBackButton").addEventListener("click", () => {
+ // If in first view, go back to start, otherwise go back in the flow.
+ this.dispatchEvent(
+ new CustomEvent("open-view", {
+ bubbles: true,
+ composed: true,
+ detail: { type: "START" },
+ })
+ );
+ });
+ }
+
+ /**
+ * Check whether the user entered the minimum amount of information needed to
+ * leave the first view and is allowed to proceed to the detection step.
+ */
+ #checkValidForm() {
+ const isValidForm =
+ this.#email.checkValidity() && this.#realName.checkValidity();
+ this.#domain = isValidForm
+ ? this.#email.value.split("@")[1].toLowerCase()
+ : "";
+
+ this.#continueButton.disabled = !isValidForm;
+ }
+
+ /**
+ * Handle the password visibility toggle on password input.
+ */
+ #onPasswordInput() {
+ if (!this.#password.value) {
+ this.#togglePasswordInput(false);
+ }
+ }
+
+ /**
+ * Toggle the password field type between `password` and `text` to allow users
+ * reading their typed password.
+ *
+ * @param {boolean} show - If the password field should become a text field.
+ */
+ #togglePasswordInput(show) {
+ this.#password.type = show ? "text" : "password";
+ this.#passwordToggleButton.setAttribute("aria-pressed", show.toString());
+ document.l10n.setAttributes(
+ this.#passwordToggleButton,
+ show
+ ? "account-setup-password-toggle-hide"
+ : "account-setup-password-toggle-show"
+ );
+ }
+
+ /**
+ * Check if any operation is currently in process and return true only if we
+ * can leave this view.
+ *
+ * @returns {boolean} - If the account hub can remove this view.
+ */
+ reset() {
+ // TODO
+ // Check for:
+ // - Non-abortable operations (autoconfig, email account setup, etc)
+
+ this.#form.reset();
+ this.#togglePasswordInput(false);
+ // TODO
+ // Before resetting we need to:
+ // - Clean up the fields.
+ // - Reset the autoconfig (cached server info).
+ // - Reset the view to the initial screen.
+ return true;
+ }
+}
+customElements.define("account-hub-email", AccountHubEmail);
diff --git a/comm/mail/components/accountcreation/views/start.mjs b/comm/mail/components/accountcreation/views/start.mjs
new file mode 100644
index 0000000000..66487cc928
--- /dev/null
+++ b/comm/mail/components/accountcreation/views/start.mjs
@@ -0,0 +1,163 @@
+/* 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 gSync */
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { UIState } = ChromeUtils.importESModule(
+ "resource://services-sync/UIState.sys.mjs"
+);
+
+class AccountHubStart extends HTMLElement {
+ #accounts = [
+ {
+ id: "email",
+ l10n: "account-hub-email-setup-button",
+ type: "MAIL",
+ },
+ {
+ id: "calendar",
+ l10n: "account-hub-calendar-setup-button",
+ type: "CALENDAR",
+ },
+ {
+ id: "addressBook",
+ l10n: "account-hub-address-book-setup-button",
+ type: "ADDRESS_BOOK",
+ },
+ {
+ id: "chat",
+ l10n: "account-hub-chat-setup-button",
+ type: "CHAT",
+ },
+ {
+ id: "feed",
+ l10n: "account-hub-feed-setup-button",
+ type: "FEED",
+ },
+ {
+ id: "newsgroup",
+ l10n: "account-hub-newsgroup-setup-button",
+ type: "NNTP",
+ },
+ // TODO: Import/Export of profiles is kinda broken so we don't want to
+ // expose it so much for now.
+ // {
+ // id: "import",
+ // l10n: "account-hub-import-setup-button",
+ // type: "IMPORT",
+ // },
+ ];
+
+ connectedCallback() {
+ if (this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ this.classList.add("account-hub-view");
+
+ let template = document.getElementById("accountHubStart");
+ this.appendChild(template.content.cloneNode(true));
+
+ this.initUI();
+
+ this.setupAccountFlows();
+ }
+
+ /**
+ * Update the UI to reflect reality whenever this view is triggered.
+ */
+ initUI() {
+ const hasAccounts = MailServices.accounts.accounts.length;
+ this.querySelector("#welcomeHeader").hidden = hasAccounts;
+ this.querySelector("#defaultHeader").hidden = !hasAccounts;
+
+ if (AppConstants.NIGHTLY_BUILD) {
+ this.updateFxAButton();
+ }
+
+ // Hide the release notes link for nightly builds since we don't have any.
+ if (AppConstants.NIGHTLY_BUILD) {
+ this.querySelector("#hubReleaseNotes").closest("li").hidden = true;
+ return;
+ }
+
+ if (
+ Services.prefs.getPrefType("app.releaseNotesURL") !=
+ Services.prefs.PREF_INVALID
+ ) {
+ let relNotesURL = Services.urlFormatter.formatURLPref(
+ "app.releaseNotesURL"
+ );
+ if (relNotesURL != "about:blank") {
+ this.querySelector("#hubReleaseNotes").href = relNotesURL;
+ return;
+ }
+ // Hide the release notes link if we don't have a URL to add.
+ this.querySelector("#hubReleaseNotes").closest("li").hidden = true;
+ }
+ }
+
+ /**
+ * Populate the main container fo the start view with all the available
+ * account creation flows.
+ */
+ setupAccountFlows() {
+ const fragment = new DocumentFragment();
+ for (const account of this.#accounts) {
+ const button = document.createElement("button");
+ button.id = `${account.id}Button`;
+ button.classList.add("button", "button-account");
+ document.l10n.setAttributes(button, account.l10n);
+ button.addEventListener("click", () => {
+ this.dispatchEvent(
+ new CustomEvent("open-view", {
+ bubbles: true,
+ composed: true,
+ detail: {
+ type: account.type,
+ },
+ })
+ );
+ });
+ fragment.append(button);
+ }
+ this.querySelector(".hub-body-grid").replaceChildren(fragment);
+
+ if (AppConstants.NIGHTLY_BUILD) {
+ this.querySelector("#hubSyncButton").addEventListener("click", () => {
+ // FIXME: Open this in a dialog or browser inside the modal, or find a
+ // way to close the account hub without an account and open it again in
+ // case the FxA login fails to set up accounts.
+ gSync.initFxA();
+ });
+ }
+ }
+
+ /**
+ * Set up the Firefox Sync button.
+ */
+ updateFxAButton() {
+ const state = UIState.get();
+ this.querySelector("#hubSyncButton").hidden =
+ state.status == UIState.STATUS_SIGNED_IN;
+ }
+
+ /**
+ * The start view doesn't have any abortable operation that needs to be
+ * checked, so we always return true.
+ *
+ * @returns {boolean} - Always true.
+ */
+ reset() {
+ return true;
+ }
+}
+customElements.define("account-hub-start", AccountHubStart);