diff options
Diffstat (limited to 'mailnews/base/prefs/content/accountcreation')
12 files changed, 5833 insertions, 0 deletions
diff --git a/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js b/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js new file mode 100644 index 000000000..9f4c5034e --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/MyBadCertHandler.js @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This class implements nsIBadCertListener. It's job is to prevent "bad cert" + * security dialogs from being shown to the user. We call back to the + * 'callback' object's method "processCertError" so that it can deal with it as + * needed (in the case of autoconfig, setting up temporary overrides). + */ +function BadCertHandler(callback) +{ + this._init(callback); +} + +BadCertHandler.prototype = +{ + _init: function(callback) { + this._callback = callback; + }, + + // Suppress any certificate errors + notifyCertProblem: function(socketInfo, status, targetSite) { + return this._callback.processCertError(socketInfo, status, targetSite); + }, + + // nsIInterfaceRequestor + getInterface: function(iid) { + return this.QueryInterface(iid); + }, + + // nsISupports + QueryInterface: function(iid) { + if (!iid.equals(Components.interfaces.nsIBadCertListener2) && + !iid.equals(Components.interfaces.nsIInterfaceRequestor) && + !iid.equals(Components.interfaces.nsISupports)) + throw Components.results.NS_ERROR_NO_INTERFACE; + return this; + } +}; diff --git a/mailnews/base/prefs/content/accountcreation/accountConfig.js b/mailnews/base/prefs/content/accountcreation/accountConfig.js new file mode 100644 index 000000000..3a757d8ee --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/accountConfig.js @@ -0,0 +1,259 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file creates the class AccountConfig, which is a JS object that holds + * a configuration for a certain account. It is *not* created in the backend + * yet (use aw-createAccount.js for that), and it may be incomplete. + * + * Several AccountConfig objects may co-exist, e.g. for autoconfig. + * One AccountConfig object is used to prefill and read the widgets + * in the Wizard UI. + * When we autoconfigure, we autoconfig writes the values into a + * new object and returns that, and the caller can copy these + * values into the object used by the UI. + * + * See also + * <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat> + * for values stored. + */ + +function AccountConfig() +{ + this.incoming = this.createNewIncoming(); + this.incomingAlternatives = []; + this.outgoing = this.createNewOutgoing(); + this.outgoingAlternatives = []; + this.identity = + { + // displayed real name of user + realname : "%REALNAME%", + // email address of user, as shown in From of outgoing mails + emailAddress : "%EMAILADDRESS%", + }; + this.inputFields = []; + this.domains = []; +}; +AccountConfig.prototype = +{ + // @see createNewIncoming() + incoming : null, + // @see createNewOutgoing() + outgoing : null, + /** + * Other servers which can be used instead of |incoming|, + * in order of decreasing preference. + * (|incoming| itself should not be included here.) + * { Array of incoming/createNewIncoming() } + */ + incomingAlternatives : null, + outgoingAlternatives : null, + // OAuth2 configuration, if needed. + oauthSettings : null, + // just an internal string to refer to this. Do not show to user. + id : null, + // who created the config. + // { one of kSource* } + source : 0, + displayName : null, + // { Array of { varname (value without %), displayName, exampleValue } } + inputFields : null, + // email address domains for which this config is applicable + // { Array of Strings } + domains : null, + + /** + * Factory function for incoming and incomingAlternatives + */ + createNewIncoming : function() + { + return { + // { String-enum: "pop3", "imap", "nntp" } + type : null, + hostname : null, + // { Integer } + port : null, + // May be a placeholder (starts and ends with %). { String } + username : null, + password : null, + // { enum: 1 = plain, 2 = SSL/TLS, 3 = STARTTLS always, 0 = not inited } + // ('TLS when available' is insecure and not supported here) + socketType : 0, + /** + * true when the cert is invalid (and thus SSL useless), because it's + * 1) not from an accepted CA (including self-signed certs) + * 2) for a different hostname or + * 3) expired. + * May go back to false when user explicitly accepted the cert. + */ + badCert : false, + /** + * How to log in to the server: plaintext or encrypted pw, GSSAPI etc. + * Defined by Ci.nsMsgAuthMethod + * Same as server pref "authMethod". + */ + auth : 0, + /** + * Other auth methods that we think the server supports. + * They are ordered by descreasing preference. + * (|auth| itself is not included in |authAlternatives|) + * {Array of Ci.nsMsgAuthMethod} (same as .auth) + */ + authAlternatives : null, + // in minutes { Integer } + checkInterval : 10, + loginAtStartup : true, + // POP3 only: + // Not yet implemented. { Boolean } + useGlobalInbox : false, + leaveMessagesOnServer : true, + daysToLeaveMessagesOnServer : 14, + deleteByAgeFromServer : true, + // When user hits delete, delete from local store and from server + deleteOnServerWhenLocalDelete : true, + downloadOnBiff : true, + }; + }, + /** + * Factory function for outgoing and outgoingAlternatives + */ + createNewOutgoing : function() + { + return { + type : "smtp", + hostname : null, + port : null, // see incoming + username : null, // see incoming. may be null, if auth is 0. + password : null, // see incoming. may be null, if auth is 0. + socketType : 0, // see incoming + badCert : false, // see incoming + auth : 0, // see incoming + authAlternatives : null, // see incoming + addThisServer : true, // if we already have an SMTP server, add this + // if we already have an SMTP server, use it. + useGlobalPreferredServer : false, + // we should reuse an already configured SMTP server. + // nsISmtpServer.key + existingServerKey : null, + // user display value for existingServerKey + existingServerLabel : null, + }; + }, + + /** + * Returns a deep copy of this object, + * i.e. modifying the copy will not affect the original object. + */ + copy : function() + { + // Workaround: deepCopy() fails to preserve base obj (instanceof) + var result = new AccountConfig(); + for (var prop in this) + result[prop] = deepCopy(this[prop]); + + return result; + }, + isComplete : function() + { + return (!!this.incoming.hostname && !!this.incoming.port && + !!this.incoming.socketType && !!this.incoming.auth && + !!this.incoming.username && + (!!this.outgoing.existingServerKey || + (!!this.outgoing.hostname && !!this.outgoing.port && + !!this.outgoing.socketType && !!this.outgoing.auth && + !!this.outgoing.username))); + }, +}; + + +// enum consts + +// .source +AccountConfig.kSourceUser = 1; // user manually entered the config +AccountConfig.kSourceXML = 2; // config from XML from ISP or Mozilla DB +AccountConfig.kSourceGuess = 3; // guessConfig() + + +/** + * Some fields on the account config accept placeholders (when coming from XML). + * + * These are the predefined ones + * * %EMAILADDRESS% (full email address of the user, usually entered by user) + * * %EMAILLOCALPART% (email address, part before @) + * * %EMAILDOMAIN% (email address, part after @) + * * %REALNAME% + * as well as those defined in account.inputFields.*.varname, with % added + * before and after. + * + * These must replaced with real values, supplied by the user or app, + * before the account is created. This is done here. You call this function once + * you have all the data - gathered the standard vars mentioned above as well as + * all listed in account.inputFields, and pass them in here. This function will + * insert them in the fields, returning a fully filled-out account ready to be + * created. + * + * @param account {AccountConfig} + * The account data to be modified. It may or may not contain placeholders. + * After this function, it should not contain placeholders anymore. + * This object will be modified in-place. + * + * @param emailfull {String} + * Full email address of this account, e.g. "joe@example.com". + * Empty of incomplete email addresses will/may be rejected. + * + * @param realname {String} + * Real name of user, as will appear in From of outgoing messages + * + * @param password {String} + * The password for the incoming server and (if necessary) the outgoing server + */ +function replaceVariables(account, realname, emailfull, password) +{ + sanitize.nonemptystring(emailfull); + let emailsplit = emailfull.split("@"); + assert(emailsplit.length == 2, + "email address not in expected format: must contain exactly one @"); + let emaillocal = sanitize.nonemptystring(emailsplit[0]); + let emaildomain = sanitize.hostname(emailsplit[1]); + sanitize.label(realname); + sanitize.nonemptystring(realname); + + let otherVariables = {}; + otherVariables.EMAILADDRESS = emailfull; + otherVariables.EMAILLOCALPART = emaillocal; + otherVariables.EMAILDOMAIN = emaildomain; + otherVariables.REALNAME = realname; + + if (password) { + account.incoming.password = password; + account.outgoing.password = password; // set member only if auth required? + } + account.incoming.username = _replaceVariable(account.incoming.username, + otherVariables); + account.outgoing.username = _replaceVariable(account.outgoing.username, + otherVariables); + account.incoming.hostname = + _replaceVariable(account.incoming.hostname, otherVariables); + if (account.outgoing.hostname) // will be null if user picked existing server. + account.outgoing.hostname = + _replaceVariable(account.outgoing.hostname, otherVariables); + account.identity.realname = + _replaceVariable(account.identity.realname, otherVariables); + account.identity.emailAddress = + _replaceVariable(account.identity.emailAddress, otherVariables); + account.displayName = _replaceVariable(account.displayName, otherVariables); +} + +function _replaceVariable(variable, values) +{ + let str = variable; + if (typeof(str) != "string") + return str; + + for (let varname in values) + str = str.replace("%" + varname + "%", values[varname]); + + return str; +} diff --git a/mailnews/base/prefs/content/accountcreation/createInBackend.js b/mailnews/base/prefs/content/accountcreation/createInBackend.js new file mode 100644 index 000000000..d959c3ae9 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/createInBackend.js @@ -0,0 +1,333 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Takes an |AccountConfig| JS object and creates that account in the + * Thunderbird backend (which also writes it to prefs). + * + * @param config {AccountConfig} The account to create + * + * @return - the account created. + */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +function createAccountInBackend(config) +{ + // incoming server + let inServer = MailServices.accounts.createIncomingServer( + config.incoming.username, + config.incoming.hostname, + sanitize.enum(config.incoming.type, ["pop3", "imap", "nntp"])); + inServer.port = config.incoming.port; + inServer.authMethod = config.incoming.auth; + inServer.password = config.incoming.password; + if (config.rememberPassword && config.incoming.password.length) + rememberPassword(inServer, config.incoming.password); + + if (inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) { + inServer.setCharValue("oauth2.scope", config.oauthSettings.scope); + inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer); + } + + // SSL + if (config.incoming.socketType == 1) // plain + inServer.socketType = Ci.nsMsgSocketType.plain; + else if (config.incoming.socketType == 2) // SSL / TLS + inServer.socketType = Ci.nsMsgSocketType.SSL; + else if (config.incoming.socketType == 3) // STARTTLS + inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + //inServer.prettyName = config.displayName; + inServer.prettyName = config.identity.emailAddress; + + inServer.doBiff = true; + inServer.biffMinutes = config.incoming.checkInterval; + const loginAtStartupPrefTemplate = + "mail.server.%serverkey%.login_at_startup"; + var loginAtStartupPref = + loginAtStartupPrefTemplate.replace("%serverkey%", inServer.key); + Services.prefs.setBoolPref(loginAtStartupPref, + config.incoming.loginAtStartup); + if (config.incoming.type == "pop3") + { + const leaveOnServerPrefTemplate = + "mail.server.%serverkey%.leave_on_server"; + const daysToLeaveOnServerPrefTemplate = + "mail.server.%serverkey%.num_days_to_leave_on_server"; + const deleteFromServerPrefTemplate = + "mail.server.%serverkey%.delete_mail_left_on_server"; + const deleteByAgeFromServerPrefTemplate = + "mail.server.%serverkey%.delete_by_age_from_server"; + const downloadOnBiffPrefTemplate = + "mail.server.%serverkey%.download_on_biff"; + var leaveOnServerPref = + leaveOnServerPrefTemplate.replace("%serverkey%", inServer.key); + var ageFromServerPref = + deleteByAgeFromServerPrefTemplate.replace("%serverkey%", inServer.key); + var daysToLeaveOnServerPref = + daysToLeaveOnServerPrefTemplate.replace("%serverkey%", inServer.key); + var deleteFromServerPref = + deleteFromServerPrefTemplate.replace("%serverkey%", inServer.key); + let downloadOnBiffPref = + downloadOnBiffPrefTemplate.replace("%serverkey%", inServer.key); + Services.prefs.setBoolPref(leaveOnServerPref, + config.incoming.leaveMessagesOnServer); + Services.prefs.setIntPref(daysToLeaveOnServerPref, + config.incoming.daysToLeaveMessagesOnServer); + Services.prefs.setBoolPref(deleteFromServerPref, + config.incoming.deleteOnServerWhenLocalDelete); + Services.prefs.setBoolPref(ageFromServerPref, + config.incoming.deleteByAgeFromServer); + Services.prefs.setBoolPref(downloadOnBiffPref, + config.incoming.downloadOnBiff); + } + inServer.valid = true; + + let username = config.outgoing.auth > 1 ? config.outgoing.username : null; + let outServer = MailServices.smtp.findServer(username, config.outgoing.hostname); + assert(config.outgoing.addThisServer || + config.outgoing.useGlobalPreferredServer || + config.outgoing.existingServerKey, + "No SMTP server: inconsistent flags"); + + if (config.outgoing.addThisServer && !outServer) + { + outServer = MailServices.smtp.createServer(); + outServer.hostname = config.outgoing.hostname; + outServer.port = config.outgoing.port; + outServer.authMethod = config.outgoing.auth; + if (config.outgoing.auth > 1) + { + outServer.username = username; + outServer.password = config.incoming.password; + if (config.rememberPassword && config.incoming.password.length) + rememberPassword(outServer, config.incoming.password); + } + + if (outServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) { + let pref = "mail.smtpserver." + outServer.key + "."; + Services.prefs.setCharPref(pref + "oauth2.scope", + config.oauthSettings.scope); + Services.prefs.setCharPref(pref + "oauth2.issuer", + config.oauthSettings.issuer); + } + + if (config.outgoing.socketType == 1) // no SSL + outServer.socketType = Ci.nsMsgSocketType.plain; + else if (config.outgoing.socketType == 2) // SSL / TLS + outServer.socketType = Ci.nsMsgSocketType.SSL; + else if (config.outgoing.socketType == 3) // STARTTLS + outServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + + // API problem: <http://mxr.mozilla.org/seamonkey/source/mailnews/compose/public/nsISmtpServer.idl#93> + outServer.description = config.displayName; + if (config.password) + outServer.password = config.outgoing.password; + + // If this is the first SMTP server, set it as default + if (!MailServices.smtp.defaultServer || + !MailServices.smtp.defaultServer.hostname) + MailServices.smtp.defaultServer = outServer; + } + + // identity + // TODO accounts without identity? + let identity = MailServices.accounts.createIdentity(); + identity.fullName = config.identity.realname; + identity.email = config.identity.emailAddress; + + // for new accounts, default to replies being positioned above the quote + // if a default account is defined already, take its settings instead + if (config.incoming.type == "imap" || config.incoming.type == "pop3") + { + identity.replyOnTop = 1; + // identity.sigBottom = false; // don't set this until Bug 218346 is fixed + + if (MailServices.accounts.accounts.length && + MailServices.accounts.defaultAccount) + { + let defAccount = MailServices.accounts.defaultAccount; + let defIdentity = defAccount.defaultIdentity; + if (defAccount.incomingServer.canBeDefaultServer && + defIdentity && defIdentity.valid) + { + identity.replyOnTop = defIdentity.replyOnTop; + identity.sigBottom = defIdentity.sigBottom; + } + } + } + + // due to accepted conventions, news accounts should default to plain text + if (config.incoming.type == "nntp") + identity.composeHtml = false; + + identity.valid = true; + + if (config.outgoing.existingServerKey) + identity.smtpServerKey = config.outgoing.existingServerKey; + else if (!config.outgoing.useGlobalPreferredServer) + identity.smtpServerKey = outServer.key; + + // account and hook up + // Note: Setting incomingServer will cause the AccountManager to refresh + // itself, which could be a problem if we came from it and we haven't set + // the identity (see bug 521955), so make sure everything else on the + // account is set up before you set the incomingServer. + let account = MailServices.accounts.createAccount(); + account.addIdentity(identity); + account.incomingServer = inServer; + if (inServer.canBeDefaultServer && (!MailServices.accounts.defaultAccount || + !MailServices.accounts.defaultAccount + .incomingServer.canBeDefaultServer)) + MailServices.accounts.defaultAccount = account; + + verifyLocalFoldersAccount(MailServices.accounts); + setFolders(identity, inServer); + + // save + MailServices.accounts.saveAccountInfo(); + try { + Services.prefs.savePrefFile(null); + } catch (ex) { + ddump("Could not write out prefs: " + ex); + } + return account; +} + +function setFolders(identity, server) +{ + // TODO: support for local folders for global inbox (or use smart search + // folder instead) + + var baseURI = server.serverURI + "/"; + + // Names will be localized in UI, not in folder names on server/disk + // TODO allow to override these names in the XML config file, + // in case e.g. Google or AOL use different names? + // Workaround: Let user fix it :) + var fccName = "Sent"; + var draftName = "Drafts"; + var templatesName = "Templates"; + + identity.draftFolder = baseURI + draftName; + identity.stationeryFolder = baseURI + templatesName; + identity.fccFolder = baseURI + fccName; + + identity.fccFolderPickerMode = 0; + identity.draftsFolderPickerMode = 0; + identity.tmplFolderPickerMode = 0; +} + +function rememberPassword(server, password) +{ + if (server instanceof Components.interfaces.nsIMsgIncomingServer) + var passwordURI = server.localStoreType + "://" + server.hostName; + else if (server instanceof Components.interfaces.nsISmtpServer) + var passwordURI = "smtp://" + server.hostname; + else + throw new NotReached("Server type not supported"); + + let login = Cc["@mozilla.org/login-manager/loginInfo;1"] + .createInstance(Ci.nsILoginInfo); + login.init(passwordURI, null, passwordURI, server.username, password, "", ""); + try { + Services.logins.addLogin(login); + } catch (e) { + if (e.message.includes("This login already exists")) { + // TODO modify + } else { + throw e; + } + } +} + +/** + * Check whether the user's setup already has an incoming server + * which matches (hostname, port, username) the primary one + * in the config. + * (We also check the email address as username.) + * + * @param config {AccountConfig} filled in (no placeholders) + * @return {nsIMsgIncomingServer} If it already exists, the server + * object is returned. + * If it's a new server, |null| is returned. + */ +function checkIncomingServerAlreadyExists(config) +{ + assert(config instanceof AccountConfig); + let incoming = config.incoming; + let existing = MailServices.accounts.findRealServer(incoming.username, + incoming.hostname, + sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]), + incoming.port); + + // if username does not have an '@', also check the e-mail + // address form of the name. + if (!existing && !incoming.username.includes("@")) + existing = MailServices.accounts.findRealServer(config.identity.emailAddress, + incoming.hostname, + sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]), + incoming.port); + return existing; +}; + +/** + * Check whether the user's setup already has an outgoing server + * which matches (hostname, port, username) the primary one + * in the config. + * + * @param config {AccountConfig} filled in (no placeholders) + * @return {nsISmtpServer} If it already exists, the server + * object is returned. + * If it's a new server, |null| is returned. + */ +function checkOutgoingServerAlreadyExists(config) +{ + assert(config instanceof AccountConfig); + let smtpServers = MailServices.smtp.servers; + while (smtpServers.hasMoreElements()) + { + let existingServer = smtpServers.getNext() + .QueryInterface(Ci.nsISmtpServer); + // TODO check username with full email address, too, like for incoming + if (existingServer.hostname == config.outgoing.hostname && + existingServer.port == config.outgoing.port && + existingServer.username == config.outgoing.username) + return existingServer; + } + return null; +}; + +/** + * Check if there already is a "Local Folders". If not, create it. + * Copied from AccountWizard.js with minor updates. + */ +function verifyLocalFoldersAccount(am) +{ + let localMailServer; + try { + localMailServer = am.localFoldersServer; + } + catch (ex) { + localMailServer = null; + } + + try { + if (!localMailServer) + { + // creates a copy of the identity you pass in + am.createLocalMailAccount(); + try { + localMailServer = am.localFoldersServer; + } + catch (ex) { + ddump("Error! we should have found the local mail server " + + "after we created it."); + } + } + } + catch (ex) { ddump("Error in verifyLocalFoldersAccount " + ex); } +} diff --git a/mailnews/base/prefs/content/accountcreation/emailWizard.js b/mailnews/base/prefs/content/accountcreation/emailWizard.js new file mode 100644 index 000000000..b4e6854da --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/emailWizard.js @@ -0,0 +1,1959 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource:///modules/hostnameUtils.jsm"); +Components.utils.import("resource://gre/modules/OAuth2Providers.jsm"); + +/** + * This is the dialog opened by menu File | New account | Mail... . + * + * It gets the user's realname, email address and password, + * and tries to automatically configure the account from that, + * using various mechanisms. If all fails, the user can enter/edit + * the config, then we create the account. + * + * Steps: + * - User enters realname, email address and password + * - check for config files on disk + * (shipping with Thunderbird, for enterprise deployments) + * - (if fails) try to get the config file from the ISP via a + * fixed URL on the domain of the email address + * - (if fails) try to get the config file from our own database + * at MoMo servers, maintained by the community + * - (if fails) try to guess the config, by guessing hostnames, + * probing ports, checking config via server's CAPS line etc.. + * - verify the setup, by trying to login to the configured servers + * - let user verify and maybe edit the server names and ports + * - If user clicks OK, create the account + */ + + +// from http://xyfer.blogspot.com/2005/01/javascript-regexp-email-validator.html +var emailRE = /^[-_a-z0-9\'+*$^&%=~!?{}]+(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*@(?:[-a-z0-9.]+\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/i; + +if (typeof gEmailWizardLogger == "undefined") { + Cu.import("resource:///modules/gloda/log4moz.js"); + var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard"); +} + +var gStringsBundle; +var gMessengerBundle; +var gBrandShortName; + +/********************* +TODO for bug 549045 +- autodetect protocol +Polish +- reformat code style to match +<https://developer.mozilla.org/En/Mozilla_Coding_Style_Guide#Control_Structures> +- bold status +- remove status when user edited in manual edit +- add and adapt test from bug 534588 +Bugs +- SSL cert errors + - invalid cert (hostname mismatch) doesn't trigger warning dialog as it should + - accept self-signed cert (e.g. imap.mail.ru) doesn't work + (works without my patch), + verifyConfig.js line 124 has no inServer, for whatever reason, + although I didn't change verifyConfig.js at all + (the change you see in that file is irrelevant: that was an attempt to fix + the bug and clean up the code). +- Set radio IMAP vs. POP3, see TODO in code +Things to test (works for me): +- state transitions, buttons enable, status msgs + - stop button + - showes up again after stopping detection and restarting it + - when stopping [retest]: buttons proper? + - enter nonsense domain. guess fails, (so automatically) manual, + change domain to real one (not in DB), guess succeeds. + former bug: goes to manual first shortly, then to result +**********************/ + +// To debug, set mail.wizard.logging.dump (or .console)="All" and kDebug = true + +function e(elementID) +{ + return document.getElementById(elementID); +}; + +function _hide(id) +{ + e(id).hidden = true; +} + +function _show(id) +{ + e(id).hidden = false; +} + +function _enable(id) +{ + e(id).disabled = false; +} + +function _disable(id) +{ + e(id).disabled = true; +} + +function setText(id, value) +{ + var element = e(id); + assert(element, "setText() on non-existant element ID"); + + if (element.localName == "textbox" || element.localName == "label") { + element.value = value; + } else if (element.localName == "description") { + element.textContent = value; + } else { + throw new NotReached("XUL element type not supported"); + } +} + +function setLabelFromStringBundle(elementID, stringName) +{ + e(elementID).label = gMessengerBundle.getString(stringName); +}; + +function EmailConfigWizard() +{ + this._init(); +} +EmailConfigWizard.prototype = +{ + _init : function EmailConfigWizard__init() + { + gEmailWizardLogger.info("Initializing setup wizard"); + this._abortable = null; + }, + + onLoad : function() + { + /** + * this._currentConfig is the config we got either from the XML file or + * from guessing or from the user. Unless it's from the user, it contains + * placeholders like %EMAILLOCALPART% in username and other fields. + * + * The config here must retain these placeholders, to be able to + * adapt when the user enters a different realname, or password or + * email local part. (A change of the domain name will trigger a new + * detection anyways.) + * That means, before you actually use the config (e.g. to create an + * account or to show it to the user), you need to run replaceVariables(). + */ + this._currentConfig = null; + + let userFullname; + try { + let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo); + userFullname = userInfo.fullname; + } catch(e) { + // nsIUserInfo may not be implemented on all platforms, and name might + // not be avaialble even if it is. + } + + this._domain = ""; + this._email = ""; + this._realname = (userFullname) ? userFullname : ""; + e("realname").value = this._realname; + this._password = ""; + this._okCallback = null; + + if (window.arguments && window.arguments[0]) { + if (window.arguments[0].msgWindow) { + this._parentMsgWindow = window.arguments[0].msgWindow; + } + if (window.arguments[0].okCallback) { + this._okCallback = window.arguments[0].okCallback; + } + } + + gEmailWizardLogger.info("Email account setup dialog loaded."); + + gStringsBundle = e("strings"); + gMessengerBundle = e("bundle_messenger"); + gBrandShortName = e("bundle_brand").getString("brandShortName"); + + setLabelFromStringBundle("in-authMethod-password-cleartext", + "authPasswordCleartextViaSSL"); // will warn about insecure later + setLabelFromStringBundle("in-authMethod-password-encrypted", + "authPasswordEncrypted"); + setLabelFromStringBundle("in-authMethod-kerberos", "authKerberos"); + setLabelFromStringBundle("in-authMethod-ntlm", "authNTLM"); + setLabelFromStringBundle("in-authMethod-oauth2", "authOAuth2"); + setLabelFromStringBundle("out-authMethod-no", "authNo"); + setLabelFromStringBundle("out-authMethod-password-cleartext", + "authPasswordCleartextViaSSL"); // will warn about insecure later + setLabelFromStringBundle("out-authMethod-password-encrypted", + "authPasswordEncrypted"); + setLabelFromStringBundle("out-authMethod-kerberos", "authKerberos"); + setLabelFromStringBundle("out-authMethod-ntlm", "authNTLM"); + setLabelFromStringBundle("out-authMethod-oauth2", "authOAuth2"); + + e("incoming_port").value = gStringsBundle.getString("port_auto"); + this.fillPortDropdown("smtp"); + + // If the account provisioner is preffed off, don't display + // the account provisioner button. + if (!Services.prefs.getBoolPref("mail.provider.enabled")) + _hide("provisioner_button"); + + // Populate SMTP server dropdown with already configured SMTP servers from + // other accounts. + var menulist = e("outgoing_hostname"); + let smtpServers = MailServices.smtp.servers; + while (smtpServers.hasMoreElements()) { + let server = smtpServers.getNext().QueryInterface(Ci.nsISmtpServer); + let label = server.displayname; + let key = server.key; + if (MailServices.smtp.defaultServer && + MailServices.smtp.defaultServer.key == key) { + label += " " + gStringsBundle.getString("default_server_tag"); + } + let menuitem = menulist.appendItem(label, key, ""); // label,value,descr + menuitem.serverKey = key; + } + // Add the entry for the new host to the menulist + let menuitem = menulist.insertItemAt(0, "", "-new-"); // pos,label,value + menuitem.serverKey = null; + + // admin-locked prefs hurray + if (!Services.prefs.getBoolPref("signon.rememberSignons")) { + let rememberPasswordE = e("remember_password"); + rememberPasswordE.checked = false; + rememberPasswordE.disabled = true; + } + + // First, unhide the main window areas, and store the width, + // so that we don't resize wildly when we unhide areas. + // switchToMode() will then hide the unneeded parts again. + // We will add some leeway of 10px, in case some of the <description>s wrap, + // e.g. outgoing username != incoming username. + _show("status_area"); + _show("result_area"); + _hide("manual-edit_area"); + window.sizeToContent(); + e("mastervbox").setAttribute("style", + "min-width: " + document.width + "px; " + + "min-height: " + (document.height + 10) + "px;"); + + this.switchToMode("start"); + e("realname").select(); + }, + + /** + * Changes the window configuration to the different modes we have. + * Shows/hides various window parts and buttons. + * @param modename {String-enum} + * "start" : Just the realname, email address, password fields + * "find-config" : detection step, adds the progress message/spinner + * "result" : We found a config and display it to the user. + * The user may create the account. + * "manual-edit" : The user wants (or needs) to manually enter their + * the server hostname and other settings. We'll use them as provided. + * Additionally, there are the following sub-modes which can be entered after + * you entered the main mode: + * "manual-edit-have-hostname" : user entered a hostname for both servers + * that we can use + * "manual-edit-testing" : User pressed the [Re-test] button and + * we're currently detecting the "Auto" values + * "manual-edit-complete" : user entered (or we tested) all necessary + * values, and we're ready to create to account + * Currently, this doesn't cover the warning dialogs etc.. It may later. + */ + switchToMode : function(modename) + { + if (modename == this._currentModename) { + return; + } + this._currentModename = modename; + gEmailWizardLogger.info("switching to UI mode " + modename) + + //_show("initialSettings"); always visible + //_show("cancel_button"); always visible + if (modename == "start") { + _hide("status_area"); + _hide("result_area"); + _hide("manual-edit_area"); + + _show("next_button"); + _disable("next_button"); // will be enabled by code + _hide("half-manual-test_button"); + _hide("create_button"); + _hide("stop_button"); + _hide("manual-edit_button"); + _hide("advanced-setup_button"); + } else if (modename == "find-config") { + _show("status_area"); + _hide("result_area"); + _hide("manual-edit_area"); + + _show("next_button"); + _disable("next_button"); + _hide("half-manual-test_button"); + _hide("create_button"); + _show("stop_button"); + this.onStop = this.onStopFindConfig; + _show("manual-edit_button"); + _hide("advanced-setup_button"); + } else if (modename == "result") { + _show("status_area"); + _show("result_area"); + _hide("manual-edit_area"); + + _hide("next_button"); + _hide("half-manual-test_button"); + _show("create_button"); + _enable("create_button"); + _hide("stop_button"); + _show("manual-edit_button"); + _hide("advanced-setup_button"); + } else if (modename == "manual-edit") { + _show("status_area"); + _hide("result_area"); + _show("manual-edit_area"); + + _hide("next_button"); + _show("half-manual-test_button"); + _disable("half-manual-test_button"); + _show("create_button"); + _disable("create_button"); + _hide("stop_button"); + _hide("manual-edit_button"); + _show("advanced-setup_button"); + _disable("advanced-setup_button"); + } else if (modename == "manual-edit-have-hostname") { + _show("status_area"); + _hide("result_area"); + _show("manual-edit_area"); + _hide("manual-edit_button"); + _hide("next_button"); + _show("create_button"); + + _show("half-manual-test_button"); + _enable("half-manual-test_button"); + _disable("create_button"); + _hide("stop_button"); + _show("advanced-setup_button"); + _disable("advanced-setup_button"); + } else if (modename == "manual-edit-testing") { + _show("status_area"); + _hide("result_area"); + _show("manual-edit_area"); + _hide("manual-edit_button"); + _hide("next_button"); + _show("create_button"); + + _show("half-manual-test_button"); + _disable("half-manual-test_button"); + _disable("create_button"); + _show("stop_button"); + this.onStop = this.onStopHalfManualTesting; + _show("advanced-setup_button"); + _disable("advanced-setup_button"); + } else if (modename == "manual-edit-complete") { + _show("status_area"); + _hide("result_area"); + _show("manual-edit_area"); + _hide("manual-edit_button"); + _hide("next_button"); + _show("create_button"); + + _show("half-manual-test_button"); + _enable("half-manual-test_button"); + _enable("create_button"); + _hide("stop_button"); + _show("advanced-setup_button"); + _enable("advanced-setup_button"); + } else { + throw new NotReached("unknown mode"); + } + // If we're offline, we're going to disable the create button, but enable + // the advanced config button if we have a current config. + if (Services.io.offline) { + if (this._currentConfig != null) { + _show("advanced-setup_button"); + _enable("advanced-setup_button"); + _hide("half-manual-test_button"); + _hide("create_button"); + _hide("manual-edit_button"); + } + } + window.sizeToContent(); + }, + + /** + * Start from beginning with possibly new email address. + */ + onStartOver : function() + { + if (this._abortable) { + this.onStop(); + } + this.switchToMode("start"); + }, + + getConcreteConfig : function() + { + var result = this._currentConfig.copy(); + replaceVariables(result, this._realname, this._email, this._password); + result.rememberPassword = e("remember_password").checked && + !!this._password; + return result; + }, + + /* + * This checks if the email address is at least possibly valid, meaning it + * has an '@' before the last char. + */ + validateEmailMinimally : function(emailAddr) + { + let atPos = emailAddr.lastIndexOf("@"); + return atPos > 0 && atPos + 1 < emailAddr.length; + }, + + /* + * This checks if the email address is syntactically valid, + * as far as we can determine. We try hard to make full checks. + * + * OTOH, we have a very small chance of false negatives, + * because the RFC822 address spec is insanely complicated, + * but rarely needed, so when this here fails, we show an error message, + * but don't stop the user from continuing. + * In contrast, if validateEmailMinimally() fails, we stop the user. + */ + validateEmail : function(emailAddr) + { + return emailRE.test(emailAddr); + }, + + /** + * onInputEmail and onInputRealname are called on input = keypresses, and + * enable/disable the next button based on whether there's a semi-proper + * e-mail address and non-blank realname to start with. + * + * A change to the email address also automatically restarts the + * whole process. + */ + onInputEmail : function() + { + this._email = e("email").value; + this.onStartOver(); + this.checkStartDone(); + }, + onInputRealname : function() + { + this._realname = e("realname").value; + this.checkStartDone(); + }, + + onInputPassword : function() + { + this._password = e("password").value; + }, + + /** + * This does very little other than to check that a name was entered at all + * Since this is such an insignificant test we should be using a very light + * or even jovial warning. + */ + onBlurRealname : function() + { + let realnameEl = e("realname"); + if (this._realname) { + this.clearError("nameerror"); + _show("nametext"); + realnameEl.removeAttribute("error"); + // bug 638790: don't show realname error until user enter an email address + } else if (this.validateEmailMinimally(this._email)) { + _hide("nametext"); + this.setError("nameerror", "please_enter_name"); + realnameEl.setAttribute("error", "true"); + } + }, + + /** + * This check is only done as an informative warning. + * We don't want to block the person, if they've entered an email address + * that doesn't conform to our regex. + */ + onBlurEmail : function() + { + if (!this._email) { + return; + } + var emailEl = e("email"); + if (this.validateEmail(this._email)) { + this.clearError("emailerror"); + emailEl.removeAttribute("error"); + this.onBlurRealname(); + } else { + this.setError("emailerror", "double_check_email"); + emailEl.setAttribute("error", "true"); + } + }, + + /** + * If the user just tabbed through the password input without entering + * anything, set the type back to text so we don't wind up showing the + * emptytext as bullet characters. + */ + onBlurPassword : function() + { + if (!this._password) { + e("password").type = "text"; + } + }, + + /** + * @see onBlurPassword() + */ + onFocusPassword : function() + { + e("password").type = "password"; + }, + + /** + * Check whether the user entered the minimum of information + * needed to leave the "start" mode (entering of name, email, pw) + * and is allowed to proceed to detection step. + */ + checkStartDone : function() + { + if (this.validateEmailMinimally(this._email) && + this._realname) { + this._domain = this._email.split("@")[1].toLowerCase(); + _enable("next_button"); + } else { + _disable("next_button"); + } + }, + + /** + * When the [Continue] button is clicked, we move from the initial account + * information stage to using that information to configure account details. + */ + onNext : function() + { + this.findConfig(this._domain, this._email); + }, + + + ///////////////////////////////////////////////////////////////// + // Detection step + + /** + * Try to find an account configuration for this email address. + * This is the function which runs the autoconfig. + */ + findConfig : function(domain, email) + { + gEmailWizardLogger.info("findConfig()"); + if (this._abortable) { + this.onStop(); + } + this.switchToMode("find-config"); + this.startSpinner("looking_up_settings_disk"); + var self = this; + this._abortable = fetchConfigFromDisk(domain, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner("found_settings_disk"); + }, + function(e) // fetchConfigFromDisk failed + { + if (e instanceof CancelledException) { + return; + } + gEmailWizardLogger.info("fetchConfigFromDisk failed: " + e); + self.startSpinner("looking_up_settings_isp"); + self._abortable = fetchConfigFromISP(domain, email, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner("found_settings_isp"); + }, + function(e) // fetchConfigFromISP failed + { + if (e instanceof CancelledException) { + return; + } + gEmailWizardLogger.info("fetchConfigFromISP failed: " + e); + logException(e); + self.startSpinner("looking_up_settings_db"); + self._abortable = fetchConfigFromDB(domain, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner("found_settings_db"); + }, + function(e) // fetchConfigFromDB failed + { + if (e instanceof CancelledException) { + return; + } + logException(e); + gEmailWizardLogger.info("fetchConfigFromDB failed: " + e); + self.startSpinner("looking_up_settings_db"); + self._abortable = fetchConfigForMX(domain, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner("found_settings_db"); + }, + function(e) // fetchConfigForMX failed + { + if (e instanceof CancelledException) { + return; + } + logException(e); + gEmailWizardLogger.info("fetchConfigForMX failed: " + e); + var initialConfig = new AccountConfig(); + self._prefillConfig(initialConfig); + self._guessConfig(domain, initialConfig); + }); + }); + }); + }); + }, + + /** + * Just a continuation of findConfig() + */ + _guessConfig : function(domain, initialConfig) + { + this.startSpinner("looking_up_settings_guess") + var self = this; + self._abortable = guessConfig(domain, + function(type, hostname, port, ssl, done, config) // progress + { + gEmailWizardLogger.info("progress callback host " + hostname + + " port " + port + " type " + type); + }, + function(config) // success + { + self._abortable = null; + self.foundConfig(config); + self.stopSpinner(Services.io.offline ? + "guessed_settings_offline" : "found_settings_guess"); + window.sizeToContent(); + }, + function(e, config) // guessconfig failed + { + if (e instanceof CancelledException) { + return; + } + self._abortable = null; + gEmailWizardLogger.info("guessConfig failed: " + e); + self.showErrorStatus("failed_to_find_settings"); + self.editConfigDetails(); + }, + initialConfig, "both"); + }, + + /** + * When findConfig() was successful, it calls this. + * This displays the config to the user. + */ + foundConfig : function(config) + { + gEmailWizardLogger.info("foundConfig()"); + assert(config instanceof AccountConfig, + "BUG: Arg 'config' needs to be an AccountConfig object"); + + this._haveValidConfigForDomain = this._email.split("@")[1]; + + if (!this._realname || !this._email) { + return; + } + this._foundConfig2(config); + }, + + // Continuation of foundConfig2() after custom fields. + _foundConfig2 : function(config) + { + this.displayConfigResult(config); + }, + + /** + * [Stop] button click handler. + * This allows the user to abort any longer operation, esp. network activity. + * We currently have 3 such cases here: + * 1. findConfig(), i.e. fetch config from DB, guessConfig etc. + * 2. onHalfManualTest(), i.e. the [Retest] button in manual config. + * 3. verifyConfig() - We can't stop this yet, so irrelevant here currently. + * Given that these need slightly different actions, this function will be set + * to a function (i.e. overwritten) by whoever enables the stop button. + * + * We also call this from the code when the user started a different action + * without explicitly clicking [Stop] for the old one first. + */ + onStop : function() + { + throw new NotReached("onStop should be overridden by now"); + }, + _onStopCommon : function() + { + if (!this._abortable) { + throw new NotReached("onStop called although there's nothing to stop"); + } + gEmailWizardLogger.info("onStop cancelled _abortable"); + this._abortable.cancel(new UserCancelledException()); + this._abortable = null; + this.stopSpinner(); + }, + onStopFindConfig : function() + { + this._onStopCommon(); + this.switchToMode("start"); + this.checkStartDone(); + }, + onStopHalfManualTesting : function() + { + this._onStopCommon(); + this.validateManualEditComplete(); + }, + + + + /////////////////////////////////////////////////////////////////// + // status area + + startSpinner : function(actionStrName) + { + e("status_area").setAttribute("status", "loading"); + gEmailWizardLogger.warn("spinner start " + actionStrName); + this._showStatusTitle(actionStrName); + }, + + stopSpinner : function(actionStrName) + { + e("status_area").setAttribute("status", "result"); + _hide("stop_button"); + this._showStatusTitle(actionStrName); + gEmailWizardLogger.warn("all spinner stop " + actionStrName); + }, + + showErrorStatus : function(actionStrName) + { + e("status_area").setAttribute("status", "error"); + gEmailWizardLogger.warn("status error " + actionStrName); + this._showStatusTitle(actionStrName); + }, + + _showStatusTitle : function(msgName) + { + let msg = " "; // assure height. Do via min-height in CSS, for 2 lines? + try { + if (msgName) { + msg = gStringsBundle.getFormattedString(msgName, [gBrandShortName]); + } + } catch(ex) { + gEmailWizardLogger.error("missing string for " + msgName); + msg = msgName + " (missing string in translation!)"; + } + + e("status_msg").textContent = msg; + gEmailWizardLogger.info("status msg: " + msg); + }, + + + + ///////////////////////////////////////////////////////////////// + // Result area + + /** + * Displays a (probed) config to the user, + * in the result config details area. + * + * @param config {AccountConfig} The config to present to user + */ + displayConfigResult : function(config) + { + assert(config instanceof AccountConfig); + this._currentConfig = config; + var configFilledIn = this.getConcreteConfig(); + + var unknownString = gStringsBundle.getString("resultUnknown"); + + function _makeHostDisplayString(server, stringName) + { + let type = gStringsBundle.getString(sanitize.translate(server.type, + { imap : "resultIMAP", pop3 : "resultPOP3", smtp : "resultSMTP" }), + unknownString); + let host = server.hostname + + (isStandardPort(server.port) ? "" : ":" + server.port); + let ssl = gStringsBundle.getString(sanitize.translate(server.socketType, + { 1 : "resultNoEncryption", 2 : "resultSSL", 3 : "resultSTARTTLS" }), + unknownString); + let certStatus = gStringsBundle.getString(server.badCert ? + "resultSSLCertWeak" : "resultSSLCertOK"); + // TODO: we should really also display authentication method here. + return gStringsBundle.getFormattedString(stringName, + [ type, host, ssl, certStatus ]); + }; + + var incomingResult = unknownString; + if (configFilledIn.incoming.hostname) { + incomingResult = _makeHostDisplayString(configFilledIn.incoming, + "resultIncoming"); + } + + var outgoingResult = unknownString; + if (!config.outgoing.existingServerKey) { + if (configFilledIn.outgoing.hostname) { + outgoingResult = _makeHostDisplayString(configFilledIn.outgoing, + "resultOutgoing"); + } + } else { + outgoingResult = gStringsBundle.getString("resultOutgoingExisting"); + } + + var usernameResult; + if (configFilledIn.incoming.username == configFilledIn.outgoing.username) { + usernameResult = gStringsBundle.getFormattedString("resultUsernameBoth", + [ configFilledIn.incoming.username || unknownString ]); + } else { + usernameResult = gStringsBundle.getFormattedString( + "resultUsernameDifferent", + [ configFilledIn.incoming.username || unknownString, + configFilledIn.outgoing.username || unknownString ]); + } + + setText("result-incoming", incomingResult); + setText("result-outgoing", outgoingResult); + setText("result-username", usernameResult); + + gEmailWizardLogger.info(debugObject(config, "config")); + // IMAP / POP dropdown + var lookForAltType = + config.incoming.type == "imap" ? "pop3" : "imap"; + var alternative = null; + for (let i = 0; i < config.incomingAlternatives.length; i++) { + let alt = config.incomingAlternatives[i]; + if (alt.type == lookForAltType) { + alternative = alt; + break; + } + } + if (alternative) { + _show("result_imappop"); + e("result_select_" + alternative.type).configIncoming = alternative; + e("result_select_" + config.incoming.type).configIncoming = + config.incoming; + e("result_imappop").value = + config.incoming.type == "imap" ? 1 : 2; + } else { + _hide("result_imappop"); + } + + this.switchToMode("result"); + }, + + /** + * Handle the user switching between IMAP and POP3 settings using the + * radio buttons. + * + * Note: This function must only be called by user action, not by setting + * the value or selectedItem or selectedIndex of the radiogroup! + * This is why we use the oncommand attribute of the radio elements + * instead of the onselect attribute of the radiogroup. + */ + onResultIMAPOrPOP3 : function() + { + var config = this._currentConfig; + var radiogroup = e("result_imappop"); + // add current server as best alternative to start of array + config.incomingAlternatives.unshift(config.incoming); + // use selected server (stored as special property on the <radio> node) + config.incoming = radiogroup.selectedItem.configIncoming; + // remove newly selected server from list of alternatives + config.incomingAlternatives = config.incomingAlternatives.filter( + function(e) { return e != config.incoming; }); + this.displayConfigResult(config); + }, + + + + ///////////////////////////////////////////////////////////////// + // Manual Edit area + + /** + * Gets the values from the user in the manual edit area. + * + * Realname and password are not part of that area and still + * placeholders, but hostname and username are concrete and + * no placeholders anymore. + */ + getUserConfig : function() + { + var config = this.getConcreteConfig(); + if (!config) { + config = new AccountConfig(); + } + config.source = AccountConfig.kSourceUser; + + // Incoming server + try { + var inHostnameField = e("incoming_hostname"); + config.incoming.hostname = sanitize.hostname(inHostnameField.value); + inHostnameField.value = config.incoming.hostname; + } catch (e) { gEmailWizardLogger.warn(e); } + try { + config.incoming.port = sanitize.integerRange(e("incoming_port").value, + kMinPort, kMaxPort); + } catch (e) { + config.incoming.port = undefined; // incl. default "Auto" + } + config.incoming.type = sanitize.translate(e("incoming_protocol").value, + { 1: "imap", 2 : "pop3", 0 : null }); + config.incoming.socketType = sanitize.integer(e("incoming_ssl").value); + config.incoming.auth = sanitize.integer(e("incoming_authMethod").value); + config.incoming.username = e("incoming_username").value; + + // Outgoing server + + // Did the user select one of the already configured SMTP servers from the + // drop-down list? If so, use it. + var outHostnameCombo = e("outgoing_hostname"); + var outMenuitem = outHostnameCombo.selectedItem; + if (outMenuitem && outMenuitem.serverKey) { + config.outgoing.existingServerKey = outMenuitem.serverKey; + config.outgoing.existingServerLabel = outMenuitem.label; + config.outgoing.addThisServer = false; + config.outgoing.useGlobalPreferredServer = false; + } else { + config.outgoing.existingServerKey = null; + config.outgoing.addThisServer = true; + config.outgoing.useGlobalPreferredServer = false; + + try { + config.outgoing.hostname = sanitize.hostname( + outHostnameCombo.inputField.value); + outHostnameCombo.inputField.value = config.outgoing.hostname; + } catch (e) { gEmailWizardLogger.warn(e); } + try { + config.outgoing.port = sanitize.integerRange(e("outgoing_port").value, + kMinPort, kMaxPort); + } catch (e) { + config.outgoing.port = undefined; // incl. default "Auto" + } + config.outgoing.socketType = sanitize.integer(e("outgoing_ssl").value); + config.outgoing.auth = sanitize.integer(e("outgoing_authMethod").value); + } + config.outgoing.username = e("outgoing_username").value; + + return config; + }, + + /** + * [Manual Config] button click handler. This turns the config details area + * into an editable form and makes the (Go) button appear. The edit button + * should only be available after the config probing is completely finished, + * replacing what was the (Stop) button. + */ + onManualEdit : function() + { + if (this._abortable) { + this.onStop(); + } + this.editConfigDetails(); + }, + + /** + * Setting the config details form so it can be edited. We also disable + * (and hide) the create button during this time because we don't know what + * might have changed. The function called from the button that restarts + * the config check should be enabling the config button as needed. + */ + editConfigDetails : function() + { + gEmailWizardLogger.info("manual edit"); + + if (!this._currentConfig) { + this._currentConfig = new AccountConfig(); + this._currentConfig.incoming.type = "imap"; + this._currentConfig.incoming.username = "%EMAILLOCALPART%"; + this._currentConfig.outgoing.username = "%EMAILLOCALPART%"; + this._currentConfig.incoming.hostname = ".%EMAILDOMAIN%"; + this._currentConfig.outgoing.hostname = ".%EMAILDOMAIN%"; + } + // Although we go manual, and we need to display the concrete username, + // however the realname and password is not part of manual config and + // must stay a placeholder in _currentConfig. @see getUserConfig() + + this._fillManualEditFields(this.getConcreteConfig()); + + // _fillManualEditFields() indirectly calls validateManualEditComplete(), + // but it's important to not forget it in case the code is rewritten, + // so calling it explicitly again. Doesn't do harm, speed is irrelevant. + this.validateManualEditComplete(); + }, + + /** + * Fills the manual edit textfields with the provided config. + * @param config {AccountConfig} The config to present to user + */ + _fillManualEditFields : function(config) + { + assert(config instanceof AccountConfig); + + // incoming server + e("incoming_protocol").value = sanitize.translate(config.incoming.type, + { "imap" : 1, "pop3" : 2 }, 1); + e("incoming_hostname").value = config.incoming.hostname; + e("incoming_ssl").value = sanitize.enum(config.incoming.socketType, + [ 0, 1, 2, 3 ], 0); + e("incoming_authMethod").value = sanitize.enum(config.incoming.auth, + [ 0, 3, 4, 5, 6, 10 ], 0); + e("incoming_username").value = config.incoming.username; + if (config.incoming.port) { + e("incoming_port").value = config.incoming.port; + } else { + this.adjustIncomingPortToSSLAndProtocol(config); + } + this.fillPortDropdown(config.incoming.type); + + // If the hostname supports OAuth2 and imap is enabled, enable OAuth2. + let iDetails = OAuth2Providers.getHostnameDetails(config.incoming.hostname); + gEmailWizardLogger.info("OAuth2 details for incoming hostname " + + config.incoming.hostname + " is " + iDetails); + e("in-authMethod-oauth2").hidden = !(iDetails && e("incoming_protocol").value == 1); + if (!e("in-authMethod-oauth2").hidden) { + config.oauthSettings = {}; + [config.oauthSettings.issuer, config.oauthSettings.scope] = iDetails; + // oauthsettings are not stored nor changable in the user interface, so just + // store them in the base configuration. + this._currentConfig.oauthSettings = config.oauthSettings; + } + + // outgoing server + e("outgoing_hostname").value = config.outgoing.hostname; + e("outgoing_username").value = config.outgoing.username; + // While sameInOutUsernames is true we synchronize values of incoming + // and outgoing username. + this.sameInOutUsernames = true; + e("outgoing_ssl").value = sanitize.enum(config.outgoing.socketType, + [ 0, 1, 2, 3 ], 0); + e("outgoing_authMethod").value = sanitize.enum(config.outgoing.auth, + [ 0, 1, 3, 4, 5, 6, 10 ], 0); + if (config.outgoing.port) { + e("outgoing_port").value = config.outgoing.port; + } else { + this.adjustOutgoingPortToSSLAndProtocol(config); + } + + // If the hostname supports OAuth2 and imap is enabled, enable OAuth2. + let oDetails = OAuth2Providers.getHostnameDetails(config.outgoing.hostname); + gEmailWizardLogger.info("OAuth2 details for outgoing hostname " + + config.outgoing.hostname + " is " + oDetails); + e("out-authMethod-oauth2").hidden = !oDetails; + if (!e("out-authMethod-oauth2").hidden) { + config.oauthSettings = {}; + [config.oauthSettings.issuer, config.oauthSettings.scope] = oDetails; + // oauthsettings are not stored nor changable in the user interface, so just + // store them in the base configuration. + this._currentConfig.oauthSettings = config.oauthSettings; + } + + // populate fields even if existingServerKey, in case user changes back + if (config.outgoing.existingServerKey) { + let menulist = e("outgoing_hostname"); + // We can't use menulist.value = config.outgoing.existingServerKey + // because would overwrite the text field, so have to do it manually: + let menuitems = menulist.menupopup.childNodes; + for (let menuitem of menuitems) { + if (menuitem.serverKey == config.outgoing.existingServerKey) { + menulist.selectedItem = menuitem; + break; + } + } + } + this.onChangedOutgoingDropdown(); // show/hide outgoing port, SSL, ... + }, + + /** + * Automatically fill port field in manual edit, + * unless user entered a non-standard port. + * @param config {AccountConfig} + */ + adjustIncomingPortToSSLAndProtocol : function(config) + { + var autoPort = gStringsBundle.getString("port_auto"); + var incoming = config.incoming; + // we could use getHostEntry() here, but that API is bad, so don't bother + var newInPort = undefined; + if (!incoming.port || isStandardPort(incoming.port)) { + if (incoming.type == "imap") { + if (incoming.socketType == 1 || incoming.socketType == 3) { + newInPort = 143; + } else if (incoming.socketType == 2) { // Normal SSL + newInPort = 993; + } else { // auto + newInPort = autoPort; + } + } else if (incoming.type == "pop3") { + if (incoming.socketType == 1 || incoming.socketType == 3) { + newInPort = 110; + } else if (incoming.socketType == 2) { // Normal SSLs + newInPort = 995; + } else { // auto + newInPort = autoPort; + } + } + } + if (newInPort != undefined) { + e("incoming_port").value = newInPort; + e("incoming_authMethod").value = 0; // auto + } + }, + + /** + * @see adjustIncomingPortToSSLAndProtocol() + */ + adjustOutgoingPortToSSLAndProtocol : function(config) + { + var autoPort = gStringsBundle.getString("port_auto"); + var outgoing = config.outgoing; + var newOutPort = undefined; + if (!outgoing.port || isStandardPort(outgoing.port)) { + if (outgoing.socketType == 1 || outgoing.socketType == 3) { + // standard port is 587 *or* 25, so set to auto + // unless user or config already entered one of these two ports. + if (outgoing.port != 25 && outgoing.port != 587) { + newOutPort = autoPort; + } + } else if (outgoing.socketType == 2) { // Normal SSL + newOutPort = 465; + } else { // auto + newOutPort = autoPort; + } + } + if (newOutPort != undefined) { + e("outgoing_port").value = newOutPort; + e("outgoing_authMethod").value = 0; // auto + } + }, + + /** + * If the user changed the port manually, adjust the SSL value, + * (only) if the new port is impossible with the old SSL value. + * @param config {AccountConfig} + */ + adjustIncomingSSLToPort : function(config) + { + var incoming = config.incoming; + var newInSocketType = undefined; + if (!incoming.port || // auto + !isStandardPort(incoming.port)) { + return; + } + if (incoming.type == "imap") { + // normal SSL impossible + if (incoming.port == 143 && incoming.socketType == 2) { + newInSocketType = 0; // auto + // must be normal SSL + } else if (incoming.port == 993 && incoming.socketType != 2) { + newInSocketType = 2; + } + } else if (incoming.type == "pop3") { + // normal SSL impossible + if (incoming.port == 110 && incoming.socketType == 2) { + newInSocketType = 0; // auto + // must be normal SSL + } else if (incoming.port == 995 && incoming.socketType != 2) { + newInSocketType = 2; + } + } + if (newInSocketType != undefined) { + e("incoming_ssl").value = newInSocketType; + e("incoming_authMethod").value = 0; // auto + } + }, + + /** + * @see adjustIncomingSSLToPort() + */ + adjustOutgoingSSLToPort : function(config) + { + var outgoing = config.outgoing; + var newOutSocketType = undefined; + if (!outgoing.port || // auto + !isStandardPort(outgoing.port)) { + return; + } + // normal SSL impossible + if ((outgoing.port == 587 || outgoing.port == 25) && + outgoing.socketType == 2) { + newOutSocketType = 0; // auto + // must be normal SSL + } else if (outgoing.port == 465 && outgoing.socketType != 2) { + newOutSocketType = 2; + } + if (newOutSocketType != undefined) { + e("outgoing_ssl").value = newOutSocketType; + e("outgoing_authMethod").value = 0; // auto + } + }, + + /** + * Sets the prefilled values of the port fields. + * Filled statically with the standard ports for the given protocol, + * plus "Auto". + */ + fillPortDropdown : function(protocolType) + { + var menu = e(protocolType == "smtp" ? "outgoing_port" : "incoming_port"); + + // menulist.removeAllItems() is nice, but nicely clears the user value, too + var popup = menu.menupopup; + while (popup.hasChildNodes()) + popup.lastChild.remove(); + + // add standard ports + var autoPort = gStringsBundle.getString("port_auto"); + menu.appendItem(autoPort, autoPort, ""); // label,value,descr + for (let port of getStandardPorts(protocolType)) { + menu.appendItem(port, port, ""); // label,value,descr + } + }, + + onChangedProtocolIncoming : function() + { + var config = this.getUserConfig(); + this.adjustIncomingPortToSSLAndProtocol(config); + this.fillPortDropdown(config.incoming.type); + this.onChangedManualEdit(); + }, + onChangedPortIncoming : function() + { + gEmailWizardLogger.info("incoming port changed"); + this.adjustIncomingSSLToPort(this.getUserConfig()); + this.onChangedManualEdit(); + }, + onChangedPortOutgoing : function() + { + gEmailWizardLogger.info("outgoing port changed"); + this.adjustOutgoingSSLToPort(this.getUserConfig()); + this.onChangedManualEdit(); + }, + onChangedSSLIncoming : function() + { + this.adjustIncomingPortToSSLAndProtocol(this.getUserConfig()); + this.onChangedManualEdit(); + }, + onChangedSSLOutgoing : function() + { + this.adjustOutgoingPortToSSLAndProtocol(this.getUserConfig()); + this.onChangedManualEdit(); + }, + onChangedInAuth : function() + { + this.onChangedManualEdit(); + }, + onChangedOutAuth : function(aSelectedAuth) + { + if (aSelectedAuth) { + e("outgoing_label").hidden = e("outgoing_username").hidden = + (aSelectedAuth.id == "out-authMethod-no"); + } + this.onChangedManualEdit(); + }, + onInputInUsername : function() + { + if (this.sameInOutUsernames) + e("outgoing_username").value = e("incoming_username").value; + this.onChangedManualEdit(); + }, + onInputOutUsername : function() + { + this.sameInOutUsernames = false; + this.onChangedManualEdit(); + }, + onInputHostname : function() + { + this.onChangedManualEdit(); + }, + + /** + * Sets the label of the first entry of the dropdown which represents + * the new outgoing server. + */ + onOpenOutgoingDropdown : function() + { + var menulist = e("outgoing_hostname"); + // If the menulist is not editable, there is nothing to update + // and menulist.inputField does not even exist. + if (!menulist.editable) + return; + + var menuitem = menulist.getItemAtIndex(0); + assert(!menuitem.serverKey, "I wanted the special item for the new host"); + menuitem.label = menulist.inputField.value; + }, + + /** + * User selected an existing SMTP server (or deselected it). + * This changes only the UI. The values are read in getUserConfig(). + */ + onChangedOutgoingDropdown : function() + { + var menulist = e("outgoing_hostname"); + var menuitem = menulist.selectedItem; + if (menuitem && menuitem.serverKey) { + // an existing server has been selected from the dropdown + menulist.editable = false; + _hide("outgoing_port"); + _hide("outgoing_ssl"); + _hide("outgoing_authMethod"); + } else { + // new server, with hostname, port etc. + menulist.editable = true; + _show("outgoing_port"); + _show("outgoing_ssl"); + _show("outgoing_authMethod"); + } + + this.onChangedManualEdit(); + }, + + onChangedManualEdit : function() + { + if (this._abortable) { + this.onStop(); + } + this.validateManualEditComplete(); + }, + + /** + * This enables the buttons which allow the user to proceed + * once he has entered enough information. + * + * We can easily and fairly surely autodetect everything apart from the + * hostname (and username). So, once the user has entered + * proper hostnames, change to "manual-edit-have-hostname" mode + * which allows to press [Re-test], which starts the detection + * of the other values. + * Once the user has entered (or we detected) all values, he may + * do [Create Account] (tests login and if successful creates the account) + * or [Advanced Setup] (goes to Account Manager). Esp. in the latter case, + * we will not second-guess his setup and just to as told, so here we make + * sure that he at least entered all values. + */ + validateManualEditComplete : function() + { + // getUserConfig() is expensive, but still OK, not a problem + var manualConfig = this.getUserConfig(); + this._currentConfig = manualConfig; + if (manualConfig.isComplete()) { + this.switchToMode("manual-edit-complete"); + } else if (!!manualConfig.incoming.hostname && + !!manualConfig.outgoing.hostname) { + this.switchToMode("manual-edit-have-hostname"); + } else { + this.switchToMode("manual-edit"); + } + }, + + /** + * [Switch to provisioner] button click handler. Always active, allows + * one to switch to the account provisioner screen. + */ + onSwitchToProvisioner : function () + { + // We have to close this window first, otherwise msgNewMailAccount + // in accountUtils.js will think that this window still + // exists when it's called from the account provisioner window. + // This is because the account provisioner window is modal, + // and therefore blocks. Therefore, we override the _okCallback + // with a function that spawns the account provisioner, and then + // close the window. + this._okCallback = function() { + NewMailAccountProvisioner(window.arguments[0].msgWindow, window.arguments[0].extraData); + } + window.close(); + }, + + /** + * [Advanced Setup...] button click handler + * Only active in manual edit mode, and goes straight into + * Account Settings (pref UI) dialog. Requires a backend account, + * which requires proper hostname, port and protocol. + */ + onAdvancedSetup : function() + { + assert(this._currentConfig instanceof AccountConfig); + let configFilledIn = this.getConcreteConfig(); + + if (checkIncomingServerAlreadyExists(configFilledIn)) { + alertPrompt(gStringsBundle.getString("error_creating_account"), + gStringsBundle.getString("incoming_server_exists")); + return; + } + + gEmailWizardLogger.info("creating account in backend"); + let newAccount = createAccountInBackend(configFilledIn); + + let existingAccountManager = Services.wm + .getMostRecentWindow("mailnews:accountmanager"); + if (existingAccountManager) { + existingAccountManager.focus(); + } else { + window.openDialog("chrome://messenger/content/AccountManager.xul", + "AccountManager", "chrome,centerscreen,modal,titlebar", + { server: newAccount.incomingServer, + selectPage: "am-server.xul" }); + } + window.close(); + }, + + /** + * [Re-test] button click handler. + * Restarts the config guessing process after a person editing the server + * fields. + * It's called "half-manual", because we take the user-entered values + * as given and will not second-guess them, to respect the user wishes. + * (Yes, Sir! Will do as told!) + * The values that the user left empty or on "Auto" will be guessed/probed + * here. We will also check that the user-provided values work. + */ + onHalfManualTest : function() + { + var newConfig = this.getUserConfig(); + gEmailWizardLogger.info(debugObject(newConfig, "manualConfigToTest")); + this.startSpinner("looking_up_settings_halfmanual"); + this.switchToMode("manual-edit-testing"); + // if (this._userPickedOutgoingServer) TODO + var self = this; + this._abortable = guessConfig(this._domain, + function(type, hostname, port, ssl, done, config) // progress + { + gEmailWizardLogger.info("progress callback host " + hostname + + " port " + port + " type " + type); + }, + function(config) // success + { + self._abortable = null; + self._fillManualEditFields(config); + self.switchToMode("manual-edit-complete"); + self.stopSpinner("found_settings_halfmanual"); + }, + function(e, config) // guessconfig failed + { + if (e instanceof CancelledException) { + return; + } + self._abortable = null; + gEmailWizardLogger.info("guessConfig failed: " + e); + self.showErrorStatus("failed_to_find_settings"); + self.switchToMode("manual-edit-have-hostname"); + }, + newConfig, + newConfig.outgoing.existingServerKey ? "incoming" : "both"); + }, + + + + ///////////////////////////////////////////////////////////////// + // UI helper functions + + _prefillConfig : function(initialConfig) + { + var emailsplit = this._email.split("@"); + assert(emailsplit.length > 1); + var emaillocal = sanitize.nonemptystring(emailsplit[0]); + initialConfig.incoming.username = emaillocal; + initialConfig.outgoing.username = emaillocal; + return initialConfig; + }, + + clearError : function(which) + { + _hide(which); + _hide(which + "icon"); + e(which).textContent = ""; + }, + + setError : function(which, msg_name) + { + try { + _show(which); + _show(which + "icon"); + e(which).textContent = gStringsBundle.getString(msg_name); + window.sizeToContent(); + } catch (ex) { alertPrompt("missing error string", msg_name); } + }, + + + + ///////////////////////////////////////////////////////////////// + // Finish & dialog close functions + + onKeyDown : function(event) + { + let key = event.keyCode; + if (key == 27) { // Escape key + this.onCancel(); + return true; + } + if (key == 13) { // OK key + let buttons = [ + { id: "next_button", action: makeCallback(this, this.onNext) }, + { id: "create_button", action: makeCallback(this, this.onCreate) }, + { id: "half-manual-test_button", + action: makeCallback(this, this.onHalfManualTest) }, + ]; + for (let button of buttons) { + button.e = e(button.id); + if (button.e.hidden || button.e.disabled) { + continue; + } + button.action(); + return true; + } + } + return false; + }, + + onCancel : function() + { + window.close(); + // The window onclose handler will call onWizardShutdown for us. + }, + + onWizardShutdown : function() + { + if (this._abortable) { + this._abortable.cancel(new UserCancelledException()); + } + + if (this._okCallback) { + this._okCallback(); + } + gEmailWizardLogger.info("Shutting down email config dialog"); + }, + + + onCreate : function() + { + try { + gEmailWizardLogger.info("Create button clicked"); + + var configFilledIn = this.getConcreteConfig(); + var self = this; + // If the dialog is not needed, it will go straight to OK callback + gSecurityWarningDialog.open(this._currentConfig, configFilledIn, true, + function() // on OK + { + self.validateAndFinish(configFilledIn); + }, + function() {}); // on cancel, do nothing + } catch (ex) { + gEmailWizardLogger.error("Error creating account. ex=" + ex + + ", stack=" + ex.stack); + alertPrompt(gStringsBundle.getString("error_creating_account"), ex); + } + }, + + // called by onCreate() + validateAndFinish : function() + { + var configFilledIn = this.getConcreteConfig(); + + if (checkIncomingServerAlreadyExists(configFilledIn)) { + alertPrompt(gStringsBundle.getString("error_creating_account"), + gStringsBundle.getString("incoming_server_exists")); + return; + } + + if (configFilledIn.outgoing.addThisServer) { + let existingServer = checkOutgoingServerAlreadyExists(configFilledIn); + if (existingServer) { + configFilledIn.outgoing.addThisServer = false; + configFilledIn.outgoing.existingServerKey = existingServer.key; + } + } + + // TODO use a UI mode (switchToMode()) for verfication, too. + // But we need to go back to the previous mode, because we might be in + // "result" or "manual-edit-complete" mode. + _disable("create_button"); + _disable("half-manual-test_button"); + _disable("advanced-setup_button"); + // no stop button: backend has no ability to stop :-( + var self = this; + this.startSpinner("checking_password"); + // logic function defined in verifyConfig.js + verifyConfig( + configFilledIn, + // guess login config? + configFilledIn.source != AccountConfig.kSourceXML, + // TODO Instead, the following line would be correct, but I cannot use it, + // because some other code doesn't adhere to the expectations/specs. + // Find out what it was and fix it. + //concreteConfig.source == AccountConfig.kSourceGuess, + this._parentMsgWindow, + function(successfulConfig) // success + { + self.stopSpinner(successfulConfig.incoming.password ? + "password_ok" : null); + + // the auth might have changed, so we + // should back-port it to the current config. + self._currentConfig.incoming.auth = successfulConfig.incoming.auth; + self._currentConfig.outgoing.auth = successfulConfig.outgoing.auth; + self._currentConfig.incoming.username = successfulConfig.incoming.username; + self._currentConfig.outgoing.username = successfulConfig.outgoing.username; + + // We loaded dynamic client registration, fill this data back in to the + // config set. + if (successfulConfig.oauthSettings) + self._currentConfig.oauthSettings = successfulConfig.oauthSettings; + + self.finish(); + }, + function(e) // failed + { + self.showErrorStatus("config_unverifiable"); + // TODO bug 555448: wrong error msg, there may be a 1000 other + // reasons why this failed, and this is misleading users. + self.setError("passworderror", "user_pass_invalid"); + // TODO use switchToMode(), see above + // give user something to proceed after fixing + _enable("create_button"); + // hidden in non-manual mode, so it's fine to enable + _enable("half-manual-test_button"); + _enable("advanced-setup_button"); + }); + }, + + finish : function() + { + gEmailWizardLogger.info("creating account in backend"); + createAccountInBackend(this.getConcreteConfig()); + window.close(); + }, +}; + +var gEmailConfigWizard = new EmailConfigWizard(); + +function serverMatches(a, b) +{ + return a.type == b.type && + a.hostname == b.hostname && + a.port == b.port && + a.socketType == b.socketType && + a.auth == b.auth; +} + +var _gStandardPorts = {}; +_gStandardPorts["imap"] = [ 143, 993 ]; +_gStandardPorts["pop3"] = [ 110, 995 ]; +_gStandardPorts["smtp"] = [ 587, 25, 465 ]; // order matters +var _gAllStandardPorts = _gStandardPorts["smtp"] + .concat(_gStandardPorts["imap"]).concat(_gStandardPorts["pop3"]); + +function isStandardPort(port) +{ + return _gAllStandardPorts.indexOf(port) != -1; +} + +function getStandardPorts(protocolType) +{ + return _gStandardPorts[protocolType]; +} + + +/** + * Warning dialog, warning user about lack of, or inappropriate, encryption. + * + * This is effectively a separate dialog, but implemented as part of + * this dialog. It works by hiding the main dialog part and unhiding + * the this part, and vice versa, and resizing the dialog. + */ +function SecurityWarningDialog() +{ + this._acknowledged = new Array(); +} +SecurityWarningDialog.prototype = +{ + /** + * {Array of {(incoming or outgoing) server part of {AccountConfig}} + * A list of the servers for which we already showed this dialog and the + * user approved the configs. For those, we won't show the warning again. + * (Make sure to store a copy in case the underlying object is changed.) + */ + _acknowledged : null, + + _inSecurityBad: 0x0001, + _inCertBad: 0x0010, + _outSecurityBad: 0x0100, + _outCertBad: 0x1000, + + /** + * Checks whether we need to warn about this config. + * + * We (currently) warn if + * - the mail travels unsecured (no SSL/STARTTLS) + * - the SSL certificate is not proper + * - (We don't warn about unencrypted passwords specifically, + * because they'd be encrypted with SSL and without SSL, we'd + * warn anyways.) + * + * We may not warn despite these conditions if we had shown the + * warning for that server before and the user acknowledged it. + * (Given that this dialog object is static/global and persistent, + * we can store that approval state here in this object.) + * + * @param configSchema @see open() + * @param configFilledIn @see open() + * @returns {Boolean} true when the dialog should be shown + * (call open()). if false, the dialog can and should be skipped. + */ + needed : function(configSchema, configFilledIn) + { + assert(configSchema instanceof AccountConfig); + assert(configFilledIn instanceof AccountConfig); + assert(configSchema.isComplete()); + assert(configFilledIn.isComplete()); + + var incomingBad = ((configFilledIn.incoming.socketType > 1) ? 0 : this._inSecurityBad) | + ((configFilledIn.incoming.badCert) ? this._inCertBad : 0); + var outgoingBad = 0; + if (!configFilledIn.outgoing.existingServerKey) { + outgoingBad = ((configFilledIn.outgoing.socketType > 1) ? 0 : this._outSecurityBad) | + ((configFilledIn.outgoing.badCert) ? this._outCertBad : 0); + } + + if (incomingBad > 0) { + if (this._acknowledged.some( + function(ackServer) { + return serverMatches(ackServer, configFilledIn.incoming); + })) + incomingBad = 0; + } + if (outgoingBad > 0) { + if (this._acknowledged.some( + function(ackServer) { + return serverMatches(ackServer, configFilledIn.outgoing); + })) + outgoingBad = 0; + } + + return incomingBad | outgoingBad; + }, + + /** + * Opens the dialog, fills it with values, and shows it to the user. + * + * The function is async: it returns immediately, and when the user clicks + * OK or Cancel, the callbacks are called. There the callers proceed as + * appropriate. + * + * @param configSchema The config, with placeholders not replaced yet. + * This object may be modified to store the user's confirmations, but + * currently that's not the case. + * @param configFilledIn The concrete config with placeholders replaced. + * @param onlyIfNeeded {Boolean} If there is nothing to warn about, + * call okCallback() immediately (and sync). + * @param okCallback {function(config {AccountConfig})} + * Called when the user clicked OK and approved the config including + * the warnings. |config| is without placeholders replaced. + * @param cancalCallback {function()} + * Called when the user decided to heed the warnings and not approve. + */ + open : function(configSchema, configFilledIn, onlyIfNeeded, + okCallback, cancelCallback) + { + assert(typeof(okCallback) == "function"); + assert(typeof(cancelCallback) == "function"); + // needed() also checks the parameters + var needed = this.needed(configSchema, configFilledIn); + if ((needed == 0) && onlyIfNeeded) { + okCallback(); + return; + } + + assert(needed > 0 , "security dialog opened needlessly"); + this._currentConfigFilledIn = configFilledIn; + this._okCallback = okCallback; + this._cancelCallback = cancelCallback; + var incoming = configFilledIn.incoming; + var outgoing = configFilledIn.outgoing; + + _hide("mastervbox"); + _show("warningbox"); + // reset dialog, in case we've shown it before + e("acknowledge_warning").checked = false; + _disable("iknow"); + e("incoming_technical").removeAttribute("expanded"); + e("incoming_details").setAttribute("collapsed", true); + e("outgoing_technical").removeAttribute("expanded"); + e("outgoing_details").setAttribute("collapsed", true); + + if (needed & this._inSecurityBad) { + setText("warning_incoming", gStringsBundle.getFormattedString( + "cleartext_warning", [incoming.hostname])); + setText("incoming_details", gStringsBundle.getString( + "cleartext_details")); + _show("incoming_box"); + } else if (needed & this._inCertBad) { + setText("warning_incoming", gStringsBundle.getFormattedString( + "selfsigned_warning", [incoming.hostname])); + setText("incoming_details", gStringsBundle.getString( + "selfsigned_details")); + _show("incoming_box"); + } else { + _hide("incoming_box"); + } + + if (needed & this._outSecurityBad) { + setText("warning_outgoing", gStringsBundle.getFormattedString( + "cleartext_warning", [outgoing.hostname])); + setText("outgoing_details", gStringsBundle.getString( + "cleartext_details")); + _show("outgoing_box"); + } else if (needed & this._outCertBad) { + setText("warning_outgoing", gStringsBundle.getFormattedString( + "selfsigned_warning", [outgoing.hostname])); + setText("outgoing_details", gStringsBundle.getString( + "selfsigned_details")); + _show("outgoing_box"); + } else { + _hide("outgoing_box"); + } + _show("acknowledge_warning"); + assert(!e("incoming_box").hidden || !e("outgoing_box").hidden, + "warning dialog shown for unknown reason"); + + window.sizeToContent(); + }, + + toggleDetails : function (id) + { + let details = e(id + "_details"); + let tech = e(id + "_technical"); + if (details.getAttribute("collapsed")) { + details.removeAttribute("collapsed"); + tech.setAttribute("expanded", true); + } else { + details.setAttribute("collapsed", true); + tech.removeAttribute("expanded"); + } + }, + + /** + * user checked checkbox that he understood it and wishes + * to ignore the warning. + */ + toggleAcknowledge : function() + { + if (e("acknowledge_warning").checked) { + _enable("iknow"); + } else { + _disable("iknow"); + } + }, + + /** + * [Cancel] button pressed. Get me out of here! + */ + onCancel : function() + { + _hide("warningbox"); + _show("mastervbox"); + window.sizeToContent(); + + this._cancelCallback(); + }, + + /** + * [OK] button pressed. + * Implies that the user toggled the acknowledge checkbox, + * i.e. approved the config and ignored the warnings, + * otherwise the button would have been disabled. + */ + onOK : function() + { + assert(e("acknowledge_warning").checked); + + var overrideOK = this.showCertOverrideDialog(this._currentConfigFilledIn); + if (!overrideOK) { + this.onCancel(); + return; + } + + // need filled in, in case hostname is placeholder + var storeConfig = this._currentConfigFilledIn.copy(); + this._acknowledged.push(storeConfig.incoming); + this._acknowledged.push(storeConfig.outgoing); + + _show("mastervbox"); + _hide("warningbox"); + window.sizeToContent(); + + this._okCallback(); + }, + + /** + * Shows a(nother) dialog which allows the user to see and override + * (manually accept) a bad certificate. It also optionally adds it + * permanently to the "good certs" store of NSS in the profile. + * Only shows the dialog, if there are bad certs. Otherwise, it's a no-op. + * + * The dialog is the standard PSM cert override dialog. + * + * @param config {AccountConfig} concrete + * @returns true, if all certs are fine or the user accepted them. + * false, if the user cancelled. + * + * static function + * sync function: blocks until the dialog is closed. + */ + showCertOverrideDialog : function(config) + { + if (config.incoming.socketType > 1 && // SSL or STARTTLS + config.incoming.badCert) { + var params = { + exceptionAdded : false, + prefetchCert : true, + location : config.incoming.targetSite, + }; + window.openDialog("chrome://pippki/content/exceptionDialog.xul", + "","chrome,centerscreen,modal", params); + if (params.exceptionAdded) { // set by dialog + config.incoming.badCert = false; + } else { + return false; + } + } + if (!config.outgoing.existingServerKey) { + if (config.outgoing.socketType > 1 && // SSL or STARTTLS + config.outgoing.badCert) { + var params = { + exceptionAdded : false, + prefetchCert : true, + location : config.outgoing.targetSite, + }; + window.openDialog("chrome://pippki/content/exceptionDialog.xul", + "","chrome,centerscreen,modal", params); + if (params.exceptionAdded) { // set by dialog + config.outgoing.badCert = false; + } else { + return false; + } + } + } + return true; + }, +} +var gSecurityWarningDialog = new SecurityWarningDialog(); diff --git a/mailnews/base/prefs/content/accountcreation/emailWizard.xul b/mailnews/base/prefs/content/accountcreation/emailWizard.xul new file mode 100644 index 000000000..0777d1651 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/emailWizard.xul @@ -0,0 +1,493 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> +<?xml-stylesheet href="chrome://messenger/skin/accountCreation.css" + type="text/css"?> + +<!DOCTYPE window [ + <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd"> + %brandDTD; + <!ENTITY % acDTD SYSTEM "chrome://messenger/locale/accountCreation.dtd"> + %acDTD; +]> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="autoconfigWizard" + windowtype="mail:autoconfig" + title="&autoconfigWizard.title;" + onload="gEmailConfigWizard.onLoad();" + onkeypress="gEmailConfigWizard.onKeyDown(event);" + onclose="gEmailConfigWizard.onWizardShutdown();" + onunload="gEmailConfigWizard.onWizardShutdown();" + > + + <stringbundleset> + <stringbundle id="bundle_brand" + src="chrome://branding/locale/brand.properties"/> + <stringbundle id="strings" + src="chrome://messenger/locale/accountCreation.properties"/> + <stringbundle id="utilstrings" + src="chrome://messenger/locale/accountCreationUtil.properties"/> + <stringbundle id="bundle_messenger" + src="chrome://messenger/locale/messenger.properties"/> + </stringbundleset> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/util.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/accountConfig.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/emailWizard.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/sanitizeDatatypes.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/fetchhttp.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/readFromXML.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/guessConfig.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/verifyConfig.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/fetchConfig.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/createInBackend.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountcreation/MyBadCertHandler.js"/> + <script type="application/javascript" + src="chrome://messenger/content/accountUtils.js" /> + + <keyset id="mailKeys"> + <key keycode="VK_ESCAPE" oncommand="window.close();"/> + </keyset> + + <panel id="insecureserver-cleartext-panel" class="popup-panel"> + <hbox> + <image class="insecureLarry"/> + <vbox flex="1"> + <description class="title">&insecureServer.tooltip.title;</description> + <description class="details"> + &insecureUnencrypted.description;</description> + </vbox> + </hbox> + </panel> + <panel id="insecureserver-selfsigned-panel" class="popup-panel"> + <hbox> + <image class="insecureLarry"/> + <vbox flex="1"> + <description class="title">&insecureServer.tooltip.title;</description> + <description class="details"> + &insecureSelfSigned.description;</description> + </vbox> + </hbox> + </panel> + <panel id="secureserver-panel" class="popup-panel"> + <hbox> + <image class="secureLarry"/> + <vbox flex="1"> + <description class="title">&secureServer.description;</description> + </vbox> + </hbox> + </panel> + + <tooltip id="insecureserver-cleartext"> + <hbox> + <image class="insecureLarry"/> + <vbox> + <description class="title">&insecureServer.tooltip.title;</description> + <description class="details"> + &insecureServer.tooltip.details;</description> + </vbox> + </hbox> + </tooltip> + <tooltip id="insecureserver-selfsigned"> + <hbox> + <image class="insecureLarry"/> + <vbox> + <description class="title">&insecureServer.tooltip.title;</description> + <description class="details"> + &insecureServer.tooltip.details;</description> + </vbox> + </hbox> + </tooltip> + <tooltip id="secureservertooltip"> + <hbox> + <image class="secureLarry"/> + <description class="title">&secureServer.description;</description> + </hbox> + </tooltip> + <tooltip id="optional-password"> + <description>&password.text;</description> + </tooltip> + + <spacer id="fullwidth"/> + + <vbox id="mastervbox" class="mastervbox" flex="1"> + <grid id="initialSettings"> + <columns> + <column/> + <column/> + <column/> + </columns> + <rows> + <row align="center"> + <label accesskey="&name.accesskey;" + class="autoconfigLabel" + value="&name.label;" + control="realname"/> + <textbox id="realname" + class="padded" + placeholder="&name.placeholder;" + oninput="gEmailConfigWizard.onInputRealname();" + onblur="gEmailConfigWizard.onBlurRealname();"/> + <hbox> + <description id="nametext" class="initialDesc">&name.text;</description> + <image id="nameerroricon" + hidden="true" + class="warningicon"/> + <description id="nameerror" class="errordescription" hidden="true"/> + </hbox> + </row> + <row align="center"> + <label accesskey="&email.accesskey;" + class="autoconfigLabel" + value="&email.label;" + control="email"/> + <textbox id="email" + class="padded uri-element" + placeholder="&email.placeholder;" + oninput="gEmailConfigWizard.onInputEmail();" + onblur="gEmailConfigWizard.onBlurEmail();"/> + <hbox> + <image id="emailerroricon" + hidden="true" + class="warningicon"/> + <description id="emailerror" class="errordescription" hidden="true"/> + </hbox> + </row> + <row align="center"> + <!-- this starts out as text so the emptytext shows, but then + changes to type=password once it's not empty --> + <label accesskey="&password.accesskey;" + class="autoconfigLabel" + value="&password.label;" + control="password" + tooltip="optional-password"/> + <textbox id="password" + class="padded" + placeholder="&password.placeholder;" + type="text" + oninput="gEmailConfigWizard.onInputPassword();" + onfocus="gEmailConfigWizard.onFocusPassword();" + onblur="gEmailConfigWizard.onBlurPassword();"/> + <hbox> + <image id="passworderroricon" + hidden="true" + class="warningicon"/> + <description id="passworderror" class="errordescription" hidden="true"/> + </hbox> + </row> + <row align="center" pack="start"> + <label class="autoconfigLabel"/> + <checkbox id="remember_password" + label="&rememberPassword.label;" + accesskey="&rememberPassword.accesskey;" + checked="true"/> + </row> + </rows> + </grid> + <spacer flex="1" /> + + <hbox id="status_area" flex="1"> + <vbox id="status_img_before" pack="start"/> + <description id="status_msg"> </description> + <!-- Include 160 = nbsp, to make the element occupy the + full height, for at least one line. With a normal space, + it does not have sufficient height. --> + <vbox id="status_img_after" pack="start"/> + </hbox> + + <groupbox id="result_area" hidden="true"> + <radiogroup id="result_imappop" orient="horizontal"> + <radio id="result_select_imap" label="&imapLong.label;" value="1" + oncommand="gEmailConfigWizard.onResultIMAPOrPOP3();"/> + <radio id="result_select_pop3" label="&pop3Long.label;" value="2" + oncommand="gEmailConfigWizard.onResultIMAPOrPOP3();"/> + </radiogroup> + <grid> + <columns> + <column/> + <column flex="1"/> + </columns> + <rows> + <row align="center"> + <label class="textbox-label" value="&incoming.label;" + control="result-incoming"/> + <textbox id="result-incoming" disabled="true" flex="1"/> + </row> + <row align="center"> + <label class="textbox-label" value="&outgoing.label;" + control="result-outgoing"/> + <textbox id="result-outgoing" disabled="true" flex="1"/> + </row> + <row align="center"> + <label class="textbox-label" value="&username.label;" + control="result-username"/> + <textbox id="result-username" disabled="true" flex="1"/> + </row> + </rows> + </grid> + </groupbox> + + <groupbox id="manual-edit_area" hidden="true"> + <grid> + <columns> + <column/><!-- row label, e.g. "incoming" --> + <column/><!-- protocol, e.g. "IMAP" --> + <column flex="1"/><!-- hostname / username --> + <column/><!-- port --> + <column/><!-- SSL --> + <column/><!-- auth method --> + </columns> + <rows> + <row id="labels_row" align="center"> + <spacer/> + <spacer/> + <label value="&hostname.label;" class="columnHeader"/> + <label value="&port.label;" class="columnHeader"/> + <label value="&ssl.label;" class="columnHeader"/> + <label value="&auth.label;" class="columnHeader"/> + </row> + <row id="incoming_server_area"> + <hbox align="center" pack="end"> + <label class="textbox-label" + value="&incoming.label;" + control="incoming_hostname"/> + </hbox> + <menulist id="incoming_protocol" + oncommand="gEmailConfigWizard.onChangedProtocolIncoming();" + sizetopopup="always"> + <menupopup> + <menuitem label="&imap.label;" value="1"/> + <menuitem label="&pop3.label;" value="2"/> + </menupopup> + </menulist> + <textbox id="incoming_hostname" + oninput="gEmailConfigWizard.onInputHostname();" + class="host uri-element"/> + <menulist id="incoming_port" + editable="true" + oninput="gEmailConfigWizard.onChangedPortIncoming();" + oncommand="gEmailConfigWizard.onChangedPortIncoming();" + class="port"> + <menupopup/> + </menulist> + <menulist id="incoming_ssl" + class="security" + oncommand="gEmailConfigWizard.onChangedSSLIncoming();" + sizetopopup="always"> + <menupopup> + <!-- values defined in nsMsgSocketType --> + <menuitem label="&autodetect.label;" value="0"/> + <menuitem label="&noEncryption.label;" value="1"/> + <menuitem label="&starttls.label;" value="3"/> + <menuitem label="&sslTls.label;" value="2"/> + </menupopup> + </menulist> + <menulist id="incoming_authMethod" + class="auth" + oncommand="gEmailConfigWizard.onChangedInAuth();" + sizetopopup="always"> + <menupopup> + <menuitem label="&autodetect.label;" value="0"/> + <!-- values defined in nsMsgAuthMethod --> + <!-- labels set from messenger.properties + to avoid duplication --> + <menuitem id="in-authMethod-password-cleartext" value="3"/> + <menuitem id="in-authMethod-password-encrypted" value="4"/> + <menuitem id="in-authMethod-kerberos" value="5"/> + <menuitem id="in-authMethod-ntlm" value="6"/> + <menuitem id="in-authMethod-oauth2" value="10" hidden="true"/> + </menupopup> + </menulist> + </row> + <row id="outgoing_server_area" align="center"> + <label class="textbox-label" + value="&outgoing.label;" + control="outgoing_hostname"/> + <label id="outgoing_protocol" + value="&smtp.label;"/> + <menulist id="outgoing_hostname" + editable="true" + sizetopopup="none" + oninput="gEmailConfigWizard.onInputHostname();" + oncommand="gEmailConfigWizard.onChangedOutgoingDropdown();" + onpopupshowing="gEmailConfigWizard.onOpenOutgoingDropdown();" + class="host uri-element"> + <menupopup id="outgoing_hostname_popup"/> + </menulist> + <menulist id="outgoing_port" + editable="true" + oninput="gEmailConfigWizard.onChangedPortOutgoing();" + oncommand="gEmailConfigWizard.onChangedPortOutgoing();" + class="port"> + <menupopup/> + </menulist> + <menulist id="outgoing_ssl" + class="security" + oncommand="gEmailConfigWizard.onChangedSSLOutgoing();" + sizetopopup="always"> + <menupopup> + <!-- @see incoming --> + <menuitem label="&autodetect.label;" value="0"/> + <menuitem label="&noEncryption.label;" value="1"/> + <menuitem label="&starttls.label;" value="3"/> + <menuitem label="&sslTls.label;" value="2"/> + </menupopup> + </menulist> + <menulist id="outgoing_authMethod" + class="auth" + oncommand="gEmailConfigWizard.onChangedOutAuth(this.selectedItem);" + sizetopopup="always"> + <menupopup> + <menuitem label="&autodetect.label;" value="0"/> + <!-- @see incoming --> + <menuitem id="out-authMethod-no" value="1"/> + <menuitem id="out-authMethod-password-cleartext" value="3"/> + <menuitem id="out-authMethod-password-encrypted" value="4"/> + <menuitem id="out-authMethod-kerberos" value="5"/> + <menuitem id="out-authMethod-ntlm" value="6"/> + <menuitem id="out-authMethod-oauth2" value="10" hidden="true"/> + </menupopup> + </menulist> + </row> + <row id="username_area" align="center"> + <label class="textbox-label" + value="&username.label;"/> + <label class="columnHeader" + value="&incoming.label;" + control="incoming_username"/> + <textbox id="incoming_username" + oninput="gEmailConfigWizard.onInputInUsername();" + class="username"/> + <spacer/> + <label class="columnHeader" + id="outgoing_label" + value="&outgoing.label;" + control="outgoing_username"/> + <textbox id="outgoing_username" + oninput="gEmailConfigWizard.onInputOutUsername();" + class="username"/> + </row> + </rows> + </grid> + </groupbox> + + <spacer flex="1" /> + <hbox id="buttons_area"> + <hbox id="left_buttons_area" align="center" pack="start"> + <button id="provisioner_button" + label="&switch-to-provisioner.label;" + accesskey="&switch-to-provisioner.accesskey;" + class="larger-button" + oncommand="gEmailConfigWizard.onSwitchToProvisioner();"/> + <button id="manual-edit_button" + label="&manual-edit.label;" + accesskey="&manual-edit.accesskey;" + hidden="true" + oncommand="gEmailConfigWizard.onManualEdit();"/> + <button id="advanced-setup_button" + label="&advancedSetup.label;" + accesskey="&advancedSetup.accesskey;" + disabled="true" + hidden="true" + oncommand="gEmailConfigWizard.onAdvancedSetup();"/> + </hbox> + <spacer flex="1"/> + <hbox id="right_buttons_area" align="center" pack="end"> + <button id="stop_button" + label="&stop.label;" + accesskey="&stop.accesskey;" + hidden="true" + oncommand="gEmailConfigWizard.onStop();"/> + <button id="cancel_button" + label="&cancel.label;" + accesskey="&cancel.accesskey;" + oncommand="gEmailConfigWizard.onCancel();"/> + <button id="half-manual-test_button" + label="&half-manual-test.label;" + accesskey="&half-manual-test.accesskey;" + hidden="true" + oncommand="gEmailConfigWizard.onHalfManualTest();"/> + <button id="next_button" + label="&continue.label;" + accesskey="&continue.accesskey;" + hidden="false" + oncommand="gEmailConfigWizard.onNext();"/> + <button id="create_button" + label="&doneAccount.label;" + accesskey="&doneAccount.accesskey;" + class="important-button" + hidden="true" + oncommand="gEmailConfigWizard.onCreate();"/> + </hbox> + </hbox> + </vbox> + + + <vbox id="warningbox" hidden="true" flex="1"> + <hbox class="warning" flex="1"> + <vbox class="larrybox"> + <image id="insecure_larry" class="insecureLarry"/> + </vbox> + <vbox flex="1" class="warning_text"> + <label class="warning-heading">&warning.label;</label> + <vbox id="incoming_box"> + <hbox> + <label class="warning_settings" value="&incomingSettings.label;"/> + <description id="warning_incoming"/> + </hbox> + <label id="incoming_technical" + class="technical_details" + value="&technicaldetails.label;" + onclick="gSecurityWarningDialog.toggleDetails('incoming');"/> + <description id="incoming_details" collapsed="true"/> + </vbox> + <vbox id="outgoing_box"> + <hbox> + <label class="warning_settings" value="&outgoingSettings.label;"/> + <description id="warning_outgoing"/> + </hbox> + <label id="outgoing_technical" + class="technical_details" + value="&technicaldetails.label;" + onclick="gSecurityWarningDialog.toggleDetails('outgoing');"/> + <description id="outgoing_details" collapsed="true"/> + </vbox> + <spacer flex="10"/> + <description id="findoutmore"> + &contactYourProvider.description;</description> + <spacer flex="100"/> + <checkbox id="acknowledge_warning" + label="&confirmWarning.label;" + accesskey="&confirmWarning.accesskey;" + class="acknowledge_checkbox" + oncommand="gSecurityWarningDialog.toggleAcknowledge()"/> + <hbox> + <button id="getmeoutofhere" + label="&changeSettings.label;" + accesskey="&changeSettings.accesskey;" + oncommand="gSecurityWarningDialog.onCancel()"/> + <spacer flex="1"/> + <button id="iknow" + label="&doneAccount.label;" + accesskey="&doneAccount.accesskey;" + disabled="true" + oncommand="gSecurityWarningDialog.onOK()"/> + </hbox> + </vbox> + </hbox> + </vbox> +</window> diff --git a/mailnews/base/prefs/content/accountcreation/fetchConfig.js b/mailnews/base/prefs/content/accountcreation/fetchConfig.js new file mode 100644 index 000000000..07a9f5586 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/fetchConfig.js @@ -0,0 +1,240 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tries to find a configuration for this ISP on the local harddisk, in the + * application install directory's "isp" subdirectory. + * Params @see fetchConfigFromISP() + */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/JXON.js"); + + +function fetchConfigFromDisk(domain, successCallback, errorCallback) +{ + return new TimeoutAbortable(runAsync(function() + { + try { + // <TB installdir>/isp/example.com.xml + var configLocation = Services.dirsvc.get("CurProcD", Ci.nsIFile); + configLocation.append("isp"); + configLocation.append(sanitize.hostname(domain) + ".xml"); + + var contents = + readURLasUTF8(Services.io.newFileURI(configLocation)); + let domParser = Cc["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Ci.nsIDOMParser); + successCallback(readFromXML(JXON.build( + domParser.parseFromString(contents, "text/xml")))); + } catch (e) { errorCallback(e); } + })); +} + +/** + * Tries to get a configuration from the ISP / mail provider directly. + * + * Disclaimers: + * - To support domain hosters, we cannot use SSL. That means we + * rely on insecure DNS and http, which means the results may be + * forged when under attack. The same is true for guessConfig(), though. + * + * @param domain {String} The domain part of the user's email address + * @param emailAddress {String} The user's email address + * @param successCallback {Function(config {AccountConfig}})} A callback that + * will be called when we could retrieve a configuration. + * The AccountConfig object will be passed in as first parameter. + * @param errorCallback {Function(ex)} A callback that + * will be called when we could not retrieve a configuration, + * for whatever reason. This is expected (e.g. when there's no config + * for this domain at this location), + * so do not unconditionally show this to the user. + * The first paramter will be an exception object or error string. + */ +function fetchConfigFromISP(domain, emailAddress, successCallback, + errorCallback) +{ + if (!Services.prefs.getBoolPref( + "mailnews.auto_config.fetchFromISP.enabled")) { + errorCallback("ISP fetch disabled per user preference"); + return; + } + + let url1 = "http://autoconfig." + sanitize.hostname(domain) + + "/mail/config-v1.1.xml"; + // .well-known/ <http://tools.ietf.org/html/draft-nottingham-site-meta-04> + let url2 = "http://" + sanitize.hostname(domain) + + "/.well-known/autoconfig/mail/config-v1.1.xml"; + let sucAbortable = new SuccessiveAbortable(); + var time = Date.now(); + var urlArgs = { emailaddress: emailAddress }; + if (!Services.prefs.getBoolPref( + "mailnews.auto_config.fetchFromISP.sendEmailAddress")) { + delete urlArgs.emailaddress; + } + let fetch1 = new FetchHTTP(url1, urlArgs, false, + function(result) + { + successCallback(readFromXML(result)); + }, + function(e1) // fetch1 failed + { + ddump("fetchisp 1 <" + url1 + "> took " + (Date.now() - time) + + "ms and failed with " + e1); + time = Date.now(); + if (e1 instanceof CancelledException) + { + errorCallback(e1); + return; + } + + let fetch2 = new FetchHTTP(url2, urlArgs, false, + function(result) + { + successCallback(readFromXML(result)); + }, + function(e2) + { + ddump("fetchisp 2 <" + url2 + "> took " + (Date.now() - time) + + "ms and failed with " + e2); + // return the error for the primary call, + // unless the fetch was cancelled + errorCallback(e2 instanceof CancelledException ? e2 : e1); + }); + sucAbortable.current = fetch2; + fetch2.start(); + }); + sucAbortable.current = fetch1; + fetch1.start(); + return sucAbortable; +} + +/** + * Tries to get a configuration for this ISP from a central database at + * Mozilla servers. + * Params @see fetchConfigFromISP() + */ + +function fetchConfigFromDB(domain, successCallback, errorCallback) +{ + let url = Services.prefs.getCharPref("mailnews.auto_config_url"); + domain = sanitize.hostname(domain); + + // If we don't specify a place to put the domain, put it at the end. + if (!url.includes("{{domain}}")) + url = url + domain; + else + url = url.replace("{{domain}}", domain); + url = url.replace("{{accounts}}", MailServices.accounts.accounts.length); + + if (!url.length) + return errorCallback("no fetch url set"); + let fetch = new FetchHTTP(url, null, false, + function(result) + { + successCallback(readFromXML(result)); + }, + errorCallback); + fetch.start(); + return fetch; +} + +/** + * Does a lookup of DNS MX, to get the server who is responsible for + * recieving mail for this domain. Then it takes the domain of that + * server, and does another lookup (in ISPDB and possible at ISP autoconfig + * server) and if such a config is found, returns that. + * + * Disclaimers: + * - DNS is unprotected, meaning the results could be forged. + * The same is true for fetchConfigFromISP() and guessConfig(), though. + * - DNS MX tells us the incoming server, not the mailbox (IMAP) server. + * They are different. This mechnism is only an approximation + * for hosted domains (yourname.com is served by mx.hoster.com and + * therefore imap.hoster.com - that "therefore" is exactly the + * conclusional jump we make here.) and alternative domains + * (e.g. yahoo.de -> yahoo.com). + * - We make a look up for the base domain. E.g. if MX is + * mx1.incoming.servers.hoster.com, we look up hoster.com. + * Thanks to Services.eTLD, we also get bbc.co.uk right. + * + * Params @see fetchConfigFromISP() + */ +function fetchConfigForMX(domain, successCallback, errorCallback) +{ + domain = sanitize.hostname(domain); + + var sucAbortable = new SuccessiveAbortable(); + var time = Date.now(); + sucAbortable.current = getMX(domain, + function(mxHostname) // success + { + ddump("getmx took " + (Date.now() - time) + "ms"); + let sld = Services.eTLD.getBaseDomainFromHost(mxHostname); + ddump("base domain " + sld + " for " + mxHostname); + if (sld == domain) + { + errorCallback("MX lookup would be no different from domain"); + return; + } + sucAbortable.current = fetchConfigFromDB(sld, successCallback, + errorCallback); + }, + errorCallback); + return sucAbortable; +} + +/** + * Queries the DNS MX for the domain + * + * The current implementation goes to a web service to do the + * DNS resolve for us, because Mozilla unfortunately has no implementation + * to do it. That's just a workaround. Once bug 545866 is fixed, we make + * the DNS query directly on the client. The API of this function should not + * change then. + * + * Returns (in successCallback) the hostname of the MX server. + * If there are several entires with different preference values, + * only the most preferred (i.e. those with the lowest value) + * is returned. If there are several most preferred servers (i.e. + * round robin), only one of them is returned. + * + * @param domain @see fetchConfigFromISP() + * @param successCallback {function(hostname {String}) + * Called when we found an MX for the domain. + * For |hostname|, see description above. + * @param errorCallback @see fetchConfigFromISP() + * @returns @see fetchConfigFromISP() + */ +function getMX(domain, successCallback, errorCallback) +{ + domain = sanitize.hostname(domain); + + let url = Services.prefs.getCharPref("mailnews.mx_service_url"); + if (!url) + errorCallback("no URL for MX service configured"); + url += domain; + + let fetch = new FetchHTTP(url, null, false, + function(result) + { + // result is plain text, with one line per server. + // So just take the first line + ddump("MX query result: \n" + result + "(end)"); + assert(typeof(result) == "string"); + let first = result.split("\n")[0]; + first.toLowerCase().replace(/[^a-z0-9\-_\.]*/g, ""); + if (first.length == 0) + { + errorCallback("no MX found"); + return; + } + successCallback(first); + }, + errorCallback); + fetch.start(); + return fetch; +} diff --git a/mailnews/base/prefs/content/accountcreation/fetchhttp.js b/mailnews/base/prefs/content/accountcreation/fetchhttp.js new file mode 100644 index 000000000..04f5272cd --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/fetchhttp.js @@ -0,0 +1,267 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This is a small wrapper around XMLHttpRequest, which solves various + * inadequacies of the API, e.g. error handling. It is entirely generic and + * can be used for purposes outside of even mail. + * + * It does not provide download progress, but assumes that the + * fetched resource is so small (<1 10 KB) that the roundtrip and + * response generation is far more significant than the + * download time of the response. In other words, it's fine for RPC, + * but not for bigger file downloads. + */ + +Components.utils.import("resource://gre/modules/JXON.js"); + +/** + * Set up a fetch. + * + * @param url {String} URL of the server function. + * ATTENTION: The caller needs to make sure that the URL is secure to call. + * @param urlArgs {Object, associative array} Parameters to add + * to the end of the URL as query string. E.g. + * { foo: "bla", bar: "blub blub" } will add "?foo=bla&bar=blub%20blub" + * to the URL + * (unless the URL already has a "?", then it adds "&foo..."). + * The values will be urlComponentEncoded, so pass them unencoded. + * @param post {Boolean} HTTP GET or POST + * Only influences the HTTP request method, + * i.e. first line of the HTTP request, not the body or parameters. + * Use POST when you modify server state, + * GET when you only request information. + * + * @param successCallback {Function(result {String})} + * Called when the server call worked (no errors). + * |result| will contain the body of the HTTP reponse, as string. + * @param errorCallback {Function(ex)} + * Called in case of error. ex contains the error + * with a user-displayable but not localized |.message| and maybe a + * |.code|, which can be either + * - an nsresult error code, + * - an HTTP result error code (0...1000) or + * - negative: 0...-100 : + * -2 = can't resolve server in DNS etc. + * -4 = response body (e.g. XML) malformed + */ +/* not yet supported: + * @param headers {Object, associative array} Like urlArgs, + * just that the params will be added as HTTP headers. + * { foo: "blub blub" } will add "Foo: Blub blub" + * The values will be urlComponentEncoded, apart from space, + * so pass them unencoded. + * @param headerArgs {Object, associative array} Like urlArgs, + * just that the params will be added as HTTP headers. + * { foo: "blub blub" } will add "X-Moz-Arg-Foo: Blub blub" + * The values will be urlComponentEncoded, apart from space, + * so pass them unencoded. + * @param bodyArgs {Object, associative array} Like urlArgs, + * just that the params will be sent x-url-encoded in the body, + * like a HTML form post. + * The values will be urlComponentEncoded, so pass them unencoded. + * This cannot be used together with |uploadBody|. + * @param uploadbody {Object} Arbitrary object, which to use as + * body of the HTTP request. Will also set the mimetype accordingly. + * Only supported object types, currently only E4X is supported + * (sending XML). + * Usually, you have nothing to upload, so just pass |null|. + */ +function FetchHTTP(url, urlArgs, post, successCallback, errorCallback) +{ + assert(typeof(successCallback) == "function", "BUG: successCallback"); + assert(typeof(errorCallback) == "function", "BUG: errorCallback"); + this._url = sanitize.string(url); + if (!urlArgs) + urlArgs = {}; + + this._urlArgs = urlArgs; + this._post = sanitize.boolean(post); + this._successCallback = successCallback; + this._errorCallback = errorCallback; +} +FetchHTTP.prototype = +{ + __proto__: Abortable.prototype, + _url : null, // URL as passed to ctor, without arguments + _urlArgs : null, + _post : null, + _successCallback : null, + _errorCallback : null, + _request : null, // the XMLHttpRequest object + result : null, + + start : function() + { + var url = this._url; + for (var name in this._urlArgs) + { + url += (!url.includes("?") ? "?" : "&") + + name + "=" + encodeURIComponent(this._urlArgs[name]); + } + this._request = new XMLHttpRequest(); + let request = this._request; + request.open(this._post ? "POST" : "GET", url); + request.channel.loadGroup = null; + // needs bug 407190 patch v4 (or higher) - uncomment if that lands. + // try { + // var channel = request.channel.QueryInterface(Ci.nsIHttpChannel2); + // channel.connectTimeout = 5; + // channel.requestTimeout = 5; + // } catch (e) { dump(e + "\n"); } + + var me = this; + request.onload = function() { me._response(true); } + request.onerror = function() { me._response(false); } + request.send(null); + }, + _response : function(success, exStored) + { + try + { + var errorCode = null; + var errorStr = null; + + if (success && this._request.status >= 200 && + this._request.status < 300) // HTTP level success + { + try + { + // response + var mimetype = this._request.getResponseHeader("Content-Type"); + if (!mimetype) + mimetype = ""; + mimetype = mimetype.split(";")[0]; + if (mimetype == "text/xml" || + mimetype == "application/xml" || + mimetype == "text/rdf") + { + this.result = JXON.build(this._request.responseXML); + } + else + { + //ddump("mimetype: " + mimetype + " only supported as text"); + this.result = this._request.responseText; + } + //ddump("result:\n" + this.result); + } + catch (e) + { + success = false; + errorStr = getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties") + .GetStringFromName("bad_response_content.error"); + errorCode = -4; + } + } + else + { + success = false; + try + { + errorCode = this._request.status; + errorStr = this._request.statusText; + } catch (e) { + // If we can't resolve the hostname in DNS etc., .statusText throws + errorCode = -2; + errorStr = getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties") + .GetStringFromName("cannot_contact_server.error"); + ddump(errorStr); + } + } + + // Callbacks + if (success) + { + try { + this._successCallback(this.result); + } catch (e) { + logException(e); + this._error(e); + } + } + else if (exStored) + this._error(exStored); + else + this._error(new ServerException(errorStr, errorCode, this._url)); + + if (this._finishedCallback) + { + try { + this._finishedCallback(this); + } catch (e) { + logException(e); + this._error(e); + } + } + + } catch (e) { + // error in our fetchhttp._response() code + logException(e); + this._error(e); + } + }, + _error : function(e) + { + try { + this._errorCallback(e); + } catch (e) { + // error in errorCallback, too! + logException(e); + alertPrompt("Error in errorCallback for fetchhttp", e); + } + }, + /** + * Call this between start() and finishedCallback fired. + */ + cancel : function(ex) + { + assert(!this.result, "Call already returned"); + + this._request.abort(); + + // Need to manually call error handler + // <https://bugzilla.mozilla.org/show_bug.cgi?id=218236#c11> + this._response(false, ex ? ex : new UserCancelledException()); + }, + /** + * Allows caller or lib to be notified when the call is done. + * This is useful to enable and disable a Cancel button in the UI, + * which allows to cancel the network request. + */ + setFinishedCallback : function(finishedCallback) + { + this._finishedCallback = finishedCallback; + } +} + +function CancelledException(msg) +{ + Exception.call(this, msg); +} +CancelledException.prototype = Object.create(Exception.prototype); +CancelledException.prototype.constructor = CancelledException; + +function UserCancelledException(msg) +{ + // The user knows they cancelled so I don't see a need + // for a message to that effect. + if (!msg) + msg = "User cancelled"; + CancelledException.call(this, msg); +} +UserCancelledException.prototype = Object.create(CancelledException.prototype); +UserCancelledException.prototype.constructor = UserCancelledException; + +function ServerException(msg, code, uri) +{ + Exception.call(this, msg); + this.code = code; + this.uri = uri; +} +ServerException.prototype = Object.create(Exception.prototype); +ServerException.prototype.constructor = ServerException; + diff --git a/mailnews/base/prefs/content/accountcreation/guessConfig.js b/mailnews/base/prefs/content/accountcreation/guessConfig.js new file mode 100644 index 000000000..755c499cd --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/guessConfig.js @@ -0,0 +1,1145 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Cu.import("resource:///modules/gloda/log4moz.js"); +Cu.import("resource://gre/modules/Services.jsm"); + +var TIMEOUT = 10; // in seconds + +// This is a bit ugly - we set outgoingDone to false +// when emailWizard.js cancels the outgoing probe because the user picked +// an outoing server. It does this by poking the probeAbortable object, +// so we need outgoingDone to have global scope. +var outgoingDone = false; + +/** + * Try to guess the config, by: + * - guessing hostnames (pop3.<domain>, pop.<domain>, imap.<domain>, + * mail.<domain> etc.) + * - probing known ports (for IMAP, POP3 etc., with SSL, STARTTLS etc.) + * - opening a connection via the right protocol and checking the + * protocol-specific CAPABILITIES like that the server returns. + * + * Final verification is not done here, but in verifyConfig(). + * + * This function is async. + * @param domain {String} the domain part of the email address + * @param progressCallback {function(type, hostname, port, ssl, done)} + * Called when we try a new hostname/port. + * type {String-enum} @see AccountConfig type - "imap", "pop3", "smtp" + * hostname {String} + * port {Integer} + * socketType {Integer-enum} @see AccountConfig.incoming.socketType + * 1 = plain, 2 = SSL, 3 = STARTTLS + * done {Boolean} false, if we start probing this host/port, true if we're + * done and the host is good. (there is no notification when a host is + * bad, we'll just tell about the next host tried) + * @param successCallback {function(config {AccountConfig})} + * Called when we could guess the config. + * param accountConfig {AccountConfig} The guessed account config. + * username, password, realname, emailaddress etc. are not filled out, + * but placeholders to be filled out via replaceVariables(). + * @param errorCallback function(ex) + * Called when we could guess not the config, either + * because we have not found anything or + * because there was an error (e.g. no network connection). + * The ex.message will contain a user-presentable message. + * @param resultConfig {AccountConfig} (optional) + * A config which may be partially filled in. If so, it will be used as base + * for the guess. + * @param which {String-enum} (optional) "incoming", "outgoing", or "both". + * Default "both". Whether to guess only the incoming or outgoing server. + * @result {Abortable} Allows you to cancel the guess + */ +function guessConfig(domain, progressCallback, successCallback, errorCallback, + resultConfig, which) +{ + assert(typeof(progressCallback) == "function", "need progressCallback"); + assert(typeof(successCallback) == "function", "need successCallback"); + assert(typeof(errorCallback) == "function", "need errorCallback"); + + // Servers that we know enough that they support OAuth2 do not need guessing. + if (resultConfig.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) { + successCallback(resultConfig); + return null; + } + + if (!resultConfig) + resultConfig = new AccountConfig(); + resultConfig.source = AccountConfig.kSourceGuess; + + if (!Services.prefs.getBoolPref( + "mailnews.auto_config.guess.enabled")) { + errorCallback("Guessing config disabled per user preference"); + return; + } + + var incomingHostDetector = null; + var outgoingHostDetector = null; + var incomingEx = null; // if incoming had error, store ex here + var outgoingEx = null; // if incoming had error, store ex here + var incomingDone = (which == "outgoing"); + var outgoingDone = (which == "incoming"); + // If we're offline, we're going to pick the most common settings. + // (Not the "best" settings, but common). + if (Services.io.offline) + { + resultConfig.source = AccountConfig.kSourceUser; + resultConfig.incoming.hostname = "mail." + domain; + resultConfig.incoming.username = resultConfig.identity.emailAddress; + resultConfig.outgoing.username = resultConfig.identity.emailAddress; + resultConfig.incoming.type = "imap"; + resultConfig.incoming.port = 143; + resultConfig.incoming.socketType = 3; // starttls + resultConfig.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext; + resultConfig.outgoing.hostname = "smtp." + domain; + resultConfig.outgoing.socketType = 1; + resultConfig.outgoing.port = 587; + resultConfig.outgoing.auth = Ci.nsMsgAuthMethod.passwordCleartext; + resultConfig.incomingAlternatives.push({ + hostname: "mail." + domain, + username: resultConfig.identity.emailAddress, + type: "pop3", + port: 110, + socketType: 3, + auth: Ci.nsMsgAuthMethod.passwordCleartext + }); + successCallback(resultConfig); + return null; + } + var progress = function(thisTry) + { + progressCallback(protocolToString(thisTry.protocol), thisTry.hostname, + thisTry.port, sslConvertToSocketType(thisTry.ssl), false, + resultConfig); + }; + + var updateConfig = function(config) + { + resultConfig = config; + }; + + var errorInCallback = function(e) + { + // The caller's errorCallback threw. + // hopefully shouldn't happen for users. + alertPrompt("Error in errorCallback for guessConfig()", e); + }; + + var checkDone = function() + { + if (incomingEx) + { + try { + errorCallback(incomingEx, resultConfig); + } catch (e) { errorInCallback(e); } + return; + } + if (outgoingEx) + { + try { + errorCallback(outgoingEx, resultConfig); + } catch (e) { errorInCallback(e); } + return; + } + if (incomingDone && outgoingDone) + { + try { + successCallback(resultConfig); + } catch (e) { + try { + errorCallback(e); + } catch (e) { errorInCallback(e); } + } + return; + } + }; + + var logger = Log4Moz.getConfiguredLogger("mail.wizard"); + var HostTryToAccountServer = function(thisTry, server) + { + server.type = protocolToString(thisTry.protocol); + server.hostname = thisTry.hostname; + server.port = thisTry.port; + server.socketType = sslConvertToSocketType(thisTry.ssl); + server.auth = chooseBestAuthMethod(thisTry.authMethods); + server.authAlternatives = thisTry.authMethods; + // TODO + // cert is also bad when targetSite is set. (Same below for incoming.) + // Fix SSLErrorHandler and security warning dialog in emailWizard.js. + server.badCert = thisTry.selfSignedCert; + server.targetSite = thisTry.targetSite; + logger.info("CHOOSING " + server.type + " "+ server.hostname + ":" + + server.port + ", auth method " + server.auth + " " + + server.authAlternatives.join(",") + ", SSL " + server.socketType + + (server.badCert ? " (bad cert!)" : "")); + }; + + var outgoingSuccess = function(thisTry, alternativeTries) + { + assert(thisTry.protocol == SMTP, "I only know SMTP for outgoing"); + // Ensure there are no previously saved outgoing errors, if we've got + // success here. + outgoingEx = null; + HostTryToAccountServer(thisTry, resultConfig.outgoing); + + for (let alternativeTry of alternativeTries) + { + // resultConfig.createNewOutgoing(); misses username etc., so copy + let altServer = deepCopy(resultConfig.outgoing); + HostTryToAccountServer(alternativeTry, altServer); + assert(resultConfig.outgoingAlternatives); + resultConfig.outgoingAlternatives.push(altServer); + } + + progressCallback(resultConfig.outgoing.type, + resultConfig.outgoing.hostname, resultConfig.outgoing.port, + resultConfig.outgoing.socketType, true, resultConfig); + outgoingDone = true; + checkDone(); + }; + + var incomingSuccess = function(thisTry, alternativeTries) + { + // Ensure there are no previously saved incoming errors, if we've got + // success here. + incomingEx = null; + HostTryToAccountServer(thisTry, resultConfig.incoming); + + for (let alternativeTry of alternativeTries) + { + // resultConfig.createNewIncoming(); misses username etc., so copy + let altServer = deepCopy(resultConfig.incoming); + HostTryToAccountServer(alternativeTry, altServer); + assert(resultConfig.incomingAlternatives); + resultConfig.incomingAlternatives.push(altServer); + } + + progressCallback(resultConfig.incoming.type, + resultConfig.incoming.hostname, resultConfig.incoming.port, + resultConfig.incoming.socketType, true, resultConfig); + incomingDone = true; + checkDone(); + }; + + var incomingError = function(ex) + { + incomingEx = ex; + checkDone(); + incomingHostDetector.cancel(new CancelOthersException()); + outgoingHostDetector.cancel(new CancelOthersException()); + }; + + var outgoingError = function(ex) + { + outgoingEx = ex; + checkDone(); + incomingHostDetector.cancel(new CancelOthersException()); + outgoingHostDetector.cancel(new CancelOthersException()); + }; + + incomingHostDetector = new IncomingHostDetector(progress, incomingSuccess, + incomingError); + outgoingHostDetector = new OutgoingHostDetector(progress, outgoingSuccess, + outgoingError); + if (which == "incoming" || which == "both") + { + incomingHostDetector.start(resultConfig.incoming.hostname ? + resultConfig.incoming.hostname : domain, + !!resultConfig.incoming.hostname, resultConfig.incoming.type, + resultConfig.incoming.port, resultConfig.incoming.socketType); + } + if (which == "outgoing" || which == "both") + { + outgoingHostDetector.start(resultConfig.outgoing.hostname ? + resultConfig.outgoing.hostname : domain, + !!resultConfig.outgoing.hostname, "smtp", + resultConfig.outgoing.port, resultConfig.outgoing.socketType); + } + + return new GuessAbortable(incomingHostDetector, outgoingHostDetector, + updateConfig); +} + +function GuessAbortable(incomingHostDetector, outgoingHostDetector, + updateConfig) +{ + Abortable.call(this); + this._incomingHostDetector = incomingHostDetector; + this._outgoingHostDetector = outgoingHostDetector; + this._updateConfig = updateConfig; +} +GuessAbortable.prototype = Object.create(Abortable.prototype); +GuessAbortable.prototype.constructor = GuessAbortable; +GuessAbortable.prototype.cancel = function(ex) +{ + this._incomingHostDetector.cancel(ex); + this._outgoingHostDetector.cancel(ex); +}; + +////////////////////////////////////////////////////////////////////////////// +// Implementation +// +// Objects, functions and constants that follow are not to be used outside +// this file. + +var kNotTried = 0; +var kOngoing = 1; +var kFailed = 2; +var kSuccess = 3; + +/** + * Internal object holding one server that we should try or did try. + * Used as |thisTry|. + * + * Note: The consts it uses for protocol and ssl are defined towards the end + * of this file and not the same as those used in AccountConfig (type, + * socketType). (fix this) + */ +function HostTry() +{ +} +HostTry.prototype = +{ + // IMAP, POP or SMTP + protocol : UNKNOWN, + // {String} + hostname : undefined, + // {Integer} + port : undefined, + // NONE, SSL or TLS + ssl : UNKNOWN, + // {String} what to send to server + commands : null, + // {Integer-enum} kNotTried, kOngoing, kFailed or kSuccess + status : kNotTried, + // {Abortable} allows to cancel the socket comm + abortable : null, + + // {Array of {Integer-enum}} @see _advertisesAuthMethods() result + // Info about the server, from the protocol and SSL chat + authMethods : null, + // {String} Whether the SSL cert is not from a proper CA + selfSignedCert : false, + // {String} Which host the SSL cert is made for, if not hostname. + // If set, this is an SSL error. + targetSite : null, +}; + +/** + * When the success or errorCallbacks are called to abort the other requests + * which happened in parallel, this ex is used as param for cancel(), so that + * the cancel doesn't trigger another callback. + */ +function CancelOthersException() +{ + CancelledException.call(this, "we're done, cancelling the other probes"); +} +CancelOthersException.prototype = Object.create(CancelledException.prototype); +CancelOthersException.prototype.constructor = CancelOthersException; + +/** + * @param successCallback {function(result {HostTry}, alts {Array of HostTry})} + * Called when the config is OK + * |result| is the most preferred server. + * |alts| currently exists only for |IncomingHostDetector| and contains + * some servers of the other type (POP3 instead of IMAP), if available. + * @param errorCallback {function(ex)} Called when we could not find a config + * @param progressCallback { function(server {HostTry}) } Called when we tried + * (will try?) a new hostname and port + */ +function HostDetector(progressCallback, successCallback, errorCallback) +{ + this.mSuccessCallback = successCallback; + this.mProgressCallback = progressCallback; + this.mErrorCallback = errorCallback; + this._cancel = false; + // {Array of {HostTry}}, ordered by decreasing preference + this._hostsToTry = new Array(); + + // init logging + this._log = Log4Moz.getConfiguredLogger("mail.wizard"); + this._log.info("created host detector"); +} + +HostDetector.prototype = +{ + cancel : function(ex) + { + this._cancel = true; + // We have to actively stop the network calls, as they may result in + // callbacks e.g. to the cert handler. If the dialog is gone by the time + // this happens, the javascript stack is horked. + for (let i = 0; i < this._hostsToTry.length; i++) + { + let thisTry = this._hostsToTry[i]; // {HostTry} + if (thisTry.abortable) + thisTry.abortable.cancel(ex); + thisTry.status = kFailed; // or don't set? Maybe we want to continue. + } + if (ex instanceof CancelOthersException) + return; + if (!ex) + ex = new CancelledException(); + this.mErrorCallback(ex); + }, + + /** + * Start the detection + * + * @param domain {String} to be used as base for guessing. + * Should be a domain (e.g. yahoo.co.uk). + * If hostIsPrecise == true, it should be a full hostname + * @param hostIsPrecise {Boolean} (default false) use only this hostname, + * do not guess hostnames. + * @param type {String-enum}@see AccountConfig type + * (Optional. default, 0, undefined, null = guess it) + * @param port {Integer} (Optional. default, 0, undefined, null = guess it) + * @param socketType {Integer-enum}@see AccountConfig socketType + * (Optional. default, 0, undefined, null = guess it) + */ + start : function(domain, hostIsPrecise, type, port, socketType) + { + domain = domain.replace(/\s*/g, ""); // Remove whitespace + if (!hostIsPrecise) + hostIsPrecise = false; + var protocol = sanitize.translate(type, + { "imap" : IMAP, "pop3" : POP, "smtp" : SMTP }, UNKNOWN); + if (!port) + port = UNKNOWN; + var ssl = ConvertSocketTypeToSSL(socketType); + this._cancel = false; + this._log.info("doing auto detect for protocol " + protocol + + ", domain " + domain + ", (exactly: " + hostIsPrecise + + "), port " + port + ", ssl " + ssl); + + // fill this._hostsToTry + this._hostsToTry = []; + var hostnamesToTry = []; + // if hostIsPrecise is true, it's because that's what the user input + // explicitly, and we'll just try it, nothing else. + if (hostIsPrecise) + hostnamesToTry.push(domain); + else + hostnamesToTry = this._hostnamesToTry(protocol, domain); + + for (let i = 0; i < hostnamesToTry.length; i++) + { + let hostname = hostnamesToTry[i]; + let hostEntries = this._portsToTry(hostname, protocol, ssl, port); + for (let j = 0; j < hostEntries.length; j++) + { + let hostTry = hostEntries[j]; // from getHostEntry() + hostTry.hostname = hostname; + hostTry.status = kNotTried; + this._hostsToTry.push(hostTry); + } + } + + this._hostsToTry = sortTriesByPreference(this._hostsToTry); + this._tryAll(); + }, + + // We make all host/port combinations run in parallel, store their + // results in an array, and as soon as one finishes successfully and all + // higher-priority ones have failed, we abort all lower-priority ones. + + _tryAll : function() + { + if (this._cancel) + return; + var me = this; + for (let i = 0; i < this._hostsToTry.length; i++) + { + let thisTry = this._hostsToTry[i]; // {HostTry} + if (thisTry.status != kNotTried) + continue; + this._log.info("poking at " + thisTry.hostname + " port " + + thisTry.port + " ssl "+ thisTry.ssl + " protocol " + + protocolToString(thisTry.protocol)); + if (i == 0) // showing 50 servers at once is pointless + this.mProgressCallback(thisTry); + + thisTry.abortable = SocketUtil( + thisTry.hostname, thisTry.port, thisTry.ssl, + thisTry.commands, TIMEOUT, + new SSLErrorHandler(thisTry, this._log), + function(wiredata) // result callback + { + if (me._cancel) + return; // don't use response anymore + me.mProgressCallback(thisTry); + me._processResult(thisTry, wiredata); + me._checkFinished(); + }, + function(e) // error callback + { + if (me._cancel) + return; // who set cancel to true already called mErrorCallback() + me._log.warn(e); + thisTry.status = kFailed; + me._checkFinished(); + }); + thisTry.status = kOngoing; + } + }, + + /** + * @param thisTry {HostTry} + * @param wiredata {Array of {String}} what the server returned + * in response to our protocol chat + */ + _processResult : function(thisTry, wiredata) + { + if (thisTry._gotCertError) + { + if (thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_MISMATCH) + { + thisTry._gotCertError = 0; + thisTry.status = kFailed; + return; + } + + if (thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_UNTRUSTED || + thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_TIME) + { + this._log.info("TRYING AGAIN, hopefully with exception recorded"); + thisTry._gotCertError = 0; + thisTry.selfSignedCert = true; // _next_ run gets this exception + thisTry.status = kNotTried; // try again (with exception) + this._tryAll(); + return; + } + } + + if (wiredata == null || wiredata === undefined) + { + this._log.info("no data"); + thisTry.status = kFailed; + return; + } + this._log.info("wiredata: " + wiredata.join("")); + thisTry.authMethods = + this._advertisesAuthMethods(thisTry.protocol, wiredata); + if (thisTry.ssl == TLS && !this._hasTLS(thisTry, wiredata)) + { + this._log.info("STARTTLS wanted, but not offered"); + thisTry.status = kFailed; + return; + } + this._log.info("success with " + thisTry.hostname + ":" + + thisTry.port + " " + protocolToString(thisTry.protocol) + + " ssl " + thisTry.ssl + + (thisTry.selfSignedCert ? " (selfSignedCert)" : "")); + thisTry.status = kSuccess; + + if (thisTry.selfSignedCert) { // eh, ERROR_UNTRUSTED or ERROR_TIME + // We clear the temporary override now after success. If we clear it + // earlier we get into an infinite loop, probably because the cert + // remembering is temporary and the next try gets a new connection which + // isn't covered by that temporariness. + this._log.info("clearing validity override for " + thisTry.hostname); + Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService) + .clearValidityOverride(thisTry.hostname, thisTry.port); + } + }, + + _checkFinished : function() + { + var successfulTry = null; + var successfulTryAlternative = null; // POP3 + var unfinishedBusiness = false; + // this._hostsToTry is ordered by decreasing preference + for (let i = 0; i < this._hostsToTry.length; i++) + { + let thisTry = this._hostsToTry[i]; + if (thisTry.status == kNotTried || thisTry.status == kOngoing) + unfinishedBusiness = true; + // thisTry is good, and all higher preference tries failed, so use this + else if (thisTry.status == kSuccess && !unfinishedBusiness) + { + if (!successfulTry) + { + successfulTry = thisTry; + if (successfulTry.protocol == SMTP) + break; + } + else if (successfulTry.protocol != thisTry.protocol) + { + successfulTryAlternative = thisTry; + break; + } + } + } + if (successfulTry && (successfulTryAlternative || !unfinishedBusiness)) + { + this.mSuccessCallback(successfulTry, + successfulTryAlternative ? [ successfulTryAlternative ] : []); + this.cancel(new CancelOthersException()); + } + else if (!unfinishedBusiness) // all failed + { + this._log.info("ran out of options"); + var errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties") + .GetStringFromName("cannot_find_server.error"); + this.mErrorCallback(new Exception(errorMsg)); + // no need to cancel, all failed + } + // else let ongoing calls continue + }, + + + /** + * Which auth mechanism the server claims to support. + * (That doesn't necessarily reflect reality, it is more an upper bound.) + * + * @param protocol {Integer-enum} IMAP, POP or SMTP + * @param capaResponse {Array of {String}} on the wire data + * that the server returned. May be the full exchange or just capa. + * @returns {Array of {Integer-enum} values for AccountConfig.incoming.auth + * (or outgoing), in decreasing order of preference. + * E.g. [ 5, 4 ] for a server that supports only Kerberos and + * encrypted passwords. + */ + _advertisesAuthMethods : function(protocol, capaResponse) + { + // for imap, capabilities include e.g.: + // "AUTH=CRAM-MD5", "AUTH=NTLM", "AUTH=GSSAPI", "AUTH=MSN" + // for pop3, the auth mechanisms are returned in capa as the following: + // "CRAM-MD5", "NTLM", "MSN", "GSSAPI" + // For smtp, EHLO will return AUTH and then a list of the + // mechanism(s) supported, e.g., + // AUTH LOGIN NTLM MSN CRAM-MD5 GSSAPI + var result = new Array(); + var line = capaResponse.join("\n").toUpperCase(); + var prefix = ""; + if (protocol == POP) + prefix = ""; + else if (protocol == IMAP) + prefix = "AUTH="; + else if (protocol == SMTP) + prefix = "AUTH.*"; + else + throw NotReached("must pass protocol"); + // add in decreasing order of preference + if (new RegExp(prefix + "GSSAPI").test(line)) + result.push(Ci.nsMsgAuthMethod.GSSAPI); + if (new RegExp(prefix + "CRAM-MD5").test(line)) + result.push(Ci.nsMsgAuthMethod.passwordEncrypted); + if (new RegExp(prefix + "(NTLM|MSN)").test(line)) + result.push(Ci.nsMsgAuthMethod.NTLM); + if (protocol != IMAP || !line.includes("LOGINDISABLED")) + result.push(Ci.nsMsgAuthMethod.passwordCleartext); + return result; + }, + + _hasTLS : function(thisTry, wiredata) + { + var capa = thisTry.protocol == POP ? "STLS" : "STARTTLS"; + return thisTry.ssl == TLS && + wiredata.join("").toUpperCase().includes(capa); + }, +} + +/** + * @param authMethods @see return value of _advertisesAuthMethods() + * Note: the returned auth method will be removed from the array. + * @return one of them, the preferred one + * Note: this might be Kerberos, which might not actually work, + * so you might need to try the others, too. + */ +function chooseBestAuthMethod(authMethods) +{ + if (!authMethods || !authMethods.length) + return Ci.nsMsgAuthMethod.passwordCleartext; + return authMethods.shift(); // take first (= most preferred) +} + + +function IncomingHostDetector( + progressCallback, successCallback, errorCallback) +{ + HostDetector.call(this, progressCallback, successCallback, errorCallback); +} +IncomingHostDetector.prototype = +{ + __proto__: HostDetector.prototype, + _hostnamesToTry : function(protocol, domain) + { + var hostnamesToTry = []; + if (protocol != POP) + hostnamesToTry.push("imap." + domain); + if (protocol != IMAP) + { + hostnamesToTry.push("pop3." + domain); + hostnamesToTry.push("pop." + domain); + } + hostnamesToTry.push("mail." + domain); + hostnamesToTry.push(domain); + return hostnamesToTry; + }, + _portsToTry : getIncomingTryOrder, +} + +function OutgoingHostDetector( + progressCallback, successCallback, errorCallback) +{ + HostDetector.call(this, progressCallback, successCallback, errorCallback); +} +OutgoingHostDetector.prototype = +{ + __proto__: HostDetector.prototype, + _hostnamesToTry : function(protocol, domain) + { + var hostnamesToTry = []; + hostnamesToTry.push("smtp." + domain); + hostnamesToTry.push("mail." + domain); + hostnamesToTry.push(domain); + return hostnamesToTry; + }, + _portsToTry : getOutgoingTryOrder, +} + +////////////////////////////////////////////////////////////////////////// +// Encode protocol ports and order of preference + +// Protocol Types +var UNKNOWN = -1; +var IMAP = 0; +var POP = 1; +var SMTP = 2; +// Security Types +var NONE = 0; // no encryption +//1 would be "TLS if available" +var TLS = 2; // STARTTLS +var SSL = 3; // SSL / TLS + +var IMAP_PORTS = {} +IMAP_PORTS[NONE] = 143; +IMAP_PORTS[TLS] = 143; +IMAP_PORTS[SSL] = 993; + +var POP_PORTS = {} +POP_PORTS[NONE] = 110; +POP_PORTS[TLS] = 110; +POP_PORTS[SSL] = 995; + +var SMTP_PORTS = {} +SMTP_PORTS[NONE] = 587; +SMTP_PORTS[TLS] = 587; +SMTP_PORTS[SSL] = 465; + +var CMDS = {} +CMDS[IMAP] = ["1 CAPABILITY\r\n", "2 LOGOUT\r\n"]; +CMDS[POP] = ["CAPA\r\n", "QUIT\r\n"]; +CMDS[SMTP] = ["EHLO we-guess.mozilla.org\r\n", "QUIT\r\n"]; + +/** + * Sort by preference of SSL, IMAP etc. + * @param tries {Array of {HostTry}} + * @returns {Array of {HostTry}} + */ +function sortTriesByPreference(tries) +{ + return tries.sort(function __sortByPreference(a, b) + { + // -1 = a is better; 1 = b is better; 0 = equal + // Prefer SSL/TLS above all else + if (a.ssl != NONE && b.ssl == NONE) + return -1; + if (b.ssl != NONE && a.ssl == NONE) + return 1; + // Prefer IMAP over POP + if (a.protocol == IMAP && b.protocol == POP) + return -1; + if (b.protocol == IMAP && a.protocol == POP) + return 1; + // For hostnames, leave existing sorting, as in _hostnamesToTry() + // For ports, leave existing sorting, as in getOutgoingTryOrder() + return 0; + }); +}; + +// TODO prefer SSL over STARTTLS, +// either in sortTriesByPreference or in getIncomingTryOrder() (and outgoing) + +/** + * @returns {Array of {HostTry}} + */ +function getIncomingTryOrder(host, protocol, ssl, port) +{ + var lowerCaseHost = host.toLowerCase(); + + if (protocol == UNKNOWN && + (lowerCaseHost.startsWith("pop.") || lowerCaseHost.startsWith("pop3."))) + protocol = POP; + else if (protocol == UNKNOWN && lowerCaseHost.startsWith("imap.")) + protocol = IMAP; + + if (protocol != UNKNOWN) { + if (ssl == UNKNOWN) + return [getHostEntry(protocol, TLS, port), + //getHostEntry(protocol, SSL, port), + getHostEntry(protocol, NONE, port)]; + return [getHostEntry(protocol, ssl, port)]; + } + if (ssl == UNKNOWN) + return [getHostEntry(IMAP, TLS, port), + //getHostEntry(IMAP, SSL, port), + getHostEntry(POP, TLS, port), + //getHostEntry(POP, SSL, port), + getHostEntry(IMAP, NONE, port), + getHostEntry(POP, NONE, port)]; + return [getHostEntry(IMAP, ssl, port), + getHostEntry(POP, ssl, port)]; +}; + +/** + * @returns {Array of {HostTry}} + */ +function getOutgoingTryOrder(host, protocol, ssl, port) +{ + assert(protocol == SMTP, "need SMTP as protocol for outgoing"); + if (ssl == UNKNOWN) + { + if (port == UNKNOWN) + // neither SSL nor port known + return [getHostEntry(SMTP, TLS, UNKNOWN), + getHostEntry(SMTP, TLS, 25), + //getHostEntry(SMTP, SSL, UNKNOWN), + getHostEntry(SMTP, NONE, UNKNOWN), + getHostEntry(SMTP, NONE, 25)]; + // port known, SSL not + return [getHostEntry(SMTP, TLS, port), + //getHostEntry(SMTP, SSL, port), + getHostEntry(SMTP, NONE, port)]; + } + // SSL known, port not + if (port == UNKNOWN) + { + if (ssl == SSL) + return [getHostEntry(SMTP, SSL, UNKNOWN)]; + else // TLS or NONE + return [getHostEntry(SMTP, ssl, UNKNOWN), + getHostEntry(SMTP, ssl, 25)]; + } + // SSL and port known + return [getHostEntry(SMTP, ssl, port)]; +}; + +/** + * @returns {HostTry} with proper default port and commands, + * but without hostname. + */ +function getHostEntry(protocol, ssl, port) +{ + if (!port || port == UNKNOWN) { + switch (protocol) { + case POP: + port = POP_PORTS[ssl]; + break; + case IMAP: + port = IMAP_PORTS[ssl]; + break; + case SMTP: + port = SMTP_PORTS[ssl]; + break; + default: + throw new NotReached("unsupported protocol " + protocol); + } + } + + var r = new HostTry(); + r.protocol = protocol; + r.ssl = ssl; + r.port = port; + r.commands = CMDS[protocol]; + return r; +}; + + +// Convert consts from those used here to those from AccountConfig +// TODO adjust consts to match AccountConfig + +// here -> AccountConfig +function sslConvertToSocketType(ssl) +{ + if (ssl == NONE) + return 1; + if (ssl == SSL) + return 2; + if (ssl == TLS) + return 3; + throw new NotReached("unexpected SSL type"); +} + +// AccountConfig -> here +function ConvertSocketTypeToSSL(socketType) +{ + if (socketType == 1) + return NONE; + if (socketType == 2) + return SSL; + if (socketType == 3) + return TLS; + return UNKNOWN; +} + +// here -> AccountConfig +function protocolToString(type) +{ + if (type == IMAP) + return "imap"; + if (type == POP) + return "pop3"; + if (type == SMTP) + return "smtp"; + throw new NotReached("unexpected protocol"); +} + + + +///////////////////////////////////////////////////////// +// SSL cert error handler + +/** + * Called by MyBadCertHandler.js, which called by PSM + * to tell us about SSL certificate errors. + * @param thisTry {HostTry} + * @param logger {Log4Moz logger} + */ +function SSLErrorHandler(thisTry, logger) +{ + this._try = thisTry; + this._log = logger; + // _ gotCertError will be set to an error code (one of those defined in + // nsICertOverrideService) + this._gotCertError = 0; +} +SSLErrorHandler.prototype = +{ + processCertError : function(socketInfo, status, targetSite) + { + this._log.error("Got Cert error for "+ targetSite); + + if (!status) + return true; + + let cert = status.QueryInterface(Ci.nsISSLStatus).serverCert; + let flags = 0; + + let parts = targetSite.split(":"); + let host = parts[0]; + let port = parts[1]; + + /* The following 2 cert problems are unfortunately common: + * 1) hostname mismatch: + * user is custeromer at a domain hoster, he owns yourname.org, + * and the IMAP server is imap.hoster.com (but also reachable as + * imap.yourname.org), and has a cert for imap.hoster.com. + * 2) self-signed: + * a company has an internal IMAP server, and it's only for + * 30 employees, and they didn't want to buy a cert, so + * they use a self-signed cert. + * + * We would like the above to pass, somehow, with user confirmation. + * The following case should *not* pass: + * + * 1) MITM + * User has @gmail.com, and an attacker is between the user and + * the Internet and runs a man-in-the-middle (MITM) attack. + * Attacker controls DNS and sends imap.gmail.com to his own + * imap.attacker.com. He has either a valid, CA-issued + * cert for imap.attacker.com, or a self-signed cert. + * Of course, attacker.com could also be legit-sounding gmailservers.com. + * + * What makes it dangerous is that we (!) propose the server to the user, + * and he cannot judge whether imap.gmailservers.com is correct or not, + * and he will likely approve it. + */ + + if (status.isDomainMismatch) { + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_MISMATCH; + flags |= Ci.nsICertOverrideService.ERROR_MISMATCH; + } + else if (status.isUntrusted) { // e.g. self-signed + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_UNTRUSTED; + flags |= Ci.nsICertOverrideService.ERROR_UNTRUSTED; + } + else if (status.isNotValidAtThisTime) { + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_TIME; + flags |= Ci.nsICertOverrideService.ERROR_TIME; + } + else { + this._try._gotCertError = -1; // other + } + + /* We will add a temporary cert exception here, so that + * we can continue and connect and try. + * But we will remove it again as soon as we close the + * connection, in _processResult(). + * _gotCertError will serve as the marker that we + * have to clear the override later. + * + * In verifyConfig(), before we send the password, we *must* + * get another cert exception, this time with dialog to the user + * so that he gets informed about this and can make a choice. + */ + + this._try.targetSite = targetSite; + Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService) + .rememberValidityOverride(host, port, cert, flags, + true); // temporary override + this._log.warn("!! Overrode bad cert temporarily " + host + " " + port + + " flags=" + flags + "\n"); + return true; + }, +} + + + +////////////////////////////////////////////////////////////////// +// Socket Util + + +/** + * @param hostname {String} The DNS hostname to connect to. + * @param port {Integer} The numberic port to connect to on the host. + * @param ssl {Integer} SSL, TLS or NONE + * @param commands {Array of String}: protocol commands + * to send to the server. + * @param timeout {Integer} seconds to wait for a server response, then cancel. + * @param sslErrorHandler {SSLErrorHandler} + * @param resultCallback {function(wiredata)} This function will + * be called with the result string array from the server + * or null if no communication occurred. + * @param errorCallback {function(e)} + */ +function SocketUtil(hostname, port, ssl, commands, timeout, + sslErrorHandler, resultCallback, errorCallback) +{ + assert(commands && commands.length, "need commands"); + + var index = 0; // commands[index] is next to send to server + var initialized = false; + var aborted = false; + + function _error(e) + { + if (aborted) + return; + aborted = true; + errorCallback(e); + } + + function timeoutFunc() + { + if (!initialized) + _error("timeout"); + } + + // In case DNS takes too long or does not resolve or another blocking + // issue occurs before the timeout can be set on the socket, this + // ensures that the listener callback will be fired in a timely manner. + // XXX There might to be some clean up needed after the timeout is fired + // for socket and io resources. + + // The timeout value plus 2 seconds + setTimeout(timeoutFunc, (timeout * 1000) + 2000); + + var transportService = Cc["@mozilla.org/network/socket-transport-service;1"] + .getService(Ci.nsISocketTransportService); + + // @see NS_NETWORK_SOCKET_CONTRACTID_PREFIX + var socketTypeName = ssl == SSL ? "ssl" : (ssl == TLS ? "starttls" : null); + var transport = transportService.createTransport([socketTypeName], + ssl == NONE ? 0 : 1, + hostname, port, null); + + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, timeout); + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, timeout); + try { + transport.securityCallbacks = new BadCertHandler(sslErrorHandler); + } catch (e) { + _error(e); + } + var outstream = transport.openOutputStream(0, 0, 0); + var stream = transport.openInputStream(0, 0, 0); + var instream = Cc["@mozilla.org/scriptableinputstream;1"] + .createInstance(Ci.nsIScriptableInputStream); + instream.init(stream); + + var dataListener = + { + data : new Array(), + onStartRequest: function(request, context) + { + try { + initialized = true; + if (!aborted) + { + // Send the first request + let outputData = commands[index++]; + outstream.write(outputData, outputData.length); + } + } catch (e) { _error(e); } + }, + onStopRequest: function(request, context, status) + { + try { + instream.close(); + outstream.close(); + resultCallback(this.data.length ? this.data : null); + } catch (e) { _error(e); } + }, + onDataAvailable: function(request, context, inputStream, offset, count) + { + try { + if (!aborted) + { + let inputData = instream.read(count); + this.data.push(inputData); + if (index < commands.length) + { + // Send the next request to the server. + let outputData = commands[index++]; + outstream.write(outputData, outputData.length); + } + } + } catch (e) { _error(e); } + } + }; + + try { + var pump = Cc["@mozilla.org/network/input-stream-pump;1"] + .createInstance(Ci.nsIInputStreamPump); + + pump.init(stream, -1, -1, 0, 0, false); + pump.asyncRead(dataListener, null); + return new SocketAbortable(transport); + } catch (e) { _error(e); } + return null; +} + +function SocketAbortable(transport) +{ + Abortable.call(this); + assert(transport instanceof Ci.nsITransport, "need transport"); + this._transport = transport; +} +SocketAbortable.prototype = Object.create(Abortable.prototype); +SocketAbortable.prototype.constructor = UserCancelledException; +SocketAbortable.prototype.cancel = function(ex) +{ + try { + this._transport.close(Components.results.NS_ERROR_ABORT); + } catch (e) { + ddump("canceling socket failed: " + e); + } +} + diff --git a/mailnews/base/prefs/content/accountcreation/readFromXML.js b/mailnews/base/prefs/content/accountcreation/readFromXML.js new file mode 100644 index 000000000..c7e796f5f --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/readFromXML.js @@ -0,0 +1,238 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Takes an XML snipplet (as JXON) and reads the values into + * a new AccountConfig object. + * It does so securely (or tries to), by trying to avoid remote execution + * and similar holes which can appear when reading too naively. + * Of course it cannot tell whether the actual values are correct, + * e.g. it can't tell whether the host name is a good server. + * + * The XML format is documented at + * <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat> + * + * @param clientConfigXML {JXON} The <clientConfig> node. + * @return AccountConfig object filled with the data from XML + */ +Components.utils.import("resource:///modules/hostnameUtils.jsm"); + +function readFromXML(clientConfigXML) +{ + function array_or_undef(value) { + return value === undefined ? [] : value; + } + var exception; + if (typeof(clientConfigXML) != "object" || + !("clientConfig" in clientConfigXML) || + !("emailProvider" in clientConfigXML.clientConfig)) + { + dump("client config xml = " + JSON.stringify(clientConfigXML) + "\n"); + var stringBundle = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties"); + throw stringBundle.GetStringFromName("no_emailProvider.error"); + } + var xml = clientConfigXML.clientConfig.emailProvider; + + var d = new AccountConfig(); + d.source = AccountConfig.kSourceXML; + + d.id = sanitize.hostname(xml["@id"]); + d.displayName = d.id; + try { + d.displayName = sanitize.label(xml.displayName); + } catch (e) { logException(e); } + for (var domain of xml.$domain) + { + try { + d.domains.push(sanitize.hostname(domain)); + } catch (e) { logException(e); exception = e; } + } + if (d.domains.length == 0) + throw exception ? exception : "need proper <domain> in XML"; + exception = null; + + // incoming server + for (let iX of array_or_undef(xml.$incomingServer)) // input (XML) + { + let iO = d.createNewIncoming(); // output (object) + try { + // throws if not supported + iO.type = sanitize.enum(iX["@type"], ["pop3", "imap", "nntp"]); + iO.hostname = sanitize.hostname(iX.hostname); + iO.port = sanitize.integerRange(iX.port, kMinPort, kMaxPort); + // We need a username even for Kerberos, need it even internally. + iO.username = sanitize.string(iX.username); // may be a %VARIABLE% + + if ("password" in iX) { + d.rememberPassword = true; + iO.password = sanitize.string(iX.password); + } + + for (let iXsocketType of array_or_undef(iX.$socketType)) + { + try { + iO.socketType = sanitize.translate(iXsocketType, + { plain : 1, SSL: 2, STARTTLS: 3 }); + break; // take first that we support + } catch (e) { exception = e; } + } + if (!iO.socketType) + throw exception ? exception : "need proper <socketType> in XML"; + exception = null; + + for (let iXauth of array_or_undef(iX.$authentication)) + { + try { + iO.auth = sanitize.translate(iXauth, + { "password-cleartext" : Ci.nsMsgAuthMethod.passwordCleartext, + // @deprecated TODO remove + "plain" : Ci.nsMsgAuthMethod.passwordCleartext, + "password-encrypted" : Ci.nsMsgAuthMethod.passwordEncrypted, + // @deprecated TODO remove + "secure" : Ci.nsMsgAuthMethod.passwordEncrypted, + "GSSAPI" : Ci.nsMsgAuthMethod.GSSAPI, + "NTLM" : Ci.nsMsgAuthMethod.NTLM, + "OAuth2" : Ci.nsMsgAuthMethod.OAuth2 }); + break; // take first that we support + } catch (e) { exception = e; } + } + if (!iO.auth) + throw exception ? exception : "need proper <authentication> in XML"; + exception = null; + + // defaults are in accountConfig.js + if (iO.type == "pop3" && "pop3" in iX) + { + try { + if ("leaveMessagesOnServer" in iX.pop3) + iO.leaveMessagesOnServer = + sanitize.boolean(iX.pop3.leaveMessagesOnServer); + if ("daysToLeaveMessagesOnServer" in iX.pop3) + iO.daysToLeaveMessagesOnServer = + sanitize.integer(iX.pop3.daysToLeaveMessagesOnServer); + } catch (e) { logException(e); } + try { + if ("downloadOnBiff" in iX.pop3) + iO.downloadOnBiff = sanitize.boolean(iX.pop3.downloadOnBiff); + } catch (e) { logException(e); } + } + + // processed successfully, now add to result object + if (!d.incoming.hostname) // first valid + d.incoming = iO; + else + d.incomingAlternatives.push(iO); + } catch (e) { exception = e; } + } + if (!d.incoming.hostname) + // throw exception for last server + throw exception ? exception : "Need proper <incomingServer> in XML file"; + exception = null; + + // outgoing server + for (let oX of array_or_undef(xml.$outgoingServer)) // input (XML) + { + let oO = d.createNewOutgoing(); // output (object) + try { + if (oX["@type"] != "smtp") + { + var stringBundle = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties"); + throw stringBundle.GetStringFromName("outgoing_not_smtp.error"); + } + oO.hostname = sanitize.hostname(oX.hostname); + oO.port = sanitize.integerRange(oX.port, kMinPort, kMaxPort); + + for (let oXsocketType of array_or_undef(oX.$socketType)) + { + try { + oO.socketType = sanitize.translate(oXsocketType, + { plain : 1, SSL: 2, STARTTLS: 3 }); + break; // take first that we support + } catch (e) { exception = e; } + } + if (!oO.socketType) + throw exception ? exception : "need proper <socketType> in XML"; + exception = null; + + for (let oXauth of array_or_undef(oX.$authentication)) + { + try { + oO.auth = sanitize.translate(oXauth, + { // open relay + "none" : Ci.nsMsgAuthMethod.none, + // inside ISP or corp network + "client-IP-address" : Ci.nsMsgAuthMethod.none, + // hope for the best + "smtp-after-pop" : Ci.nsMsgAuthMethod.none, + "password-cleartext" : Ci.nsMsgAuthMethod.passwordCleartext, + // @deprecated TODO remove + "plain" : Ci.nsMsgAuthMethod.passwordCleartext, + "password-encrypted" : Ci.nsMsgAuthMethod.passwordEncrypted, + // @deprecated TODO remove + "secure" : Ci.nsMsgAuthMethod.passwordEncrypted, + "GSSAPI" : Ci.nsMsgAuthMethod.GSSAPI, + "NTLM" : Ci.nsMsgAuthMethod.NTLM, + "OAuth2" : Ci.nsMsgAuthMethod.OAuth2, + }); + + break; // take first that we support + } catch (e) { exception = e; } + } + if (!oO.auth) + throw exception ? exception : "need proper <authentication> in XML"; + exception = null; + + if ("username" in oX || + // if password-based auth, we need a username, + // so go there anyways and throw. + oO.auth == Ci.nsMsgAuthMethod.passwordCleartext || + oO.auth == Ci.nsMsgAuthMethod.passwordEncrypted) + oO.username = sanitize.string(oX.username); + + if ("password" in oX) { + d.rememberPassword = true; + oO.password = sanitize.string(oX.password); + } + + try { + // defaults are in accountConfig.js + if ("addThisServer" in oX) + oO.addThisServer = sanitize.boolean(oX.addThisServer); + if ("useGlobalPreferredServer" in oX) + oO.useGlobalPreferredServer = + sanitize.boolean(oX.useGlobalPreferredServer); + } catch (e) { logException(e); } + + // processed successfully, now add to result object + if (!d.outgoing.hostname) // first valid + d.outgoing = oO; + else + d.outgoingAlternatives.push(oO); + } catch (e) { logException(e); exception = e; } + } + if (!d.outgoing.hostname) + // throw exception for last server + throw exception ? exception : "Need proper <outgoingServer> in XML file"; + exception = null; + + d.inputFields = new Array(); + for (let inputField of array_or_undef(xml.$inputField)) + { + try { + var fieldset = + { + varname : sanitize.alphanumdash(inputField["@key"]).toUpperCase(), + displayName : sanitize.label(inputField["@label"]), + exampleValue : sanitize.label(inputField.value) + }; + d.inputFields.push(fieldset); + } catch (e) { logException(e); } // for now, don't throw, + // because we don't support custom fields yet anyways. + } + + return d; +} diff --git a/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js b/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js new file mode 100644 index 000000000..0f95f78d1 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/sanitizeDatatypes.js @@ -0,0 +1,207 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This is a generic input validation lib. Use it when you process + * data from the network. + * + * Just a few functions which verify, for security purposes, that the + * input variables (strings, if nothing else is noted) are of the expected + * type and syntax. + * + * The functions take a string (unless noted otherwise) and return + * the expected datatype in JS types. If the value is not as expected, + * they throw exceptions. + */ + +Components.utils.import("resource:///modules/hostnameUtils.jsm"); + +var sanitize = +{ + integer : function(unchecked) + { + if (typeof(unchecked) == "number" && !isNaN(unchecked)) + return unchecked; + + var r = parseInt(unchecked); + if (isNaN(r)) + throw new MalformedException("no_number.error", unchecked); + + return r; + }, + + integerRange : function(unchecked, min, max) + { + var int = this.integer(unchecked); + if (int < min) + throw new MalformedException("number_too_small.error", unchecked); + + if (int > max) + throw new MalformedException("number_too_large.error", unchecked); + + return int; + }, + + boolean : function(unchecked) + { + if (typeof(unchecked) == "boolean") + return unchecked; + + if (unchecked == "true") + return true; + + if (unchecked == "false") + return false; + + throw new MalformedException("boolean.error", unchecked); + }, + + string : function(unchecked) + { + return String(unchecked); + }, + + nonemptystring : function(unchecked) + { + if (!unchecked) + throw new MalformedException("string_empty.error", unchecked); + + return this.string(unchecked); + }, + + /** + * Allow only letters, numbers, "-" and "_". + * + * Empty strings not allowed (good idea?). + */ + alphanumdash : function(unchecked) + { + var str = this.nonemptystring(unchecked); + if (!/^[a-zA-Z0-9\-\_]*$/.test(str)) + throw new MalformedException("alphanumdash.error", unchecked); + + return str; + }, + + /** + * DNS hostnames like foo.bar.example.com + * Allow only letters, numbers, "-" and "." + * Empty strings not allowed. + * Currently does not support IDN (international domain names). + */ + hostname : function(unchecked) + { + let str = cleanUpHostName(this.nonemptystring(unchecked)); + + // Allow placeholders. TODO move to a new hostnameOrPlaceholder() + // The regex is "anything, followed by one or more (placeholders than + // anything)". This doesn't catch the non-placeholder case, but that's + // handled down below. + if (/^[a-zA-Z0-9\-\.]*(%[A-Z0-9]+%[a-zA-Z0-9\-\.]*)+$/.test(str)) + return str; + + if (!isLegalHostNameOrIP(str)) + throw new MalformedException("hostname_syntax.error", unchecked); + + return str.toLowerCase(); + }, + /** + * A non-chrome URL that's safe to request. + */ + url : function (unchecked) + { + var str = this.string(unchecked); + if (!str.startsWith("http") && !str.startsWith("https")) + throw new MalformedException("url_scheme.error", unchecked); + + var uri; + try { + uri = Services.io.newURI(str, null, null); + uri = uri.QueryInterface(Ci.nsIURL); + } catch (e) { + throw new MalformedException("url_parsing.error", unchecked); + } + + if (uri.scheme != "http" && uri.scheme != "https") + throw new MalformedException("url_scheme.error", unchecked); + + return uri.spec; + }, + + /** + * A value which should be shown to the user in the UI as label + */ + label : function(unchecked) + { + return this.string(unchecked); + }, + + /** + * Allows only certain values as input, otherwise throw. + * + * @param unchecked {Any} The value to check + * @param allowedValues {Array} List of values that |unchecked| may have. + * @param defaultValue {Any} (Optional) If |unchecked| does not match + * anything in |mapping|, a |defaultValue| can be returned instead of + * throwing an exception. The latter is the default and happens when + * no |defaultValue| is passed. + * @throws MalformedException + */ + enum : function(unchecked, allowedValues, defaultValue) + { + for (let allowedValue of allowedValues) + { + if (allowedValue == unchecked) + return allowedValue; + } + // value is bad + if (typeof(defaultValue) == "undefined") + throw new MalformedException("allowed_value.error", unchecked); + return defaultValue; + }, + + /** + * Like enum, allows only certain (string) values as input, but allows the + * caller to specify another value to return instead of the input value. E.g., + * if unchecked == "foo", return 1, if unchecked == "bar", return 2, + * otherwise throw. This allows to translate string enums into integer enums. + * + * @param unchecked {Any} The value to check + * @param mapping {Object} Associative array. property name is the input + * value, property value is the output value. E.g. the example above + * would be: { foo: 1, bar : 2 }. + * Use quotes when you need freaky characters: "baz-" : 3. + * @param defaultValue {Any} (Optional) If |unchecked| does not match + * anything in |mapping|, a |defaultValue| can be returned instead of + * throwing an exception. The latter is the default and happens when + * no |defaultValue| is passed. + * @throws MalformedException + */ + translate : function(unchecked, mapping, defaultValue) + { + for (var inputValue in mapping) + { + if (inputValue == unchecked) + return mapping[inputValue]; + } + // value is bad + if (typeof(defaultValue) == "undefined") + throw new MalformedException("allowed_value.error", unchecked); + return defaultValue; + } +}; + +function MalformedException(msgID, uncheckedBadValue) +{ + var stringBundle = getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties"); + var msg = stringBundle.GetStringFromName(msgID); + if (kDebug) + msg += " (bad value: " + new String(uncheckedBadValue) + ")"; + Exception.call(this, msg); +} +MalformedException.prototype = Object.create(Exception.prototype); +MalformedException.prototype.constructor = MalformedException; + diff --git a/mailnews/base/prefs/content/accountcreation/util.js b/mailnews/base/prefs/content/accountcreation/util.js new file mode 100644 index 000000000..d867bfbe9 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/util.js @@ -0,0 +1,304 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/** + * Some common, generic functions + */ + +try { + var Cc = Components.classes; + var Ci = Components.interfaces; +} catch (e) { ddump(e); } // if already declared, as in xpcshell-tests +try { + var Cu = Components.utils; +} catch (e) { ddump(e); } + +Cu.import("resource:///modules/errUtils.js"); +Cu.import("resource://gre/modules/Services.jsm"); + +function assert(test, errorMsg) +{ + if (!test) + throw new NotReached(errorMsg ? errorMsg : + "Programming bug. Assertion failed, see log."); +} + +function makeCallback(obj, func) +{ + return function() + { + return func.apply(obj, arguments); + } +} + + +/** + * Runs the given function sometime later + * + * Currently implemented using setTimeout(), but + * can later be replaced with an nsITimer impl, + * when code wants to use it in a module. + */ +function runAsync(func) +{ + setTimeout(func, 0); +} + + +/** + * @param uriStr {String} + * @result {nsIURI} + */ +function makeNSIURI(uriStr) +{ + return Services.io.newURI(uriStr, null, null); +} + + +/** + * Reads UTF8 data from a URL. + * + * @param uri {nsIURI} what you want to read + * @return {Array of String} the contents of the file, one string per line + */ +function readURLasUTF8(uri) +{ + assert(uri instanceof Ci.nsIURI, "uri must be an nsIURI"); + try { + let chan = Services.io.newChannelFromURI2(uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_NORMAL, + Ci.nsIContentPolicy.TYPE_OTHER); + let is = Cc["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Ci.nsIConverterInputStream); + is.init(chan.open(), "UTF-8", 1024, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + let content = ""; + let strOut = new Object(); + try { + while (is.readString(1024, strOut) != 0) + content += strOut.value; + // catch in outer try/catch + } finally { + is.close(); + } + + return content; + } catch (e) { + // TODO this has a numeric error message. We need to ship translations + // into human language. + throw e; + } +} + +/** + * Takes a string (which is typically the content of a file, + * e.g. the result returned from readURLUTF8() ), and splits + * it into lines, and returns an array with one string per line + * + * Linebreaks are not contained in the result,, + * and all of \r\n, (Windows) \r (Mac) and \n (Unix) counts as linebreak. + * + * @param content {String} one long string with the whole file + * @return {Array of String} one string per line (no linebreaks) + */ +function splitLines(content) +{ + content = content.replace("\r\n", "\n"); + content = content.replace("\r", "\n"); + return content.split("\n"); +} + +/** + * @param bundleURI {String} chrome URL to properties file + * @return nsIStringBundle + */ +function getStringBundle(bundleURI) +{ + try { + return Services.strings.createBundle(bundleURI); + } catch (e) { + throw new Exception("Failed to get stringbundle URI <" + bundleURI + + ">. Error: " + e); + } +} + + +function Exception(msg) +{ + this._message = msg; + + // get stack + try { + not.found.here += 1; // force a native exception ... + } catch (e) { + this.stack = e.stack; // ... to get the current stack + } +} +Exception.prototype = +{ + get message() + { + return this._message; + }, + toString : function() + { + return this._message; + } +} + +function NotReached(msg) +{ + Exception.call(this, msg); // call super constructor + logException(this); +} +// Make NotReached extend Exception. +NotReached.prototype = Object.create(Exception.prototype); +NotReached.prototype.constructor = NotReached; + +/** + * A handle for an async function which you can cancel. + * The async function will return an object of this type (a subtype) + * and you can call cancel() when you feel like killing the function. + */ +function Abortable() +{ +} +Abortable.prototype = +{ + cancel : function() + { + } +} + +/** + * Utility implementation, for allowing to abort a setTimeout. + * Use like: return new TimeoutAbortable(setTimeout(function(){ ... }, 0)); + * @param setTimeoutID {Integer} Return value of setTimeout() + */ +function TimeoutAbortable(setTimeoutID) +{ + Abortable.call(this, setTimeoutID); // call super constructor + this._id = setTimeoutID; +} +TimeoutAbortable.prototype = Object.create(Abortable.prototype); +TimeoutAbortable.prototype.constructor = TimeoutAbortable; +TimeoutAbortable.prototype.cancel = function() { clearTimeout(this._id); } + +/** + * Utility implementation, for allowing to abort a setTimeout. + * Use like: return new TimeoutAbortable(setTimeout(function(){ ... }, 0)); + * @param setIntervalID {Integer} Return value of setInterval() + */ +function IntervalAbortable(setIntervalID) +{ + Abortable.call(this, setIntervalID); // call super constructor + this._id = setIntervalID; +} +IntervalAbortable.prototype = Object.create(Abortable.prototype); +IntervalAbortable.prototype.constructor = IntervalAbortable; +IntervalAbortable.prototype.cancel = function() { clearInterval(this._id); } + +// Allows you to make several network calls, but return +// only one Abortable object. +function SuccessiveAbortable() +{ + Abortable.call(this); // call super constructor + this._current = null; +} +SuccessiveAbortable.prototype = { + __proto__: Abortable.prototype, + get current() { return this._current; }, + set current(abortable) + { + assert(abortable instanceof Abortable || abortable == null, + "need an Abortable object (or null)"); + this._current = abortable; + }, + cancel: function() + { + if (this._current) + this._current.cancel(); + } +} + +function deepCopy(org) +{ + if (typeof(org) == "undefined") + return undefined; + if (org == null) + return null; + if (typeof(org) == "string") + return org; + if (typeof(org) == "number") + return org; + if (typeof(org) == "boolean") + return org == true; + if (typeof(org) == "function") + return org; + if (typeof(org) != "object") + throw "can't copy objects of type " + typeof(org) + " yet"; + + //TODO still instanceof org != instanceof copy + //var result = new org.constructor(); + var result = new Object(); + if (typeof(org.length) != "undefined") + var result = new Array(); + for (var prop in org) + result[prop] = deepCopy(org[prop]); + return result; +} + +if (typeof gEmailWizardLogger == "undefined") { + Cu.import("resource:///modules/gloda/log4moz.js"); + var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard"); +} +function ddump(text) +{ + gEmailWizardLogger.info(text); +} + +function debugObject(obj, name, maxDepth, curDepth) +{ + if (curDepth == undefined) + curDepth = 0; + if (maxDepth != undefined && curDepth > maxDepth) + return ""; + + var result = ""; + var i = 0; + for (let prop in obj) + { + i++; + try { + if (typeof(obj[prop]) == "object") + { + if (obj[prop] && obj[prop].length != undefined) + result += name + "." + prop + "=[probably array, length " + + obj[prop].length + "]\n"; + else + result += name + "." + prop + "=[" + typeof(obj[prop]) + "]\n"; + result += debugObject(obj[prop], name + "." + prop, + maxDepth, curDepth + 1); + } + else if (typeof(obj[prop]) == "function") + result += name + "." + prop + "=[function]\n"; + else + result += name + "." + prop + "=" + obj[prop] + "\n"; + } catch (e) { + result += name + "." + prop + "-> Exception(" + e + ")\n"; + } + } + if (!i) + result += name + " is empty\n"; + return result; +} + +function alertPrompt(alertTitle, alertMsg) +{ + Services.prompt.alert(window, alertTitle, alertMsg); +} diff --git a/mailnews/base/prefs/content/accountcreation/verifyConfig.js b/mailnews/base/prefs/content/accountcreation/verifyConfig.js new file mode 100644 index 000000000..a2afbdad8 --- /dev/null +++ b/mailnews/base/prefs/content/accountcreation/verifyConfig.js @@ -0,0 +1,347 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This checks a given config, by trying a real connection and login, + * with username and password. + * + * TODO + * - give specific errors, bug 555448 + * - return a working |Abortable| to allow cancel + * + * @param accountConfig {AccountConfig} The guessed account config. + * username, password, realname, emailaddress etc. are not filled out, + * but placeholders to be filled out via replaceVariables(). + * @param alter {boolean} + * Try other usernames and login schemes, until login works. + * Warning: Modifies |accountConfig|. + * + * This function is async. + * @param successCallback function(accountConfig) + * Called when we could guess the config. + * For accountConfig, see below. + * @param errorCallback function(ex) + * Called when we could guess not the config, either + * because we have not found anything or + * because there was an error (e.g. no network connection). + * The ex.message will contain a user-presentable message. + */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/OAuth2Providers.jsm"); + +if (typeof gEmailWizardLogger == "undefined") { + Cu.import("resource:///modules/gloda/log4moz.js"); + var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard"); +} + +function verifyConfig(config, alter, msgWindow, successCallback, errorCallback) +{ + ddump(debugObject(config, "config", 3)); + assert(config instanceof AccountConfig, + "BUG: Arg 'config' needs to be an AccountConfig object"); + assert(typeof(alter) == "boolean"); + assert(typeof(successCallback) == "function"); + assert(typeof(errorCallback) == "function"); + + if (MailServices.accounts.findRealServer(config.incoming.username, + config.incoming.hostname, + sanitize.enum(config.incoming.type, + ["pop3", "imap", "nntp"]), + config.incoming.port)) { + errorCallback("Incoming server exists"); + return; + } + + // incoming server + let inServer = + MailServices.accounts.createIncomingServer(config.incoming.username, + config.incoming.hostname, + sanitize.enum(config.incoming.type, + ["pop3", "imap", "nntp"])); + inServer.port = config.incoming.port; + inServer.password = config.incoming.password; + if (config.incoming.socketType == 1) // plain + inServer.socketType = Ci.nsMsgSocketType.plain; + else if (config.incoming.socketType == 2) // SSL + inServer.socketType = Ci.nsMsgSocketType.SSL; + else if (config.incoming.socketType == 3) // STARTTLS + inServer.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + + gEmailWizardLogger.info("Setting incoming server authMethod to " + + config.incoming.auth); + inServer.authMethod = config.incoming.auth; + + try { + // Lookup issuer if needed. + if (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2 || + config.outgoing.auth == Ci.nsMsgAuthMethod.OAuth2) { + if (!config.oauthSettings) + config.oauthSettings = {}; + if (!config.oauthSettings.issuer || !config.oauthSettings.scope) { + // lookup issuer or scope from hostname + let hostname = (config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) ? + config.incoming.hostname : config.outgoing.hostname; + let hostDetails = OAuth2Providers.getHostnameDetails(hostname); + if (hostDetails) + [config.oauthSettings.issuer, config.oauthSettings.scope] = hostDetails; + if (!config.oauthSettings.issuer || !config.oauthSettings.scope) + throw "Could not get issuer for oauth2 authentication"; + } + gEmailWizardLogger.info("Saving oauth parameters for issuer " + + config.oauthSettings.issuer); + inServer.setCharValue("oauth2.scope", config.oauthSettings.scope); + inServer.setCharValue("oauth2.issuer", config.oauthSettings.issuer); + gEmailWizardLogger.info("OAuth2 issuer, scope is " + + config.oauthSettings.issuer + ", " + config.oauthSettings.scope); + } + + if (inServer.password || + inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) + verifyLogon(config, inServer, alter, msgWindow, + successCallback, errorCallback); + else { + // Avoid pref pollution, clear out server prefs. + MailServices.accounts.removeIncomingServer(inServer, true); + successCallback(config); + } + return; + } + catch (e) { + gEmailWizardLogger.error("ERROR: verify logon shouldn't have failed"); + } + // Avoid pref pollution, clear out server prefs. + MailServices.accounts.removeIncomingServer(inServer, true); + errorCallback(e); +} + +function verifyLogon(config, inServer, alter, msgWindow, successCallback, + errorCallback) +{ + gEmailWizardLogger.info("verifyLogon for server at " + inServer.hostName); + // hack - save away the old callbacks. + let saveCallbacks = msgWindow.notificationCallbacks; + // set our own callbacks - this works because verifyLogon will + // synchronously create the transport and use the notification callbacks. + let listener = new urlListener(config, inServer, alter, msgWindow, + successCallback, errorCallback); + // our listener listens both for the url and cert errors. + msgWindow.notificationCallbacks = listener; + // try to work around bug where backend is clearing password. + try { + inServer.password = config.incoming.password; + let uri = inServer.verifyLogon(listener, msgWindow); + // clear msgWindow so url won't prompt for passwords. + uri.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = null; + } + catch (e) { gEmailWizardLogger.error("verifyLogon failed: " + e); throw e;} + finally { + // restore them + msgWindow.notificationCallbacks = saveCallbacks; + } +} + +/** + * The url listener also implements nsIBadCertListener2. Its job is to prevent + * "bad cert" security dialogs from being shown to the user. Currently it puts + * up the cert override dialog, though we'd like to give the user more detailed + * information in the future. + */ + +function urlListener(config, server, alter, msgWindow, successCallback, + errorCallback) +{ + this.mConfig = config; + this.mServer = server; + this.mAlter = alter; + this.mSuccessCallback = successCallback; + this.mErrorCallback = errorCallback; + this.mMsgWindow = msgWindow; + this.mCertError = false; + this._log = Log4Moz.getConfiguredLogger("mail.wizard"); +} +urlListener.prototype = +{ + OnStartRunningUrl: function(aUrl) + { + this._log.info("Starting to test username"); + this._log.info(" username=" + (this.mConfig.incoming.username != + this.mConfig.identity.emailAddress) + + ", have savedUsername=" + + (this.mConfig.usernameSaved ? "true" : "false")); + this._log.info(" authMethod=" + this.mServer.authMethod); + }, + + OnStopRunningUrl: function(aUrl, aExitCode) + { + this._log.info("Finished verifyConfig resulted in " + aExitCode); + if (Components.isSuccessCode(aExitCode)) + { + this._cleanup(); + this.mSuccessCallback(this.mConfig); + } + // Logon failed, and we aren't supposed to try other variations. + else if (!this.mAlter) + { + this._cleanup(); + var errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties") + .GetStringFromName("cannot_login.error"); + this.mErrorCallback(new Exception(errorMsg)); + } + // Try other variations, unless there's a cert error, in which + // case we'll see what the user chooses. + else if (!this.mCertError) + { + this.tryNextLogon() + } + }, + + tryNextLogon: function() + { + this._log.info("tryNextLogon()"); + this._log.info(" username=" + (this.mConfig.incoming.username != + this.mConfig.identity.emailAddress) + + ", have savedUsername=" + + (this.mConfig.usernameSaved ? "true" : "false")); + this._log.info(" authMethod=" + this.mServer.authMethod); + // check if we tried full email address as username + if (this.mConfig.incoming.username != this.mConfig.identity.emailAddress) + { + this._log.info(" Changing username to email address."); + this.mConfig.usernameSaved = this.mConfig.incoming.username; + this.mConfig.incoming.username = this.mConfig.identity.emailAddress; + this.mConfig.outgoing.username = this.mConfig.identity.emailAddress; + this.mServer.username = this.mConfig.incoming.username; + this.mServer.password = this.mConfig.incoming.password; + verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow, + this.mSuccessCallback, this.mErrorCallback); + return; + } + + if (this.mConfig.usernameSaved) + { + this._log.info(" Re-setting username."); + // If we tried the full email address as the username, then let's go + // back to trying just the username before trying the other cases. + this.mConfig.incoming.username = this.mConfig.usernameSaved; + this.mConfig.outgoing.username = this.mConfig.usernameSaved; + this.mConfig.usernameSaved = null; + this.mServer.username = this.mConfig.incoming.username; + this.mServer.password = this.mConfig.incoming.password; + } + + // sec auth seems to have failed, and we've tried both + // varieties of user name, sadly. + // So fall back to non-secure auth, and + // again try the user name and email address as username + assert(this.mConfig.incoming.auth == this.mServer.authMethod); + this._log.info(" Using SSL: " + + (this.mServer.socketType == Ci.nsMsgSocketType.SSL || + this.mServer.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS)); + if (this.mConfig.incoming.authAlternatives && + this.mConfig.incoming.authAlternatives.length) + // We may be dropping back to insecure auth methods here, + // which is not good. But then again, we already warned the user, + // if it is a config without SSL. + { + this._log.info(" auth alternatives = " + + this.mConfig.incoming.authAlternatives.join(",")); + this._log.info(" Decreasing auth."); + this._log.info(" Have password: " + + (this.mServer.password ? "true" : "false")); + let brokenAuth = this.mConfig.incoming.auth; + // take the next best method (compare chooseBestAuthMethod() in guess) + this.mConfig.incoming.auth = + this.mConfig.incoming.authAlternatives.shift(); + this.mServer.authMethod = this.mConfig.incoming.auth; + // Assume that SMTP server has same methods working as incoming. + // Broken assumption, but we currently have no SMTP verification. + // TODO implement real SMTP verification + if (this.mConfig.outgoing.auth == brokenAuth && + this.mConfig.outgoing.authAlternatives.indexOf( + this.mConfig.incoming.auth) != -1) + this.mConfig.outgoing.auth = this.mConfig.incoming.auth; + this._log.info(" outgoing auth: " + this.mConfig.outgoing.auth); + verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow, + this.mSuccessCallback, this.mErrorCallback); + return; + } + + // Tried all variations we can. Give up. + this._log.info("Giving up."); + this._cleanup(); + let errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties") + .GetStringFromName("cannot_login.error"); + this.mErrorCallback(new Exception(errorMsg)); + return; + }, + + _cleanup : function() + { + try { + // Avoid pref pollution, clear out server prefs. + if (this.mServer) { + MailServices.accounts.removeIncomingServer(this.mServer, true); + this.mServer = null; + } + } catch (e) { this._log.error(e); } + }, + + // Suppress any certificate errors + notifyCertProblem: function(socketInfo, status, targetSite) { + this.mCertError = true; + this._log.error("cert error"); + let self = this; + setTimeout(function () { + try { + self.informUserOfCertError(socketInfo, status, targetSite); + } catch (e) { logException(e); } + }, 0); + return true; + }, + + informUserOfCertError : function(socketInfo, status, targetSite) { + var params = { + exceptionAdded : false, + sslStatus : status, + prefetchCert : true, + location : targetSite, + }; + window.openDialog("chrome://pippki/content/exceptionDialog.xul", + "","chrome,centerscreen,modal", params); + this._log.info("cert exception dialog closed"); + this._log.info("cert exceptionAdded = " + params.exceptionAdded); + if (!params.exceptionAdded) { + this._cleanup(); + let errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties") + .GetStringFromName("cannot_login.error"); + this.mErrorCallback(new Exception(errorMsg)); + } + else { + // Retry the logon now that we've added the cert exception. + verifyLogon(this.mConfig, this.mServer, this.mAlter, this.mMsgWindow, + this.mSuccessCallback, this.mErrorCallback); + } + }, + + // nsIInterfaceRequestor + getInterface: function(iid) { + return this.QueryInterface(iid); + }, + + // nsISupports + QueryInterface: function(iid) { + if (!iid.equals(Components.interfaces.nsIBadCertListener2) && + !iid.equals(Components.interfaces.nsIInterfaceRequestor) && + !iid.equals(Components.interfaces.nsIUrlListener) && + !iid.equals(Components.interfaces.nsISupports)) + throw Components.results.NS_ERROR_NO_INTERFACE; + + return this; + } +} |