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