summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/accountcreation/GuessConfig.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/accountcreation/GuessConfig.jsm')
-rw-r--r--comm/mail/components/accountcreation/GuessConfig.jsm1317
1 files changed, 1317 insertions, 0 deletions
diff --git a/comm/mail/components/accountcreation/GuessConfig.jsm b/comm/mail/components/accountcreation/GuessConfig.jsm
new file mode 100644
index 0000000000..3d590311d9
--- /dev/null
+++ b/comm/mail/components/accountcreation/GuessConfig.jsm
@@ -0,0 +1,1317 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["GuessConfig"];
+
+const { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "AccountConfig",
+ "resource:///modules/accountcreation/AccountConfig.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const {
+ Abortable,
+ alertPrompt,
+ assert,
+ CancelledException,
+ ddump,
+ deepCopy,
+ Exception,
+ gAccountSetupLogger,
+ getStringBundle,
+ NotReached,
+ UserCancelledException,
+} = AccountCreationUtils;
+
+/**
+ * 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, socketType, done)}
+ * Called when we try a new hostname/port.
+ * type {String-enum} @see AccountConfig type - "imap", "pop3", "smtp"
+ * hostname {String}
+ * port {Integer}
+ * socketType {nsMsgSocketType} @see MailNewsTypes2.idl
+ * 0 = plain, 2 = STARTTLS, 3 = SSL
+ * 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 new Abortable();
+ }
+
+ if (!resultConfig) {
+ resultConfig = new lazy.AccountConfig();
+ }
+ resultConfig.source = lazy.AccountConfig.kSourceGuess;
+
+ if (!which) {
+ which = "both";
+ }
+
+ if (!Services.prefs.getBoolPref("mailnews.auto_config.guess.enabled")) {
+ errorCallback("Guessing config disabled per user preference");
+ return new Abortable();
+ }
+
+ 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) {
+ // TODO: don't do this. Bug 599173.
+ resultConfig.source = lazy.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 = Ci.nsMsgSocketType.alwaysSTARTTLS;
+ resultConfig.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext;
+ resultConfig.outgoing.hostname = "smtp." + domain;
+ resultConfig.outgoing.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS;
+ 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: Ci.nsMsgSocketType.alwaysSTARTTLS,
+ auth: Ci.nsMsgAuthMethod.passwordCleartext,
+ });
+ successCallback(resultConfig);
+ return new Abortable();
+ }
+ var progress = function (thisTry) {
+ progressCallback(
+ protocolToString(thisTry.protocol),
+ thisTry.hostname,
+ thisTry.port,
+ thisTry.socketType,
+ false,
+ resultConfig
+ );
+ };
+
+ var checkDone = function () {
+ if (incomingEx) {
+ try {
+ errorCallback(incomingEx, resultConfig);
+ } catch (e) {
+ console.error(e);
+ alertPrompt("Error in errorCallback for guessConfig()", e);
+ }
+ return;
+ }
+ if (outgoingEx) {
+ try {
+ errorCallback(outgoingEx, resultConfig);
+ } catch (e) {
+ console.error(e);
+ alertPrompt("Error in errorCallback for guessConfig()", e);
+ }
+ return;
+ }
+ if (incomingDone && outgoingDone) {
+ try {
+ successCallback(resultConfig);
+ } catch (e) {
+ try {
+ errorCallback(e);
+ } catch (e) {
+ console.error(e);
+ alertPrompt("Error in errorCallback for guessConfig()", e);
+ }
+ }
+ }
+ };
+
+ var logger = gAccountSetupLogger;
+ var HostTryToAccountServer = function (thisTry, server) {
+ server.type = protocolToString(thisTry.protocol);
+ server.hostname = thisTry.hostname;
+ server.port = thisTry.port;
+ server.socketType = thisTry.socketType;
+ server.auth =
+ thisTry.authMethod || 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 accountSetup.js.
+ server.badCert = thisTry.selfSignedCert;
+ server.targetSite = thisTry.targetSite;
+ logger.info(
+ "CHOOSING " +
+ server.type +
+ " " +
+ server.hostname +
+ ":" +
+ server.port +
+ ", auth method " +
+ server.auth +
+ (server.authAlternatives.length
+ ? " " + server.authAlternatives.join(",")
+ : "") +
+ ", socketType " +
+ 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,
+ resultConfig.incoming.auth
+ );
+ }
+ if (which == "outgoing" || which == "both") {
+ outgoingHostDetector.start(
+ resultConfig.outgoing.hostname ? resultConfig.outgoing.hostname : domain,
+ !!resultConfig.outgoing.hostname,
+ "smtp",
+ resultConfig.outgoing.port,
+ resultConfig.outgoing.socketType,
+ resultConfig.outgoing.auth
+ );
+ }
+
+ return new GuessAbortable(incomingHostDetector, outgoingHostDetector);
+}
+
+function GuessAbortable(incomingHostDetector, outgoingHostDetector) {
+ Abortable.call(this);
+ this._incomingHostDetector = incomingHostDetector;
+ this._outgoingHostDetector = outgoingHostDetector;
+}
+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 is defined towards the end of this file
+ * and not the same as those used in AccountConfig (type). (fix
+ * this)
+ */
+function HostTry() {}
+HostTry.prototype = {
+ // IMAP, POP or SMTP
+ protocol: UNKNOWN,
+ // {String}
+ hostname: undefined,
+ // {Integer}
+ port: undefined,
+ // {nsMsgSocketType}
+ socketType: 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 = [];
+
+ // init logging
+ this._log = gAccountSetupLogger;
+ this._log.info("created host detector");
+}
+
+HostDetector.prototype = {
+ cancel(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 {string} domain - Domain 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 {boolean} hostIsPrecise - If true, use only this hostname,
+ * do not guess hostnames.
+ * @param {"pop3"|"imap"|"exchange"|"smtp"|""} - Account type.
+ * @param {integer} port - The port to use. 0 to autodetect
+ * @param {nsMsgSocketType|-1} socketType - Socket type. -1 to autodetect.
+ * @param {nsMsgAuthMethod|0} authMethod - Authentication method. 0 to autodetect.
+ */
+ start(domain, hostIsPrecise, type, port, socketType, authMethod) {
+ domain = domain.replace(/\s*/g, ""); // Remove whitespace
+ if (!hostIsPrecise) {
+ hostIsPrecise = false;
+ }
+ var protocol = lazy.Sanitizer.translate(
+ type,
+ { imap: IMAP, pop3: POP, smtp: SMTP },
+ UNKNOWN
+ );
+ if (!port) {
+ port = UNKNOWN;
+ }
+ var ssl_only = Services.prefs.getBoolPref(
+ "mailnews.auto_config.guess.sslOnly"
+ );
+ this._cancel = false;
+ this._log.info(
+ `Starting ${protocol} detection on ${
+ !hostIsPrecise ? "~ " : ""
+ }${domain}:${port} with socketType=${socketType} and authMethod=${authMethod}`
+ );
+
+ // 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, socketType, port);
+ for (let j = 0; j < hostEntries.length; j++) {
+ let hostTry = hostEntries[j]; // from getHostEntry()
+ if (ssl_only && hostTry.socketType == NONE) {
+ continue;
+ }
+ hostTry.hostname = hostname;
+ hostTry.status = kNotTried;
+ hostTry.desc =
+ hostTry.hostname +
+ ":" +
+ hostTry.port +
+ " socketType=" +
+ hostTry.socketType +
+ " " +
+ protocolToString(hostTry.protocol);
+ hostTry.authMethod = authMethod;
+ 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() {
+ if (this._cancel) {
+ return;
+ }
+ var me = this;
+ var timeout = Services.prefs.getIntPref(
+ "mailnews.auto_config.guess.timeout"
+ );
+ // We assume we'll resolve the same proxy for all tries, and
+ // proceed to use the first resolved proxy for all tries. This
+ // assumption is generally sound, but not always: mechanisms like
+ // the pref network.proxy.no_proxies_on can make imap.domain and
+ // pop.domain resolve differently.
+ doProxy(this._hostsToTry[0].hostname, function (proxy) {
+ for (let i = 0; i < me._hostsToTry.length; i++) {
+ let thisTry = me._hostsToTry[i]; // {HostTry}
+ if (thisTry.status != kNotTried) {
+ continue;
+ }
+ me._log.info(thisTry.desc + ": initializing probe...");
+ if (i == 0) {
+ // showing 50 servers at once is pointless
+ me.mProgressCallback(thisTry);
+ }
+
+ thisTry.abortable = SocketUtil(
+ thisTry.hostname,
+ thisTry.port,
+ thisTry.socketType,
+ thisTry.commands,
+ timeout,
+ proxy,
+ new SSLErrorHandler(thisTry, me._log),
+ function (wiredata) {
+ // result callback
+ if (me._cancel) {
+ // Don't use response anymore.
+ return;
+ }
+ me.mProgressCallback(thisTry);
+ me._processResult(thisTry, wiredata);
+ me._checkFinished();
+ },
+ function (e) {
+ // error callback
+ if (me._cancel) {
+ // Who set cancel to true already called mErrorCallback().
+ return;
+ }
+ me._log.warn(thisTry.desc + ": " + e);
+ thisTry.status = kFailed;
+ me._checkFinished();
+ }
+ );
+ thisTry.status = kOngoing;
+ }
+ });
+ },
+
+ /**
+ * @param {HostTry} thisTry
+ * @param {string[]} wiredata - What the server returned in response to our protocol chat.
+ */
+ _processResult(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(
+ thisTry.desc + ": 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(thisTry.desc + ": no data");
+ thisTry.status = kFailed;
+ return;
+ }
+ this._log.info(thisTry.desc + ": wiredata: " + wiredata.join(""));
+ thisTry.authMethods = this._advertisesAuthMethods(
+ thisTry.protocol,
+ wiredata
+ );
+ if (
+ thisTry.socketType == STARTTLS &&
+ !this._hasSTARTTLS(thisTry, wiredata)
+ ) {
+ this._log.info(thisTry.desc + ": STARTTLS wanted, but not offered");
+ thisTry.status = kFailed;
+ return;
+ }
+ this._log.info(
+ thisTry.desc +
+ ": success" +
+ (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(
+ thisTry.desc + ": clearing validity override for " + thisTry.hostname
+ );
+ Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService)
+ .clearValidityOverride(thisTry.hostname, thisTry.port, {});
+ }
+ },
+
+ _checkFinished() {
+ 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;
+ } else if (thisTry.status == kSuccess && !unfinishedBusiness) {
+ // thisTry is good, and all higher preference tries failed, so use this
+ 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 {integer} protocol - IMAP, POP or SMTP
+ * @param {string[]} capaResponse - On the wire data that the server returned.
+ * May be the full exchange or just capa.
+ * @returns {nsMsgAuthMethod[]} Advertised authentication methods,
+ * in decreasing order of preference.
+ * E.g. [ nsMsgAuthMethod.GSSAPI, nsMsgAuthMethod.passwordEncrypted ]
+ * for a server that supports only Kerberos and encrypted passwords.
+ */
+ _advertisesAuthMethods(protocol, capaResponse) {
+ // For IMAP, capabilities include e.g.:
+ // "AUTH=CRAM-MD5", "AUTH=NTLM", "AUTH=GSSAPI", "AUTH=MSN", "AUTH=PLAIN"
+ // 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 supported = new Set();
+ 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)) {
+ supported.add(Ci.nsMsgAuthMethod.GSSAPI);
+ }
+ if (new RegExp(prefix + "CRAM-MD5").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.passwordEncrypted);
+ }
+ if (new RegExp(prefix + "(NTLM|MSN)").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.NTLM);
+ }
+ if (new RegExp(prefix + "LOGIN").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.passwordCleartext);
+ }
+ if (new RegExp(prefix + "PLAIN").test(line)) {
+ supported.add(Ci.nsMsgAuthMethod.passwordCleartext);
+ }
+ if (protocol != IMAP || !line.includes("LOGINDISABLED")) {
+ supported.add(Ci.nsMsgAuthMethod.passwordCleartext);
+ }
+ // The array elements will be in the Set's order of addition.
+ return Array.from(supported);
+ },
+
+ _hasSTARTTLS(thisTry, wiredata) {
+ var capa = thisTry.protocol == POP ? "STLS" : "STARTTLS";
+ return (
+ thisTry.socketType == STARTTLS &&
+ wiredata.join("").toUpperCase().includes(capa)
+ );
+ },
+};
+
+/**
+ * @param {nsMsgAuthMethod[]} authMethods - Authentication methods to choose from.
+ * See return value of _advertisesAuthMethods()
+ * Note: the returned auth method will be removed from the array.
+ * @returns {nsMsgAuthMethod} 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(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(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 = Ci.nsMsgSocketType.plain;
+var STARTTLS = Ci.nsMsgSocketType.alwaysSTARTTLS;
+var SSL = Ci.nsMsgSocketType.SSL;
+
+var IMAP_PORTS = {};
+IMAP_PORTS[NONE] = 143;
+IMAP_PORTS[STARTTLS] = 143;
+IMAP_PORTS[SSL] = 993;
+
+var POP_PORTS = {};
+POP_PORTS[NONE] = 110;
+POP_PORTS[STARTTLS] = 110;
+POP_PORTS[SSL] = 995;
+
+var SMTP_PORTS = {};
+SMTP_PORTS[NONE] = 587;
+SMTP_PORTS[STARTTLS] = 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 (a, b) {
+ // -1 = a is better; 1 = b is better; 0 = equal
+ // Prefer SSL/STARTTLS above all else
+ if (a.socketType != NONE && b.socketType == NONE) {
+ return -1;
+ }
+ if (b.socketType != NONE && a.socketType == 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;
+ }
+ // Prefer SSL/TLS over STARTTLS
+ if (a.socketType == SSL && b.socketType == STARTTLS) {
+ return -1;
+ }
+ if (a.socketType == STARTTLS && b.socketType == SSL) {
+ return 1;
+ }
+ // For hostnames, leave existing sorting, as in _hostnamesToTry()
+ // For ports, leave existing sorting, as in getOutgoingTryOrder()
+ return 0;
+ });
+}
+
+/**
+ * @returns {HostTry[]} Hosts to try.
+ */
+function getIncomingTryOrder(host, protocol, socketType, 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 (socketType == UNKNOWN) {
+ return [
+ getHostEntry(protocol, STARTTLS, port),
+ getHostEntry(protocol, SSL, port),
+ getHostEntry(protocol, NONE, port),
+ ];
+ }
+ return [getHostEntry(protocol, socketType, port)];
+ }
+ if (socketType == UNKNOWN) {
+ return [
+ getHostEntry(IMAP, STARTTLS, port),
+ getHostEntry(IMAP, SSL, port),
+ getHostEntry(POP, STARTTLS, port),
+ getHostEntry(POP, SSL, port),
+ getHostEntry(IMAP, NONE, port),
+ getHostEntry(POP, NONE, port),
+ ];
+ }
+ return [
+ getHostEntry(IMAP, socketType, port),
+ getHostEntry(POP, socketType, port),
+ ];
+}
+
+/**
+ * @returns {Array of {HostTry}}
+ */
+function getOutgoingTryOrder(host, protocol, socketType, port) {
+ assert(protocol == SMTP, "need SMTP as protocol for outgoing");
+ if (socketType == UNKNOWN) {
+ if (port == UNKNOWN) {
+ // neither SSL nor port known
+ return [
+ getHostEntry(SMTP, STARTTLS, UNKNOWN),
+ getHostEntry(SMTP, STARTTLS, 25),
+ getHostEntry(SMTP, SSL, UNKNOWN),
+ getHostEntry(SMTP, NONE, UNKNOWN),
+ getHostEntry(SMTP, NONE, 25),
+ ];
+ }
+ // port known, SSL not
+ return [
+ getHostEntry(SMTP, STARTTLS, port),
+ getHostEntry(SMTP, SSL, port),
+ getHostEntry(SMTP, NONE, port),
+ ];
+ }
+ // SSL known, port not
+ if (port == UNKNOWN) {
+ if (socketType == SSL) {
+ return [getHostEntry(SMTP, SSL, UNKNOWN)];
+ }
+ return [
+ getHostEntry(SMTP, socketType, UNKNOWN),
+ getHostEntry(SMTP, socketType, 25),
+ ];
+ }
+ // SSL and port known
+ return [getHostEntry(SMTP, socketType, port)];
+}
+
+/**
+ * @returns {HostTry} with proper default port and commands,
+ * but without hostname.
+ */
+function getHostEntry(protocol, socketType, port) {
+ if (!port || port == UNKNOWN) {
+ switch (protocol) {
+ case POP:
+ port = POP_PORTS[socketType];
+ break;
+ case IMAP:
+ port = IMAP_PORTS[socketType];
+ break;
+ case SMTP:
+ port = SMTP_PORTS[socketType];
+ break;
+ default:
+ throw new NotReached("unsupported protocol " + protocol);
+ }
+ }
+
+ var r = new HostTry();
+ r.protocol = protocol;
+ r.socketType = socketType;
+ r.port = port;
+ r.commands = CMDS[protocol];
+ return r;
+}
+
+// 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
+
+/**
+ * @param thisTry {HostTry}
+ * @param logger {ConsoleAPI}
+ */
+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(secInfo, targetSite) {
+ this._log.error("Got Cert error for " + targetSite);
+
+ if (!secInfo) {
+ return;
+ }
+
+ let cert = secInfo.serverCert;
+
+ 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 customer 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 (secInfo.isDomainMismatch) {
+ this._try._gotCertError = Ci.nsICertOverrideService.ERROR_MISMATCH;
+ } else if (secInfo.isUntrusted) {
+ // e.g. self-signed
+ this._try._gotCertError = Ci.nsICertOverrideService.ERROR_UNTRUSTED;
+ } else if (secInfo.isNotValidAtThisTime) {
+ this._try._gotCertError = 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, true); // temporary override
+ this._log.warn(`Added temporary override of bad cert for: ${host}:${port}`);
+ },
+};
+
+// -----------
+// Socket Util
+
+/**
+ * @param hostname {String} The DNS hostname to connect to.
+ * @param port {Integer} The numeric port to connect to on the host.
+ * @param socketType {nsMsgSocketType} SSL, STARTTLS 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 proxy {nsIProxyInfo} The proxy to use (or null to not use any).
+ * @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,
+ socketType,
+ commands,
+ timeout,
+ proxy,
+ 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;
+ if (socketType == SSL) {
+ socketTypeName = ["ssl"];
+ } else if (socketType == STARTTLS) {
+ socketTypeName = ["starttls"];
+ } else {
+ socketTypeName = [];
+ }
+ var transport = transportService.createTransport(
+ socketTypeName,
+ hostname,
+ port,
+ proxy,
+ null
+ );
+
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, timeout);
+ transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, timeout);
+
+ 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: [],
+ onStartRequest(request) {
+ try {
+ initialized = true;
+ if (!aborted) {
+ // Send the first request
+ let outputData = commands[index++];
+ outstream.write(outputData, outputData.length);
+ }
+ } catch (e) {
+ _error(e);
+ }
+ },
+ async onStopRequest(request, status) {
+ try {
+ instream.close();
+ outstream.close();
+ // Did it fail because of a bad certificate?
+ let isCertError = false;
+ if (!Components.isSuccessCode(status)) {
+ let nssErrorsService = Cc[
+ "@mozilla.org/nss_errors_service;1"
+ ].getService(Ci.nsINSSErrorsService);
+ try {
+ let errorType = nssErrorsService.getErrorClass(status);
+ if (errorType == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
+ isCertError = true;
+ }
+ } catch (e) {
+ // nsINSSErrorsService.getErrorClass throws if given a non-STARTTLS,
+ // non-cert error, so ignore this.
+ }
+ }
+ if (isCertError) {
+ if (
+ Services.prefs.getBoolPref(
+ "mailnews.auto_config.guess.requireGoodCert",
+ true
+ )
+ ) {
+ gAccountSetupLogger.info(
+ `Bad (overridable) certificate for ${hostname}:${port}. Set mailnews.auto_config.guess.requireGoodCert to false to allow detecting this as a valid SSL/TLS configuration`
+ );
+ } else {
+ let socketTransport = transport.QueryInterface(
+ Ci.nsISocketTransport
+ );
+ let secInfo =
+ await socketTransport.tlsSocketControl?.asyncGetSecurityInfo();
+ sslErrorHandler.processCertError(secInfo, hostname + ":" + port);
+ }
+ }
+ resultCallback(this.data.length ? this.data : null);
+ } catch (e) {
+ _error(e);
+ }
+ },
+ onDataAvailable(request, 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, 0, 0, false);
+ pump.asyncRead(dataListener);
+ 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(Cr.NS_ERROR_ABORT);
+ } catch (e) {
+ ddump("canceling socket failed: " + e);
+ }
+};
+
+/**
+ * Resolve a proxy for some domain and expose it via a callback.
+ *
+ * @param hostname {String} The hostname which a proxy will be resolved for
+ * @param resultCallback {function(proxyInfo)}
+ * Called after the proxy has been resolved for hostname.
+ * proxy {nsIProxyInfo} The resolved proxy, or null if none were found
+ * for hostname
+ */
+function doProxy(hostname, resultCallback) {
+ // This implements the nsIProtocolProxyCallback interface:
+ function ProxyResolveCallback() {}
+ ProxyResolveCallback.prototype = {
+ onProxyAvailable(req, uri, proxy, status) {
+ // Anything but a SOCKS proxy will be unusable for email.
+ if (proxy != null && proxy.type != "socks" && proxy.type != "socks4") {
+ proxy = null;
+ }
+ resultCallback(proxy);
+ },
+ };
+ var proxyService = Cc[
+ "@mozilla.org/network/protocol-proxy-service;1"
+ ].getService(Ci.nsIProtocolProxyService);
+ // Use some arbitrary scheme just because it is required...
+ var uri = Services.io.newURI("http://" + hostname);
+ // ... we'll ignore it any way. We prefer SOCKS since that's the
+ // only thing we can use for email protocols.
+ var proxyFlags =
+ Ci.nsIProtocolProxyService.RESOLVE_IGNORE_URI_SCHEME |
+ Ci.nsIProtocolProxyService.RESOLVE_PREFER_SOCKS_PROXY;
+ if (Services.prefs.getBoolPref("network.proxy.socks_remote_dns")) {
+ proxyFlags |= Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL;
+ }
+ proxyService.asyncResolve(uri, proxyFlags, new ProxyResolveCallback());
+}
+
+var GuessConfig = {
+ UNKNOWN,
+ IMAP,
+ POP,
+ SMTP,
+ NONE,
+ STARTTLS,
+ SSL,
+ getHostEntry,
+ getIncomingTryOrder,
+ getOutgoingTryOrder,
+ guessConfig,
+};