Implement Autodiscover for Exchange servers
* Autodiscover allows complete configuration using only email address
and password
* Code handles the two standard autodiscover addresses and redirect
* Autodiscover process starts when the user chooses "Exchange" as the
account type. If the account is created via the AccountManager,
autodiscover begins upon tapping "Next" for the first time
* If autodiscover fails due to anything other than auth failure for
autodiscover-capable servers, the user is placed into the standard manual
configuration screen
Bug: 2366019
Change-Id: I936712b924833d9a133e8da04e11c3ba45d92f92
diff --git a/src/com/android/exchange/EasSyncService.java b/src/com/android/exchange/EasSyncService.java
index 418b1ea..20c32e6 100644
--- a/src/com/android/exchange/EasSyncService.java
+++ b/src/com/android/exchange/EasSyncService.java
@@ -28,6 +28,7 @@
import com.android.email.provider.EmailContent.Mailbox;
import com.android.email.provider.EmailContent.MailboxColumns;
import com.android.email.provider.EmailContent.Message;
+import com.android.email.service.EmailServiceProxy;
import com.android.exchange.adapter.AbstractSyncAdapter;
import com.android.exchange.adapter.AccountSyncAdapter;
import com.android.exchange.adapter.ContactsSyncAdapter;
@@ -42,25 +43,35 @@
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
+import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlSerializer;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
+import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.util.Log;
+import android.util.Xml;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -94,6 +105,11 @@
// Define our default protocol version as 2.5 (Exchange 2003)
static private final String DEFAULT_PROTOCOL_VERSION = "2.5";
+ static private final String AUTO_DISCOVER_SCHEMA_PREFIX =
+ "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
+ static private final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
+ static private final int AUTO_DISCOVER_REDIRECT_CODE = 451;
+
/**
* We start with an 8 minute timeout, and increase/decrease by 3 minutes at a time. There's
* no point having a timeout shorter than 5 minutes, I think; at that point, we can just let
@@ -200,7 +216,7 @@
* @return whether or not the code represents an authentication error
*/
protected boolean isAuthError(int code) {
- return ((code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN));
+ return (code == HttpStatus.SC_UNAUTHORIZED) || (code == HttpStatus.SC_FORBIDDEN);
}
@Override
@@ -261,6 +277,300 @@
}
+ /**
+ * Gets the redirect location from the HTTP headers and uses that to modify the HttpPost so that
+ * it can be reused
+ *
+ * @param resp the HttpResponse that indicates a redirect (451)
+ * @param post the HttpPost that was originally sent to the server
+ * @return the HttpPost, updated with the redirect location
+ */
+ private HttpPost getRedirect(HttpResponse resp, HttpPost post) {
+ Header locHeader = resp.getFirstHeader("X-MS-Location");
+ if (locHeader != null) {
+ String loc = locHeader.getValue();
+ // If we've gotten one and it shows signs of looking like an address, we try
+ // sending our request there
+ if (loc != null && loc.startsWith("http")) {
+ post.setURI(URI.create(loc));
+ return post;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Send the POST command to the autodiscover server, handling a redirect, if necessary, and
+ * return the HttpResponse
+ *
+ * @param client the HttpClient to be used for the request
+ * @param post the HttpPost we're going to send
+ * @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)
+ throws IOException, MessagingException {
+ userLog("Posting autodiscover to: " + post.getURI());
+ HttpResponse resp = client.execute(post);
+ int code = resp.getStatusLine().getStatusCode();
+ // On a redirect, try the new location
+ if (code == AUTO_DISCOVER_REDIRECT_CODE) {
+ post = getRedirect(resp, post);
+ if (post != null) {
+ userLog("Posting autodiscover to redirect: " + post.getURI());
+ return client.execute(post);
+ }
+ } else if (isAuthError(code)) {
+ throw new MessagingException(MessagingException.AUTHENTICATION_FAILED);
+ } else if (code != HttpStatus.SC_OK) {
+ // We'll try the next address if this doesn't work
+ userLog("Code: " + code + ", throwing IOException");
+ throw new IOException();
+ }
+ return resp;
+ }
+
+ /**
+ * Use the Exchange 2007 AutoDiscover feature to try to retrieve server information using
+ * only an email address and the password
+ *
+ * @param userName the user's email address
+ * @param password the user's password
+ * @return a HostAuth ready to be saved in an Account or null (failure)
+ */
+ public Bundle tryAutodiscover(String userName, String password) throws RemoteException {
+ XmlSerializer s = Xml.newSerializer();
+ ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
+ HostAuth hostAuth = new HostAuth();
+ Bundle bundle = new Bundle();
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.NO_ERROR);
+ try {
+ // Build the XML document that's sent to the autodiscover server(s)
+ s.setOutput(os, "UTF-8");
+ s.startDocument("UTF-8", false);
+ s.startTag(null, "Autodiscover");
+ s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
+ s.startTag(null, "Request");
+ s.startTag(null, "EMailAddress").text(userName).endTag(null, "EMailAddress");
+ s.startTag(null, "AcceptableResponseSchema");
+ s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
+ s.endTag(null, "AcceptableResponseSchema");
+ s.endTag(null, "Request");
+ s.endTag(null, "Autodiscover");
+ s.endDocument();
+ String req = os.toString();
+
+ // Initialize the user name and password
+ mUserName = userName;
+ mPassword = password;
+ // Make sure the authentication string is created (mAuthString)
+ makeUriString("foo", null);
+
+ // Split out the domain name
+ int amp = userName.indexOf('@');
+ // The UI ensures that userName is a valid email address
+ if (amp < 0) {
+ throw new RemoteException();
+ }
+ String domain = userName.substring(amp + 1);
+
+ // 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)
+
+ // Try the domain first and see if we can get a response
+ HttpPost post = new HttpPost("https://" + domain + AUTO_DISCOVER_PAGE);
+ setHeaders(post);
+ post.setHeader("Content-Type", "text/xml");
+ post.setEntity(new StringEntity(req));
+ HttpClient client = getHttpClient(COMMAND_TIMEOUT);
+ HttpResponse resp;
+ try {
+ resp = postAutodiscover(client, post);
+ } catch (ClientProtocolException e1) {
+ return null;
+ } catch (IOException e1) {
+ // 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);
+ }
+
+ // Get the "final" code; if it's not 200, just return null
+ int code = resp.getStatusLine().getStatusCode();
+ userLog("Code: " + code);
+ if (code != HttpStatus.SC_OK) return null;
+
+ // At this point, we have a 200 response (SC_OK)
+ HttpEntity e = resp.getEntity();
+ InputStream is = e.getContent();
+ try {
+ // The response to Autodiscover is regular XML (not WBXML)
+ // If we ever get an error in this process, we'll just punt and return null
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlPullParser parser = factory.newPullParser();
+ parser.setInput(is, "UTF-8");
+ int type = parser.getEventType();
+ if (type == XmlPullParser.START_DOCUMENT) {
+ type = parser.next();
+ if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("Autodiscover")) {
+ hostAuth = new HostAuth();
+ parseAutodiscover(parser, hostAuth);
+ // On success, we'll have a server address and login
+ if (hostAuth.mAddress != null && hostAuth.mLogin != null) {
+ // Fill in the rest of the HostAuth
+ hostAuth.mPassword = password;
+ hostAuth.mPort = 443;
+ hostAuth.mProtocol = "eas";
+ hostAuth.mFlags =
+ HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
+ bundle.putParcelable(
+ EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH, hostAuth);
+ } else {
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.UNSPECIFIED_EXCEPTION);
+ }
+ }
+ }
+ }
+ } catch (XmlPullParserException e1) {
+ // This would indicate an I/O error of some sort
+ // We will simply return null and user can configure manually
+ }
+ // There's no reason at all for exceptions to be thrown, and it's ok if so.
+ // We just won't do auto-discover; user can configure manually
+ } catch (IllegalArgumentException e) {
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.UNSPECIFIED_EXCEPTION);
+ } catch (IllegalStateException e) {
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.UNSPECIFIED_EXCEPTION);
+ } catch (IOException e) {
+ userLog("IOException in Autodiscover", e);
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.IOERROR);
+ } catch (MessagingException e) {
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ MessagingException.AUTHENTICATION_FAILED);
+ }
+ return bundle;
+ }
+
+ void parseServer(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ boolean mobileSync = false;
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Server")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("Type")) {
+ if (parser.nextText().equals("MobileSync")) {
+ mobileSync = true;
+ }
+ } else if (mobileSync && name.equals("Url")) {
+ String url = parser.nextText().toLowerCase();
+ // This will look like https://<server address>/Microsoft-Server-ActiveSync
+ // We need to extract the <server address>
+ if (url.startsWith("https://") &&
+ url.endsWith("/microsoft-server-activesync")) {
+ int lastSlash = url.lastIndexOf('/');
+ hostAuth.mAddress = url.substring(8, lastSlash);
+ userLog("Autodiscover, server: " + hostAuth.mAddress);
+ }
+ }
+ }
+ }
+ }
+
+ void parseSettings(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Settings")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("Server")) {
+ parseServer(parser, hostAuth);
+ }
+ }
+ }
+ }
+
+ void parseAction(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Action")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("Error")) {
+ // Should parse the error
+ } else if (name.equals("Redirect")) {
+ Log.d(TAG, "Redirect: " + parser.nextText());
+ } else if (name.equals("Settings")) {
+ parseSettings(parser, hostAuth);
+ }
+ }
+ }
+ }
+
+ void parseUser(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("User")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("EMailAddress")) {
+ String addr = parser.nextText();
+ hostAuth.mLogin = addr;
+ userLog("Autodiscover, login: " + addr);
+ } else if (name.equals("DisplayName")) {
+ String dn = parser.nextText();
+ userLog("Autodiscover, user: " + dn);
+ }
+ }
+ }
+ }
+
+ void parseResponse(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.next();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Response")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG) {
+ String name = parser.getName();
+ if (name.equals("User")) {
+ parseUser(parser, hostAuth);
+ } else if (name.equals("Action")) {
+ parseAction(parser, hostAuth);
+ }
+ }
+ }
+ }
+
+ void parseAutodiscover(XmlPullParser parser, HostAuth hostAuth)
+ throws XmlPullParserException, IOException {
+ while (true) {
+ int type = parser.nextTag();
+ if (type == XmlPullParser.END_TAG && parser.getName().equals("Autodiscover")) {
+ break;
+ } else if (type == XmlPullParser.START_TAG && parser.getName().equals("Response")) {
+ parseResponse(parser, hostAuth);
+ }
+ }
+ }
+
private void doStatusCallback(long messageId, long attachmentId, int status) {
try {
SyncManager.callback().loadAttachmentStatus(messageId, attachmentId, status, 0);
@@ -372,7 +682,7 @@
errorLog("totalRead is greater than attachment length?");
break;
}
- int pct = (totalRead * 100 / length);
+ int pct = (totalRead * 100) / length;
doProgressCallback(msg.mId, att.mId, pct);
}
}
@@ -721,7 +1031,6 @@
int pingStatus = SyncManager.pingStatus(mailboxId);
String mailboxName = c.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN);
if (pingStatus == SyncManager.PING_STATUS_OK) {
-
String syncKey = c.getString(Mailbox.CONTENT_SYNC_KEY_COLUMN);
if ((syncKey == null) || syncKey.equals("0")) {
// We can't push until the initial sync is done
@@ -1156,7 +1465,7 @@
}
} catch (IOException e) {
String message = e.getMessage();
- userLog("Caught IOException: ", ((message == null) ? "No message" : message));
+ userLog("Caught IOException: ", (message == null) ? "No message" : message);
mExitStatus = EXIT_IO_ERROR;
} catch (Exception e) {
userLog("Uncaught exception in EasSyncService", e);
diff --git a/src/com/android/exchange/EmailContent.aidl b/src/com/android/exchange/EmailContent.aidl
deleted file mode 100644
index c6b4a7d..0000000
--- a/src/com/android/exchange/EmailContent.aidl
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2008-2009 Marc Blank
- * Licensed to The Android Open Source Project.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.exchange;
-
-parcelable EmailContent.Attachment;
-
diff --git a/src/com/android/exchange/IEmailService.aidl b/src/com/android/exchange/IEmailService.aidl
index ec0fd5e..81181eb 100644
--- a/src/com/android/exchange/IEmailService.aidl
+++ b/src/com/android/exchange/IEmailService.aidl
@@ -17,7 +17,8 @@
package com.android.exchange;
import com.android.exchange.IEmailServiceCallback;
-import com.android.exchange.EmailContent;
+import com.android.email.provider.EmailContent;
+import android.os.Bundle;
interface IEmailService {
int validate(in String protocol, in String host, in String userName, in String password,
@@ -40,4 +41,6 @@
void setLogging(int on);
void hostChanged(long accountId);
+
+ Bundle autoDiscover(String userName, String password);
}
\ No newline at end of file
diff --git a/src/com/android/exchange/SyncManager.java b/src/com/android/exchange/SyncManager.java
index 41a6561..9e1f0db 100644
--- a/src/com/android/exchange/SyncManager.java
+++ b/src/com/android/exchange/SyncManager.java
@@ -277,6 +277,10 @@
}
}
+ public Bundle autoDiscover(String userName, String password) throws RemoteException {
+ return new EasSyncService().tryAutodiscover(userName, password);
+ }
+
public void startSync(long mailboxId) throws RemoteException {
checkSyncManagerServiceRunning();
Mailbox m = Mailbox.restoreMailboxWithId(INSTANCE, mailboxId);