diff options
Diffstat (limited to 'mailnews/base/prefs/content/accountcreation/emailWizard.js')
-rw-r--r-- | mailnews/base/prefs/content/accountcreation/emailWizard.js | 1959 |
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(); |