Merge "Make sendMeetingResponse and Autodiscover an EasOperation" into ub-mail-master
diff --git a/src/com/android/exchange/service/EasAutoDiscover.java b/src/com/android/exchange/eas/EasAutoDiscover.java
similarity index 61%
rename from src/com/android/exchange/service/EasAutoDiscover.java
rename to src/com/android/exchange/eas/EasAutoDiscover.java
index 026e227..fd30087 100644
--- a/src/com/android/exchange/service/EasAutoDiscover.java
+++ b/src/com/android/exchange/eas/EasAutoDiscover.java
@@ -1,20 +1,20 @@
-package com.android.exchange.service;
+package com.android.exchange.eas;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.util.Xml;
-import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.service.EmailServiceProxy;
+import com.android.exchange.CommandStatusException;
import com.android.exchange.Eas;
import com.android.exchange.EasResponse;
import com.android.mail.utils.LogUtils;
+import org.apache.http.HttpEntity;
import org.apache.http.HttpStatus;
-import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -23,18 +23,19 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.net.URI;
-import java.security.cert.CertificateException;
-/**
- * Performs Autodiscover for Exchange servers. This feature tries to find all the configuration
- * options needed based on just a username and password.
- */
-public class EasAutoDiscover extends EasServerConnection {
- private static final String TAG = Eas.LOG_TAG;
+public class EasAutoDiscover extends EasOperation {
+
+ public final static int RESULT_OK = 1;
+ public final static int RESULT_SC_UNAUTHORIZED = RESULT_OP_SPECIFIC_ERROR_RESULT - 0;
+ public final static int RESULT_REDIRECT = RESULT_OP_SPECIFIC_ERROR_RESULT - 1;
+ public final static int RESULT_BAD_RESPONSE = RESULT_OP_SPECIFIC_ERROR_RESULT - 2;
+ public final static int RESULT_FATAL_SERVER_ERROR = RESULT_OP_SPECIFIC_ERROR_RESULT - 3;
+
+ private final static String TAG = LogUtils.TAG;
private static final String AUTO_DISCOVER_SCHEMA_PREFIX =
- "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
+ "http://schemas.microsoft.com/exchange/autodiscover/mobilesync/";
private static final String AUTO_DISCOVER_PAGE = "/autodiscover/autodiscover.xml";
// Set of string constants for parsing the autodiscover response.
@@ -53,95 +54,56 @@
private static final String ELEMENT_NAME_RESPONSE = "Response";
private static final String ELEMENT_NAME_AUTODISCOVER = "Autodiscover";
- public EasAutoDiscover(final Context context, final String username, final String password) {
- super(context, new Account(), new HostAuth());
- mHostAuth.mLogin = username;
- mHostAuth.mPassword = password;
- mHostAuth.mFlags = HostAuth.FLAG_AUTHENTICATE | HostAuth.FLAG_SSL;
+ private final String mUri;
+ private final String mUsername;
+ private final String mPassword;
+ private HostAuth mHostAuth;
+ private String mRedirectUri;
+
+ public EasAutoDiscover(final Context context, final String uri, final String username,
+ final String password) {
+ // We don't actually need an account or a hostAuth, but the EasServerConnection requires
+ // one. Just create dummy values.
+ super(context, -1);
+ mUri = uri;
+ mUsername = username;
+ mPassword = password;
+ mHostAuth = new HostAuth();
+ mHostAuth.mLogin = mUsername;
+ mHostAuth.mPassword = mPassword;
mHostAuth.mPort = 443;
+ mHostAuth.mProtocol = Eas.PROTOCOL;
+ mHostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
+ setAccount(new Account(), mHostAuth);
}
- /**
- * Do all the work of autodiscovery.
- * @return A {@link Bundle} with the host information if autodiscovery succeeded. If we failed
- * due to an authentication failure, we return a {@link Bundle} with no host info but with
- * an appropriate error code. Otherwise, we return null.
- */
- public Bundle doAutodiscover() {
- final String domain = getDomain();
- if (domain == null) {
- return null;
- }
-
- final StringEntity entity = buildRequestEntity();
- if (entity == null) {
- return null;
- }
- try {
- final HttpPost post = makePost("https://" + domain + AUTO_DISCOVER_PAGE, entity,
- "text/xml", false);
- final EasResponse resp = getResponse(post, domain);
- if (resp == null) {
- return null;
- }
-
- try {
- // resp is either an authentication error, or a good response.
- final int code = resp.getStatus();
- if (code == HttpStatus.SC_UNAUTHORIZED) {
- final Bundle bundle = new Bundle(1);
- bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
- MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED);
- return bundle;
- } else {
- final HostAuth hostAuth = parseAutodiscover(resp);
- if (hostAuth != null) {
- // Fill in the rest of the HostAuth
- // We use the user name and password that were successful during
- // the autodiscover process
- hostAuth.mLogin = mHostAuth.mLogin;
- hostAuth.mPassword = mHostAuth.mPassword;
- // Note: there is no way we can auto-discover the proper client
- // SSL certificate to use, if one is needed.
- hostAuth.mPort = 443;
- hostAuth.mProtocol = Eas.PROTOCOL;
- hostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
- final Bundle bundle = new Bundle(2);
- bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH,
- hostAuth);
- bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
- MessagingException.NO_ERROR);
- return bundle;
- }
- }
- } finally {
- resp.close();
- }
- } catch (final IllegalArgumentException e) {
- // This happens when the domain is malformatted.
- // TODO: Fix sanitizing of the domain -- we try to in UI but apparently not correctly.
- LogUtils.e(TAG, "ISE with domain: %s", domain);
- }
- return null;
+ protected String getRequestUri() {
+ return mUri;
}
- /**
- * Get the domain of our account.
- * @return The domain of the email address.
- */
- private String getDomain() {
- final int amp = mHostAuth.mLogin.indexOf('@');
+ public static String getDomain(final String login) {
+ final int amp = login.indexOf('@');
if (amp < 0) {
return null;
}
- return mHostAuth.mLogin.substring(amp + 1);
+ return login.substring(amp + 1);
}
- /**
- * Create the payload of the request.
- * @return A {@link StringEntity} for the request XML.
- */
- private StringEntity buildRequestEntity() {
+ public static String createUri(final String domain) {
+ return "https://" + domain + AUTO_DISCOVER_PAGE;
+ }
+
+ public static String createAlternateUri(final String domain) {
+ return "https://autodiscover." + domain + AUTO_DISCOVER_PAGE;
+ }
+
+ @Override
+ protected String getCommand() {
+ return null;
+ }
+
+ @Override
+ protected HttpEntity getRequestEntity() throws IOException, MessageInvalidException {
try {
final XmlSerializer s = Xml.newSerializer();
final ByteArrayOutputStream os = new ByteArrayOutputStream(1024);
@@ -150,7 +112,7 @@
s.startTag(null, "Autodiscover");
s.attribute(null, "xmlns", AUTO_DISCOVER_SCHEMA_PREFIX + "requestschema/2006");
s.startTag(null, "Request");
- s.startTag(null, "EMailAddress").text(mHostAuth.mLogin).endTag(null, "EMailAddress");
+ s.startTag(null, "EMailAddress").text(mUsername).endTag(null, "EMailAddress");
s.startTag(null, "AcceptableResponseSchema");
s.text(AUTO_DISCOVER_SCHEMA_PREFIX + "responseschema/2006");
s.endTag(null, "AcceptableResponseSchema");
@@ -163,76 +125,65 @@
} catch (final IllegalArgumentException e) {
} catch (final IllegalStateException e) {
}
-
return null;
}
- /**
- * Perform all requests necessary and get the server response. If the post fails or is
- * redirected, we alter the post and retry.
- * @param post The initial {@link HttpPost} for this request.
- * @param domain The domain for our account.
- * @return If this request succeeded or has an unrecoverable authentication error, an
- * {@link EasResponse} with the details. For other errors, we return null.
- */
- private EasResponse getResponse(final HttpPost post, final String domain) {
- EasResponse resp = doPost(post, true);
- if (resp == null) {
- LogUtils.d(TAG, "Error in autodiscover, trying aternate address");
- post.setURI(URI.create("https://autodiscover." + domain + AUTO_DISCOVER_PAGE));
- resp = doPost(post, true);
- }
- return resp;
+ public String getRedirectUri() {
+ return mRedirectUri;
}
- /**
- * Perform one attempt to get autodiscover information. Redirection and some authentication
- * errors are handled by recursively calls with modified host information.
- * @param post The {@link HttpPost} for this request.
- * @param canRetry Whether we can retry after an authentication failure.
- * @return If this request succeeded or has an unrecoverable authentication error, an
- * {@link EasResponse} with the details. For other errors, we return null.
- */
- private EasResponse doPost(final HttpPost post, final boolean canRetry) {
- final EasResponse resp;
- try {
- resp = executePost(post);
- } catch (final IOException e) {
- return null;
- } catch (final CertificateException e) {
- // TODO: Raise this error to the user or something
- return null;
- }
+ @Override
+ protected int handleResponse(final EasResponse response) throws
+ IOException, CommandStatusException {
+ // resp is either an authentication error, or a good response.
+ final int code = response.getStatus();
- final int code = resp.getStatus();
-
- if (resp.isRedirectError()) {
- final String loc = resp.getRedirectAddress();
+ if (response.isRedirectError()) {
+ final String loc = response.getRedirectAddress();
if (loc != null && loc.startsWith("http")) {
LogUtils.d(TAG, "Posting autodiscover to redirect: " + loc);
- redirectHostAuth(loc);
- post.setURI(URI.create(loc));
- return doPost(post, canRetry);
+ mRedirectUri = loc;
+ return RESULT_REDIRECT;
+ } else {
+ LogUtils.w(TAG, "Invalid redirect %s", loc);
+ return RESULT_FATAL_SERVER_ERROR;
}
- return null;
}
if (code == HttpStatus.SC_UNAUTHORIZED) {
- if (canRetry && mHostAuth.mLogin.contains("@")) {
- // Try again using the bare user name
- final int atSignIndex = mHostAuth.mLogin.indexOf('@');
- mHostAuth.mLogin = mHostAuth.mLogin.substring(0, atSignIndex);
- LogUtils.d(TAG, "401 received; trying username: %s", mHostAuth.mLogin);
- resetAuthorization(post);
- return doPost(post, false);
- }
+ LogUtils.w(TAG, "Autodiscover received SC_UNAUTHORIZED");
+ return RESULT_SC_UNAUTHORIZED;
} else if (code != HttpStatus.SC_OK) {
// We'll try the next address if this doesn't work
LogUtils.d(TAG, "Bad response code when posting autodiscover: %d", code);
- return null;
+ return RESULT_BAD_RESPONSE;
+ } else {
+ mHostAuth = parseAutodiscover(response);
+ if (mHostAuth != null) {
+ // Fill in the rest of the HostAuth
+ // We use the user name and password that were successful during
+ // the autodiscover process
+ mHostAuth.mLogin = mUsername;
+ mHostAuth.mPassword = mPassword;
+ // Note: there is no way we can auto-discover the proper client
+ // SSL certificate to use, if one is needed.
+ mHostAuth.mPort = 443;
+ mHostAuth.mProtocol = Eas.PROTOCOL;
+ mHostAuth.mFlags = HostAuth.FLAG_SSL | HostAuth.FLAG_AUTHENTICATE;
+ return RESULT_OK;
+ } else {
+ return RESULT_HARD_DATA_FAILURE;
+ }
}
+ }
- return resp;
+ public Bundle getResultBundle() {
+ final Bundle bundle = new Bundle(2);
+ bundle.putParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH,
+ mHostAuth);
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ RESULT_OK);
+ return bundle;
}
/**
diff --git a/src/com/android/exchange/eas/EasOperation.java b/src/com/android/exchange/eas/EasOperation.java
index aa0ad14..259a540 100644
--- a/src/com/android/exchange/eas/EasOperation.java
+++ b/src/com/android/exchange/eas/EasOperation.java
@@ -41,6 +41,7 @@
import com.android.exchange.service.EasServerConnection;
import com.android.mail.providers.UIProvider;
import com.android.mail.utils.LogUtils;
+import com.google.common.annotations.VisibleForTesting;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.HttpUriRequest;
@@ -160,6 +161,11 @@
}
}
+ @VisibleForTesting
+ public void replaceEasServerConnection(EasServerConnection connection) {
+ mConnection = connection;
+ }
+
/**
* Constructor which defers loading of account and connection info.
* @param context
@@ -230,6 +236,19 @@
return (mAccount != null);
}
+ /**
+ * Sets the account. This is for use in cases where the account is not available upon
+ * construction. This will also create the EasServerConnection.
+ * @param account
+ * @param hostAuth
+ */
+ protected void setAccount(final Account account, final HostAuth hostAuth) {
+ mAccount = account;
+ if (mAccount != null) {
+ mConnection = new EasServerConnection(mContext, mAccount, hostAuth);
+ }
+ }
+
public final long getAccountId() {
return mAccountId;
}
@@ -466,8 +485,10 @@
if (requestUri == null) {
return mConnection.makeOptions();
}
- return mConnection.makePost(requestUri, getRequestEntity(),
+
+ HttpUriRequest req = mConnection.makePost(requestUri, getRequestEntity(),
getRequestContentType(), addPolicyKeyHeaderToRequest());
+ return req;
}
/**
@@ -488,7 +509,7 @@
* Build the {@link HttpEntity} which is used to construct the POST. Typically this function
* will build the Exchange request using a {@link Serializer} and then call {@link #makeEntity}.
* If the subclass is not using a POST, then it should override this to return null.
- * @return The {@link HttpEntity} to pass to {@link EasServerConnection#makePost}.
+ * @return The {@link HttpEntity} to pass to {@link com.android.exchange.service.EasServerConnection#makePost}.
* @throws IOException
*/
protected abstract HttpEntity getRequestEntity() throws IOException, MessageInvalidException;
diff --git a/src/com/android/exchange/eas/EasSendMeetingResponse.java b/src/com/android/exchange/eas/EasSendMeetingResponse.java
new file mode 100644
index 0000000..7897bb5
--- /dev/null
+++ b/src/com/android/exchange/eas/EasSendMeetingResponse.java
@@ -0,0 +1,234 @@
+package com.android.exchange.eas;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.provider.CalendarContract;
+import android.text.TextUtils;
+
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.MeetingInfo;
+import com.android.emailcommon.mail.PackedString;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceConstants;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.EasResponse;
+import com.android.exchange.adapter.MeetingResponseParser;
+import com.android.exchange.adapter.Serializer;
+import com.android.exchange.adapter.Tags;
+import com.android.exchange.utility.CalendarUtilities;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.LogUtils;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpStatus;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.text.ParseException;
+
+public class EasSendMeetingResponse extends EasOperation {
+ public final static int RESULT_OK = 1;
+
+ private final static String TAG = LogUtils.TAG;
+
+ /** Projection for getting the server id for a mailbox. */
+ private static final String[] MAILBOX_SERVER_ID_PROJECTION = {
+ EmailContent.MailboxColumns.SERVER_ID };
+ private static final int MAILBOX_SERVER_ID_COLUMN = 0;
+
+ /** EAS protocol values for UserResponse. */
+ private static final int EAS_RESPOND_ACCEPT = 1;
+ private static final int EAS_RESPOND_TENTATIVE = 2;
+ private static final int EAS_RESPOND_DECLINE = 3;
+ /** Value to use if we get a UI response value that we can't handle. */
+ private static final int EAS_RESPOND_UNKNOWN = -1;
+
+ private final EmailContent.Message mMessage;
+ private final int mMeetingResponse;
+ private int mEasResponse;
+
+ public EasSendMeetingResponse(final Context context, final long accountId,
+ final EmailContent.Message message, final int meetingResponse) {
+ super(context, accountId);
+ mMessage = message;
+ mMeetingResponse = meetingResponse;
+ }
+
+ /**
+ * Translate from {@link com.android.mail.providers.UIProvider.MessageOperations} constants to
+ * EAS values. They're currently identical but this is for future-proofing.
+ * @param messageOperationResponse The response value that came from the UI.
+ * @return The EAS protocol value to use.
+ */
+ private static int messageOperationResponseToUserResponse(final int messageOperationResponse) {
+ switch (messageOperationResponse) {
+ case UIProvider.MessageOperations.RESPOND_ACCEPT:
+ return EAS_RESPOND_ACCEPT;
+ case UIProvider.MessageOperations.RESPOND_TENTATIVE:
+ return EAS_RESPOND_TENTATIVE;
+ case UIProvider.MessageOperations.RESPOND_DECLINE:
+ return EAS_RESPOND_DECLINE;
+ }
+ return EAS_RESPOND_UNKNOWN;
+ }
+
+ @Override
+ protected String getCommand() {
+ return "MeetingResponse";
+ }
+
+ @Override
+ protected HttpEntity getRequestEntity() throws IOException {
+ mEasResponse = messageOperationResponseToUserResponse(mMeetingResponse);
+ if (mEasResponse == EAS_RESPOND_UNKNOWN) {
+ LogUtils.e(TAG, "Bad response value: %d", mMeetingResponse);
+ return null;
+ }
+ final Account account = Account.restoreAccountWithId(mContext, mMessage.mAccountKey);
+ if (account == null) {
+ LogUtils.e(TAG, "Could not load account %d for message %d", mMessage.mAccountKey,
+ mMessage.mId);
+ return null;
+ }
+ final String mailboxServerId = Utility.getFirstRowString(mContext,
+ ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMessage.mMailboxKey),
+ MAILBOX_SERVER_ID_PROJECTION, null, null, null, MAILBOX_SERVER_ID_COLUMN);
+ if (mailboxServerId == null) {
+ LogUtils.e(TAG, "Could not load mailbox %d for message %d", mMessage.mMailboxKey,
+ mMessage.mId);
+ return null;
+ }
+ final HttpEntity response;
+ try {
+ response = makeResponse(mMessage, mailboxServerId, mEasResponse);
+ } catch (CertificateException e) {
+ LogUtils.e(TAG, e, "CertficateException");
+ return null;
+ }
+ return response;
+ }
+
+ private HttpEntity makeResponse(final EmailContent.Message msg, final String mailboxServerId,
+ final int easResponse)
+ throws IOException, CertificateException {
+ final Serializer s = new Serializer();
+ s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
+ s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(easResponse));
+ s.data(Tags.MREQ_COLLECTION_ID, mailboxServerId);
+ s.data(Tags.MREQ_REQ_ID, msg.mServerId);
+ s.end().end().done();
+ return makeEntity(s);
+ }
+
+ @Override
+ protected int handleResponse(final EasResponse response)
+ throws IOException, CommandStatusException {
+ final int status = response.getStatus();
+ if (status == HttpStatus.SC_OK) {
+ if (!response.isEmpty()) {
+ // TODO: Improve the parsing to actually handle error statuses.
+ new MeetingResponseParser(response.getInputStream()).parse();
+
+ if (mMessage.mMeetingInfo != null) {
+ final PackedString meetingInfo = new PackedString(mMessage.mMeetingInfo);
+ final String responseRequested =
+ meetingInfo.get(MeetingInfo.MEETING_RESPONSE_REQUESTED);
+ // If there's no tag, or a non-zero tag, we send the response mail
+ if (!"0".equals(responseRequested)) {
+ sendMeetingResponseMail(meetingInfo, mEasResponse);
+ }
+ }
+ }
+ } else if (response.isAuthError()) {
+ // TODO: Handle this gracefully.
+ //throw new EasAuthenticationException();
+ } else {
+ LogUtils.e(TAG, "Meeting response request failed, code: %d", status);
+ throw new IOException();
+ }
+ return RESULT_OK;
+ }
+
+
+ private void sendMeetingResponseMail(final PackedString meetingInfo, final int response) {
+ // This will come as "First Last" <box@server.blah>, so we use Address to
+ // parse it into parts; we only need the email address part for the ics file
+ final Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL));
+ // It shouldn't be possible, but handle it anyway
+ if (addrs.length != 1) return;
+ final String organizerEmail = addrs[0].getAddress();
+
+ final String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP);
+ final String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART);
+ final String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND);
+ if (TextUtils.isEmpty(dtStamp) || TextUtils.isEmpty(dtStart) || TextUtils.isEmpty(dtEnd)) {
+ LogUtils.w(TAG, "blank dtStamp %s dtStart %s dtEnd %s", dtStamp, dtStart, dtEnd);
+ return;
+ }
+
+ // What we're doing here is to create an Entity that looks like an Event as it would be
+ // stored by CalendarProvider
+ final ContentValues entityValues = new ContentValues(6);
+ final Entity entity = new Entity(entityValues);
+
+ // Fill in times, location, title, and organizer
+ entityValues.put("DTSTAMP",
+ CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp));
+ try {
+ entityValues.put(CalendarContract.Events.DTSTART,
+ Utility.parseEmailDateTimeToMillis(dtStart));
+ entityValues.put(CalendarContract.Events.DTEND,
+ Utility.parseEmailDateTimeToMillis(dtEnd));
+ } catch (ParseException e) {
+ LogUtils.w(TAG, "Parse error for DTSTART/DTEND tags.", e);
+ }
+ entityValues.put(CalendarContract.Events.EVENT_LOCATION,
+ meetingInfo.get(MeetingInfo.MEETING_LOCATION));
+ entityValues.put(CalendarContract.Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
+ entityValues.put(CalendarContract.Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
+ entityValues.put(CalendarContract.Events.ORGANIZER, organizerEmail);
+
+ // Add ourselves as an attendee, using our account email address
+ final ContentValues attendeeValues = new ContentValues(2);
+ attendeeValues.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
+ CalendarContract.Attendees.RELATIONSHIP_ATTENDEE);
+ attendeeValues.put(CalendarContract.Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress);
+ entity.addSubValue(CalendarContract.Attendees.CONTENT_URI, attendeeValues);
+
+ // Add the organizer
+ final ContentValues organizerValues = new ContentValues(2);
+ organizerValues.put(CalendarContract.Attendees.ATTENDEE_RELATIONSHIP,
+ CalendarContract.Attendees.RELATIONSHIP_ORGANIZER);
+ organizerValues.put(CalendarContract.Attendees.ATTENDEE_EMAIL, organizerEmail);
+ entity.addSubValue(CalendarContract.Attendees.CONTENT_URI, organizerValues);
+
+ // Create a message from the Entity we've built. The message will have fields like
+ // to, subject, date, and text filled in. There will also be an "inline" attachment
+ // which is in iCalendar format
+ final int flag;
+ switch(response) {
+ case EmailServiceConstants.MEETING_REQUEST_ACCEPTED:
+ flag = EmailContent.Message.FLAG_OUTGOING_MEETING_ACCEPT;
+ break;
+ case EmailServiceConstants.MEETING_REQUEST_DECLINED:
+ flag = EmailContent.Message.FLAG_OUTGOING_MEETING_DECLINE;
+ break;
+ case EmailServiceConstants.MEETING_REQUEST_TENTATIVE:
+ default:
+ flag = EmailContent.Message.FLAG_OUTGOING_MEETING_TENTATIVE;
+ break;
+ }
+ final EmailContent.Message outgoingMsg =
+ CalendarUtilities.createMessageForEntity(mContext, entity, flag,
+ meetingInfo.get(MeetingInfo.MEETING_UID), mAccount);
+ // Assuming we got a message back (we might not if the event has been deleted), send it
+ if (outgoingMsg != null) {
+ sendMessage(mAccount, outgoingMsg);
+ }
+ }
+}
diff --git a/src/com/android/exchange/service/EasMeetingResponder.java b/src/com/android/exchange/service/EasMeetingResponder.java
deleted file mode 100644
index f42f583..0000000
--- a/src/com/android/exchange/service/EasMeetingResponder.java
+++ /dev/null
@@ -1,240 +0,0 @@
-package com.android.exchange.service;
-
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Entity;
-import android.provider.CalendarContract.Attendees;
-import android.provider.CalendarContract.Events;
-import android.text.TextUtils;
-
-import com.android.emailcommon.mail.Address;
-import com.android.emailcommon.mail.MeetingInfo;
-import com.android.emailcommon.mail.PackedString;
-import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent.MailboxColumns;
-import com.android.emailcommon.provider.EmailContent.Message;
-import com.android.emailcommon.provider.Mailbox;
-import com.android.emailcommon.service.EmailServiceConstants;
-import com.android.emailcommon.utility.Utility;
-import com.android.exchange.Eas;
-import com.android.exchange.EasResponse;
-import com.android.exchange.adapter.MeetingResponseParser;
-import com.android.exchange.adapter.Serializer;
-import com.android.exchange.adapter.Tags;
-import com.android.exchange.utility.CalendarUtilities;
-import com.android.mail.providers.UIProvider;
-import com.android.mail.utils.LogUtils;
-
-import org.apache.http.HttpStatus;
-
-import java.io.IOException;
-import java.security.cert.CertificateException;
-import java.text.ParseException;
-
-/**
- * Responds to a meeting request, both notifying the EAS server and sending email.
- */
-public class EasMeetingResponder extends EasServerConnection {
-
- private static final String TAG = Eas.LOG_TAG;
-
- /** Projection for getting the server id for a mailbox. */
- private static final String[] MAILBOX_SERVER_ID_PROJECTION = { MailboxColumns.SERVER_ID };
- private static final int MAILBOX_SERVER_ID_COLUMN = 0;
-
- /** EAS protocol values for UserResponse. */
- private static final int EAS_RESPOND_ACCEPT = 1;
- private static final int EAS_RESPOND_TENTATIVE = 2;
- private static final int EAS_RESPOND_DECLINE = 3;
-
- /** Value to use if we get a UI response value that we can't handle. */
- private static final int EAS_RESPOND_UNKNOWN = -1;
-
- private EasMeetingResponder(final Context context, final Account account) {
- super(context, account);
- }
-
- /**
- * Translate from {@link UIProvider.MessageOperations} constants to EAS values.
- * They're currently identical but this is for future-proofing.
- * @param messageOperationResponse The response value that came from the UI.
- * @return The EAS protocol value to use.
- */
- private static int messageOperationResponseToUserResponse(final int messageOperationResponse) {
- switch (messageOperationResponse) {
- case UIProvider.MessageOperations.RESPOND_ACCEPT:
- return EAS_RESPOND_ACCEPT;
- case UIProvider.MessageOperations.RESPOND_TENTATIVE:
- return EAS_RESPOND_TENTATIVE;
- case UIProvider.MessageOperations.RESPOND_DECLINE:
- return EAS_RESPOND_DECLINE;
- }
- return EAS_RESPOND_UNKNOWN;
- }
-
- /**
- * Send the response to both the EAS server and as email (if appropriate).
- * @param context Our {@link Context}.
- * @param messageId The db id for the message containing the meeting request.
- * @param response The UI's value for the user's response to the meeting.
- */
- public static void sendMeetingResponse(final Context context, final long messageId,
- final int response) {
- final int easResponse = messageOperationResponseToUserResponse(response);
- if (easResponse == EAS_RESPOND_UNKNOWN) {
- LogUtils.e(TAG, "Bad response value: %d", response);
- return;
- }
- final Message msg = Message.restoreMessageWithId(context, messageId);
- if (msg == null) {
- LogUtils.d(TAG, "Could not load message %d", messageId);
- return;
- }
- final Account account = Account.restoreAccountWithId(context, msg.mAccountKey);
- if (account == null) {
- LogUtils.e(TAG, "Could not load account %d for message %d", msg.mAccountKey, msg.mId);
- return;
- }
- final String mailboxServerId = Utility.getFirstRowString(context,
- ContentUris.withAppendedId(Mailbox.CONTENT_URI, msg.mMailboxKey),
- MAILBOX_SERVER_ID_PROJECTION, null, null, null, MAILBOX_SERVER_ID_COLUMN);
- if (mailboxServerId == null) {
- LogUtils.e(TAG, "Could not load mailbox %d for message %d", msg.mMailboxKey, msg.mId);
- return;
- }
-
- final EasMeetingResponder responder = new EasMeetingResponder(context, account);
- try {
- responder.sendResponse(msg, mailboxServerId, easResponse);
- } catch (final IOException e) {
- LogUtils.e(TAG, "IOException: %s", e.getMessage());
- } catch (final CertificateException e) {
- LogUtils.e(TAG, "CertificateException: %s", e.getMessage());
- }
- }
-
- /**
- * Send an email response to a meeting invitation.
- * @param meetingInfo The meeting info that was extracted from the invitation message.
- * @param response The EAS value for the user's response to the meeting.
- */
- private void sendMeetingResponseMail(final PackedString meetingInfo, final int response) {
- // This will come as "First Last" <box@server.blah>, so we use Address to
- // parse it into parts; we only need the email address part for the ics file
- final Address[] addrs = Address.parse(meetingInfo.get(MeetingInfo.MEETING_ORGANIZER_EMAIL));
- // It shouldn't be possible, but handle it anyway
- if (addrs.length != 1) return;
- final String organizerEmail = addrs[0].getAddress();
-
- final String dtStamp = meetingInfo.get(MeetingInfo.MEETING_DTSTAMP);
- final String dtStart = meetingInfo.get(MeetingInfo.MEETING_DTSTART);
- final String dtEnd = meetingInfo.get(MeetingInfo.MEETING_DTEND);
- if (TextUtils.isEmpty(dtStamp) || TextUtils.isEmpty(dtStart) ||
- TextUtils.isEmpty(dtEnd)) {
- return;
- }
-
- // What we're doing here is to create an Entity that looks like an Event as it would be
- // stored by CalendarProvider
- final ContentValues entityValues = new ContentValues(6);
- final Entity entity = new Entity(entityValues);
-
- // Fill in times, location, title, and organizer
- entityValues.put("DTSTAMP",
- CalendarUtilities.convertEmailDateTimeToCalendarDateTime(dtStamp));
- try {
- entityValues.put(Events.DTSTART, Utility.parseEmailDateTimeToMillis(dtStart));
- entityValues.put(Events.DTEND, Utility.parseEmailDateTimeToMillis(dtEnd));
- } catch (ParseException e) {
- LogUtils.w(TAG, "Parse error for DTSTART/DTEND tags.", e);
- return;
- }
- entityValues.put(Events.EVENT_LOCATION, meetingInfo.get(MeetingInfo.MEETING_LOCATION));
- entityValues.put(Events.TITLE, meetingInfo.get(MeetingInfo.MEETING_TITLE));
- entityValues.put(Events.ORGANIZER, organizerEmail);
-
- // Add ourselves as an attendee, using our account email address
- final ContentValues attendeeValues = new ContentValues(2);
- attendeeValues.put(Attendees.ATTENDEE_RELATIONSHIP,
- Attendees.RELATIONSHIP_ATTENDEE);
- attendeeValues.put(Attendees.ATTENDEE_EMAIL, mAccount.mEmailAddress);
- entity.addSubValue(Attendees.CONTENT_URI, attendeeValues);
-
- // Add the organizer
- final ContentValues organizerValues = new ContentValues(2);
- organizerValues.put(Attendees.ATTENDEE_RELATIONSHIP,
- Attendees.RELATIONSHIP_ORGANIZER);
- organizerValues.put(Attendees.ATTENDEE_EMAIL, organizerEmail);
- entity.addSubValue(Attendees.CONTENT_URI, organizerValues);
-
- // Create a message from the Entity we've built. The message will have fields like
- // to, subject, date, and text filled in. There will also be an "inline" attachment
- // which is in iCalendar format
- final int flag;
- switch(response) {
- case EmailServiceConstants.MEETING_REQUEST_ACCEPTED:
- flag = Message.FLAG_OUTGOING_MEETING_ACCEPT;
- break;
- case EmailServiceConstants.MEETING_REQUEST_DECLINED:
- flag = Message.FLAG_OUTGOING_MEETING_DECLINE;
- break;
- case EmailServiceConstants.MEETING_REQUEST_TENTATIVE:
- default:
- flag = Message.FLAG_OUTGOING_MEETING_TENTATIVE;
- break;
- }
- final Message outgoingMsg =
- CalendarUtilities.createMessageForEntity(mContext, entity, flag,
- meetingInfo.get(MeetingInfo.MEETING_UID), mAccount);
- // Assuming we got a message back (we might not if the event has been deleted), send it
- if (outgoingMsg != null) {
- sendMessage(mAccount, outgoingMsg);
- }
- }
-
- /**
- * Send the response to the EAS server, and also via email if requested.
- * @param msg The email message for the meeting invitation.
- * @param mailboxServerId The server id for the mailbox that msg is in.
- * @param response The EAS value for the user's response.
- * @throws IOException
- */
- private void sendResponse(final Message msg, final String mailboxServerId, final int response)
- throws IOException, CertificateException {
- final Serializer s = new Serializer();
- s.start(Tags.MREQ_MEETING_RESPONSE).start(Tags.MREQ_REQUEST);
- s.data(Tags.MREQ_USER_RESPONSE, Integer.toString(response));
- s.data(Tags.MREQ_COLLECTION_ID, mailboxServerId);
- s.data(Tags.MREQ_REQ_ID, msg.mServerId);
- s.end().end().done();
- final EasResponse resp = sendHttpClientPost("MeetingResponse", s.toByteArray());
- try {
- final int status = resp.getStatus();
- if (status == HttpStatus.SC_OK) {
- if (!resp.isEmpty()) {
- // TODO: Improve the parsing to actually handle error statuses.
- new MeetingResponseParser(resp.getInputStream()).parse();
-
- if (msg.mMeetingInfo != null) {
- final PackedString meetingInfo = new PackedString(msg.mMeetingInfo);
- final String responseRequested =
- meetingInfo.get(MeetingInfo.MEETING_RESPONSE_REQUESTED);
- // If there's no tag, or a non-zero tag, we send the response mail
- if (!"0".equals(responseRequested)) {
- sendMeetingResponseMail(meetingInfo, response);
- }
- }
- }
- } else if (resp.isAuthError()) {
- // TODO: Handle this gracefully.
- //throw new EasAuthenticationException();
- } else {
- LogUtils.e(TAG, "Meeting response request failed, code: %d", status);
- throw new IOException();
- }
- } finally {
- resp.close();
- }
- }
-}
diff --git a/src/com/android/exchange/service/EasServerConnection.java b/src/com/android/exchange/service/EasServerConnection.java
index 2a91c41..e7f741b 100644
--- a/src/com/android/exchange/service/EasServerConnection.java
+++ b/src/com/android/exchange/service/EasServerConnection.java
@@ -137,7 +137,7 @@
private EmailClientConnectionManager mClientConnectionManager;
public EasServerConnection(final Context context, final Account account,
- final HostAuth hostAuth) {
+ final HostAuth hostAuth) {
mContext = context;
mHostAuth = hostAuth;
mAccount = account;
@@ -145,10 +145,6 @@
setProtocolVersion(account.mProtocolVersion);
}
- public EasServerConnection(final Context context, final Account account) {
- this(context, account, HostAuth.restoreHostAuthWithId(context, account.mHostAuthKeyRecv));
- }
-
protected EmailClientConnectionManager getClientConnectionManager()
throws CertificateException {
final EmailClientConnectionManager connManager =
diff --git a/src/com/android/exchange/service/EasService.java b/src/com/android/exchange/service/EasService.java
index 6b64af1..fd7b17d 100644
--- a/src/com/android/exchange/service/EasService.java
+++ b/src/com/android/exchange/service/EasService.java
@@ -31,15 +31,18 @@
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
import com.android.emailcommon.service.SearchParams;
import com.android.emailcommon.service.ServiceProxy;
import com.android.exchange.Eas;
+import com.android.exchange.eas.EasAutoDiscover;
import com.android.exchange.eas.EasFolderSync;
import com.android.exchange.eas.EasLoadAttachment;
import com.android.exchange.eas.EasOperation;
import com.android.exchange.eas.EasSearch;
+import com.android.exchange.eas.EasSendMeetingResponse;
import com.android.mail.utils.LogUtils;
import java.util.HashSet;
@@ -123,13 +126,66 @@
@Override
public void sendMeetingResponse(final long messageId, final int response) {
- LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
+ EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(EasService.this,
+ messageId);
+ if (msg == null) {
+ LogUtils.e(TAG, "Could not load message %d in sendMeetingResponse", messageId);
+ return;
+ }
+
+ final EasSendMeetingResponse operation = new EasSendMeetingResponse(EasService.this,
+ msg.mAccountKey, msg, response);
+ doOperation(operation, "IEmailService.sendMeetingResponse");
}
@Override
public Bundle autoDiscover(final String username, final String password) {
- LogUtils.d(TAG, "IEmailService.autoDiscover");
- return null;
+ final String domain = EasAutoDiscover.getDomain(username);
+ final String uri = EasAutoDiscover.createUri(domain);
+ final Bundle result = autoDiscoverInternal(uri, username, password, true);
+ final int resultCode = result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
+ if (resultCode == EasAutoDiscover.RESULT_BAD_RESPONSE) {
+ // Try the alternate uri
+ final String alternateUri = EasAutoDiscover.createAlternateUri(domain);
+ return autoDiscoverInternal(alternateUri, username, password, true);
+ } else {
+ return result;
+ }
+ }
+
+ private Bundle autoDiscoverInternal(final String uri, final String username,
+ final String password, final boolean canRetry) {
+ final EasAutoDiscover op = new EasAutoDiscover(EasService.this, uri, username, password);
+ final int result = op.performOperation();
+ if (result == EasAutoDiscover.RESULT_REDIRECT) {
+ // Try again recursively with the new uri. TODO we should limit the number of redirects.
+ final String redirectUri = op.getRedirectUri();
+ return autoDiscoverInternal(redirectUri, username, password, canRetry);
+ } else if (result == EasAutoDiscover.RESULT_SC_UNAUTHORIZED) {
+ if (canRetry && username.contains("@")) {
+ // Try again using the bare user name
+ final int atSignIndex = username.indexOf('@');
+ final String bareUsername = username.substring(0, atSignIndex);
+ LogUtils.d(TAG, "%d received; trying username: %s", result, atSignIndex);
+ // Try again recursively, but this time don't allow retries for username.
+ return autoDiscoverInternal(uri, bareUsername, password, false);
+ } else {
+ // Either we're already on our second try or the username didn't have an "@"
+ // to begin with. Either way, failure.
+ final Bundle bundle = new Bundle(1);
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ EasAutoDiscover.RESULT_OTHER_FAILURE);
+ return bundle;
+ }
+ } else if (result != EasAutoDiscover.RESULT_OK) {
+ // Return failure, we'll try again with an alternate address
+ final Bundle bundle = new Bundle(1);
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ EasAutoDiscover.RESULT_BAD_RESPONSE);
+ return bundle;
+ }
+ // Success.
+ return op.getResultBundle();
}
@Override
@@ -140,6 +196,7 @@
@Override
public void deleteAccountPIMData(final String emailAddress) {
LogUtils.d(TAG, "IEmailService.deleteAccountPIMData");
+ // TODO: remove this, move it completely to Email code.
}
};
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index bb18bb6..6534b8f 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -52,6 +52,7 @@
import com.android.emailcommon.provider.EmailContent.SyncColumns;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.service.EmailServiceProxy;
import com.android.emailcommon.service.EmailServiceStatus;
import com.android.emailcommon.service.IEmailService;
import com.android.emailcommon.service.IEmailServiceCallback;
@@ -60,9 +61,10 @@
import com.android.emailcommon.utility.IntentUtilities;
import com.android.emailcommon.utility.Utility;
import com.android.exchange.Eas;
-import com.android.exchange.R.drawable;
-import com.android.exchange.R.string;
+import com.android.exchange.R;
import com.android.exchange.adapter.PingParser;
+import com.android.exchange.eas.EasAutoDiscover;
+import com.android.exchange.eas.EasSendMeetingResponse;
import com.android.exchange.eas.EasSyncContacts;
import com.android.exchange.eas.EasSyncCalendar;
import com.android.exchange.eas.EasFolderSync;
@@ -395,11 +397,55 @@
@Override
public Bundle autoDiscover(final String username, final String password) {
- LogUtils.d(TAG, "IEmailService.autoDiscover");
- return new EasAutoDiscover(EmailSyncAdapterService.this, username, password)
- .doAutodiscover();
+ final String domain = EasAutoDiscover.getDomain(username);
+ final String uri = EasAutoDiscover.createUri(domain);
+ final Bundle result = autoDiscoverInternal(uri, username, password, true);
+ final int resultCode = result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
+ if (resultCode == EasAutoDiscover.RESULT_BAD_RESPONSE) {
+ // Try the alternate uri
+ final String alternateUri = EasAutoDiscover.createAlternateUri(domain);
+ return autoDiscoverInternal(alternateUri, username, password, true);
+ } else {
+ return result;
+ }
}
+ private Bundle autoDiscoverInternal(final String uri, final String username,
+ final String password, final boolean canRetry) {
+ EasAutoDiscover op = new EasAutoDiscover(EmailSyncAdapterService.this, uri, username, password);
+ final int result = op.performOperation();
+ if (result == EasAutoDiscover.RESULT_REDIRECT) {
+ // Try again recursively with the new uri. TODO we should limit the number of redirects.
+ String redirectUri = op.getRedirectUri();
+ return autoDiscoverInternal(redirectUri, username, password, canRetry);
+ } else if (result == EasAutoDiscover.RESULT_SC_UNAUTHORIZED) {
+ if (canRetry && username.contains("@")) {
+ // Try again using the bare user name
+ final int atSignIndex = username.indexOf('@');
+ final String bareUsername = username.substring(0, atSignIndex);
+ LogUtils.d(TAG, "%d received; trying username: %s", result, atSignIndex);
+ // Try again recursively, but this time don't allow retries for username.
+ return autoDiscoverInternal(uri, bareUsername, password, false);
+ } else {
+ // Either we're already on our second try or the username didn't have an "@"
+ // to begin with. Either way, failure.
+ final Bundle bundle = new Bundle(1);
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ EasAutoDiscover.RESULT_OTHER_FAILURE);
+ return bundle;
+ }
+ } else if (result != EasAutoDiscover.RESULT_OK) {
+ // Return failure, we'll try again with an alternate address
+ final Bundle bundle = new Bundle(1);
+ bundle.putInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE,
+ EasAutoDiscover.RESULT_BAD_RESPONSE);
+ return bundle;
+
+ } else {
+ // Success.
+ return op.getResultBundle();
+ }
+ }
@Override
public void updateFolderList(final long accountId) {
LogUtils.d(TAG, "IEmailService.updateFolderList: %d", accountId);
@@ -443,8 +489,14 @@
@Override
public void sendMeetingResponse(final long messageId, final int response) {
LogUtils.d(TAG, "IEmailService.sendMeetingResponse: %d, %d", messageId, response);
- EasMeetingResponder.sendMeetingResponse(EmailSyncAdapterService.this, messageId,
- response);
+ final EmailContent.Message msg = EmailContent.Message.restoreMessageWithId(
+ EmailSyncAdapterService.this, messageId);
+ final EasOperation op = new EasSendMeetingResponse(EmailSyncAdapterService.this,
+ msg.mAccountKey, msg, response);
+ final int result = op.performOperation();
+ if (result != EasSendMeetingResponse.RESULT_OK) {
+ LogUtils.w(TAG, "Unexpected result %d from sendMeetingResponse", result);
+ }
}
/**
@@ -932,10 +984,10 @@
0);
final Notification notification = new Builder(this)
- .setContentTitle(this.getString(string.auth_error_notification_title))
+ .setContentTitle(this.getString(R.string.auth_error_notification_title))
.setContentText(this.getString(
- string.auth_error_notification_text, accountName))
- .setSmallIcon(drawable.stat_notify_auth)
+ R.string.auth_error_notification_text, accountName))
+ .setSmallIcon(R.drawable.stat_notify_auth)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build();
diff --git a/src/com/android/exchange/utility/CalendarUtilities.java b/src/com/android/exchange/utility/CalendarUtilities.java
index 43aeb6f..176f598 100644
--- a/src/com/android/exchange/utility/CalendarUtilities.java
+++ b/src/com/android/exchange/utility/CalendarUtilities.java
@@ -1725,8 +1725,8 @@
* Create a Message for an (Event) Entity
* @param entity the Entity for the Event (as might be retrieved by CalendarProvider)
* @param messageFlag the Message.FLAG_XXX constant indicating the type of email to be sent
- * @param the unique id of this Event, or null if it can be retrieved from the Event
- * @param the user's account
+ * @param uid the unique id of this Event, or null if it can be retrieved from the Event
+ * @param account the user's account
* @return a Message with many fields pre-filled (more later)
*/
static public Message createMessageForEntity(Context context, Entity entity,