1/*
2 * Copyright (C) 2008-2009 Marc Blank
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18// TODO: Deprecated, remove this file.
19
20package com.android.exchange.adapter;
21
22import android.content.ContentProviderOperation;
23import android.content.ContentResolver;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.OperationApplicationException;
28import android.database.Cursor;
29import android.net.Uri;
30import android.os.RemoteException;
31import android.os.TransactionTooLargeException;
32import android.provider.CalendarContract.Events;
33import android.text.Html;
34import android.text.SpannedString;
35import android.text.TextUtils;
36import android.util.Base64;
37import android.webkit.MimeTypeMap;
38
39import com.android.emailcommon.internet.MimeMessage;
40import com.android.emailcommon.internet.MimeUtility;
41import com.android.emailcommon.mail.Address;
42import com.android.emailcommon.mail.MeetingInfo;
43import com.android.emailcommon.mail.MessagingException;
44import com.android.emailcommon.mail.PackedString;
45import com.android.emailcommon.mail.Part;
46import com.android.emailcommon.provider.Account;
47import com.android.emailcommon.provider.EmailContent;
48import com.android.emailcommon.provider.EmailContent.AccountColumns;
49import com.android.emailcommon.provider.EmailContent.Attachment;
50import com.android.emailcommon.provider.EmailContent.Body;
51import com.android.emailcommon.provider.EmailContent.MailboxColumns;
52import com.android.emailcommon.provider.EmailContent.Message;
53import com.android.emailcommon.provider.EmailContent.MessageColumns;
54import com.android.emailcommon.provider.EmailContent.SyncColumns;
55import com.android.emailcommon.provider.Mailbox;
56import com.android.emailcommon.provider.Policy;
57import com.android.emailcommon.provider.ProviderUnavailableException;
58import com.android.emailcommon.service.SyncWindow;
59import com.android.emailcommon.utility.AttachmentUtilities;
60import com.android.emailcommon.utility.ConversionUtilities;
61import com.android.emailcommon.utility.TextUtilities;
62import com.android.emailcommon.utility.Utility;
63import com.android.exchange.CommandStatusException;
64import com.android.exchange.Eas;
65import com.android.exchange.EasResponse;
66import com.android.exchange.EasSyncService;
67import com.android.exchange.MessageMoveRequest;
68import com.android.exchange.R;
69import com.android.exchange.utility.CalendarUtilities;
70import com.android.mail.utils.LogUtils;
71import com.google.common.annotations.VisibleForTesting;
72
73import org.apache.http.HttpStatus;
74import org.apache.http.entity.ByteArrayEntity;
75
76import java.io.ByteArrayInputStream;
77import java.io.IOException;
78import java.io.InputStream;
79import java.util.ArrayList;
80import java.util.Calendar;
81import java.util.GregorianCalendar;
82import java.util.TimeZone;
83
84/**
85 * Sync adapter for EAS email
86 *
87 */
88public class EmailSyncAdapter extends AbstractSyncAdapter {
89
90    private static final String TAG = Eas.LOG_TAG;
91
92    private static final int UPDATES_READ_COLUMN = 0;
93    private static final int UPDATES_MAILBOX_KEY_COLUMN = 1;
94    private static final int UPDATES_SERVER_ID_COLUMN = 2;
95    private static final int UPDATES_FLAG_COLUMN = 3;
96    private static final String[] UPDATES_PROJECTION =
97        {MessageColumns.FLAG_READ, MessageColumns.MAILBOX_KEY, SyncColumns.SERVER_ID,
98            MessageColumns.FLAG_FAVORITE};
99
100    private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
101    private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
102    private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
103        new String[] { Message.RECORD_ID, MessageColumns.SUBJECT };
104
105    private static final String WHERE_BODY_SOURCE_MESSAGE_KEY = Body.SOURCE_MESSAGE_KEY + "=?";
106    private static final String WHERE_MAILBOX_KEY_AND_MOVED =
107        MessageColumns.MAILBOX_KEY + "=? AND (" + MessageColumns.FLAGS + "&" +
108        EasSyncService.MESSAGE_FLAG_MOVED_MESSAGE + ")!=0";
109    private static final String[] FETCH_REQUEST_PROJECTION =
110        new String[] {EmailContent.RECORD_ID, SyncColumns.SERVER_ID};
111    private static final int FETCH_REQUEST_RECORD_ID = 0;
112    private static final int FETCH_REQUEST_SERVER_ID = 1;
113
114    private static final String EMAIL_WINDOW_SIZE = "5";
115
116    @VisibleForTesting
117    static final int LAST_VERB_REPLY = 1;
118    @VisibleForTesting
119    static final int LAST_VERB_REPLY_ALL = 2;
120    @VisibleForTesting
121    static final int LAST_VERB_FORWARD = 3;
122
123    @VisibleForTesting
124    ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
125    @VisibleForTesting
126    ArrayList<Long> mUpdatedIdList = new ArrayList<Long>();
127    private final ArrayList<FetchRequest> mFetchRequestList = new ArrayList<FetchRequest>();
128
129    // Holds the parser's value for isLooping()
130    private boolean mIsLooping = false;
131
132    public EmailSyncAdapter(EasSyncService service) {
133        super(service);
134    }
135
136    @Override
137    public void wipe() {
138        mContentResolver.delete(Message.CONTENT_URI,
139                Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
140        mContentResolver.delete(Message.DELETED_CONTENT_URI,
141                Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
142        mContentResolver.delete(Message.UPDATED_CONTENT_URI,
143                Message.MAILBOX_KEY + "=" + mMailbox.mId, null);
144        mService.clearRequests();
145        mFetchRequestList.clear();
146        // Delete attachments...
147        AttachmentUtilities.deleteAllMailboxAttachmentFiles(mContext, mAccount.mId, mMailbox.mId);
148    }
149
150    private String getEmailFilter() {
151        int syncLookback = mMailbox.mSyncLookback;
152        if (syncLookback == SyncWindow.SYNC_WINDOW_ACCOUNT
153                || mMailbox.mType == Mailbox.TYPE_INBOX) {
154            syncLookback = mAccount.mSyncLookback;
155        }
156        switch (syncLookback) {
157            case SyncWindow.SYNC_WINDOW_1_DAY:
158                return Eas.FILTER_1_DAY;
159            case SyncWindow.SYNC_WINDOW_3_DAYS:
160                return Eas.FILTER_3_DAYS;
161            case SyncWindow.SYNC_WINDOW_1_WEEK:
162                return Eas.FILTER_1_WEEK;
163            case SyncWindow.SYNC_WINDOW_2_WEEKS:
164                return Eas.FILTER_2_WEEKS;
165            case SyncWindow.SYNC_WINDOW_1_MONTH:
166                return Eas.FILTER_1_MONTH;
167            case SyncWindow.SYNC_WINDOW_ALL:
168                return Eas.FILTER_ALL;
169            default:
170                // Auto window is deprecated and will also use the default.
171                return Eas.FILTER_1_WEEK;
172        }
173    }
174
175    /**
176     * Holder for fetch request information (record id and server id)
177     */
178    private static class FetchRequest {
179        @SuppressWarnings("unused")
180        final long messageId;
181        final String serverId;
182
183        FetchRequest(long _messageId, String _serverId) {
184            messageId = _messageId;
185            serverId = _serverId;
186        }
187    }
188
189    @Override
190    public void sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)
191            throws IOException  {
192        if (initialSync) return;
193        mFetchRequestList.clear();
194        // Find partially loaded messages; this should typically be a rare occurrence
195        Cursor c = mContext.getContentResolver().query(Message.CONTENT_URI,
196                FETCH_REQUEST_PROJECTION,
197                MessageColumns.FLAG_LOADED + "=" + Message.FLAG_LOADED_PARTIAL + " AND " +
198                MessageColumns.MAILBOX_KEY + "=?",
199                new String[] {Long.toString(mMailbox.mId)}, null);
200        try {
201            // Put all of these messages into a list; we'll need both id and server id
202            while (c.moveToNext()) {
203                mFetchRequestList.add(new FetchRequest(c.getLong(FETCH_REQUEST_RECORD_ID),
204                        c.getString(FETCH_REQUEST_SERVER_ID)));
205            }
206        } finally {
207            c.close();
208        }
209
210        // The "empty" case is typical; we send a request for changes, and also specify a sync
211        // window, body preference type (HTML for EAS 12.0 and later; MIME for EAS 2.5), and
212        // truncation
213        // If there are fetch requests, we only want the fetches (i.e. no changes from the server)
214        // so we turn MIME support off.  Note that we are always using EAS 2.5 if there are fetch
215        // requests
216        if (mFetchRequestList.isEmpty()) {
217            // Permanently delete if in trash mailbox
218            // In Exchange 2003, deletes-as-moves tag = true; no tag = false
219            // In Exchange 2007 and up, deletes-as-moves tag is "0" (false) or "1" (true)
220            boolean isTrashMailbox = mMailbox.mType == Mailbox.TYPE_TRASH;
221            if (protocolVersion < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
222                if (!isTrashMailbox) {
223                    s.tag(Tags.SYNC_DELETES_AS_MOVES);
224                }
225            } else {
226                s.data(Tags.SYNC_DELETES_AS_MOVES, isTrashMailbox ? "0" : "1");
227            }
228            s.tag(Tags.SYNC_GET_CHANGES);
229            s.data(Tags.SYNC_WINDOW_SIZE, EMAIL_WINDOW_SIZE);
230            s.start(Tags.SYNC_OPTIONS);
231            // Set the lookback appropriately (EAS calls this a "filter")
232            s.data(Tags.SYNC_FILTER_TYPE, getEmailFilter());
233            // Set the truncation amount for all classes
234            if (protocolVersion >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
235                s.start(Tags.BASE_BODY_PREFERENCE);
236                // HTML for email
237                s.data(Tags.BASE_TYPE, Eas.BODY_PREFERENCE_HTML);
238                s.data(Tags.BASE_TRUNCATION_SIZE, Eas.EAS12_TRUNCATION_SIZE);
239                s.end();
240            } else {
241                // Use MIME data for EAS 2.5
242                s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_MIME);
243                s.data(Tags.SYNC_MIME_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
244            }
245            s.end();
246        } else {
247            s.start(Tags.SYNC_OPTIONS);
248            // Ask for plain text, rather than MIME data.  This guarantees that we'll get a usable
249            // text body
250            s.data(Tags.SYNC_MIME_SUPPORT, Eas.MIME_BODY_PREFERENCE_TEXT);
251            s.data(Tags.SYNC_TRUNCATION, Eas.EAS2_5_TRUNCATION_SIZE);
252            s.end();
253        }
254    }
255
256    @Override
257    public boolean parse(InputStream is) throws IOException, CommandStatusException {
258        EasEmailSyncParser p = new EasEmailSyncParser(is, this);
259        boolean res = p.parse();
260        // Hold on to the parser's value for isLooping() to pass back to the service
261        mIsLooping = p.isLooping();
262        // If we've need a body fetch, or we've just finished one, return true in order to continue
263        if (p.fetchNeeded() || !mFetchRequestList.isEmpty()) {
264            return true;
265        }
266
267        return res;
268    }
269
270    /**
271     * This function is no longer used, but keeping it here in case we revive this functionality.
272     * @throws IOException
273     */
274    @Deprecated
275    private void getAutomaticLookback() throws IOException {
276        // If we're using an auto lookback, check how many items in the past week
277        // TODO Make the literal ints below constants once we twiddle them a bit
278        int items = getEstimate(Eas.FILTER_1_WEEK);
279        int lookback;
280        if (items > 1050) {
281            // Over 150/day, just use one day (smallest)
282            lookback = SyncWindow.SYNC_WINDOW_1_DAY;
283        } else if (items > 350 || (items == -1)) {
284            // 50-150/day, use 3 days (150 to 450 messages synced)
285            lookback = SyncWindow.SYNC_WINDOW_3_DAYS;
286        } else if (items > 150) {
287            // 20-50/day, use 1 week (140 to 350 messages synced)
288            lookback = SyncWindow.SYNC_WINDOW_1_WEEK;
289        } else if (items > 75) {
290            // 10-25/day, use 1 week (140 to 350 messages synced)
291            lookback = SyncWindow.SYNC_WINDOW_2_WEEKS;
292        } else if (items < 5) {
293            // If there are only a couple, see if it makes sense to get everything
294            items = getEstimate(Eas.FILTER_ALL);
295            if (items >= 0 && items < 100) {
296                lookback = SyncWindow.SYNC_WINDOW_ALL;
297            } else {
298                lookback = SyncWindow.SYNC_WINDOW_1_MONTH;
299            }
300        } else {
301            lookback = SyncWindow.SYNC_WINDOW_1_MONTH;
302        }
303
304        // Limit lookback to policy limit
305        if (mAccount.mPolicyKey > 0) {
306            Policy policy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
307            if (policy != null) {
308                int maxLookback = policy.mMaxEmailLookback;
309                if (maxLookback != 0 && (lookback > policy.mMaxEmailLookback)) {
310                    lookback = policy.mMaxEmailLookback;
311                }
312            }
313        }
314
315        // Store the new lookback and persist it
316        // TODO Code similar to this is used elsewhere (e.g. MailboxSettings); try to clean this up
317        ContentValues cv = new ContentValues();
318        Uri uri;
319        if (mMailbox.mType == Mailbox.TYPE_INBOX) {
320            mAccount.mSyncLookback = lookback;
321            cv.put(AccountColumns.SYNC_LOOKBACK, lookback);
322            uri = ContentUris.withAppendedId(Account.CONTENT_URI, mAccount.mId);
323        } else {
324            mMailbox.mSyncLookback = lookback;
325            cv.put(MailboxColumns.SYNC_LOOKBACK, lookback);
326            uri = ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId);
327        }
328        mContentResolver.update(uri, cv, null, null);
329
330        CharSequence[] windowEntries = mContext.getResources().getTextArray(
331                R.array.account_settings_mail_window_entries);
332        LogUtils.d(TAG, "Auto lookback: " + windowEntries[lookback]);
333    }
334
335    private static class GetItemEstimateParser extends Parser {
336        private int mEstimate = -1;
337
338        public GetItemEstimateParser(InputStream in) throws IOException {
339            super(in);
340        }
341
342        @Override
343        public boolean parse() throws IOException {
344            // Loop here through the remaining xml
345            while (nextTag(START_DOCUMENT) != END_DOCUMENT) {
346                if (tag == Tags.GIE_GET_ITEM_ESTIMATE) {
347                    parseGetItemEstimate();
348                } else {
349                    skipTag();
350                }
351            }
352            return true;
353        }
354
355        public void parseGetItemEstimate() throws IOException {
356            while (nextTag(Tags.GIE_GET_ITEM_ESTIMATE) != END) {
357                if (tag == Tags.GIE_RESPONSE) {
358                    parseResponse();
359                } else {
360                    skipTag();
361                }
362            }
363        }
364
365        public void parseResponse() throws IOException {
366            while (nextTag(Tags.GIE_RESPONSE) != END) {
367                if (tag == Tags.GIE_STATUS) {
368                    LogUtils.d(TAG, "GIE status: " + getValue());
369                } else if (tag == Tags.GIE_COLLECTION) {
370                    parseCollection();
371                } else {
372                    skipTag();
373                }
374            }
375        }
376
377        public void parseCollection() throws IOException {
378            while (nextTag(Tags.GIE_COLLECTION) != END) {
379                if (tag == Tags.GIE_CLASS) {
380                    LogUtils.d(TAG, "GIE class: " + getValue());
381                } else if (tag == Tags.GIE_COLLECTION_ID) {
382                    LogUtils.d(TAG, "GIE collectionId: " + getValue());
383                } else if (tag == Tags.GIE_ESTIMATE) {
384                    mEstimate = getValueInt();
385                    LogUtils.d(TAG, "GIE estimate: " + mEstimate);
386                } else {
387                    skipTag();
388                }
389            }
390        }
391    }
392
393    /**
394     * Return the estimated number of items to be synced in the current mailbox, based on the
395     * passed in filter argument
396     * @param filter an EAS "window" filter
397     * @return the estimated number of items to be synced, or -1 if unknown
398     * @throws IOException
399     */
400    private int getEstimate(String filter) throws IOException {
401        Serializer s = new Serializer();
402        boolean ex10 = mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2010_DOUBLE;
403        boolean ex03 = mService.mProtocolVersionDouble < Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE;
404        boolean ex07 = !ex10 && !ex03;
405
406        String className = getCollectionName();
407        String syncKey = getSyncKey();
408        userLog("gie, sending ", className, " syncKey: ", syncKey);
409
410        s.start(Tags.GIE_GET_ITEM_ESTIMATE).start(Tags.GIE_COLLECTIONS);
411        s.start(Tags.GIE_COLLECTION);
412        if (ex07) {
413            // Exchange 2007 likes collection id first
414            s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
415            s.data(Tags.SYNC_FILTER_TYPE, filter);
416            s.data(Tags.SYNC_SYNC_KEY, syncKey);
417        } else if (ex03) {
418            // Exchange 2003 needs the "class" element
419            s.data(Tags.GIE_CLASS, className);
420            s.data(Tags.SYNC_SYNC_KEY, syncKey);
421            s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
422            s.data(Tags.SYNC_FILTER_TYPE, filter);
423        } else {
424            // Exchange 2010 requires the filter inside an OPTIONS container and sync key first
425            s.data(Tags.SYNC_SYNC_KEY, syncKey);
426            s.data(Tags.GIE_COLLECTION_ID, mMailbox.mServerId);
427            s.start(Tags.SYNC_OPTIONS).data(Tags.SYNC_FILTER_TYPE, filter).end();
428        }
429        s.end().end().end().done(); // GIE_COLLECTION, GIE_COLLECTIONS, GIE_GET_ITEM_ESTIMATE
430
431        EasResponse resp = mService.sendHttpClientPost("GetItemEstimate",
432                new ByteArrayEntity(s.toByteArray()), EasSyncService.COMMAND_TIMEOUT);
433        try {
434            int code = resp.getStatus();
435            if (code == HttpStatus.SC_OK) {
436                if (!resp.isEmpty()) {
437                    InputStream is = resp.getInputStream();
438                    GetItemEstimateParser gieParser = new GetItemEstimateParser(is);
439                    gieParser.parse();
440                    // Return the estimated number of items
441                    return gieParser.mEstimate;
442                }
443            }
444        } finally {
445            resp.close();
446        }
447        // If we can't get an estimate, indicate this...
448        return -1;
449    }
450
451    /**
452     * Return the value of isLooping() as returned from the parser
453     */
454    @Override
455    public boolean isLooping() {
456        return mIsLooping;
457    }
458
459    @Override
460    public boolean isSyncable() {
461        return true;
462    }
463
464    public static class EasEmailSyncParser extends AbstractSyncParser {
465
466        private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY =
467            SyncColumns.SERVER_ID + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
468
469        private final String mMailboxIdAsString;
470
471        private final ArrayList<Message> newEmails = new ArrayList<Message>();
472        private final ArrayList<Message> fetchedEmails = new ArrayList<Message>();
473        private final ArrayList<Long> deletedEmails = new ArrayList<Long>();
474        private final ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
475
476        private final Policy mPolicy;
477        private boolean mFetchNeeded = false;
478
479        public EasEmailSyncParser(InputStream in, EmailSyncAdapter adapter) throws IOException {
480            super(in, adapter);
481            mMailboxIdAsString = Long.toString(mMailbox.mId);
482            if (mAccount.mPolicyKey != 0) {
483                mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
484            } else {
485                mPolicy = null;
486            }
487        }
488
489        public EasEmailSyncParser(final Context context, final ContentResolver resolver,
490                final InputStream in, final Mailbox mailbox, final Account account)
491                        throws IOException {
492            super(context, resolver, in, mailbox, account);
493            mMailboxIdAsString = Long.toString(mMailbox.mId);
494            if (mAccount.mPolicyKey != 0) {
495                mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
496            } else {
497                mPolicy = null;
498            }
499        }
500
501        public EasEmailSyncParser(Parser parser, EmailSyncAdapter adapter) throws IOException {
502            super(parser, adapter);
503            mMailboxIdAsString = Long.toString(mMailbox.mId);
504            if (mAccount.mPolicyKey != 0) {
505                mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
506            } else {
507                mPolicy = null;
508            }
509        }
510
511        public boolean fetchNeeded() {
512            return mFetchNeeded;
513        }
514
515        public void addData (Message msg, int endingTag) throws IOException {
516            ArrayList<Attachment> atts = new ArrayList<Attachment>();
517            boolean truncated = false;
518
519            while (nextTag(endingTag) != END) {
520                switch (tag) {
521                    case Tags.EMAIL_ATTACHMENTS:
522                    case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
523                        attachmentsParser(atts, msg);
524                        break;
525                    case Tags.EMAIL_TO:
526                        msg.mTo = Address.pack(Address.parse(getValue()));
527                        break;
528                    case Tags.EMAIL_FROM:
529                        Address[] froms = Address.parse(getValue());
530                        if (froms != null && froms.length > 0) {
531                            msg.mDisplayName = froms[0].toFriendly();
532                        }
533                        msg.mFrom = Address.pack(froms);
534                        break;
535                    case Tags.EMAIL_CC:
536                        msg.mCc = Address.pack(Address.parse(getValue()));
537                        break;
538                    case Tags.EMAIL_REPLY_TO:
539                        msg.mReplyTo = Address.pack(Address.parse(getValue()));
540                        break;
541                    case Tags.EMAIL_DATE_RECEIVED:
542                        msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue());
543                        break;
544                    case Tags.EMAIL_SUBJECT:
545                        msg.mSubject = getValue();
546                        break;
547                    case Tags.EMAIL_READ:
548                        msg.mFlagRead = getValueInt() == 1;
549                        break;
550                    case Tags.BASE_BODY:
551                        bodyParser(msg);
552                        break;
553                    case Tags.EMAIL_FLAG:
554                        msg.mFlagFavorite = flagParser();
555                        break;
556                    case Tags.EMAIL_MIME_TRUNCATED:
557                        truncated = getValueInt() == 1;
558                        break;
559                    case Tags.EMAIL_MIME_DATA:
560                        // We get MIME data for EAS 2.5.  First we parse it, then we take the
561                        // html and/or plain text data and store it in the message
562                        if (truncated) {
563                            // If the MIME data is truncated, don't bother parsing it, because
564                            // it will take time and throw an exception anyway when EOF is reached
565                            // In this case, we will load the body separately by tagging the message
566                            // "partially loaded".
567                            // Get the data (and ignore it)
568                            getValue();
569                            userLog("Partially loaded: ", msg.mServerId);
570                            msg.mFlagLoaded = Message.FLAG_LOADED_PARTIAL;
571                            mFetchNeeded = true;
572                        } else {
573                            mimeBodyParser(msg, getValue());
574                        }
575                        break;
576                    case Tags.EMAIL_BODY:
577                        String text = getValue();
578                        msg.mText = text;
579                        break;
580                    case Tags.EMAIL_MESSAGE_CLASS:
581                        String messageClass = getValue();
582                        if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
583                            msg.mFlags |= Message.FLAG_INCOMING_MEETING_INVITE;
584                        } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
585                            msg.mFlags |= Message.FLAG_INCOMING_MEETING_CANCEL;
586                        }
587                        break;
588                    case Tags.EMAIL_MEETING_REQUEST:
589                        meetingRequestParser(msg);
590                        break;
591                    case Tags.EMAIL_THREAD_TOPIC:
592                        msg.mThreadTopic = getValue();
593                        break;
594                    case Tags.RIGHTS_LICENSE:
595                        skipParser(tag);
596                        break;
597                    case Tags.EMAIL2_CONVERSATION_ID:
598                        msg.mServerConversationId =
599                                Base64.encodeToString(getValueBytes(), Base64.URL_SAFE);
600                        break;
601                    case Tags.EMAIL2_CONVERSATION_INDEX:
602                        // Ignore this byte array since we're not constructing a tree.
603                        getValueBytes();
604                        break;
605                    case Tags.EMAIL2_LAST_VERB_EXECUTED:
606                        int val = getValueInt();
607                        if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
608                            // We aren't required to distinguish between reply and reply all here
609                            msg.mFlags |= Message.FLAG_REPLIED_TO;
610                        } else if (val == LAST_VERB_FORWARD) {
611                            msg.mFlags |= Message.FLAG_FORWARDED;
612                        }
613                        break;
614                    default:
615                        skipTag();
616                }
617            }
618
619            if (atts.size() > 0) {
620                msg.mAttachments = atts;
621            }
622
623            if ((msg.mFlags & Message.FLAG_INCOMING_MEETING_MASK) != 0) {
624                String text = TextUtilities.makeSnippetFromHtmlText(
625                        msg.mText != null ? msg.mText : msg.mHtml);
626                if (TextUtils.isEmpty(text)) {
627                    // Create text for this invitation
628                    String meetingInfo = msg.mMeetingInfo;
629                    if (!TextUtils.isEmpty(meetingInfo)) {
630                        PackedString ps = new PackedString(meetingInfo);
631                        ContentValues values = new ContentValues();
632                        putFromMeeting(ps, MeetingInfo.MEETING_LOCATION, values,
633                                Events.EVENT_LOCATION);
634                        String dtstart = ps.get(MeetingInfo.MEETING_DTSTART);
635                        if (!TextUtils.isEmpty(dtstart)) {
636                            long startTime = Utility.parseEmailDateTimeToMillis(dtstart);
637                            values.put(Events.DTSTART, startTime);
638                        }
639                        putFromMeeting(ps, MeetingInfo.MEETING_ALL_DAY, values,
640                                Events.ALL_DAY);
641                        msg.mText = CalendarUtilities.buildMessageTextFromEntityValues(
642                                mContext, values, null);
643                        msg.mHtml = Html.toHtml(new SpannedString(msg.mText));
644                    }
645                }
646            }
647        }
648
649        private static void putFromMeeting(PackedString ps, String field, ContentValues values,
650                String column) {
651            String val = ps.get(field);
652            if (!TextUtils.isEmpty(val)) {
653                values.put(column, val);
654            }
655        }
656
657        /**
658         * Set up the meetingInfo field in the message with various pieces of information gleaned
659         * from MeetingRequest tags.  This information will be used later to generate an appropriate
660         * reply email if the user chooses to respond
661         * @param msg the Message being built
662         * @throws IOException
663         */
664        private void meetingRequestParser(Message msg) throws IOException {
665            PackedString.Builder packedString = new PackedString.Builder();
666            while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) {
667                switch (tag) {
668                    case Tags.EMAIL_DTSTAMP:
669                        packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue());
670                        break;
671                    case Tags.EMAIL_START_TIME:
672                        packedString.put(MeetingInfo.MEETING_DTSTART, getValue());
673                        break;
674                    case Tags.EMAIL_END_TIME:
675                        packedString.put(MeetingInfo.MEETING_DTEND, getValue());
676                        break;
677                    case Tags.EMAIL_ORGANIZER:
678                        packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue());
679                        break;
680                    case Tags.EMAIL_LOCATION:
681                        packedString.put(MeetingInfo.MEETING_LOCATION, getValue());
682                        break;
683                    case Tags.EMAIL_GLOBAL_OBJID:
684                        packedString.put(MeetingInfo.MEETING_UID,
685                                CalendarUtilities.getUidFromGlobalObjId(getValue()));
686                        break;
687                    case Tags.EMAIL_CATEGORIES:
688                        skipParser(tag);
689                        break;
690                    case Tags.EMAIL_RECURRENCES:
691                        recurrencesParser();
692                        break;
693                    case Tags.EMAIL_RESPONSE_REQUESTED:
694                        packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue());
695                        break;
696                    case Tags.EMAIL_ALL_DAY_EVENT:
697                        if (getValueInt() == 1) {
698                            packedString.put(MeetingInfo.MEETING_ALL_DAY, "1");
699                        }
700                        break;
701                    default:
702                        skipTag();
703                }
704            }
705            if (msg.mSubject != null) {
706                packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject);
707            }
708            msg.mMeetingInfo = packedString.toString();
709        }
710
711        private void recurrencesParser() throws IOException {
712            while (nextTag(Tags.EMAIL_RECURRENCES) != END) {
713                switch (tag) {
714                    case Tags.EMAIL_RECURRENCE:
715                        skipParser(tag);
716                        break;
717                    default:
718                        skipTag();
719                }
720            }
721        }
722
723        /**
724         * Parse a message from the server stream.
725         * @return the parsed Message
726         * @throws IOException
727         */
728        private Message addParser() throws IOException, CommandStatusException {
729            Message msg = new Message();
730            msg.mAccountKey = mAccount.mId;
731            msg.mMailboxKey = mMailbox.mId;
732            msg.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
733            // Default to 1 (success) in case we don't get this tag
734            int status = 1;
735
736            while (nextTag(Tags.SYNC_ADD) != END) {
737                switch (tag) {
738                    case Tags.SYNC_SERVER_ID:
739                        msg.mServerId = getValue();
740                        break;
741                    case Tags.SYNC_STATUS:
742                        status = getValueInt();
743                        break;
744                    case Tags.SYNC_APPLICATION_DATA:
745                        addData(msg, tag);
746                        break;
747                    default:
748                        skipTag();
749                }
750            }
751            // For sync, status 1 = success
752            if (status != 1) {
753                throw new CommandStatusException(status, msg.mServerId);
754            }
755            return msg;
756        }
757
758        // For now, we only care about the "active" state
759        private Boolean flagParser() throws IOException {
760            Boolean state = false;
761            while (nextTag(Tags.EMAIL_FLAG) != END) {
762                switch (tag) {
763                    case Tags.EMAIL_FLAG_STATUS:
764                        state = getValueInt() == 2;
765                        break;
766                    default:
767                        skipTag();
768                }
769            }
770            return state;
771        }
772
773        private void bodyParser(Message msg) throws IOException {
774            String bodyType = Eas.BODY_PREFERENCE_TEXT;
775            String body = "";
776            while (nextTag(Tags.EMAIL_BODY) != END) {
777                switch (tag) {
778                    case Tags.BASE_TYPE:
779                        bodyType = getValue();
780                        break;
781                    case Tags.BASE_DATA:
782                        body = getValue();
783                        break;
784                    default:
785                        skipTag();
786                }
787            }
788            // We always ask for TEXT or HTML; there's no third option
789            if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
790                msg.mHtml = body;
791            } else {
792                msg.mText = body;
793            }
794        }
795
796        /**
797         * Parses untruncated MIME data, saving away the text parts
798         * @param msg the message we're building
799         * @param mimeData the MIME data we've received from the server
800         * @throws IOException
801         */
802        private static void mimeBodyParser(Message msg, String mimeData) throws IOException {
803            try {
804                ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes());
805                // The constructor parses the message
806                MimeMessage mimeMessage = new MimeMessage(in);
807                // Now process body parts & attachments
808                ArrayList<Part> viewables = new ArrayList<Part>();
809                // We'll ignore the attachments, as we'll get them directly from EAS
810                ArrayList<Part> attachments = new ArrayList<Part>();
811                MimeUtility.collectParts(mimeMessage, viewables, attachments);
812                // parseBodyFields fills in the content fields of the Body
813                ConversionUtilities.BodyFieldData data =
814                        ConversionUtilities.parseBodyFields(viewables);
815                // But we need them in the message itself for handling during commit()
816                msg.setFlags(data.isQuotedReply, data.isQuotedForward);
817                msg.mSnippet = data.snippet;
818                msg.mHtml = data.htmlContent;
819                msg.mText = data.textContent;
820            } catch (MessagingException e) {
821                // This would most likely indicate a broken stream
822                throw new IOException(e);
823            }
824        }
825
826        private void attachmentsParser(ArrayList<Attachment> atts, Message msg) throws IOException {
827            while (nextTag(Tags.EMAIL_ATTACHMENTS) != END) {
828                switch (tag) {
829                    case Tags.EMAIL_ATTACHMENT:
830                    case Tags.BASE_ATTACHMENT:  // BASE_ATTACHMENT is used in EAS 12.0 and up
831                        attachmentParser(atts, msg);
832                        break;
833                    default:
834                        skipTag();
835                }
836            }
837        }
838
839        private void attachmentParser(ArrayList<Attachment> atts, Message msg) throws IOException {
840            String fileName = null;
841            String length = null;
842            String location = null;
843            boolean isInline = false;
844            String contentId = null;
845
846            while (nextTag(Tags.EMAIL_ATTACHMENT) != END) {
847                switch (tag) {
848                    // We handle both EAS 2.5 and 12.0+ attachments here
849                    case Tags.EMAIL_DISPLAY_NAME:
850                    case Tags.BASE_DISPLAY_NAME:
851                        fileName = getValue();
852                        break;
853                    case Tags.EMAIL_ATT_NAME:
854                    case Tags.BASE_FILE_REFERENCE:
855                        location = getValue();
856                        break;
857                    case Tags.EMAIL_ATT_SIZE:
858                    case Tags.BASE_ESTIMATED_DATA_SIZE:
859                        length = getValue();
860                        break;
861                    case Tags.BASE_IS_INLINE:
862                        isInline = getValueInt() == 1;
863                        break;
864                    case Tags.BASE_CONTENT_ID:
865                        contentId = getValue();
866                        break;
867                    default:
868                        skipTag();
869                }
870            }
871
872            if ((fileName != null) && (length != null) && (location != null)) {
873                Attachment att = new Attachment();
874                att.mEncoding = "base64";
875                att.mSize = Long.parseLong(length);
876                att.mFileName = fileName;
877                att.mLocation = location;
878                att.mMimeType = getMimeTypeFromFileName(fileName);
879                att.mAccountKey = mAccount.mId;
880                // Save away the contentId, if we've got one (for inline images); note that the
881                // EAS docs appear to be wrong about the tags used; inline images come with
882                // contentId rather than contentLocation, when sent from Ex03, Ex07, and Ex10
883                if (isInline && !TextUtils.isEmpty(contentId)) {
884                    att.mContentId = contentId;
885                }
886                // Check if this attachment can't be downloaded due to an account policy
887                if (mPolicy != null) {
888                    if (mPolicy.mDontAllowAttachments ||
889                            (mPolicy.mMaxAttachmentSize > 0 &&
890                                    (att.mSize > mPolicy.mMaxAttachmentSize))) {
891                        att.mFlags = Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
892                    }
893                }
894                atts.add(att);
895                msg.mFlagAttachment = true;
896            }
897        }
898
899        /**
900         * Returns an appropriate mimetype for the given file name's extension. If a mimetype
901         * cannot be determined, {@code application/<<x>>} [where @{code <<x>> is the extension,
902         * if it exists or {@code application/octet-stream}].
903         * At the moment, this is somewhat lame, since many file types aren't recognized
904         * @param fileName the file name to ponder
905         */
906        // Note: The MimeTypeMap method currently uses a very limited set of mime types
907        // A bug has been filed against this issue.
908        public String getMimeTypeFromFileName(String fileName) {
909            String mimeType;
910            int lastDot = fileName.lastIndexOf('.');
911            String extension = null;
912            if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
913                extension = fileName.substring(lastDot + 1).toLowerCase();
914            }
915            if (extension == null) {
916                // A reasonable default for now.
917                mimeType = "application/octet-stream";
918            } else {
919                mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
920                if (mimeType == null) {
921                    mimeType = "application/" + extension;
922                }
923            }
924            return mimeType;
925        }
926
927        private Cursor getServerIdCursor(String serverId, String[] projection) {
928            Cursor c = mContentResolver.query(Message.CONTENT_URI, projection,
929                    WHERE_SERVER_ID_AND_MAILBOX_KEY, new String[] {serverId, mMailboxIdAsString},
930                    null);
931            if (c == null) throw new ProviderUnavailableException();
932            if (c.getCount() > 1) {
933                userLog("Multiple messages with the same serverId/mailbox: " + serverId);
934            }
935            return c;
936        }
937
938        @VisibleForTesting
939        void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
940            while (nextTag(entryTag) != END) {
941                switch (tag) {
942                    case Tags.SYNC_SERVER_ID:
943                        String serverId = getValue();
944                        // Find the message in this mailbox with the given serverId
945                        Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
946                        try {
947                            if (c.moveToFirst()) {
948                                deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
949                                if (Eas.USER_LOG) {
950                                    userLog("Deleting ", serverId + ", "
951                                            + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
952                                }
953                            }
954                        } finally {
955                            c.close();
956                        }
957                        break;
958                    default:
959                        skipTag();
960                }
961            }
962        }
963
964        @VisibleForTesting
965        class ServerChange {
966            final long id;
967            final Boolean read;
968            final Boolean flag;
969            final Integer flags;
970
971            ServerChange(long _id, Boolean _read, Boolean _flag, Integer _flags) {
972                id = _id;
973                read = _read;
974                flag = _flag;
975                flags = _flags;
976            }
977        }
978
979        @VisibleForTesting
980        void changeParser(ArrayList<ServerChange> changes) throws IOException {
981            String serverId = null;
982            Boolean oldRead = false;
983            Boolean oldFlag = false;
984            int flags = 0;
985            long id = 0;
986            while (nextTag(Tags.SYNC_CHANGE) != END) {
987                switch (tag) {
988                    case Tags.SYNC_SERVER_ID:
989                        serverId = getValue();
990                        Cursor c = getServerIdCursor(serverId, Message.LIST_PROJECTION);
991                        try {
992                            if (c.moveToFirst()) {
993                                userLog("Changing ", serverId);
994                                oldRead = c.getInt(Message.LIST_READ_COLUMN) == Message.READ;
995                                oldFlag = c.getInt(Message.LIST_FAVORITE_COLUMN) == 1;
996                                flags = c.getInt(Message.LIST_FLAGS_COLUMN);
997                                id = c.getLong(Message.LIST_ID_COLUMN);
998                            }
999                        } finally {
1000                            c.close();
1001                        }
1002                        break;
1003                    case Tags.SYNC_APPLICATION_DATA:
1004                        changeApplicationDataParser(changes, oldRead, oldFlag, flags, id);
1005                        break;
1006                    default:
1007                        skipTag();
1008                }
1009            }
1010        }
1011
1012        private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
1013                Boolean oldFlag, int oldFlags, long id) throws IOException {
1014            Boolean read = null;
1015            Boolean flag = null;
1016            Integer flags = null;
1017            while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
1018                switch (tag) {
1019                    case Tags.EMAIL_READ:
1020                        read = getValueInt() == 1;
1021                        break;
1022                    case Tags.EMAIL_FLAG:
1023                        flag = flagParser();
1024                        break;
1025                    case Tags.EMAIL2_LAST_VERB_EXECUTED:
1026                        int val = getValueInt();
1027                        // Clear out the old replied/forward flags and add in the new flag
1028                        flags = oldFlags & ~(Message.FLAG_REPLIED_TO | Message.FLAG_FORWARDED);
1029                        if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
1030                            // We aren't required to distinguish between reply and reply all here
1031                            flags |= Message.FLAG_REPLIED_TO;
1032                        } else if (val == LAST_VERB_FORWARD) {
1033                            flags |= Message.FLAG_FORWARDED;
1034                        }
1035                        break;
1036                    default:
1037                        skipTag();
1038                }
1039            }
1040            // See if there are flag changes re: read, flag (favorite) or replied/forwarded
1041            if (((read != null) && !oldRead.equals(read)) ||
1042                    ((flag != null) && !oldFlag.equals(flag)) || (flags != null)) {
1043                changes.add(new ServerChange(id, read, flag, flags));
1044            }
1045        }
1046
1047        /* (non-Javadoc)
1048         * @see com.android.exchange.adapter.EasContentParser#commandsParser()
1049         */
1050        @Override
1051        public void commandsParser() throws IOException, CommandStatusException {
1052            while (nextTag(Tags.SYNC_COMMANDS) != END) {
1053                if (tag == Tags.SYNC_ADD) {
1054                    newEmails.add(addParser());
1055                } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
1056                    deleteParser(deletedEmails, tag);
1057                } else if (tag == Tags.SYNC_CHANGE) {
1058                    changeParser(changedEmails);
1059                } else
1060                    skipTag();
1061            }
1062
1063        }
1064
1065        /**
1066         * Removed any messages with status 7 (mismatch) from the updatedIdList
1067         * @param endTag the tag we end with
1068         * @throws IOException
1069         */
1070        public void failedUpdateParser(int endTag) throws IOException {
1071            // We get serverId and status in the responses
1072            String serverId = null;
1073            while (nextTag(endTag) != END) {
1074                if (tag == Tags.SYNC_STATUS) {
1075                    int status = getValueInt();
1076                    if (status == 7 && serverId != null) {
1077                        Cursor c = getServerIdCursor(serverId, Message.ID_COLUMN_PROJECTION);
1078                        try {
1079                            if (c.moveToFirst()) {
1080                                Long id = c.getLong(Message.ID_PROJECTION_COLUMN);
1081                                userLog("Update of " + serverId + " failed; will retry");
1082                                //mUpdatedIdList.remove(id);
1083                                //mService.mUpsyncFailed = true;
1084                            }
1085                        } finally {
1086                            c.close();
1087                        }
1088                    }
1089                } else if (tag == Tags.SYNC_SERVER_ID) {
1090                    serverId = getValue();
1091                } else {
1092                    skipTag();
1093                }
1094            }
1095        }
1096
1097        @Override
1098        public void responsesParser() throws IOException {
1099            while (nextTag(Tags.SYNC_RESPONSES) != END) {
1100                if (tag == Tags.SYNC_ADD || tag == Tags.SYNC_CHANGE || tag == Tags.SYNC_DELETE) {
1101                    failedUpdateParser(tag);
1102                } else if (tag == Tags.SYNC_FETCH) {
1103                    try {
1104                        fetchedEmails.add(addParser());
1105                    } catch (CommandStatusException sse) {
1106                        if (sse.mStatus == 8) {
1107                            // 8 = object not found; delete the message from EmailProvider
1108                            // No other status should be seen in a fetch response, except, perhaps,
1109                            // for some temporary server failure
1110                            mContentResolver.delete(Message.CONTENT_URI,
1111                                    WHERE_SERVER_ID_AND_MAILBOX_KEY,
1112                                    new String[] {sse.mItemId, mMailboxIdAsString});
1113                        }
1114                    }
1115                }
1116            }
1117        }
1118
1119        @Override
1120        public void commit() {
1121            commitImpl(0);
1122        }
1123
1124        public void commitImpl(int tryCount) {
1125            // Use a batch operation to handle the changes
1126            ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1127
1128            // Maximum size of message text per fetch
1129            int numFetched = fetchedEmails.size();
1130            int maxPerFetch = 0;
1131            if (numFetched > 0 && tryCount > 0) {
1132                // Educated guess that 450000 chars (900k) is ok; 600k is a killer
1133                // Remember that when fetching, we're not getting any other data
1134                // We'll keep trying, reducing the maximum each time
1135                // Realistically, this will rarely exceed 1, and probably never 2
1136                maxPerFetch = 450000 / numFetched / tryCount;
1137            }
1138            for (Message msg: fetchedEmails) {
1139                // Find the original message's id (by serverId and mailbox)
1140                Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
1141                String id = null;
1142                try {
1143                    if (c.moveToFirst()) {
1144                        id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
1145                        while (c.moveToNext()) {
1146                            // This shouldn't happen, but clean up if it does
1147                            Long dupId =
1148                                    Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN));
1149                            userLog("Delete duplicate with id: " + dupId);
1150                            deletedEmails.add(dupId);
1151                        }
1152                    }
1153                } finally {
1154                    c.close();
1155                }
1156
1157                // If we find one, we do two things atomically: 1) set the body text for the
1158                // message, and 2) mark the message loaded (i.e. completely loaded)
1159                if (id != null) {
1160                    userLog("Fetched body successfully for ", id);
1161                    final String[] bindArgument = new String[] {id};
1162                    if ((maxPerFetch > 0) && (msg.mText.length() > maxPerFetch)) {
1163                        userLog("Truncating message to " + maxPerFetch);
1164                        msg.mText = msg.mText.substring(0, maxPerFetch) + "...";
1165                    }
1166                    ops.add(ContentProviderOperation.newUpdate(Body.CONTENT_URI)
1167                            .withSelection(Body.MESSAGE_KEY + "=?", bindArgument)
1168                            .withValue(Body.TEXT_CONTENT, msg.mText)
1169                            .build());
1170                    ops.add(ContentProviderOperation.newUpdate(Message.CONTENT_URI)
1171                            .withSelection(EmailContent.RECORD_ID + "=?", bindArgument)
1172                            .withValue(Message.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE)
1173                            .build());
1174                }
1175            }
1176
1177            for (Message msg: newEmails) {
1178                msg.addSaveOps(ops);
1179            }
1180
1181            for (Long id : deletedEmails) {
1182                ops.add(ContentProviderOperation.newDelete(
1183                        ContentUris.withAppendedId(Message.CONTENT_URI, id)).build());
1184                AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
1185            }
1186
1187            if (!changedEmails.isEmpty()) {
1188                // Server wins in a conflict...
1189                for (ServerChange change : changedEmails) {
1190                     ContentValues cv = new ContentValues();
1191                    if (change.read != null) {
1192                        cv.put(MessageColumns.FLAG_READ, change.read);
1193                    }
1194                    if (change.flag != null) {
1195                        cv.put(MessageColumns.FLAG_FAVORITE, change.flag);
1196                    }
1197                    if (change.flags != null) {
1198                        cv.put(MessageColumns.FLAGS, change.flags);
1199                    }
1200                    ops.add(ContentProviderOperation.newUpdate(
1201                            ContentUris.withAppendedId(Message.CONTENT_URI, change.id))
1202                                .withValues(cv)
1203                                .build());
1204                }
1205            }
1206
1207            // We only want to update the sync key here
1208            ContentValues mailboxValues = new ContentValues();
1209            mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
1210            ops.add(ContentProviderOperation.newUpdate(
1211                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
1212                        .withValues(mailboxValues).build());
1213
1214            try {
1215                mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
1216                userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
1217            } catch (TransactionTooLargeException e) {
1218                LogUtils.w(TAG, "Transaction failed on fetched message; retrying...");
1219                commitImpl(++tryCount);
1220             } catch (RemoteException e) {
1221                // There is nothing to be done here; fail by returning null
1222            } catch (OperationApplicationException e) {
1223                // There is nothing to be done here; fail by returning null
1224            }
1225        }
1226
1227        @Override
1228        protected void wipe() {
1229            // This file is deprecated, no need to implement this.
1230        }
1231    }
1232
1233    @Override
1234    public String getCollectionName() {
1235        return "Email";
1236    }
1237
1238    private void addCleanupOps(ArrayList<ContentProviderOperation> ops) {
1239        // If we've sent local deletions, clear out the deleted table
1240        for (Long id: mDeletedIdList) {
1241            ops.add(ContentProviderOperation.newDelete(
1242                    ContentUris.withAppendedId(Message.DELETED_CONTENT_URI, id)).build());
1243        }
1244        // And same with the updates
1245        for (Long id: mUpdatedIdList) {
1246            ops.add(ContentProviderOperation.newDelete(
1247                    ContentUris.withAppendedId(Message.UPDATED_CONTENT_URI, id)).build());
1248        }
1249    }
1250
1251    @Override
1252    public void cleanup() {
1253        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
1254        // Delete any moved messages (since we've just synced the mailbox, and no longer need the
1255        // placeholder message); this prevents duplicates from appearing in the mailbox.
1256        ops.add(ContentProviderOperation.newDelete(Message.CONTENT_URI)
1257                .withSelection(WHERE_MAILBOX_KEY_AND_MOVED,
1258                        new String[] {Long.toString(mMailbox.mId)}).build());
1259        // If we've done deletions/updates, clean up the deleted/updated tables
1260        if (!mDeletedIdList.isEmpty() || !mUpdatedIdList.isEmpty()) {
1261            addCleanupOps(ops);
1262        }
1263        try {
1264            mContext.getContentResolver()
1265                .applyBatch(EmailContent.AUTHORITY, ops);
1266        } catch (RemoteException e) {
1267            // There is nothing to be done here; fail by returning null
1268        } catch (OperationApplicationException e) {
1269            // There is nothing to be done here; fail by returning null
1270        }
1271    }
1272
1273    private static String formatTwo(int num) {
1274        if (num < 10) {
1275            return "0" + (char)('0' + num);
1276        } else
1277            return Integer.toString(num);
1278    }
1279
1280    /**
1281     * Create date/time in RFC8601 format.  Oddly enough, for calendar date/time, Microsoft uses
1282     * a different format that excludes the punctuation (this is why I'm not putting this in a
1283     * parent class)
1284     */
1285    public String formatDateTime(Calendar calendar) {
1286        StringBuilder sb = new StringBuilder();
1287        //YYYY-MM-DDTHH:MM:SS.MSSZ
1288        sb.append(calendar.get(Calendar.YEAR));
1289        sb.append('-');
1290        sb.append(formatTwo(calendar.get(Calendar.MONTH) + 1));
1291        sb.append('-');
1292        sb.append(formatTwo(calendar.get(Calendar.DAY_OF_MONTH)));
1293        sb.append('T');
1294        sb.append(formatTwo(calendar.get(Calendar.HOUR_OF_DAY)));
1295        sb.append(':');
1296        sb.append(formatTwo(calendar.get(Calendar.MINUTE)));
1297        sb.append(':');
1298        sb.append(formatTwo(calendar.get(Calendar.SECOND)));
1299        sb.append(".000Z");
1300        return sb.toString();
1301    }
1302
1303    /**
1304     * Note that messages in the deleted database preserve the message's unique id; therefore, we
1305     * can utilize this id to find references to the message.  The only reference situation at this
1306     * point is in the Body table; it is when sending messages via SmartForward and SmartReply
1307     */
1308    private static boolean messageReferenced(ContentResolver cr, long id) {
1309        // See if this id is referenced in a body
1310        Cursor c = cr.query(Body.CONTENT_URI, Body.ID_PROJECTION, WHERE_BODY_SOURCE_MESSAGE_KEY,
1311                new String[] {Long.toString(id)}, null);
1312        try {
1313            return c.moveToFirst();
1314        } finally {
1315            c.close();
1316        }
1317    }
1318
1319    /*private*/ /**
1320     * Serialize commands to delete items from the server; as we find items to delete, add their
1321     * id's to the deletedId's array
1322     *
1323     * @param s the Serializer we're using to create post data
1324     * @param deletedIds ids whose deletions are being sent to the server
1325     * @param first whether or not this is the first command being sent
1326     * @return true if SYNC_COMMANDS hasn't been sent (false otherwise)
1327     * @throws IOException
1328     */
1329    @VisibleForTesting
1330    boolean sendDeletedItems(Serializer s, ArrayList<Long> deletedIds, boolean first)
1331            throws IOException {
1332        ContentResolver cr = mContext.getContentResolver();
1333
1334        // Find any of our deleted items
1335        Cursor c = cr.query(Message.DELETED_CONTENT_URI, Message.LIST_PROJECTION,
1336                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
1337        // We keep track of the list of deleted item id's so that we can remove them from the
1338        // deleted table after the server receives our command
1339        deletedIds.clear();
1340        try {
1341            while (c.moveToNext()) {
1342                String serverId = c.getString(Message.LIST_SERVER_ID_COLUMN);
1343                // Keep going if there's no serverId
1344                if (serverId == null) {
1345                    continue;
1346                // Also check if this message is referenced elsewhere
1347                } else if (messageReferenced(cr, c.getLong(Message.CONTENT_ID_COLUMN))) {
1348                    userLog("Postponing deletion of referenced message: ", serverId);
1349                    continue;
1350                } else if (first) {
1351                    s.start(Tags.SYNC_COMMANDS);
1352                    first = false;
1353                }
1354                // Send the command to delete this message
1355                s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
1356                deletedIds.add(c.getLong(Message.LIST_ID_COLUMN));
1357            }
1358        } finally {
1359            c.close();
1360        }
1361
1362       return first;
1363    }
1364
1365    @Override
1366    public boolean sendLocalChanges(Serializer s) throws IOException {
1367        ContentResolver cr = mContext.getContentResolver();
1368
1369        if (getSyncKey().equals("0")) {
1370            return false;
1371        }
1372
1373        // Never upsync from these folders
1374        if (mMailbox.mType == Mailbox.TYPE_DRAFTS || mMailbox.mType == Mailbox.TYPE_OUTBOX) {
1375            return false;
1376        }
1377
1378        // This code is split out for unit testing purposes
1379        boolean firstCommand = sendDeletedItems(s, mDeletedIdList, true);
1380
1381        if (!mFetchRequestList.isEmpty()) {
1382            // Add FETCH commands for messages that need a body (i.e. we didn't find it during
1383            // our earlier sync; this happens only in EAS 2.5 where the body couldn't be found
1384            // after parsing the message's MIME data)
1385            if (firstCommand) {
1386                s.start(Tags.SYNC_COMMANDS);
1387                firstCommand = false;
1388            }
1389            for (FetchRequest req: mFetchRequestList) {
1390                s.start(Tags.SYNC_FETCH).data(Tags.SYNC_SERVER_ID, req.serverId).end();
1391            }
1392        }
1393
1394        // Find our trash mailbox, since deletions will have been moved there...
1395        long trashMailboxId =
1396            Mailbox.findMailboxOfType(mContext, mMailbox.mAccountKey, Mailbox.TYPE_TRASH);
1397
1398        // Do the same now for updated items
1399        Cursor c = cr.query(Message.UPDATED_CONTENT_URI, Message.LIST_PROJECTION,
1400                MessageColumns.MAILBOX_KEY + '=' + mMailbox.mId, null, null);
1401
1402        // We keep track of the list of updated item id's as we did above with deleted items
1403        mUpdatedIdList.clear();
1404        try {
1405            ContentValues cv = new ContentValues();
1406            while (c.moveToNext()) {
1407                long id = c.getLong(Message.LIST_ID_COLUMN);
1408                // Say we've handled this update
1409                mUpdatedIdList.add(id);
1410                // We have the id of the changed item.  But first, we have to find out its current
1411                // state, since the updated table saves the opriginal state
1412                Cursor currentCursor = cr.query(ContentUris.withAppendedId(Message.CONTENT_URI, id),
1413                        UPDATES_PROJECTION, null, null, null);
1414                try {
1415                    // If this item no longer exists (shouldn't be possible), just move along
1416                    if (!currentCursor.moveToFirst()) {
1417                        continue;
1418                    }
1419                    // Keep going if there's no serverId
1420                    String serverId = currentCursor.getString(UPDATES_SERVER_ID_COLUMN);
1421                    if (serverId == null) {
1422                        continue;
1423                    }
1424
1425                    boolean flagChange = false;
1426                    boolean readChange = false;
1427
1428                    long mailbox = currentCursor.getLong(UPDATES_MAILBOX_KEY_COLUMN);
1429                    // If the message is now in the trash folder, it has been deleted by the user
1430                    if (mailbox == trashMailboxId) {
1431                         if (firstCommand) {
1432                            s.start(Tags.SYNC_COMMANDS);
1433                            firstCommand = false;
1434                        }
1435                        // Send the command to delete this message
1436                        s.start(Tags.SYNC_DELETE).data(Tags.SYNC_SERVER_ID, serverId).end();
1437                        // Mark the message as moved (so the copy will be deleted if/when the server
1438                        // version is synced)
1439                        int flags = c.getInt(Message.LIST_FLAGS_COLUMN);
1440                        cv.put(MessageColumns.FLAGS,
1441                                flags | EasSyncService.MESSAGE_FLAG_MOVED_MESSAGE);
1442                        cr.update(ContentUris.withAppendedId(Message.CONTENT_URI, id), cv,
1443                                null, null);
1444                        continue;
1445                    } else if (mailbox != c.getLong(Message.LIST_MAILBOX_KEY_COLUMN)) {
1446                        // The message has moved to another mailbox; add a request for this
1447                        // Note: The Sync command doesn't handle moving messages, so we need
1448                        // to handle this as a "request" (similar to meeting response and
1449                        // attachment load)
1450                        mService.addRequest(new MessageMoveRequest(id, mailbox));
1451                        // Regardless of other changes that might be made, we don't want to indicate
1452                        // that this message has been updated until the move request has been
1453                        // handled (without this, a crash between the flag upsync and the move
1454                        // would cause the move to be lost)
1455                        mUpdatedIdList.remove(id);
1456                    }
1457
1458                    // We can only send flag changes to the server in 12.0 or later
1459                    int flag = 0;
1460                    if (mService.mProtocolVersionDouble >= Eas.SUPPORTED_PROTOCOL_EX2007_DOUBLE) {
1461                        flag = currentCursor.getInt(UPDATES_FLAG_COLUMN);
1462                        if (flag != c.getInt(Message.LIST_FAVORITE_COLUMN)) {
1463                            flagChange = true;
1464                        }
1465                    }
1466
1467                    int read = currentCursor.getInt(UPDATES_READ_COLUMN);
1468                    if (read != c.getInt(Message.LIST_READ_COLUMN)) {
1469                        readChange = true;
1470                    }
1471
1472                    if (!flagChange && !readChange) {
1473                        // In this case, we've got nothing to send to the server
1474                        continue;
1475                    }
1476
1477                    if (firstCommand) {
1478                        s.start(Tags.SYNC_COMMANDS);
1479                        firstCommand = false;
1480                    }
1481                    // Send the change to "read" and "favorite" (flagged)
1482                    s.start(Tags.SYNC_CHANGE)
1483                        .data(Tags.SYNC_SERVER_ID, c.getString(Message.LIST_SERVER_ID_COLUMN))
1484                        .start(Tags.SYNC_APPLICATION_DATA);
1485                    if (readChange) {
1486                        s.data(Tags.EMAIL_READ, Integer.toString(read));
1487                    }
1488                    // "Flag" is a relatively complex concept in EAS 12.0 and above.  It is not only
1489                    // the boolean "favorite" that we think of in Gmail, but it also represents a
1490                    // follow up action, which can include a subject, start and due dates, and even
1491                    // recurrences.  We don't support any of this as yet, but EAS 12.0 and higher
1492                    // require that a flag contain a status, a type, and four date fields, two each
1493                    // for start date and end (due) date.
1494                    if (flagChange) {
1495                        if (flag != 0) {
1496                            // Status 2 = set flag
1497                            s.start(Tags.EMAIL_FLAG).data(Tags.EMAIL_FLAG_STATUS, "2");
1498                            // "FollowUp" is the standard type
1499                            s.data(Tags.EMAIL_FLAG_TYPE, "FollowUp");
1500                            long now = System.currentTimeMillis();
1501                            Calendar calendar =
1502                                GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT"));
1503                            calendar.setTimeInMillis(now);
1504                            // Flags are required to have a start date and end date (duplicated)
1505                            // First, we'll set the current date/time in GMT as the start time
1506                            String utc = formatDateTime(calendar);
1507                            s.data(Tags.TASK_START_DATE, utc).data(Tags.TASK_UTC_START_DATE, utc);
1508                            // And then we'll use one week from today for completion date
1509                            calendar.setTimeInMillis(now + 1*WEEKS);
1510                            utc = formatDateTime(calendar);
1511                            s.data(Tags.TASK_DUE_DATE, utc).data(Tags.TASK_UTC_DUE_DATE, utc);
1512                            s.end();
1513                        } else {
1514                            s.tag(Tags.EMAIL_FLAG);
1515                        }
1516                    }
1517                    s.end().end(); // SYNC_APPLICATION_DATA, SYNC_CHANGE
1518                } finally {
1519                    currentCursor.close();
1520                }
1521            }
1522        } finally {
1523            c.close();
1524        }
1525
1526        if (!firstCommand) {
1527            s.end(); // SYNC_COMMANDS
1528        }
1529        return false;
1530    }
1531}
1532