summaryrefslogtreecommitdiffstats
path: root/mailnews/base/prefs/content/accountcreation/emailWizard.js
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/base/prefs/content/accountcreation/emailWizard.js')
-rw-r--r--mailnews/base/prefs/content/accountcreation/emailWizard.js1959
1 files changed, 1959 insertions, 0 deletions
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();