Created new Exchange2 target

This links with the emailcommon2 static library

Bug: 6308640
Change-Id: I18f3a31ce9667af24008fb98c35d11909d67f873
diff --git a/Android.mk b/Android.mk
index 842d3d9..73e21a2 100644
--- a/Android.mk
+++ b/Android.mk
@@ -15,14 +15,39 @@
 LOCAL_PATH:= $(call my-dir)
 include $(CLEAR_VARS)
 
+#
+# Exchange
+#
+
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES += $(call all-java-files-under, exchange1_src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-common com.android.emailcommon
+LOCAL_STATIC_JAVA_LIBRARIES += calendar-common
+
+LOCAL_PACKAGE_NAME := Exchange
+
+LOCAL_PROGUARD_FLAG_FILES := proguard.flags
+
+LOCAL_EMMA_COVERAGE_FILTER += +com.android.exchange.*
+
+include $(BUILD_PACKAGE)
+
+include $(CLEAR_VARS)
+#
+# Exchange2
+#
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES += $(call all-java-files-under, exchange2_src)
 
 LOCAL_STATIC_JAVA_LIBRARIES := android-common com.android.emailcommon2
 LOCAL_STATIC_JAVA_LIBRARIES += calendar-common
 
-LOCAL_PACKAGE_NAME := Exchange
+LOCAL_PACKAGE_NAME := Exchange2
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.flags
 
diff --git a/exchange1_src/com/android/exchange/adapter/EmailSyncAdapter.java b/exchange1_src/com/android/exchange/adapter/EmailSyncAdapter.java
new file mode 100644
index 0000000..c498eb6
--- /dev/null
+++ b/exchange1_src/com/android/exchange/adapter/EmailSyncAdapter.java
@@ -0,0 +1,1458 @@
+/*
+ * Copyright (C) 2008-2009 Marc Blank
+ * Licensed to The Android Open Source Project.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.adapter;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+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.EmailContent.AccountColumns;
+import com.android.emailcommon.provider.EmailContent.Attachment;
+import com.android.emailcommon.provider.EmailContent.Body;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+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.provider.Policy;
+import com.android.emailcommon.provider.ProviderUnavailableException;
+import com.android.emailcommon.service.SyncWindow;
+import com.android.emailcommon.utility.AttachmentUtilities;
+import com.android.emailcommon.utility.ConversionUtilities;
+import com.android.emailcommon.utility.Utility;
+import com.android.exchange.CommandStatusException;
+import com.android.exchange.Eas;
+import com.android.exchange.EasResponse;
+import com.android.exchange.EasSyncService;
+import com.android.exchange.MessageMoveRequest;
+import com.android.exchange.R;
+import com.android.exchange.utility.CalendarUtilities;
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.ByteArrayEntity;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.TimeZone;
+
+/**
+ * Sync adapter for EAS email
+ *
+ */
+public class EmailSyncAdapter extends AbstractSyncAdapter {
+
+    private static final String TAG = "EmailSyncAdapter";
+
+    private static final int UPDATES_READ_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 String[] UPDATES_PROJECTION =
+        {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
+            MessageColumns.FLAG_FAVORITE};
+
+    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[] { Message.RECORD_ID, MessageColumns.SUBJECT };
+
+    private static final String WHERE_BODY_SOURCE_MESSAGE_KEY = Body.SOURCE_MESSAGE_KEY + "=?";
+    private static final String WHERE_MAILBOX_KEY_AND_MOVED =
+        MessageColumns.MAILBOX_KEY + "=? AND (" + MessageColumns.FLAGS + "&" +
+        EasSyncService.MESSAGE_FLAG_MOVED_MESSAGE + ")!=0";
+    private static final String[] FETCH_REQUEST_PROJECTION =
+        new String[] {EmailContent.RECORD_ID, SyncColumns.SERVER_ID};
+    private static final int FETCH_REQUEST_RECORD_ID = 0;
+    private static final int FETCH_REQUEST_SERVER_ID = 1;
+
+    private static final String EMAIL_WINDOW_SIZE = "5";
+
+    @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 String[] mBindArguments = new String[2];
+    private final String[] mBindArgument = new String[1];
+
+    @VisibleForTesting
+    ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
+    @VisibleForTesting
+    ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
+    private final ArrayList<FetchRequest> mFetchRequestList = new ArrayList<FetchRequest>();
+    private boolean mFetchNeeded = false;
+
+    // Holds the parser's value for isLooping()
+    private boolean mIsLooping = false;
+
+    // The policy (if any) for this adapter's Account
+    private final Policy mPolicy;
+
+    public EmailSyncAdapter(EasSyncService service) {
+        super(service);
+        // If we've got an account with a policy, cache it now
+        if (mAccount.mPolicyKey != 0) {
+            mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
+        } else {
+            mPolicy = null;
+        }
+    }
+
+    @Override
+    public void wipe() {
+        mContentResolver.delete(Message.CONTENT_URI,
+                Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+        mContentResolver.delete(Message.DELETED_CONTENT_URI,
+                Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+        mContentResolver.delete(Message.UPDATED_CONTENT_URI,
+                Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
+        mService.clearRequests();
+        mFetchRequestList.clear();
+        // Delete attachments...
+        AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, mMailbox.mId);
+    }
+
+    private String getEmailFilter() {
+        int syncLookback = mMailbox.mSyncLookback;
+        if (syncLookback == SyncWindow.SYNC_WINDOW_UNKNOWN
+                || mMailbox.mType == Mailbox.TYPE_INBOX) {
+            syncLookback = mAccount.mSyncLookback;
+        }
+        switch (syncLookback) {
+            case SyncWindow.SYNC_WINDOW_AUTO:
+                return Eas.FILTER_AUTO;
+            case SyncWindow.SYNC_WINDOW_1_DAY:
+                return Eas.FILTER_1_DAY;
+            case SyncWindow.SYNC_WINDOW_3_DAYS:
+                return Eas.FILTER_3_DAYS;
+            case SyncWindow.SYNC_WINDOW_1_WEEK:
+                return Eas.FILTER_1_WEEK;
+            case SyncWindow.SYNC_WINDOW_2_WEEKS:
+                return Eas.FILTER_2_WEEKS;
+            case SyncWindow.SYNC_WINDOW_1_MONTH:
+                return Eas.FILTER_1_MONTH;
+            case SyncWindow.SYNC_WINDOW_ALL:
+                return Eas.FILTER_ALL;
+            default:
+                return Eas.FILTER_1_WEEK;
+        }
+    }
+
+    /**
+     * Holder for fetch request information (record id and server id)
+     */
+    private static class FetchRequest {
+        @SuppressWarnings("unused")
+        final long messageId;
+        final String serverId;
+
+        FetchRequest(long _messageId, String _serverId) {
+            messageId = _messageId;
+            serverId = _serverId;
+        }
+    }
+
+    @Override
+    public void sendSyncOptions(Double protocolVersion, Serializer s)
+            throws IOException  {
+        mFetchRequestList.clear();
+        // Find partially loaded messages; this should typically be a rare occurrence
+        Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
+                FETCH_REQUEST_PROJECTION,
+                MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " +
+                MessageColumns.MAILBOX_KEY + "=?",
+                new String[] {Long.toString(mMailbox.mId)}, null);
+        try {
+            // Put all of these messages into a list; we'll need both id and server id
+            while (c.moveToNext()) {
+                mFetchRequestList.add(new FetchRequest(c.getLong(FETCH_REQUEST_RECORD_ID),
+                        c.getString(FETCH_REQUEST_SERVER_ID)));
+            }
+        } finally {
+            c.close();
+        }
+
+        // The "empty" case is typical; we send a request for changes, and also specify a sync
+        // window, body preference type (HTML for EAS 12.0 and later; MIME for EAS 2.5), and
+        // truncation
+        // If there are fetch requests, we only want the fetches (i.e. no changes from the server)
+        // so we turn MIME support off.  Note that we are always using EAS 2.5 if there are fetch
+        // requests
+        if (mFetchRequestList.isEmpty()) {
+            // Permanently delete if in trash mailbox
+            // In Exchange 2003, deletes-as-moves tag = true; no tag = false
+            // In Exchange 2007 and up, deletes-as-moves tag is "0" (false) or "1" (true)
+            boolean isTrashMailbox = mMailbox.mType == Mailbox.TYPE_TRASH;
+            if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+                if (!isTrashMailbox) {
+                    s.tag(Tags.SYNC_DELETES_AS_MOVES);
+                }
+            } else {
+                s.data(Tags.SYNC_DELETES_AS_MOVES, isTrashMailbox ? "0" : "1");
+            }
+            s.tag(Tags.SYNC_GET_CHANGES);
+            s.data(Tags.SYNC_WINDOW_SIZE, EMAIL_WINDOW_SIZE);
+            s.start(Tags.SYNC_OPTIONS);
+            // Set the lookback appropriately (EAS calls this a "filter")
+            String filter = getEmailFilter();
+            // We shouldn't get FILTER_AUTO here, but if we do, make it something legal...
+            if (filter.equals(Eas.FILTER_AUTO)) {
+                filter = Eas.FILTER_3_DAYS;
+            }
+            s.data(Tags.SYNC_FILTER_TYPE, filter);
+            // Set the truncation amount for all classes
+            if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+                s.start(Tags.BASE_BODY_PREFERENCE);
+                // HTML for email
+                s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
+                s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
+                s.end();
+            } else {
+                // Use MIME data for EAS 2.5
+                s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_MIME);
+                s.data(Tags.SYNC_MIME_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+            }
+            s.end();
+        } else {
+            s.start(Tags.SYNC_OPTIONS);
+            // Ask for plain text, rather than MIME data.  This guarantees that we'll get a usable
+            // text body
+            s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_TEXT);
+            s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
+            s.end();
+        }
+    }
+
+    @Override
+    public boolean parse(InputStream is) throws IOException, CommandStatusException {
+        EasEmailSyncParser p = new EasEmailSyncParser(is, this);
+        mFetchNeeded = false;
+        boolean res = p.parse();
+        // Hold on to the parser's value for isLooping() to pass back to the service
+        mIsLooping = p.isLooping();
+        // If we've need a body fetch, or we've just finished one, return true in order to continue
+        if (mFetchNeeded || !mFetchRequestList.isEmpty()) {
+            return true;
+        }
+
+        // Don't check for "auto" on the initial sync
+        if (!("0".equals(mMailbox.mSyncKey))) {
+            // We've completed the first successful sync
+            if (getEmailFilter().equals(Eas.FILTER_AUTO)) {
+                getAutomaticLookback();
+             }
+        }
+
+        return res;
+    }
+
+    private void getAutomaticLookback() throws IOException {
+        // If we're using an auto lookback, check how many items in the past week
+        // TODO Make the literal ints below constants once we twiddle them a bit
+        int items = getEstimate(Eas.FILTER_1_WEEK);
+        int lookback;
+        if (items > 1050) {
+            // Over 150/day, just use one day (smallest)
+            lookback = SyncWindow.SYNC_WINDOW_1_DAY;
+        } else if (items > 350 || (items == -1)) {
+            // 50-150/day, use 3 days (150 to 450 messages synced)
+            lookback = SyncWindow.SYNC_WINDOW_3_DAYS;
+        } else if (items > 150) {
+            // 20-50/day, use 1 week (140 to 350 messages synced)
+            lookback = SyncWindow.SYNC_WINDOW_1_WEEK;
+        } else if (items > 75) {
+            // 10-25/day, use 1 week (140 to 350 messages synced)
+            lookback = SyncWindow.SYNC_WINDOW_2_WEEKS;
+        } else if (items < 5) {
+            // If there are only a couple, see if it makes sense to get everything
+            items = getEstimate(Eas.FILTER_ALL);
+            if (items >= 0 && items < 100) {
+                lookback = SyncWindow.SYNC_WINDOW_ALL;
+            } else {
+                lookback = SyncWindow.SYNC_WINDOW_1_MONTH;
+            }
+        } else {
+            lookback = SyncWindow.SYNC_WINDOW_1_MONTH;
+        }
+
+        // Limit lookback to policy limit
+        if (mAccount.mPolicyKey > 0) {
+            Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
+            if (policy != null) {
+                int maxLookback = policy.mMaxEmailLookback;
+                if (maxLookback != 0 && (lookback > policy.mMaxEmailLookback)) {
+                    lookback = policy.mMaxEmailLookback;
+                }
+            }
+        }
+
+        // Store the new lookback and persist it
+        // TODO Code similar to this is used elsewhere (e.g. MailboxSettings); try to clean this up
+        ContentValues cv = new ContentValues();
+        Uri uri;
+        if (mMailbox.mType == Mailbox.TYPE_INBOX) {
+            mAccount.mSyncLookback = lookback;
+            cv.put(AccountColumns.SYNC_LOOKBACK, lookback);
+            uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId);
+        } else {
+            mMailbox.mSyncLookback = lookback;
+            cv.put(MailboxColumns.SYNC_LOOKBACK, lookback);
+            uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId);
+        }
+        mContentResolver.update(uri, cv, null, null);
+
+        CharSequence[] windowEntries = mContext.getResources().getTextArray(
+                R.array.account_settings_mail_window_entries);
+        Log.d(TAG, "Auto lookback: " + windowEntries[lookback]);
+    }
+
+    private static class GetItemEstimateParser extends Parser {
+        @SuppressWarnings("hiding")
+        private static final String TAG = "GetItemEstimateParser";
+        private int mEstimate = -1;
+
+        public GetItemEstimateParser(InputStream in) throws IOException {
+            super(in);
+        }
+
+        @Override
+        public boolean parse() throws IOException {
+            // Loop here through the remaining xml
+            while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
+                if (tag == Tags.GIE_GET_ITEM_ESTIMATE) {
+                    parseGetItemEstimate();
+                } else {
+                    skipTag();
+                }
+            }
+            return true;
+        }
+
+        public void parseGetItemEstimate() throws IOException {
+            while (nextTag(Tags.GIE_GET_ITEM_ESTIMATE) != END) {
+                if (tag == Tags.GIE_RESPONSE) {
+                    parseResponse();
+                } else {
+                    skipTag();
+                }
+            }
+        }
+
+        public void parseResponse() throws IOException {
+            while (nextTag(Tags.GIE_RESPONSE) != END) {
+                if (tag == Tags.GIE_STATUS) {
+                    Log.d(TAG, "GIE status: " + getValue());
+                } else if (tag == Tags.GIE_COLLECTION) {
+                    parseCollection();
+                } else {
+                    skipTag();
+                }
+            }
+        }
+
+        public void parseCollection() throws IOException {
+            while (nextTag(Tags.GIE_COLLECTION) != END) {
+                if (tag == Tags.GIE_CLASS) {
+                    Log.d(TAG, "GIE class: " + getValue());
+                } else if (tag == Tags.GIE_COLLECTION_ID) {
+                    Log.d(TAG, "GIE collectionId: " + getValue());
+                } else if (tag == Tags.GIE_ESTIMATE) {
+                    mEstimate = getValueInt();
+                    Log.d(TAG, "GIE estimate: " + mEstimate);
+                } else {
+                    skipTag();
+                }
+            }
+        }
+    }
+
+    /**
+     * Return the estimated number of items to be synced in the current mailbox, based on the
+     * passed in filter argument
+     * @param filter an EAS "window" filter
+     * @return the estimated number of items to be synced, or -1 if unknown
+     * @throws IOException
+     */
+    private int getEstimate(String filter) throws IOException {
+        Serializer s = new Serializer();
+        boolean ex10 = mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE;
+        boolean ex03 = mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE;
+        boolean ex07 = !ex10 && !ex03;
+
+        String className = getCollectionName();
+        String syncKey = getSyncKey();
+        userLog("gie, sending ", className, " syncKey: ", syncKey);
+
+        s.start(Tags.GIE_GET_ITEM_ESTIMATE).start(Tags.GIE_COLLECTIONS);
+        s.start(Tags.GIE_COLLECTION);
+        if (ex07) {
+            // Exchange 2007 likes collection id first
+            s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
+            s.data(Tags.SYNC_FILTER_TYPE, filter);
+            s.data(Tags.SYNC_SYNC_KEY, syncKey);
+        } else if (ex03) {
+            // Exchange 2003 needs the "class" element
+            s.data(Tags.GIE_CLASS, className);
+            s.data(Tags.SYNC_SYNC_KEY, syncKey);
+            s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
+            s.data(Tags.SYNC_FILTER_TYPE, filter);
+        } else {
+            // Exchange 2010 requires the filter inside an OPTIONS container and sync key first
+            s.data(Tags.SYNC_SYNC_KEY, syncKey);
+            s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
+            s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, filter).end();
+        }
+        s.end().end().end().done(); // GIE_COLLECTION, GIE_COLLECTIONS, GIE_GET_ITEM_ESTIMATE
+
+        EasResponse resp = mService.sendHttpClientPost("GetItemEstimate",
+                new ByteArrayEntity(s.toByteArray()), EasSyncService.COMMAND_TIMEOUT);
+        try {
+            int code = resp.getStatus();
+            if (code == HttpStatus.SC_OK) {
+                if (!resp.isEmpty()) {
+                    InputStream is = resp.getInputStream();
+                    GetItemEstimateParser gieParser = new GetItemEstimateParser(is);
+                    gieParser.parse();
+                    // Return the estimated number of items
+                    return gieParser.mEstimate;
+                }
+            }
+        } finally {
+            resp.close();
+        }
+        // If we can't get an estimate, indicate this...
+        return -1;
+    }
+
+    /**
+     * Return the value of isLooping() as returned from the parser
+     */
+    @Override
+    public boolean isLooping() {
+        return mIsLooping;
+    }
+
+    @Override
+    public boolean isSyncable() {
+        return true;
+    }
+
+    public class EasEmailSyncParser extends AbstractSyncParser {
+
+        private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
+            SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
+
+        private final String mMailboxIdAsString;
+
+        private final ArrayList<Message> newEmails = new ArrayList<Message>();
+        private final ArrayList<Message> fetchedEmails = new ArrayList<Message>();
+        private final ArrayList<Long> deletedEmails = new ArrayList<Long>();
+        private final ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
+
+        public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException {
+            super(in, adapter);
+            mMailboxIdAsString = Long.toString(mMailbox.mId);
+        }
+
+        public EasEmailSyncParser(Parser parser, EmailSyncAdapter adapter) throws IOException {
+            super(parser, adapter);
+            mMailboxIdAsString = Long.toString(mMailbox.mId);
+        }
+
+        public void addData (Message msg, int endingTag) throws IOException {
+            ArrayList<Attachment> atts = new ArrayList<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 = 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 |= Message.FLAG_INCOMING_MEETING_INVITE;
+                        } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
+                            msg.mFlags |= Message.FLAG_INCOMING_MEETING_CANCEL;
+                        }
+                        break;
+                    case Tags.EMAIL_MEETING_REQUEST:
+                        meetingRequestParser(msg);
+                        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 |= Message.FLAG_REPLIED_TO;
+                        } else if (val == LAST_VERB_FORWARD) {
+                            msg.mFlags |= Message.FLAG_FORWARDED;
+                        }
+                        break;
+                    default:
+                        skipTag();
+                }
+            }
+
+            if (atts.size() > 0) {
+                msg.mAttachments = atts;
+            }
+        }
+
+        /**
+         * 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(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;
+                    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 Message addParser() throws IOException, CommandStatusException {
+            Message msg = new Message();
+            msg.mAccountKey = mAccount.mId;
+            msg.mMailboxKey = mMailbox.mId;
+            msg.mFlagLoaded = 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(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 void mimeBodyParser(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);
+                Body tempBody = new Body();
+                // updateBodyFields fills in the content fields of the Body
+                ConversionUtilities.updateBodyFields(tempBody, msg, viewables);
+                // But we need them in the message itself for handling during commit()
+                msg.mHtml = tempBody.mHtmlContent;
+                msg.mText = tempBody.mTextContent;
+            } catch (MessagingException e) {
+                // This would most likely indicate a broken stream
+                throw new IOException(e);
+            }
+        }
+
+        private void attachmentsParser(ArrayList<Attachment> atts, 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<Attachment> atts, 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)) {
+                Attachment att = new Attachment();
+                att.mEncoding = "base64";
+                att.mSize = Long.parseLong(length);
+                att.mFileName = fileName;
+                att.mLocation = location;
+                att.mMimeType = getMimeTypeFromFileName(fileName);
+                att.mAccountKey = mService.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 = 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) {
+            mBindArguments[0] = serverId;
+            mBindArguments[1] = mMailboxIdAsString;
+            Cursor c = mContentResolver.query(Message.CONTENT_URI, projection,
+                    WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments, 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, Message.LIST_PROJECTION);
+                        try {
+                            if (c.moveToFirst()) {
+                                userLog("Changing ", serverId);
+                                oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ;
+                                oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1;
+                                flags = c.getInt(Message.LIST_FLAGS_COLUMN);
+                                id = c.getLong(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 & ~(Message.FLAG_REPLIED_TO | 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 |= Message.FLAG_REPLIED_TO;
+                        } else if (val == LAST_VERB_FORWARD) {
+                            flags |= 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());
+                    incrementChangeCount();
+                } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
+                    deleteParser(deletedEmails, tag);
+                    incrementChangeCount();
+                } else if (tag == Tags.SYNC_CHANGE) {
+                    changeParser(changedEmails);
+                    incrementChangeCount();
+                } else
+                    skipTag();
+            }
+        }
+
+        /**
+         * Removed any messages with status 7 (mismatch) from the updatedIdList
+         * @param endTag the tag we end with
+         * @throws IOException
+         */
+        public void failedUpdateParser(int endTag) throws IOException {
+            // We get serverId and status in the responses
+            String serverId = null;
+            while (nextTag(endTag) != END) {
+                if (tag == Tags.SYNC_STATUS) {
+                    int status = getValueInt();
+                    if (status == 7 && serverId != null) {
+                        Cursor c = getServerIdCursor(serverId, Message.ID_COLUMN_PROJECTION);
+                        try {
+                            if (c.moveToFirst()) {
+                                Long id = c.getLong(Message.ID_PROJECTION_COLUMN);
+                                mService.userLog("Update of " + serverId + " failed; will retry");
+                                mUpdatedIdList.remove(id);
+                                mService.mUpsyncFailed = true;
+                            }
+                        } finally {
+                            c.close();
+                        }
+                    }
+                } else if (tag == Tags.SYNC_SERVER_ID) {
+                    serverId = getValue();
+                } else {
+                    skipTag();
+                }
+            }
+        }
+
+        @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) {
+                    failedUpdateParser(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
+                            mBindArguments[0] = sse.mItemId;
+                            mBindArguments[1] = mMailboxIdAsString;
+                            mContentResolver.delete(Message.CONTENT_URI,
+                                    WHERE_SERVER_ID_AND_MAILBOX_KEY, mBindArguments);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void commit() {
+            // Use a batch operation to handle the changes
+            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
+
+            for (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);
+                    mBindArgument[0] = id;
+                    ops.add(ContentProviderOperation.newUpdate(Body.CONTENT_URI)
+                            .withSelection(Body.MESSAGE_KEY + "=?", mBindArgument)
+                            .withValue(Body.TEXT_CONTENT, msg.mText)
+                            .build());
+                    ops.add(ContentProviderOperation.newUpdate(Message.CONTENT_URI)
+                            .withSelection(EmailContent.RECORD_ID + "=?", mBindArgument)
+                            .withValue(Message.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE)
+                            .build());
+                }
+            }
+
+            for (Message msg: newEmails) {
+                msg.addSaveOps(ops);
+            }
+
+            for (Long id : deletedEmails) {
+                ops.add(ContentProviderOperation.newDelete(
+                        ContentUris.withAppendedId(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(MessageColumns.FLAG_READ, change.read);
+                    }
+                    if (change.flag != null) {
+                        cv.put(MessageColumns.FLAG_FAVORITE, change.flag);
+                    }
+                    if (change.flags != null) {
+                        cv.put(MessageColumns.FLAGS, change.flags);
+                    }
+                    ops.add(ContentProviderOperation.newUpdate(
+                            ContentUris.withAppendedId(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());
+
+            // No commits if we're stopped
+            synchronized (mService.getSynchronizer()) {
+                if (mService.isStopped()) return;
+                try {
+                    mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
+                    userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
+                } 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
+                }
+            }
+        }
+    }
+
+    @Override
+    public String getCollectionName() {
+        return "Email";
+    }
+
+    private void addCleanupOps(ArrayList<ContentProviderOperation> ops) {
+        // If we've sent local deletions, clear out the deleted table
+        for (Long id: mDeletedIdList) {
+            ops.add(ContentProviderOperation.newDelete(
+                    ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
+        }
+        // And same with the updates
+        for (Long id: mUpdatedIdList) {
+            ops.add(ContentProviderOperation.newDelete(
+                    ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
+        }
+    }
+
+    @Override
+    public void cleanup() {
+        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.
+        mBindArgument[0] = Long.toString(mMailbox.mId);
+        ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI)
+                .withSelection(WHERE_MAILBOX_KEY_AND_MOVED, mBindArgument).build());
+        // If we've done deletions/updates, clean up the deleted/updated tables
+        if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) {
+            addCleanupOps(ops);
+        }
+        try {
+            mContext.getContentResolver()
+                .applyBatch(EmailContent.AUTHORITY, ops);
+        } 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
+        }
+    }
+
+    private String formatTwo(int num) {
+        if (num < 10) {
+            return "0" + (char)('0' + num);
+        } else
+            return Integer.toString(num);
+    }
+
+    /**
+     * 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)
+     */
+    public String formatDateTime(Calendar calendar) {
+        StringBuilder sb = new StringBuilder();
+        //YYYY-MM-DDTHH:MM:SS.MSSZ
+        sb.append(calendar.get(Calendar.YEAR));
+        sb.append('-');
+        sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1));
+        sb.append('-');
+        sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH)));
+        sb.append('T');
+        sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY)));
+        sb.append(':');
+        sb.append(formatTwo(calendar.get(Calendar.MINUTE)));
+        sb.append(':');
+        sb.append(formatTwo(calendar.get(Calendar.SECOND)));
+        sb.append(".000Z");
+        return sb.toString();
+    }
+
+    /**
+     * Note that messages in the deleted database preserve the message's unique id; therefore, we
+     * can utilize this id to find references to the message.  The only reference situation at this
+     * point is in the Body table; it is when sending messages via SmartForward and SmartReply
+     */
+    private boolean messageReferenced(ContentResolver cr, long id) {
+        mBindArgument[0] = Long.toString(id);
+        // See if this id is referenced in a body
+        Cursor c = cr.query(Body.CONTENT_URI, Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY,
+                mBindArgument, null);
+        try {
+            return c.moveToFirst();
+        } finally {
+            c.close();
+        }
+    }
+
+    /*private*/ /**
+     * Serialize commands to delete items from the server; as we find items to delete, add their
+     * id's to the deletedId's array
+     *
+     * @param s the Serializer we're using to create post data
+     * @param deletedIds ids whose deletions are being sent to the server
+     * @param first whether or not this is the first command being sent
+     * @return true if SYNC_COMMANDS hasn't been sent (false otherwise)
+     * @throws IOException
+     */
+    @VisibleForTesting
+    boolean sendDeletedItems(Serializer s, ArrayList<Long> deletedIds, boolean first)
+            throws IOException {
+        ContentResolver cr = mContext.getContentResolver();
+
+        // Find any of our deleted items
+        Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION,
+                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+        // We keep track of the list of deleted item id's so that we can remove them from the
+        // deleted table after the server receives our command
+        deletedIds.clear();
+        try {
+            while (c.moveToNext()) {
+                String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN);
+                // Keep going if there's no serverId
+                if (serverId == null) {
+                    continue;
+                // Also check if this message is referenced elsewhere
+                } else if (messageReferenced(cr, c.getLong(Message.CONTENT_ID_COLUMN))) {
+                    userLog("Postponing deletion of referenced message: ", serverId);
+                    continue;
+                } else if (first) {
+                    s.start(Tags.SYNC_COMMANDS);
+                    first = false;
+                }
+                // Send the command to delete this message
+                s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+                deletedIds.add(c.getLong(Message.LIST_ID_COLUMN));
+            }
+        } finally {
+            c.close();
+        }
+
+       return first;
+    }
+
+    @Override
+    public boolean sendLocalChanges(Serializer s) throws IOException {
+        ContentResolver cr = mContext.getContentResolver();
+
+        if (getSyncKey().equals("0")) {
+            return false;
+        }
+
+        // Never upsync from these folders
+        if (mMailbox.mType == Mailbox.TYPE_DRAFTS || mMailbox.mType == Mailbox.TYPE_OUTBOX) {
+            return false;
+        }
+
+        // This code is split out for unit testing purposes
+        boolean firstCommand = sendDeletedItems(s, mDeletedIdList, true);
+
+        if (!mFetchRequestList.isEmpty()) {
+            // 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)
+            if (firstCommand) {
+                s.start(Tags.SYNC_COMMANDS);
+                firstCommand = false;
+            }
+            for (FetchRequest req: mFetchRequestList) {
+                s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, req.serverId).end();
+            }
+        }
+
+        // Find our trash mailbox, since deletions will have been moved there...
+        long trashMailboxId =
+            Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
+
+        // Do the same now for updated items
+        Cursor c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION,
+                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
+
+        // We keep track of the list of updated item id's as we did above with deleted items
+        mUpdatedIdList.clear();
+        try {
+            ContentValues cv = new ContentValues();
+            while (c.moveToNext()) {
+                long id = c.getLong(Message.LIST_ID_COLUMN);
+                // Say we've handled this update
+                mUpdatedIdList.add(id);
+                // We have the id of the changed item.  But first, we have to find out its current
+                // state, since the updated table saves the opriginal state
+                Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id),
+                        UPDATES_PROJECTION, null, null, null);
+                try {
+                    // If this item no longer exists (shouldn't be possible), just move along
+                    if (!currentCursor.moveToFirst()) {
+                        continue;
+                    }
+                    // Keep going if there's no serverId
+                    String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
+                    if (serverId == null) {
+                        continue;
+                    }
+
+                    boolean flagChange = false;
+                    boolean readChange = false;
+
+                    long mailbox = currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
+                    // If the message is now in the trash folder, it has been deleted by the user
+                    if (mailbox == trashMailboxId) {
+                         if (firstCommand) {
+                            s.start(Tags.SYNC_COMMANDS);
+                            firstCommand = false;
+                        }
+                        // Send the command to delete this message
+                        s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
+                        // Mark the message as moved (so the copy will be deleted if/when the server
+                        // version is synced)
+                        int flags = c.getInt(Message.LIST_FLAGS_COLUMN);
+                        cv.put(MessageColumns.FLAGS,
+                                flags | EasSyncService.MESSAGE_FLAG_MOVED_MESSAGE);
+                        cr.update(ContentUris.withAppendedId(Message.CONTENT_URI, id), cv,
+                                null, null);
+                        continue;
+                    } else if (mailbox != c.getLong(Message.LIST_MAILBOX_KEY_COLUMN)) {
+                        // The message has moved to another mailbox; add a request for this
+                        // Note: The Sync command doesn't handle moving messages, so we need
+                        // to handle this as a "request" (similar to meeting response and
+                        // attachment load)
+                        mService.addRequest(new MessageMoveRequest(id, mailbox));
+                        // Regardless of other changes that might be made, we don't want to indicate
+                        // that this message has been updated until the move request has been
+                        // handled (without this, a crash between the flag upsync and the move
+                        // would cause the move to be lost)
+                        mUpdatedIdList.remove(id);
+                    }
+
+                    // We can only send flag changes to the server in 12.0 or later
+                    int flag = 0;
+                    if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
+                        flag = currentCursor.getInt(UPDATES_FLAG_COLUMN);
+                        if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) {
+                            flagChange = true;
+                        }
+                    }
+
+                    int read = currentCursor.getInt(UPDATES_READ_COLUMN);
+                    if (read != c.getInt(Message.LIST_READ_COLUMN)) {
+                        readChange = true;
+                    }
+
+                    if (!flagChange && !readChange) {
+                        // In this case, we've got nothing to send to the server
+                        continue;
+                    }
+
+                    if (firstCommand) {
+                        s.start(Tags.SYNC_COMMANDS);
+                        firstCommand = false;
+                    }
+                    // Send the change to "read" and "favorite" (flagged)
+                    s.start(Tags.SYNC_CHANGE)
+                        .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN))
+                        .start(Tags.SYNC_APPLICATION_DATA);
+                    if (readChange) {
+                        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 (flagChange) {
+                        if (flag != 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");
+                            long now = System.currentTimeMillis();
+                            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 + 1*WEEKS);
+                            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
+                } finally {
+                    currentCursor.close();
+                }
+            }
+        } finally {
+            c.close();
+        }
+
+        if (!firstCommand) {
+            s.end(); // SYNC_COMMANDS
+        }
+        return false;
+    }
+}
diff --git a/exchange1_src/com/android/exchange/provider/MailboxUtilities.java b/exchange1_src/com/android/exchange/provider/MailboxUtilities.java
new file mode 100644
index 0000000..6be7920
--- /dev/null
+++ b/exchange1_src/com/android/exchange/provider/MailboxUtilities.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.exchange.provider;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.util.Log;
+
+import com.android.emailcommon.Logging;
+import com.android.emailcommon.provider.Account;
+import com.android.emailcommon.provider.EmailContent.MailboxColumns;
+import com.android.emailcommon.provider.Mailbox;
+
+public class MailboxUtilities {
+    public static final String WHERE_PARENT_KEY_UNINITIALIZED =
+        "(" + MailboxColumns.PARENT_KEY + " isnull OR " + MailboxColumns.PARENT_KEY + "=" +
+        Mailbox.PARENT_KEY_UNINITIALIZED + ")";
+    // The flag we use in Account to indicate a mailbox change in progress
+    private static final int ACCOUNT_MAILBOX_CHANGE_FLAG = Account.FLAGS_SYNC_ADAPTER;
+
+    /**
+     * Recalculate a mailbox's flags and the parent key of any children
+     * @param context the caller's context
+     * @param parentCursor a cursor to a mailbox that requires fixup
+     */
+    public static void setFlagsAndChildrensParentKey(Context context, Cursor parentCursor,
+            String accountSelector) {
+        ContentResolver resolver = context.getContentResolver();
+        String[] selectionArgs = new String[1];
+        ContentValues parentValues = new ContentValues();
+        // Get the data we need first
+        long parentId = parentCursor.getLong(Mailbox.CONTENT_ID_COLUMN);
+        int parentFlags = 0;
+        int parentType = parentCursor.getInt(Mailbox.CONTENT_TYPE_COLUMN);
+        String parentServerId = parentCursor.getString(Mailbox.CONTENT_SERVER_ID_COLUMN);
+        // All email-type boxes hold mail
+        if (parentType <= Mailbox.TYPE_NOT_EMAIL) {
+            parentFlags |= Mailbox.FLAG_HOLDS_MAIL;
+        }
+        // Outbox, Drafts, and Sent don't allow mail to be moved to them
+        if (parentType == Mailbox.TYPE_MAIL || parentType == Mailbox.TYPE_TRASH ||
+                parentType == Mailbox.TYPE_JUNK || parentType == Mailbox.TYPE_INBOX) {
+            parentFlags |= Mailbox.FLAG_ACCEPTS_MOVED_MAIL;
+        }
+        // There's no concept of "append" in EAS so FLAG_ACCEPTS_APPENDED_MAIL is never used
+        // Mark parent mailboxes as parents & add parent key to children
+        // An example of a mailbox with a null serverId would be an Outbox that we create locally
+        // for hotmail accounts (which don't have a server-based Outbox)
+        if (parentServerId != null) {
+            selectionArgs[0] = parentServerId;
+            Cursor childCursor = resolver.query(Mailbox.CONTENT_URI,
+                    Mailbox.ID_PROJECTION, MailboxColumns.PARENT_SERVER_ID + "=? AND " +
+                    accountSelector, selectionArgs, null);
+            if (childCursor == null) return;
+            try {
+                while (childCursor.moveToNext()) {
+                    parentFlags |= Mailbox.FLAG_HAS_CHILDREN | Mailbox.FLAG_CHILDREN_VISIBLE;
+                    ContentValues childValues = new ContentValues();
+                    childValues.put(Mailbox.PARENT_KEY, parentId);
+                    long childId = childCursor.getLong(Mailbox.ID_PROJECTION_COLUMN);
+                    resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, childId),
+                            childValues, null, null);
+                }
+            } finally {
+                childCursor.close();
+            }
+        } else {
+            // Mark this is having no parent, so that we don't examine this mailbox again
+            parentValues.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
+            Log.w(Logging.LOG_TAG, "Mailbox with null serverId: " +
+                    parentCursor.getString(Mailbox.CONTENT_DISPLAY_NAME_COLUMN) + ", type: " +
+                    parentType);
+        }
+        // Save away updated flags and parent key (if any)
+        parentValues.put(Mailbox.FLAGS, parentFlags);
+        resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, parentId),
+                parentValues, null, null);
+    }
+
+    /**
+     * Recalculate a mailbox's flags and the parent key of any children
+     * @param context the caller's context
+     * @param accountSelector (see description below in fixupUninitializedParentKeys)
+     * @param serverId the server id of an individual mailbox
+     */
+    public static void setFlagsAndChildrensParentKey(Context context, String accountSelector,
+            String serverId) {
+        Cursor cursor = context.getContentResolver().query(Mailbox.CONTENT_URI,
+                Mailbox.CONTENT_PROJECTION, MailboxColumns.SERVER_ID + "=? AND " + accountSelector,
+                new String[] {serverId}, null);
+        if (cursor == null) return;
+        try {
+            if (cursor.moveToFirst()) {
+                setFlagsAndChildrensParentKey(context, cursor, accountSelector);
+            }
+        } finally {
+            cursor.close();
+        }
+    }
+
+    /**
+     * Given an account selector, specifying the account(s) on which to work, create the parentKey
+     * and flags for each mailbox in the account(s) that is uninitialized (parentKey = 0 or null)
+     *
+     * @param accountSelector a sqlite WHERE clause expression to be used in determining the
+     * mailboxes to be acted upon, e.g. accountKey IN (1, 2), accountKey = 12, etc.
+     */
+    public static void fixupUninitializedParentKeys(Context context, String accountSelector) {
+        // Sanity check first on our arguments
+        if (accountSelector == null) throw new IllegalArgumentException();
+        // The selection we'll use to find uninitialized parent key mailboxes
+        String noParentKeySelection = WHERE_PARENT_KEY_UNINITIALIZED + " AND " + accountSelector;
+
+        // We'll loop through mailboxes with an uninitialized parent key
+        ContentResolver resolver = context.getContentResolver();
+        Cursor noParentKeyMailboxCursor =
+                resolver.query(Mailbox.CONTENT_URI, Mailbox.CONTENT_PROJECTION,
+                        noParentKeySelection, null, null);
+        if (noParentKeyMailboxCursor == null) return;
+        try {
+            while (noParentKeyMailboxCursor.moveToNext()) {
+                setFlagsAndChildrensParentKey(context, noParentKeyMailboxCursor, accountSelector);
+                String parentServerId =
+                        noParentKeyMailboxCursor.getString(Mailbox.CONTENT_PARENT_SERVER_ID_COLUMN);
+                // Fixup the parent so that the children's parentKey is updated
+                if (parentServerId != null) {
+                    setFlagsAndChildrensParentKey(context, accountSelector, parentServerId);
+                }
+            }
+        } finally {
+            noParentKeyMailboxCursor.close();
+        }
+
+        // Any mailboxes without a parent key should have parentKey set to -1 (no parent)
+        ContentValues values = new ContentValues();
+        values.clear();
+        values.put(Mailbox.PARENT_KEY, Mailbox.NO_MAILBOX);
+        resolver.update(Mailbox.CONTENT_URI, values, noParentKeySelection, null);
+     }
+
+    private static void setAccountSyncAdapterFlag(Context context, long accountId, boolean start) {
+        Account account = Account.restoreAccountWithId(context, accountId);
+        if (account == null) return;
+        // Set temporary flag indicating state of update of mailbox list
+        ContentValues cv = new ContentValues();
+        cv.put(Account.FLAGS, start ? (account.mFlags | ACCOUNT_MAILBOX_CHANGE_FLAG) :
+            account.mFlags & ~ACCOUNT_MAILBOX_CHANGE_FLAG);
+        context.getContentResolver().update(
+                ContentUris.withAppendedId(Account.CONTENT_URI, account.mId), cv, null, null);
+    }
+
+    /**
+     * Indicate that the specified account is starting the process of changing its mailbox list
+     * @param context the caller's context
+     * @param accountId the account that is starting to change its mailbox list
+     */
+    public static void startMailboxChanges(Context context, long accountId) {
+        setAccountSyncAdapterFlag(context, accountId, true);
+    }
+
+    /**
+     * Indicate that the specified account is ending the process of changing its mailbox list
+     * @param context the caller's context
+     * @param accountId the account that is finished with changes to its mailbox list
+     */
+    public static void endMailboxChanges(Context context, long accountId) {
+        setAccountSyncAdapterFlag(context, accountId, false);
+    }
+
+    /**
+     * Check that we didn't leave the account's mailboxes in a (possibly) inconsistent state
+     * If we did, make them consistent again
+     * @param context the caller's context
+     * @param accountId the account whose mailboxes are to be checked
+     */
+    public static void checkMailboxConsistency(Context context, long accountId) {
+        // If our temporary flag is set, we were interrupted during an update
+        // First, make sure we're current (really fast w/ caching)
+        Account account = Account.restoreAccountWithId(context, accountId);
+        if (account == null) return;
+        if ((account.mFlags & ACCOUNT_MAILBOX_CHANGE_FLAG) != 0) {
+            Log.w(Logging.LOG_TAG, "Account " + account.mDisplayName +
+                    " has inconsistent mailbox data; fixing up...");
+            // Set all account mailboxes to uninitialized parent key
+            ContentValues values = new ContentValues();
+            values.put(Mailbox.PARENT_KEY, Mailbox.PARENT_KEY_UNINITIALIZED);
+            String accountSelector = Mailbox.ACCOUNT_KEY + "=" + account.mId;
+            ContentResolver resolver = context.getContentResolver();
+            resolver.update(Mailbox.CONTENT_URI, values, accountSelector, null);
+            // Fix up keys and flags
+            MailboxUtilities.fixupUninitializedParentKeys(context, accountSelector);
+            // Clear the temporary flag
+            endMailboxChanges(context, accountId);
+        }
+    }
+}
diff --git a/src/com/android/exchange/adapter/EmailSyncAdapter.java b/exchange2_src/com/android/exchange/adapter/EmailSyncAdapter.java
similarity index 100%
rename from src/com/android/exchange/adapter/EmailSyncAdapter.java
rename to exchange2_src/com/android/exchange/adapter/EmailSyncAdapter.java
diff --git a/src/com/android/exchange/provider/MailboxUtilities.java b/exchange2_src/com/android/exchange/provider/MailboxUtilities.java
similarity index 100%
rename from src/com/android/exchange/provider/MailboxUtilities.java
rename to exchange2_src/com/android/exchange/provider/MailboxUtilities.java