Try autodiscover with bare name if we get 401 with address

* Some autodiscover servers appear to require the bare user name
  for authentication rather than the user's email address.  This
  is apparently common for complex organizations maintaining a
  group of email domains
* If we get a 401 when trying to connect to an autodiscover server
  using the email address, we try again using just the bare name

Bug: 2682833
Change-Id: Ia07ca336e189069d4f3539e2245b3d53c82e3324
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index a01bdc2..aa0d5b4 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -467,15 +467,17 @@
 
     /**
      * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
-     * return the HttpResponse
+     * return the HttpResponse.  If we get a 401 (unauthorized) error and we're using the
+     * full email address, try the bare user name instead (e.g. foo instead of foo@bar.com)
      *
      * @param client the HttpClient to be used for the request
      * @param post the HttpPost we're going to send
+     * @param canRetry whether we can retry using the bare name on an authentication failure (401)
      * @return an HttpResponse from the original or redirect server
      * @throws IOException on any IOException within the HttpClient code
      * @throws MessagingException
      */
-    private HttpResponse postAutodiscover(HttpClient client, HttpPost post)
+    private HttpResponse postAutodiscover(HttpClient client, HttpPost post, boolean canRetry)
             throws IOException, MessagingException {
         userLog("Posting autodiscover to: " + post.getURI());
         HttpResponse resp = executePostWithTimeout(client, post, COMMAND_TIMEOUT);
@@ -487,10 +489,21 @@
                 userLog("Posting autodiscover to redirect: " + post.getURI());
                 return executePostWithTimeout(client, post, COMMAND_TIMEOUT);
             }
+        // 401 (Unauthorized) is for true auth errors when used in Autodiscover
         } else if (code == HttpStatus.SC_UNAUTHORIZED) {
-            // 401 (Unauthorized) is for true auth errors when used in Autodiscover
-            // 403 (and others) we'll just punt on
+            if (canRetry && mUserName.contains("@")) {
+                // Try again using the bare user name
+                int atSignIndex = mUserName.indexOf('@');
+                mUserName = mUserName.substring(0, atSignIndex);
+                cacheAuthAndCmdString();
+                userLog("401 received; trying username: ", mUserName);
+                // Recreate the basic authentication string and reset the header
+                post.removeHeaders("Authorization");
+                post.setHeader("Authorization", mAuthString);
+                return postAutodiscover(client, post, false);
+            }
             throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
+        // 403 (and others) we'll just punt on
         } else if (code != HttpStatus.SC_OK) {
             // We'll try the next address if this doesn't work
             userLog("Code: " + code + ", throwing IOException");
@@ -533,8 +546,8 @@
             // Initialize the user name and password
             mUserName = userName;
             mPassword = password;
-            // Make sure the authentication string is created (mAuthString)
-            makeUriString("foo", null);
+            // Make sure the authentication string is recreated and cached
+            cacheAuthAndCmdString();
 
             // Split out the domain name
             int amp = userName.indexOf('@');
@@ -546,6 +559,9 @@
 
             // There are up to four attempts here; the two URLs that we're supposed to try per the
             // specification, and up to one redirect for each (handled in postAutodiscover)
+            // Note: The expectation is that, of these four attempts, only a single server will
+            // actually be identified as the autodiscover server.  For the identified server,
+            // we may also try a 2nd connection with a different format (bare name).
 
             // Try the domain first and see if we can get a response
             HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
@@ -555,14 +571,14 @@
             HttpClient client = getHttpClient(COMMAND_TIMEOUT);
             HttpResponse resp;
             try {
-                resp = postAutodiscover(client, post);
+                resp = postAutodiscover(client, post, true /*canRetry*/);
             } catch (IOException e1) {
                 userLog("IOException in autodiscover; trying alternate address");
                 // We catch the IOException here because we have an alternate address to try
                 post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
                 // If we fail here, we're out of options, so we let the outer try catch the
                 // IOException and return null
-                resp = postAutodiscover(client, post);
+                resp = postAutodiscover(client, post, true /*canRetry*/);
             }
 
             // Get the "final" code; if it's not 200, just return null
@@ -588,9 +604,12 @@
                             hostAuth = new HostAuth();
                             parseAutodiscover(parser, hostAuth);
                             // On success, we'll have a server address and login
-                            if (hostAuth.mAddress != null && hostAuth.mLogin != null) {
+                            if (hostAuth.mAddress != null) {
                                 // Fill in the rest of the HostAuth
-                                hostAuth.mPassword = password;
+                                // We use the user name and password that were successful during
+                                // the autodiscover process
+                                hostAuth.mLogin = mUserName;
+                                hostAuth.mPassword = mPassword;
                                 hostAuth.mPort = 443;
                                 hostAuth.mProtocol = "eas";
                                 hostAuth.mFlags =
@@ -699,8 +718,7 @@
                 String name = parser.getName();
                 if (name.equals("EMailAddress")) {
                     String addr = parser.nextText();
-                    hostAuth.mLogin = addr;
-                    userLog("Autodiscover, login: " + addr);
+                    userLog("Autodiscover, email: " + addr);
                 } else if (name.equals("DisplayName")) {
                     String dn = parser.nextText();
                     userLog("Autodiscover, user: " + dn);
@@ -1041,15 +1059,24 @@
         }
     }
 
+    /**
+     * Using mUserName and mPassword, create and cache mAuthString and mCacheString, which are used
+     * in all HttpPost commands.  This should be called if these strings are null, or if mUserName
+     * and/or mPassword are changed
+     */
     @SuppressWarnings("deprecation")
+    private void cacheAuthAndCmdString() {
+        String safeUserName = URLEncoder.encode(mUserName);
+        String cs = mUserName + ':' + mPassword;
+        mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
+        mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId +
+            "&DeviceType=" + mDeviceType;
+    }
+
     private String makeUriString(String cmd, String extra) throws IOException {
          // Cache the authentication string and the command string
-        String safeUserName = URLEncoder.encode(mUserName);
-        if (mAuthString == null) {
-            String cs = mUserName + ':' + mPassword;
-            mAuthString = "Basic " + Base64.encodeToString(cs.getBytes(), Base64.NO_WRAP);
-            mCmdString = "&User=" + safeUserName + "&DeviceId=" + mDeviceId + "&DeviceType="
-                    + mDeviceType;
+        if (mAuthString == null || mCmdString == null) {
+            cacheAuthAndCmdString();
         }
         String us = (mSsl ? (mTrustSsl ? "httpts" : "https") : "http") + "://" + mHostAddress +
             "/Microsoft-Server-ActiveSync";