summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/accountcreation/FetchConfig.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/accountcreation/FetchConfig.jsm')
-rw-r--r--comm/mail/components/accountcreation/FetchConfig.jsm299
1 files changed, 299 insertions, 0 deletions
diff --git a/comm/mail/components/accountcreation/FetchConfig.jsm b/comm/mail/components/accountcreation/FetchConfig.jsm
new file mode 100644
index 0000000000..1bf16ca2ed
--- /dev/null
+++ b/comm/mail/components/accountcreation/FetchConfig.jsm
@@ -0,0 +1,299 @@
+/* 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 = ["FetchConfig"];
+
+const { AccountCreationUtils } = ChromeUtils.import(
+ "resource:///modules/accountcreation/AccountCreationUtils.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "FetchHTTP",
+ "resource:///modules/accountcreation/FetchHTTP.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "readFromXML",
+ "resource:///modules/accountcreation/readFromXML.jsm"
+);
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "Sanitizer",
+ "resource:///modules/accountcreation/Sanitizer.jsm"
+);
+
+const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm");
+const { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm");
+
+const {
+ Abortable,
+ ddump,
+ Exception,
+ PriorityOrderAbortable,
+ PromiseAbortable,
+ readURLasUTF8,
+ runAsync,
+ SuccessiveAbortable,
+ TimeoutAbortable,
+} = AccountCreationUtils;
+
+/**
+ * Tries to find a configuration for this ISP on the local harddisk, in the
+ * application install directory's "isp" subdirectory.
+ * Params @see fetchConfigFromISP()
+ */
+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(lazy.Sanitizer.hostname(domain) + ".xml");
+
+ if (!configLocation.exists() || !configLocation.isReadable()) {
+ errorCallback(new Exception("local file not found"));
+ return;
+ }
+ var contents = readURLasUTF8(Services.io.newFileURI(configLocation));
+ let domParser = new DOMParser();
+ const xml = JXON.build(domParser.parseFromString(contents, "text/xml"));
+ successCallback(lazy.readFromXML(xml, "disk"));
+ } 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 parameter will be an exception object or error string.
+ */
+function fetchConfigFromISP(
+ domain,
+ emailAddress,
+ successCallback,
+ errorCallback
+) {
+ if (
+ !Services.prefs.getBoolPref("mailnews.auto_config.fetchFromISP.enabled")
+ ) {
+ errorCallback(new Exception("ISP fetch disabled per user preference"));
+ return new Abortable();
+ }
+
+ let conf1 =
+ "autoconfig." + lazy.Sanitizer.hostname(domain) + "/mail/config-v1.1.xml";
+ // .well-known/ <http://tools.ietf.org/html/draft-nottingham-site-meta-04>
+ let conf2 =
+ lazy.Sanitizer.hostname(domain) +
+ "/.well-known/autoconfig/mail/config-v1.1.xml";
+ // This list is sorted by decreasing priority
+ var urls = ["https://" + conf1, "https://" + conf2];
+ if (
+ !Services.prefs.getBoolPref("mailnews.auto_config.fetchFromISP.sslOnly")
+ ) {
+ urls.push("http://" + conf1, "http://" + conf2);
+ }
+ let callArgs = {
+ urlArgs: {
+ emailaddress: emailAddress,
+ },
+ };
+ if (
+ !Services.prefs.getBoolPref(
+ "mailnews.auto_config.fetchFromISP.sendEmailAddress"
+ )
+ ) {
+ delete callArgs.urlArgs.emailaddress;
+ }
+ let call;
+ let fetch;
+
+ let priority = new PriorityOrderAbortable(
+ (xml, call) =>
+ successCallback(lazy.readFromXML(xml, `isp-${call.foundMsg}`)),
+ errorCallback
+ );
+ for (let url of urls) {
+ call = priority.addCall();
+ call.foundMsg = url.startsWith("https") ? "https" : "http";
+ fetch = new lazy.FetchHTTP(
+ url,
+ callArgs,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+ fetch.start();
+ }
+
+ return priority;
+}
+
+/**
+ * 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");
+ if (!url) {
+ errorCallback(new Exception("no URL for ISP DB configured"));
+ return new Abortable();
+ }
+ domain = lazy.Sanitizer.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);
+ }
+
+ let fetch = new lazy.FetchHTTP(
+ url,
+ { timeout: 10000 }, // 10 seconds
+ function (result) {
+ successCallback(lazy.readFromXML(result, "db"));
+ },
+ errorCallback
+ );
+ fetch.start();
+ return fetch;
+}
+
+/**
+ * Does a lookup of DNS MX, to get the server that is responsible for
+ * receiving mail for this domain. Then it takes the domain of that
+ * server, and does another lookup (in ISPDB and possibly 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 mechanism 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) {
+ const sanitizedDomain = lazy.Sanitizer.hostname(domain);
+ const sucAbortable = new SuccessiveAbortable();
+ const time = Date.now();
+
+ sucAbortable.current = getMX(
+ sanitizedDomain,
+ function (mxHostname) {
+ // success
+ ddump("getmx took " + (Date.now() - time) + "ms");
+ let sld = Services.eTLD.getBaseDomainFromHost(mxHostname);
+ ddump("base domain " + sld + " for " + mxHostname);
+ if (sld == sanitizedDomain) {
+ errorCallback(
+ new Exception("MX lookup would be no different from domain")
+ );
+ return;
+ }
+
+ // In addition to just the base domain, also check the full domain of the MX server
+ // to differentiate between Outlook.com/Hotmail and Office365 business domains.
+ let mxDomain;
+ try {
+ mxDomain = Services.eTLD.getNextSubDomain(mxHostname);
+ } catch (ex) {
+ // e.g. hostname doesn't have enough components
+ console.error(ex); // not fatal
+ }
+ let priority = new PriorityOrderAbortable(successCallback, errorCallback);
+ if (mxDomain && sld != mxDomain) {
+ let call = priority.addCall();
+ let fetch = fetchConfigFromDB(
+ mxDomain,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+ }
+ let call = priority.addCall();
+ let fetch = fetchConfigFromDB(
+ sld,
+ call.successCallback(),
+ call.errorCallback()
+ );
+ call.setAbortable(fetch);
+ sucAbortable.current = priority;
+ },
+ errorCallback
+ );
+ return sucAbortable;
+}
+
+/**
+ * Queries the DNS MX records for a given domain. Calls `successCallback` with
+ * the hostname of the MX server. If there are several entries with different
+ * preference values, only the most preferred (i.e. has the lowest value)
+ * is used. If there are several most preferred servers (i.e. round robin),
+ * only one of them is used.
+ *
+ * @param {string} sanitizedDomain @see fetchConfigFromISP()
+ * @param {function(hostname {string})} - successCallback
+ * Called when we found an MX for the domain.
+ * For |hostname|, see description above.
+ * @param {function({Exception|string})} errorCallback @see fetchConfigFromISP()
+ */
+function getMX(sanitizedDomain, successCallback, errorCallback) {
+ return new PromiseAbortable(
+ DNS.mx(sanitizedDomain),
+ function (records) {
+ const filteredRecs = records.filter(record => record.host);
+
+ if (filteredRecs.length > 0) {
+ const sortedRecs = filteredRecs.sort((a, b) => a.prio > b.prio);
+ const firstHost = sortedRecs[0].host;
+ successCallback(firstHost);
+ } else {
+ errorCallback(
+ new Exception(
+ "No hostname found in MX records for sanitizedDomain=" +
+ sanitizedDomain
+ )
+ );
+ }
+ },
+ errorCallback
+ );
+}
+
+var FetchConfig = {
+ forMX: fetchConfigForMX,
+ fromDB: fetchConfigFromDB,
+ fromISP: fetchConfigFromISP,
+ fromDisk: fetchConfigFromDisk,
+};