Move upsync from EasMailboxSyncHandler to EasSync.

Eventually EasSync should handle downsync as well, but the immediate
task is to decouple upsync from downsync in EasMailboxSyncHandler.
This move also fixes some upsync bugs.

Also put EmailSyncParser in its own file.

Bug: 10678136
Change-Id: Ie5e131a3f316176c203d275df35d3e8a8f9141c7
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/src/com/android/exchange/adapter/EmailSyncAdapter.java
index 9c5fa0c..a3af99b 100644
--- a/src/com/android/exchange/adapter/EmailSyncAdapter.java
+++ b/src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -15,6 +15,8 @@
  * limitations under the License.
  */
 
+// TODO: Deprecated, remove this file.
+
 package com.android.exchange.adapter;
 
 import android.content.ContentProviderOperation;
diff --git a/src/com/android/exchange/adapter/EmailSyncParser.java b/src/com/android/exchange/adapter/EmailSyncParser.java
new file mode 100644
index 0000000..8024b98
--- /dev/null
+++ b/src/com/android/exchange/adapter/EmailSyncParser.java
@@ -0,0 +1,830 @@
+package com.android.exchange.adapter;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.os.RemoteException;
+import android.os.TransactionTooLargeException;
+import android.provider.CalendarContract;
+import android.text.Html;
+import android.text.SpannedString;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.webkit.MimeTypeMap;
+
+import com.android.emailcommon.internet.MimeMessage;
+import com.android.emailcommon.internet.MimeUtility;
+import com.android.emailcommon.mail.Address;
+import com.android.emailcommon.mail.MeetingInfo;
+import com.android.emailcommon.mail.MessagingException;
+import com.android.emailcommon.mail.PackedString;
+import com.android.emailcommon.mail.Part;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
+import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.Policy;
+import com.android.emailcommon.provider.ProviderUnavailableException;
+import com.android.emailcommon.utility.AttachmentUtilities;
+import com.android.emailcommon.utility.ConversionUtilities;
+import com.android.emailcommon.utility.TextUtilities;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.Eas;
+import com.android.exchange.utility.CalendarUtilities;
+import com.android.mail.utils.LogUtils;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Parser for Sync on an email collection.
+ */
+public class EmailSyncParser extends AbstractSyncParser {
+    private static final String TAG = "EmailSyncParser";
+
+    private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = EmailContent.SyncColumns.SERVER_ID
+            + "=? and " + EmailContent.MessageColumns.MAILBOX_KEY + "=?";
+
+    private final String mMailboxIdAsString;
+
+    private final ArrayList<EmailContent.Message>
+            newEmails = new ArrayList<EmailContent.Message>();
+    private final ArrayList<EmailContent.Message> fetchedEmails =
+            new ArrayList<EmailContent.Message>();
+    private final ArrayList<Long> deletedEmails = new ArrayList<Long>();
+    private final ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
+
+    private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
+    private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
+    private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
+            new String[] { EmailContent.Message.RECORD_ID, EmailContent.MessageColumns.SUBJECT };
+
+    @VisibleForTesting
+    static final int LAST_VERB_REPLY = 1;
+    @VisibleForTesting
+    static final int LAST_VERB_REPLY_ALL = 2;
+    @VisibleForTesting
+    static final int LAST_VERB_FORWARD = 3;
+
+    private final Policy mPolicy;
+    private boolean mFetchNeeded = false;
+
+    private final Map<String, Integer> mMessageUpdateStatus = new HashMap();
+
+    public EmailSyncParser(final Context context, final ContentResolver resolver,
+            final InputStream in, final Mailbox mailbox, final Account account)
+            throws IOException {
+        super(context, resolver, in, mailbox, account);
+        mMailboxIdAsString = Long.toString(mMailbox.mId);
+        if (mAccount.mPolicyKey != 0) {
+            mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
+        } else {
+            mPolicy = null;
+        }
+    }
+
+    public boolean fetchNeeded() {
+        return mFetchNeeded;
+    }
+
+    public Map<String, Integer> getMessageStatuses() {
+        return mMessageUpdateStatus;
+    }
+
+    public void addData (EmailContent.Message msg, int endingTag) throws IOException {
+        ArrayList<EmailContent.Attachment> atts = new ArrayList<EmailContent.Attachment>();
+        boolean truncated = false;
+
+        while (nextTag(endingTag) != END) {
+            switch (tag) {
+                case Tags.EMAIL_ATTACHMENTS:
+                case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
+                    attachmentsParser(atts, msg);
+                    break;
+                case Tags.EMAIL_TO:
+                    msg.mTo = Address.pack(Address.parse(getValue()));
+                    break;
+                case Tags.EMAIL_FROM:
+                    Address[] froms = Address.parse(getValue());
+                    if (froms != null && froms.length > 0) {
+                        msg.mDisplayName = froms[0].toFriendly();
+                    }
+                    msg.mFrom = Address.pack(froms);
+                    break;
+                case Tags.EMAIL_CC:
+                    msg.mCc = Address.pack(Address.parse(getValue()));
+                    break;
+                case Tags.EMAIL_REPLY_TO:
+                    msg.mReplyTo = Address.pack(Address.parse(getValue()));
+                    break;
+                case Tags.EMAIL_DATE_RECEIVED:
+                    msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue());
+                    break;
+                case Tags.EMAIL_SUBJECT:
+                    msg.mSubject = getValue();
+                    break;
+                case Tags.EMAIL_READ:
+                    msg.mFlagRead = getValueInt() == 1;
+                    break;
+                case Tags.BASE_BODY:
+                    bodyParser(msg);
+                    break;
+                case Tags.EMAIL_FLAG:
+                    msg.mFlagFavorite = flagParser();
+                    break;
+                case Tags.EMAIL_MIME_TRUNCATED:
+                    truncated = getValueInt() == 1;
+                    break;
+                case Tags.EMAIL_MIME_DATA:
+                    // We get MIME data for EAS 2.5.  First we parse it, then we take the
+                    // html and/or plain text data and store it in the message
+                    if (truncated) {
+                        // If the MIME data is truncated, don't bother parsing it, because
+                        // it will take time and throw an exception anyway when EOF is reached
+                        // In this case, we will load the body separately by tagging the message
+                        // "partially loaded".
+                        // Get the data (and ignore it)
+                        getValue();
+                        userLog("Partially loaded: ", msg.mServerId);
+                        msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
+                        mFetchNeeded = true;
+                    } else {
+                        mimeBodyParser(msg, getValue());
+                    }
+                    break;
+                case Tags.EMAIL_BODY:
+                    String text = getValue();
+                    msg.mText = text;
+                    break;
+                case Tags.EMAIL_MESSAGE_CLASS:
+                    String messageClass = getValue();
+                    if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
+                        msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_INVITE;
+                    } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
+                        msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_CANCEL;
+                    }
+                    break;
+                case Tags.EMAIL_MEETING_REQUEST:
+                    meetingRequestParser(msg);
+                    break;
+                case Tags.EMAIL_THREAD_TOPIC:
+                    msg.mThreadTopic = getValue();
+                    break;
+                case Tags.RIGHTS_LICENSE:
+                    skipParser(tag);
+                    break;
+                case Tags.EMAIL2_CONVERSATION_ID:
+                    msg.mServerConversationId =
+                            Base64.encodeToString(getValueBytes(), Base64.URL_SAFE);
+                    break;
+                case Tags.EMAIL2_CONVERSATION_INDEX:
+                    // Ignore this byte array since we're not constructing a tree.
+                    getValueBytes();
+                    break;
+                case Tags.EMAIL2_LAST_VERB_EXECUTED:
+                    int val = getValueInt();
+                    if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
+                        // We aren't required to distinguish between reply and reply all here
+                        msg.mFlags |= EmailContent.Message.FLAG_REPLIED_TO;
+                    } else if (val == LAST_VERB_FORWARD) {
+                        msg.mFlags |= EmailContent.Message.FLAG_FORWARDED;
+                    }
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+
+        if (atts.size() > 0) {
+            msg.mAttachments = atts;
+        }
+
+        if ((msg.mFlags & EmailContent.Message.FLAG_INCOMING_MEETING_MASK) != 0) {
+            String text = TextUtilities.makeSnippetFromHtmlText(
+                    msg.mText != null ? msg.mText : msg.mHtml);
+            if (TextUtils.isEmpty(text)) {
+                // Create text for this invitation
+                String meetingInfo = msg.mMeetingInfo;
+                if (!TextUtils.isEmpty(meetingInfo)) {
+                    PackedString ps = new PackedString(meetingInfo);
+                    ContentValues values = new ContentValues();
+                    putFromMeeting(ps, MeetingInfo.MEETING_LOCATION, values,
+                            CalendarContract.Events.EVENT_LOCATION);
+                    String dtstart = ps.get(MeetingInfo.MEETING_DTSTART);
+                    if (!TextUtils.isEmpty(dtstart)) {
+                        long startTime = Utility.parseEmailDateTimeToMillis(dtstart);
+                        values.put(CalendarContract.Events.DTSTART, startTime);
+                    }
+                    putFromMeeting(ps, MeetingInfo.MEETING_ALL_DAY, values,
+                            CalendarContract.Events.ALL_DAY);
+                    msg.mText = CalendarUtilities.buildMessageTextFromEntityValues(
+                            mContext, values, null);
+                    msg.mHtml = Html.toHtml(new SpannedString(msg.mText));
+                }
+            }
+        }
+    }
+
+    private static void putFromMeeting(PackedString ps, String field, ContentValues values,
+            String column) {
+        String val = ps.get(field);
+        if (!TextUtils.isEmpty(val)) {
+            values.put(column, val);
+        }
+    }
+
+    /**
+     * Set up the meetingInfo field in the message with various pieces of information gleaned
+     * from MeetingRequest tags.  This information will be used later to generate an appropriate
+     * reply email if the user chooses to respond
+     * @param msg the Message being built
+     * @throws IOException
+     */
+    private void meetingRequestParser(EmailContent.Message msg) throws IOException {
+        PackedString.Builder packedString = new PackedString.Builder();
+        while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) {
+            switch (tag) {
+                case Tags.EMAIL_DTSTAMP:
+                    packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue());
+                    break;
+                case Tags.EMAIL_START_TIME:
+                    packedString.put(MeetingInfo.MEETING_DTSTART, getValue());
+                    break;
+                case Tags.EMAIL_END_TIME:
+                    packedString.put(MeetingInfo.MEETING_DTEND, getValue());
+                    break;
+                case Tags.EMAIL_ORGANIZER:
+                    packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue());
+                    break;
+                case Tags.EMAIL_LOCATION:
+                    packedString.put(MeetingInfo.MEETING_LOCATION, getValue());
+                    break;
+                case Tags.EMAIL_GLOBAL_OBJID:
+                    packedString.put(MeetingInfo.MEETING_UID,
+                            CalendarUtilities.getUidFromGlobalObjId(getValue()));
+                    break;
+                case Tags.EMAIL_CATEGORIES:
+                    skipParser(tag);
+                    break;
+                case Tags.EMAIL_RECURRENCES:
+                    recurrencesParser();
+                    break;
+                case Tags.EMAIL_RESPONSE_REQUESTED:
+                    packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue());
+                    break;
+                case Tags.EMAIL_ALL_DAY_EVENT:
+                    if (getValueInt() == 1) {
+                        packedString.put(MeetingInfo.MEETING_ALL_DAY, "1");
+                    }
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+        if (msg.mSubject != null) {
+            packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject);
+        }
+        msg.mMeetingInfo = packedString.toString();
+    }
+
+    private void recurrencesParser() throws IOException {
+        while (nextTag(Tags.EMAIL_RECURRENCES) != END) {
+            switch (tag) {
+                case Tags.EMAIL_RECURRENCE:
+                    skipParser(tag);
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+    }
+
+    /**
+     * Parse a message from the server stream.
+     * @return the parsed Message
+     * @throws IOException
+     */
+    private EmailContent.Message addParser() throws IOException, CommandStatusException {
+        EmailContent.Message msg = new EmailContent.Message();
+        msg.mAccountKey = mAccount.mId;
+        msg.mMailboxKey = mMailbox.mId;
+        msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE;
+        // Default to 1 (success) in case we don't get this tag
+        int status = 1;
+
+        while (nextTag(Tags.SYNC_ADD) != END) {
+            switch (tag) {
+                case Tags.SYNC_SERVER_ID:
+                    msg.mServerId = getValue();
+                    break;
+                case Tags.SYNC_STATUS:
+                    status = getValueInt();
+                    break;
+                case Tags.SYNC_APPLICATION_DATA:
+                    addData(msg, tag);
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+        // For sync, status 1 = success
+        if (status != 1) {
+            throw new CommandStatusException(status, msg.mServerId);
+        }
+        return msg;
+    }
+
+    // For now, we only care about the "active" state
+    private Boolean flagParser() throws IOException {
+        Boolean state = false;
+        while (nextTag(Tags.EMAIL_FLAG) != END) {
+            switch (tag) {
+                case Tags.EMAIL_FLAG_STATUS:
+                    state = getValueInt() == 2;
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+        return state;
+    }
+
+    private void bodyParser(EmailContent.Message msg) throws IOException {
+        String bodyType = Eas.BODY_PREFERENCE_TEXT;
+        String body = "";
+        while (nextTag(Tags.EMAIL_BODY) != END) {
+            switch (tag) {
+                case Tags.BASE_TYPE:
+                    bodyType = getValue();
+                    break;
+                case Tags.BASE_DATA:
+                    body = getValue();
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+        // We always ask for TEXT or HTML; there's no third option
+        if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
+            msg.mHtml = body;
+        } else {
+            msg.mText = body;
+        }
+    }
+
+    /**
+     * Parses untruncated MIME data, saving away the text parts
+     * @param msg the message we're building
+     * @param mimeData the MIME data we've received from the server
+     * @throws IOException
+     */
+    private static void mimeBodyParser(EmailContent.Message msg, String mimeData)
+            throws IOException {
+        try {
+            ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes());
+            // The constructor parses the message
+            MimeMessage mimeMessage = new MimeMessage(in);
+            // Now process body parts & attachments
+            ArrayList<Part> viewables = new ArrayList<Part>();
+            // We'll ignore the attachments, as we'll get them directly from EAS
+            ArrayList<Part> attachments = new ArrayList<Part>();
+            MimeUtility.collectParts(mimeMessage, viewables, attachments);
+            // parseBodyFields fills in the content fields of the Body
+            ConversionUtilities.BodyFieldData data =
+                    ConversionUtilities.parseBodyFields(viewables);
+            // But we need them in the message itself for handling during commit()
+            msg.setFlags(data.isQuotedReply, data.isQuotedForward);
+            msg.mSnippet = data.snippet;
+            msg.mHtml = data.htmlContent;
+            msg.mText = data.textContent;
+        } catch (MessagingException e) {
+            // This would most likely indicate a broken stream
+            throw new IOException(e);
+        }
+    }
+
+    private void attachmentsParser(ArrayList<EmailContent.Attachment> atts,
+            EmailContent.Message msg) throws IOException {
+        while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
+            switch (tag) {
+                case Tags.EMAIL_ATTACHMENT:
+                case Tags.BASE_ATTACHMENT:  // BASE_ATTACHMENT is used in EAS 12.0 and up
+                    attachmentParser(atts, msg);
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+    }
+
+    private void attachmentParser(ArrayList<EmailContent.Attachment> atts,
+            EmailContent.Message msg) throws IOException {
+        String fileName = null;
+        String length = null;
+        String location = null;
+        boolean isInline = false;
+        String contentId = null;
+
+        while (nextTag(Tags.EMAIL_ATTACHMENT) != END) {
+            switch (tag) {
+                // We handle both EAS 2.5 and 12.0+ attachments here
+                case Tags.EMAIL_DISPLAY_NAME:
+                case Tags.BASE_DISPLAY_NAME:
+                    fileName = getValue();
+                    break;
+                case Tags.EMAIL_ATT_NAME:
+                case Tags.BASE_FILE_REFERENCE:
+                    location = getValue();
+                    break;
+                case Tags.EMAIL_ATT_SIZE:
+                case Tags.BASE_ESTIMATED_DATA_SIZE:
+                    length = getValue();
+                    break;
+                case Tags.BASE_IS_INLINE:
+                    isInline = getValueInt() == 1;
+                    break;
+                case Tags.BASE_CONTENT_ID:
+                    contentId = getValue();
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+
+        if ((fileName != null) && (length != null) && (location != null)) {
+            EmailContent.Attachment att = new EmailContent.Attachment();
+            att.mEncoding = "base64";
+            att.mSize = Long.parseLong(length);
+            att.mFileName = fileName;
+            att.mLocation = location;
+            att.mMimeType = getMimeTypeFromFileName(fileName);
+            att.mAccountKey = mAccount.mId;
+            // Save away the contentId, if we've got one (for inline images); note that the
+            // EAS docs appear to be wrong about the tags used; inline images come with
+            // contentId rather than contentLocation, when sent from Ex03, Ex07, and Ex10
+            if (isInline && !TextUtils.isEmpty(contentId)) {
+                att.mContentId = contentId;
+            }
+            // Check if this attachment can't be downloaded due to an account policy
+            if (mPolicy != null) {
+                if (mPolicy.mDontAllowAttachments ||
+                        (mPolicy.mMaxAttachmentSize > 0 &&
+                                (att.mSize > mPolicy.mMaxAttachmentSize))) {
+                    att.mFlags = EmailContent.Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
+                }
+            }
+            atts.add(att);
+            msg.mFlagAttachment = true;
+        }
+    }
+
+    /**
+     * Returns an appropriate mimetype for the given file name's extension. If a mimetype
+     * cannot be determined, {@code application/<<x>>} [where @{code <<x>> is the extension,
+     * if it exists or {@code application/octet-stream}].
+     * At the moment, this is somewhat lame, since many file types aren't recognized
+     * @param fileName the file name to ponder
+     */
+    // Note: The MimeTypeMap method currently uses a very limited set of mime types
+    // A bug has been filed against this issue.
+    public String getMimeTypeFromFileName(String fileName) {
+        String mimeType;
+        int lastDot = fileName.lastIndexOf('.');
+        String extension = null;
+        if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
+            extension = fileName.substring(lastDot + 1).toLowerCase();
+        }
+        if (extension == null) {
+            // A reasonable default for now.
+            mimeType = "application/octet-stream";
+        } else {
+            mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+            if (mimeType == null) {
+                mimeType = "application/" + extension;
+            }
+        }
+        return mimeType;
+    }
+
+    private Cursor getServerIdCursor(String serverId, String[] projection) {
+        Cursor c = mContentResolver.query(EmailContent.Message.CONTENT_URI, projection,
+                WHERE_SERVER_ID_AND_MAILBOX_KEY, new String[] {serverId, mMailboxIdAsString},
+                null);
+        if (c == null) throw new ProviderUnavailableException();
+        if (c.getCount() > 1) {
+            userLog("Multiple messages with the same serverId/mailbox: " + serverId);
+        }
+        return c;
+    }
+
+    @VisibleForTesting
+    void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
+        while (nextTag(entryTag) != END) {
+            switch (tag) {
+                case Tags.SYNC_SERVER_ID:
+                    String serverId = getValue();
+                    // Find the message in this mailbox with the given serverId
+                    Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
+                    try {
+                        if (c.moveToFirst()) {
+                            deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
+                            if (Eas.USER_LOG) {
+                                userLog("Deleting ", serverId + ", "
+                                        + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
+                            }
+                        }
+                    } finally {
+                        c.close();
+                    }
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+    }
+
+    @VisibleForTesting
+    class ServerChange {
+        final long id;
+        final Boolean read;
+        final Boolean flag;
+        final Integer flags;
+
+        ServerChange(long _id, Boolean _read, Boolean _flag, Integer _flags) {
+            id = _id;
+            read = _read;
+            flag = _flag;
+            flags = _flags;
+        }
+    }
+
+    @VisibleForTesting
+    void changeParser(ArrayList<ServerChange> changes) throws IOException {
+        String serverId = null;
+        Boolean oldRead = false;
+        Boolean oldFlag = false;
+        int flags = 0;
+        long id = 0;
+        while (nextTag(Tags.SYNC_CHANGE) != END) {
+            switch (tag) {
+                case Tags.SYNC_SERVER_ID:
+                    serverId = getValue();
+                    Cursor c = getServerIdCursor(serverId, EmailContent.Message.LIST_PROJECTION);
+                    try {
+                        if (c.moveToFirst()) {
+                            userLog("Changing ", serverId);
+                            oldRead = c.getInt(EmailContent.Message.LIST_READ_COLUMN)
+                                    == EmailContent.Message.READ;
+                            oldFlag = c.getInt(EmailContent.Message.LIST_FAVORITE_COLUMN) == 1;
+                            flags = c.getInt(EmailContent.Message.LIST_FLAGS_COLUMN);
+                            id = c.getLong(EmailContent.Message.LIST_ID_COLUMN);
+                        }
+                    } finally {
+                        c.close();
+                    }
+                    break;
+                case Tags.SYNC_APPLICATION_DATA:
+                    changeApplicationDataParser(changes, oldRead, oldFlag, flags, id);
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+    }
+
+    private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
+            Boolean oldFlag, int oldFlags, long id) throws IOException {
+        Boolean read = null;
+        Boolean flag = null;
+        Integer flags = null;
+        while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
+            switch (tag) {
+                case Tags.EMAIL_READ:
+                    read = getValueInt() == 1;
+                    break;
+                case Tags.EMAIL_FLAG:
+                    flag = flagParser();
+                    break;
+                case Tags.EMAIL2_LAST_VERB_EXECUTED:
+                    int val = getValueInt();
+                    // Clear out the old replied/forward flags and add in the new flag
+                    flags = oldFlags & ~(EmailContent.Message.FLAG_REPLIED_TO
+                            | EmailContent.Message.FLAG_FORWARDED);
+                    if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
+                        // We aren't required to distinguish between reply and reply all here
+                        flags |= EmailContent.Message.FLAG_REPLIED_TO;
+                    } else if (val == LAST_VERB_FORWARD) {
+                        flags |= EmailContent.Message.FLAG_FORWARDED;
+                    }
+                    break;
+                default:
+                    skipTag();
+            }
+        }
+        // See if there are flag changes re: read, flag (favorite) or replied/forwarded
+        if (((read != null) && !oldRead.equals(read)) ||
+                ((flag != null) && !oldFlag.equals(flag)) || (flags != null)) {
+            changes.add(new ServerChange(id, read, flag, flags));
+        }
+    }
+
+    /* (non-Javadoc)
+     * @see com.android.exchange.adapter.EasContentParser#commandsParser()
+     */
+    @Override
+    public void commandsParser() throws IOException, CommandStatusException {
+        while (nextTag(Tags.SYNC_COMMANDS) != END) {
+            if (tag == Tags.SYNC_ADD) {
+                newEmails.add(addParser());
+            } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
+                deleteParser(deletedEmails, tag);
+            } else if (tag == Tags.SYNC_CHANGE) {
+                changeParser(changedEmails);
+            } else
+                skipTag();
+        }
+    }
+
+    // EAS values for status element of sync responses.
+    // TODO: Not all are used yet, but I wanted to transcribe all possible values.
+    public static final int EAS_SYNC_STATUS_SUCCESS = 1;
+    public static final int EAS_SYNC_STATUS_BAD_SYNC_KEY = 3;
+    public static final int EAS_SYNC_STATUS_PROTOCOL_ERROR = 4;
+    public static final int EAS_SYNC_STATUS_SERVER_ERROR = 5;
+    public static final int EAS_SYNC_STATUS_BAD_CLIENT_DATA = 6;
+    public static final int EAS_SYNC_STATUS_CONFLICT = 7;
+    public static final int EAS_SYNC_STATUS_OBJECT_NOT_FOUND = 8;
+    public static final int EAS_SYNC_STATUS_CANNOT_COMPLETE = 9;
+    public static final int EAS_SYNC_STATUS_FOLDER_SYNC_NEEDED = 12;
+    public static final int EAS_SYNC_STATUS_INCOMPLETE_REQUEST = 13;
+    public static final int EAS_SYNC_STATUS_BAD_HEARTBEAT_VALUE = 14;
+    public static final int EAS_SYNC_STATUS_TOO_MANY_COLLECTIONS = 15;
+    public static final int EAS_SYNC_STATUS_RETRY = 16;
+
+    public static boolean shouldRetry(final int status) {
+        return status == EAS_SYNC_STATUS_SERVER_ERROR || status == EAS_SYNC_STATUS_RETRY;
+    }
+
+    /**
+     * Parse the status for a single message update.
+     * @param endTag the tag we end with
+     * @throws IOException
+     */
+    public void messageUpdateParser(int endTag) throws IOException {
+        // We get serverId and status in the responses
+        String serverId = null;
+        int status = -1;
+        while (nextTag(endTag) != END) {
+            if (tag == Tags.SYNC_STATUS) {
+                status = getValueInt();
+            } else if (tag == Tags.SYNC_SERVER_ID) {
+                serverId = getValue();
+            } else {
+                skipTag();
+            }
+        }
+        if (serverId != null && status != -1) {
+            mMessageUpdateStatus.put(serverId, status);
+        }
+    }
+
+    @Override
+    public void responsesParser() throws IOException {
+        while (nextTag(Tags.SYNC_RESPONSES) != END) {
+            if (tag == Tags.SYNC_ADD || tag == Tags.SYNC_CHANGE || tag == Tags.SYNC_DELETE) {
+                messageUpdateParser(tag);
+            } else if (tag == Tags.SYNC_FETCH) {
+                try {
+                    fetchedEmails.add(addParser());
+                } catch (CommandStatusException sse) {
+                    if (sse.mStatus == 8) {
+                        // 8 = object not found; delete the message from EmailProvider
+                        // No other status should be seen in a fetch response, except, perhaps,
+                        // for some temporary server failure
+                        mContentResolver.delete(EmailContent.Message.CONTENT_URI,
+                                WHERE_SERVER_ID_AND_MAILBOX_KEY,
+                                new String[] {sse.mItemId, mMailboxIdAsString});
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void commit() {
+        commitImpl(0);
+    }
+
+    public void commitImpl(int tryCount) {
+        // Use a batch operation to handle the changes
+        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+        // Maximum size of message text per fetch
+        int numFetched = fetchedEmails.size();
+        int maxPerFetch = 0;
+        if (numFetched > 0 && tryCount > 0) {
+            // Educated guess that 450000 chars (900k) is ok; 600k is a killer
+            // Remember that when fetching, we're not getting any other data
+            // We'll keep trying, reducing the maximum each time
+            // Realistically, this will rarely exceed 1, and probably never 2
+            maxPerFetch = 450000 / numFetched / tryCount;
+        }
+        for (EmailContent.Message msg: fetchedEmails) {
+            // Find the original message's id (by serverId and mailbox)
+            Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
+            String id = null;
+            try {
+                if (c.moveToFirst()) {
+                    id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
+                    while (c.moveToNext()) {
+                        // This shouldn't happen, but clean up if it does
+                        Long dupId =
+                                Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN));
+                        userLog("Delete duplicate with id: " + dupId);
+                        deletedEmails.add(dupId);
+                    }
+                }
+            } finally {
+                c.close();
+            }
+
+            // If we find one, we do two things atomically: 1) set the body text for the
+            // message, and 2) mark the message loaded (i.e. completely loaded)
+            if (id != null) {
+                userLog("Fetched body successfully for ", id);
+                final String[] bindArgument = new String[] {id};
+                if ((maxPerFetch > 0) && (msg.mText.length() > maxPerFetch)) {
+                    userLog("Truncating message to " + maxPerFetch);
+                    msg.mText = msg.mText.substring(0, maxPerFetch) + "...";
+                }
+                ops.add(ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI)
+                        .withSelection(EmailContent.Body.MESSAGE_KEY + "=?", bindArgument)
+                        .withValue(EmailContent.Body.TEXT_CONTENT, msg.mText)
+                        .build());
+                ops.add(ContentProviderOperation.newUpdate(EmailContent.Message.CONTENT_URI)
+                        .withSelection(EmailContent.RECORD_ID + "=?", bindArgument)
+                        .withValue(EmailContent.Message.FLAG_LOADED,
+                                EmailContent.Message.FLAG_LOADED_COMPLETE)
+                        .build());
+            }
+        }
+
+        for (EmailContent.Message msg: newEmails) {
+            msg.addSaveOps(ops);
+        }
+
+        for (Long id : deletedEmails) {
+            ops.add(ContentProviderOperation.newDelete(
+                    ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, id)).build());
+            AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
+        }
+
+        if (!changedEmails.isEmpty()) {
+            // Server wins in a conflict...
+            for (ServerChange change : changedEmails) {
+                ContentValues cv = new ContentValues();
+                if (change.read != null) {
+                    cv.put(EmailContent.MessageColumns.FLAG_READ, change.read);
+                }
+                if (change.flag != null) {
+                    cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, change.flag);
+                }
+                if (change.flags != null) {
+                    cv.put(EmailContent.MessageColumns.FLAGS, change.flags);
+                }
+                ops.add(ContentProviderOperation.newUpdate(
+                        ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, change.id))
+                        .withValues(cv)
+                        .build());
+            }
+        }
+
+        // We only want to update the sync key here
+        ContentValues mailboxValues = new ContentValues();
+        mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
+        ops.add(ContentProviderOperation.newUpdate(
+                ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
+                .withValues(mailboxValues).build());
+
+        try {
+            mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
+            userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
+        } catch (TransactionTooLargeException e) {
+            LogUtils.w(TAG, "Transaction failed on fetched message; retrying...");
+            commitImpl(++tryCount);
+        } catch (RemoteException e) {
+            // There is nothing to be done here; fail by returning null
+        } catch (OperationApplicationException e) {
+            // There is nothing to be done here; fail by returning null
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/exchange/eas/EasSync.java b/src/com/android/exchange/eas/EasSync.java
index 03c614b..c70386e 100644
--- a/src/com/android/exchange/eas/EasSync.java
+++ b/src/com/android/exchange/eas/EasSync.java
@@ -16,38 +16,138 @@
 
 package com.android.exchange.eas;
 
+import android.content.ContentResolver;
+import android.content.ContentUris;
 import android.content.Context;
 import android.content.SyncResult;
+import android.database.Cursor;
+import android.support.v4.util.LongSparseArray;
 import android.text.format.DateUtils;
 
 import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent;
 import com.android.emailcommon.provider.Mailbox;
+import com.android.emailcommon.provider.MessageStateChange;
+import com.android.exchange.CommandStatusException;
 import com.android.exchange.Eas;
 import com.android.exchange.EasResponse;
+import com.android.exchange.adapter.EmailSyncParser;
+import com.android.exchange.adapter.Parser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
 
 import org.apache.http.HttpEntity;
 
 import java.io.IOException;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TimeZone;
 
 /**
  * Performs an Exchange Sync operation for one {@link Mailbox}.
- * TODO: Allow multiple mailboxes in one sync.
+ * TODO: For now, only handles upsync.
+ * TODO: Handle multiple folders in one request. Not sure if parser can handle it yet.
  */
 public class EasSync extends EasOperation {
 
-    protected final Mailbox mMailbox;
-    protected boolean mInitialSync;
+    // TODO: When we handle downsync, this will become relevant.
+    private boolean mInitialSync;
 
-    public EasSync(final Context context, final Account account, final Mailbox mailbox) {
+    // State for the mailbox we're currently syncing.
+    private long mMailboxId;
+    private String mMailboxServerId;
+    private String mMailboxSyncKey;
+    private List<MessageStateChange> mStateChanges;
+    private Map<String, Integer> mMessageUpdateStatus;
+
+    public EasSync(final Context context, final Account account) {
         super(context, account);
-        mMailbox = mailbox;
         mInitialSync = false;
     }
 
-    public static final int SYNC_RESULT_SUCCESS = 0;
-    public static final int SYNC_RESULT_MORE_AVAILABLE = 1;
+    private long getMessageId(final String serverId) {
+        // TODO: Improve this.
+        for (final MessageStateChange change : mStateChanges) {
+            if (change.getServerId().equals(serverId)) {
+                return change.getMessageId();
+            }
+        }
+        return EmailContent.Message.NO_MESSAGE;
+    }
+
+    private void handleMessageUpdateStatus(final Map<String, Integer> messageStatus,
+            final long[][] messageIds, final int[] counts) {
+        for (final Map.Entry<String, Integer> entry : messageStatus.entrySet()) {
+            final String serverId = entry.getKey();
+            final int status = entry.getValue();
+            final int index;
+            if (EmailSyncParser.shouldRetry(status)) {
+                index = 1;
+            } else {
+                index = 0;
+            }
+            final long messageId = getMessageId(serverId);
+            if (messageId != EmailContent.Message.NO_MESSAGE) {
+                messageIds[index][counts[index]] = messageId;
+                ++counts[index];
+            }
+        }
+    }
+
+    /**
+     * TODO: return value doesn't do what it claims.
+     * @return Number of messages successfully synced, or -1 if we encountered an error.
+     */
+    public final int upsync(final SyncResult syncResult) {
+        final List<MessageStateChange> changes = MessageStateChange.getChanges(mContext, mAccountId,
+                        getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE);
+        if (changes == null) {
+            return 0;
+        }
+        final LongSparseArray<List<MessageStateChange>> allData =
+                MessageStateChange.convertToChangesMap(changes);
+        if (allData == null) {
+            return 0;
+        }
+
+        final long[][] messageIds = new long[2][changes.size()];
+        final int[] counts = new int[2];
+
+        for (int i = 0; i < allData.size(); ++i) {
+            mMailboxId = allData.keyAt(i);
+            mStateChanges = allData.valueAt(i);
+            final Cursor mailboxCursor = mContext.getContentResolver().query(
+                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
+                    Mailbox.ProjectionSyncData.PROJECTION, null, null, null);
+            if (mailboxCursor != null) {
+                try {
+                    if (mailboxCursor.moveToFirst()) {
+                        mMailboxServerId = mailboxCursor.getString(
+                                Mailbox.ProjectionSyncData.COLUMN_SERVER_ID);
+                        mMailboxSyncKey = mailboxCursor.getString(
+                                Mailbox.ProjectionSyncData.COLUMN_SYNC_KEY);
+                        final int result = performOperation(syncResult);
+                        if (result == 0) {
+                            handleMessageUpdateStatus(mMessageUpdateStatus, messageIds, counts);
+                        } else {
+                            // TODO: Retry or revert in this case?
+                        }
+                    }
+                } finally {
+                    mailboxCursor.close();
+                }
+            }
+        }
+
+        final ContentResolver cr = mContext.getContentResolver();
+        MessageStateChange.upsyncSuccessful(cr, messageIds[0], counts[0]);
+        MessageStateChange.upsyncRetry(cr, messageIds[1], counts[1]);
+
+        return 0;
+    }
 
     @Override
     protected String getCommand() {
@@ -59,15 +159,35 @@
         final Serializer s = new Serializer();
         s.start(Tags.SYNC_SYNC);
         s.start(Tags.SYNC_COLLECTIONS);
-        addOneCollectionToRequest(s, mMailbox);
+        addOneCollectionToRequest(s, Mailbox.TYPE_MAIL, mMailboxServerId, mMailboxSyncKey,
+                mStateChanges);
         s.end().end().done();
         return makeEntity(s);
     }
 
-
     @Override
     protected int handleResponse(final EasResponse response, final SyncResult syncResult)
             throws IOException {
+        final Account account = Account.restoreAccountWithId(mContext, mAccountId);
+        if (account == null) {
+            // TODO: Make this some other error type, since the account is just gone now.
+            return RESULT_OTHER_FAILURE;
+        }
+        final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mMailboxId);
+        if (mailbox == null) {
+            return RESULT_OTHER_FAILURE;
+        }
+        final EmailSyncParser parser = new EmailSyncParser(mContext, mContext.getContentResolver(),
+                response.getInputStream(), mailbox, account);
+        try {
+            parser.parse();
+            mMessageUpdateStatus = parser.getMessageStatuses();
+        } catch (final Parser.EmptyStreamException e) {
+            // This indicates a compressed response which was empty, which is OK.
+        } catch (final CommandStatusException e) {
+            // TODO: This is the wrong error type.
+            return RESULT_OTHER_FAILURE;
+        }
         return 0;
     }
 
@@ -79,21 +199,81 @@
         return super.getTimeout();
     }
 
-    private final void addOneCollectionToRequest(final Serializer s, final Mailbox mailbox)
-            throws IOException {
-        s.start(Tags.SYNC_COLLECTION);
-        if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
-            s.data(Tags.SYNC_CLASS, Eas.getFolderClass(mailbox.mType));
-        }
-        s.data(Tags.SYNC_SYNC_KEY, getSyncKeyForMailbox(mailbox));
-        s.data(Tags.SYNC_COLLECTION_ID, mailbox.mServerId);
-        s.end();
+    /**
+     * Create date/time in RFC8601 format.  Oddly enough, for calendar date/time, Microsoft uses
+     * a different format that excludes the punctuation (this is why I'm not putting this in a
+     * parent class)
+     */
+    private static String formatDateTime(final Calendar calendar) {
+        final StringBuilder sb = new StringBuilder();
+        //YYYY-MM-DDTHH:MM:SS.MSSZ
+        sb.append(calendar.get(Calendar.YEAR));
+        sb.append('-');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MONTH) + 1));
+        sb.append('-');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.DAY_OF_MONTH)));
+        sb.append('T');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.HOUR_OF_DAY)));
+        sb.append(':');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MINUTE)));
+        sb.append(':');
+        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.SECOND)));
+        sb.append(".000Z");
+        return sb.toString();
     }
 
-    private final String getSyncKeyForMailbox(final Mailbox mailbox) {
-        switch (mailbox.mType) {
+    private final void addOneCollectionToRequest(final Serializer s, final int collectionType,
+            final String mailboxServerId, final String mailboxSyncKey,
+            final List<MessageStateChange> stateChanges) throws IOException {
 
+        s.start(Tags.SYNC_COLLECTION);
+        if (getProtocolVersion() < Eas.SUPPORTED_PROTOCOL_EX2007_SP1_DOUBLE) {
+            s.data(Tags.SYNC_CLASS, Eas.getFolderClass(collectionType));
         }
-        return null;
+        s.data(Tags.SYNC_SYNC_KEY, mailboxSyncKey);
+        s.data(Tags.SYNC_COLLECTION_ID, mailboxServerId);
+        s.data(Tags.SYNC_GET_CHANGES, "0");
+        s.start(Tags.SYNC_COMMANDS);
+        for (final MessageStateChange change : stateChanges) {
+            s.start(Tags.SYNC_CHANGE);
+            s.data(Tags.SYNC_SERVER_ID, change.getServerId());
+            s.start(Tags.SYNC_APPLICATION_DATA);
+            final int newFlagRead = change.getNewFlagRead();
+            if (newFlagRead != MessageStateChange.VALUE_UNCHANGED) {
+                s.data(Tags.EMAIL_READ, Integer.toString(newFlagRead));
+            }
+            final int newFlagFavorite = change.getNewFlagFavorite();
+            if (newFlagFavorite != MessageStateChange.VALUE_UNCHANGED) {
+                // "Flag" is a relatively complex concept in EAS 12.0 and above.  It is not only
+                // the boolean "favorite" that we think of in Gmail, but it also represents a
+                // follow up action, which can include a subject, start and due dates, and even
+                // recurrences.  We don't support any of this as yet, but EAS 12.0 and higher
+                // require that a flag contain a status, a type, and four date fields, two each
+                // for start date and end (due) date.
+                if (newFlagFavorite != 0) {
+                    // Status 2 = set flag
+                    s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
+                    // "FollowUp" is the standard type
+                    s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
+                    final long now = System.currentTimeMillis();
+                    final Calendar calendar =
+                            GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
+                    calendar.setTimeInMillis(now);
+                    // Flags are required to have a start date and end date (duplicated)
+                    // First, we'll set the current date/time in GMT as the start time
+                    String utc = formatDateTime(calendar);
+                    s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
+                    // And then we'll use one week from today for completion date
+                    calendar.setTimeInMillis(now + DateUtils.WEEK_IN_MILLIS);
+                    utc = formatDateTime(calendar);
+                    s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
+                    s.end();
+                } else {
+                    s.tag(Tags.EMAIL_FLAG);
+                }
+            }
+            s.end().end();  // SYNC_APPLICATION_DATA, SYNC_CHANGE
+        }
+        s.end().end();  // SYNC_COMMANDS, SYNC_COLLECTION
     }
 }
diff --git a/src/com/android/exchange/service/EasMailboxSyncHandler.java b/src/com/android/exchange/service/EasMailboxSyncHandler.java
index efcb4c0..3364a78 100644
--- a/src/com/android/exchange/service/EasMailboxSyncHandler.java
+++ b/src/com/android/exchange/service/EasMailboxSyncHandler.java
@@ -1,139 +1,45 @@
 package com.android.exchange.service;
 
-import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Context;
-import android.content.OperationApplicationException;
 import android.content.SyncResult;
 import android.database.Cursor;
 import android.os.Bundle;
-import android.os.RemoteException;
-import android.text.format.DateUtils;
 
 import com.android.emailcommon.TrafficFlags;
 import com.android.emailcommon.provider.Account;
-import com.android.emailcommon.provider.EmailContent;
 import com.android.emailcommon.provider.EmailContent.Message;
 import com.android.emailcommon.provider.EmailContent.MessageColumns;
 import com.android.emailcommon.provider.EmailContent.SyncColumns;
 import com.android.emailcommon.provider.Mailbox;
 import com.android.emailcommon.service.SyncWindow;
 import com.android.exchange.Eas;
-import com.android.exchange.EasAuthenticationException;
-import com.android.exchange.EasResponse;
 import com.android.exchange.adapter.AbstractSyncParser;
-import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
-import com.android.exchange.adapter.MoveItemsParser;
+import com.android.exchange.adapter.EmailSyncParser;
 import com.android.exchange.adapter.Serializer;
 import com.android.exchange.adapter.Tags;
-import com.android.mail.utils.LogUtils;
-
-import org.apache.http.HttpStatus;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.Locale;
-import java.util.TimeZone;
 
 /**
  * Performs an Exchange mailbox sync for "normal" mailboxes.
  */
 public class EasMailboxSyncHandler extends EasSyncHandler {
-    private static final String TAG = "EasMailboxSyncHandler";
-
     /**
      * The projection used for building the fetch request list.
      */
     private static final String[] FETCH_REQUEST_PROJECTION = { SyncColumns.SERVER_ID };
     private static final int FETCH_REQUEST_SERVER_ID = 0;
 
-    /**
-     * The projection used for querying message tables for the purpose of determining what needs
-     * to be set as a sync update.
-     */
-    private static final String[] UPDATES_PROJECTION = { MessageColumns.ID,
-            MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
-            MessageColumns.FLAGS, MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE };
-    private static final int UPDATES_ID_COLUMN = 0;
-    private static final int UPDATES_MAILBOX_KEY_COLUMN = 1;
-    private static final int UPDATES_SERVER_ID_COLUMN = 2;
-    private static final int UPDATES_FLAG_COLUMN = 3;
-    private static final int UPDATES_READ_COLUMN = 4;
-    private static final int UPDATES_FAVORITE_COLUMN = 5;
-
-    /**
-     * Message flags value to signify that the message has been moved, and eventually needs to be
-     * deleted.
-     */
-    public static final int MESSAGE_FLAG_MOVED_MESSAGE = 1 << Message.FLAG_SYNC_ADAPTER_SHIFT;
-
-    /**
-     * The selection for moved messages that get deleted after a successful sync.
-     */
-    private static final String WHERE_MAILBOX_KEY_AND_MOVED =
-            MessageColumns.MAILBOX_KEY + "=? AND (" + MessageColumns.FLAGS + "&" +
-            MESSAGE_FLAG_MOVED_MESSAGE + ")!=0";
-
     private static final String EMAIL_WINDOW_SIZE = "5";
 
-    private static final String WHERE_BODY_SOURCE_MESSAGE_KEY =
-            EmailContent.Body.SOURCE_MESSAGE_KEY + "=?";
-
-    // State needed across multiple functions during a Sync.
-    // TODO: We should perhaps invert the meaning of mDeletedMessages & mUpdatedMessages and
-    // store the values we want to *retain* after a successful upsync.
-
-    /**
-     * List of message ids that were read from Message_Deletes and were sent in the Commands section
-     * of the current sync.
-     */
-    private final ArrayList<Long> mDeletedMessages = new ArrayList<Long>();
-
-    /**
-     * List of message ids that were read from Message_Updates and were sent in the Commands section
-     * of the current sync.
-     */
-    private final ArrayList<Long> mUpdatedMessages = new ArrayList<Long>();
-
     /**
      * List of server ids for messages to fetch from the server.
      */
     private final ArrayList<String> mMessagesToFetch = new ArrayList<String>();
 
-    /**
-     * Holds all the data needed to process a MoveItems request.
-     */
-    private static class MoveRequest {
-        public final long messageId;
-        public final String messageServerId;
-        public final int messageFlags;
-        public final long sourceFolderId;
-        public final String sourceFolderServerId;
-        public final String destFolderServerId;
-
-        public MoveRequest(final long _messageId, final String _messageServerId,
-                final int _messageFlags,
-                final long _sourceFolderId, final String _sourceFolderServerId,
-                final String _destFolderServerId) {
-            messageId = _messageId;
-            messageServerId = _messageServerId;
-            messageFlags = _messageFlags;
-            sourceFolderId = _sourceFolderId;
-            sourceFolderServerId = _sourceFolderServerId;
-            destFolderServerId = _destFolderServerId;
-        }
-    }
-
-    /**
-     * List of all MoveRequests, i.e. all messages which have different mailboxes than they used to.
-     */
-    private final ArrayList<MoveRequest> mMessagesToMove = new ArrayList<MoveRequest>();
-
     public EasMailboxSyncHandler(final Context context, final ContentResolver contentResolver,
             final Account account, final Mailbox mailbox, final Bundle syncExtras,
             final SyncResult syncResult) {
@@ -208,7 +114,7 @@
 
     @Override
     protected AbstractSyncParser getParser(final InputStream is) throws IOException {
-        return new EasEmailSyncParser(mContext, mContentResolver, is, mMailbox, mAccount);
+        return new EmailSyncParser(mContext, mContentResolver, is, mMailbox, mAccount);
     }
 
     @Override
@@ -268,423 +174,32 @@
     }
 
     /**
-     * Check whether a message is referenced by another (and therefore must be kept around).
-     * @param messageId The id of the message to check.
-     * @return Whether the message in question is referenced by another message.
-     */
-    private boolean messageReferenced(final long messageId) {
-        final Cursor c = mContentResolver.query(EmailContent.Body.CONTENT_URI,
-                EmailContent.Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY,
-                new String[] {Long.toString(messageId)}, null);
-        if (c != null) {
-            try {
-                return c.moveToFirst();
-            } finally {
-                c.close();
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Write the command to delete a message to the {@link Serializer}.
-     * @param s The {@link Serializer} for this sync command.
-     * @param serverId The server id for the message to delete.
-     * @param firstCommand Whether any sync commands have already been written to s.
-     * @throws IOException
-     */
-    private static void addDeleteMessageCommand(final Serializer s, final String serverId,
-            final boolean firstCommand) throws IOException {
-        if (firstCommand) {
-            s.start(Tags.SYNC_COMMANDS);
-        }
-        s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
-    }
-
-    /**
-     * Adds a sync delete command for all messages in the Message_Deletes table.
-     * @param s The {@link Serializer} for this sync command.
-     * @param hasCommands Whether any Commands have already been written to s.
-     * @return Whether this function wrote any commands to s.
-     * @throws IOException
-     */
-    private boolean addDeletedCommands(final Serializer s, final boolean hasCommands)
-            throws IOException {
-        boolean wroteCommands = false;
-        final Cursor c = mContentResolver.query(Message.DELETED_CONTENT_URI,
-                Message.ID_COLUMNS_PROJECTION, MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId,
-                null, null);
-        if (c != null) {
-            try {
-                while (c.moveToNext()) {
-                    final String serverId = c.getString(Message.ID_COLUMNS_SYNC_SERVER_ID);
-                    final long messageId = c.getLong(Message.ID_COLUMNS_ID_COLUMN);
-                    // Only upsync delete commands for messages that have server ids and are not
-                    // referenced by other messages.
-                    if (serverId != null && !messageReferenced(messageId)) {
-                        addDeleteMessageCommand(s, serverId, wroteCommands || hasCommands);
-                        wroteCommands = true;
-                        mDeletedMessages.add(messageId);
-                    }
-                }
-            } finally {
-                c.close();
-            }
-        }
-
-        return wroteCommands;
-    }
-
-    /**
-     * Create date/time in RFC8601 format.  Oddly enough, for calendar date/time, Microsoft uses
-     * a different format that excludes the punctuation (this is why I'm not putting this in a
-     * parent class)
-     */
-    private static String formatDateTime(final Calendar calendar) {
-        final StringBuilder sb = new StringBuilder();
-        //YYYY-MM-DDTHH:MM:SS.MSSZ
-        sb.append(calendar.get(Calendar.YEAR));
-        sb.append('-');
-        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MONTH) + 1));
-        sb.append('-');
-        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.DAY_OF_MONTH)));
-        sb.append('T');
-        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.HOUR_OF_DAY)));
-        sb.append(':');
-        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.MINUTE)));
-        sb.append(':');
-        sb.append(String.format(Locale.US, "%02d", calendar.get(Calendar.SECOND)));
-        sb.append(".000Z");
-        return sb.toString();
-    }
-
-    /**
-     * Get the server id for a mailbox from the content provider.
-     * @param mailboxId The id of the mailbox we're interested in.
-     * @return The server id for the mailbox.
-     */
-    private String getServerIdForMailbox(final long mailboxId) {
-        final Cursor c = mContentResolver.query(
-                ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
-                new String [] { EmailContent.MailboxColumns.SERVER_ID }, null, null, null);
-        if (c != null) {
-            try {
-                if (c.moveToNext()) {
-                    return c.getString(0);
-                }
-            } finally {
-                c.close();
-            }
-        }
-        return null;
-    }
-
-    /**
-     * For a message that's in the Message_Updates table, add a sync command to the
-     * {@link Serializer} if appropriate, and add the message to a list if it should be removed from
-     * Message_Updates.
-     * @param updatedMessageCursor The {@link Cursor} positioned at the message from Message_Updates
-     *                             that we're processing.
-     * @param s The {@link Serializer} for this sync command.
-     * @param hasCommands Whether the {@link Serializer} already has sync commands added to it.
-     * @param trashMailboxId The id for the trash mailbox.
-     * @return Whether we added a sync command to s.
-     * @throws IOException
-     */
-    private boolean handleOneUpdatedMessage(final Cursor updatedMessageCursor, final Serializer s,
-            final boolean hasCommands, final long trashMailboxId) throws IOException {
-        final long messageId = updatedMessageCursor.getLong(UPDATES_ID_COLUMN);
-        // Get the current state of this message (updated table has original state).
-        final Cursor currentCursor = mContentResolver.query(
-                ContentUris.withAppendedId(Message.CONTENT_URI, messageId),
-                UPDATES_PROJECTION, null, null, null);
-        if (currentCursor == null) {
-            // If, somehow, the message isn't still around, we still want to handle it as having
-            // been updated so that it gets removed from the updated table.
-            mUpdatedMessages.add(messageId);
-            return false;
-        }
-
-        try {
-            if (currentCursor.moveToFirst()) {
-                final String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
-                if (serverId == null) {
-                    // No serverId means there's nothing to do, but we should still remove from the
-                    // updated table.
-                    mUpdatedMessages.add(messageId);
-                    return false;
-                }
-
-                final long currentMailboxId = currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
-                final int currentFlags = currentCursor.getInt(UPDATES_FLAG_COLUMN);
-
-                // Handle message deletion (i.e. move to trash).
-                if (currentMailboxId == trashMailboxId) {
-                    mUpdatedMessages.add(messageId);
-                    addDeleteMessageCommand(s, serverId, !hasCommands);
-                    // Also mark the message as moved in the DB (so the copy will be deleted if/when
-                    // the server version is synced)
-                    final ContentValues cv = new ContentValues(1);
-                    cv.put(MessageColumns.FLAGS, currentFlags | MESSAGE_FLAG_MOVED_MESSAGE);
-                    mContentResolver.update(
-                            ContentUris.withAppendedId(Message.CONTENT_URI, messageId),
-                            cv, null, null);
-                    return true;
-                }
-
-                // Handle message moved.
-                final long originalMailboxId =
-                        updatedMessageCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
-                if (currentMailboxId != originalMailboxId) {
-                    final String sourceMailboxId = getServerIdForMailbox(originalMailboxId);
-                    final String destMailboxId;
-                    if (sourceMailboxId != null) {
-                        destMailboxId = getServerIdForMailbox(currentMailboxId);
-                    } else {
-                        destMailboxId = null;
-                    }
-                    if (destMailboxId != null) {
-                        mMessagesToMove.add(
-                                new MoveRequest(messageId, serverId, currentFlags,
-                                        originalMailboxId, sourceMailboxId, destMailboxId));
-                        // Since we don't want to remove this message from updated table until it
-                        // downsyncs, we do not add it to updatedIds.
-                    } else {
-                        // TODO: If the message's mailboxes aren't there, handle it better.
-                    }
-                } else {
-                    mUpdatedMessages.add(messageId);
-                }
-
-                final int favorite;
-                final boolean favoriteChanged;
-                // We can only send flag changes to the server in 12.0 or later
-                if (getProtocolVersion() >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
-                    favorite = currentCursor.getInt(UPDATES_FAVORITE_COLUMN);
-                    favoriteChanged =
-                            favorite != updatedMessageCursor.getInt(UPDATES_FAVORITE_COLUMN);
-                } else {
-                    favorite = 0;
-                    favoriteChanged = false;
-                }
-
-                final int read = currentCursor.getInt(UPDATES_READ_COLUMN);
-                final boolean readChanged =
-                        read != updatedMessageCursor.getInt(UPDATES_READ_COLUMN);
-
-                if (favoriteChanged || readChanged) {
-                    if (!hasCommands) {
-                        s.start(Tags.SYNC_COMMANDS);
-                    }
-                    s.start(Tags.SYNC_CHANGE);
-                    s.data(Tags.SYNC_SERVER_ID, serverId);
-                    s.start(Tags.SYNC_APPLICATION_DATA);
-                    if (readChanged) {
-                        s.data(Tags.EMAIL_READ, Integer.toString(read));
-                    }
-
-                    // "Flag" is a relatively complex concept in EAS 12.0 and above.  It is not only
-                    // the boolean "favorite" that we think of in Gmail, but it also represents a
-                    // follow up action, which can include a subject, start and due dates, and even
-                    // recurrences.  We don't support any of this as yet, but EAS 12.0 and higher
-                    // require that a flag contain a status, a type, and four date fields, two each
-                    // for start date and end (due) date.
-                    if (favoriteChanged) {
-                        if (favorite != 0) {
-                            // Status 2 = set flag
-                            s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
-                            // "FollowUp" is the standard type
-                            s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
-                            final long now = System.currentTimeMillis();
-                            final Calendar calendar =
-                                GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
-                            calendar.setTimeInMillis(now);
-                            // Flags are required to have a start date and end date (duplicated)
-                            // First, we'll set the current date/time in GMT as the start time
-                            String utc = formatDateTime(calendar);
-                            s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
-                            // And then we'll use one week from today for completion date
-                            calendar.setTimeInMillis(now + DateUtils.WEEK_IN_MILLIS);
-                            utc = formatDateTime(calendar);
-                            s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
-                            s.end();
-                        } else {
-                            s.tag(Tags.EMAIL_FLAG);
-                        }
-                    }
-                    s.end().end();  // SYNC_APPLICATION_DATA, SYNC_CHANGE
-                    return true;
-                }
-            }
-        } finally {
-            currentCursor.close();
-        }
-        return false;
-    }
-
-    /**
-     * Send all message move requests and process responses.
-     * TODO: Make this just one request/response, which requires changes to the parser.
-     * @throws IOException
-     */
-    private void performMessageMove() throws IOException {
-
-        for (final MoveRequest req : mMessagesToMove) {
-            final Serializer s = new Serializer();
-            s.start(Tags.MOVE_MOVE_ITEMS);
-            s.start(Tags.MOVE_MOVE);
-            s.data(Tags.MOVE_SRCMSGID, req.messageServerId);
-            s.data(Tags.MOVE_SRCFLDID, req.sourceFolderServerId);
-            s.data(Tags.MOVE_DSTFLDID, req.destFolderServerId);
-            s.end();
-            s.end().done();
-            final EasResponse resp = sendHttpClientPost("MoveItems", s.toByteArray());
-            try {
-                final int status = resp.getStatus();
-                if (status == HttpStatus.SC_OK) {
-                    if (!resp.isEmpty()) {
-                        final MoveItemsParser p = new MoveItemsParser(resp.getInputStream());
-                        p.parse();
-                        final int statusCode = p.getStatusCode();
-                        final ContentValues cv = new ContentValues();
-                        if (statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
-                            // Restore the old mailbox id
-                            cv.put(MessageColumns.MAILBOX_KEY, req.sourceFolderId);
-                            mContentResolver.update(
-                                    ContentUris.withAppendedId(Message.CONTENT_URI, req.messageId),
-                                    cv, null, null);
-                        } else if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS) {
-                            // Update with the new server id
-                            cv.put(SyncColumns.SERVER_ID, p.getNewServerId());
-                            cv.put(Message.FLAGS, req.messageFlags | MESSAGE_FLAG_MOVED_MESSAGE);
-                            mContentResolver.update(
-                                    ContentUris.withAppendedId(Message.CONTENT_URI, req.messageId),
-                                    cv, null, null);
-                        }
-                        if (statusCode == MoveItemsParser.STATUS_CODE_SUCCESS
-                                || statusCode == MoveItemsParser.STATUS_CODE_REVERT) {
-                            // If we revert or succeed, we no longer need the update information
-                            // OR the now-duplicate email (the new copy will be synced down)
-                            mContentResolver.delete(ContentUris.withAppendedId(
-                                    Message.UPDATED_CONTENT_URI, req.messageId), null, null);
-                        } else {
-                            // In this case, we're retrying, so do nothing.  The request will be
-                            // handled next sync
-                        }
-                    }
-                } else if (resp.isAuthError()) {
-                    throw new EasAuthenticationException();
-                } else {
-                    LogUtils.i(TAG, "Move items request failed, code: %d", status);
-                    throw new IOException();
-                }
-            } finally {
-                resp.close();
-            }
-        }
-    }
-
-    /**
-     * For each message in Message_Updates, add a sync command if appropriate, and add its id to
-     * our list of processed messages if appropriate.
-     * @param s The {@link Serializer} for this sync request.
-     * @param hasCommands Whether sync commands have already been written to s.
-     * @return Whether this function added any sync commands to s.
-     * @throws IOException
-     */
-    private boolean addUpdatedCommands(final Serializer s, final boolean hasCommands)
-            throws IOException {
-        // Find our trash mailbox, since deletions will have been moved there.
-        final long trashMailboxId =
-                Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
-        final Cursor c = mContentResolver.query(Message.UPDATED_CONTENT_URI, UPDATES_PROJECTION,
-                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
-        boolean addedCommands = false;
-        if (c != null) {
-            try {
-                while (c.moveToNext()) {
-                    addedCommands |= handleOneUpdatedMessage(c, s, hasCommands || addedCommands,
-                            trashMailboxId);
-                }
-            } finally {
-                c.close();
-            }
-        }
-
-        // mMessagesToMove is now populated. If it's non-empty, let's send the move request now.
-        if (!mMessagesToMove.isEmpty()) {
-            performMessageMove();
-        }
-
-        return addedCommands;
-    }
-
-    /**
      * Add FETCH commands for messages that need a body (i.e. we didn't find it during our earlier
      * sync; this happens only in EAS 2.5 where the body couldn't be found after parsing the
      * message's MIME data).
      * @param s The {@link Serializer} for this sync request.
-     * @param hasCommands Whether sync commands have already been written to s.
-     * @return Whether this function added any sync commands to s.
      * @throws IOException
      */
-    private boolean addFetchCommands(final Serializer s, final boolean hasCommands)
-            throws IOException {
-        if (!hasCommands && !mMessagesToFetch.isEmpty()) {
+    private void addFetchCommands(final Serializer s) throws IOException {
+        if (!mMessagesToFetch.isEmpty()) {
             s.start(Tags.SYNC_COMMANDS);
-        }
-        for (final String serverId : mMessagesToFetch) {
-            s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, serverId).end();
-        }
-
-        return !mMessagesToFetch.isEmpty();
-    }
-
-    @Override
-    protected void setUpsyncCommands(final Serializer s) throws IOException {
-        boolean addedCommands = addDeletedCommands(s, false);
-        addedCommands = addFetchCommands(s, addedCommands);
-        addedCommands = addUpdatedCommands(s, addedCommands);
-        if (addedCommands) {
+            for (final String serverId : mMessagesToFetch) {
+                s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, serverId).end();
+            }
             s.end();
         }
     }
 
     @Override
-    protected void cleanup(final int syncResult) {
-        // After a successful sync, we have things to delete from the DB.
-        if (syncResult != SYNC_RESULT_FAILED) {
-            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
-            // Delete any moved messages (since we've just synced the mailbox, and no longer need
-            // the placeholder message); this prevents duplicates from appearing in the mailbox.
-            // TODO: Verify this still makes sense.
-            ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI)
-                    .withSelection(WHERE_MAILBOX_KEY_AND_MOVED,
-                            new String[] {Long.toString(mMailbox.mId)}).build());
-            // Delete any entries in Message_Updates and Message_Deletes that were upsynced.
-            for (final long id: mDeletedMessages) {
-                ops.add(ContentProviderOperation.newDelete(
-                        ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
-            }
-            for (final long id: mUpdatedMessages) {
-                ops.add(ContentProviderOperation.newDelete(
-                        ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
-            }
-            try {
-                mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
-            } catch (final RemoteException e) {
-                // TODO: Improve handling.
-            } catch (final OperationApplicationException e) {
-                // TODO: Improve handing.
-            }
-        }
+    protected void setUpsyncCommands(final Serializer s) throws IOException {
+        addFetchCommands(s);
+    }
 
+    @Override
+    protected void cleanup(final int syncResult) {
         if (syncResult == SYNC_RESULT_MORE_AVAILABLE) {
             // Prepare our member variables for another sync request.
-            mDeletedMessages.clear();
-            mUpdatedMessages.clear();
             mMessagesToFetch.clear();
-            mMessagesToMove.clear();
         }
     }
 }
diff --git a/src/com/android/exchange/service/EmailSyncAdapterService.java b/src/com/android/exchange/service/EmailSyncAdapterService.java
index f391550..f67e362 100644
--- a/src/com/android/exchange/service/EmailSyncAdapterService.java
+++ b/src/com/android/exchange/service/EmailSyncAdapterService.java
@@ -48,6 +48,7 @@
 import com.android.exchange.eas.EasMoveItems;
 import com.android.exchange.eas.EasOperation;
 import com.android.exchange.eas.EasPing;
+import com.android.exchange.eas.EasSync;
 import com.android.mail.providers.UIProvider.AccountCapabilities;
 import com.android.mail.utils.LogUtils;
 
@@ -548,8 +549,13 @@
             // Do the bookkeeping for starting a sync, including stopping a ping if necessary.
             mSyncHandlerMap.startSync(account.mId);
 
+            // Perform email upsync for this account. Moves first, then state changes.
             EasMoveItems move = new EasMoveItems(context, account);
             move.upsyncMovedMessages(syncResult);
+            // TODO: EasSync should eventually handle both up and down; for now, it's used purely
+            // for upsync.
+            EasSync upsync = new EasSync(context, account);
+            upsync.upsync(syncResult);
 
             // TODO: Should we refresh the account here? It may have changed while waiting for any
             // pings to stop. It may not matter since the things that may have been twiddled might