Bug 1500105 - Support Exchange AutoDiscover and parallelize network calls. r=aceman,mkmelin,Neil
authorBen Bucksch <ben.bucksch@beonex.com>
Fri, 14 Dec 2018 12:05:14 +0100
changeset 33996 1403c0fa3d1ac8b10ec74adb92b245575059298f
parent 33995 f5c4404b29d27444dfd885fa781fe44351c0c18a
child 33997 480eb7e81d39fddb4af9779d5d1ea3bc7a51a54c
push id389
push userclokep@gmail.com
push dateMon, 18 Mar 2019 19:01:53 +0000
reviewersaceman, mkmelin, Neil
bugs1500105
Bug 1500105 - Support Exchange AutoDiscover and parallelize network calls. r=aceman,mkmelin,Neil Summary: * Parallelize network calls * Exchange AutoDiscover protocol implementation * Try to find IMAP servers in the server response * Offer to install an extension which supports the Exchange protocol to get mails Runs all the ISP config lookup network calls in parallel. Class PriorityOrderAbortable (subclass of ParallelAbortable) implements a policy that waits until one of the calls returns successfully, then takes that result and cancels all pending less desirable calls. Implements the Exchange AutoDiscover protocol to detect Exchange servers. If the server gives an IMAP configuration, we offer that to the user. Alternatively, we offer a compatible verified extension that implements the specific Exchange protocol that the Exchange server returned. Exchange has at least 7 protocols, and we show extensions that support the protocols that the server listed and that are known to work well and actively maintained. The setup process then continues without interruption. Test plan: Exchange autoconfig: 1. To test Exchange AutoDiscover with an hotmail/outlook.com account (which has an IMAP config in our ISPDB), set these prefs: mailnews.auto_config.guess.enabled = false mailnews.auto_config_url = "" mailnews.mx_service_url = "" 2. Enter you@outlook.com and a valid password (it will not work without valid password, due to the Exchange AutoDiscover protocol design) 3. [Continue] 4. -> TB should find an Exchange server with hostname 5. -> TB will offer you to install an extension that supports this protocol type, with explanatory text and a link 6. Click [Install] 7. -> The password is checked, the dialog closes, and the account appears, and your emails are downloaded. Parallel network calls: 1. Open account creation dialog 2. Enter "foo@gmail.com", "foo@yahoo.com", "foo@sys4.de", "foo@example.com", or any other domain 3. -> It works functionally as before, see https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration 4. -> It's faster than before Differential Revision: https://phabricator.services.mozilla.com/D9215
mail/components/accountcreation/content/.eslintrc.js
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/locales/en-US/chrome/messenger/accountCreation.dtd
mail/locales/en-US/chrome/messenger/accountCreation.properties
mail/locales/en-US/chrome/messenger/accountCreationModel.properties
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/.eslintrc.js
+++ b/mail/components/accountcreation/content/.eslintrc.js
@@ -1,22 +1,24 @@
 "use strict";
 
 module.exports = {
   "globals": {
     "Abortable": true,
     "AccountConfig": true,
+    "AddonInstaller": true,
     "BadCertHandler": true,
     "CancelledException": true,
     "Exception": true,
     "FetchHTTP": true,
     "Log4Moz": true,
     "MailServices": true,
     "NewMailAccountProvisioner": true,
     "NotReached": true,
+    "PriorityOrderAbortable": true,
     "Services": true,
     "SuccessiveAbortable": true,
     "TimeoutAbortable": true,
     "UserCancelledException": true,
     "alertPrompt": true,
     "assert": true,
     "checkIncomingServerAlreadyExists": true,
     "checkOutgoingServerAlreadyExists": true,
@@ -24,16 +26,17 @@ module.exports = {
     "createAccountInBackend": true,
     "ddump": true,
     "debugObject": true,
     "deepCopy": true,
     "errorWithDebug": true,
     "fetchConfigForMX": true,
     "fetchConfigFromDB": true,
     "fetchConfigFromDisk": true,
+    "fetchConfigFromExchange": true,
     "fetchConfigFromISP": true,
     "getStringBundle": true,
     "guessConfig": true,
     "isLegalHostName": true,
     "isLegalHostNameOrIP": true,
     "isLegalIPAddress": true,
     "isLegalIPv4Address": true,
     "isLegalIPv6Address": true,
--- a/mail/components/accountcreation/content/accountConfig.js
+++ b/mail/components/accountcreation/content/accountConfig.js
@@ -63,17 +63,17 @@ AccountConfig.prototype =
   // { Array of Strings }
   domains: null,
 
   /**
    * Factory function for incoming and incomingAlternatives
    */
   createNewIncoming() {
     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 }
@@ -107,16 +107,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() {
     return {
       type: "smtp",
@@ -135,45 +140,95 @@ 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() {
     // 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() {
     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,31 @@
 /* -*- 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.jsm");
+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.jsm");
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-/* eslint-disable complexity */
 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);
@@ -249,25 +248,25 @@ function rememberPassword(server, passwo
  *     object is returned.
  *     If it's a new server, |null| is returned.
  */
 function checkIncomingServerAlreadyExists(config) {
   assert(config instanceof AccountConfig);
   let incoming = config.incoming;
   let existing = MailServices.accounts.findRealServer(incoming.username,
         incoming.hostname,
-        sanitize.enum(incoming.type, ["pop3", "imap", "nntp"]),
+        incoming.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
@@ -31,17 +31,20 @@ ChromeUtils.import("resource:///modules/
  * - If user clicks OK, create the account
  */
 
 // from http://xyfer.blogspot.com/2005/01/javascript-regexp-email-validator.html
 var emailRE = /^[-_a-z0-9\'+*$^&%=~!?{}]+(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*@(?:[-a-z0-9.]+\.[a-z]{2,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:
@@ -108,16 +111,22 @@ function setText(id, value) {
     throw new NotReached("XUL element type not supported");
   }
 }
 
 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() {
     gEmailWizardLogger.info("Initializing setup wizard");
     this._abortable = null;
@@ -285,41 +294,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");
@@ -339,45 +351,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() {
@@ -522,81 +537,90 @@ EmailConfigWizard.prototype =
 
   // --------------
   // Detection step
 
   /**
    * Try to find an account configuration for this email address.
    * This is the function which runs the autoconfig.
    */
-  findConfig(domain, email) {
+  findConfig(domain, emailAddress) {
     gEmailWizardLogger.info("findConfig()");
     if (this._abortable) {
       this.onStop();
     }
     this.switchToMode("find-config");
-    this.startSpinner("looking_up_settings_disk");
+    this.startSpinner("looking_up_settings");
+
     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_mx", call);
+      call.foundMsg = "found_settings_db";
+      fetch = fetchConfigForMX(domain,
+        call.successCallback(), call.errorCallback());
+      call.setAbortable(fetch);
+
+      call = priority.addCall();
+      this.addStatusLine("looking_up_settings_exchange", call);
+      call.foundMsg = "found_settings_exchange";
+      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(domain, initialConfig) {
     this.startSpinner("looking_up_settings_guess");
     var self = this;
@@ -625,29 +649,26 @@ EmailConfigWizard.prototype =
   },
 
   /**
    * When findConfig() was successful, it calls this.
    * This displays the config to the user.
    */
   foundConfig(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(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.
@@ -698,45 +719,199 @@ EmailConfigWizard.prototype =
   },
 
   showErrorStatus(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(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.
    *
    * @param config {AccountConfig} The config to present to user
    */
   displayConfigResult(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 {
+          _hide("status_area");
+          _show("result_addon_intro");
+          var msg = gStringsBundle.getString("addon-intro");
+          if (!config.incomingAlternatives.find(alt => (alt.type == "imap" || alt.type == "pop3"))) {
+            msg = gStringsBundle.getString("no-open-protocols") + " " + msg;
+          }
+          setText("result_addon_intro", msg);
+
+          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", gStringsBundle.getString("addonInstallShortLabel"));
+            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 +
           (isStandardPort(server.port) ? "" : ":" + server.port);
@@ -776,64 +951,70 @@ 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() {
+  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");
+    this.startSpinner("addonInstallStarted");
+
+    try {
+      var installer = this._abortable = new AddonInstaller(addon);
+      await installer.install();
+
+      this._abortable = null;
+      this.stopSpinner("addonInstallSuccess");
+      _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.
    *
    * Realname and password are not part of that area and still
    * placeholders, but hostname and username are concrete and
@@ -1145,20 +1326,18 @@ EmailConfigWizard.prototype =
   /**
    * Sets the prefilled values of the port fields.
    * Filled statically with the standard ports for the given protocol,
    * plus "Auto".
    */
   fillPortDropdown(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
     }
   },
@@ -1521,35 +1700,39 @@ 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() {
+  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 &&
@@ -1558,18 +1741,21 @@ function serverMatches(a, b) {
          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
+_gStandardPorts.exchange = [ 443 ];
 var _gAllStandardPorts = _gStandardPorts.smtp
-    .concat(_gStandardPorts.imap).concat(_gStandardPorts.pop3);
+    .concat(_gStandardPorts.imap)
+    .concat(_gStandardPorts.pop3)
+    .concat(_gStandardPorts.exchange);
 
 function isStandardPort(port) {
   return _gAllStandardPorts.includes(port);
 }
 
 function getStandardPorts(protocolType) {
   return _gStandardPorts[protocolType];
 }
@@ -1623,17 +1809,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.label;" 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="&exchange-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_autodiscover.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.requestedLocale;
+  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,33 +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.jsm");
+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.jsm");
-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");
       configLocation.append(sanitize.hostname(domain) + ".xml");
 
       if (!configLocation.exists() || !configLocation.isReadable()) {
-        errorCallback("local file not found");
+        errorCallback(new Exception("local file not found"));
         return;
       }
       var contents =
         readURLasUTF8(Services.io.newFileURI(configLocation));
       let domParser = new DOMParser();
       successCallback(readFromXML(JXON.build(
         domParser.parseFromString(contents, "text/xml"))));
     } catch (e) { errorCallback(e); }
@@ -53,88 +52,80 @@ function fetchConfigFromDisk(domain, suc
  *         for this domain at this location),
  *         so do not unconditionally show this to the user.
  *         The first parameter will be an exception object or error string.
  */
 function fetchConfigFromISP(domain, emailAddress, successCallback,
                             errorCallback) {
   if (!Services.prefs.getBoolPref(
       "mailnews.auto_config.fetchFromISP.enabled")) {
-    errorCallback("ISP fetch disabled per user preference");
-    return null;
+    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
@@ -161,17 +152,17 @@ function fetchConfigForMX(domain, succes
   var sucAbortable = new SuccessiveAbortable();
   var time = Date.now();
   sucAbortable.current = getMX(domain,
     function(mxHostname) { // success
       ddump("getmx took " + (Date.now() - time) + "ms");
       let sld = Services.eTLD.getBaseDomainFromHost(mxHostname);
       ddump("base domain " + sld + " for " + mxHostname);
       if (sld == domain) {
-        errorCallback("MX lookup would be no different from domain");
+        errorCallback(new Exception("MX lookup would be no different from domain"));
         return;
       }
       sucAbortable.current = fetchConfigFromDB(sld, successCallback,
                                                errorCallback);
     },
     errorCallback);
   return sucAbortable;
 }
@@ -197,30 +188,32 @@ function fetchConfigForMX(domain, succes
  *   For |hostname|, see description above.
  * @param errorCallback @see fetchConfigFromISP()
  * @returns @see fetchConfigFromISP()
  */
 function getMX(domain, successCallback, errorCallback) {
   domain = sanitize.hostname(domain);
 
   let url = Services.prefs.getCharPref("mailnews.mx_service_url");
-  if (!url)
-    errorCallback("no URL for MX service configured");
+  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,139 +15,241 @@
  * 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,
+  _args: null,
   _successCallback: null,
   _errorCallback: null,
   _request: null, // the XMLHttpRequest object
   result: null,
 
   start() {
-    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.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(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) {
         // If we can't resolve the hostname in DNS etc., .statusText throws
         errorCode = -2;
@@ -159,30 +261,33 @@ 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);
     }
@@ -236,9 +341,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
@@ -57,27 +57,31 @@ function guessConfig(domain, progressCal
                      resultConfig, which) {
   assert(typeof(progressCallback) == "function", "need progressCallback");
   assert(typeof(successCallback) == "function", "need successCallback");
   assert(typeof(errorCallback) == "function", "need errorCallback");
 
   // Servers that we know enough that they support OAuth2 do not need guessing.
   if (resultConfig.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) {
     successCallback(resultConfig);
-    return null;
+    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 null;
+    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");
@@ -100,17 +104,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);
   };
 
   var updateConfig = function(config) {
@@ -143,17 +147,17 @@ function guessConfig(domain, progressCal
         try {
           errorCallback(e);
         } catch (e) { errorInCallback(e); }
       }
 
     }
   };
 
-  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;
     // TODO
--- a/mail/components/accountcreation/content/readFromXML.js
+++ b/mail/components/accountcreation/content/readFromXML.js
@@ -1,30 +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");
-
-/* eslint-disable complexity */
 function readFromXML(clientConfigXML) {
   function array_or_undef(value) {
     return value === undefined ? [] : value;
   }
   var exception;
   if (typeof(clientConfigXML) != "object" ||
       !("clientConfig" in clientConfigXML) ||
       !("emailProvider" in clientConfigXML.clientConfig)) {
--- a/mail/components/accountcreation/content/util.js
+++ b/mail/components/accountcreation/content/util.js
@@ -3,49 +3,53 @@
  * 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) {
   assert(uri instanceof Ci.nsIURI, "uri must be an nsIURI");
@@ -105,16 +109,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;
   this.stack = Components.stack.formattedStack;
 }
 Exception.prototype =
 {
   get message() {
     return this._message;
@@ -127,75 +134,426 @@ Exception.prototype =
 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() {
+  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() {
-    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, "application/x-xpinstall", null, this._name);
+  installer.promptHandler = makeCallback(this, this.prompt);
+  await installer.install(); // throws, if failed
+
+  var addon = await AddonManager.getAddonByID(this._id);
+  await addon.enable();
+
+  // 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;
   if (typeof(org) == "number")
@@ -214,18 +572,22 @@ function deepCopy(org) {
     result = [];
   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)
     curDepth = 0;
   if (maxDepth != undefined && curDepth > maxDepth)
--- 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.jsm");
+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,48 +31,37 @@
  *   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.jsm");
-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/locales/en-US/chrome/messenger/accountCreation.dtd
+++ b/mail/locales/en-US/chrome/messenger/accountCreation.dtd
@@ -25,25 +25,30 @@
 <!ENTITY incoming.label                  "Incoming:">
 <!ENTITY outgoing.label                  "Outgoing:">
 <!ENTITY username.label                  "Username:">
 <!ENTITY hostname.label                  "Server hostname">
 <!ENTITY port.label                      "Port">
 <!ENTITY ssl.label                       "SSL">
 <!ENTITY auth.label                      "Authentication">
 <!ENTITY imap.label                      "IMAP">
-<!ENTITY pop3.label                       "POP3">
+<!ENTITY pop3.label                      "POP3">
+<!-- LOCALIZATION NOTE(exchange.label): Do not translate Exchange, it is a product name. -->
+<!ENTITY exchange.label                  "Exchange">
 <!ENTITY smtp.label                      "SMTP">
 <!ENTITY autodetect.label                "Autodetect">
 <!-- LOCALIZATION NOTE(noEncryption.label): Neither SSL/TLS nor STARTTLS.
      Transmission of emails in cleartext over the Internet. -->
 <!ENTITY noEncryption.label              "None">
 <!ENTITY starttls.label                  "STARTTLS">
 <!ENTITY sslTls.label                    "SSL/TLS">
 
+<!-- LOCALIZATION NOTE(exchange-hostname.label): Do not translate Exchange, it is a product name. -->
+<!ENTITY exchange-hostname.label         "Exchange server:">
+
 <!ENTITY advancedSetup.label             "Advanced config">
 <!ENTITY advancedSetup.accesskey         "A">
 <!ENTITY cancel.label                    "Cancel">
 <!ENTITY cancel.accesskey                "a">
 <!ENTITY continue.label                  "Continue">
 <!ENTITY continue.accesskey              "C">
 <!ENTITY stop.label                      "Stop">
 <!ENTITY stop.accesskey                  "S">
--- a/mail/locales/en-US/chrome/messenger/accountCreation.properties
+++ b/mail/locales/en-US/chrome/messenger/accountCreation.properties
@@ -13,29 +13,37 @@ cleartext_details=Insecure mail servers 
 # LOCALIZATION NOTE(default_server_tag): Used to indicate the default smtp server in the server dropdown list.
 default_server_tag= (default)
 # LOCALIZATION NOTE(port_auto): It must be short (4-5 characters max.).
 # Content of server port field (usually a number), used when the user didn't
 # enter anything yet and we'll automatically detect it later.
 port_auto=Auto
 
 # config titles
+looking_up_settings=Looking up configuration…
 # LOCALIZATION NOTE(looking_up_settings_disk): Referring to Thunderbird installation folder on user's harddisk. %1$S will be the brandShortName.
 looking_up_settings_disk=Looking up configuration: %1$S installation
 looking_up_settings_isp=Looking up configuration: Email provider
 # LOCALIZATION NOTE(looking_up_settings_db): Do not translate or replace Mozilla. It stands for the public project mozilla.org, not Mozilla Corporation. The database is a generic, public domain facility usable by any client.
 looking_up_settings_db=Looking up configuration: Mozilla ISP database
+looking_up_settings_mx=Looking up configuration: Incoming mail domain
+# LOCALIZATION NOTE(looking_up_settings_exchange): Exchange is a product name
+looking_up_settings_exchange=Looking up configuration: Exchange server
 # LOCALIZATION NOTE(looking_up_settings_guess): We are checking common server names like pop., pop3., smtp., mail., without knowing whether they exist or really serve this email account. If a server responds, we try to talk to it via POP/IMAP/SMTP protocols and query its capabilities. If that succeeds, we assume we found a configuration. Of course, it may still be wrong, but it often works.
 looking_up_settings_guess=Looking up configuration: Trying common server names
 looking_up_settings_halfmanual=Looking up configuration: Probing server
 # LOCALIZATION NOTE(found_settings_disk): Referring to Thunderbird installation folder on user's harddisk. %1$S will be the brandShortName.
 found_settings_disk=Configuration found on %1$S installation
 found_settings_isp=Configuration found at email provider
 # LOCALIZATION NOTE(found_settings_db): Do not translate or replace Mozilla. It stands for the public project mozilla.org, not Mozilla Corporation. The database is a generic, public domain facility usable by any client.
 found_settings_db=Configuration found in Mozilla ISP database
+# LOCALIZATION NOTE(found_settings_exchange): Microsoft Exchange is a product name.
+found_settings_exchange=Configuration found for a Microsoft Exchange server
+no-open-protocols=This email server unfortunately does not support open protocols.
+addon-intro=A third-party add-on can allow you to access your email account on this server:
 # LOCALIZATION NOTE(found_settings_guess): We tried common mail server names and we found a mail server and talked to it and it responded properly, so we think we found a suitable configuration, but we are only about 80% certain that it is the correct setting for this email address. There's a chance that email address may not actually be served by this server and it won't work, or that there is a better server.
 found_settings_guess=Configuration found by trying common server names
 found_settings_halfmanual=The following settings were found by probing the given server
 # LOCALIZATION NOTE(failed_to_find_settings): %1$S will be the brandShortName.
 failed_to_find_settings=%1$S failed to find the settings for your email account.
 manually_edit_config=Editing Config
 # LOCALIZATION NOTE(guessed_settings_offline) User is offline, so we just took a wild guess and the user will have to enter the right settings.
 guessed_settings_offline=You are offline. We guessed some settings but you will need to enter the right settings.
@@ -52,28 +60,34 @@ guessing_from_email=guessing configuration…
 config_details_found=Your configuration details have been found!
 config_unverifiable=Configuration could not be verified — is the username or password wrong?
 incoming_found_specify_outgoing=Your incoming server configuration details have been found, please specify the sending hostname.
 outgoing_found_specify_incoming=Your outgoing server configuration details have been found, please specify the receiving hostname.
 please_enter_missing_hostnames=Could not guess settings — please enter missing hostnames.
 incoming_failed_trying_outgoing=Could not automatically configure incoming server, still trying for outgoing server.
 outgoing_failed_trying_incoming=Could not automatically configure outgoing server, still trying for incoming server.
 checking_password=Checking password…
-password_ok=Password ok!
+password_ok=Password OK
 user_pass_invalid=Username or password invalid
 check_server_details=Checking server details
 check_in_server_details=Checking incoming server details
 check_out_server_details=Checking outgoing server details
 
 error_creating_account=Error Creating Account
 incoming_server_exists=Incoming server already exists.
 
 please_enter_name=Please enter your name.
 double_check_email=Double-check this email address!
 
+# add-on install
+addonInstallStarted=Downloading and installing add-on…
+addonInstallSuccess=Successfully installed the add-on.
+# LOCALIZATION NOTE(addonInstallLabel): %1$S will be the add-on name
+addonInstallShortLabel=Install
+
 #config result display
 # LOCALIZATION NOTE(resultUnknown): Displayed instead of resultIncoming,
 # resultOutgoing or resultUsername when we don't have a proper value.
 resultUnknown=Unknown
 # LOCALIZATION NOTE(resultIncoming):
 # %1$S will be replaced with either resultIMAP, resultPOP3 or resultSMTP.
 # %2$S will be replaced with the server hostname
 #   with possibly a port appended as ":"+port.
@@ -85,16 +99,17 @@ resultUnknown=Unknown
 # You may adjust the strings to be a real sentence.
 resultIncoming=%1$S, %2$S, %3$S%4$S
 # LOCALIZATION NOTE(resultOutgoing): see resultIncoming
 resultOutgoing=%1$S, %2$S, %3$S%4$S
 resultOutgoingExisting=Use existing outgoing SMTP server
 resultIMAP=IMAP
 resultPOP3=POP3
 resultSMTP=SMTP
+resultExchange=Exchange
 # LOCALIZATION NOTE(resultNoEncryption): Neither SSL/TLS nor STARTTLS. Transmission of emails in cleartext over the Internet.
 resultNoEncryption=No Encryption
 resultSSL=SSL
 resultSTARTTLS=STARTTLS
 # LOCALIZATION NOTE(resultSSLCertWeak): \u0020 is just a space
 resultSSLCertWeak=\u0020(Warning: Could not verify server)
 resultSSLCertOK=
 resultUsernameBoth=%1$S
--- a/mail/locales/en-US/chrome/messenger/accountCreationModel.properties
+++ b/mail/locales/en-US/chrome/messenger/accountCreationModel.properties
@@ -8,11 +8,13 @@
 
 # readFromXML.js
 no_emailProvider.error=The config file XML does not contain an email account configuration.
 outgoing_not_smtp.error=The outgoing server must be of type SMTP
 
 # verifyConfig.js
 cannot_login.error=Unable to log in at server. Probably wrong configuration, username or password.
 
-
 # guessConfig.js
 cannot_find_server.error=Can't find a server
+
+# exchangeAutoDiscover.js
+no_autodiscover.error=The Exchange AutoDiscover XML is invalid.
--- 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;
@@ -237,24 +239,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;
 }
@@ -220,24 +222,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
@@ -78,16 +78,17 @@
   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/thread-col.svg                 (../shared/mail/icons/thread-col.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/icons/waiting.svg                    (../shared/mail/icons/waiting.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 */
@@ -254,24 +255,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
@@ -894,23 +894,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");