Bug 1518155 - [autoconfig] Exchange AutoDiscover: Ask for username only if necessary. r=Neil a=jorgk
authorBen Bucksch <ben.bucksch@beonex.com>
Wed, 16 Jan 2019 14:32:33 +0100
changeset 33946 d90827241325c5d38974e01ea61bdac3c7cd1fbb
parent 33945 1e7392826bd5be19adc18ad1d6824e45306ef7b4
child 33947 492bea4ded8961c2171ba05a185555c5e6deb7af
push id388
push userclokep@gmail.com
push dateMon, 28 Jan 2019 20:54:56 +0000
reviewersNeil, jorgk
bugs1518155
Bug 1518155 - [autoconfig] Exchange AutoDiscover: Ask for username only if necessary. r=Neil a=jorgk
mail/components/accountcreation/content/emailWizard.js
mail/components/accountcreation/content/emailWizard.xul
mail/components/accountcreation/content/exchangeAutoDiscover.js
mail/components/accountcreation/content/util.js
--- a/mail/components/accountcreation/content/emailWizard.js
+++ b/mail/components/accountcreation/content/emailWizard.js
@@ -155,16 +155,17 @@ EmailConfigWizard.prototype =
       // not be available even if it is.
     }
 
     this._domain = "";
     this._email = "";
     this._realname = (userFullname) ? userFullname : "";
     e("realname").value = this._realname;
     this._password = "";
+    this._exchangeUsername = ""; // only for Exchange AutoDiscover and only if needed
     this._okCallback = null;
 
     if (window.arguments && window.arguments[0]) {
       if (window.arguments[0].msgWindow) {
         this._parentMsgWindow = window.arguments[0].msgWindow;
       }
       if (window.arguments[0].okCallback) {
         this._okCallback = window.arguments[0].okCallback;
@@ -440,23 +441,30 @@ EmailConfigWizard.prototype =
    * A change to the email address also automatically restarts the
    * whole process.
    */
   onInputEmail() {
     this._email = e("email").value;
     this.onStartOver();
     this.checkStartDone();
   },
+
   onInputRealname() {
     this._realname = e("realname").value;
     this.checkStartDone();
   },
 
+  onInputUsername() {
+    this._exchangeUsername = e("usernameEx").value;
+    this.checkStartDone();
+  },
+
   onInputPassword() {
     this._password = e("password").value;
+    this.checkStartDone();
   },
 
   /**
    * This does very little other than to check that a name was entered at all
    * Since this is such an insignificant test we should be using a very light
    * or even jovial warning.
    */
   onBlurRealname() {
@@ -601,18 +609,28 @@ EmailConfigWizard.prototype =
       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());
+      fetch = fetchConfigFromExchange(domain,
+        emailAddress, this._exchangeUsername, this._password,
+        call.successCallback(),
+        (e, allErrors) => {
+          // Must call error callback in any case, to stop the discover mode
+          call.errorCallback()(e); // ()(e) is correct
+          if (e.code == 401 || allErrors && allErrors.find(e => e.code == 401)) { // Auth failed
+            // ask user for username
+            _show("usernameRow");
+            this.switchToMode("start");
+          }
+        });
       call.setAbortable(fetch);
 
     } catch (e) { // e.g. when entering an invalid domain like "c@c.-com"
       this.showErrorMsg(e);
       this.removeStatusLines();
       this.onStop();
     }
   },
@@ -764,17 +782,16 @@ EmailConfigWizard.prototype =
       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) {
--- a/mail/components/accountcreation/content/emailWizard.xul
+++ b/mail/components/accountcreation/content/emailWizard.xul
@@ -194,16 +194,31 @@
         </row>
         <row align="center" pack="start">
           <label class="autoconfigLabel"/>
           <checkbox id="remember_password"
                     label="&rememberPassword.label;"
                     accesskey="&rememberPassword.accesskey;"
                     checked="true"/>
         </row>
+        <row id="usernameRow" align="center" hidden="true">
+          <!-- This is used only used for Exchange AutoDiscover, and even then
+               only when absolutely necessary and known to be needed. -->
+          <label
+                 class="autoconfigLabel"
+                 value="&username.label;"
+                 control="usernameEx"/>
+          <textbox id="usernameEx"
+                   class="padded"
+                   placeholder="DOMAIN\username"
+                   oninput="gEmailConfigWizard.onInputUsername();" />
+          <hbox>
+            <description id="usernametextEx" class="initialDesc"></description>
+          </hbox>
+        </row>
       </rows>
     </grid>
     <spacer 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
--- a/mail/components/accountcreation/content/exchangeAutoDiscover.js
+++ b/mail/components/accountcreation/content/exchangeAutoDiscover.js
@@ -14,75 +14,77 @@ ChromeUtils.defineModuleGetter(this, "Ad
  *
  * 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} username - (Optional) The user's login name.
+ *         If null, email address will be used.
  * @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,
+function fetchConfigFromExchange(domain, emailAddress, username, 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) +
+  // <https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638(v%3Dexchg.140)>, search for "The Autodiscover service uses one of these four methods"
+  let url1 = "https://autodiscover." + sanitize.hostname(domain) +
              "/autodiscover/autodiscover.xml";
-  let url2 = "https://autodiscover." + sanitize.hostname(domain) +
+  let url2 = "https://" + sanitize.hostname(domain) +
              "/autodiscover/autodiscover.xml";
   let url3 = "http://autodiscover." + sanitize.hostname(domain) +
-             "/autodiscover/autodiscover.xml"; // needed by email hosters
+             "/autodiscover/autodiscover.xml";
   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,
+    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) { // success
-      readAutoDiscoverResponse(xml, successive, password, function(config) {
+      readAutoDiscoverResponse(xml, successive, username, 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());
@@ -114,52 +116,54 @@ function fetchConfigFromExchange(domain,
 
 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) {
+  successive, username, 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,
+      redirectEmailAddress, username, password,
       successCallback, errorCallback);
   }
 
-  let config = readAutoDiscoverXML(autoDiscoverXML);
+  let config = readAutoDiscoverXML(autoDiscoverXML, username);
 
   if (config.isComplete()) {
     successCallback(config);
   } else {
     errorCallback(new Exception("No valid configs found in AutoDiscover XML"));
   }
 }
 
 /**
  * @param {JXON} xml - The Exchange server AutoDiscover response
+ * @param {string} username - (Optional) The user's login name
+ *     If null, email address placeholder will be used.
  * @returns {AccountConfig} - @see accountConfig.js
  *
  * @see <https://www.msxfaq.de/exchange/autodiscover/autodiscover_xml.htm>
  */
-function readAutoDiscoverXML(autoDiscoverXML) {
+function readAutoDiscoverXML(autoDiscoverXML, username) {
   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"));
@@ -167,17 +171,17 @@ function readAutoDiscoverXML(autoDiscove
   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.username = 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;
 
@@ -252,17 +256,17 @@ function readAutoDiscoverXML(autoDiscove
         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%";
+          server.username = username || "%EMAILADDRESS%";
         }
 
         if (type == "SMTP") {
           if (!config.outgoing.hostname) {
             config.outgoing = server;
           } else {
             config.outgoingAlternatives.push(server);
           }
--- a/mail/components/accountcreation/content/util.js
+++ b/mail/components/accountcreation/content/util.js
@@ -329,17 +329,19 @@ ParallelCall.prototype = {
   },
   /**
    * 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 +
+      ddump("call " + this.position + " took " + (Date.now() - this._time) +
+          "ms and failed with " + (typeof(e.code) == "number" ? e.code + " " : "") +
+          (e.toString() ? e.toString() : "unknown error, probably no host connection") +
           (this.callerAbortable && this.callerAbortable._url ? " at <" + this.callerAbortable._url + ">" : ""));
       this.e = e;
       this.finished = true;
       this.succeeded = false;
       this._parallelAbortable._notifyFinished(this);
     };
   },
   /**
@@ -364,17 +366,20 @@ ParallelCall.prototype = {
  * 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.
+ * @param {Function(e, allErrors)} errorCallback - All calls failed.
+ *     {Exception} e - 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.addOneFinishedObserver(finishedCall => {
     let haveHigherPending = false;
@@ -411,17 +416,17 @@ function PriorityOrderAbortable(successC
           call.e = e;
           call.succeeded = false;
           haveHigherSuccess = false;
         }
       }
     }
     if (!haveHigherPending && !haveHigherSuccess) {
       // all failed
-      errorCallback(this._calls[0].e);
+      errorCallback(this._calls[0].e, this._calls.map(call => call.e)); // see docs above
     }
   });
 }
 PriorityOrderAbortable.prototype = Object.create(ParallelAbortable.prototype);
 PriorityOrderAbortable.prototype.constructor = PriorityOrderAbortable;
 
 function NoLongerNeededException(msg) {
   CancelledException.call(this, msg);