summaryrefslogtreecommitdiffstats
path: root/mailnews/base/prefs/content/accountcreation
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/base/prefs/content/accountcreation')
-rw-r--r--mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js41
-rw-r--r--mailnews/base/prefs/content/accountcreation/accountConfig.js259
-rw-r--r--mailnews/base/prefs/content/accountcreation/createInBackend.js333
-rw-r--r--mailnews/base/prefs/content/accountcreation/emailWizard.js1959
-rw-r--r--mailnews/base/prefs/content/accountcreation/emailWizard.xul493
-rw-r--r--mailnews/base/prefs/content/accountcreation/fetchConfig.js240
-rw-r--r--mailnews/base/prefs/content/accountcreation/fetchhttp.js267
-rw-r--r--mailnews/base/prefs/content/accountcreation/guessConfig.js1145
-rw-r--r--mailnews/base/prefs/content/accountcreation/readFromXML.js238
-rw-r--r--mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js207
-rw-r--r--mailnews/base/prefs/content/accountcreation/util.js304
-rw-r--r--mailnews/base/prefs/content/accountcreation/verifyConfig.js347
12 files changed, 5833 insertions, 0 deletions
diff --git a/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js b/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js
new file mode 100644
index 000000000..9f4c5034e
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js
@@ -0,0 +1,41 @@
+/* -*- 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/. */
+
+/**
+ * This class implements nsIBadCertListener. It's job is to prevent "bad cert"
+ * security dialogs from being shown to the user. We call back to the
+ * 'callback' object's method "processCertError" so that it can deal with it as
+ * needed (in the case of autoconfig, setting up temporary overrides).
+ */
+function BadCertHandler(callback)
+{
+ this._init(callback);
+}
+
+BadCertHandler.prototype =
+{
+ _init: function(callback) {
+ this._callback = callback;
+ },
+
+ // Suppress any certificate errors
+ notifyCertProblem: function(socketInfo, status, targetSite) {
+ return this._callback.processCertError(socketInfo, status, targetSite);
+ },
+
+ // nsIInterfaceRequestor
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ // nsISupports
+ QueryInterface: function(iid) {
+ if (!iid.equals(Components.interfaces.nsIBadCertListener2) &&
+ !iid.equals(Components.interfaces.nsIInterfaceRequestor) &&
+ !iid.equals(Components.interfaces.nsISupports))
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+};
diff --git a/mailnews/base/prefs/content/accountcreation/accountConfig.js b/mailnews/base/prefs/content/accountcreation/accountConfig.js
new file mode 100644
index 000000000..3a757d8ee
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/accountConfig.js
@@ -0,0 +1,259 @@
+/* -*- Mode: Java; 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/. */
+
+/**
+ * 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.
+ */
+
+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,
+ // OAuth2 configuration, if needed.
+ oauthSettings : null,
+ // just an internal string to refer to this. Do not show to user.
+ id : null,
+ // who created the config.
+ // { one of kSource* }
+ source : 0,
+ 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 : function()
+ {
+ return {
+ // { String-enum: "pop3", "imap", "nntp" }
+ type : null,
+ hostname : null,
+ // { Integer }
+ port : null,
+ // May be a placeholder (starts and ends with %). { String }
+ username : null,
+ password : null,
+ // { enum: 1 = plain, 2 = SSL/TLS, 3 = STARTTLS always, 0 = not inited }
+ // ('TLS when available' is insecure and not supported here)
+ socketType : 0,
+ /**
+ * 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,
+ };
+ },
+ /**
+ * Factory function for outgoing and outgoingAlternatives
+ */
+ createNewOutgoing : function()
+ {
+ 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 : 0, // 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,
+ };
+ },
+
+ /**
+ * Returns a deep copy of this object,
+ * i.e. modifying the copy will not affect the original object.
+ */
+ copy : function()
+ {
+ // Workaround: deepCopy() fails to preserve base obj (instanceof)
+ var result = new AccountConfig();
+ for (var prop in this)
+ result[prop] = deepCopy(this[prop]);
+
+ return result;
+ },
+ isComplete : function()
+ {
+ return (!!this.incoming.hostname && !!this.incoming.port &&
+ !!this.incoming.socketType && !!this.incoming.auth &&
+ !!this.incoming.username &&
+ (!!this.outgoing.existingServerKey ||
+ (!!this.outgoing.hostname && !!this.outgoing.port &&
+ !!this.outgoing.socketType && !!this.outgoing.auth &&
+ !!this.outgoing.username)));
+ },
+};
+
+
+// enum consts
+
+// .source
+AccountConfig.kSourceUser = 1; // user manually entered the config
+AccountConfig.kSourceXML = 2; // config from XML from ISP or Mozilla DB
+AccountConfig.kSourceGuess = 3; // guessConfig()
+
+
+/**
+ * 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
+ */
+function replaceVariables(account, realname, emailfull, password)
+{
+ sanitize.nonemptystring(emailfull);
+ let emailsplit = emailfull.split("@");
+ assert(emailsplit.length == 2,
+ "email address not in expected format: must contain exactly one @");
+ let emaillocal = sanitize.nonemptystring(emailsplit[0]);
+ let emaildomain = sanitize.hostname(emailsplit[1]);
+ sanitize.label(realname);
+ sanitize.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/mailnews/base/prefs/content/accountcreation/createInBackend.js b/mailnews/base/prefs/content/accountcreation/createInBackend.js
new file mode 100644
index 000000000..d959c3ae9
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/createInBackend.js
@@ -0,0 +1,333 @@
+/* -*- Mode: Java; 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/. */
+
+/**
+ * Takes an |AccountConfig| JS object and creates that account in the
+ * Thunderbird backend (which also writes it to prefs).
+ *
+ * @param config {AccountConfig} The account to create
+ *
+ * @return - the account created.
+ */
+
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function createAccountInBackend(config)
+{
+ // incoming server
+ let inServer = MailServices.accounts.createIncomingServer(
+ config.incoming.username,
+ config.incoming.hostname,
+ sanitize.enum(config.incoming.type, ["pop3", "imap", "nntp"]));
+ inServer.port = config.incoming.port;
+ inServer.authMethod = config.incoming.auth;
+ inServer.password = config.incoming.password;
+ if (config.rememberPassword && config.incoming.password.length)
+ rememberPassword(inServer, config.incoming.password);
+
+ if (inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
+ inServer.setCharValue("oauth2.scope", config.oauthSettings.scope);
+ inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer);
+ }
+
+ // SSL
+ if (config.incoming.socketType == 1) // plain
+ inServer.socketType = Ci.nsMsgSocketType.plain;
+ else if (config.incoming.socketType == 2) // SSL / TLS
+ inServer.socketType = Ci.nsMsgSocketType.SSL;
+ else if (config.incoming.socketType == 3) // STARTTLS
+ inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+ //inServer.prettyName = config.displayName;
+ inServer.prettyName = config.identity.emailAddress;
+
+ inServer.doBiff = true;
+ inServer.biffMinutes = config.incoming.checkInterval;
+ const loginAtStartupPrefTemplate =
+ "mail.server.%serverkey%.login_at_startup";
+ var loginAtStartupPref =
+ loginAtStartupPrefTemplate.replace("%serverkey%", inServer.key);
+ Services.prefs.setBoolPref(loginAtStartupPref,
+ config.incoming.loginAtStartup);
+ if (config.incoming.type == "pop3")
+ {
+ const leaveOnServerPrefTemplate =
+ "mail.server.%serverkey%.leave_on_server";
+ const daysToLeaveOnServerPrefTemplate =
+ "mail.server.%serverkey%.num_days_to_leave_on_server";
+ const deleteFromServerPrefTemplate =
+ "mail.server.%serverkey%.delete_mail_left_on_server";
+ const deleteByAgeFromServerPrefTemplate =
+ "mail.server.%serverkey%.delete_by_age_from_server";
+ const downloadOnBiffPrefTemplate =
+ "mail.server.%serverkey%.download_on_biff";
+ var leaveOnServerPref =
+ leaveOnServerPrefTemplate.replace("%serverkey%", inServer.key);
+ var ageFromServerPref =
+ deleteByAgeFromServerPrefTemplate.replace("%serverkey%", inServer.key);
+ var daysToLeaveOnServerPref =
+ daysToLeaveOnServerPrefTemplate.replace("%serverkey%", inServer.key);
+ var deleteFromServerPref =
+ deleteFromServerPrefTemplate.replace("%serverkey%", inServer.key);
+ let downloadOnBiffPref =
+ downloadOnBiffPrefTemplate.replace("%serverkey%", inServer.key);
+ Services.prefs.setBoolPref(leaveOnServerPref,
+ config.incoming.leaveMessagesOnServer);
+ Services.prefs.setIntPref(daysToLeaveOnServerPref,
+ config.incoming.daysToLeaveMessagesOnServer);
+ Services.prefs.setBoolPref(deleteFromServerPref,
+ config.incoming.deleteOnServerWhenLocalDelete);
+ Services.prefs.setBoolPref(ageFromServerPref,
+ config.incoming.deleteByAgeFromServer);
+ Services.prefs.setBoolPref(downloadOnBiffPref,
+ config.incoming.downloadOnBiff);
+ }
+ inServer.valid = true;
+
+ let username = config.outgoing.auth > 1 ? config.outgoing.username : null;
+ let outServer = MailServices.smtp.findServer(username, config.outgoing.hostname);
+ assert(config.outgoing.addThisServer ||
+ config.outgoing.useGlobalPreferredServer ||
+ config.outgoing.existingServerKey,
+ "No SMTP server: inconsistent flags");
+
+ if (config.outgoing.addThisServer && !outServer)
+ {
+ outServer = MailServices.smtp.createServer();
+ outServer.hostname = config.outgoing.hostname;
+ outServer.port = config.outgoing.port;
+ outServer.authMethod = config.outgoing.auth;
+ if (config.outgoing.auth > 1)
+ {
+ outServer.username = username;
+ outServer.password = config.incoming.password;
+ if (config.rememberPassword && config.incoming.password.length)
+ rememberPassword(outServer, config.incoming.password);
+ }
+
+ if (outServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) {
+ let pref = "mail.smtpserver." + outServer.key + ".";
+ Services.prefs.setCharPref(pref + "oauth2.scope",
+ config.oauthSettings.scope);
+ Services.prefs.setCharPref(pref + "oauth2.issuer",
+ config.oauthSettings.issuer);
+ }
+
+ if (config.outgoing.socketType == 1) // no SSL
+ outServer.socketType = Ci.nsMsgSocketType.plain;
+ else if (config.outgoing.socketType == 2) // SSL / TLS
+ outServer.socketType = Ci.nsMsgSocketType.SSL;
+ else if (config.outgoing.socketType == 3) // STARTTLS
+ outServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+
+ // API problem: <http://mxr.mozilla.org/seamonkey/source/mailnews/compose/public/nsISmtpServer.idl#93>
+ outServer.description = config.displayName;
+ if (config.password)
+ outServer.password = config.outgoing.password;
+
+ // 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.existingServerKey)
+ identity.smtpServerKey = config.outgoing.existingServerKey;
+ else if (!config.outgoing.useGlobalPreferredServer)
+ 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) {
+ ddump("Could not write out prefs: " + ex);
+ }
+ return account;
+}
+
+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)
+{
+ if (server instanceof Components.interfaces.nsIMsgIncomingServer)
+ var passwordURI = server.localStoreType + "://" + server.hostName;
+ else if (server instanceof Components.interfaces.nsISmtpServer)
+ var passwordURI = "smtp://" + server.hostname;
+ else
+ throw new 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)
+ * @return {nsIMsgIncomingServer} If it already exists, the server
+ * object is returned.
+ * If it's a new server, |null| is returned.
+ */
+function checkIncomingServerAlreadyExists(config)
+{
+ assert(config instanceof AccountConfig);
+ let incoming = config.incoming;
+ let existing = MailServices.accounts.findRealServer(incoming.username,
+ incoming.hostname,
+ sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]),
+ 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.findRealServer(config.identity.emailAddress,
+ incoming.hostname,
+ sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]),
+ 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)
+ * @return {nsISmtpServer} If it already exists, the server
+ * object is returned.
+ * If it's a new server, |null| is returned.
+ */
+function checkOutgoingServerAlreadyExists(config)
+{
+ assert(config instanceof AccountConfig);
+ let smtpServers = MailServices.smtp.servers;
+ while (smtpServers.hasMoreElements())
+ {
+ let existingServer = smtpServers.getNext()
+ .QueryInterface(Ci.nsISmtpServer);
+ // 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 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) {
+ ddump("Error! we should have found the local mail server " +
+ "after we created it.");
+ }
+ }
+ }
+ catch (ex) { ddump("Error in verifyLocalFoldersAccount " + ex); }
+}
diff --git a/mailnews/base/prefs/content/accountcreation/emailWizard.js b/mailnews/base/prefs/content/accountcreation/emailWizard.js
new file mode 100644
index 000000000..b4e6854da
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/emailWizard.js
@@ -0,0 +1,1959 @@
+/* -*- Mode: Java; 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/. */
+
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource:///modules/hostnameUtils.jsm");
+Components.utils.import("resource://gre/modules/OAuth2Providers.jsm");
+
+/**
+ * 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
+ */
+
+
+// from http://xyfer.blogspot.com/2005/01/javascript-regexp-email-validator.html
+var emailRE = /^[-_a-z0-9\'+*$^&%=~!?{}]+(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*@(?:[-a-z0-9.]+\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/i;
+
+if (typeof gEmailWizardLogger == "undefined") {
+ Cu.import("resource:///modules/gloda/log4moz.js");
+ var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+}
+
+var gStringsBundle;
+var gMessengerBundle;
+var gBrandShortName;
+
+/*********************
+TODO for bug 549045
+- autodetect protocol
+Polish
+- reformat code style to match
+<https://developer.mozilla.org/En/Mozilla_Coding_Style_Guide#Control_Structures>
+- bold status
+- remove status when user edited in manual edit
+- add and adapt test from bug 534588
+Bugs
+- SSL cert errors
+ - invalid cert (hostname mismatch) doesn't trigger warning dialog as it should
+ - accept self-signed cert (e.g. imap.mail.ru) doesn't work
+ (works without my patch),
+ verifyConfig.js line 124 has no inServer, for whatever reason,
+ although I didn't change verifyConfig.js at all
+ (the change you see in that file is irrelevant: that was an attempt to fix
+ the bug and clean up the code).
+- Set radio IMAP vs. POP3, see TODO in code
+Things to test (works for me):
+- state transitions, buttons enable, status msgs
+ - stop button
+ - showes up again after stopping detection and restarting it
+ - when stopping [retest]: buttons proper?
+ - enter nonsense domain. guess fails, (so automatically) manual,
+ change domain to real one (not in DB), guess succeeds.
+ former bug: goes to manual first shortly, then to result
+**********************/
+
+// To debug, set mail.wizard.logging.dump (or .console)="All" and kDebug = true
+
+function e(elementID)
+{
+ return document.getElementById(elementID);
+};
+
+function _hide(id)
+{
+ e(id).hidden = true;
+}
+
+function _show(id)
+{
+ e(id).hidden = false;
+}
+
+function _enable(id)
+{
+ e(id).disabled = false;
+}
+
+function _disable(id)
+{
+ e(id).disabled = true;
+}
+
+function setText(id, value)
+{
+ var element = e(id);
+ assert(element, "setText() on non-existant element ID");
+
+ if (element.localName == "textbox" || element.localName == "label") {
+ element.value = value;
+ } else if (element.localName == "description") {
+ element.textContent = value;
+ } else {
+ throw new NotReached("XUL element type not supported");
+ }
+}
+
+function setLabelFromStringBundle(elementID, stringName)
+{
+ e(elementID).label = gMessengerBundle.getString(stringName);
+};
+
+function EmailConfigWizard()
+{
+ this._init();
+}
+EmailConfigWizard.prototype =
+{
+ _init : function EmailConfigWizard__init()
+ {
+ gEmailWizardLogger.info("Initializing setup wizard");
+ this._abortable = null;
+ },
+
+ onLoad : function()
+ {
+ /**
+ * 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;
+
+ let userFullname;
+ try {
+ let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo);
+ userFullname = userInfo.fullname;
+ } catch(e) {
+ // nsIUserInfo may not be implemented on all platforms, and name might
+ // not be avaialble even if it is.
+ }
+
+ this._domain = "";
+ this._email = "";
+ this._realname = (userFullname) ? userFullname : "";
+ e("realname").value = this._realname;
+ this._password = "";
+ this._okCallback = null;
+
+ if (window.arguments && window.arguments[0]) {
+ if (window.arguments[0].msgWindow) {
+ this._parentMsgWindow = window.arguments[0].msgWindow;
+ }
+ if (window.arguments[0].okCallback) {
+ this._okCallback = window.arguments[0].okCallback;
+ }
+ }
+
+ gEmailWizardLogger.info("Email account setup dialog loaded.");
+
+ gStringsBundle = e("strings");
+ gMessengerBundle = e("bundle_messenger");
+ gBrandShortName = e("bundle_brand").getString("brandShortName");
+
+ setLabelFromStringBundle("in-authMethod-password-cleartext",
+ "authPasswordCleartextViaSSL"); // will warn about insecure later
+ setLabelFromStringBundle("in-authMethod-password-encrypted",
+ "authPasswordEncrypted");
+ setLabelFromStringBundle("in-authMethod-kerberos", "authKerberos");
+ setLabelFromStringBundle("in-authMethod-ntlm", "authNTLM");
+ setLabelFromStringBundle("in-authMethod-oauth2", "authOAuth2");
+ setLabelFromStringBundle("out-authMethod-no", "authNo");
+ setLabelFromStringBundle("out-authMethod-password-cleartext",
+ "authPasswordCleartextViaSSL"); // will warn about insecure later
+ setLabelFromStringBundle("out-authMethod-password-encrypted",
+ "authPasswordEncrypted");
+ setLabelFromStringBundle("out-authMethod-kerberos", "authKerberos");
+ setLabelFromStringBundle("out-authMethod-ntlm", "authNTLM");
+ setLabelFromStringBundle("out-authMethod-oauth2", "authOAuth2");
+
+ e("incoming_port").value = gStringsBundle.getString("port_auto");
+ this.fillPortDropdown("smtp");
+
+ // If the account provisioner is preffed off, don't display
+ // the account provisioner button.
+ if (!Services.prefs.getBoolPref("mail.provider.enabled"))
+ _hide("provisioner_button");
+
+ // Populate SMTP server dropdown with already configured SMTP servers from
+ // other accounts.
+ var menulist = e("outgoing_hostname");
+ let smtpServers = MailServices.smtp.servers;
+ while (smtpServers.hasMoreElements()) {
+ let server = smtpServers.getNext().QueryInterface(Ci.nsISmtpServer);
+ let label = server.displayname;
+ let key = server.key;
+ if (MailServices.smtp.defaultServer &&
+ MailServices.smtp.defaultServer.key == key) {
+ label += " " + gStringsBundle.getString("default_server_tag");
+ }
+ let menuitem = menulist.appendItem(label, key, ""); // label,value,descr
+ menuitem.serverKey = key;
+ }
+ // Add the entry for the new host to the menulist
+ let menuitem = menulist.insertItemAt(0, "", "-new-"); // pos,label,value
+ menuitem.serverKey = null;
+
+ // admin-locked prefs hurray
+ if (!Services.prefs.getBoolPref("signon.rememberSignons")) {
+ let rememberPasswordE = e("remember_password");
+ rememberPasswordE.checked = false;
+ rememberPasswordE.disabled = true;
+ }
+
+ // First, unhide the main window areas, and store the width,
+ // so that we don't resize wildly when we unhide areas.
+ // switchToMode() will then hide the unneeded parts again.
+ // We will add some leeway of 10px, in case some of the <description>s wrap,
+ // e.g. outgoing username != incoming username.
+ _show("status_area");
+ _show("result_area");
+ _hide("manual-edit_area");
+ window.sizeToContent();
+ e("mastervbox").setAttribute("style",
+ "min-width: " + document.width + "px; " +
+ "min-height: " + (document.height + 10) + "px;");
+
+ this.switchToMode("start");
+ e("realname").select();
+ },
+
+ /**
+ * Changes the window configuration to the different modes we have.
+ * Shows/hides various window parts and buttons.
+ * @param modename {String-enum}
+ * "start" : Just the realname, email address, password fields
+ * "find-config" : detection step, adds the progress message/spinner
+ * "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 : function(modename)
+ {
+ if (modename == this._currentModename) {
+ return;
+ }
+ this._currentModename = modename;
+ gEmailWizardLogger.info("switching to UI mode " + modename)
+
+ //_show("initialSettings"); always visible
+ //_show("cancel_button"); always visible
+ if (modename == "start") {
+ _hide("status_area");
+ _hide("result_area");
+ _hide("manual-edit_area");
+
+ _show("next_button");
+ _disable("next_button"); // will be enabled by code
+ _hide("half-manual-test_button");
+ _hide("create_button");
+ _hide("stop_button");
+ _hide("manual-edit_button");
+ _hide("advanced-setup_button");
+ } else if (modename == "find-config") {
+ _show("status_area");
+ _hide("result_area");
+ _hide("manual-edit_area");
+
+ _show("next_button");
+ _disable("next_button");
+ _hide("half-manual-test_button");
+ _hide("create_button");
+ _show("stop_button");
+ this.onStop = this.onStopFindConfig;
+ _show("manual-edit_button");
+ _hide("advanced-setup_button");
+ } else if (modename == "result") {
+ _show("status_area");
+ _show("result_area");
+ _hide("manual-edit_area");
+
+ _hide("next_button");
+ _hide("half-manual-test_button");
+ _show("create_button");
+ _enable("create_button");
+ _hide("stop_button");
+ _show("manual-edit_button");
+ _hide("advanced-setup_button");
+ } else if (modename == "manual-edit") {
+ _show("status_area");
+ _hide("result_area");
+ _show("manual-edit_area");
+
+ _hide("next_button");
+ _show("half-manual-test_button");
+ _disable("half-manual-test_button");
+ _show("create_button");
+ _disable("create_button");
+ _hide("stop_button");
+ _hide("manual-edit_button");
+ _show("advanced-setup_button");
+ _disable("advanced-setup_button");
+ } else if (modename == "manual-edit-have-hostname") {
+ _show("status_area");
+ _hide("result_area");
+ _show("manual-edit_area");
+ _hide("manual-edit_button");
+ _hide("next_button");
+ _show("create_button");
+
+ _show("half-manual-test_button");
+ _enable("half-manual-test_button");
+ _disable("create_button");
+ _hide("stop_button");
+ _show("advanced-setup_button");
+ _disable("advanced-setup_button");
+ } else if (modename == "manual-edit-testing") {
+ _show("status_area");
+ _hide("result_area");
+ _show("manual-edit_area");
+ _hide("manual-edit_button");
+ _hide("next_button");
+ _show("create_button");
+
+ _show("half-manual-test_button");
+ _disable("half-manual-test_button");
+ _disable("create_button");
+ _show("stop_button");
+ this.onStop = this.onStopHalfManualTesting;
+ _show("advanced-setup_button");
+ _disable("advanced-setup_button");
+ } else if (modename == "manual-edit-complete") {
+ _show("status_area");
+ _hide("result_area");
+ _show("manual-edit_area");
+ _hide("manual-edit_button");
+ _hide("next_button");
+ _show("create_button");
+
+ _show("half-manual-test_button");
+ _enable("half-manual-test_button");
+ _enable("create_button");
+ _hide("stop_button");
+ _show("advanced-setup_button");
+ _enable("advanced-setup_button");
+ } else {
+ throw new NotReached("unknown mode");
+ }
+ // 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) {
+ if (this._currentConfig != null) {
+ _show("advanced-setup_button");
+ _enable("advanced-setup_button");
+ _hide("half-manual-test_button");
+ _hide("create_button");
+ _hide("manual-edit_button");
+ }
+ }
+ window.sizeToContent();
+ },
+
+ /**
+ * Start from beginning with possibly new email address.
+ */
+ onStartOver : function()
+ {
+ if (this._abortable) {
+ this.onStop();
+ }
+ this.switchToMode("start");
+ },
+
+ getConcreteConfig : function()
+ {
+ var result = this._currentConfig.copy();
+ replaceVariables(result, this._realname, this._email, this._password);
+ result.rememberPassword = e("remember_password").checked &&
+ !!this._password;
+ return result;
+ },
+
+ /*
+ * This checks if the email address is at least possibly valid, meaning it
+ * has an '@' before the last char.
+ */
+ validateEmailMinimally : function(emailAddr)
+ {
+ let atPos = emailAddr.lastIndexOf("@");
+ return atPos > 0 && atPos + 1 < emailAddr.length;
+ },
+
+ /*
+ * This checks if the email address is syntactically valid,
+ * as far as we can determine. We try hard to make full checks.
+ *
+ * OTOH, we have a very small chance of false negatives,
+ * because the RFC822 address spec is insanely complicated,
+ * but rarely needed, so when this here fails, we show an error message,
+ * but don't stop the user from continuing.
+ * In contrast, if validateEmailMinimally() fails, we stop the user.
+ */
+ validateEmail : function(emailAddr)
+ {
+ return emailRE.test(emailAddr);
+ },
+
+ /**
+ * 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 : function()
+ {
+ this._email = e("email").value;
+ this.onStartOver();
+ this.checkStartDone();
+ },
+ onInputRealname : function()
+ {
+ this._realname = e("realname").value;
+ this.checkStartDone();
+ },
+
+ onInputPassword : function()
+ {
+ this._password = e("password").value;
+ },
+
+ /**
+ * This does very little other than to check that a name was entered at all
+ * Since this is such an insignificant test we should be using a very light
+ * or even jovial warning.
+ */
+ onBlurRealname : function()
+ {
+ let realnameEl = e("realname");
+ if (this._realname) {
+ this.clearError("nameerror");
+ _show("nametext");
+ realnameEl.removeAttribute("error");
+ // bug 638790: don't show realname error until user enter an email address
+ } else if (this.validateEmailMinimally(this._email)) {
+ _hide("nametext");
+ this.setError("nameerror", "please_enter_name");
+ realnameEl.setAttribute("error", "true");
+ }
+ },
+
+ /**
+ * This check is only done as an informative warning.
+ * We don't want to block the person, if they've entered an email address
+ * that doesn't conform to our regex.
+ */
+ onBlurEmail : function()
+ {
+ if (!this._email) {
+ return;
+ }
+ var emailEl = e("email");
+ if (this.validateEmail(this._email)) {
+ this.clearError("emailerror");
+ emailEl.removeAttribute("error");
+ this.onBlurRealname();
+ } else {
+ this.setError("emailerror", "double_check_email");
+ emailEl.setAttribute("error", "true");
+ }
+ },
+
+ /**
+ * If the user just tabbed through the password input without entering
+ * anything, set the type back to text so we don't wind up showing the
+ * emptytext as bullet characters.
+ */
+ onBlurPassword : function()
+ {
+ if (!this._password) {
+ e("password").type = "text";
+ }
+ },
+
+ /**
+ * @see onBlurPassword()
+ */
+ onFocusPassword : function()
+ {
+ e("password").type = "password";
+ },
+
+ /**
+ * Check whether the user entered the minimum of information
+ * needed to leave the "start" mode (entering of name, email, pw)
+ * and is allowed to proceed to detection step.
+ */
+ checkStartDone : function()
+ {
+ if (this.validateEmailMinimally(this._email) &&
+ this._realname) {
+ this._domain = this._email.split("@")[1].toLowerCase();
+ _enable("next_button");
+ } else {
+ _disable("next_button");
+ }
+ },
+
+ /**
+ * When the [Continue] button is clicked, we move from the initial account
+ * information stage to using that information to configure account details.
+ */
+ onNext : function()
+ {
+ 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 : function(domain, email)
+ {
+ gEmailWizardLogger.info("findConfig()");
+ if (this._abortable) {
+ this.onStop();
+ }
+ this.switchToMode("find-config");
+ this.startSpinner("looking_up_settings_disk");
+ var self = this;
+ this._abortable = fetchConfigFromDisk(domain,
+ function(config) // success
+ {
+ self._abortable = null;
+ self.foundConfig(config);
+ self.stopSpinner("found_settings_disk");
+ },
+ function(e) // fetchConfigFromDisk failed
+ {
+ if (e instanceof CancelledException) {
+ return;
+ }
+ gEmailWizardLogger.info("fetchConfigFromDisk failed: " + e);
+ self.startSpinner("looking_up_settings_isp");
+ self._abortable = fetchConfigFromISP(domain, email,
+ function(config) // success
+ {
+ self._abortable = null;
+ self.foundConfig(config);
+ self.stopSpinner("found_settings_isp");
+ },
+ function(e) // fetchConfigFromISP failed
+ {
+ if (e instanceof CancelledException) {
+ return;
+ }
+ gEmailWizardLogger.info("fetchConfigFromISP failed: " + e);
+ logException(e);
+ self.startSpinner("looking_up_settings_db");
+ self._abortable = fetchConfigFromDB(domain,
+ function(config) // success
+ {
+ self._abortable = null;
+ self.foundConfig(config);
+ self.stopSpinner("found_settings_db");
+ },
+ function(e) // fetchConfigFromDB failed
+ {
+ if (e instanceof CancelledException) {
+ return;
+ }
+ logException(e);
+ gEmailWizardLogger.info("fetchConfigFromDB failed: " + e);
+ self.startSpinner("looking_up_settings_db");
+ self._abortable = fetchConfigForMX(domain,
+ function(config) // success
+ {
+ self._abortable = null;
+ self.foundConfig(config);
+ self.stopSpinner("found_settings_db");
+ },
+ function(e) // fetchConfigForMX failed
+ {
+ if (e instanceof CancelledException) {
+ return;
+ }
+ logException(e);
+ gEmailWizardLogger.info("fetchConfigForMX failed: " + e);
+ var initialConfig = new AccountConfig();
+ self._prefillConfig(initialConfig);
+ self._guessConfig(domain, initialConfig);
+ });
+ });
+ });
+ });
+ },
+
+ /**
+ * Just a continuation of findConfig()
+ */
+ _guessConfig : function(domain, initialConfig)
+ {
+ this.startSpinner("looking_up_settings_guess")
+ var self = this;
+ self._abortable = guessConfig(domain,
+ function(type, hostname, port, ssl, done, config) // progress
+ {
+ gEmailWizardLogger.info("progress callback host " + hostname +
+ " port " + port + " type " + type);
+ },
+ function(config) // success
+ {
+ self._abortable = null;
+ self.foundConfig(config);
+ self.stopSpinner(Services.io.offline ?
+ "guessed_settings_offline" : "found_settings_guess");
+ window.sizeToContent();
+ },
+ function(e, config) // guessconfig failed
+ {
+ if (e instanceof CancelledException) {
+ return;
+ }
+ self._abortable = null;
+ gEmailWizardLogger.info("guessConfig failed: " + e);
+ self.showErrorStatus("failed_to_find_settings");
+ self.editConfigDetails();
+ },
+ initialConfig, "both");
+ },
+
+ /**
+ * When findConfig() was successful, it calls this.
+ * This displays the config to the user.
+ */
+ foundConfig : function(config)
+ {
+ gEmailWizardLogger.info("foundConfig()");
+ assert(config instanceof AccountConfig,
+ "BUG: Arg 'config' needs to be an AccountConfig object");
+
+ this._haveValidConfigForDomain = this._email.split("@")[1];
+
+ if (!this._realname || !this._email) {
+ return;
+ }
+ this._foundConfig2(config);
+ },
+
+ // Continuation of foundConfig2() after custom fields.
+ _foundConfig2 : function(config)
+ {
+ this.displayConfigResult(config);
+ },
+
+ /**
+ * [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. onHalfManualTest(), 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 : function()
+ {
+ throw new NotReached("onStop should be overridden by now");
+ },
+ _onStopCommon : function()
+ {
+ if (!this._abortable) {
+ throw new NotReached("onStop called although there's nothing to stop");
+ }
+ gEmailWizardLogger.info("onStop cancelled _abortable");
+ this._abortable.cancel(new UserCancelledException());
+ this._abortable = null;
+ this.stopSpinner();
+ },
+ onStopFindConfig : function()
+ {
+ this._onStopCommon();
+ this.switchToMode("start");
+ this.checkStartDone();
+ },
+ onStopHalfManualTesting : function()
+ {
+ this._onStopCommon();
+ this.validateManualEditComplete();
+ },
+
+
+
+ ///////////////////////////////////////////////////////////////////
+ // status area
+
+ startSpinner : function(actionStrName)
+ {
+ e("status_area").setAttribute("status", "loading");
+ gEmailWizardLogger.warn("spinner start " + actionStrName);
+ this._showStatusTitle(actionStrName);
+ },
+
+ stopSpinner : function(actionStrName)
+ {
+ e("status_area").setAttribute("status", "result");
+ _hide("stop_button");
+ this._showStatusTitle(actionStrName);
+ gEmailWizardLogger.warn("all spinner stop " + actionStrName);
+ },
+
+ showErrorStatus : function(actionStrName)
+ {
+ e("status_area").setAttribute("status", "error");
+ gEmailWizardLogger.warn("status error " + actionStrName);
+ this._showStatusTitle(actionStrName);
+ },
+
+ _showStatusTitle : function(msgName)
+ {
+ let msg = " "; // assure height. Do via min-height in CSS, for 2 lines?
+ try {
+ if (msgName) {
+ msg = gStringsBundle.getFormattedString(msgName, [gBrandShortName]);
+ }
+ } catch(ex) {
+ gEmailWizardLogger.error("missing string for " + msgName);
+ msg = msgName + " (missing string in translation!)";
+ }
+
+ e("status_msg").textContent = msg;
+ gEmailWizardLogger.info("status msg: " + msg);
+ },
+
+
+
+ /////////////////////////////////////////////////////////////////
+ // Result area
+
+ /**
+ * Displays a (probed) config to the user,
+ * in the result config details area.
+ *
+ * @param config {AccountConfig} The config to present to user
+ */
+ displayConfigResult : function(config)
+ {
+ assert(config instanceof AccountConfig);
+ this._currentConfig = config;
+ var configFilledIn = this.getConcreteConfig();
+
+ var unknownString = gStringsBundle.getString("resultUnknown");
+
+ function _makeHostDisplayString(server, stringName)
+ {
+ let type = gStringsBundle.getString(sanitize.translate(server.type,
+ { imap : "resultIMAP", pop3 : "resultPOP3", smtp : "resultSMTP" }),
+ unknownString);
+ let host = server.hostname +
+ (isStandardPort(server.port) ? "" : ":" + server.port);
+ let ssl = gStringsBundle.getString(sanitize.translate(server.socketType,
+ { 1 : "resultNoEncryption", 2 : "resultSSL", 3 : "resultSTARTTLS" }),
+ unknownString);
+ let certStatus = gStringsBundle.getString(server.badCert ?
+ "resultSSLCertWeak" : "resultSSLCertOK");
+ // TODO: we should really also display authentication method here.
+ return gStringsBundle.getFormattedString(stringName,
+ [ type, host, ssl, certStatus ]);
+ };
+
+ var incomingResult = unknownString;
+ if (configFilledIn.incoming.hostname) {
+ incomingResult = _makeHostDisplayString(configFilledIn.incoming,
+ "resultIncoming");
+ }
+
+ var outgoingResult = unknownString;
+ if (!config.outgoing.existingServerKey) {
+ if (configFilledIn.outgoing.hostname) {
+ outgoingResult = _makeHostDisplayString(configFilledIn.outgoing,
+ "resultOutgoing");
+ }
+ } else {
+ outgoingResult = gStringsBundle.getString("resultOutgoingExisting");
+ }
+
+ var usernameResult;
+ if (configFilledIn.incoming.username == configFilledIn.outgoing.username) {
+ usernameResult = gStringsBundle.getFormattedString("resultUsernameBoth",
+ [ configFilledIn.incoming.username || unknownString ]);
+ } else {
+ usernameResult = gStringsBundle.getFormattedString(
+ "resultUsernameDifferent",
+ [ configFilledIn.incoming.username || unknownString,
+ configFilledIn.outgoing.username || unknownString ]);
+ }
+
+ setText("result-incoming", incomingResult);
+ setText("result-outgoing", outgoingResult);
+ setText("result-username", usernameResult);
+
+ gEmailWizardLogger.info(debugObject(config, "config"));
+ // IMAP / POP dropdown
+ var lookForAltType =
+ config.incoming.type == "imap" ? "pop3" : "imap";
+ var alternative = null;
+ for (let i = 0; i < config.incomingAlternatives.length; i++) {
+ let alt = config.incomingAlternatives[i];
+ if (alt.type == lookForAltType) {
+ alternative = alt;
+ break;
+ }
+ }
+ if (alternative) {
+ _show("result_imappop");
+ e("result_select_" + alternative.type).configIncoming = alternative;
+ e("result_select_" + config.incoming.type).configIncoming =
+ config.incoming;
+ e("result_imappop").value =
+ config.incoming.type == "imap" ? 1 : 2;
+ } else {
+ _hide("result_imappop");
+ }
+
+ this.switchToMode("result");
+ },
+
+ /**
+ * Handle the user switching between IMAP and POP3 settings using the
+ * radio buttons.
+ *
+ * Note: This function must only be called by user action, not by setting
+ * the value or selectedItem or selectedIndex of the radiogroup!
+ * This is why we use the oncommand attribute of the radio elements
+ * instead of the onselect attribute of the radiogroup.
+ */
+ onResultIMAPOrPOP3 : function()
+ {
+ var config = this._currentConfig;
+ var radiogroup = e("result_imappop");
+ // add current server as best alternative to start of array
+ config.incomingAlternatives.unshift(config.incoming);
+ // use selected server (stored as special property on the <radio> node)
+ config.incoming = radiogroup.selectedItem.configIncoming;
+ // remove newly selected server from list of alternatives
+ config.incomingAlternatives = config.incomingAlternatives.filter(
+ function(e) { return e != config.incoming; });
+ this.displayConfigResult(config);
+ },
+
+
+
+ /////////////////////////////////////////////////////////////////
+ // 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 : function()
+ {
+ var config = this.getConcreteConfig();
+ if (!config) {
+ config = new AccountConfig();
+ }
+ config.source = AccountConfig.kSourceUser;
+
+ // Incoming server
+ try {
+ var inHostnameField = e("incoming_hostname");
+ config.incoming.hostname = sanitize.hostname(inHostnameField.value);
+ inHostnameField.value = config.incoming.hostname;
+ } catch (e) { gEmailWizardLogger.warn(e); }
+ try {
+ config.incoming.port = sanitize.integerRange(e("incoming_port").value,
+ kMinPort, kMaxPort);
+ } catch (e) {
+ config.incoming.port = undefined; // incl. default "Auto"
+ }
+ config.incoming.type = sanitize.translate(e("incoming_protocol").value,
+ { 1: "imap", 2 : "pop3", 0 : null });
+ config.incoming.socketType = sanitize.integer(e("incoming_ssl").value);
+ config.incoming.auth = sanitize.integer(e("incoming_authMethod").value);
+ config.incoming.username = e("incoming_username").value;
+
+ // Outgoing server
+
+ // Did the user select one of the already configured SMTP servers from the
+ // drop-down list? If so, use it.
+ var outHostnameCombo = e("outgoing_hostname");
+ var outMenuitem = outHostnameCombo.selectedItem;
+ if (outMenuitem && outMenuitem.serverKey) {
+ config.outgoing.existingServerKey = outMenuitem.serverKey;
+ config.outgoing.existingServerLabel = outMenuitem.label;
+ config.outgoing.addThisServer = false;
+ config.outgoing.useGlobalPreferredServer = false;
+ } else {
+ config.outgoing.existingServerKey = null;
+ config.outgoing.addThisServer = true;
+ config.outgoing.useGlobalPreferredServer = false;
+
+ try {
+ config.outgoing.hostname = sanitize.hostname(
+ outHostnameCombo.inputField.value);
+ outHostnameCombo.inputField.value = config.outgoing.hostname;
+ } catch (e) { gEmailWizardLogger.warn(e); }
+ try {
+ config.outgoing.port = sanitize.integerRange(e("outgoing_port").value,
+ kMinPort, kMaxPort);
+ } catch (e) {
+ config.outgoing.port = undefined; // incl. default "Auto"
+ }
+ config.outgoing.socketType = sanitize.integer(e("outgoing_ssl").value);
+ config.outgoing.auth = sanitize.integer(e("outgoing_authMethod").value);
+ }
+ config.outgoing.username = e("outgoing_username").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 : function()
+ {
+ if (this._abortable) {
+ this.onStop();
+ }
+ this.editConfigDetails();
+ },
+
+ /**
+ * 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 : function()
+ {
+ gEmailWizardLogger.info("manual edit");
+
+ if (!this._currentConfig) {
+ this._currentConfig = new AccountConfig();
+ this._currentConfig.incoming.type = "imap";
+ this._currentConfig.incoming.username = "%EMAILLOCALPART%";
+ this._currentConfig.outgoing.username = "%EMAILLOCALPART%";
+ 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 config {AccountConfig} The config to present to user
+ */
+ _fillManualEditFields : function(config)
+ {
+ assert(config instanceof AccountConfig);
+
+ // incoming server
+ e("incoming_protocol").value = sanitize.translate(config.incoming.type,
+ { "imap" : 1, "pop3" : 2 }, 1);
+ e("incoming_hostname").value = config.incoming.hostname;
+ e("incoming_ssl").value = sanitize.enum(config.incoming.socketType,
+ [ 0, 1, 2, 3 ], 0);
+ e("incoming_authMethod").value = sanitize.enum(config.incoming.auth,
+ [ 0, 3, 4, 5, 6, 10 ], 0);
+ e("incoming_username").value = config.incoming.username;
+ if (config.incoming.port) {
+ e("incoming_port").value = config.incoming.port;
+ } else {
+ this.adjustIncomingPortToSSLAndProtocol(config);
+ }
+ this.fillPortDropdown(config.incoming.type);
+
+ // If the hostname supports OAuth2 and imap is enabled, enable OAuth2.
+ let iDetails = OAuth2Providers.getHostnameDetails(config.incoming.hostname);
+ gEmailWizardLogger.info("OAuth2 details for incoming hostname " +
+ config.incoming.hostname + " is " + iDetails);
+ e("in-authMethod-oauth2").hidden = !(iDetails && e("incoming_protocol").value == 1);
+ if (!e("in-authMethod-oauth2").hidden) {
+ config.oauthSettings = {};
+ [config.oauthSettings.issuer, config.oauthSettings.scope] = iDetails;
+ // oauthsettings are not stored nor changable in the user interface, so just
+ // store them in the base configuration.
+ this._currentConfig.oauthSettings = config.oauthSettings;
+ }
+
+ // outgoing server
+ e("outgoing_hostname").value = config.outgoing.hostname;
+ e("outgoing_username").value = config.outgoing.username;
+ // While sameInOutUsernames is true we synchronize values of incoming
+ // and outgoing username.
+ this.sameInOutUsernames = true;
+ e("outgoing_ssl").value = sanitize.enum(config.outgoing.socketType,
+ [ 0, 1, 2, 3 ], 0);
+ e("outgoing_authMethod").value = sanitize.enum(config.outgoing.auth,
+ [ 0, 1, 3, 4, 5, 6, 10 ], 0);
+ if (config.outgoing.port) {
+ e("outgoing_port").value = config.outgoing.port;
+ } else {
+ this.adjustOutgoingPortToSSLAndProtocol(config);
+ }
+
+ // If the hostname supports OAuth2 and imap is enabled, enable OAuth2.
+ let oDetails = OAuth2Providers.getHostnameDetails(config.outgoing.hostname);
+ gEmailWizardLogger.info("OAuth2 details for outgoing hostname " +
+ config.outgoing.hostname + " is " + oDetails);
+ e("out-authMethod-oauth2").hidden = !oDetails;
+ if (!e("out-authMethod-oauth2").hidden) {
+ config.oauthSettings = {};
+ [config.oauthSettings.issuer, config.oauthSettings.scope] = oDetails;
+ // oauthsettings are not stored nor changable in the user interface, so just
+ // store them in the base configuration.
+ this._currentConfig.oauthSettings = config.oauthSettings;
+ }
+
+ // populate fields even if existingServerKey, in case user changes back
+ if (config.outgoing.existingServerKey) {
+ let menulist = e("outgoing_hostname");
+ // We can't use menulist.value = config.outgoing.existingServerKey
+ // because would overwrite the text field, so have to do it manually:
+ let menuitems = menulist.menupopup.childNodes;
+ for (let menuitem of menuitems) {
+ if (menuitem.serverKey == config.outgoing.existingServerKey) {
+ menulist.selectedItem = menuitem;
+ break;
+ }
+ }
+ }
+ this.onChangedOutgoingDropdown(); // show/hide outgoing port, SSL, ...
+ },
+
+ /**
+ * Automatically fill port field in manual edit,
+ * unless user entered a non-standard port.
+ * @param config {AccountConfig}
+ */
+ adjustIncomingPortToSSLAndProtocol : function(config)
+ {
+ var autoPort = gStringsBundle.getString("port_auto");
+ var incoming = config.incoming;
+ // we could use getHostEntry() here, but that API is bad, so don't bother
+ var newInPort = undefined;
+ if (!incoming.port || isStandardPort(incoming.port)) {
+ if (incoming.type == "imap") {
+ if (incoming.socketType == 1 || incoming.socketType == 3) {
+ newInPort = 143;
+ } else if (incoming.socketType == 2) { // Normal SSL
+ newInPort = 993;
+ } else { // auto
+ newInPort = autoPort;
+ }
+ } else if (incoming.type == "pop3") {
+ if (incoming.socketType == 1 || incoming.socketType == 3) {
+ newInPort = 110;
+ } else if (incoming.socketType == 2) { // Normal SSLs
+ newInPort = 995;
+ } else { // auto
+ newInPort = autoPort;
+ }
+ }
+ }
+ if (newInPort != undefined) {
+ e("incoming_port").value = newInPort;
+ e("incoming_authMethod").value = 0; // auto
+ }
+ },
+
+ /**
+ * @see adjustIncomingPortToSSLAndProtocol()
+ */
+ adjustOutgoingPortToSSLAndProtocol : function(config)
+ {
+ var autoPort = gStringsBundle.getString("port_auto");
+ var outgoing = config.outgoing;
+ var newOutPort = undefined;
+ if (!outgoing.port || isStandardPort(outgoing.port)) {
+ if (outgoing.socketType == 1 || outgoing.socketType == 3) {
+ // standard port is 587 *or* 25, so set to auto
+ // unless user or config already entered one of these two ports.
+ if (outgoing.port != 25 && outgoing.port != 587) {
+ newOutPort = autoPort;
+ }
+ } else if (outgoing.socketType == 2) { // Normal SSL
+ newOutPort = 465;
+ } else { // auto
+ newOutPort = autoPort;
+ }
+ }
+ if (newOutPort != undefined) {
+ e("outgoing_port").value = newOutPort;
+ e("outgoing_authMethod").value = 0; // auto
+ }
+ },
+
+ /**
+ * 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 : function(config)
+ {
+ var incoming = config.incoming;
+ var newInSocketType = undefined;
+ if (!incoming.port || // auto
+ !isStandardPort(incoming.port)) {
+ return;
+ }
+ if (incoming.type == "imap") {
+ // normal SSL impossible
+ if (incoming.port == 143 && incoming.socketType == 2) {
+ newInSocketType = 0; // auto
+ // must be normal SSL
+ } else if (incoming.port == 993 && incoming.socketType != 2) {
+ newInSocketType = 2;
+ }
+ } else if (incoming.type == "pop3") {
+ // normal SSL impossible
+ if (incoming.port == 110 && incoming.socketType == 2) {
+ newInSocketType = 0; // auto
+ // must be normal SSL
+ } else if (incoming.port == 995 && incoming.socketType != 2) {
+ newInSocketType = 2;
+ }
+ }
+ if (newInSocketType != undefined) {
+ e("incoming_ssl").value = newInSocketType;
+ e("incoming_authMethod").value = 0; // auto
+ }
+ },
+
+ /**
+ * @see adjustIncomingSSLToPort()
+ */
+ adjustOutgoingSSLToPort : function(config)
+ {
+ var outgoing = config.outgoing;
+ var newOutSocketType = undefined;
+ if (!outgoing.port || // auto
+ !isStandardPort(outgoing.port)) {
+ return;
+ }
+ // normal SSL impossible
+ if ((outgoing.port == 587 || outgoing.port == 25) &&
+ outgoing.socketType == 2) {
+ newOutSocketType = 0; // auto
+ // must be normal SSL
+ } else if (outgoing.port == 465 && outgoing.socketType != 2) {
+ newOutSocketType = 2;
+ }
+ if (newOutSocketType != undefined) {
+ e("outgoing_ssl").value = newOutSocketType;
+ e("outgoing_authMethod").value = 0; // auto
+ }
+ },
+
+ /**
+ * Sets the prefilled values of the port fields.
+ * Filled statically with the standard ports for the given protocol,
+ * plus "Auto".
+ */
+ fillPortDropdown : function(protocolType)
+ {
+ var menu = e(protocolType == "smtp" ? "outgoing_port" : "incoming_port");
+
+ // menulist.removeAllItems() is nice, but nicely clears the user value, too
+ var popup = menu.menupopup;
+ while (popup.hasChildNodes())
+ popup.lastChild.remove();
+
+ // add standard ports
+ var autoPort = gStringsBundle.getString("port_auto");
+ menu.appendItem(autoPort, autoPort, ""); // label,value,descr
+ for (let port of getStandardPorts(protocolType)) {
+ menu.appendItem(port, port, ""); // label,value,descr
+ }
+ },
+
+ onChangedProtocolIncoming : function()
+ {
+ var config = this.getUserConfig();
+ this.adjustIncomingPortToSSLAndProtocol(config);
+ this.fillPortDropdown(config.incoming.type);
+ this.onChangedManualEdit();
+ },
+ onChangedPortIncoming : function()
+ {
+ gEmailWizardLogger.info("incoming port changed");
+ this.adjustIncomingSSLToPort(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+ onChangedPortOutgoing : function()
+ {
+ gEmailWizardLogger.info("outgoing port changed");
+ this.adjustOutgoingSSLToPort(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+ onChangedSSLIncoming : function()
+ {
+ this.adjustIncomingPortToSSLAndProtocol(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+ onChangedSSLOutgoing : function()
+ {
+ this.adjustOutgoingPortToSSLAndProtocol(this.getUserConfig());
+ this.onChangedManualEdit();
+ },
+ onChangedInAuth : function()
+ {
+ this.onChangedManualEdit();
+ },
+ onChangedOutAuth : function(aSelectedAuth)
+ {
+ if (aSelectedAuth) {
+ e("outgoing_label").hidden = e("outgoing_username").hidden =
+ (aSelectedAuth.id == "out-authMethod-no");
+ }
+ this.onChangedManualEdit();
+ },
+ onInputInUsername : function()
+ {
+ if (this.sameInOutUsernames)
+ e("outgoing_username").value = e("incoming_username").value;
+ this.onChangedManualEdit();
+ },
+ onInputOutUsername : function()
+ {
+ this.sameInOutUsernames = false;
+ this.onChangedManualEdit();
+ },
+ onInputHostname : function()
+ {
+ this.onChangedManualEdit();
+ },
+
+ /**
+ * Sets the label of the first entry of the dropdown which represents
+ * the new outgoing server.
+ */
+ onOpenOutgoingDropdown : function()
+ {
+ var menulist = e("outgoing_hostname");
+ // If the menulist is not editable, there is nothing to update
+ // and menulist.inputField does not even exist.
+ if (!menulist.editable)
+ return;
+
+ var menuitem = menulist.getItemAtIndex(0);
+ assert(!menuitem.serverKey, "I wanted the special item for the new host");
+ menuitem.label = menulist.inputField.value;
+ },
+
+ /**
+ * User selected an existing SMTP server (or deselected it).
+ * This changes only the UI. The values are read in getUserConfig().
+ */
+ onChangedOutgoingDropdown : function()
+ {
+ var menulist = e("outgoing_hostname");
+ var menuitem = menulist.selectedItem;
+ if (menuitem && menuitem.serverKey) {
+ // an existing server has been selected from the dropdown
+ menulist.editable = false;
+ _hide("outgoing_port");
+ _hide("outgoing_ssl");
+ _hide("outgoing_authMethod");
+ } else {
+ // new server, with hostname, port etc.
+ menulist.editable = true;
+ _show("outgoing_port");
+ _show("outgoing_ssl");
+ _show("outgoing_authMethod");
+ }
+
+ this.onChangedManualEdit();
+ },
+
+ onChangedManualEdit : function()
+ {
+ if (this._abortable) {
+ this.onStop();
+ }
+ this.validateManualEditComplete();
+ },
+
+ /**
+ * 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 : function()
+ {
+ // getUserConfig() is expensive, but still OK, not a problem
+ var manualConfig = this.getUserConfig();
+ this._currentConfig = manualConfig;
+ if (manualConfig.isComplete()) {
+ this.switchToMode("manual-edit-complete");
+ } else if (!!manualConfig.incoming.hostname &&
+ !!manualConfig.outgoing.hostname) {
+ this.switchToMode("manual-edit-have-hostname");
+ } else {
+ this.switchToMode("manual-edit");
+ }
+ },
+
+ /**
+ * [Switch to provisioner] button click handler. Always active, allows
+ * one to switch to the account provisioner screen.
+ */
+ onSwitchToProvisioner : function ()
+ {
+ // We have to close this window first, otherwise msgNewMailAccount
+ // in accountUtils.js will think that this window still
+ // exists when it's called from the account provisioner window.
+ // This is because the account provisioner window is modal,
+ // and therefore blocks. Therefore, we override the _okCallback
+ // with a function that spawns the account provisioner, and then
+ // close the window.
+ this._okCallback = function() {
+ NewMailAccountProvisioner(window.arguments[0].msgWindow, window.arguments[0].extraData);
+ }
+ window.close();
+ },
+
+ /**
+ * [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.
+ */
+ onAdvancedSetup : function()
+ {
+ assert(this._currentConfig instanceof AccountConfig);
+ let configFilledIn = this.getConcreteConfig();
+
+ if (checkIncomingServerAlreadyExists(configFilledIn)) {
+ alertPrompt(gStringsBundle.getString("error_creating_account"),
+ gStringsBundle.getString("incoming_server_exists"));
+ return;
+ }
+
+ gEmailWizardLogger.info("creating account in backend");
+ let newAccount = createAccountInBackend(configFilledIn);
+
+ let existingAccountManager = Services.wm
+ .getMostRecentWindow("mailnews:accountmanager");
+ if (existingAccountManager) {
+ existingAccountManager.focus();
+ } else {
+ window.openDialog("chrome://messenger/content/AccountManager.xul",
+ "AccountManager", "chrome,centerscreen,modal,titlebar",
+ { server: newAccount.incomingServer,
+ selectPage: "am-server.xul" });
+ }
+ window.close();
+ },
+
+ /**
+ * [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.
+ */
+ onHalfManualTest : function()
+ {
+ var newConfig = this.getUserConfig();
+ gEmailWizardLogger.info(debugObject(newConfig, "manualConfigToTest"));
+ this.startSpinner("looking_up_settings_halfmanual");
+ this.switchToMode("manual-edit-testing");
+ // if (this._userPickedOutgoingServer) TODO
+ var self = this;
+ this._abortable = guessConfig(this._domain,
+ function(type, hostname, port, ssl, done, config) // progress
+ {
+ gEmailWizardLogger.info("progress callback host " + hostname +
+ " port " + port + " type " + type);
+ },
+ function(config) // success
+ {
+ self._abortable = null;
+ self._fillManualEditFields(config);
+ self.switchToMode("manual-edit-complete");
+ self.stopSpinner("found_settings_halfmanual");
+ },
+ function(e, config) // guessconfig failed
+ {
+ if (e instanceof CancelledException) {
+ return;
+ }
+ self._abortable = null;
+ gEmailWizardLogger.info("guessConfig failed: " + e);
+ self.showErrorStatus("failed_to_find_settings");
+ self.switchToMode("manual-edit-have-hostname");
+ },
+ newConfig,
+ newConfig.outgoing.existingServerKey ? "incoming" : "both");
+ },
+
+
+
+ /////////////////////////////////////////////////////////////////
+ // UI helper functions
+
+ _prefillConfig : function(initialConfig)
+ {
+ var emailsplit = this._email.split("@");
+ assert(emailsplit.length > 1);
+ var emaillocal = sanitize.nonemptystring(emailsplit[0]);
+ initialConfig.incoming.username = emaillocal;
+ initialConfig.outgoing.username = emaillocal;
+ return initialConfig;
+ },
+
+ clearError : function(which)
+ {
+ _hide(which);
+ _hide(which + "icon");
+ e(which).textContent = "";
+ },
+
+ setError : function(which, msg_name)
+ {
+ try {
+ _show(which);
+ _show(which + "icon");
+ e(which).textContent = gStringsBundle.getString(msg_name);
+ window.sizeToContent();
+ } catch (ex) { alertPrompt("missing error string", msg_name); }
+ },
+
+
+
+ /////////////////////////////////////////////////////////////////
+ // Finish & dialog close functions
+
+ onKeyDown : function(event)
+ {
+ let key = event.keyCode;
+ if (key == 27) { // Escape key
+ this.onCancel();
+ return true;
+ }
+ if (key == 13) { // OK key
+ let buttons = [
+ { id: "next_button", action: makeCallback(this, this.onNext) },
+ { id: "create_button", action: makeCallback(this, this.onCreate) },
+ { id: "half-manual-test_button",
+ action: makeCallback(this, this.onHalfManualTest) },
+ ];
+ for (let button of buttons) {
+ button.e = e(button.id);
+ if (button.e.hidden || button.e.disabled) {
+ continue;
+ }
+ button.action();
+ return true;
+ }
+ }
+ return false;
+ },
+
+ onCancel : function()
+ {
+ window.close();
+ // The window onclose handler will call onWizardShutdown for us.
+ },
+
+ onWizardShutdown : function()
+ {
+ if (this._abortable) {
+ this._abortable.cancel(new UserCancelledException());
+ }
+
+ if (this._okCallback) {
+ this._okCallback();
+ }
+ gEmailWizardLogger.info("Shutting down email config dialog");
+ },
+
+
+ onCreate : function()
+ {
+ try {
+ gEmailWizardLogger.info("Create button clicked");
+
+ var configFilledIn = this.getConcreteConfig();
+ var self = this;
+ // If the dialog is not needed, it will go straight to OK callback
+ gSecurityWarningDialog.open(this._currentConfig, configFilledIn, true,
+ function() // on OK
+ {
+ self.validateAndFinish(configFilledIn);
+ },
+ function() {}); // on cancel, do nothing
+ } catch (ex) {
+ gEmailWizardLogger.error("Error creating account. ex=" + ex +
+ ", stack=" + ex.stack);
+ alertPrompt(gStringsBundle.getString("error_creating_account"), ex);
+ }
+ },
+
+ // called by onCreate()
+ validateAndFinish : function()
+ {
+ var configFilledIn = this.getConcreteConfig();
+
+ if (checkIncomingServerAlreadyExists(configFilledIn)) {
+ alertPrompt(gStringsBundle.getString("error_creating_account"),
+ gStringsBundle.getString("incoming_server_exists"));
+ return;
+ }
+
+ if (configFilledIn.outgoing.addThisServer) {
+ let existingServer = checkOutgoingServerAlreadyExists(configFilledIn);
+ if (existingServer) {
+ configFilledIn.outgoing.addThisServer = false;
+ configFilledIn.outgoing.existingServerKey = existingServer.key;
+ }
+ }
+
+ // TODO use a UI mode (switchToMode()) for verfication, too.
+ // But we need to go back to the previous mode, because we might be in
+ // "result" or "manual-edit-complete" mode.
+ _disable("create_button");
+ _disable("half-manual-test_button");
+ _disable("advanced-setup_button");
+ // no stop button: backend has no ability to stop :-(
+ var self = this;
+ this.startSpinner("checking_password");
+ // logic function defined in verifyConfig.js
+ 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,
+ this._parentMsgWindow,
+ function(successfulConfig) // success
+ {
+ self.stopSpinner(successfulConfig.incoming.password ?
+ "password_ok" : null);
+
+ // the auth might have changed, so we
+ // should back-port it to 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.oauthSettings)
+ self._currentConfig.oauthSettings = successfulConfig.oauthSettings;
+
+ self.finish();
+ },
+ function(e) // failed
+ {
+ self.showErrorStatus("config_unverifiable");
+ // TODO bug 555448: wrong error msg, there may be a 1000 other
+ // reasons why this failed, and this is misleading users.
+ self.setError("passworderror", "user_pass_invalid");
+ // TODO use switchToMode(), see above
+ // give user something to proceed after fixing
+ _enable("create_button");
+ // hidden in non-manual mode, so it's fine to enable
+ _enable("half-manual-test_button");
+ _enable("advanced-setup_button");
+ });
+ },
+
+ finish : function()
+ {
+ gEmailWizardLogger.info("creating account in backend");
+ createAccountInBackend(this.getConcreteConfig());
+ window.close();
+ },
+};
+
+var gEmailConfigWizard = new EmailConfigWizard();
+
+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;
+}
+
+var _gStandardPorts = {};
+_gStandardPorts["imap"] = [ 143, 993 ];
+_gStandardPorts["pop3"] = [ 110, 995 ];
+_gStandardPorts["smtp"] = [ 587, 25, 465 ]; // order matters
+var _gAllStandardPorts = _gStandardPorts["smtp"]
+ .concat(_gStandardPorts["imap"]).concat(_gStandardPorts["pop3"]);
+
+function isStandardPort(port)
+{
+ return _gAllStandardPorts.indexOf(port) != -1;
+}
+
+function getStandardPorts(protocolType)
+{
+ return _gStandardPorts[protocolType];
+}
+
+
+/**
+ * Warning dialog, warning user about lack of, or inappropriate, encryption.
+ *
+ * This is effectively a separate dialog, but implemented as part of
+ * this dialog. It works by hiding the main dialog part and unhiding
+ * the this part, and vice versa, and resizing the dialog.
+ */
+function SecurityWarningDialog()
+{
+ this._acknowledged = new Array();
+}
+SecurityWarningDialog.prototype =
+{
+ /**
+ * {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 : null,
+
+ _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)
+ * - the SSL certificate is not proper
+ * - (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 : function(configSchema, configFilledIn)
+ {
+ assert(configSchema instanceof AccountConfig);
+ assert(configFilledIn instanceof AccountConfig);
+ assert(configSchema.isComplete());
+ assert(configFilledIn.isComplete());
+
+ var incomingBad = ((configFilledIn.incoming.socketType > 1) ? 0 : this._inSecurityBad) |
+ ((configFilledIn.incoming.badCert) ? this._inCertBad : 0);
+ var outgoingBad = 0;
+ if (!configFilledIn.outgoing.existingServerKey) {
+ outgoingBad = ((configFilledIn.outgoing.socketType > 1) ? 0 : this._outSecurityBad) |
+ ((configFilledIn.outgoing.badCert) ? this._outCertBad : 0);
+ }
+
+ if (incomingBad > 0) {
+ if (this._acknowledged.some(
+ function(ackServer) {
+ return serverMatches(ackServer, configFilledIn.incoming);
+ }))
+ incomingBad = 0;
+ }
+ if (outgoingBad > 0) {
+ if (this._acknowledged.some(
+ function(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 : function(configSchema, configFilledIn, onlyIfNeeded,
+ okCallback, cancelCallback)
+ {
+ assert(typeof(okCallback) == "function");
+ assert(typeof(cancelCallback) == "function");
+ // needed() also checks the parameters
+ var needed = this.needed(configSchema, configFilledIn);
+ if ((needed == 0) && onlyIfNeeded) {
+ okCallback();
+ return;
+ }
+
+ assert(needed > 0 , "security dialog opened needlessly");
+ this._currentConfigFilledIn = configFilledIn;
+ this._okCallback = okCallback;
+ this._cancelCallback = cancelCallback;
+ var incoming = configFilledIn.incoming;
+ var outgoing = configFilledIn.outgoing;
+
+ _hide("mastervbox");
+ _show("warningbox");
+ // reset dialog, in case we've shown it before
+ e("acknowledge_warning").checked = false;
+ _disable("iknow");
+ e("incoming_technical").removeAttribute("expanded");
+ e("incoming_details").setAttribute("collapsed", true);
+ e("outgoing_technical").removeAttribute("expanded");
+ e("outgoing_details").setAttribute("collapsed", true);
+
+ if (needed & this._inSecurityBad) {
+ setText("warning_incoming", gStringsBundle.getFormattedString(
+ "cleartext_warning", [incoming.hostname]));
+ setText("incoming_details", gStringsBundle.getString(
+ "cleartext_details"));
+ _show("incoming_box");
+ } else if (needed & this._inCertBad) {
+ setText("warning_incoming", gStringsBundle.getFormattedString(
+ "selfsigned_warning", [incoming.hostname]));
+ setText("incoming_details", gStringsBundle.getString(
+ "selfsigned_details"));
+ _show("incoming_box");
+ } else {
+ _hide("incoming_box");
+ }
+
+ if (needed & this._outSecurityBad) {
+ setText("warning_outgoing", gStringsBundle.getFormattedString(
+ "cleartext_warning", [outgoing.hostname]));
+ setText("outgoing_details", gStringsBundle.getString(
+ "cleartext_details"));
+ _show("outgoing_box");
+ } else if (needed & this._outCertBad) {
+ setText("warning_outgoing", gStringsBundle.getFormattedString(
+ "selfsigned_warning", [outgoing.hostname]));
+ setText("outgoing_details", gStringsBundle.getString(
+ "selfsigned_details"));
+ _show("outgoing_box");
+ } else {
+ _hide("outgoing_box");
+ }
+ _show("acknowledge_warning");
+ assert(!e("incoming_box").hidden || !e("outgoing_box").hidden,
+ "warning dialog shown for unknown reason");
+
+ window.sizeToContent();
+ },
+
+ toggleDetails : function (id)
+ {
+ let details = e(id + "_details");
+ let tech = e(id + "_technical");
+ if (details.getAttribute("collapsed")) {
+ details.removeAttribute("collapsed");
+ tech.setAttribute("expanded", true);
+ } else {
+ details.setAttribute("collapsed", true);
+ tech.removeAttribute("expanded");
+ }
+ },
+
+ /**
+ * user checked checkbox that he understood it and wishes
+ * to ignore the warning.
+ */
+ toggleAcknowledge : function()
+ {
+ if (e("acknowledge_warning").checked) {
+ _enable("iknow");
+ } else {
+ _disable("iknow");
+ }
+ },
+
+ /**
+ * [Cancel] button pressed. Get me out of here!
+ */
+ onCancel : function()
+ {
+ _hide("warningbox");
+ _show("mastervbox");
+ window.sizeToContent();
+
+ 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 : function()
+ {
+ assert(e("acknowledge_warning").checked);
+
+ var overrideOK = this.showCertOverrideDialog(this._currentConfigFilledIn);
+ if (!overrideOK) {
+ this.onCancel();
+ return;
+ }
+
+ // need filled in, in case hostname is placeholder
+ var storeConfig = this._currentConfigFilledIn.copy();
+ this._acknowledged.push(storeConfig.incoming);
+ this._acknowledged.push(storeConfig.outgoing);
+
+ _show("mastervbox");
+ _hide("warningbox");
+ window.sizeToContent();
+
+ this._okCallback();
+ },
+
+ /**
+ * Shows a(nother) dialog which allows the user to see and override
+ * (manually accept) a bad certificate. It also optionally adds it
+ * permanently to the "good certs" store of NSS in the profile.
+ * Only shows the dialog, if there are bad certs. Otherwise, it's a no-op.
+ *
+ * The dialog is the standard PSM cert override dialog.
+ *
+ * @param config {AccountConfig} concrete
+ * @returns true, if all certs are fine or the user accepted them.
+ * false, if the user cancelled.
+ *
+ * static function
+ * sync function: blocks until the dialog is closed.
+ */
+ showCertOverrideDialog : function(config)
+ {
+ if (config.incoming.socketType > 1 && // SSL or STARTTLS
+ config.incoming.badCert) {
+ var params = {
+ exceptionAdded : false,
+ prefetchCert : true,
+ location : config.incoming.targetSite,
+ };
+ window.openDialog("chrome://pippki/content/exceptionDialog.xul",
+ "","chrome,centerscreen,modal", params);
+ if (params.exceptionAdded) { // set by dialog
+ config.incoming.badCert = false;
+ } else {
+ return false;
+ }
+ }
+ if (!config.outgoing.existingServerKey) {
+ if (config.outgoing.socketType > 1 && // SSL or STARTTLS
+ config.outgoing.badCert) {
+ var params = {
+ exceptionAdded : false,
+ prefetchCert : true,
+ location : config.outgoing.targetSite,
+ };
+ window.openDialog("chrome://pippki/content/exceptionDialog.xul",
+ "","chrome,centerscreen,modal", params);
+ if (params.exceptionAdded) { // set by dialog
+ config.outgoing.badCert = false;
+ } else {
+ return false;
+ }
+ }
+ }
+ return true;
+ },
+}
+var gSecurityWarningDialog = new SecurityWarningDialog();
diff --git a/mailnews/base/prefs/content/accountcreation/emailWizard.xul b/mailnews/base/prefs/content/accountcreation/emailWizard.xul
new file mode 100644
index 000000000..0777d1651
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/emailWizard.xul
@@ -0,0 +1,493 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountCreation.css"
+ type="text/css"?>
+
+<!DOCTYPE window [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+ %brandDTD;
+ <!ENTITY % acDTD SYSTEM "chrome://messenger/locale/accountCreation.dtd">
+ %acDTD;
+]>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="autoconfigWizard"
+ windowtype="mail:autoconfig"
+ title="&autoconfigWizard.title;"
+ onload="gEmailConfigWizard.onLoad();"
+ onkeypress="gEmailConfigWizard.onKeyDown(event);"
+ onclose="gEmailConfigWizard.onWizardShutdown();"
+ onunload="gEmailConfigWizard.onWizardShutdown();"
+ >
+
+ <stringbundleset>
+ <stringbundle id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="strings"
+ src="chrome://messenger/locale/accountCreation.properties"/>
+ <stringbundle id="utilstrings"
+ src="chrome://messenger/locale/accountCreationUtil.properties"/>
+ <stringbundle id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"/>
+ </stringbundleset>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/util.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/accountConfig.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/emailWizard.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/sanitizeDatatypes.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/fetchhttp.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/readFromXML.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/guessConfig.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/verifyConfig.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/fetchConfig.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/createInBackend.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountcreation/MyBadCertHandler.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/accountUtils.js" />
+
+ <keyset id="mailKeys">
+ <key keycode="VK_ESCAPE" oncommand="window.close();"/>
+ </keyset>
+
+ <panel id="insecureserver-cleartext-panel" class="popup-panel">
+ <hbox>
+ <image class="insecureLarry"/>
+ <vbox flex="1">
+ <description class="title">&insecureServer.tooltip.title;</description>
+ <description class="details">
+ &insecureUnencrypted.description;</description>
+ </vbox>
+ </hbox>
+ </panel>
+ <panel id="insecureserver-selfsigned-panel" class="popup-panel">
+ <hbox>
+ <image class="insecureLarry"/>
+ <vbox flex="1">
+ <description class="title">&insecureServer.tooltip.title;</description>
+ <description class="details">
+ &insecureSelfSigned.description;</description>
+ </vbox>
+ </hbox>
+ </panel>
+ <panel id="secureserver-panel" class="popup-panel">
+ <hbox>
+ <image class="secureLarry"/>
+ <vbox flex="1">
+ <description class="title">&secureServer.description;</description>
+ </vbox>
+ </hbox>
+ </panel>
+
+ <tooltip id="insecureserver-cleartext">
+ <hbox>
+ <image class="insecureLarry"/>
+ <vbox>
+ <description class="title">&insecureServer.tooltip.title;</description>
+ <description class="details">
+ &insecureServer.tooltip.details;</description>
+ </vbox>
+ </hbox>
+ </tooltip>
+ <tooltip id="insecureserver-selfsigned">
+ <hbox>
+ <image class="insecureLarry"/>
+ <vbox>
+ <description class="title">&insecureServer.tooltip.title;</description>
+ <description class="details">
+ &insecureServer.tooltip.details;</description>
+ </vbox>
+ </hbox>
+ </tooltip>
+ <tooltip id="secureservertooltip">
+ <hbox>
+ <image class="secureLarry"/>
+ <description class="title">&secureServer.description;</description>
+ </hbox>
+ </tooltip>
+ <tooltip id="optional-password">
+ <description>&password.text;</description>
+ </tooltip>
+
+ <spacer id="fullwidth"/>
+
+ <vbox id="mastervbox" class="mastervbox" flex="1">
+ <grid id="initialSettings">
+ <columns>
+ <column/>
+ <column/>
+ <column/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label accesskey="&name.accesskey;"
+ class="autoconfigLabel"
+ value="&name.label;"
+ control="realname"/>
+ <textbox id="realname"
+ class="padded"
+ placeholder="&name.placeholder;"
+ oninput="gEmailConfigWizard.onInputRealname();"
+ onblur="gEmailConfigWizard.onBlurRealname();"/>
+ <hbox>
+ <description id="nametext" class="initialDesc">&name.text;</description>
+ <image id="nameerroricon"
+ hidden="true"
+ class="warningicon"/>
+ <description id="nameerror" class="errordescription" hidden="true"/>
+ </hbox>
+ </row>
+ <row align="center">
+ <label accesskey="&email.accesskey;"
+ class="autoconfigLabel"
+ value="&email.label;"
+ control="email"/>
+ <textbox id="email"
+ class="padded uri-element"
+ placeholder="&email.placeholder;"
+ oninput="gEmailConfigWizard.onInputEmail();"
+ onblur="gEmailConfigWizard.onBlurEmail();"/>
+ <hbox>
+ <image id="emailerroricon"
+ hidden="true"
+ class="warningicon"/>
+ <description id="emailerror" class="errordescription" hidden="true"/>
+ </hbox>
+ </row>
+ <row align="center">
+ <!-- this starts out as text so the emptytext shows, but then
+ changes to type=password once it's not empty -->
+ <label accesskey="&password.accesskey;"
+ class="autoconfigLabel"
+ value="&password.label;"
+ control="password"
+ tooltip="optional-password"/>
+ <textbox id="password"
+ class="padded"
+ placeholder="&password.placeholder;"
+ type="text"
+ oninput="gEmailConfigWizard.onInputPassword();"
+ onfocus="gEmailConfigWizard.onFocusPassword();"
+ onblur="gEmailConfigWizard.onBlurPassword();"/>
+ <hbox>
+ <image id="passworderroricon"
+ hidden="true"
+ class="warningicon"/>
+ <description id="passworderror" class="errordescription" hidden="true"/>
+ </hbox>
+ </row>
+ <row align="center" pack="start">
+ <label class="autoconfigLabel"/>
+ <checkbox id="remember_password"
+ label="&rememberPassword.label;"
+ accesskey="&rememberPassword.accesskey;"
+ checked="true"/>
+ </row>
+ </rows>
+ </grid>
+ <spacer flex="1" />
+
+ <hbox id="status_area" flex="1">
+ <vbox id="status_img_before" pack="start"/>
+ <description id="status_msg">&#160;</description>
+ <!-- Include 160 = nbsp, to make the element occupy the
+ full height, for at least one line. With a normal space,
+ it does not have sufficient height. -->
+ <vbox id="status_img_after" pack="start"/>
+ </hbox>
+
+ <groupbox id="result_area" hidden="true">
+ <radiogroup id="result_imappop" orient="horizontal">
+ <radio id="result_select_imap" label="&imapLong.label;" value="1"
+ oncommand="gEmailConfigWizard.onResultIMAPOrPOP3();"/>
+ <radio id="result_select_pop3" label="&pop3Long.label;" value="2"
+ oncommand="gEmailConfigWizard.onResultIMAPOrPOP3();"/>
+ </radiogroup>
+ <grid>
+ <columns>
+ <column/>
+ <column flex="1"/>
+ </columns>
+ <rows>
+ <row align="center">
+ <label class="textbox-label" value="&incoming.label;"
+ control="result-incoming"/>
+ <textbox id="result-incoming" disabled="true" flex="1"/>
+ </row>
+ <row align="center">
+ <label class="textbox-label" value="&outgoing.label;"
+ control="result-outgoing"/>
+ <textbox id="result-outgoing" disabled="true" flex="1"/>
+ </row>
+ <row align="center">
+ <label class="textbox-label" value="&username.label;"
+ control="result-username"/>
+ <textbox id="result-username" disabled="true" flex="1"/>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <groupbox id="manual-edit_area" hidden="true">
+ <grid>
+ <columns>
+ <column/><!-- row label, e.g. "incoming" -->
+ <column/><!-- protocol, e.g. "IMAP" -->
+ <column flex="1"/><!-- hostname / username -->
+ <column/><!-- port -->
+ <column/><!-- SSL -->
+ <column/><!-- auth method -->
+ </columns>
+ <rows>
+ <row id="labels_row" align="center">
+ <spacer/>
+ <spacer/>
+ <label value="&hostname.label;" class="columnHeader"/>
+ <label value="&port.label;" class="columnHeader"/>
+ <label value="&ssl.label;" class="columnHeader"/>
+ <label value="&auth.label;" class="columnHeader"/>
+ </row>
+ <row id="incoming_server_area">
+ <hbox align="center" pack="end">
+ <label class="textbox-label"
+ value="&incoming.label;"
+ control="incoming_hostname"/>
+ </hbox>
+ <menulist id="incoming_protocol"
+ oncommand="gEmailConfigWizard.onChangedProtocolIncoming();"
+ sizetopopup="always">
+ <menupopup>
+ <menuitem label="&imap.label;" value="1"/>
+ <menuitem label="&pop3.label;" value="2"/>
+ </menupopup>
+ </menulist>
+ <textbox id="incoming_hostname"
+ oninput="gEmailConfigWizard.onInputHostname();"
+ class="host uri-element"/>
+ <menulist id="incoming_port"
+ editable="true"
+ oninput="gEmailConfigWizard.onChangedPortIncoming();"
+ oncommand="gEmailConfigWizard.onChangedPortIncoming();"
+ class="port">
+ <menupopup/>
+ </menulist>
+ <menulist id="incoming_ssl"
+ class="security"
+ oncommand="gEmailConfigWizard.onChangedSSLIncoming();"
+ sizetopopup="always">
+ <menupopup>
+ <!-- values defined in nsMsgSocketType -->
+ <menuitem label="&autodetect.label;" value="0"/>
+ <menuitem label="&noEncryption.label;" value="1"/>
+ <menuitem label="&starttls.label;" value="3"/>
+ <menuitem label="&sslTls.label;" value="2"/>
+ </menupopup>
+ </menulist>
+ <menulist id="incoming_authMethod"
+ class="auth"
+ oncommand="gEmailConfigWizard.onChangedInAuth();"
+ sizetopopup="always">
+ <menupopup>
+ <menuitem label="&autodetect.label;" value="0"/>
+ <!-- values defined in nsMsgAuthMethod -->
+ <!-- labels set from messenger.properties
+ to avoid duplication -->
+ <menuitem id="in-authMethod-password-cleartext" value="3"/>
+ <menuitem id="in-authMethod-password-encrypted" value="4"/>
+ <menuitem id="in-authMethod-kerberos" value="5"/>
+ <menuitem id="in-authMethod-ntlm" value="6"/>
+ <menuitem id="in-authMethod-oauth2" value="10" hidden="true"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row id="outgoing_server_area" align="center">
+ <label class="textbox-label"
+ value="&outgoing.label;"
+ control="outgoing_hostname"/>
+ <label id="outgoing_protocol"
+ value="&smtp.label;"/>
+ <menulist id="outgoing_hostname"
+ editable="true"
+ sizetopopup="none"
+ oninput="gEmailConfigWizard.onInputHostname();"
+ oncommand="gEmailConfigWizard.onChangedOutgoingDropdown();"
+ onpopupshowing="gEmailConfigWizard.onOpenOutgoingDropdown();"
+ class="host uri-element">
+ <menupopup id="outgoing_hostname_popup"/>
+ </menulist>
+ <menulist id="outgoing_port"
+ editable="true"
+ oninput="gEmailConfigWizard.onChangedPortOutgoing();"
+ oncommand="gEmailConfigWizard.onChangedPortOutgoing();"
+ class="port">
+ <menupopup/>
+ </menulist>
+ <menulist id="outgoing_ssl"
+ class="security"
+ oncommand="gEmailConfigWizard.onChangedSSLOutgoing();"
+ sizetopopup="always">
+ <menupopup>
+ <!-- @see incoming -->
+ <menuitem label="&autodetect.label;" value="0"/>
+ <menuitem label="&noEncryption.label;" value="1"/>
+ <menuitem label="&starttls.label;" value="3"/>
+ <menuitem label="&sslTls.label;" value="2"/>
+ </menupopup>
+ </menulist>
+ <menulist id="outgoing_authMethod"
+ class="auth"
+ oncommand="gEmailConfigWizard.onChangedOutAuth(this.selectedItem);"
+ sizetopopup="always">
+ <menupopup>
+ <menuitem label="&autodetect.label;" value="0"/>
+ <!-- @see incoming -->
+ <menuitem id="out-authMethod-no" value="1"/>
+ <menuitem id="out-authMethod-password-cleartext" value="3"/>
+ <menuitem id="out-authMethod-password-encrypted" value="4"/>
+ <menuitem id="out-authMethod-kerberos" value="5"/>
+ <menuitem id="out-authMethod-ntlm" value="6"/>
+ <menuitem id="out-authMethod-oauth2" value="10" hidden="true"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row id="username_area" align="center">
+ <label class="textbox-label"
+ value="&username.label;"/>
+ <label class="columnHeader"
+ value="&incoming.label;"
+ control="incoming_username"/>
+ <textbox id="incoming_username"
+ oninput="gEmailConfigWizard.onInputInUsername();"
+ class="username"/>
+ <spacer/>
+ <label class="columnHeader"
+ id="outgoing_label"
+ value="&outgoing.label;"
+ control="outgoing_username"/>
+ <textbox id="outgoing_username"
+ oninput="gEmailConfigWizard.onInputOutUsername();"
+ class="username"/>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <spacer flex="1" />
+ <hbox id="buttons_area">
+ <hbox id="left_buttons_area" align="center" pack="start">
+ <button id="provisioner_button"
+ label="&switch-to-provisioner.label;"
+ accesskey="&switch-to-provisioner.accesskey;"
+ class="larger-button"
+ oncommand="gEmailConfigWizard.onSwitchToProvisioner();"/>
+ <button id="manual-edit_button"
+ label="&manual-edit.label;"
+ accesskey="&manual-edit.accesskey;"
+ hidden="true"
+ oncommand="gEmailConfigWizard.onManualEdit();"/>
+ <button id="advanced-setup_button"
+ label="&advancedSetup.label;"
+ accesskey="&advancedSetup.accesskey;"
+ disabled="true"
+ hidden="true"
+ oncommand="gEmailConfigWizard.onAdvancedSetup();"/>
+ </hbox>
+ <spacer flex="1"/>
+ <hbox id="right_buttons_area" align="center" pack="end">
+ <button id="stop_button"
+ label="&stop.label;"
+ accesskey="&stop.accesskey;"
+ hidden="true"
+ oncommand="gEmailConfigWizard.onStop();"/>
+ <button id="cancel_button"
+ label="&cancel.label;"
+ accesskey="&cancel.accesskey;"
+ oncommand="gEmailConfigWizard.onCancel();"/>
+ <button id="half-manual-test_button"
+ label="&half-manual-test.label;"
+ accesskey="&half-manual-test.accesskey;"
+ hidden="true"
+ oncommand="gEmailConfigWizard.onHalfManualTest();"/>
+ <button id="next_button"
+ label="&continue.label;"
+ accesskey="&continue.accesskey;"
+ hidden="false"
+ oncommand="gEmailConfigWizard.onNext();"/>
+ <button id="create_button"
+ label="&doneAccount.label;"
+ accesskey="&doneAccount.accesskey;"
+ class="important-button"
+ hidden="true"
+ oncommand="gEmailConfigWizard.onCreate();"/>
+ </hbox>
+ </hbox>
+ </vbox>
+
+
+ <vbox id="warningbox" hidden="true" flex="1">
+ <hbox class="warning" flex="1">
+ <vbox class="larrybox">
+ <image id="insecure_larry" class="insecureLarry"/>
+ </vbox>
+ <vbox flex="1" class="warning_text">
+ <label class="warning-heading">&warning.label;</label>
+ <vbox id="incoming_box">
+ <hbox>
+ <label class="warning_settings" value="&incomingSettings.label;"/>
+ <description id="warning_incoming"/>
+ </hbox>
+ <label id="incoming_technical"
+ class="technical_details"
+ value="&technicaldetails.label;"
+ onclick="gSecurityWarningDialog.toggleDetails('incoming');"/>
+ <description id="incoming_details" collapsed="true"/>
+ </vbox>
+ <vbox id="outgoing_box">
+ <hbox>
+ <label class="warning_settings" value="&outgoingSettings.label;"/>
+ <description id="warning_outgoing"/>
+ </hbox>
+ <label id="outgoing_technical"
+ class="technical_details"
+ value="&technicaldetails.label;"
+ onclick="gSecurityWarningDialog.toggleDetails('outgoing');"/>
+ <description id="outgoing_details" collapsed="true"/>
+ </vbox>
+ <spacer flex="10"/>
+ <description id="findoutmore">
+ &contactYourProvider.description;</description>
+ <spacer flex="100"/>
+ <checkbox id="acknowledge_warning"
+ label="&confirmWarning.label;"
+ accesskey="&confirmWarning.accesskey;"
+ class="acknowledge_checkbox"
+ oncommand="gSecurityWarningDialog.toggleAcknowledge()"/>
+ <hbox>
+ <button id="getmeoutofhere"
+ label="&changeSettings.label;"
+ accesskey="&changeSettings.accesskey;"
+ oncommand="gSecurityWarningDialog.onCancel()"/>
+ <spacer flex="1"/>
+ <button id="iknow"
+ label="&doneAccount.label;"
+ accesskey="&doneAccount.accesskey;"
+ disabled="true"
+ oncommand="gSecurityWarningDialog.onOK()"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ </vbox>
+</window>
diff --git a/mailnews/base/prefs/content/accountcreation/fetchConfig.js b/mailnews/base/prefs/content/accountcreation/fetchConfig.js
new file mode 100644
index 000000000..07a9f5586
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/fetchConfig.js
@@ -0,0 +1,240 @@
+/* -*- Mode: Java; 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/. */
+
+/**
+ * Tries to find a configuration for this ISP on the local harddisk, in the
+ * application install directory's "isp" subdirectory.
+ * Params @see fetchConfigFromISP()
+ */
+
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/JXON.js");
+
+
+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(sanitize.hostname(domain) + ".xml");
+
+ var contents =
+ readURLasUTF8(Services.io.newFileURI(configLocation));
+ let domParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+ successCallback(readFromXML(JXON.build(
+ domParser.parseFromString(contents, "text/xml"))));
+ } 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 paramter will be an exception object or error string.
+ */
+function fetchConfigFromISP(domain, emailAddress, successCallback,
+ errorCallback)
+{
+ if (!Services.prefs.getBoolPref(
+ "mailnews.auto_config.fetchFromISP.enabled")) {
+ errorCallback("ISP fetch disabled per user preference");
+ return;
+ }
+
+ let url1 = "http://autoconfig." + sanitize.hostname(domain) +
+ "/mail/config-v1.1.xml";
+ // .well-known/ <http://tools.ietf.org/html/draft-nottingham-site-meta-04>
+ let url2 = "http://" + sanitize.hostname(domain) +
+ "/.well-known/autoconfig/mail/config-v1.1.xml";
+ let sucAbortable = new SuccessiveAbortable();
+ var time = Date.now();
+ var urlArgs = { emailaddress: emailAddress };
+ if (!Services.prefs.getBoolPref(
+ "mailnews.auto_config.fetchFromISP.sendEmailAddress")) {
+ delete urlArgs.emailaddress;
+ }
+ let fetch1 = new FetchHTTP(url1, urlArgs, false,
+ function(result)
+ {
+ successCallback(readFromXML(result));
+ },
+ function(e1) // fetch1 failed
+ {
+ ddump("fetchisp 1 <" + url1 + "> took " + (Date.now() - time) +
+ "ms and failed with " + e1);
+ time = Date.now();
+ if (e1 instanceof CancelledException)
+ {
+ errorCallback(e1);
+ return;
+ }
+
+ let fetch2 = new FetchHTTP(url2, urlArgs, false,
+ function(result)
+ {
+ successCallback(readFromXML(result));
+ },
+ function(e2)
+ {
+ ddump("fetchisp 2 <" + url2 + "> took " + (Date.now() - time) +
+ "ms and failed with " + e2);
+ // return the error for the primary call,
+ // unless the fetch was cancelled
+ errorCallback(e2 instanceof CancelledException ? e2 : e1);
+ });
+ sucAbortable.current = fetch2;
+ fetch2.start();
+ });
+ sucAbortable.current = fetch1;
+ fetch1.start();
+ return sucAbortable;
+}
+
+/**
+ * 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");
+ domain = sanitize.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);
+ url = url.replace("{{accounts}}", MailServices.accounts.accounts.length);
+
+ if (!url.length)
+ return errorCallback("no fetch url set");
+ let fetch = new FetchHTTP(url, null, false,
+ function(result)
+ {
+ successCallback(readFromXML(result));
+ },
+ errorCallback);
+ fetch.start();
+ return fetch;
+}
+
+/**
+ * Does a lookup of DNS MX, to get the server who is responsible for
+ * recieving mail for this domain. Then it takes the domain of that
+ * server, and does another lookup (in ISPDB and possible 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 mechnism 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)
+{
+ domain = sanitize.hostname(domain);
+
+ var sucAbortable = new SuccessiveAbortable();
+ var time = Date.now();
+ sucAbortable.current = getMX(domain,
+ function(mxHostname) // success
+ {
+ ddump("getmx took " + (Date.now() - time) + "ms");
+ let sld = Services.eTLD.getBaseDomainFromHost(mxHostname);
+ ddump("base domain " + sld + " for " + mxHostname);
+ if (sld == domain)
+ {
+ errorCallback("MX lookup would be no different from domain");
+ return;
+ }
+ sucAbortable.current = fetchConfigFromDB(sld, successCallback,
+ errorCallback);
+ },
+ errorCallback);
+ return sucAbortable;
+}
+
+/**
+ * Queries the DNS MX for the domain
+ *
+ * The current implementation goes to a web service to do the
+ * DNS resolve for us, because Mozilla unfortunately has no implementation
+ * to do it. That's just a workaround. Once bug 545866 is fixed, we make
+ * the DNS query directly on the client. The API of this function should not
+ * change then.
+ *
+ * Returns (in successCallback) the hostname of the MX server.
+ * If there are several entires with different preference values,
+ * only the most preferred (i.e. those with the lowest value)
+ * is returned. If there are several most preferred servers (i.e.
+ * round robin), only one of them is returned.
+ *
+ * @param domain @see fetchConfigFromISP()
+ * @param successCallback {function(hostname {String})
+ * Called when we found an MX for the domain.
+ * For |hostname|, see description above.
+ * @param errorCallback @see fetchConfigFromISP()
+ * @returns @see fetchConfigFromISP()
+ */
+function getMX(domain, successCallback, errorCallback)
+{
+ domain = sanitize.hostname(domain);
+
+ let url = Services.prefs.getCharPref("mailnews.mx_service_url");
+ if (!url)
+ errorCallback("no URL for MX service configured");
+ url += domain;
+
+ let fetch = new FetchHTTP(url, null, false,
+ function(result)
+ {
+ // result is plain text, with one line per server.
+ // So just take the first line
+ ddump("MX query result: \n" + result + "(end)");
+ assert(typeof(result) == "string");
+ let first = result.split("\n")[0];
+ first.toLowerCase().replace(/[^a-z0-9\-_\.]*/g, "");
+ if (first.length == 0)
+ {
+ errorCallback("no MX found");
+ return;
+ }
+ successCallback(first);
+ },
+ errorCallback);
+ fetch.start();
+ return fetch;
+}
diff --git a/mailnews/base/prefs/content/accountcreation/fetchhttp.js b/mailnews/base/prefs/content/accountcreation/fetchhttp.js
new file mode 100644
index 000000000..04f5272cd
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/fetchhttp.js
@@ -0,0 +1,267 @@
+/* -*- Mode: Java; 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/. */
+
+/**
+ * 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.
+ */
+
+Components.utils.import("resource://gre/modules/JXON.js");
+
+/**
+ * Set up a fetch.
+ *
+ * @param url {String} URL of the server function.
+ * ATTENTION: The caller needs to make sure that the URL is secure to call.
+ * @param urlArgs {Object, associative array} 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 post {Boolean} 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.
+ *
+ * @param successCallback {Function(result {String})}
+ * Called when the server call worked (no errors).
+ * |result| will contain the body of the HTTP reponse, as string.
+ * @param errorCallback {Function(ex)}
+ * 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
+ */
+/* not yet supported:
+ * @param headers {Object, associative array} Like urlArgs,
+ * just that the params will be added as HTTP headers.
+ * { foo: "blub blub" } will add "Foo: Blub blub"
+ * The values will be urlComponentEncoded, apart from space,
+ * so pass them unencoded.
+ * @param headerArgs {Object, associative array} Like urlArgs,
+ * just that the params will be added as HTTP headers.
+ * { foo: "blub blub" } will add "X-Moz-Arg-Foo: Blub blub"
+ * The values will be urlComponentEncoded, apart from space,
+ * so pass them unencoded.
+ * @param bodyArgs {Object, associative array} 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 uploadbody {Object} Arbitrary object, which to use as
+ * body of the HTTP request. Will also set the mimetype accordingly.
+ * Only supported object types, currently only E4X is supported
+ * (sending XML).
+ * Usually, you have nothing to upload, so just pass |null|.
+ */
+function FetchHTTP(url, urlArgs, post, successCallback, errorCallback)
+{
+ assert(typeof(successCallback) == "function", "BUG: successCallback");
+ assert(typeof(errorCallback) == "function", "BUG: errorCallback");
+ this._url = sanitize.string(url);
+ if (!urlArgs)
+ urlArgs = {};
+
+ this._urlArgs = urlArgs;
+ this._post = sanitize.boolean(post);
+ this._successCallback = successCallback;
+ this._errorCallback = errorCallback;
+}
+FetchHTTP.prototype =
+{
+ __proto__: Abortable.prototype,
+ _url : null, // URL as passed to ctor, without arguments
+ _urlArgs : null,
+ _post : null,
+ _successCallback : null,
+ _errorCallback : null,
+ _request : null, // the XMLHttpRequest object
+ result : null,
+
+ start : function()
+ {
+ var url = this._url;
+ for (var name in this._urlArgs)
+ {
+ url += (!url.includes("?") ? "?" : "&") +
+ name + "=" + encodeURIComponent(this._urlArgs[name]);
+ }
+ this._request = new XMLHttpRequest();
+ let request = this._request;
+ request.open(this._post ? "POST" : "GET", url);
+ request.channel.loadGroup = null;
+ // 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"); }
+
+ var me = this;
+ request.onload = function() { me._response(true); }
+ request.onerror = function() { me._response(false); }
+ request.send(null);
+ },
+ _response : function(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")
+ {
+ this.result = JXON.build(this._request.responseXML);
+ }
+ else
+ {
+ //ddump("mimetype: " + mimetype + " only supported as text");
+ this.result = this._request.responseText;
+ }
+ //ddump("result:\n" + this.result);
+ }
+ catch (e)
+ {
+ success = false;
+ errorStr = getStringBundle(
+ "chrome://messenger/locale/accountCreationUtil.properties")
+ .GetStringFromName("bad_response_content.error");
+ errorCode = -4;
+ }
+ }
+ else
+ {
+ success = false;
+ try
+ {
+ errorCode = this._request.status;
+ errorStr = this._request.statusText;
+ } catch (e) {
+ // If we can't resolve the hostname in DNS etc., .statusText throws
+ errorCode = -2;
+ errorStr = getStringBundle(
+ "chrome://messenger/locale/accountCreationUtil.properties")
+ .GetStringFromName("cannot_contact_server.error");
+ ddump(errorStr);
+ }
+ }
+
+ // Callbacks
+ if (success)
+ {
+ try {
+ this._successCallback(this.result);
+ } catch (e) {
+ logException(e);
+ this._error(e);
+ }
+ }
+ else if (exStored)
+ this._error(exStored);
+ else
+ this._error(new ServerException(errorStr, errorCode, this._url));
+
+ if (this._finishedCallback)
+ {
+ try {
+ this._finishedCallback(this);
+ } catch (e) {
+ logException(e);
+ this._error(e);
+ }
+ }
+
+ } catch (e) {
+ // error in our fetchhttp._response() code
+ logException(e);
+ this._error(e);
+ }
+ },
+ _error : function(e)
+ {
+ try {
+ this._errorCallback(e);
+ } catch (e) {
+ // error in errorCallback, too!
+ logException(e);
+ alertPrompt("Error in errorCallback for fetchhttp", e);
+ }
+ },
+ /**
+ * Call this between start() and finishedCallback fired.
+ */
+ cancel : function(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 : function(finishedCallback)
+ {
+ this._finishedCallback = finishedCallback;
+ }
+}
+
+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;
+
+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/mailnews/base/prefs/content/accountcreation/guessConfig.js b/mailnews/base/prefs/content/accountcreation/guessConfig.js
new file mode 100644
index 000000000..755c499cd
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/guessConfig.js
@@ -0,0 +1,1145 @@
+/* -*- Mode: Java; 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/. */
+
+Cu.import("resource:///modules/gloda/log4moz.js");
+Cu.import("resource://gre/modules/Services.jsm");
+
+var TIMEOUT = 10; // in seconds
+
+// This is a bit ugly - we set outgoingDone to false
+// when emailWizard.js cancels the outgoing probe because the user picked
+// an outoing server. It does this by poking the probeAbortable object,
+// so we need outgoingDone to have global scope.
+var outgoingDone = false;
+
+/**
+ * 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, ssl, done)}
+ * Called when we try a new hostname/port.
+ * type {String-enum} @see AccountConfig type - "imap", "pop3", "smtp"
+ * hostname {String}
+ * port {Integer}
+ * socketType {Integer-enum} @see AccountConfig.incoming.socketType
+ * 1 = plain, 2 = SSL, 3 = STARTTLS
+ * 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 null;
+ }
+
+ if (!resultConfig)
+ resultConfig = new AccountConfig();
+ resultConfig.source = AccountConfig.kSourceGuess;
+
+ if (!Services.prefs.getBoolPref(
+ "mailnews.auto_config.guess.enabled")) {
+ errorCallback("Guessing config disabled per user preference");
+ return;
+ }
+
+ 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)
+ {
+ resultConfig.source = 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 = 3; // starttls
+ resultConfig.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext;
+ resultConfig.outgoing.hostname = "smtp." + domain;
+ resultConfig.outgoing.socketType = 1;
+ 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: 3,
+ auth: Ci.nsMsgAuthMethod.passwordCleartext
+ });
+ successCallback(resultConfig);
+ return null;
+ }
+ var progress = function(thisTry)
+ {
+ progressCallback(protocolToString(thisTry.protocol), thisTry.hostname,
+ thisTry.port, sslConvertToSocketType(thisTry.ssl), false,
+ resultConfig);
+ };
+
+ var updateConfig = function(config)
+ {
+ resultConfig = config;
+ };
+
+ var errorInCallback = function(e)
+ {
+ // The caller's errorCallback threw.
+ // hopefully shouldn't happen for users.
+ alertPrompt("Error in errorCallback for guessConfig()", e);
+ };
+
+ var checkDone = function()
+ {
+ if (incomingEx)
+ {
+ try {
+ errorCallback(incomingEx, resultConfig);
+ } catch (e) { errorInCallback(e); }
+ return;
+ }
+ if (outgoingEx)
+ {
+ try {
+ errorCallback(outgoingEx, resultConfig);
+ } catch (e) { errorInCallback(e); }
+ return;
+ }
+ if (incomingDone && outgoingDone)
+ {
+ try {
+ successCallback(resultConfig);
+ } catch (e) {
+ try {
+ errorCallback(e);
+ } catch (e) { errorInCallback(e); }
+ }
+ return;
+ }
+ };
+
+ var logger = Log4Moz.getConfiguredLogger("mail.wizard");
+ var HostTryToAccountServer = function(thisTry, server)
+ {
+ server.type = protocolToString(thisTry.protocol);
+ server.hostname = thisTry.hostname;
+ server.port = thisTry.port;
+ server.socketType = sslConvertToSocketType(thisTry.ssl);
+ server.auth = 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 emailWizard.js.
+ server.badCert = thisTry.selfSignedCert;
+ server.targetSite = thisTry.targetSite;
+ logger.info("CHOOSING " + server.type + " "+ server.hostname + ":" +
+ server.port + ", auth method " + server.auth + " " +
+ server.authAlternatives.join(",") + ", SSL " + 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);
+ }
+ if (which == "outgoing" || which == "both")
+ {
+ outgoingHostDetector.start(resultConfig.outgoing.hostname ?
+ resultConfig.outgoing.hostname : domain,
+ !!resultConfig.outgoing.hostname, "smtp",
+ resultConfig.outgoing.port, resultConfig.outgoing.socketType);
+ }
+
+ return new GuessAbortable(incomingHostDetector, outgoingHostDetector,
+ updateConfig);
+}
+
+function GuessAbortable(incomingHostDetector, outgoingHostDetector,
+ updateConfig)
+{
+ Abortable.call(this);
+ this._incomingHostDetector = incomingHostDetector;
+ this._outgoingHostDetector = outgoingHostDetector;
+ this._updateConfig = updateConfig;
+}
+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 and ssl are defined towards the end
+ * of this file and not the same as those used in AccountConfig (type,
+ * socketType). (fix this)
+ */
+function HostTry()
+{
+}
+HostTry.prototype =
+{
+ // IMAP, POP or SMTP
+ protocol : UNKNOWN,
+ // {String}
+ hostname : undefined,
+ // {Integer}
+ port : undefined,
+ // NONE, SSL or TLS
+ ssl : 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 = new Array();
+
+ // init logging
+ this._log = Log4Moz.getConfiguredLogger("mail.wizard");
+ this._log.info("created host detector");
+}
+
+HostDetector.prototype =
+{
+ cancel : function(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 domain {String} 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 hostIsPrecise {Boolean} (default false) use only this hostname,
+ * do not guess hostnames.
+ * @param type {String-enum}@see AccountConfig type
+ * (Optional. default, 0, undefined, null = guess it)
+ * @param port {Integer} (Optional. default, 0, undefined, null = guess it)
+ * @param socketType {Integer-enum}@see AccountConfig socketType
+ * (Optional. default, 0, undefined, null = guess it)
+ */
+ start : function(domain, hostIsPrecise, type, port, socketType)
+ {
+ domain = domain.replace(/\s*/g, ""); // Remove whitespace
+ if (!hostIsPrecise)
+ hostIsPrecise = false;
+ var protocol = sanitize.translate(type,
+ { "imap" : IMAP, "pop3" : POP, "smtp" : SMTP }, UNKNOWN);
+ if (!port)
+ port = UNKNOWN;
+ var ssl = ConvertSocketTypeToSSL(socketType);
+ this._cancel = false;
+ this._log.info("doing auto detect for protocol " + protocol +
+ ", domain " + domain + ", (exactly: " + hostIsPrecise +
+ "), port " + port + ", ssl " + ssl);
+
+ // 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, ssl, port);
+ for (let j = 0; j < hostEntries.length; j++)
+ {
+ let hostTry = hostEntries[j]; // from getHostEntry()
+ hostTry.hostname = hostname;
+ hostTry.status = kNotTried;
+ 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 : function()
+ {
+ if (this._cancel)
+ return;
+ var me = this;
+ for (let i = 0; i < this._hostsToTry.length; i++)
+ {
+ let thisTry = this._hostsToTry[i]; // {HostTry}
+ if (thisTry.status != kNotTried)
+ continue;
+ this._log.info("poking at " + thisTry.hostname + " port " +
+ thisTry.port + " ssl "+ thisTry.ssl + " protocol " +
+ protocolToString(thisTry.protocol));
+ if (i == 0) // showing 50 servers at once is pointless
+ this.mProgressCallback(thisTry);
+
+ thisTry.abortable = SocketUtil(
+ thisTry.hostname, thisTry.port, thisTry.ssl,
+ thisTry.commands, TIMEOUT,
+ new SSLErrorHandler(thisTry, this._log),
+ function(wiredata) // result callback
+ {
+ if (me._cancel)
+ return; // don't use response anymore
+ me.mProgressCallback(thisTry);
+ me._processResult(thisTry, wiredata);
+ me._checkFinished();
+ },
+ function(e) // error callback
+ {
+ if (me._cancel)
+ return; // who set cancel to true already called mErrorCallback()
+ me._log.warn(e);
+ thisTry.status = kFailed;
+ me._checkFinished();
+ });
+ thisTry.status = kOngoing;
+ }
+ },
+
+ /**
+ * @param thisTry {HostTry}
+ * @param wiredata {Array of {String}} what the server returned
+ * in response to our protocol chat
+ */
+ _processResult : function(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("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("no data");
+ thisTry.status = kFailed;
+ return;
+ }
+ this._log.info("wiredata: " + wiredata.join(""));
+ thisTry.authMethods =
+ this._advertisesAuthMethods(thisTry.protocol, wiredata);
+ if (thisTry.ssl == TLS && !this._hasTLS(thisTry, wiredata))
+ {
+ this._log.info("STARTTLS wanted, but not offered");
+ thisTry.status = kFailed;
+ return;
+ }
+ this._log.info("success with " + thisTry.hostname + ":" +
+ thisTry.port + " " + protocolToString(thisTry.protocol) +
+ " ssl " + thisTry.ssl +
+ (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("clearing validity override for " + thisTry.hostname);
+ Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService)
+ .clearValidityOverride(thisTry.hostname, thisTry.port);
+ }
+ },
+
+ _checkFinished : function()
+ {
+ 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;
+ // thisTry is good, and all higher preference tries failed, so use this
+ else if (thisTry.status == kSuccess && !unfinishedBusiness)
+ {
+ 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 protocol {Integer-enum} IMAP, POP or SMTP
+ * @param capaResponse {Array of {String}} on the wire data
+ * that the server returned. May be the full exchange or just capa.
+ * @returns {Array of {Integer-enum} values for AccountConfig.incoming.auth
+ * (or outgoing), in decreasing order of preference.
+ * E.g. [ 5, 4 ] for a server that supports only Kerberos and
+ * encrypted passwords.
+ */
+ _advertisesAuthMethods : function(protocol, capaResponse)
+ {
+ // for imap, capabilities include e.g.:
+ // "AUTH=CRAM-MD5", "AUTH=NTLM", "AUTH=GSSAPI", "AUTH=MSN"
+ // 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 result = new Array();
+ 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))
+ result.push(Ci.nsMsgAuthMethod.GSSAPI);
+ if (new RegExp(prefix + "CRAM-MD5").test(line))
+ result.push(Ci.nsMsgAuthMethod.passwordEncrypted);
+ if (new RegExp(prefix + "(NTLM|MSN)").test(line))
+ result.push(Ci.nsMsgAuthMethod.NTLM);
+ if (protocol != IMAP || !line.includes("LOGINDISABLED"))
+ result.push(Ci.nsMsgAuthMethod.passwordCleartext);
+ return result;
+ },
+
+ _hasTLS : function(thisTry, wiredata)
+ {
+ var capa = thisTry.protocol == POP ? "STLS" : "STARTTLS";
+ return thisTry.ssl == TLS &&
+ wiredata.join("").toUpperCase().includes(capa);
+ },
+}
+
+/**
+ * @param authMethods @see return value of _advertisesAuthMethods()
+ * Note: the returned auth method will be removed from the array.
+ * @return 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 : function(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 : function(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 = 0; // no encryption
+//1 would be "TLS if available"
+var TLS = 2; // STARTTLS
+var SSL = 3; // SSL / TLS
+
+var IMAP_PORTS = {}
+IMAP_PORTS[NONE] = 143;
+IMAP_PORTS[TLS] = 143;
+IMAP_PORTS[SSL] = 993;
+
+var POP_PORTS = {}
+POP_PORTS[NONE] = 110;
+POP_PORTS[TLS] = 110;
+POP_PORTS[SSL] = 995;
+
+var SMTP_PORTS = {}
+SMTP_PORTS[NONE] = 587;
+SMTP_PORTS[TLS] = 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 __sortByPreference(a, b)
+ {
+ // -1 = a is better; 1 = b is better; 0 = equal
+ // Prefer SSL/TLS above all else
+ if (a.ssl != NONE && b.ssl == NONE)
+ return -1;
+ if (b.ssl != NONE && a.ssl == 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;
+ // For hostnames, leave existing sorting, as in _hostnamesToTry()
+ // For ports, leave existing sorting, as in getOutgoingTryOrder()
+ return 0;
+ });
+};
+
+// TODO prefer SSL over STARTTLS,
+// either in sortTriesByPreference or in getIncomingTryOrder() (and outgoing)
+
+/**
+ * @returns {Array of {HostTry}}
+ */
+function getIncomingTryOrder(host, protocol, ssl, 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 (ssl == UNKNOWN)
+ return [getHostEntry(protocol, TLS, port),
+ //getHostEntry(protocol, SSL, port),
+ getHostEntry(protocol, NONE, port)];
+ return [getHostEntry(protocol, ssl, port)];
+ }
+ if (ssl == UNKNOWN)
+ return [getHostEntry(IMAP, TLS, port),
+ //getHostEntry(IMAP, SSL, port),
+ getHostEntry(POP, TLS, port),
+ //getHostEntry(POP, SSL, port),
+ getHostEntry(IMAP, NONE, port),
+ getHostEntry(POP, NONE, port)];
+ return [getHostEntry(IMAP, ssl, port),
+ getHostEntry(POP, ssl, port)];
+};
+
+/**
+ * @returns {Array of {HostTry}}
+ */
+function getOutgoingTryOrder(host, protocol, ssl, port)
+{
+ assert(protocol == SMTP, "need SMTP as protocol for outgoing");
+ if (ssl == UNKNOWN)
+ {
+ if (port == UNKNOWN)
+ // neither SSL nor port known
+ return [getHostEntry(SMTP, TLS, UNKNOWN),
+ getHostEntry(SMTP, TLS, 25),
+ //getHostEntry(SMTP, SSL, UNKNOWN),
+ getHostEntry(SMTP, NONE, UNKNOWN),
+ getHostEntry(SMTP, NONE, 25)];
+ // port known, SSL not
+ return [getHostEntry(SMTP, TLS, port),
+ //getHostEntry(SMTP, SSL, port),
+ getHostEntry(SMTP, NONE, port)];
+ }
+ // SSL known, port not
+ if (port == UNKNOWN)
+ {
+ if (ssl == SSL)
+ return [getHostEntry(SMTP, SSL, UNKNOWN)];
+ else // TLS or NONE
+ return [getHostEntry(SMTP, ssl, UNKNOWN),
+ getHostEntry(SMTP, ssl, 25)];
+ }
+ // SSL and port known
+ return [getHostEntry(SMTP, ssl, port)];
+};
+
+/**
+ * @returns {HostTry} with proper default port and commands,
+ * but without hostname.
+ */
+function getHostEntry(protocol, ssl, port)
+{
+ if (!port || port == UNKNOWN) {
+ switch (protocol) {
+ case POP:
+ port = POP_PORTS[ssl];
+ break;
+ case IMAP:
+ port = IMAP_PORTS[ssl];
+ break;
+ case SMTP:
+ port = SMTP_PORTS[ssl];
+ break;
+ default:
+ throw new NotReached("unsupported protocol " + protocol);
+ }
+ }
+
+ var r = new HostTry();
+ r.protocol = protocol;
+ r.ssl = ssl;
+ r.port = port;
+ r.commands = CMDS[protocol];
+ return r;
+};
+
+
+// Convert consts from those used here to those from AccountConfig
+// TODO adjust consts to match AccountConfig
+
+// here -> AccountConfig
+function sslConvertToSocketType(ssl)
+{
+ if (ssl == NONE)
+ return 1;
+ if (ssl == SSL)
+ return 2;
+ if (ssl == TLS)
+ return 3;
+ throw new NotReached("unexpected SSL type");
+}
+
+// AccountConfig -> here
+function ConvertSocketTypeToSSL(socketType)
+{
+ if (socketType == 1)
+ return NONE;
+ if (socketType == 2)
+ return SSL;
+ if (socketType == 3)
+ return TLS;
+ return UNKNOWN;
+}
+
+// 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
+
+/**
+ * Called by MyBadCertHandler.js, which called by PSM
+ * to tell us about SSL certificate errors.
+ * @param thisTry {HostTry}
+ * @param logger {Log4Moz logger}
+ */
+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 : function(socketInfo, status, targetSite)
+ {
+ this._log.error("Got Cert error for "+ targetSite);
+
+ if (!status)
+ return true;
+
+ let cert = status.QueryInterface(Ci.nsISSLStatus).serverCert;
+ let flags = 0;
+
+ 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 custeromer 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 (status.isDomainMismatch) {
+ this._try._gotCertError = Ci.nsICertOverrideService.ERROR_MISMATCH;
+ flags |= Ci.nsICertOverrideService.ERROR_MISMATCH;
+ }
+ else if (status.isUntrusted) { // e.g. self-signed
+ this._try._gotCertError = Ci.nsICertOverrideService.ERROR_UNTRUSTED;
+ flags |= Ci.nsICertOverrideService.ERROR_UNTRUSTED;
+ }
+ else if (status.isNotValidAtThisTime) {
+ this._try._gotCertError = Ci.nsICertOverrideService.ERROR_TIME;
+ flags |= 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, flags,
+ true); // temporary override
+ this._log.warn("!! Overrode bad cert temporarily " + host + " " + port +
+ " flags=" + flags + "\n");
+ return true;
+ },
+}
+
+
+
+//////////////////////////////////////////////////////////////////
+// Socket Util
+
+
+/**
+ * @param hostname {String} The DNS hostname to connect to.
+ * @param port {Integer} The numberic port to connect to on the host.
+ * @param ssl {Integer} SSL, TLS 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 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, ssl, commands, timeout,
+ 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 = ssl == SSL ? "ssl" : (ssl == TLS ? "starttls" : null);
+ var transport = transportService.createTransport([socketTypeName],
+ ssl == NONE ? 0 : 1,
+ hostname, port, null);
+
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, timeout);
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, timeout);
+ try {
+ transport.securityCallbacks = new BadCertHandler(sslErrorHandler);
+ } catch (e) {
+ _error(e);
+ }
+ 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 : new Array(),
+ onStartRequest: function(request, context)
+ {
+ try {
+ initialized = true;
+ if (!aborted)
+ {
+ // Send the first request
+ let outputData = commands[index++];
+ outstream.write(outputData, outputData.length);
+ }
+ } catch (e) { _error(e); }
+ },
+ onStopRequest: function(request, context, status)
+ {
+ try {
+ instream.close();
+ outstream.close();
+ resultCallback(this.data.length ? this.data : null);
+ } catch (e) { _error(e); }
+ },
+ onDataAvailable: function(request, context, 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, -1, -1, 0, 0, false);
+ pump.asyncRead(dataListener, null);
+ 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(Components.results.NS_ERROR_ABORT);
+ } catch (e) {
+ ddump("canceling socket failed: " + e);
+ }
+}
+
diff --git a/mailnews/base/prefs/content/accountcreation/readFromXML.js b/mailnews/base/prefs/content/accountcreation/readFromXML.js
new file mode 100644
index 000000000..c7e796f5f
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/readFromXML.js
@@ -0,0 +1,238 @@
+/* -*- Mode: Java; 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/. */
+
+/**
+ * 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.
+ * @return AccountConfig object filled with the data from XML
+ */
+Components.utils.import("resource:///modules/hostnameUtils.jsm");
+
+function readFromXML(clientConfigXML)
+{
+ 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) + "\n");
+ var stringBundle = getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties");
+ throw stringBundle.GetStringFromName("no_emailProvider.error");
+ }
+ var xml = clientConfigXML.clientConfig.emailProvider;
+
+ var d = new AccountConfig();
+ d.source = AccountConfig.kSourceXML;
+
+ d.id = sanitize.hostname(xml["@id"]);
+ d.displayName = d.id;
+ try {
+ d.displayName = sanitize.label(xml.displayName);
+ } catch (e) { logException(e); }
+ for (var domain of xml.$domain)
+ {
+ try {
+ d.domains.push(sanitize.hostname(domain));
+ } catch (e) { logException(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 = sanitize.enum(iX["@type"], ["pop3", "imap", "nntp"]);
+ iO.hostname = sanitize.hostname(iX.hostname);
+ iO.port = sanitize.integerRange(iX.port, kMinPort, kMaxPort);
+ // We need a username even for Kerberos, need it even internally.
+ iO.username = sanitize.string(iX.username); // may be a %VARIABLE%
+
+ if ("password" in iX) {
+ d.rememberPassword = true;
+ iO.password = sanitize.string(iX.password);
+ }
+
+ for (let iXsocketType of array_or_undef(iX.$socketType))
+ {
+ try {
+ iO.socketType = sanitize.translate(iXsocketType,
+ { plain : 1, SSL: 2, STARTTLS: 3 });
+ break; // take first that we support
+ } catch (e) { exception = e; }
+ }
+ if (!iO.socketType)
+ throw exception ? exception : "need proper <socketType> in XML";
+ exception = null;
+
+ for (let iXauth of array_or_undef(iX.$authentication))
+ {
+ try {
+ iO.auth = sanitize.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;
+
+ // defaults are in accountConfig.js
+ if (iO.type == "pop3" && "pop3" in iX)
+ {
+ try {
+ if ("leaveMessagesOnServer" in iX.pop3)
+ iO.leaveMessagesOnServer =
+ sanitize.boolean(iX.pop3.leaveMessagesOnServer);
+ if ("daysToLeaveMessagesOnServer" in iX.pop3)
+ iO.daysToLeaveMessagesOnServer =
+ sanitize.integer(iX.pop3.daysToLeaveMessagesOnServer);
+ } catch (e) { logException(e); }
+ try {
+ if ("downloadOnBiff" in iX.pop3)
+ iO.downloadOnBiff = sanitize.boolean(iX.pop3.downloadOnBiff);
+ } catch (e) { logException(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")
+ {
+ var stringBundle = getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties");
+ throw stringBundle.GetStringFromName("outgoing_not_smtp.error");
+ }
+ oO.hostname = sanitize.hostname(oX.hostname);
+ oO.port = sanitize.integerRange(oX.port, kMinPort, kMaxPort);
+
+ for (let oXsocketType of array_or_undef(oX.$socketType))
+ {
+ try {
+ oO.socketType = sanitize.translate(oXsocketType,
+ { plain : 1, SSL: 2, STARTTLS: 3 });
+ break; // take first that we support
+ } catch (e) { exception = e; }
+ }
+ if (!oO.socketType)
+ throw exception ? exception : "need proper <socketType> in XML";
+ exception = null;
+
+ for (let oXauth of array_or_undef(oX.$authentication))
+ {
+ try {
+ oO.auth = sanitize.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 = sanitize.string(oX.username);
+
+ if ("password" in oX) {
+ d.rememberPassword = true;
+ oO.password = sanitize.string(oX.password);
+ }
+
+ try {
+ // defaults are in accountConfig.js
+ if ("addThisServer" in oX)
+ oO.addThisServer = sanitize.boolean(oX.addThisServer);
+ if ("useGlobalPreferredServer" in oX)
+ oO.useGlobalPreferredServer =
+ sanitize.boolean(oX.useGlobalPreferredServer);
+ } catch (e) { logException(e); }
+
+ // processed successfully, now add to result object
+ if (!d.outgoing.hostname) // first valid
+ d.outgoing = oO;
+ else
+ d.outgoingAlternatives.push(oO);
+ } catch (e) { logException(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 = new Array();
+ for (let inputField of array_or_undef(xml.$inputField))
+ {
+ try {
+ var fieldset =
+ {
+ varname : sanitize.alphanumdash(inputField["@key"]).toUpperCase(),
+ displayName : sanitize.label(inputField["@label"]),
+ exampleValue : sanitize.label(inputField.value)
+ };
+ d.inputFields.push(fieldset);
+ } catch (e) { logException(e); } // for now, don't throw,
+ // because we don't support custom fields yet anyways.
+ }
+
+ return d;
+}
diff --git a/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js b/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js
new file mode 100644
index 000000000..0f95f78d1
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js
@@ -0,0 +1,207 @@
+/* -*- Mode: Java; 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/. */
+
+/**
+ * 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.
+ */
+
+Components.utils.import("resource:///modules/hostnameUtils.jsm");
+
+var sanitize =
+{
+ integer : function(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 : function(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 : function(unchecked)
+ {
+ if (typeof(unchecked) == "boolean")
+ return unchecked;
+
+ if (unchecked == "true")
+ return true;
+
+ if (unchecked == "false")
+ return false;
+
+ throw new MalformedException("boolean.error", unchecked);
+ },
+
+ string : function(unchecked)
+ {
+ return String(unchecked);
+ },
+
+ nonemptystring : function(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 : function(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 : function(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 non-chrome URL that's safe to request.
+ */
+ url : function (unchecked)
+ {
+ var str = this.string(unchecked);
+ if (!str.startsWith("http") && !str.startsWith("https"))
+ throw new MalformedException("url_scheme.error", unchecked);
+
+ var uri;
+ try {
+ uri = Services.io.newURI(str, null, null);
+ 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 : function(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 : function(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 : function(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 = getStringBundle(
+ "chrome://messenger/locale/accountCreationUtil.properties");
+ var msg = stringBundle.GetStringFromName(msgID);
+ if (kDebug)
+ msg += " (bad value: " + new String(uncheckedBadValue) + ")";
+ Exception.call(this, msg);
+}
+MalformedException.prototype = Object.create(Exception.prototype);
+MalformedException.prototype.constructor = MalformedException;
+
diff --git a/mailnews/base/prefs/content/accountcreation/util.js b/mailnews/base/prefs/content/accountcreation/util.js
new file mode 100644
index 000000000..d867bfbe9
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/util.js
@@ -0,0 +1,304 @@
+/* -*- Mode: Java; 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/. */
+/**
+ * Some common, generic functions
+ */
+
+try {
+ var Cc = Components.classes;
+ var Ci = Components.interfaces;
+} catch (e) { ddump(e); } // if already declared, as in xpcshell-tests
+try {
+ var Cu = Components.utils;
+} catch (e) { ddump(e); }
+
+Cu.import("resource:///modules/errUtils.js");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function assert(test, errorMsg)
+{
+ if (!test)
+ throw new NotReached(errorMsg ? errorMsg :
+ "Programming bug. Assertion failed, see log.");
+}
+
+function makeCallback(obj, func)
+{
+ return function()
+ {
+ return func.apply(obj, arguments);
+ }
+}
+
+
+/**
+ * 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.
+ */
+function runAsync(func)
+{
+ setTimeout(func, 0);
+}
+
+
+/**
+ * @param uriStr {String}
+ * @result {nsIURI}
+ */
+function makeNSIURI(uriStr)
+{
+ return Services.io.newURI(uriStr, null, null);
+}
+
+
+/**
+ * Reads UTF8 data from a URL.
+ *
+ * @param uri {nsIURI} what you want to read
+ * @return {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");
+ try {
+ let chan = Services.io.newChannelFromURI2(uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_NORMAL,
+ 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 = new Object();
+ try {
+ while (is.readString(1024, strOut) != 0)
+ content += strOut.value;
+ // catch in outer try/catch
+ } finally {
+ is.close();
+ }
+
+ return content;
+ } catch (e) {
+ // TODO this has a numeric error message. We need to ship translations
+ // into human language.
+ throw e;
+ }
+}
+
+/**
+ * Takes a string (which is typically the content of a file,
+ * e.g. the result returned from readURLUTF8() ), and splits
+ * it into lines, and returns an array with one string per line
+ *
+ * Linebreaks are not contained in the result,,
+ * and all of \r\n, (Windows) \r (Mac) and \n (Unix) counts as linebreak.
+ *
+ * @param content {String} one long string with the whole file
+ * @return {Array of String} one string per line (no linebreaks)
+ */
+function splitLines(content)
+{
+ content = content.replace("\r\n", "\n");
+ content = content.replace("\r", "\n");
+ return content.split("\n");
+}
+
+/**
+ * @param bundleURI {String} chrome URL to properties file
+ * @return nsIStringBundle
+ */
+function getStringBundle(bundleURI)
+{
+ try {
+ return Services.strings.createBundle(bundleURI);
+ } catch (e) {
+ throw new Exception("Failed to get stringbundle URI <" + bundleURI +
+ ">. Error: " + e);
+ }
+}
+
+
+function Exception(msg)
+{
+ this._message = msg;
+
+ // get stack
+ try {
+ not.found.here += 1; // force a native exception ...
+ } catch (e) {
+ this.stack = e.stack; // ... to get the current stack
+ }
+}
+Exception.prototype =
+{
+ get message()
+ {
+ return this._message;
+ },
+ toString : function()
+ {
+ return this._message;
+ }
+}
+
+function NotReached(msg)
+{
+ Exception.call(this, msg); // call super constructor
+ logException(this);
+}
+// Make NotReached extend Exception.
+NotReached.prototype = Object.create(Exception.prototype);
+NotReached.prototype.constructor = NotReached;
+
+/**
+ * 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 : function()
+ {
+ }
+}
+
+/**
+ * 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, setTimeoutID); // 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, setIntervalID); // 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: function()
+ {
+ if (this._current)
+ this._current.cancel();
+ }
+}
+
+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 == true;
+ if (typeof(org) == "function")
+ return org;
+ if (typeof(org) != "object")
+ throw "can't copy objects of type " + typeof(org) + " yet";
+
+ //TODO still instanceof org != instanceof copy
+ //var result = new org.constructor();
+ var result = new Object();
+ if (typeof(org.length) != "undefined")
+ var result = new Array();
+ for (var prop in org)
+ result[prop] = deepCopy(org[prop]);
+ return result;
+}
+
+if (typeof gEmailWizardLogger == "undefined") {
+ Cu.import("resource:///modules/gloda/log4moz.js");
+ var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+}
+function ddump(text)
+{
+ gEmailWizardLogger.info(text);
+}
+
+function debugObject(obj, name, maxDepth, curDepth)
+{
+ if (curDepth == undefined)
+ curDepth = 0;
+ if (maxDepth != undefined && curDepth > maxDepth)
+ return "";
+
+ var result = "";
+ var i = 0;
+ for (let prop in obj)
+ {
+ i++;
+ try {
+ if (typeof(obj[prop]) == "object")
+ {
+ if (obj[prop] && obj[prop].length != undefined)
+ result += name + "." + prop + "=[probably array, length " +
+ obj[prop].length + "]\n";
+ else
+ result += name + "." + prop + "=[" + typeof(obj[prop]) + "]\n";
+ result += debugObject(obj[prop], name + "." + prop,
+ maxDepth, curDepth + 1);
+ }
+ else if (typeof(obj[prop]) == "function")
+ result += name + "." + prop + "=[function]\n";
+ else
+ result += name + "." + prop + "=" + obj[prop] + "\n";
+ } catch (e) {
+ result += name + "." + prop + "-> Exception(" + e + ")\n";
+ }
+ }
+ if (!i)
+ result += name + " is empty\n";
+ return result;
+}
+
+function alertPrompt(alertTitle, alertMsg)
+{
+ Services.prompt.alert(window, alertTitle, alertMsg);
+}
diff --git a/mailnews/base/prefs/content/accountcreation/verifyConfig.js b/mailnews/base/prefs/content/accountcreation/verifyConfig.js
new file mode 100644
index 000000000..a2afbdad8
--- /dev/null
+++ b/mailnews/base/prefs/content/accountcreation/verifyConfig.js
@@ -0,0 +1,347 @@
+/* -*- Mode: Java; 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/. */
+
+/**
+ * This checks a given config, by trying a real connection and login,
+ * with username and password.
+ *
+ * TODO
+ * - give specific errors, bug 555448
+ * - return a working |Abortable| to allow cancel
+ *
+ * @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 alter {boolean}
+ * Try other usernames and login schemes, until login works.
+ * Warning: Modifies |accountConfig|.
+ *
+ * This function is async.
+ * @param successCallback function(accountConfig)
+ * Called when we could guess the config.
+ * For accountConfig, see below.
+ * @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.
+ */
+
+Components.utils.import("resource:///modules/mailServices.js");
+Components.utils.import("resource://gre/modules/OAuth2Providers.jsm");
+
+if (typeof gEmailWizardLogger == "undefined") {
+ Cu.import("resource:///modules/gloda/log4moz.js");
+ var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+}
+
+function verifyConfig(config, alter, msgWindow, successCallback, errorCallback)
+{
+ ddump(debugObject(config, "config", 3));
+ assert(config instanceof AccountConfig,
+ "BUG: Arg 'config' needs to be an AccountConfig object");
+ assert(typeof(alter) == "boolean");
+ assert(typeof(successCallback) == "function");
+ assert(typeof(errorCallback) == "function");
+
+ if (MailServices.accounts.findRealServer(config.incoming.username,
+ config.incoming.hostname,
+ sanitize.enum(config.incoming.type,
+ ["pop3", "imap", "nntp"]),
+ config.incoming.port)) {
+ errorCallback("Incoming server exists");
+ return;
+ }
+
+ // incoming server
+ let inServer =
+ MailServices.accounts.createIncomingServer(config.incoming.username,
+ config.incoming.hostname,
+ sanitize.enum(config.incoming.type,
+ ["pop3", "imap", "nntp"]));
+ inServer.port = config.incoming.port;
+ inServer.password = config.incoming.password;
+ if (config.incoming.socketType == 1) // plain
+ inServer.socketType = Ci.nsMsgSocketType.plain;
+ else if (config.incoming.socketType == 2) // SSL
+ inServer.socketType = Ci.nsMsgSocketType.SSL;
+ else if (config.incoming.socketType == 3) // STARTTLS
+ inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+
+ gEmailWizardLogger.info("Setting incoming server authMethod to " +
+ config.incoming.auth);
+ inServer.authMethod = config.incoming.auth;
+
+ try {
+ // Lookup issuer if needed.
+ if (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2 ||
+ config.outgoing.auth == Ci.nsMsgAuthMethod.OAuth2) {
+ if (!config.oauthSettings)
+ config.oauthSettings = {};
+ if (!config.oauthSettings.issuer || !config.oauthSettings.scope) {
+ // lookup issuer or scope from hostname
+ let hostname = (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) ?
+ config.incoming.hostname : config.outgoing.hostname;
+ let hostDetails = OAuth2Providers.getHostnameDetails(hostname);
+ if (hostDetails)
+ [config.oauthSettings.issuer, config.oauthSettings.scope] = hostDetails;
+ if (!config.oauthSettings.issuer || !config.oauthSettings.scope)
+ throw "Could not get issuer for oauth2 authentication";
+ }
+ gEmailWizardLogger.info("Saving oauth parameters for issuer " +
+ config.oauthSettings.issuer);
+ inServer.setCharValue("oauth2.scope", config.oauthSettings.scope);
+ inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer);
+ gEmailWizardLogger.info("OAuth2 issuer, scope is " +
+ config.oauthSettings.issuer + ", " + config.oauthSettings.scope);
+ }
+
+ if (inServer.password ||
+ inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2)
+ verifyLogon(config, inServer, alter, msgWindow,
+ successCallback, errorCallback);
+ else {
+ // Avoid pref pollution, clear out server prefs.
+ MailServices.accounts.removeIncomingServer(inServer, true);
+ successCallback(config);
+ }
+ return;
+ }
+ catch (e) {
+ gEmailWizardLogger.error("ERROR: verify logon shouldn't have failed");
+ }
+ // Avoid pref pollution, clear out server prefs.
+ MailServices.accounts.removeIncomingServer(inServer, true);
+ errorCallback(e);
+}
+
+function verifyLogon(config, inServer, alter, msgWindow, successCallback,
+ errorCallback)
+{
+ gEmailWizardLogger.info("verifyLogon for server at " + inServer.hostName);
+ // hack - save away the old callbacks.
+ let saveCallbacks = msgWindow.notificationCallbacks;
+ // set our own callbacks - this works because verifyLogon will
+ // synchronously create the transport and use the notification callbacks.
+ let listener = new urlListener(config, inServer, alter, msgWindow,
+ successCallback, errorCallback);
+ // our listener listens both for the url and cert errors.
+ msgWindow.notificationCallbacks = listener;
+ // try to work around bug where backend is clearing password.
+ try {
+ inServer.password = config.incoming.password;
+ let uri = inServer.verifyLogon(listener, msgWindow);
+ // clear msgWindow so url won't prompt for passwords.
+ uri.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = null;
+ }
+ catch (e) { gEmailWizardLogger.error("verifyLogon failed: " + e); throw e;}
+ finally {
+ // restore them
+ msgWindow.notificationCallbacks = saveCallbacks;
+ }
+}
+
+/**
+ * The url listener also implements nsIBadCertListener2. Its job is to prevent
+ * "bad cert" security dialogs from being shown to the user. Currently it puts
+ * up the cert override dialog, though we'd like to give the user more detailed
+ * information in the future.
+ */
+
+function urlListener(config, server, alter, msgWindow, successCallback,
+ errorCallback)
+{
+ this.mConfig = config;
+ this.mServer = server;
+ this.mAlter = alter;
+ this.mSuccessCallback = successCallback;
+ this.mErrorCallback = errorCallback;
+ this.mMsgWindow = msgWindow;
+ this.mCertError = false;
+ this._log = Log4Moz.getConfiguredLogger("mail.wizard");
+}
+urlListener.prototype =
+{
+ OnStartRunningUrl: function(aUrl)
+ {
+ this._log.info("Starting to test username");
+ this._log.info(" username=" + (this.mConfig.incoming.username !=
+ this.mConfig.identity.emailAddress) +
+ ", have savedUsername=" +
+ (this.mConfig.usernameSaved ? "true" : "false"));
+ this._log.info(" authMethod=" + this.mServer.authMethod);
+ },
+
+ OnStopRunningUrl: function(aUrl, aExitCode)
+ {
+ this._log.info("Finished verifyConfig resulted in " + aExitCode);
+ if (Components.isSuccessCode(aExitCode))
+ {
+ this._cleanup();
+ this.mSuccessCallback(this.mConfig);
+ }
+ // Logon failed, and we aren't supposed to try other variations.
+ else if (!this.mAlter)
+ {
+ this._cleanup();
+ var errorMsg = getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties")
+ .GetStringFromName("cannot_login.error");
+ this.mErrorCallback(new Exception(errorMsg));
+ }
+ // Try other variations, unless there's a cert error, in which
+ // case we'll see what the user chooses.
+ else if (!this.mCertError)
+ {
+ this.tryNextLogon()
+ }
+ },
+
+ tryNextLogon: function()
+ {
+ this._log.info("tryNextLogon()");
+ this._log.info(" username=" + (this.mConfig.incoming.username !=
+ this.mConfig.identity.emailAddress) +
+ ", have savedUsername=" +
+ (this.mConfig.usernameSaved ? "true" : "false"));
+ this._log.info(" authMethod=" + this.mServer.authMethod);
+ // check if we tried full email address as username
+ if (this.mConfig.incoming.username != this.mConfig.identity.emailAddress)
+ {
+ this._log.info(" Changing username to email address.");
+ this.mConfig.usernameSaved = this.mConfig.incoming.username;
+ this.mConfig.incoming.username = this.mConfig.identity.emailAddress;
+ this.mConfig.outgoing.username = this.mConfig.identity.emailAddress;
+ this.mServer.username = this.mConfig.incoming.username;
+ this.mServer.password = this.mConfig.incoming.password;
+ verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow,
+ this.mSuccessCallback, this.mErrorCallback);
+ return;
+ }
+
+ if (this.mConfig.usernameSaved)
+ {
+ this._log.info(" 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.mConfig.incoming.username = this.mConfig.usernameSaved;
+ this.mConfig.outgoing.username = this.mConfig.usernameSaved;
+ this.mConfig.usernameSaved = null;
+ this.mServer.username = this.mConfig.incoming.username;
+ this.mServer.password = this.mConfig.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
+ assert(this.mConfig.incoming.auth == this.mServer.authMethod);
+ this._log.info(" Using SSL: " +
+ (this.mServer.socketType == Ci.nsMsgSocketType.SSL ||
+ this.mServer.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS));
+ if (this.mConfig.incoming.authAlternatives &&
+ this.mConfig.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.
+ {
+ this._log.info(" auth alternatives = " +
+ this.mConfig.incoming.authAlternatives.join(","));
+ this._log.info(" Decreasing auth.");
+ this._log.info(" Have password: " +
+ (this.mServer.password ? "true" : "false"));
+ let brokenAuth = this.mConfig.incoming.auth;
+ // take the next best method (compare chooseBestAuthMethod() in guess)
+ this.mConfig.incoming.auth =
+ this.mConfig.incoming.authAlternatives.shift();
+ this.mServer.authMethod = this.mConfig.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.mConfig.outgoing.auth == brokenAuth &&
+ this.mConfig.outgoing.authAlternatives.indexOf(
+ this.mConfig.incoming.auth) != -1)
+ this.mConfig.outgoing.auth = this.mConfig.incoming.auth;
+ this._log.info(" outgoing auth: " + this.mConfig.outgoing.auth);
+ verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow,
+ this.mSuccessCallback, this.mErrorCallback);
+ return;
+ }
+
+ // Tried all variations we can. Give up.
+ this._log.info("Giving up.");
+ this._cleanup();
+ let errorMsg = getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties")
+ .GetStringFromName("cannot_login.error");
+ this.mErrorCallback(new Exception(errorMsg));
+ return;
+ },
+
+ _cleanup : function()
+ {
+ try {
+ // Avoid pref pollution, clear out server prefs.
+ if (this.mServer) {
+ MailServices.accounts.removeIncomingServer(this.mServer, true);
+ this.mServer = null;
+ }
+ } catch (e) { this._log.error(e); }
+ },
+
+ // Suppress any certificate errors
+ notifyCertProblem: function(socketInfo, status, targetSite) {
+ this.mCertError = true;
+ this._log.error("cert error");
+ let self = this;
+ setTimeout(function () {
+ try {
+ self.informUserOfCertError(socketInfo, status, targetSite);
+ } catch (e) { logException(e); }
+ }, 0);
+ return true;
+ },
+
+ informUserOfCertError : function(socketInfo, status, targetSite) {
+ var params = {
+ exceptionAdded : false,
+ sslStatus : status,
+ prefetchCert : true,
+ location : targetSite,
+ };
+ window.openDialog("chrome://pippki/content/exceptionDialog.xul",
+ "","chrome,centerscreen,modal", params);
+ this._log.info("cert exception dialog closed");
+ this._log.info("cert exceptionAdded = " + params.exceptionAdded);
+ if (!params.exceptionAdded) {
+ this._cleanup();
+ let errorMsg = getStringBundle(
+ "chrome://messenger/locale/accountCreationModel.properties")
+ .GetStringFromName("cannot_login.error");
+ this.mErrorCallback(new Exception(errorMsg));
+ }
+ else {
+ // Retry the logon now that we've added the cert exception.
+ verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow,
+ this.mSuccessCallback, this.mErrorCallback);
+ }
+ },
+
+ // nsIInterfaceRequestor
+ getInterface: function(iid) {
+ return this.QueryInterface(iid);
+ },
+
+ // nsISupports
+ QueryInterface: function(iid) {
+ if (!iid.equals(Components.interfaces.nsIBadCertListener2) &&
+ !iid.equals(Components.interfaces.nsIInterfaceRequestor) &&
+ !iid.equals(Components.interfaces.nsIUrlListener) &&
+ !iid.equals(Components.interfaces.nsISupports))
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+
+ return this;
+ }
+}