Bug 1606610 - Warn the user when autodiscovery redirects from plain HTTP to HTTPS. Show the new target domain, and ask for confirmation. r=aleca a=wsmwk
authorBen Bucksch <ben.bucksch@beonex.com>
Mon, 29 Jun 2020 15:43:22 +0300
changeset 36200 d3b622342af68ebe1c390f67e1a369bd19a33a3f
parent 36199 b9ce60200cb6805b35d74d4c1878a9c69235d95a
child 36201 b14b9302518101cdb9dac51f8d1233418311ff3d
push id112
push userthunderbird@calypsoblue.org
push dateMon, 29 Jun 2020 23:55:13 +0000
treeherdercomm-esr68@6a7c26eb22bf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaleca, wsmwk
bugs1606610
Bug 1606610 - Warn the user when autodiscovery redirects from plain HTTP to HTTPS. Show the new target domain, and ask for confirmation. r=aleca a=wsmwk
mail/components/accountcreation/content/emailWizard.js
mail/components/accountcreation/content/emailWizard.xul
mail/components/accountcreation/content/exchangeAutoDiscover.js
mail/components/accountcreation/content/util.js
mail/themes/shared/mail/accountCreation.css
--- a/mail/components/accountcreation/content/emailWizard.js
+++ b/mail/components/accountcreation/content/emailWizard.js
@@ -142,16 +142,95 @@ function setLabelFromStringBundle(elemen
 }
 
 function removeChildNodes(el) {
   while (el.hasChildNodes()) {
     el.lastChild.remove();
   }
 }
 
+/**
+ * Inline confirmation dialog
+ * Shows, below status area:
+ *
+ * Your question here
+ *  [ Cancel ] [ OK ]
+ *
+ * @param {string} questionLabel - Text with the question.
+ * @param {string} okLabel - Text for OK/Yes button.
+ * @param {string} cancelLabel - Text for Cancel/No button.
+ * @param {function} okCallback - Called when the user clicks OK.
+ * @param {function(ex)} cancelCallback - Called when the user clicks Cancel
+ *   or if you call `Abortable.cancel()`.
+ * @returns {Abortable} - If `Abortable.cancel()` is called,
+ *   the dialog is closed and the `cancelCallback()` is called.
+ */
+function confirmDialog(
+  questionLabel,
+  okLabel,
+  cancelLabel,
+  okCallback,
+  cancelCallback
+) {
+  e("confirmationQuestion").textContent = questionLabel;
+  let okButton = e("confirmationOKButton");
+  let cancelButton = e("confirmationCancelButton");
+  okButton.label = okLabel;
+  cancelButton.label = cancelLabel;
+
+  // Disable UI we don't want in this state.
+  let statusAreaWasHidden = e("status_area").hidden;
+  let statusLineWasHidden = e("status-lines").hidden;
+  let cancelWasDisabled = e("cancel_button").disabled;
+  let stopWasDisabled = e("stop_button").disabled;
+  let manualConfigWasDisabled = e("manual-edit_button").disabled;
+  let nextWasDisabled = e("next_button").disabled;
+
+  _hide("status_area");
+  _hide("status-lines");
+  _disable("cancel_button");
+  _disable("stop_button");
+  _disable("manual-edit_button");
+  _disable("next_button");
+
+  _show("confirmationDialog");
+
+  function close() {
+    _hide("confirmationDialog");
+    e("status_area").hidden = statusAreaWasHidden;
+    e("status-lines").hidden = statusLineWasHidden;
+    e("cancel_button").disabled = cancelWasDisabled;
+    e("stop_button").disabled = stopWasDisabled;
+    e("manual-edit_button").disabled = manualConfigWasDisabled;
+    e("next_button").disabled = nextWasDisabled;
+  }
+  okButton.addEventListener(
+    "command",
+    event => {
+      close();
+      okCallback();
+    },
+    { once: true }
+  );
+  cancelButton.addEventListener(
+    "command",
+    event => {
+      close();
+      cancelCallback(new UserCancelledException());
+    },
+    { once: true }
+  );
+  let abortable = new Abortable();
+  abortable.cancel = ex => {
+    close();
+    cancelCallback(ex);
+  };
+  return abortable;
+}
+
 function EmailConfigWizard() {
   this._init();
 }
 EmailConfigWizard.prototype = {
   _init() {
     gEmailWizardLogger.info("Initializing setup wizard");
     this._abortable = null;
   },
@@ -628,17 +707,18 @@ EmailConfigWizard.prototype = {
         self.removeStatusLines();
         self.stopSpinner(call.foundMsg);
         self.foundConfig(config);
       },
       function(e, allErrors) {
         // all failed
         self._abortable = null;
         self.removeStatusLines();
-        if (allErrors.some(e => e instanceof CancelledException)) {
+        if (e instanceof CancelledException) {
+          self.onStartOver();
           return;
         }
 
         // guess config
         let initialConfig = new AccountConfig();
         self._prefillConfig(initialConfig);
         self._guessConfig(domain, initialConfig);
       }
@@ -695,17 +775,19 @@ EmailConfigWizard.prototype = {
         domain,
         emailAddress,
         this._exchangeUsername,
         this._password,
         call.successCallback(),
         (e, allErrors) => {
           // Must call error callback in any case to stop the discover mode.
           let errorCallback = call.errorCallback();
-          if (allErrors && allErrors.some(e => e.code == 401)) {
+          if (e instanceof CancelledException) {
+            errorCallback(e);
+          } else if (allErrors && allErrors.some(e => e.code == 401)) {
             // Auth failed.
             // Ask user for username.
             this.onStartOver();
             this.stopSpinner(); // clears status message
             _show("usernameRow");
             _show("status_area");
             _enable("manual-edit_button");
             errorCallback(new CancelledException());
@@ -1074,16 +1156,17 @@ EmailConfigWizard.prototype = {
               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");
+          removeChildNodes(containerE);
           for (let addon of config.addons) {
             // Creates
             // <row>
             //   <image src="https://live.thunderbird.net/owl32.png" />
             //   <label is="text-link" href="https://live.thunderbird.net/owl">
             //     A third party addon that ...
             //   </label>
             //   <button
--- a/mail/components/accountcreation/content/emailWizard.xul
+++ b/mail/components/accountcreation/content/emailWizard.xul
@@ -221,16 +221,24 @@
       <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"/>
 
+    <vbox id="confirmationDialog" hidden="true">
+       <description id="confirmationQuestion" class="autoconfigLabel"/>
+       <hbox id="confirmationButtons" pack="end">
+         <button id="confirmationCancelButton"/>
+         <button id="confirmationOKButton"/>
+       </hbox>
+    </vbox>
+
     <groupbox id="result_area" hidden="true">
       <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();"/>
--- a/mail/components/accountcreation/content/exchangeAutoDiscover.js
+++ b/mail/components/accountcreation/content/exchangeAutoDiscover.js
@@ -82,18 +82,16 @@ function fetchConfigFromExchange(
     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: 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) {
@@ -124,32 +122,110 @@ function fetchConfigFromExchange(
     callArgs,
     call.successCallback(),
     call.errorCallback()
   );
   fetch.start();
   call.setAbortable(fetch);
 
   call = priority.addCall();
-  fetch3 = new FetchHTTP(
-    url3,
-    callArgs,
-    call.successCallback(),
-    call.errorCallback()
-  );
+  let call3ErrorCallback = call.errorCallback();
+  // url3 is HTTP (not HTTPS), so suppress password. Even MS spec demands so.
+  let call3Args = deepCopy(callArgs);
+  delete call3Args.username;
+  delete call3Args.password;
+  fetch3 = new FetchHTTP(url3, call3Args, call.successCallback(), ex => {
+    // 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. For AutoDiscover mechanism to work,
+    // we need to repeat the call with the correct parameters again.
+    let redirectURL = fetch3._request.responseURL;
+    if (!redirectURL.startsWith("https:")) {
+      call3ErrorCallback(ex);
+      return;
+    }
+    let redirectURI = Services.io.newURI(redirectURL);
+    let redirectDomain = Services.eTLD.getBaseDomain(redirectURI);
+    let originalDomain = Services.eTLD.getBaseDomainFromHost(domain);
+
+    function fetchRedirect() {
+      let fetchCall = priority.addCall();
+      let fetch = new FetchHTTP(
+        redirectURL,
+        callArgs, // now with auth
+        fetchCall.successCallback(),
+        fetchCall.errorCallback()
+      );
+      fetchCall.setAbortable(fetch);
+      fetch.start();
+    }
+
+    const kSafeDomains = ["office365.com", "outlook.com"];
+    if (
+      redirectDomain != originalDomain &&
+      !kSafeDomains.includes(redirectDomain)
+    ) {
+      // Given that we received the redirect URL from an insecure HTTP call,
+      // we ask the user whether he trusts the redirect domain.
+      gEmailWizardLogger.info("AutoDiscover HTTP redirected to other domain");
+      let dialogSuccessive = new SuccessiveAbortable();
+      // Because the dialog implements Abortable, the dialog will cancel and
+      // close automatically, if a slow higher priority call returns late.
+      let dialogCall = priority.addCall();
+      dialogCall.setAbortable(dialogSuccessive);
+      call3ErrorCallback(new Exception("Redirected"));
+      dialogSuccessive.current = new TimeoutAbortable(
+        setTimeout(() => {
+          let loc = Services.locale.appLocaleAsBCP47;
+          // hardcoded otherDomain.label
+          let otherDomain =
+            esr10n.otherDomainLabel[loc] || esr10n.otherDomainLabel["en-US"];
+          let brandshortName = document
+            .getElementById("bundle_brand")
+            .getString("brandShortName");
+          let questionLabel = otherDomain
+            .replace("%1$S", brandshortName)
+            .replace("%2$S", redirectDomain);
+          // hardcoded otherDomain_ok.label
+          let okLabel =
+            esr10n.otherDomainOkLabel[loc] ||
+            esr10n.otherDomainOkLabel["en-US"];
+          // hardcoded otherDomain_cancel.label
+          let cancelLabel =
+            esr10n.otherDomainCancelLabel[loc] ||
+            esr10n.otherDomainCancelLabel["en-US"];
+          dialogSuccessive.current = confirmDialog(
+            questionLabel,
+            okLabel,
+            cancelLabel,
+            () => {
+              // User agreed.
+              fetchRedirect();
+              // Remove the dialog from the call stack.
+              dialogCall.errorCallback()(new Exception("Proceed to fetch"));
+            },
+            ex => {
+              // User rejected, or action cancelled otherwise.
+              dialogCall.errorCallback()(ex);
+            }
+          );
+          // Account for a slow server response.
+          // This will prevent showing the warning message when not necessary.
+          // The timeout is just for optics. The Abortable ensures that it works.
+        }, 2000)
+      );
+    } else {
+      fetchRedirect();
+      call3ErrorCallback(new Exception("Redirected"));
+    }
+  });
   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
@@ -564,8 +640,192 @@ function detectStandardProtocols(config,
       // Probing didn't find any open protocols.
       // Let's use the exchange (only) config that was listed then.
       successCallback(config);
     },
     config2,
     "both"
   );
 }
+
+var esr10n = {
+  // otherDomain.label
+  otherDomainLabel: {
+    "en-US":
+      "%1$S found your account setup information on %2$S. Do you want to proceed and submit your credentials?",
+    "sv-SE":
+      "%1$S hittade din kontoinställningsinformation på %2$S. Vill du fortsätta och skicka in dina autentiseringsuppgifter?",
+    sl:
+      "%1$S je našel podatke za nastavitev računa na %2$S. Ali želite nadaljevati in vnesti svoje prijavne podatke?",
+    "ja-JP-mac":
+      "%1$S が %2$S 上にあなたのアカウントセットアップ情報を見つけました。続けて認証情報を送信してもよろしいですか?",
+    kk:
+      "%1$S %2$S жерінде сіздің тіркелгіңіздің баптау ақпаратын тапты. Жалғастырып, тіркеу мәліметтерін жіберуді қалайсыз ба?",
+    "nb-NO":
+      "%1$S fant kontooppsettinformasjonen din på %2$S. Vil du fortsette og sende inn informasjonen din?",
+    sq:
+      "%1$S-i gjeti të dhëna ujdisjeje të llogarisë tuaj në %2$S. Doni të vazhdohet dhe të parashtrohen kredencialet tuaja?",
+    ja:
+      "%1$S が %2$S 上にあなたのアカウントセットアップ情報を見つけました。続けて認証情報を送信してもよろしいですか?",
+    "en-GB":
+      "%1$S found your account setup information on %2$S. Do you want to proceed and submit your credentials?",
+    "nn-NO":
+      "%1$S fann kontooppsettinformasjonen din på %2$S. Vil du fortsetje og sende inn informasjonen din?",
+    "en-CA":
+      "%1$S found your account setup information on %2$S. Do you want to proceed and submit your credentials?",
+    ru:
+      "%1$S обнаружил информацию по настройке вашей учётной записи на %2$S. Вы хотите продолжить и отправить свои учётные данные?",
+    ka:
+      "%1$S პოულობს თქვენი ანგარიშის მონაცემებს %2$S-ზე. გსურთ, განაგრძოთ და გაგზავნოთ ანგარიშის მონაცემები?",
+    cy:
+      "Mae %1$S wedi canfod manylion gosod eich cyfrif ar %2$S. Hoffech chi barhau a chyflwyno'ch manylion?",
+    "es-ES":
+      " %1$S ha encontrado su información de configuración de cuenta en %2$S. ¿Desea continuar y enviar sus credenciales?",
+    tr:
+      "%1$S, %2$S üzerinde hesap kurulum bilgilerinizi buldu. Devam etmek ve kimlik bilgilerinizi göndermek ister misiniz?",
+    "hy-AM":
+      "%1$S-ը գտավ ձեր հաշվի տեղակայման մասին տեղեկատվությունը %2$S-ում: Ցանկանո՞ւմ եք շարունակել և ներկայացնել ձեր հավատարմագրերը:",
+    "zh-CN": "%1$S 在 %2$S 上找到了您的账户设置信息,您要继续并提交凭据吗?",
+    it:
+      "%1$S ha trovato le informazioni per la configurazione dell’account su %2$S. Desideri procedere e inviare le tue credenziali?",
+    fr:
+      "%1$S a trouvé les informations de configuration de votre compte sur %2$S. Voulez-vous continuer et soumettre vos informations d’identification ?",
+    "pt-PT":
+      "%1$S encontrou as informações de configuração da sua conta em %2$S. Deseja continuar e enviar as suas credenciais?",
+    th:
+      "%1$S พบข้อมูลการตั้งค่าบัญชีของคุณบน %2$S คุณต้องการดำเนินการต่อแล้วส่งข้อมูลประจำตัวของคุณหรือไม่?",
+    kab:
+      "%1$S yufa-d talɣut n twila n umiḍan-ik deg %2$S. Tebɣiḍ ad tkemmleḍ daɣen ad tazneḍ talɣut n usulu ?",
+    uk:
+      "%1$S знайшов налаштування для вашого облікового запису на %2$S. Хочете продовжити і ввести свої облікові дані?",
+    "zh-TW": "%1$S 找到您在 %2$S 的帳號設定資訊。您想要繼續並送出登入資訊嗎?",
+    "es-AR":
+      "%1$S encontró la información de configuración de su cuenta en %2$S. ¿QUiere continuar y enviar sus credenciales?",
+    pl:
+      "%1$S znalazł informacje o konfiguracji tego konta w serwisie %2$S. Czy chcesz kontynuować i wysłać swoje dane logowania?",
+    eu:
+      "%1$S zure kontuko ezarpen informazioa aurkitu du hemen: %2$S.  Jarraitu nahi duzu eta zure kredentzialak aurkeztu?",
+    vi:
+      "%1$S đã tìm thấy thông tin thiết lập tài khoản của bạn trên %2$S. Bạn có muốn tiến hành và gửi thông tin của bạn?",
+    id:
+      "%1$S menermukan informasi pengaturan akun Anda di %2$S. Apakah Anda ingin melanjutkan dan mengirimkan kredensial Anda?",
+    da:
+      "%1$S fandt oplysninger om dine kontoindstillinger på %2$S. Vil du fortsætte og indsende dine login-informationer?",
+    fi:
+      "%1$S löysi tilisi asetustiedot verkkoalueelta %2$S. Haluatko jatkaa ja lähettää käyttöoikeustietosi?",
+    de:
+      "%1$S erkannte Informationen zur Konteneinrichtung auf %2$S. Wollen Sie fortfahren und Ihre Zugangsdaten senden?",
+    nl:
+      "%1$S heeft uw accountinstellingen van %2$S gevonden. Wilt u doorgaan en uw aanmeldgegevens versturen?",
+    ca:
+      "El %1$S ha trobat la informació de configuració del vostre compte a %2$S. Voleu continuar i enviar les vostres credencials?",
+    "fy-NL":
+      "%1$S hat jo accountynstellingen fan %2$S fûn. Wolle jo trochgean en jo oanmeldgegevens ferstjoere?",
+    ko:
+      "%1$S가 %2$S의 계정 설정 정보를 찾았습니다. 자격 증명을 진행하고 제출 하시겠습니까?",
+    "pt-BR":
+      "O %1$S encontrou as informações de configuração da sua conta em %2$S. Quer continuar e enviar suas credenciais?",
+    hsb:
+      "%1$S je informacije wo konfiguraciji konta na %2$S namakał. Chceće pokročować a swoje přizjewjenske daty wotpósłać?",
+    dsb:
+      "%1$S jo namakał informacije wó konfiguraciji konta na %2$S. Cośo pókšacowaś a swóje pśizjawjeńske daty wótpósłaś?",
+    rm:
+      "%1$S ha chattà las infurmaziuns per configurar tes conto sin %2$S. Vuls ti cuntinuar e trametter tias infurmaziuns d'annunzia?",
+    el:
+      "Το %1$S βρήκε τις πληροφορίες ρύθμισης του λογαριασμού σας στο %2$S. Θέλετε να συνεχίσετε και να υποβάλετε τα διαπιστευτήριά σας;",
+    hu:
+      "A %1$S megtalálta a fiókinformációit ehhez: %2$S. Folytatja és elküldi a hitelesítő adatait?",
+  },
+  // otherDomain_ok.label
+  otherDomainOkLabel: {
+    "en-US": "Login",
+    "sv-SE": "Inloggning",
+    sl: "Prijava",
+    "ja-JP-mac": "ログイン",
+    kk: "Логин",
+    "nb-NO": "Logg inn",
+    sq: "Kredenciale Hyrjesh",
+    ja: "ログイン",
+    "en-GB": "Login",
+    "nn-NO": "Inloggning",
+    "en-CA": "Login",
+    ru: "Войти",
+    ka: "შესვლა",
+    cak: "Rutikirisaxik molojri'ïl",
+    cy: "Mewngofnodi",
+    "es-ES": " Nombre de usuario",
+    tr: "Giriş yap",
+    "hy-AM": "Մուտք",
+    "zh-CN": "登录",
+    it: "Accedi",
+    fr: "Connexion",
+    "pt-PT": "Iniciar sessão",
+    th: "เข้าสู่ระบบ",
+    kab: "Kcem",
+    uk: "Вхід",
+    "zh-TW": "登入",
+    "es-AR": "Iniciar sesión",
+    pl: "Zaloguj się",
+    eu: "Saio-hasiera",
+    vi: "Đăng nhập",
+    id: "Info masuk",
+    da: "Login",
+    fi: "Kirjaudu",
+    de: "Senden",
+    nl: "Aanmelden",
+    ca: "Inici de sessió",
+    "fy-NL": "Oanmelde",
+    ko: "로그인",
+    "pt-BR": "Entrar",
+    hsb: "Přizjewjenje",
+    dsb: "Pśizjawjenje",
+    rm: "S'annunziar",
+    el: "Σύνδεση",
+    hu: "Bejelentkezés",
+  },
+  // otherDomain_cancel.label
+  otherDomainCancelLabel: {
+    "en-US": "Cancel",
+    "sv-SE": "Avbryt",
+    sl: "Prekliči",
+    "ja-JP-mac": "キャンセル",
+    kk: "Бас тарту",
+    "nb-NO": "Avbryt",
+    sq: "Anuloje",
+    ja: "キャンセル",
+    "en-GB": "Cancel",
+    "nn-NO": "Avbryt",
+    "en-CA": "Cancel",
+    ru: "Отмена",
+    ka: "გაუქმება",
+    cak: "Tiq'at",
+    cy: "Diddymu",
+    "es-ES": " Cancelar",
+    tr: "Vazgeç",
+    "hy-AM": "Չեղարկել",
+    "zh-CN": "取消",
+    it: "Annulla",
+    fr: "Annuler",
+    "pt-PT": "Cancelar",
+    th: "ยกเลิก",
+    kab: "Sefsex",
+    uk: "Скасувати",
+    "zh-TW": "取消",
+    "es-AR": "Cancelar",
+    pl: "Anuluj",
+    eu: "Utzi",
+    vi: "Hủy bỏ",
+    id: "Batal",
+    da: "Annuller",
+    fi: "Peruuta",
+    de: "Abbrechen",
+    nl: "Annuleren",
+    ca: "Cancel·la",
+    "fy-NL": "Annulearje",
+    ko: "취소",
+    "pt-BR": "Cancelar",
+    hsb: "Přetorhnyć",
+    dsb: "Pśetergnuś",
+    rm: "Interrumper",
+    el: "Ακύρωση",
+    hu: "Mégse",
+  },
+};
--- a/mail/components/accountcreation/content/util.js
+++ b/mail/components/accountcreation/content/util.js
@@ -441,17 +441,18 @@ ParallelCall.prototype = {
  * 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, allErrors)} errorCallback - All calls failed.
- *     {Exception} e - The exception returned by the first call.
+ *     {Exception} e - The first CancelledException, and otherwise
+ *       the exception returned by the first call.
  *     This is just to adhere to the standard API of errorCallback(e).
  *     {Array of Exception} allErrors - The exceptions from all calls.
  */
 function PriorityOrderAbortable(successCallback, errorCallback) {
   assert(typeof successCallback == "function");
   assert(typeof errorCallback == "function");
   ParallelAbortable.call(this); // call super constructor
   this._successfulCall = null;
@@ -487,17 +488,20 @@ function PriorityOrderAbortable(successC
         console.error(e);
         // If the handler failed with this data, treat this call as failed.
         call.e = e;
         call.succeeded = false;
       }
     }
     if (!this._successfulCall) {
       // all failed
-      errorCallback(this._calls[0].e, this._calls.map(call => call.e)); // see docs above
+      let allErrors = this._calls.map(call => call.e);
+      let e =
+        allErrors.find(e => e instanceof CancelledException) || allErrors[0];
+      errorCallback(e, allErrors); // see docs above
     }
   });
 }
 PriorityOrderAbortable.prototype = Object.create(ParallelAbortable.prototype);
 PriorityOrderAbortable.prototype.constructor = PriorityOrderAbortable;
 
 function NoLongerNeededException(msg) {
   CancelledException.call(this, msg);
--- a/mail/themes/shared/mail/accountCreation.css
+++ b/mail/themes/shared/mail/accountCreation.css
@@ -94,16 +94,41 @@ textbox.result-host-info > label.insecur
 #result_addon_install_column_link {
   max-width: 30em;
 }
 
 #result_addon_install_column_button {
   max-width: 10em;
 }
 
+#confirmationDialog {
+  margin-block: 10px;
+  padding: 10px;
+  background-color: #FFE900;
+  color: #333333;
+  border: 1px solid #F3CE00;
+  border-radius: 2px;
+}
+
+#confirmationButtons {
+  margin-top: 6px;
+}
+
+#confirmationDialog button {
+  background-color: -moz-Dialog;
+  border: 1px solid ThreeDShadow;
+  margin-inline: 5px;
+  margin-bottom: 0;
+}
+
+#confirmationDialog button:hover,
+#confirmationDialog button:focus {
+  background-color: #FFFFFF;
+}
+
 .btn-link {
   color: #0a84ff;
   -moz-appearance: none;
   font-size: 0.95rem;
   cursor: pointer;
 }
 .btn-link:hover {
   color: #0060df !important;