Bug 1500105 - Support Exchange AutoDiscover and parallelize network calls. r=aceman,mkmelin,Neil a=jorgk
authorBen Bucksch <ben.bucksch@beonex.com>
Fri, 18 Jan 2019 11:03:00 +0100
changeset 32052 c728c1579f04d8c596c1224dc62ef1e6cd39cae6
parent 32051 56ab5861f5cb37e01c1cd3aec27578ebe0509de2
child 32053 803b4914794b249e566ff169f0922d11630b53db
push id142
push usermozilla@jorgk.com
push dateMon, 21 Jan 2019 11:32:07 +0000
treeherdercomm-esr60@acaf08e72ead [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaceman, mkmelin, Neil, jorgk
bugs1500105
Bug 1500105 - Support Exchange AutoDiscover and parallelize network calls. r=aceman,mkmelin,Neil a=jorgk
mail/components/accountcreation/content/accountConfig.js
mail/components/accountcreation/content/createInBackend.js
mail/components/accountcreation/content/emailWizard.js
mail/components/accountcreation/content/emailWizard.xul
mail/components/accountcreation/content/exchangeAutoDiscover.js
mail/components/accountcreation/content/fetchConfig.js
mail/components/accountcreation/content/fetchhttp.js
mail/components/accountcreation/content/guessConfig.js
mail/components/accountcreation/content/readFromXML.js
mail/components/accountcreation/content/util.js
mail/components/accountcreation/content/verifyConfig.js
mail/components/accountcreation/jar.mn
mail/themes/linux/mail/accountCreation.css
mail/themes/osx/mail/accountCreation.css
mail/themes/shared/jar.inc.mn
mail/themes/shared/mail/accountCreation.css
mail/themes/windows/mail/accountCreation.css
mailnews/mailnews.js
--- a/mail/components/accountcreation/content/accountConfig.js
+++ b/mail/components/accountcreation/content/accountConfig.js
@@ -65,17 +65,17 @@ AccountConfig.prototype =
   domains : null,
 
   /**
    * Factory function for incoming and incomingAlternatives
    */
   createNewIncoming : function()
   {
     return {
-      // { String-enum: "pop3", "imap", "nntp" }
+      // { String-enum: "pop3", "imap", "nntp", "exchange" }
       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 }
@@ -109,16 +109,21 @@ AccountConfig.prototype =
       // 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,
+
+      // for Microsoft Exchange servers. Optional.
+      owaURL: null,
+      ewsURL: null,
+      easURL: null,
     };
   },
   /**
    * Factory function for outgoing and outgoingAlternatives
    */
   createNewOutgoing : function()
   {
     return {
@@ -138,47 +143,97 @@ AccountConfig.prototype =
       // nsISmtpServer.key
       existingServerKey : null,
       // user display value for existingServerKey
       existingServerLabel : null,
     };
   },
 
   /**
+   * The configuration needs an addon to handle the account type.
+   * The addon needs to be installed before the account can be created
+   * in the backend.
+   * You can choose one, if there are several addons in the list.
+   * (Optional)
+   *
+   * Array of:
+   * {
+   *   id: "owl@example.com" {string},
+   *
+   *   // already localized string
+   *   name: "Owl" {string},
+   *
+   *   // already localized string
+   *   description: "A third party addon that allows you to connect to Exchange servers" {string}
+   *
+   *   // Minimal version of the addon. Needed in case the addon is already installed,
+   *   // to verify that the installed version is sufficient.
+   *   // The XPI URL below must satisfy this.
+   *   // Must satisfy <https://developer.mozilla.org/en-US/docs/Mozilla/Toolkit_version_format>
+   *   minVersion: "0.2" {string}
+   *
+   *   xpiURL: "https://live.thunderbird.net/autoconfig/owl.xpi" {URL},
+   *   websiteURL: "https://www.beonex.com/owl/" {URL},
+   *   icon32: "https://www.beonex.com/owl/owl-32x32.png" {URL},
+   *
+   *   useType : {
+   *     // Type shown as radio button to user in the config result.
+   *     // Users won't understand OWA vs. EWS vs. EAS etc., so this is an abstraction
+   *     // from the end user perspective.
+   *     generalType: "exchange" {string},
+   *
+   *     // Protocol
+   *     // Independent of the addon
+   *     protocolType: "owa" {string},
+   *
+   *     // Account type in the Thunderbird backend.
+   *     // What nsIMsgAccount.type will be set to when creating the account.
+   *     // This is specific to the addon.
+   *     addonAccountType: "owl-owa" {string},
+   *   }
+   * }
+   */
+  addons: 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)
+    for (let 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.useGlobalPreferredServer ||
           (!!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()
+AccountConfig.kSourceExchange = 4; // from Microsoft Exchange AutoDiscover
 
 
 /**
  * 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 @)
--- a/mail/components/accountcreation/content/createInBackend.js
+++ b/mail/components/accountcreation/content/createInBackend.js
@@ -1,32 +1,32 @@
 /* -*- 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/. */
 
+
+ChromeUtils.import("resource:///modules/mailServices.js");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+/* eslint-disable complexity */
+
 /**
  * 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.
+ * @param {AccountConfig} config - The account to create
+ * @return {nsIMsgAccount} - the newly created account
  */
-
-ChromeUtils.import("resource:///modules/mailServices.js");
-ChromeUtils.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"]));
+      config.incoming.type);
   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);
@@ -256,25 +256,25 @@ function rememberPassword(server, passwo
  *     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.type,
         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.type,
           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.
--- a/mail/components/accountcreation/content/emailWizard.js
+++ b/mail/components/accountcreation/content/emailWizard.js
@@ -32,17 +32,20 @@ ChromeUtils.import("resource:///modules/
  */
 
 
 // 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,20}|\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/i;
 
 if (typeof gEmailWizardLogger == "undefined") {
   ChromeUtils.import("resource:///modules/gloda/log4moz.js");
-  var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+  var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.setup");
+  gEmailWizardLogger.level = Log4Moz.Level.Info;
+  gEmailWizardLogger.addAppender(new Log4Moz.ConsoleAppender(new Log4Moz.BasicFormatter())); // browser console
+  gEmailWizardLogger.addAppender(new Log4Moz.DumpAppender(new Log4Moz.BasicFormatter())); // stdout
 }
 
 var gStringsBundle;
 var gMessengerBundle;
 var gBrandShortName;
 
 /*********************
 TODO for bug 549045
@@ -114,16 +117,22 @@ function setText(id, value)
   }
 }
 
 function setLabelFromStringBundle(elementID, stringName)
 {
   e(elementID).label = gMessengerBundle.getString(stringName);
 };
 
+function removeChildNodes(el) {
+  while (el.hasChildNodes()) {
+    el.lastChild.remove();
+  }
+}
+
 function EmailConfigWizard()
 {
   this._init();
 }
 EmailConfigWizard.prototype =
 {
   _init : function EmailConfigWizard__init()
   {
@@ -294,41 +303,44 @@ EmailConfigWizard.prototype =
 
       _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("provisioner_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("provisioner_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");
+      _hide("provisioner_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");
@@ -348,45 +360,48 @@ EmailConfigWizard.prototype =
       _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;
+      _hide("provisioner_button");
       _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");
+      _hide("provisioner_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");
       }
+      _hide("provisioner_button");
     }
     window.sizeToContent();
   },
 
   /**
    * Start from beginning with possibly new email address.
    */
   onStartOver : function()
@@ -545,90 +560,91 @@ EmailConfigWizard.prototype =
 
   /////////////////////////////////////////////////////////////////
   // Detection step
 
   /**
    * Try to find an account configuration for this email address.
    * This is the function which runs the autoconfig.
    */
-  findConfig : function(domain, email)
-  {
+  findConfig(domain, emailAddress) {
     gEmailWizardLogger.info("findConfig()");
     if (this._abortable) {
       this.onStop();
     }
     this.switchToMode("find-config");
-    this.startSpinner("looking_up_settings_disk");
+    e("status_area").setAttribute("status", "loading");
+    e("status_msg").textContent = "";
+
     var self = this;
-    this._abortable = fetchConfigFromDisk(domain,
-      function(config) // success
-      {
+    var call = null;
+    var fetch = null;
+
+    var priority = this._abortable = new PriorityOrderAbortable(
+      function(config, call) { // success
         self._abortable = null;
+        self.removeStatusLines();
+        self.stopSpinner(call.foundMsg);
         self.foundConfig(config);
-        self.stopSpinner("found_settings_disk");
       },
-      function(e) // fetchConfigFromDisk failed
-      {
+      function(e) { // all failed
+        self._abortable = null;
+        self.removeStatusLines();
         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);
-                  });
-              });
-          });
+
+        // guess config
+        let initialConfig = new AccountConfig();
+        self._prefillConfig(initialConfig);
+        self._guessConfig(domain, initialConfig);
       });
+    priority.addOneFinishedObserver(call => this.updateStatusLine(call));
+
+    try {
+      call = priority.addCall();
+      this.addStatusLine("looking_up_settings_disk", call);
+      call.foundMsg = "found_settings_disk";
+      fetch = fetchConfigFromDisk(domain,
+        call.successCallback(), call.errorCallback());
+      call.setAbortable(fetch);
+
+      call = priority.addCall();
+      this.addStatusLine("looking_up_settings_isp", call);
+      call.foundMsg = "found_settings_isp";
+      fetch = fetchConfigFromISP(domain, emailAddress,
+        call.successCallback(), call.errorCallback());
+      call.setAbortable(fetch);
+
+      call = priority.addCall();
+      this.addStatusLine("looking_up_settings_db", call);
+      call.foundMsg = "found_settings_db";
+      fetch = fetchConfigFromDB(domain,
+        call.successCallback(), call.errorCallback());
+      call.setAbortable(fetch);
+
+      call = priority.addCall();
+      this.addStatusLine("looking_up_settings_db", call);
+      call.foundMsg = "found_settings_db";
+      fetch = fetchConfigForMX(domain,
+        call.successCallback(), call.errorCallback());
+      call.setAbortable(fetch);
+
+      call = priority.addCall();
+      this.addStatusLine("looking_up_settings_isp", call);
+      call.foundMsg = "found_settings_isp";
+      fetch = fetchConfigFromExchange(domain, emailAddress, self._password,
+        call.successCallback(), call.errorCallback());
+      call.setAbortable(fetch);
+
+    } catch (e) { // e.g. when entering an invalid domain like "c@c.-com"
+      this.showErrorMsg(e);
+      this.removeStatusLines();
+      this.onStop();
+    }
   },
 
   /**
    * Just a continuation of findConfig()
    */
   _guessConfig : function(domain, initialConfig)
   {
     this.startSpinner("looking_up_settings_guess")
@@ -662,30 +678,26 @@ EmailConfigWizard.prototype =
 
   /**
    * When findConfig() was successful, it calls this.
    * This displays the config to the user.
    */
   foundConfig : function(config)
   {
     gEmailWizardLogger.info("foundConfig()");
+    gEmailWizardLogger.info(debugObject(config, "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.
@@ -745,32 +757,84 @@ EmailConfigWizard.prototype =
 
   showErrorStatus : function(actionStrName)
   {
     e("status_area").setAttribute("status", "error");
     gEmailWizardLogger.warn("status error " + actionStrName);
     this._showStatusTitle(actionStrName);
   },
 
+  showErrorMsg(errorMsg) {
+    gEmailWizardLogger.warn("error " + errorMsg);
+    e("status_area").setAttribute("status", "error");
+    e("status_msg").textContent = errorMsg;
+  },
+
   _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);
   },
 
+  // UI to show status updates in parallel
+
+  addStatusLine(msgID, call) {
+    _show("status-lines");
+    var statusLine = document.createElement("hbox");
+    e("status-lines").appendChild(statusLine);
+    statusLine.classList.add("status-line");
+    var statusDescr = document.createElement("description");
+    statusDescr.classList.add("status_msg");
+    statusLine.appendChild(statusDescr);
+    var statusImg = document.createElement("vbox");
+    statusImg.classList.add("status-img");
+    statusImg.setAttribute("pack", "start");
+    statusLine.appendChild(statusImg);
+    let msg = msgID;
+    try {
+      msg = gStringsBundle.getFormattedString(msgID, [gBrandShortName]);
+    } catch (e) {
+      console.error(e);
+    }
+    statusDescr.textContent = msg;
+    call.statusLine = statusLine;
+    statusLine.setAttribute("status", "loading");
+  },
+
+  updateStatusLine(call) {
+    console.log("update status line for call " + call.position);
+    let line = [...document.querySelectorAll("#status-lines > .status-line")]
+      .find(line => line == call.statusLine);
+    if (!line) {
+      return;
+    }
+    if (!call.finished) {
+      line.setAttribute("status", "loading");
+    } else if (!call.succeeded) {
+      line.setAttribute("status", "failed");
+    } else {
+      line.setAttribute("status", "succeeded");
+    }
+  },
+
+  removeStatusLines() {
+    removeChildNodes(e("status-lines"));
+    _hide("status-lines");
+  },
+
 
 
   /////////////////////////////////////////////////////////////////
   // Result area
 
   /**
    * Displays a (probed) config to the user,
    * in the result config details area.
@@ -778,16 +842,113 @@ EmailConfigWizard.prototype =
    * @param config {AccountConfig} The config to present to user
    */
   displayConfigResult : function(config)
   {
     assert(config instanceof AccountConfig);
     this._currentConfig = config;
     var configFilledIn = this.getConcreteConfig();
 
+    // IMAP / POP3 server type radio buttons
+    let alternatives = config.incomingAlternatives.filter(alt =>
+        (alt.type == "imap" || alt.type == "pop3" || alt.type == "exchange") &&
+        alt.type != config.incoming.type
+      );
+    let alternative = alternatives[0];
+    if (alternative) {
+      _show("result_servertype");
+      _hide("result_select_imap");
+      _hide("result_select_pop3");
+      _hide("result_select_exchange");
+      _show("result_select_" + alternative.type);
+      _show("result_select_" + config.incoming.type);
+      e("result_select_" + alternative.type).configIncoming = alternative;
+      e("result_select_" + config.incoming.type).configIncoming =
+          config.incoming;
+      e("result_servertype").value = config.incoming.type;
+    } else {
+      _hide("result_servertype");
+    }
+
+    if (config.incoming.type == "exchange") {
+      _hide("result_hostnames");
+      _show("result_exchange");
+      setText("result_exchange_hostname", config.incoming.hostname);
+      _disable("create_button");
+      removeChildNodes(e("result_addon_install_rows"));
+      this.switchToMode("result");
+
+      (async () => {
+        for (let addon of config.addons) {
+          let installer = new AddonInstaller(addon);
+          addon.isInstalled = await installer.isInstalled();
+        }
+        let installedAddon = config.addons.find(addon => addon.isInstalled);
+        if (installedAddon) {
+          _hide("result_addon_intro");
+          _hide("result_addon_install");
+          _enable("create_button");
+          this.onCreate = () => { // TODO
+            this._currentConfig.incoming.type = installedAddon.useType.addonAccountType;
+            this.validateAndFinish();
+          };
+        } else {
+          var installLabel = getStringBundle(
+            "chrome://mozapps/locale/xpinstall/xpinstallConfirm.properties")
+            .GetStringFromName("installButtonLabel");
+          let containerE = e("result_addon_install_rows");
+          for (let addon of config.addons) {
+            // Creates
+            // <row>
+            //   <image src="https://live.thunderbird.net/owl32.png" />
+            //   <label class="text-link" href="https://live.thunderbird.net/owl">
+            //     A third party addon that ...
+            //   </label>
+            //   <button
+            //     class="larger-button"
+            //     orient="vertical" crop="right"
+            //     label="Install"
+            //     oncommand="…" />
+            // </row>
+            let addonE = document.createElement("row");
+            let iconE = document.createElement("image");
+            let descrE = document.createElement("label"); // must be <label> to be clickable
+            let buttonE = document.createElement("button");
+            addonE.appendChild(iconE);
+            addonE.appendChild(descrE);
+            addonE.appendChild(buttonE);
+            containerE.appendChild(addonE);
+            addonE.setAttribute("align", "center");
+            iconE.classList.add("icon");
+            if (addon.icon32) {
+              iconE.setAttribute("src", addon.icon32);
+            }
+            descrE.classList.add("text-link");
+            descrE.setAttribute("href", addon.websiteURL);
+            descrE.textContent = addon.description;
+            buttonE.classList.add("larger-button");
+            buttonE.setAttribute("orient", "vertical");
+            buttonE.setAttribute("crop", "right");
+            buttonE.setAttribute("label", installLabel);
+            buttonE.setAttribute("oncommand", "gEmailConfigWizard.addonInstall(this.addon);");
+            buttonE.addon = addon;
+          }
+          _show("result_addon_install");
+          _disable("create_button");
+        }
+
+        window.sizeToContent();
+      })();
+      return;
+    }
+
+    _show("result_hostnames");
+    _hide("result_exchange");
+    _enable("create_button");
+
     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 +
@@ -828,65 +989,75 @@ EmailConfigWizard.prototype =
             [ 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()
-  {
+  onResultServerTypeChanged() {
     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;
+    config.incoming = e("result_servertype").selectedItem.configIncoming;
     // remove newly selected server from list of alternatives
-    config.incomingAlternatives = config.incomingAlternatives.filter(
-        function(e) { return e != config.incoming; });
+    config.incomingAlternatives = config.incomingAlternatives.filter(alt =>
+      alt != config.incoming);
     this.displayConfigResult(config);
   },
 
+  /**
+   * Install the addon
+   * Called when user clicks [Install] button.
+   *
+   * @param {AddonInfo} addon - @see AccountConfig.addons
+   */
+  async addonInstall(addon) {
+    _hide("result_addon_install");
+    _hide("result_addon_intro");
+    _disable("create_button");
+    _show("status_area");
+    var installBundle = getStringBundle(
+      "chrome://mozapps/locale/extensions/extensions.properties");
+    e("status_msg").textContent = installBundle.GetStringFromName("installInstalling");
+    e("status_area").setAttribute("status", "loading");
+
+    try {
+      var installer = this._abortable = new AddonInstaller(addon);
+      await installer.install();
+
+      this._abortable = null;
+      _hide("stop_button");
+      e("status_area").setAttribute("status", "result");
+      e("status_msg").textContent = installBundle.GetStringFromName("installVerifying");
+      _enable("create_button");
+
+      this._currentConfig.incoming.type = addon.useType.addonAccountType;
+      this.validateAndFinish();
+    } catch (e) {
+      this.showErrorMsg(e + "");
+      _show("result_addon_install");
+    }
+  },
+
+
 
 
   /////////////////////////////////////////////////////////////////
   // Manual Edit area
 
   /**
    * Gets the values from the user in the manual edit area.
    *
@@ -1209,20 +1380,18 @@ EmailConfigWizard.prototype =
    * 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();
+    // menulist.removeAllItems() is nice, but "nicely" clears the user value, too
+    removeChildNodes(menu.menupopup);
 
     // 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
     }
   },
@@ -1614,58 +1783,64 @@ EmailConfigWizard.prototype =
         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();
+        self.finish(configFilledIn);
       },
       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()
-  {
+  finish(concreteConfig) {
     gEmailWizardLogger.info("creating account in backend");
-    createAccountInBackend(this.getConcreteConfig());
+    var account = createAccountInBackend(concreteConfig);
+
+    // Trigger first login, to get folder structure, show account, etc..
+    account.incomingServer.rootFolder.getNewMessages(null, null);
+
     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"]);
+_gStandardPorts.imap = [ 143, 993 ];
+_gStandardPorts.pop3 = [ 110, 995 ];
+_gStandardPorts.smtp = [ 587, 25, 465 ]; // order matters
+_gStandardPorts.exchange = [ 443 ];
+var _gAllStandardPorts = _gStandardPorts.smtp
+    .concat(_gStandardPorts.imap)
+    .concat(_gStandardPorts.pop3)
+    .concat(_gStandardPorts.exchange);
 
 function isStandardPort(port)
 {
   return _gAllStandardPorts.indexOf(port) != -1;
 }
 
 function getStandardPorts(protocolType)
 {
@@ -1724,17 +1899,17 @@ SecurityWarningDialog.prototype =
     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) {
+    if (configFilledIn.outgoing.addThisServer) {
       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);
--- a/mail/components/accountcreation/content/emailWizard.xul
+++ b/mail/components/accountcreation/content/emailWizard.xul
@@ -48,16 +48,18 @@
           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/exchangeAutoDiscover.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();"/>
@@ -196,33 +198,36 @@
                     label="&rememberPassword.label;"
                     accesskey="&rememberPassword.accesskey;"
                     checked="true"/>
         </row>
       </rows>
     </grid>
     <spacer flex="1" />
 
-     <hbox id="status_area" flex="1">
+    <hbox id="status_area" flex="1">
       <vbox id="status_img_before" pack="start"/>
       <description id="status_msg">&#160;</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>
+    <vbox id="status-lines"/>
 
     <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 id="result_servertype" orient="horizontal">
+        <radio id="result_select_imap" label="&imapLong.label;" value="imap"
+               oncommand="gEmailConfigWizard.onResultServerTypeChanged();"/>
+        <radio id="result_select_pop3" label="&pop3Long.label;" value="pop3"
+               oncommand="gEmailConfigWizard.onResultServerTypeChanged();"/>
+        <radio id="result_select_exchange" label="Exchange" value="exchange"
+               oncommand="gEmailConfigWizard.onResultServerTypeChanged();"/>
       </radiogroup>
-      <grid>
+      <grid id="result_hostnames">
         <columns>
           <column/>
           <column flex="1"/>
         </columns>
         <rows>
           <row align="center">
             <label class="textbox-label" value="&incoming.label;"
                    control="result-incoming"/>
@@ -235,16 +240,33 @@
           </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>
+      <vbox id="result_exchange" hidden="true">
+        <hbox id="result_exchange_hostname_container" align="center">
+          <label class="textbox-label" value="&hostname.label;"
+                 control="result_exchange_hostname"/>
+          <textbox id="result_exchange_hostname" disabled="true" flex="1"/>
+        </hbox>
+        <description id="result_addon_intro"/>
+        <grid id="result_addon_install">
+          <columns>
+            <column id="result_addon_install_column_icon" pack="start" align="center" />
+            <column id="result_addon_install_column_link" pack="start" align="center" />
+            <column id="result_addon_install_column_button" pack="start" align="center" />
+          </columns>
+          <rows id="result_addon_install_rows">
+          </rows>
+        </grid>
+      </vbox>
     </groupbox>
 
     <groupbox id="manual-edit_area" hidden="true">
       <grid>
         <columns>
           <column/><!-- row label, e.g. "incoming" -->
           <column/><!-- protocol, e.g. "IMAP" -->
           <column/><!-- hostname / username -->
new file mode 100644
--- /dev/null
+++ b/mail/components/accountcreation/content/exchangeAutoDiscover.js
@@ -0,0 +1,411 @@
+/* -*- 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/. */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource:///modules/JXON.js");
+ChromeUtils.defineModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+/* eslint-disable complexity, no-lonely-if */
+
+/**
+ * Tries to get a configuration from an MS Exchange server
+ * using Microsoft AutoDiscover protocol.
+ *
+ * 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 {string} domain - The domain part of the user's email address
+ * @param {string} emailAddress - The user's email address
+ * @param {string} password - The user's password for that email address
+ * @param {Function(config {AccountConfig})} successCallback - A callback that
+ *         will be called when we could retrieve a configuration.
+ *         The AccountConfig object will be passed in as first parameter.
+ * @param {Function(ex)} errorCallback - 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 fetchConfigFromExchange(domain, emailAddress, password,
+                                 successCallback, errorCallback) {
+  assert(typeof(successCallback) == "function");
+  assert(typeof(errorCallback) == "function");
+  if (!Services.prefs.getBoolPref(
+      "mailnews.auto_config.fetchFromExchange.enabled", true)) {
+    errorCallback("Exchange AutoDiscover disabled per user preference");
+    return new Abortable();
+  }
+
+  // <https://technet.microsoft.com/en-us/library/bb124251(v=exchg.160).aspx#Autodiscover%20services%20in%20Outlook>
+  // <https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638(v%3Dexchg.140)>
+  let url1 = "https://" + sanitize.hostname(domain) +
+             "/autodiscover/autodiscover.xml";
+  let url2 = "https://autodiscover." + sanitize.hostname(domain) +
+             "/autodiscover/autodiscover.xml";
+  let url3 = "http://autodiscover." + sanitize.hostname(domain) +
+             "/autodiscover/autodiscover.xml"; // needed by email hosters
+  let body =
+    `<?xml version="1.0" encoding="utf-8"?>
+    <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
+      <Request>
+        <EMailAddress>${emailAddress}</EMailAddress>
+        <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
+      </Request>
+    </Autodiscover>`;
+  let callArgs = {
+    uploadBody: body,
+    post: true,
+    headers: {
+      // outlook.com needs this exact string, with space and lower case "utf".
+      // Compare bug 1454325 comment 15.
+      "Content-Type": "text/xml; charset=utf-8",
+    },
+    username: emailAddress,
+    password,
+    // url3 is HTTP (not HTTPS), so suppress password. Even MS spec demands so.
+    requireSecureAuth: true,
+    allowAuthPrompt: false,
+  };
+  let call;
+  let fetch;
+  let fetch3;
+
+  let successive = new SuccessiveAbortable();
+  let priority = new PriorityOrderAbortable(
+    function(xml, call) { // success
+      readAutoDiscoverResponse(xml, successive, password, function(config) {
+        successive.current = getAddonsList(config, successCallback, errorCallback);
+      }, errorCallback);
+    },
+    errorCallback); // all failed
+
+  call = priority.addCall();
+  fetch = new FetchHTTP(url1, callArgs,
+    call.successCallback(), call.errorCallback());
+  fetch.start();
+  call.setAbortable(fetch);
+
+  call = priority.addCall();
+  fetch = new FetchHTTP(url2, callArgs,
+    call.successCallback(), call.errorCallback());
+  fetch.start();
+  call.setAbortable(fetch);
+
+  call = priority.addCall();
+  fetch3 = new FetchHTTP(url3, callArgs,
+    call.successCallback(), call.errorCallback());
+  fetch3.start();
+  call.setAbortable(fetch3);
+
+  // url3 is an HTTP URL that will redirect to the real one, usually a HTTPS
+  // URL of the hoster. XMLHttpRequest unfortunately loses the call
+  // parameters, drops the auth, drops the body, and turns POST into GET,
+  // which cause the call to fail, but FetchHTTP fixes this and automatically
+  // repeats the call. We need that, otherwise the whole AutoDiscover
+  // mechanism doesn't work.
+
+  successive.current = priority;
+  return successive;
+}
+
+var gLoopCounter = 0;
+
+/**
+ * @param {JXON} xml - The Exchange server AutoDiscover response
+ * @param {Function(config {AccountConfig})} successCallback - @see accountConfig.js
+ */
+function readAutoDiscoverResponse(autoDiscoverXML,
+  successive, password, successCallback, errorCallback) {
+  assert(successive instanceof SuccessiveAbortable);
+  assert(typeof(successCallback) == "function");
+  assert(typeof(errorCallback) == "function");
+
+  // redirect to other email address
+  if ("Action" in autoDiscoverXML.Autodiscover.Response &&
+      "Redirect" in autoDiscoverXML.Autodiscover.Response.Action) {
+    // <https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638(v%3Dexchg.140)>
+    let redirectEmailAddress = sanitize.emailAddress(
+        autoDiscoverXML.Autodiscover.Response.Action.Redirect);
+    let domain = redirectEmailAddress.split("@").pop();
+    if (++gLoopCounter > 2) {
+      throw new Exception("Too many redirects in XML response");
+    }
+    successive.current = fetchConfigFromExchange(domain,
+      redirectEmailAddress, password,
+      successCallback, errorCallback);
+  }
+
+  let config = readAutoDiscoverXML(autoDiscoverXML);
+
+  if (config.isComplete()) {
+    successCallback(config);
+  } else {
+    errorCallback(new Exception("No valid configs found in AutoDiscover XML"));
+  }
+}
+
+/**
+ * @param {JXON} xml - The Exchange server AutoDiscover response
+ * @returns {AccountConfig} - @see accountConfig.js
+ *
+ * @see <https://www.msxfaq.de/exchange/autodiscover/autodiscover_xml.htm>
+ */
+function readAutoDiscoverXML(autoDiscoverXML) {
+  if (typeof(autoDiscoverXML) != "object" ||
+      !("Autodiscover" in autoDiscoverXML) ||
+      !("Response" in autoDiscoverXML.Autodiscover) ||
+      !("Account" in autoDiscoverXML.Autodiscover.Response) ||
+      !("Protocol" in autoDiscoverXML.Autodiscover.Response.Account)) {
+    let stringBundle = getStringBundle(
+      "chrome://messenger/locale/accountCreationModel.properties");
+    throw new Exception(stringBundle.GetStringFromName("no_emailProvider.error"));
+  }
+  var xml = autoDiscoverXML.Autodiscover.Response.Account;
+
+  function array_or_undef(value) {
+    return value === undefined ? [] : value;
+  }
+
+  var config = new AccountConfig();
+  config.source = AccountConfig.kSourceExchange;
+  config.incoming.username = "%EMAILADDRESS%";
+  config.incoming.socketType = 2; // only https supported
+  config.incoming.port = 443;
+  config.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext;
+  config.incoming.authAlternatives = [ Ci.nsMsgAuthMethod.OAuth2 ];
+  config.oauthSettings = {};
+  config.outgoing.addThisServer = false;
+  config.outgoing.useGlobalPreferredServer = true;
+
+  for (let protocolX of array_or_undef(xml.$Protocol)) {
+    try {
+      let type = sanitize.enum(protocolX.Type,
+                               ["WEB", "EXHTTP", "EXCH", "EXPR", "POP3", "IMAP", "SMTP"],
+                               "unknown");
+      if (type == "WEB") {
+        let urlsX;
+        if ("External" in protocolX) {
+          urlsX = protocolX.External;
+        } else if ("Internal" in protocolX) {
+          urlsX = protocolX.Internal;
+        }
+        if (urlsX) {
+          config.incoming.owaURL = sanitize.url(urlsX.OWAUrl.value);
+          if (!config.incoming.ewsURL &&
+              "Protocol" in urlsX &&
+              "ASUrl" in urlsX.Protocol) {
+            config.incoming.ewsURL = sanitize.url(urlsX.Protocol.ASUrl);
+          }
+          config.incoming.type = "exchange";
+          let parsedURL = new URL(config.incoming.owaURL);
+          config.incoming.hostname = sanitize.hostname(parsedURL.hostname);
+          if (parsedURL.port) {
+            config.incoming.port =  sanitize.integer(parsedURL.port);
+          }
+        }
+      } else if (type == "EXHTTP" || type == "EXCH") {
+        config.incoming.ewsURL = sanitize.url(protocolX.EwsUrl);
+        if (!config.incoming.ewsURL) {
+          config.incoming.ewsURL = sanitize.url(protocolX.ASUrl);
+        }
+        config.incoming.type = "exchange";
+        let parsedURL = new URL(config.incoming.ewsURL);
+        config.incoming.hostname = sanitize.hostname(parsedURL.hostname);
+        if (parsedURL.port) {
+          config.incoming.port =  sanitize.integer(parsedURL.port);
+        }
+      } else if (type == "POP3" || type == "IMAP" || type == "SMTP") {
+        let server;
+        if (type == "SMTP") {
+          server = config.createNewOutgoing();
+        } else {
+          server = config.createNewIncoming();
+        }
+
+        server.type = sanitize.translate(type, { POP3: "pop3", IMAP: "imap", SMTP: "smtp" });
+        server.hostname = sanitize.hostname(protocolX.Server);
+        server.port = sanitize.integer(protocolX.Port);
+        server.socketType = 1; // plain
+        if ("SSL" in protocolX &&
+            sanitize.enum(protocolX.SSL, ["on", "off"]) == "on") {
+          // SSL is too unspecific. Do they mean STARTTLS or normal TLS?
+          // For now, assume normal TLS, unless it's a standard plain port.
+          switch (server.port) {
+            case 143: // IMAP standard
+            case 110: // POP3 standard
+            case 25:  // SMTP standard
+            case 587: // SMTP standard
+              server.socketType = 3; // STARTTLS
+              break;
+            case 993: // IMAP SSL
+            case 995: // POP3 SSL
+            case 465: // SMTP SSL
+            default: // if non-standard port, assume normal TLS, not STARTTLS
+              server.socketType = 2; // normal TLS
+              break;
+          }
+        }
+        if ("SPA" in protocolX &&
+            sanitize.enum(protocolX.SPA, ["on", "off"]) == "on") {
+          // Secure Password Authentication = NTLM or GSSAPI/Kerberos
+          server.auth = 8; // secure (not really, but this is MS...)
+        }
+        if ("LoginName" in protocolX) {
+          server.username = sanitize.nonemptystring(protocolX.LoginName);
+        } else {
+          server.username = "%EMAILADDRESS%";
+        }
+
+        if (type == "SMTP") {
+          if (!config.outgoing.hostname) {
+            config.outgoing = server;
+          } else {
+            config.outgoingAlternatives.push(server);
+          }
+        } else {
+          if (!config.incoming.hostname) {
+            config.incoming = server;
+          } else {
+            config.incomingAlternatives.push(server);
+          }
+        }
+      }
+
+      // else unknown or unsupported protocol
+
+    } catch (e) { logException(e); }
+  }
+
+  // OAuth2 settings, so that createInBackend() doesn't bail out
+  if (config.incoming.owaURL || config.incoming.ewsURL) {
+    config.oauthSettings = {
+      issuer: config.incoming.hostname,
+      scope: config.incoming.owaURL || config.incoming.ewsURL,
+    };
+  }
+
+  return config;
+}
+
+/**
+ * Ask server which addons can handle this config.
+ * @param {AccountConfig} config
+ * @param {Function(config {AccountConfig})} successCallback
+ * @returns {Abortable}
+ */
+function getAddonsList(config, successCallback, errorCallback) {
+  let url = Services.prefs.getCharPref("mailnews.auto_config.addons_url");
+  if (!url) {
+    errorCallback(new Exception("no URL for addons list configured"));
+    return new Abortable();
+  }
+  let fetch = new FetchHTTP(url, { allowCache: true }, function(json) {
+    let addons = readAddonsJSON(json);
+    addons = addons.filter(addon => {
+      // Find types matching the current config.
+      // Pick the first in the list as the preferred one and
+      // tell the UI to use that one.
+      addon.useType = addon.supportedTypes.find(type =>
+        config.incoming.owaURL && type.protocolType == "owa" ||
+        config.incoming.ewsURL && type.protocolType == "ews" ||
+        config.incoming.easURL && type.protocolType == "eas");
+      return !!addon.useType;
+    });
+    if (addons.length == 0) {
+      errorCallback(new Exception("Config found, but no addons known to handle the config"));
+      return;
+    }
+    config.addons = addons;
+    successCallback(config);
+  }, errorCallback);
+  fetch.start();
+  return fetch;
+}
+
+/**
+ * This reads the addons list JSON and makes security validations,
+ * e.g. that the URLs are not chrome: URLs, which could lead to exploits.
+ * It also chooses the right language etc..
+ *
+ * @param {JSON} json - the addons.json file contents
+ * @returns {Array of AddonInfo} - @see AccountConfig.addons
+ *
+ * accountTypes are listed in order of decreasing preference.
+ * Languages are 2-letter codes. If a language is not available,
+ * the first name or description will be used.
+ *
+ * Parse e.g.
+[
+  {
+    "id": "owl@beonex.com",
+    "name": {
+      "en": "Owl",
+      "de": "Eule"
+    },
+    "description": {
+      "en": "Owl is a paid third-party addon that allows you to access your email account on Exchange servers. See the website for prices.",
+      "de": "Eule ist eine Erweiterung von einem Drittanbieter, die Ihnen erlaubt, Exchange server zu benutzen. Sie ist kostenpflichtig. Die Preise finden sie auf der Website."
+    },
+    "minVersion": "0.2",
+    "xpiURL": "http://www.beonex.com/owl/latest.xpi",
+    "websiteURL": "http://www.beonex.com/owl/",
+    "icon32": "http://www.beonex.com/owl/owl-32.png",
+    "accountTypes": [
+      {
+        "generalType": "exchange",
+        "protocolType": "owa",
+        "addonAccountType": "owl-owa"
+      },
+      {
+        "generalType": "exchange",
+        "protocolType": "eas",
+        "addonAccountType": "owl-eas"
+      }
+    ]
+  }
+]
+ */
+function readAddonsJSON(json) {
+  let addons = [];
+  function ensureArray(value) {
+    return Array.isArray(value) ? value : [];
+  }
+  let xulLocale = Services.locale.getRequestedLocale();
+  let locale = xulLocale ? xulLocale.substring(0, 5) : "default";
+  for (let addonJSON of ensureArray(json)) {
+    try {
+      let addon = {
+        id: addonJSON.id,
+        minVersion: addonJSON.minVersion,
+        xpiURL: sanitize.url(addonJSON.xpiURL),
+        websiteURL: sanitize.url(addonJSON.websiteURL),
+        icon32: addonJSON.icon32 ? sanitize.url(addonJSON.icon32) : null,
+        supportedTypes: [],
+      };
+      assert(new URL(addon.xpiURL).protocol == "https:", "XPI download URL needs to be https");
+      addon.name = (locale in addonJSON.name) ?
+        addonJSON.name[locale] : addonJSON.name[0];
+      addon.description = (locale in addonJSON.description) ?
+        addonJSON.description[locale] : addonJSON.description[0];
+      for (let typeJSON of ensureArray(addonJSON.accountTypes)) {
+        try {
+          addon.supportedTypes.push({
+            generalType: sanitize.alphanumdash(typeJSON.generalType),
+            protocolType: sanitize.alphanumdash(typeJSON.protocolType),
+            addonAccountType: sanitize.alphanumdash(typeJSON.addonAccountType),
+          });
+        } catch (e) {
+          ddump(e);
+        }
+      }
+      addons.push(addon);
+    } catch (e) {
+      ddump(e);
+    }
+  }
+  return addons;
+}
--- a/mail/components/accountcreation/content/fetchConfig.js
+++ b/mail/components/accountcreation/content/fetchConfig.js
@@ -1,24 +1,22 @@
 /* -*- 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/. */
 
+ChromeUtils.import("resource:///modules/mailServices.js");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+ChromeUtils.import("resource:///modules/JXON.js");
+
 /**
  * Tries to find a configuration for this ISP on the local harddisk, in the
  * application install directory's "isp" subdirectory.
  * Params @see fetchConfigFromISP()
  */
-
-ChromeUtils.import("resource:///modules/mailServices.js");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource:///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");
@@ -58,95 +56,81 @@ function fetchConfigFromDisk(domain, suc
  *         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("ISP fetch disabled per user preference");
-    return;
+    errorCallback(new Exception("ISP fetch disabled per user preference"));
+    return new Abortable();
   }
 
   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 };
+  let callArgs = {
+    urlArgs: {
+      emailaddress: emailAddress,
+    },
+  };
   if (!Services.prefs.getBoolPref(
       "mailnews.auto_config.fetchFromISP.sendEmailAddress")) {
-    delete urlArgs.emailaddress;
+    delete callArgs.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 call;
+  let fetch;
+
+  let priority = new PriorityOrderAbortable(
+      xml => successCallback(readFromXML(xml)),
+      errorCallback);
 
-      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;
+  call = priority.addCall();
+  fetch = new FetchHTTP(url1, callArgs,
+      call.successCallback(), call.errorCallback());
+  call.setAbortable(fetch);
+  fetch.start();
+
+  call = priority.addCall();
+  fetch = new FetchHTTP(url2, 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 = 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);
+  let fetch = new FetchHTTP(url, {},
+    function(result) {
+      successCallback(readFromXML(result));
+    },
+    errorCallback);
   fetch.start();
   return fetch;
 }
 
 /**
  * Does a lookup of DNS MX, to get the server who is responsible for
  * receiving mail for this domain. Then it takes the domain of that
  * server, and does another lookup (in ISPDB and possible at ISP autoconfig
@@ -176,17 +160,17 @@ function fetchConfigForMX(domain, succes
   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");
+        errorCallback(new Exception("MX lookup would be no different from domain"));
         return;
       }
       sucAbortable.current = fetchConfigFromDB(sld, successCallback,
                                                errorCallback);
     },
     errorCallback);
   return sucAbortable;
 }
@@ -213,32 +197,34 @@ function fetchConfigForMX(domain, succes
  * @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");
+  if (!url) {
+    errorCallback(new Exception("no URL for MX service configured"));
+    return new Abortable();
+  }
   url += domain;
 
-  let fetch = new FetchHTTP(url, null, false,
+  let fetch = new FetchHTTP(url, {},
     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");
+        errorCallback(new Exception("no MX found"));
         return;
       }
       successCallback(first);
     },
     errorCallback);
   fetch.start();
   return fetch;
 }
--- a/mail/components/accountcreation/content/fetchhttp.js
+++ b/mail/components/accountcreation/content/fetchhttp.js
@@ -15,152 +15,256 @@
  * but not for bigger file downloads.
  */
 
 ChromeUtils.import("resource:///modules/JXON.js");
 
 /**
  * Set up a fetch.
  *
- * @param url {String}   URL of the server function.
+ * @param {string} url - 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 {Object} args - Additional parameters as properties, see below
  *
- * @param successCallback {Function(result {String})}
+ * @param {Function({string} result)} successCallback
  *   Called when the server call worked (no errors).
  *   |result| will contain the body of the HTTP response, as string.
- * @param errorCallback {Function(ex)}
+ * @param {Function(ex)} errorCallback
  *   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,
+ *
+ * The following optional parameters are supported as properties of the |args| object:
+ *
+ * @param {Object, associative array} urlArgs - 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 {Object, associative array} headers - HTTP headers to be added
+ *   to the HTTP request.
+ *   { foo: "blub blub" } will add HTTP header "Foo: Blub blub".
+ *   The values will be passed verbatim.
+ * @param {boolean} post - 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.
+ *   Default is GET.
+ * @param {Object, associative array} bodyFormArgs - 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
+ * @param {Object} uploadBody - 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
+ *   Only supported object types, currently only JXON is supported
  *   (sending XML).
  *   Usually, you have nothing to upload, so just pass |null|.
+ *   Only supported object types, currently supported:
+ *   JXON -> sending XML
+ *   JS object -> sending JSON
+ *   string -> sending text/plain
+ *   If you want to override the body mimetype, set header Content-Type below.
+ *   Usually, you have nothing to upload, so just leave it at |null|.
+ *   Default |null|.
+ * @param {boolean} allowCache (default true)
+ * @param {string} username (default null = no authentication)
+ * @param {string} password (default null = no authentication)
+ * @param {boolean} allowAuthPrompt (default true)
+ * @param {boolean} requireSecureAuth (default false)
+ *   Ignore the username and password unless we are using https:
+ *   This also applies to both https: to http: and http: to https: redirects.
  */
-function FetchHTTP(url, urlArgs, post, successCallback, errorCallback)
-{
+function FetchHTTP(url, args, successCallback, errorCallback) {
   assert(typeof(successCallback) == "function", "BUG: successCallback");
   assert(typeof(errorCallback) == "function", "BUG: errorCallback");
   this._url = sanitize.string(url);
-  if (!urlArgs)
-    urlArgs = {};
+  if (!args) {
+    args = {};
+  }
+  if (!args.urlArgs) {
+    args.urlArgs = {};
+  }
+  if (!args.headers) {
+    args.headers = {};
+  }
 
-  this._urlArgs = urlArgs;
-  this._post = sanitize.boolean(post);
+  this._args = args;
+  this._args.post = sanitize.boolean(args.post || false); // default false
+  this._args.allowCache = "allowCache" in args ? sanitize.boolean(args.allowCache) : true; // default true
+  this._args.allowAuthPrompt = sanitize.boolean(args.allowAuthPrompt || false); // default false
+  this._args.requireSecureAuth = sanitize.boolean(args.requireSecureAuth || false); // default false
   this._successCallback = successCallback;
   this._errorCallback = errorCallback;
+  this._logger = Log4Moz.getConfiguredLogger("mail.setup");
+  this._logger.info("Requesting <" + url + ">");
 }
 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,
+  _url: null, // URL as passed to ctor, without arguments
+  _args: null,
+  _successCallback: null,
+  _errorCallback: null,
+  _request: null, // the XMLHttpRequest object
+  result: null,
 
   start : function()
   {
-    var url = this._url;
-    for (var name in this._urlArgs)
-    {
+    let url = this._url;
+    for (let name in this._args.urlArgs) {
       url += (!url.includes("?") ? "?" : "&") +
-              name + "=" + encodeURIComponent(this._urlArgs[name]);
+             name + "=" + encodeURIComponent(this._args.urlArgs[name]);
     }
     this._request = new XMLHttpRequest();
     let request = this._request;
-    request.open(this._post ? "POST" : "GET", url);
+    request.mozBackgroundRequest = !this._args.allowAuthPrompt;
+    let username = null, password = null;
+    if (url.startsWith("https:") || !this._args.requireSecureAuth) {
+      username = this._args.username;
+      password = this._args.password;
+    }
+    request.open(this._args.post ? "POST" : "GET", url, true, username, password);
     request.channel.loadGroup = null;
+    request.timeout = 5000; // 5 seconds
     // 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"); }
 
+    if (!this._args.allowCache) {
+      // Disable Mozilla HTTP cache
+      request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+    }
+
+    // body
+    let mimetype = null;
+    let body = this._args.uploadBody;
+    if (typeof(body) == "object" && "nodeType" in body) {
+      // XML
+      mimetype = "text/xml; charset=UTF-8";
+      body = new XMLSerializer().serializeToString(body);
+    } else if (typeof(body) == "object") {
+      // JSON
+      mimetype = "text/json; charset=UTF-8";
+      body = JSON.stringify(body);
+    } else if (typeof(body) == "string") {
+      // Plaintext
+      // You can override the mimetype with { headers: {"Content-Type" : "text/foo" } }
+      mimetype = "text/plain; charset=UTF-8";
+      // body already set above
+    } else if (this._args.bodyFormArgs) {
+      mimetype = "application/x-www-form-urlencoded; charset=UTF-8";
+      body = "";
+      for (let name in this._args.bodyFormArgs) {
+        body += (body ? "&" : "") + name + "=" +
+            encodeURIComponent(this._args.bodyFormArgs[name]);
+      }
+    }
+    if (body) {
+      this._logger.info("with body:\n" + body);
+    }
+
+    // Headers
+    if (mimetype && !("Content-Type" in this._args.headers)) {
+      request.setRequestHeader("Content-Type", mimetype);
+    }
+    if (username && password) {
+      // workaround, because open(..., username, password) does not work.
+      request.setRequestHeader("Authorization", "Basic " + btoa(username + ":" + password));
+    }
+    for (let name in this._args.headers) {
+      request.setRequestHeader(name, this._args.headers[name]);
+      if (name == "Cookie") {
+        // Websites are not allowed to set this, but chrome is.
+        // Nevertheless, the cookie lib later overwrites our header.
+        // request.channel.setCookie(this._args.headers[name]); -- crashes
+        // So, deactivate that Firefox cookie lib.
+        request.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS;
+      }
+    }
+    this._logger.info(debugObject(this._args, "args"));
+
     var me = this;
-    request.onload = function() { me._response(true); }
-    request.onerror = function() { me._response(false); }
-    request.send(null);
+    request.onload = function() { me._response(true); };
+    request.onerror = function() { me._response(false); };
+    request.ontimeout = function() { me._response(false); };
+    request.send(body);
+    // Store the original stack so we can use it if there is an exception
+    this._callStack = Error().stack;
   },
   _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")
         {
+          // XML
           this.result = JXON.build(this._request.responseXML);
         }
+        else if (mimetype == "text/json" ||
+                 mimetype == "application/json")
+        {
+          // JSON
+          this.result = JSON.parse(this._request.responseText);
+        }
         else
         {
+          // Plaintext (fallback)
           //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 if (this._args.username &&
+             this._request.responseURL.replace(/\/\/.*@/, "//") != this._url &&
+             this._request.responseURL.startsWith(this._args.requireSecureAuth ? "https" : "http") &&
+             !this._isRetry)
+    {
+      // Redirects lack auth, see <https://stackoverflow.com/a/28411170>
+      this._logger.info("Call to <" + this._url + "> was redirected to <" + this._request.responseURL + ">, and failed. Re-trying the new URL with authentication again.");
+      this._url = this._request.responseURL;
+      this._isRetry = true;
+      this.start();
+      return;
+    }
     else
     {
       success = false;
       try
       {
         errorCode = this._request.status;
         errorStr = this._request.statusText;
       } catch (e) {
@@ -175,31 +279,36 @@ FetchHTTP.prototype =
 
     // Callbacks
     if (success)
     {
       try {
         this._successCallback(this.result);
       } catch (e) {
         logException(e);
+        e.stack = this._callStack;
         this._error(e);
       }
     }
     else if (exStored)
       this._error(exStored);
     else
-      this._error(new ServerException(errorStr, errorCode, this._url));
+    {
+      // Put the caller's stack into the exception
+      let e = new ServerException(errorStr, errorCode, this._url);
+      e.stack = this._callStack;
+      this._error(e);
+    }
 
     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);
     }
@@ -259,9 +368,8 @@ UserCancelledException.prototype.constru
 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;
-
--- a/mail/components/accountcreation/content/guessConfig.js
+++ b/mail/components/accountcreation/content/guessConfig.js
@@ -58,27 +58,31 @@ function guessConfig(domain, progressCal
 {
   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;
+    return new Abortable();
   }
 
   if (!resultConfig)
     resultConfig = new AccountConfig();
   resultConfig.source = AccountConfig.kSourceGuess;
 
+  if (!which) {
+    which = "both";
+  }
+
   if (!Services.prefs.getBoolPref(
       "mailnews.auto_config.guess.enabled")) {
     errorCallback("Guessing config disabled per user preference");
-    return;
+    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");
@@ -102,17 +106,17 @@ function guessConfig(domain, progressCal
       hostname: "mail." + domain,
       username: resultConfig.identity.emailAddress,
       type: "pop3",
       port: 110,
       socketType: 3,
       auth: Ci.nsMsgAuthMethod.passwordCleartext
     });
     successCallback(resultConfig);
-    return null;
+    return new Abortable();
   }
   var progress = function(thisTry)
   {
     progressCallback(protocolToString(thisTry.protocol), thisTry.hostname,
                      thisTry.port, sslConvertToSocketType(thisTry.ssl), false,
                      resultConfig);
   };
 
@@ -152,17 +156,17 @@ function guessConfig(domain, progressCal
         try {
           errorCallback(e);
         } catch (e) { errorInCallback(e); }
       }
       return;
     }
   };
 
-  var logger = Log4Moz.getConfiguredLogger("mail.wizard");
+  var logger = Log4Moz.getConfiguredLogger("mail.setup");
   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;
--- a/mail/components/accountcreation/content/readFromXML.js
+++ b/mail/components/accountcreation/content/readFromXML.js
@@ -1,29 +1,30 @@
 /* -*- 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/. */
 
+ChromeUtils.import("resource:///modules/hostnameUtils.jsm");
+/* eslint-disable complexity */
+
 /**
  * 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
  */
-ChromeUtils.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) ||
--- a/mail/components/accountcreation/content/util.js
+++ b/mail/components/accountcreation/content/util.js
@@ -3,53 +3,57 @@
  * 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
  */
 
 ChromeUtils.import("resource:///modules/errUtils.js");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
+/* eslint-disable spaced-comment */
+
+
+/////////////////////////////////////////
+// Low level, basic functions
 
 function assert(test, errorMsg)
 {
   if (!test)
     throw new NotReached(errorMsg ? errorMsg :
           "Programming bug. Assertion failed, see log.");
 }
 
 function makeCallback(obj, func)
 {
   return func.bind(obj);
 }
 
-
 /**
  * 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.
+ *
+ * @see |TimeoutAbortable|
  */
 function runAsync(func)
 {
-  setTimeout(func, 0);
+  return setTimeout(func, 0);
 }
 
-
 /**
  * @param uriStr {String}
  * @result {nsIURI}
  */
 function makeNSIURI(uriStr)
 {
   return Services.io.newURI(uriStr);
 }
 
-
 /**
  * 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)
 {
@@ -112,16 +116,19 @@ function getStringBundle(bundleURI)
     return Services.strings.createBundle(bundleURI);
   } catch (e) {
     throw new Exception("Failed to get stringbundle URI <" + bundleURI +
                         ">. Error: " + e);
   }
 }
 
 
+/////////////////////////////////////////
+// Exception
+
 function Exception(msg)
 {
   this._message = msg;
 
   // get stack
   try {
     not.found.here += 1; // force a native exception ...
   } catch (e) {
@@ -144,82 +151,440 @@ 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;
 
+
+/////////////////////////////////////////
+// Abortable
+
 /**
  * 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()
-  {
+  cancel(e) {
   }
 }
 
+function CancelledException(msg) {
+  Exception.call(this, msg);
+}
+CancelledException.prototype = Object.create(Exception.prototype);
+CancelledException.prototype.constructor = CancelledException;
+
 /**
  * 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
+  Abortable.call(this); // 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
+  Abortable.call(this); // 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.
+/**
+ * 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; },
+  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();
+  cancel(e) {
+    if (this._current) {
+      this._current.cancel(e);
+    }
   }
 }
 
+/**
+ * Allows you to make several network calls in parallel.
+ */
+function ParallelAbortable() {
+  Abortable.call(this); // call super constructor
+  // { Array of ParallelCall }
+  this._calls = [];
+  // { Array of Function }
+  this._finishedObservers = [];
+}
+ParallelAbortable.prototype = {
+  __proto__: Abortable.prototype,
+  /**
+   * @returns {Array of ParallelCall}
+   */
+  get results() {
+    return this._calls;
+  },
+  /**
+   * @returns {ParallelCall}
+   */
+  addCall() {
+    let call = new ParallelCall(this);
+    call.position = this._calls.length;
+    this._calls.push(call);
+    return call;
+  },
+  /**
+   * Observers will be called once one of the functions
+   * finishes, i.e. returns successfully or fails.
+   * @param {Function({ParallelCall} call)} func
+   */
+  addOneFinishedObserver(func) {
+    assert(typeof(func) == "function");
+    this._finishedObservers.push(func);
+  },
+  /**
+   * Will be called once *all* of the functions finished,
+   * It gives you a list of all functions that succeeded or failed,
+   * respectively.
+   * @param {Function(
+   *    {Array of ParallelCall} succeeded,
+   *    {Array of ParallelCall} failed
+   *   )} func
+   */
+  addAllFinishedObserver(func) {
+    assert(typeof(func) == "function");
+    this.addOneFinishedObserver(() => {
+      if (this._calls.some(call => !call.finished)) {
+        return;
+      }
+      let succeeded = this._calls.filter(call => call.succeeded);
+      let failed = this._calls.filter(call => !call.succeeded);
+      func(succeeded, failed);
+    });
+  },
+  _notifyFinished(call) {
+    for (let observer of this._finishedObservers) {
+      try {
+        observer(call);
+      } catch (e) {
+        console.error(e);
+      }
+    }
+  },
+  cancel(e) {
+    for (let call of this._calls) {
+      if (!call.finished && call.callerAbortable) {
+        call.callerAbortable.cancel(e);
+      }
+    }
+  },
+};
+
+/**
+ * Returned by ParallelAbortable.addCall().
+ * Do not create this object directly
+ * @param {ParallelAbortable} parallelAbortable - The controlling ParallelAbortable
+ */
+function ParallelCall(parallelAbortable) {
+  assert(parallelAbortable instanceof ParallelAbortable);
+  // {ParallelAbortable} the parent
+  this._parallelAbortable = parallelAbortable;
+  // {Abortable} Abortable of the caller function that should run in parallel
+  this.callerAbortable = null;
+  // {Integer} the order in which the function was added, and its priority
+  this.position = null;
+  // {boolean} false = running, pending, false = success or failure
+  this.finished = false;
+  // {boolean} if finished: true = returned with success, false = returned with error
+  this.succeeded = false;
+  // {Exception} if failed: the error or exception that the caller function returned
+  this.e = null;
+  // {Object} if succeeded: the result of the caller function
+  this.result = null;
+
+  this._time = Date.now();
+}
+ParallelCall.prototype = {
+  /**
+   * Returns a successCallback(result) function that you pass
+   * to your function that runs in parallel.
+   * @returns {Function(result)} successCallback
+   */
+  successCallback() {
+    return result => {
+      ddump("call " + this.position + " took " + (Date.now() - this._time) + "ms and succeeded" +
+          (this.callerAbortable && this.callerAbortable._url ? " at <" + this.callerAbortable._url + ">" : ""));
+      this.result = result;
+      this.finished = true;
+      this.succeeded = true;
+      this._parallelAbortable._notifyFinished(this);
+    };
+  },
+  /**
+   * Returns an errorCallback(e) function that you pass
+   * to your function that runs in parallel.
+   * @returns {Function(e)} errorCallback
+   */
+  errorCallback() {
+    return e => {
+      ddump("call " + this.position + " took " + (Date.now() - this._time) + "ms and failed with " + e +
+          (this.callerAbortable && this.callerAbortable._url ? " at <" + this.callerAbortable._url + ">" : ""));
+      this.e = e;
+      this.finished = true;
+      this.succeeded = false;
+      this._parallelAbortable._notifyFinished(this);
+    };
+  },
+  /**
+   * Call your function that needs to run in parallel
+   * and pass the resulting |Abortable| of your function here.
+   * @param {Abortable} abortable
+   */
+  setAbortable(abortable) {
+    assert(abortable instanceof Abortable);
+    this.callerAbortable = abortable;
+  },
+};
+
+/**
+ * Runs several calls in parallel.
+ * Returns the result of the "highest" priority call that succeeds.
+ * Unlike Promise.race(), does not return the fastest,
+ * but the first in the order they were added.
+ * So, the order in which the calls were added determines their priority,
+ * with the first to be added being the most desirable.
+ *
+ * E.g. the first failed, the second is pending, the third succeeded, and the forth is pending.
+ * It aborts the forth (because the third succeeded), and it waits for the second to return.
+ * If the second succeeds, it is the result, otherwise the third is the result.
+ *
+ * @param {Function(
+ *     {Object} result - Result of winner call
+ *     {ParallelCall} call - Winner call info
+ *   )} successCallback -  A call returned successfully
+ * @param {Function(e)} errorCallback - All functions failed. The exception is from the first one.
+ */
+function PriorityOrderAbortable(successCallback, errorCallback) {
+  assert(typeof(successCallback) == "function");
+  assert(typeof(errorCallback) == "function");
+  ParallelAbortable.call(this); // call super constructor
+
+  this.addOneFinishedObserver(finishedCall => {
+    let haveHigherPending = false;
+    let haveHigherSuccess = false;
+    for (let call of this._calls) {
+      if (!call.finished) {
+        if (haveHigherSuccess) {
+          // abort
+          if (call.callerAbortable) {
+            call.callerAbortable.cancel(NoLongerNeededException("Another higher call succeeded"));
+          }
+          continue;
+        }
+        // it's pending. ignore it for now and wait.
+        haveHigherPending = true;
+        continue;
+      }
+      if (!call.succeeded) {
+        // it failed. ignore it.
+        continue;
+      }
+      if (haveHigherSuccess) {
+        // another successful call was higher. ignore it.
+        continue;
+      }
+      haveHigherSuccess = true;
+      if (!haveHigherPending) {
+        // this is the winner
+        try {
+          successCallback(call.result, call);
+        } catch (e) {
+          console.error(e);
+          // if the handler failed with this data, treat this call as failed
+          call.e = e;
+          call.succeeded = false;
+          haveHigherSuccess = false;
+        }
+      }
+    }
+    if (!haveHigherPending && !haveHigherSuccess) {
+      // all failed
+      errorCallback(this._calls[0].e);
+    }
+  });
+}
+PriorityOrderAbortable.prototype = Object.create(ParallelAbortable.prototype);
+PriorityOrderAbortable.prototype.constructor = PriorityOrderAbortable;
+
+function NoLongerNeededException(msg) {
+  CancelledException.call(this, msg);
+}
+NoLongerNeededException.prototype = Object.create(CancelledException.prototype);
+NoLongerNeededException.prototype.constructor = NoLongerNeededException;
+
+
+/////////////////////////////////////////
+// High level features
+
+/**
+ * Allows you to install an addon.
+ *
+ * Example:
+ * var installer = new AddonInstaller({ xpiURL : "https://...xpi", id: "...", ...});
+ * installer.install();
+ *
+ * @param {Object} args - Contains parameters:
+ * @param {string} name (Optional) - Name of the addon (not important)
+ * @param {string} id (Optional) - Addon ID
+ * If you pass an ID, and the addon is already installed (and the version matches),
+ * then install() will do nothing.
+ * After the XPI is downloaded, the ID will be verified. If it doesn't match, the
+ * install will fail.
+ * If you don't pass an ID, these checks will be skipped and the addon be installed
+ * unconditionally.
+ * It is recommended to pass at least an ID, because it can confuse some addons
+ * to be reloaded at runtime.
+ * @param {string} minVersion (Optional) - Minimum version of the addon
+ * If you pass a minVersion (in addition to ID), and the installed addon is older than this,
+ * the install will be done anyway. If the downloaded addon has a lower version,
+ * the install will fail.
+ * If you do not pass a minVersion, there will be no version check.
+ * @param {URL} xpiURL - Where to download the XPI from
+ */
+function AddonInstaller(args) {
+  Abortable.call(this);
+  this._name = sanitize.label(args.name);
+  this._id = sanitize.string(args.id);
+  this._minVersion = sanitize.string(args.minVersion);
+  this._url = sanitize.url(args.xpiURL);
+}
+AddonInstaller.prototype = Object.create(Abortable.prototype);
+AddonInstaller.prototype.constructor = AddonInstaller;
+
+/**
+ * Checks whether the passed-in addon matches the
+ * id and minVersion requested by the caller.
+ * @param {nsIAddon} addon
+ * @returns {Boolean} is OK
+ */
+AddonInstaller.prototype.matches = function(addon) {
+  return !this._id || (this._id == addon.id &&
+    (!this._minVersion || Services.vc.compare(addon.version, this._minVersion) >= 0));
+};
+
+/**
+ * Start the installation
+ * @throws Exception in case of failure
+ */
+AddonInstaller.prototype.install = async function() {
+  if (await this.isInstalled()) {
+    return;
+  }
+  await this._installDirect();
+};
+
+/**
+ * Checks whether we already have an addon installed that matches the
+ * id and minVersion requested by the caller.
+ * @returns {boolean} is already installed and enabled
+ */
+AddonInstaller.prototype.isInstalled = async function() {
+  if (!this._id) {
+    return false;
+  }
+  var addon = await AddonManager.getAddonByID(this._id);
+  return addon && this.matches(addon) && addon.isActive;
+};
+
+/**
+ * Downloads and installs the addon.
+ * The downloaded XPI will be checked using prompt().
+ */
+AddonInstaller.prototype._installDirect = async function() {
+  var installer = this._installer = await AddonManager.getInstallForURL(
+    this._url, null, "application/x-xpinstall", null, this._name);
+  installer.promptHandler = makeCallback(this, this.prompt);
+  await new Promise((resolve, reject) => {
+    installer.addListener({
+      onDownloadFailed: reject,
+      onDownloadCancelled: reject,
+      onInstallFailed: reject,
+      onInstallCancelled: reject,
+      onInstallEnded: resolve,
+    });
+    installer.install();
+  });
+
+  var addon = await AddonManager.getAddonByID(this._id);
+  addon.userDisabled = false;
+
+  // Wait for addon startup code to finish
+  // Fixes: verify password fails with NOT_AVAILABLE in createIncomingServer()
+  if ("startupPromise" in addon) {
+    await addon.startupPromise;
+  }
+  let wait = ms => new Promise(resolve => setTimeout(resolve, ms));
+  await wait(1000);
+};
+
+/**
+ * Install confirmation. You may override this, if needed.
+ * @throws Exception If you want to cancel install, then throw an exception.
+ */
+AddonInstaller.prototype.prompt = async function(info) {
+  if (!this.matches(info.addon)) {
+    // happens only when we got the wrong XPI
+    throw new Exception("The downloaded addon XPI does not match the minimum requirements");
+  }
+};
+
+AddonInstaller.prototype.cancel = function() {
+  if (this._installer) {
+    try {
+      this._installer.cancel();
+    } catch (e) { // if install failed
+      ddump(e);
+    }
+  }
+};
+
+/////////////////////////////////////////
+// Debug output
+
 function deepCopy(org)
 {
   if (typeof(org) == "undefined")
     return undefined;
   if (org == null)
     return null;
   if (typeof(org) == "string")
     return org;
@@ -239,18 +604,22 @@ function deepCopy(org)
     var result = new Array();
   for (var prop in org)
     result[prop] = deepCopy(org[prop]);
   return result;
 }
 
 if (typeof gEmailWizardLogger == "undefined") {
   ChromeUtils.import("resource:///modules/gloda/log4moz.js");
-  var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+  var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.setup");
+  gEmailWizardLogger.level = Log4Moz.Level.Info;
+  gEmailWizardLogger.addAppender(new Log4Moz.ConsoleAppender(new Log4Moz.BasicFormatter())); // browser console
+  gEmailWizardLogger.addAppender(new Log4Moz.DumpAppender(new Log4Moz.BasicFormatter())); // stdout
 }
+
 function ddump(text)
 {
   gEmailWizardLogger.info(text);
 }
 
 function debugObject(obj, name, maxDepth, curDepth)
 {
   if (curDepth == undefined)
--- a/mail/components/accountcreation/content/verifyConfig.js
+++ b/mail/components/accountcreation/content/verifyConfig.js
@@ -1,13 +1,21 @@
 /* -*- 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/. */
 
+ChromeUtils.import("resource:///modules/mailServices.js");
+ChromeUtils.import("resource:///modules/OAuth2Providers.jsm");
+
+if (typeof gEmailWizardLogger == "undefined") {
+  ChromeUtils.import("resource:///modules/gloda/log4moz.js");
+  var gEmailWizardLogger = Log4Moz.getConfiguredLogger("mail.wizard");
+}
+
 /**
  * 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
  *
@@ -23,49 +31,38 @@
  *   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.
  */
-
-ChromeUtils.import("resource:///modules/mailServices.js");
-ChromeUtils.import("resource:///modules/OAuth2Providers.jsm");
-
-if (typeof gEmailWizardLogger == "undefined") {
-  ChromeUtils.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.type,
                                            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"]));
+                                               config.incoming.type);
   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;
--- a/mail/components/accountcreation/jar.mn
+++ b/mail/components/accountcreation/jar.mn
@@ -2,16 +2,17 @@
 # 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/.
 
 messenger.jar:
   content/messenger/accountcreation/accountConfig.js      (content/accountConfig.js)
   content/messenger/accountcreation/createInBackend.js    (content/createInBackend.js)
   content/messenger/accountcreation/emailWizard.js        (content/emailWizard.js)
   content/messenger/accountcreation/emailWizard.xul       (content/emailWizard.xul)
+  content/messenger/accountcreation/exchangeAutoDiscover.js        (content/exchangeAutoDiscover.js)
   content/messenger/accountcreation/fetchConfig.js        (content/fetchConfig.js)
   content/messenger/accountcreation/fetchhttp.js          (content/fetchhttp.js)
   content/messenger/accountcreation/guessConfig.js        (content/guessConfig.js)
   content/messenger/accountcreation/MyBadCertHandler.js   (content/MyBadCertHandler.js)
   content/messenger/accountcreation/readFromXML.js        (content/readFromXML.js)
   content/messenger/accountcreation/sanitizeDatatypes.js  (content/sanitizeDatatypes.js)
   content/messenger/accountcreation/util.js               (content/util.js)
   content/messenger/accountcreation/verifyConfig.js       (content/verifyConfig.js)
--- a/mail/themes/linux/mail/accountCreation.css
+++ b/mail/themes/linux/mail/accountCreation.css
@@ -1,12 +1,14 @@
 /* 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/. */
 
+@import url("chrome://messenger/skin/shared/accountCreation.css");
+
 /* ::::: BUTTONS ::::: */
 
 .important-button {
   font-weight: bold;
 }
 
 .errordescription {
   color: InfoText;
@@ -236,24 +238,8 @@ textbox.port[disabled="true"] {
 
 #outgoing_server_area {
   padding-top: 2px;
 }
 
 #incoming_protocol[disabled="true"] {
   padding-left: 5px;
 }
-
-#initialSettings, #status_area {
-  margin-bottom: 1em;
-}
-
-#result_area, #result_imappop {
-  margin-bottom: 1.5em;
-}
-
-#manual-edit_area {
-  margin-bottom: 2em;
-}
-
-#status_msg {
-  min-height: 2em;
-}
--- a/mail/themes/osx/mail/accountCreation.css
+++ b/mail/themes/osx/mail/accountCreation.css
@@ -1,12 +1,14 @@
 /* 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/. */
 
+@import url("chrome://messenger/skin/shared/accountCreation.css");
+
 /* Missing:
  * .important-button
  */
 
 .errordescription {
   padding-inline-start: 3px;
   margin-top: 3px;
 }
@@ -219,24 +221,8 @@ menulist {
 
 textbox.port[disabled="true"] {
   padding-top: 4px;
 }
 
 #incoming_protocol[disabled="true"] {
   padding-left: 5px;
 }
-
-#initialSettings, #status_area {
-  margin-bottom: 1em;
-}
-
-#result_area, #result_imappop {
-  margin-bottom: 1.5em;
-}
-
-#manual-edit_area {
-  margin-bottom: 2em;
-}
-
-#status_msg {
-  min-height: 2em;
-}
--- a/mail/themes/shared/jar.inc.mn
+++ b/mail/themes/shared/jar.inc.mn
@@ -70,16 +70,17 @@
   skin/classic/messenger/icons/spelling.svg                   (../shared/mail/icons/spelling.svg)
   skin/classic/messenger/icons/star.svg                       (../shared/mail/icons/star.svg)
   skin/classic/messenger/icons/starred.svg                    (../shared/mail/icons/starred.svg)
   skin/classic/messenger/icons/sticky.svg                     (../shared/mail/icons/sticky.svg)
   skin/classic/messenger/icons/stop.svg                       (../shared/mail/icons/stop.svg)
   skin/classic/messenger/icons/tag.svg                        (../shared/mail/icons/tag.svg)
   skin/classic/messenger/icons/timeline.svg                   (../shared/mail/icons/timeline.svg)
   skin/classic/messenger/icons/toolbarbutton-arrow.svg        (../shared/mail/icons/toolbarbutton-arrow.svg)
+  skin/classic/messenger/shared/accountCreation.css           (../shared/mail/accountCreation.css)
   skin/classic/messenger/shared/accountProvisioner.css        (../shared/mail/accountProvisioner.css)
   skin/classic/messenger/shared/addressbook.css               (../shared/mail/addressbook.css)
   skin/classic/messenger/shared/compacttheme.css              (../shared/mail/compacttheme.css)
   skin/classic/messenger/shared/in-content/dialog.css         (../shared/mail/incontentprefs/dialog.css)
   skin/classic/messenger/shared/in-content/aboutPreferences.css (../shared/mail/incontentprefs/aboutPreferences.css)
   skin/classic/messenger/shared/in-content/account.svg        (../shared/mail/incontentprefs/account.svg)
   skin/classic/messenger/shared/in-content/advanced.svg       (../shared/mail/incontentprefs/advanced.svg)
   skin/classic/messenger/shared/in-content/attachment.svg     (../shared/mail/incontentprefs/attachment.svg)
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/accountCreation.css
@@ -0,0 +1,79 @@
+#initialSettings {
+  margin-bottom: 1em;
+}
+
+#manual-edit_area {
+  margin-bottom: 2em;
+}
+
+/* status area */
+
+#status_msg {
+  min-height: 1.5em;
+}
+
+#status-lines {
+  -moz-box-pack: start;
+  -moz-box-flex: 10;
+  margin-left: 10em;
+}
+
+.status-line[status="loading"] .status-img {
+  background: url("chrome://global/skin/icons/loading.png") no-repeat;
+  width: 16px;
+  height: 16px;
+}
+
+.status-line[status="failed"] .status-img {
+  background: url("chrome://messenger/skin/icons/exclude.png") no-repeat;
+  width: 16px;
+  height: 16px;
+}
+
+.status-line[status="succeeded"] .status-img {
+  background: url("chrome://messenger/skin/icons/tick.png") no-repeat;
+  width: 16px;
+  height: 16px;
+}
+
+/* result area */
+
+#result_area {
+  margin-bottom: 1em;
+}
+
+#result_servertype {
+  margin-bottom: 1.5em;
+}
+
+#result_exchange_hostname_container {
+  margin-bottom: 0.5em;
+}
+
+#result_area description {
+  margin-top: 0px;
+  margin-bottom: 0px;
+}
+
+#result_addon_intro {
+  max-width: 40em;
+}
+
+#result_addon_install {
+  margin-top: 4px;
+}
+
+#result_addon_install image.icon {
+  width: 32px;
+  height: 32px;
+  margin-left: 0.4em;
+  margin-right: 0.4em;
+}
+
+#result_addon_install_column_link {
+  max-width: 30em;
+}
+
+#result_addon_install_column_button {
+  max-width: 10em;
+}
--- a/mail/themes/windows/mail/accountCreation.css
+++ b/mail/themes/windows/mail/accountCreation.css
@@ -1,13 +1,14 @@
 /* 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/. */
 
 @namespace html url("http://www.w3.org/1999/xhtml");
+@import url("chrome://messenger/skin/shared/accountCreation.css");
 
 /* ::::: BUTTONS ::::: */
 
 .important-button {
   font-weight: bold;
 }
 
 /* Set the order of the buttons to: (stop|re-test), (continue|create account), cancel */
@@ -253,24 +254,8 @@ textbox.port[disabled="true"] {
 
 #outgoing_server_area {
   padding-top: 2px;
 }
 
 #incoming_protocol[disabled="true"] {
   padding-left: 6px;
 }
-
-#initialSettings, #status_area {
-  margin-bottom: 1em;
-}
-
-#result_area, #result_imappop {
-  margin-bottom: 1.5em;
-}
-
-#manual-edit_area {
-  margin-bottom: 2em;
-}
-
-#status_msg {
-  min-height: 2em;
-}
--- a/mailnews/mailnews.js
+++ b/mailnews/mailnews.js
@@ -891,23 +891,35 @@ pref("dom.max_chrome_script_run_time", 0
 // For the Empty Junk/Trash confirmation dialogs.
 pref("mailnews.emptyJunk.dontAskAgain", false);
 pref("mailnews.emptyTrash.dontAskAgain", false);
 
 // where to fetch auto config information from.
 pref("mailnews.auto_config_url", "https://live.thunderbird.net/autoconfig/v1.1/");
 // Added in bug 551519. Remove when bug 545866 is fixed.
 pref("mailnews.mx_service_url", "https://live.thunderbird.net/dns/mx/");
+// The list of addons which can handle certain account types
+#ifdef RELEASE_OR_BETA
+pref("mailnews.auto_config.addons_url", "https://live.thunderbird.net/autoconfig/addons.json");
+#else
+pref("mailnews.auto_config.addons_url", "http://www.beonex.com/owl/addons-test.json");
+#endif
 // Allow to contact ISP (email address domain)
 // This happens via insecure means (HTTP), so the config cannot be trusted,
 // and also contains the email address
 pref("mailnews.auto_config.fetchFromISP.enabled", true);
 // Allow the fetch from ISP via HTTP, but not the email address
 pref("mailnews.auto_config.fetchFromISP.sendEmailAddress", true);
+// Allow the Microsoft Exchange AutoDiscover protocol.
+// This also sends the email address and password to the server,
+// which the protocol unfortunately requires in practice.
+pref("mailnews.auto_config.fetchFromExchange.enabled", true);
 pref("mailnews.auto_config.guess.enabled", true);
+// Work around bug 1454325 by disabling mimetype mungling in XmlHttpRequest
+pref("dom.xhr.standard_content_type_normalization", false);
 
 // -- Summary Database options
 // dontPreserveOnCopy: a space separated list of properties that are not
 //                     copied to the new nsIMsgHdr when a message is copied.
 //                     Allows extensions to control preservation of properties.
 pref("mailnews.database.summary.dontPreserveOnCopy",
   "account msgOffset threadParent msgThreadId statusOfset flags size numLines ProtoThreadFlags label gloda-id gloda-dirty storeToken");