1package com.android.exchange.adapter;
2
3import android.content.ContentProviderOperation;
4import android.content.ContentResolver;
5import android.content.ContentUris;
6import android.content.ContentValues;
7import android.content.Context;
8import android.content.OperationApplicationException;
9import android.database.Cursor;
10import android.os.Parcel;
11import android.os.RemoteException;
12import android.os.TransactionTooLargeException;
13import android.provider.CalendarContract;
14import android.text.Html;
15import android.text.SpannedString;
16import android.text.TextUtils;
17import android.util.Base64;
18import android.util.Log;
19import android.webkit.MimeTypeMap;
20
21import com.android.emailcommon.internet.MimeMessage;
22import com.android.emailcommon.internet.MimeUtility;
23import com.android.emailcommon.mail.Address;
24import com.android.emailcommon.mail.MeetingInfo;
25import com.android.emailcommon.mail.MessagingException;
26import com.android.emailcommon.mail.PackedString;
27import com.android.emailcommon.mail.Part;
28import com.android.emailcommon.provider.Account;
29import com.android.emailcommon.provider.EmailContent;
30import com.android.emailcommon.provider.EmailContent.MessageColumns;
31import com.android.emailcommon.provider.EmailContent.SyncColumns;
32import com.android.emailcommon.provider.Mailbox;
33import com.android.emailcommon.provider.Policy;
34import com.android.emailcommon.provider.ProviderUnavailableException;
35import com.android.emailcommon.utility.AttachmentUtilities;
36import com.android.emailcommon.utility.ConversionUtilities;
37import com.android.emailcommon.utility.TextUtilities;
38import com.android.emailcommon.utility.Utility;
39import com.android.exchange.CommandStatusException;
40import com.android.exchange.Eas;
41import com.android.exchange.utility.CalendarUtilities;
42import com.android.mail.utils.LogUtils;
43import com.google.common.annotations.VisibleForTesting;
44
45import java.io.ByteArrayInputStream;
46import java.io.IOException;
47import java.io.InputStream;
48import java.text.ParseException;
49import java.util.ArrayList;
50import java.util.HashMap;
51import java.util.Map;
52
53/**
54 * Parser for Sync on an email collection.
55 */
56public class EmailSyncParser extends AbstractSyncParser {
57    private static final String TAG = Eas.LOG_TAG;
58
59    private static final String WHERE_SERVER_ID_AND_MAILBOX_KEY = SyncColumns.SERVER_ID
60            + "=? and " + MessageColumns.MAILBOX_KEY + "=?";
61
62    private final String mMailboxIdAsString;
63
64    private final ArrayList<EmailContent.Message>
65            newEmails = new ArrayList<EmailContent.Message>();
66    private final ArrayList<EmailContent.Message> fetchedEmails =
67            new ArrayList<EmailContent.Message>();
68    private final ArrayList<Long> deletedEmails = new ArrayList<Long>();
69    private final ArrayList<ServerChange> changedEmails = new ArrayList<ServerChange>();
70
71    private static final int MESSAGE_ID_SUBJECT_ID_COLUMN = 0;
72    private static final int MESSAGE_ID_SUBJECT_SUBJECT_COLUMN = 1;
73    private static final String[] MESSAGE_ID_SUBJECT_PROJECTION =
74            new String[] { MessageColumns._ID, MessageColumns.SUBJECT };
75
76    @VisibleForTesting
77    static final int LAST_VERB_REPLY = 1;
78    @VisibleForTesting
79    static final int LAST_VERB_REPLY_ALL = 2;
80    @VisibleForTesting
81    static final int LAST_VERB_FORWARD = 3;
82
83    private final Policy mPolicy;
84
85    // Max times to retry when we get a TransactionTooLargeException exception
86    private static final int MAX_RETRIES = 10;
87
88    // Max number of ops per batch. It could end up more than this but once we detect we are at or
89    // above this number, we flush.
90    private static final int MAX_OPS_PER_BATCH = 50;
91
92    private boolean mFetchNeeded = false;
93
94    private final Map<String, Integer> mMessageUpdateStatus = new HashMap();
95
96    public EmailSyncParser(final Context context, final ContentResolver resolver,
97            final InputStream in, final Mailbox mailbox, final Account account)
98            throws IOException {
99        super(context, resolver, in, mailbox, account);
100        mMailboxIdAsString = Long.toString(mMailbox.mId);
101        if (mAccount.mPolicyKey != 0) {
102            mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
103        } else {
104            mPolicy = null;
105        }
106    }
107
108    public EmailSyncParser(final Parser parser, final Context context,
109            final ContentResolver resolver, final Mailbox mailbox, final Account account)
110                    throws IOException {
111        super(parser, context, resolver, mailbox, account);
112        mMailboxIdAsString = Long.toString(mMailbox.mId);
113        if (mAccount.mPolicyKey != 0) {
114            mPolicy = Policy.restorePolicyWithId(mContext, mAccount.mPolicyKey);
115        } else {
116            mPolicy = null;
117        }
118    }
119
120    public EmailSyncParser(final Context context, final InputStream in, final Mailbox mailbox,
121            final Account account) throws IOException {
122        this(context, context.getContentResolver(), in, mailbox, account);
123    }
124
125    public boolean fetchNeeded() {
126        return mFetchNeeded;
127    }
128
129    public Map<String, Integer> getMessageStatuses() {
130        return mMessageUpdateStatus;
131    }
132
133    public void addData(EmailContent.Message msg, int endingTag) throws IOException {
134        ArrayList<EmailContent.Attachment> atts = new ArrayList<EmailContent.Attachment>();
135        boolean truncated = false;
136
137        while (nextTag(endingTag) != END) {
138            switch (tag) {
139                case Tags.EMAIL_ATTACHMENTS:
140                case Tags.BASE_ATTACHMENTS: // BASE_ATTACHMENTS is used in EAS 12.0 and up
141                    attachmentsParser(atts, msg, tag);
142                    break;
143                case Tags.EMAIL_TO:
144                    msg.mTo = Address.toString(Address.parse(getValue()));
145                    break;
146                case Tags.EMAIL_FROM:
147                    Address[] froms = Address.parse(getValue());
148                    if (froms != null && froms.length > 0) {
149                        msg.mDisplayName = froms[0].toFriendly();
150                    }
151                    msg.mFrom = Address.toString(froms);
152                    break;
153                case Tags.EMAIL_CC:
154                    msg.mCc = Address.toString(Address.parse(getValue()));
155                    break;
156                case Tags.EMAIL_REPLY_TO:
157                    msg.mReplyTo = Address.toString(Address.parse(getValue()));
158                    break;
159                case Tags.EMAIL_DATE_RECEIVED:
160                    try {
161                        msg.mTimeStamp = Utility.parseEmailDateTimeToMillis(getValue());
162                    } catch (ParseException e) {
163                        LogUtils.w(TAG, "Parse error for EMAIL_DATE_RECEIVED tag.", e);
164                    }
165                    break;
166                case Tags.EMAIL_SUBJECT:
167                    msg.mSubject = getValue();
168                    break;
169                case Tags.EMAIL_READ:
170                    msg.mFlagRead = getValueInt() == 1;
171                    break;
172                case Tags.BASE_BODY:
173                    bodyParser(msg);
174                    break;
175                case Tags.EMAIL_FLAG:
176                    msg.mFlagFavorite = flagParser();
177                    break;
178                case Tags.EMAIL_MIME_TRUNCATED:
179                    truncated = getValueInt() == 1;
180                    break;
181                case Tags.EMAIL_MIME_DATA:
182                    // We get MIME data for EAS 2.5.  First we parse it, then we take the
183                    // html and/or plain text data and store it in the message
184                    if (truncated) {
185                        // If the MIME data is truncated, don't bother parsing it, because
186                        // it will take time and throw an exception anyway when EOF is reached
187                        // In this case, we will load the body separately by tagging the message
188                        // "partially loaded".
189                        // Get the data (and ignore it)
190                        getValue();
191                        userLog("Partially loaded: ", msg.mServerId);
192                        msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_PARTIAL;
193                        mFetchNeeded = true;
194                    } else {
195                        mimeBodyParser(msg, getValue());
196                    }
197                    break;
198                case Tags.EMAIL_BODY:
199                    String text = getValue();
200                    msg.mText = text;
201                    break;
202                case Tags.EMAIL_MESSAGE_CLASS:
203                    String messageClass = getValue();
204                    if (messageClass.equals("IPM.Schedule.Meeting.Request")) {
205                        msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_INVITE;
206                    } else if (messageClass.equals("IPM.Schedule.Meeting.Canceled")) {
207                        msg.mFlags |= EmailContent.Message.FLAG_INCOMING_MEETING_CANCEL;
208                    }
209                    break;
210                case Tags.EMAIL_MEETING_REQUEST:
211                    meetingRequestParser(msg);
212                    break;
213                case Tags.EMAIL_THREAD_TOPIC:
214                    msg.mThreadTopic = getValue();
215                    break;
216                case Tags.RIGHTS_LICENSE:
217                    skipParser(tag);
218                    break;
219                case Tags.EMAIL2_CONVERSATION_ID:
220                    msg.mServerConversationId =
221                            Base64.encodeToString(getValueBytes(), Base64.URL_SAFE);
222                    break;
223                case Tags.EMAIL2_CONVERSATION_INDEX:
224                    // Ignore this byte array since we're not constructing a tree.
225                    getValueBytes();
226                    break;
227                case Tags.EMAIL2_LAST_VERB_EXECUTED:
228                    int val = getValueInt();
229                    if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
230                        // We aren't required to distinguish between reply and reply all here
231                        msg.mFlags |= EmailContent.Message.FLAG_REPLIED_TO;
232                    } else if (val == LAST_VERB_FORWARD) {
233                        msg.mFlags |= EmailContent.Message.FLAG_FORWARDED;
234                    }
235                    break;
236                default:
237                    skipTag();
238            }
239        }
240
241        if (atts.size() > 0) {
242            msg.mAttachments = atts;
243        }
244
245        if ((msg.mFlags & EmailContent.Message.FLAG_INCOMING_MEETING_MASK) != 0) {
246            String text = TextUtilities.makeSnippetFromHtmlText(
247                    msg.mText != null ? msg.mText : msg.mHtml);
248            if (TextUtils.isEmpty(text)) {
249                // Create text for this invitation
250                String meetingInfo = msg.mMeetingInfo;
251                if (!TextUtils.isEmpty(meetingInfo)) {
252                    PackedString ps = new PackedString(meetingInfo);
253                    ContentValues values = new ContentValues();
254                    putFromMeeting(ps, MeetingInfo.MEETING_LOCATION, values,
255                            CalendarContract.Events.EVENT_LOCATION);
256                    String dtstart = ps.get(MeetingInfo.MEETING_DTSTART);
257                    if (!TextUtils.isEmpty(dtstart)) {
258                        try {
259                            final long startTime =
260                                Utility.parseEmailDateTimeToMillis(dtstart);
261                            values.put(CalendarContract.Events.DTSTART, startTime);
262                        } catch (ParseException e) {
263                            LogUtils.w(TAG, "Parse error for MEETING_DTSTART tag.", e);
264                        }
265                    }
266                    putFromMeeting(ps, MeetingInfo.MEETING_ALL_DAY, values,
267                            CalendarContract.Events.ALL_DAY);
268                    msg.mText = CalendarUtilities.buildMessageTextFromEntityValues(
269                            mContext, values, null);
270                    msg.mHtml = Html.toHtml(new SpannedString(msg.mText));
271                }
272            }
273        }
274    }
275
276    private static void putFromMeeting(PackedString ps, String field, ContentValues values,
277            String column) {
278        String val = ps.get(field);
279        if (!TextUtils.isEmpty(val)) {
280            values.put(column, val);
281        }
282    }
283
284    /**
285     * Set up the meetingInfo field in the message with various pieces of information gleaned
286     * from MeetingRequest tags.  This information will be used later to generate an appropriate
287     * reply email if the user chooses to respond
288     * @param msg the Message being built
289     * @throws IOException
290     */
291    private void meetingRequestParser(EmailContent.Message msg) throws IOException {
292        PackedString.Builder packedString = new PackedString.Builder();
293        while (nextTag(Tags.EMAIL_MEETING_REQUEST) != END) {
294            switch (tag) {
295                case Tags.EMAIL_DTSTAMP:
296                    packedString.put(MeetingInfo.MEETING_DTSTAMP, getValue());
297                    break;
298                case Tags.EMAIL_START_TIME:
299                    packedString.put(MeetingInfo.MEETING_DTSTART, getValue());
300                    break;
301                case Tags.EMAIL_END_TIME:
302                    packedString.put(MeetingInfo.MEETING_DTEND, getValue());
303                    break;
304                case Tags.EMAIL_ORGANIZER:
305                    packedString.put(MeetingInfo.MEETING_ORGANIZER_EMAIL, getValue());
306                    break;
307                case Tags.EMAIL_LOCATION:
308                    packedString.put(MeetingInfo.MEETING_LOCATION, getValue());
309                    break;
310                case Tags.EMAIL_GLOBAL_OBJID:
311                    packedString.put(MeetingInfo.MEETING_UID,
312                            CalendarUtilities.getUidFromGlobalObjId(getValue()));
313                    break;
314                case Tags.EMAIL_CATEGORIES:
315                    skipParser(tag);
316                    break;
317                case Tags.EMAIL_RECURRENCES:
318                    recurrencesParser();
319                    break;
320                case Tags.EMAIL_RESPONSE_REQUESTED:
321                    packedString.put(MeetingInfo.MEETING_RESPONSE_REQUESTED, getValue());
322                    break;
323                case Tags.EMAIL_ALL_DAY_EVENT:
324                    if (getValueInt() == 1) {
325                        packedString.put(MeetingInfo.MEETING_ALL_DAY, "1");
326                    }
327                    break;
328                default:
329                    skipTag();
330            }
331        }
332        if (msg.mSubject != null) {
333            packedString.put(MeetingInfo.MEETING_TITLE, msg.mSubject);
334        }
335        msg.mMeetingInfo = packedString.toString();
336    }
337
338    private void recurrencesParser() throws IOException {
339        while (nextTag(Tags.EMAIL_RECURRENCES) != END) {
340            switch (tag) {
341                case Tags.EMAIL_RECURRENCE:
342                    skipParser(tag);
343                    break;
344                default:
345                    skipTag();
346            }
347        }
348    }
349
350    /**
351     * Parse a message from the server stream.
352     * @return the parsed Message
353     * @throws IOException
354     */
355    private EmailContent.Message addParser(final int endingTag) throws IOException, CommandStatusException {
356        EmailContent.Message msg = new EmailContent.Message();
357        msg.mAccountKey = mAccount.mId;
358        msg.mMailboxKey = mMailbox.mId;
359        msg.mFlagLoaded = EmailContent.Message.FLAG_LOADED_COMPLETE;
360        // Default to 1 (success) in case we don't get this tag
361        int status = 1;
362
363        while (nextTag(endingTag) != END) {
364            switch (tag) {
365                case Tags.SYNC_SERVER_ID:
366                    msg.mServerId = getValue();
367                    break;
368                case Tags.SYNC_STATUS:
369                    status = getValueInt();
370                    break;
371                case Tags.SYNC_APPLICATION_DATA:
372                    addData(msg, tag);
373                    break;
374                default:
375                    skipTag();
376            }
377        }
378        // For sync, status 1 = success
379        if (status != 1) {
380            throw new CommandStatusException(status, msg.mServerId);
381        }
382        return msg;
383    }
384
385    // For now, we only care about the "active" state
386    private Boolean flagParser() throws IOException {
387        Boolean state = false;
388        while (nextTag(Tags.EMAIL_FLAG) != END) {
389            switch (tag) {
390                case Tags.EMAIL_FLAG_STATUS:
391                    state = getValueInt() == 2;
392                    break;
393                default:
394                    skipTag();
395            }
396        }
397        return state;
398    }
399
400    private void bodyParser(EmailContent.Message msg) throws IOException {
401        String bodyType = Eas.BODY_PREFERENCE_TEXT;
402        String body = "";
403        while (nextTag(Tags.BASE_BODY) != END) {
404            switch (tag) {
405                case Tags.BASE_TYPE:
406                    bodyType = getValue();
407                    break;
408                case Tags.BASE_DATA:
409                    body = getValue();
410                    break;
411                default:
412                    skipTag();
413            }
414        }
415        // We always ask for TEXT or HTML; there's no third option
416        if (bodyType.equals(Eas.BODY_PREFERENCE_HTML)) {
417            msg.mHtml = body;
418        } else {
419            msg.mText = body;
420        }
421    }
422
423    /**
424     * Parses untruncated MIME data, saving away the text parts
425     * @param msg the message we're building
426     * @param mimeData the MIME data we've received from the server
427     * @throws IOException
428     */
429    private static void mimeBodyParser(EmailContent.Message msg, String mimeData)
430            throws IOException {
431        try {
432            ByteArrayInputStream in = new ByteArrayInputStream(mimeData.getBytes());
433            // The constructor parses the message
434            MimeMessage mimeMessage = new MimeMessage(in);
435            // Now process body parts & attachments
436            ArrayList<Part> viewables = new ArrayList<Part>();
437            // We'll ignore the attachments, as we'll get them directly from EAS
438            ArrayList<Part> attachments = new ArrayList<Part>();
439            MimeUtility.collectParts(mimeMessage, viewables, attachments);
440            // parseBodyFields fills in the content fields of the Body
441            ConversionUtilities.BodyFieldData data =
442                    ConversionUtilities.parseBodyFields(viewables);
443            // But we need them in the message itself for handling during commit()
444            msg.setFlags(data.isQuotedReply, data.isQuotedForward);
445            msg.mSnippet = data.snippet;
446            msg.mHtml = data.htmlContent;
447            msg.mText = data.textContent;
448        } catch (MessagingException e) {
449            // This would most likely indicate a broken stream
450            throw new IOException(e);
451        }
452    }
453
454    private void attachmentsParser(final ArrayList<EmailContent.Attachment> atts,
455            final EmailContent.Message msg, final int endingTag) throws IOException {
456        while (nextTag(endingTag) != END) {
457            switch (tag) {
458                case Tags.EMAIL_ATTACHMENT:
459                case Tags.BASE_ATTACHMENT:  // BASE_ATTACHMENT is used in EAS 12.0 and up
460                    attachmentParser(atts, msg, tag);
461                    break;
462                default:
463                    skipTag();
464            }
465        }
466    }
467
468    private void attachmentParser(final ArrayList<EmailContent.Attachment> atts,
469            final EmailContent.Message msg, final int endingTag) throws IOException {
470        String fileName = null;
471        String length = null;
472        String location = null;
473        boolean isInline = false;
474        String contentId = null;
475
476        while (nextTag(endingTag) != END) {
477            switch (tag) {
478                // We handle both EAS 2.5 and 12.0+ attachments here
479                case Tags.EMAIL_DISPLAY_NAME:
480                case Tags.BASE_DISPLAY_NAME:
481                    fileName = getValue();
482                    break;
483                case Tags.EMAIL_ATT_NAME:
484                case Tags.BASE_FILE_REFERENCE:
485                    location = getValue();
486                    break;
487                case Tags.EMAIL_ATT_SIZE:
488                case Tags.BASE_ESTIMATED_DATA_SIZE:
489                    length = getValue();
490                    break;
491                case Tags.BASE_IS_INLINE:
492                    isInline = getValueInt() == 1;
493                    break;
494                case Tags.BASE_CONTENT_ID:
495                    contentId = getValue();
496                    break;
497                default:
498                    skipTag();
499            }
500        }
501
502        if ((fileName != null) && (length != null) && (location != null)) {
503            EmailContent.Attachment att = new EmailContent.Attachment();
504            att.mEncoding = "base64";
505            att.mSize = Long.parseLong(length);
506            att.mFileName = fileName;
507            att.mLocation = location;
508            att.mMimeType = getMimeTypeFromFileName(fileName);
509            att.mAccountKey = mAccount.mId;
510            // Save away the contentId, if we've got one (for inline images); note that the
511            // EAS docs appear to be wrong about the tags used; inline images come with
512            // contentId rather than contentLocation, when sent from Ex03, Ex07, and Ex10
513            if (isInline && !TextUtils.isEmpty(contentId)) {
514                att.mContentId = contentId;
515            }
516            // Check if this attachment can't be downloaded due to an account policy
517            if (mPolicy != null) {
518                if (mPolicy.mDontAllowAttachments ||
519                        (mPolicy.mMaxAttachmentSize > 0 &&
520                                (att.mSize > mPolicy.mMaxAttachmentSize))) {
521                    att.mFlags = EmailContent.Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD;
522                }
523            }
524            atts.add(att);
525            msg.mFlagAttachment = true;
526        }
527    }
528
529    /**
530     * Returns an appropriate mimetype for the given file name's extension. If a mimetype
531     * cannot be determined, {@code application/<<x>>} [where @{code <<x>> is the extension,
532     * if it exists or {@code application/octet-stream}].
533     * At the moment, this is somewhat lame, since many file types aren't recognized
534     * @param fileName the file name to ponder
535     */
536    // Note: The MimeTypeMap method currently uses a very limited set of mime types
537    // A bug has been filed against this issue.
538    public String getMimeTypeFromFileName(String fileName) {
539        String mimeType;
540        int lastDot = fileName.lastIndexOf('.');
541        String extension = null;
542        if ((lastDot > 0) && (lastDot < fileName.length() - 1)) {
543            extension = fileName.substring(lastDot + 1).toLowerCase();
544        }
545        if (extension == null) {
546            // A reasonable default for now.
547            mimeType = "application/octet-stream";
548        } else {
549            mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
550            if (mimeType == null) {
551                mimeType = "application/" + extension;
552            }
553        }
554        return mimeType;
555    }
556
557    private Cursor getServerIdCursor(String serverId, String[] projection) {
558        Cursor c = mContentResolver.query(EmailContent.Message.CONTENT_URI, projection,
559                WHERE_SERVER_ID_AND_MAILBOX_KEY, new String[] {serverId, mMailboxIdAsString},
560                null);
561        if (c == null) throw new ProviderUnavailableException();
562        if (c.getCount() > 1) {
563            userLog("Multiple messages with the same serverId/mailbox: " + serverId);
564        }
565        return c;
566    }
567
568    @VisibleForTesting
569    void deleteParser(ArrayList<Long> deletes, int entryTag) throws IOException {
570        while (nextTag(entryTag) != END) {
571            switch (tag) {
572                case Tags.SYNC_SERVER_ID:
573                    String serverId = getValue();
574                    // Find the message in this mailbox with the given serverId
575                    Cursor c = getServerIdCursor(serverId, MESSAGE_ID_SUBJECT_PROJECTION);
576                    try {
577                        if (c.moveToFirst()) {
578                            deletes.add(c.getLong(MESSAGE_ID_SUBJECT_ID_COLUMN));
579                            if (Eas.USER_LOG) {
580                                userLog("Deleting ", serverId + ", "
581                                        + c.getString(MESSAGE_ID_SUBJECT_SUBJECT_COLUMN));
582                            }
583                        }
584                    } finally {
585                        c.close();
586                    }
587                    break;
588                default:
589                    skipTag();
590            }
591        }
592    }
593
594    @VisibleForTesting
595    class ServerChange {
596        final long id;
597        final Boolean read;
598        final Boolean flag;
599        final Integer flags;
600
601        ServerChange(long _id, Boolean _read, Boolean _flag, Integer _flags) {
602            id = _id;
603            read = _read;
604            flag = _flag;
605            flags = _flags;
606        }
607    }
608
609    @VisibleForTesting
610    void changeParser(ArrayList<ServerChange> changes) throws IOException {
611        String serverId = null;
612        Boolean oldRead = false;
613        Boolean oldFlag = false;
614        int flags = 0;
615        long id = 0;
616        while (nextTag(Tags.SYNC_CHANGE) != END) {
617            switch (tag) {
618                case Tags.SYNC_SERVER_ID:
619                    serverId = getValue();
620                    Cursor c = getServerIdCursor(serverId, EmailContent.Message.LIST_PROJECTION);
621                    try {
622                        if (c.moveToFirst()) {
623                            userLog("Changing ", serverId);
624                            oldRead = c.getInt(EmailContent.Message.LIST_READ_COLUMN)
625                                    == EmailContent.Message.READ;
626                            oldFlag = c.getInt(EmailContent.Message.LIST_FAVORITE_COLUMN) == 1;
627                            flags = c.getInt(EmailContent.Message.LIST_FLAGS_COLUMN);
628                            id = c.getLong(EmailContent.Message.LIST_ID_COLUMN);
629                        }
630                    } finally {
631                        c.close();
632                    }
633                    break;
634                case Tags.SYNC_APPLICATION_DATA:
635                    changeApplicationDataParser(changes, oldRead, oldFlag, flags, id);
636                    break;
637                default:
638                    skipTag();
639            }
640        }
641    }
642
643    private void changeApplicationDataParser(ArrayList<ServerChange> changes, Boolean oldRead,
644            Boolean oldFlag, int oldFlags, long id) throws IOException {
645        Boolean read = null;
646        Boolean flag = null;
647        Integer flags = null;
648        while (nextTag(Tags.SYNC_APPLICATION_DATA) != END) {
649            switch (tag) {
650                case Tags.EMAIL_READ:
651                    read = getValueInt() == 1;
652                    break;
653                case Tags.EMAIL_FLAG:
654                    flag = flagParser();
655                    break;
656                case Tags.EMAIL2_LAST_VERB_EXECUTED:
657                    int val = getValueInt();
658                    // Clear out the old replied/forward flags and add in the new flag
659                    flags = oldFlags & ~(EmailContent.Message.FLAG_REPLIED_TO
660                            | EmailContent.Message.FLAG_FORWARDED);
661                    if (val == LAST_VERB_REPLY || val == LAST_VERB_REPLY_ALL) {
662                        // We aren't required to distinguish between reply and reply all here
663                        flags |= EmailContent.Message.FLAG_REPLIED_TO;
664                    } else if (val == LAST_VERB_FORWARD) {
665                        flags |= EmailContent.Message.FLAG_FORWARDED;
666                    }
667                    break;
668                default:
669                    skipTag();
670            }
671        }
672        // See if there are flag changes re: read, flag (favorite) or replied/forwarded
673        if (((read != null) && !oldRead.equals(read)) ||
674                ((flag != null) && !oldFlag.equals(flag)) || (flags != null)) {
675            changes.add(new ServerChange(id, read, flag, flags));
676        }
677    }
678
679    /* (non-Javadoc)
680     * @see com.android.exchange.adapter.EasContentParser#commandsParser()
681     */
682    @Override
683    public void commandsParser() throws IOException, CommandStatusException {
684        while (nextTag(Tags.SYNC_COMMANDS) != END) {
685            if (tag == Tags.SYNC_ADD) {
686                newEmails.add(addParser(tag));
687            } else if (tag == Tags.SYNC_DELETE || tag == Tags.SYNC_SOFT_DELETE) {
688                deleteParser(deletedEmails, tag);
689            } else if (tag == Tags.SYNC_CHANGE) {
690                changeParser(changedEmails);
691            } else
692                skipTag();
693        }
694    }
695
696    // EAS values for status element of sync responses.
697    // TODO: Not all are used yet, but I wanted to transcribe all possible values.
698    public static final int EAS_SYNC_STATUS_SUCCESS = 1;
699    public static final int EAS_SYNC_STATUS_BAD_SYNC_KEY = 3;
700    public static final int EAS_SYNC_STATUS_PROTOCOL_ERROR = 4;
701    public static final int EAS_SYNC_STATUS_SERVER_ERROR = 5;
702    public static final int EAS_SYNC_STATUS_BAD_CLIENT_DATA = 6;
703    public static final int EAS_SYNC_STATUS_CONFLICT = 7;
704    public static final int EAS_SYNC_STATUS_OBJECT_NOT_FOUND = 8;
705    public static final int EAS_SYNC_STATUS_CANNOT_COMPLETE = 9;
706    public static final int EAS_SYNC_STATUS_FOLDER_SYNC_NEEDED = 12;
707    public static final int EAS_SYNC_STATUS_INCOMPLETE_REQUEST = 13;
708    public static final int EAS_SYNC_STATUS_BAD_HEARTBEAT_VALUE = 14;
709    public static final int EAS_SYNC_STATUS_TOO_MANY_COLLECTIONS = 15;
710    public static final int EAS_SYNC_STATUS_RETRY = 16;
711
712    public static boolean shouldRetry(final int status) {
713        return status == EAS_SYNC_STATUS_SERVER_ERROR || status == EAS_SYNC_STATUS_RETRY;
714    }
715
716    /**
717     * Parse the status for a single message update.
718     * @param endTag the tag we end with
719     * @throws IOException
720     */
721    public void messageUpdateParser(int endTag) throws IOException {
722        // We get serverId and status in the responses
723        String serverId = null;
724        int status = -1;
725        while (nextTag(endTag) != END) {
726            if (tag == Tags.SYNC_STATUS) {
727                status = getValueInt();
728            } else if (tag == Tags.SYNC_SERVER_ID) {
729                serverId = getValue();
730            } else {
731                skipTag();
732            }
733        }
734        if (serverId != null && status != -1) {
735            mMessageUpdateStatus.put(serverId, status);
736        }
737    }
738
739    @Override
740    public void responsesParser() throws IOException {
741        while (nextTag(Tags.SYNC_RESPONSES) != END) {
742            if (tag == Tags.SYNC_ADD || tag == Tags.SYNC_CHANGE || tag == Tags.SYNC_DELETE) {
743                messageUpdateParser(tag);
744            } else if (tag == Tags.SYNC_FETCH) {
745                try {
746                    fetchedEmails.add(addParser(tag));
747                } catch (CommandStatusException sse) {
748                    if (sse.mStatus == 8) {
749                        // 8 = object not found; delete the message from EmailProvider
750                        // No other status should be seen in a fetch response, except, perhaps,
751                        // for some temporary server failure
752                        mContentResolver.delete(EmailContent.Message.CONTENT_URI,
753                                WHERE_SERVER_ID_AND_MAILBOX_KEY,
754                                new String[] {sse.mItemId, mMailboxIdAsString});
755                    }
756                }
757            }
758        }
759    }
760
761    @Override
762    protected void wipe() {
763        LogUtils.i(TAG, "Wiping mailbox %s", mMailbox);
764        Mailbox.resyncMailbox(mContentResolver, new android.accounts.Account(mAccount.mEmailAddress,
765                Eas.EXCHANGE_ACCOUNT_MANAGER_TYPE), mMailbox.mId);
766    }
767
768    @Override
769    public boolean parse() throws IOException, CommandStatusException {
770        final boolean result = super.parse();
771        return result || fetchNeeded();
772    }
773
774    /**
775     * Commit all changes. This results in a Binder IPC call which has constraint on the size of
776     * the data, the docs say it currently 1MB. We set a limit to the size of the message we fetch
777     * with {@link Eas#EAS12_TRUNCATION_SIZE} & {@link Eas#EAS12_TRUNCATION_SIZE} which are at 200k
778     * or bellow. As long as these limits are bellow 500k, we should be able to apply a single
779     * message (the transaction size is about double the message size because Java strings are 16
780     * bit.
781     * <b/>
782     * We first try to apply the changes in normal chunk size {@link #MAX_OPS_PER_BATCH}. If we get
783     * a {@link TransactionTooLargeException} we try again with but this time, we apply each change
784     * immediately.
785     */
786    @Override
787    public void commit() throws RemoteException, OperationApplicationException {
788        try {
789            commitImpl(MAX_OPS_PER_BATCH);
790        } catch (TransactionTooLargeException e1) {
791            // Try again but apply batch after every message. The max message size defined in
792            // Eas.EAS12_TRUNCATION_SIZE or Eas.EAS2_5_TRUNCATION_SIZE is small enough to fit
793            // in a single Binder call.
794            LogUtils.w(TAG, e1, "Transaction too large, retrying in single mode");
795            try {
796                commitImpl(1);
797            } catch (TransactionTooLargeException e2) {
798                LogUtils.wtf(TAG, e2, "Transaction too large with batch size one");
799            }
800        }
801    }
802
803    public void commitImpl(int maxOpsPerBatch)
804            throws RemoteException, OperationApplicationException {
805        // Use a batch operation to handle the changes
806        ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
807
808        // Maximum size of message text per fetch
809        int numFetched = fetchedEmails.size();
810        LogUtils.d(TAG, "commitImpl: maxOpsPerBatch=%d numFetched=%d numNew=%d "
811                + "numDeleted=%d numChanged=%d",
812                maxOpsPerBatch,
813                numFetched,
814                newEmails.size(),
815                deletedEmails.size(),
816                changedEmails.size());
817        for (EmailContent.Message msg: fetchedEmails) {
818            // Find the original message's id (by serverId and mailbox)
819            Cursor c = getServerIdCursor(msg.mServerId, EmailContent.ID_PROJECTION);
820            String id = null;
821            try {
822                if (c.moveToFirst()) {
823                    id = c.getString(EmailContent.ID_PROJECTION_COLUMN);
824                    while (c.moveToNext()) {
825                        // This shouldn't happen, but clean up if it does
826                        Long dupId =
827                                Long.parseLong(c.getString(EmailContent.ID_PROJECTION_COLUMN));
828                        userLog("Delete duplicate with id: " + dupId);
829                        deletedEmails.add(dupId);
830                    }
831                }
832            } finally {
833                c.close();
834            }
835
836            // If we find one, we do two things atomically: 1) set the body text for the
837            // message, and 2) mark the message loaded (i.e. completely loaded)
838            if (id != null) {
839                LogUtils.i(TAG, "Fetched body successfully for %s", id);
840                final String[] bindArgument = new String[] {id};
841                ops.add(ContentProviderOperation.newUpdate(EmailContent.Body.CONTENT_URI)
842                        .withSelection(EmailContent.Body.SELECTION_BY_MESSAGE_KEY, bindArgument)
843                        .withValue(EmailContent.BodyColumns.TEXT_CONTENT, msg.mText)
844                        .build());
845                ops.add(ContentProviderOperation.newUpdate(EmailContent.Message.CONTENT_URI)
846                        .withSelection(MessageColumns._ID + "=?", bindArgument)
847                        .withValue(MessageColumns.FLAG_LOADED,
848                                EmailContent.Message.FLAG_LOADED_COMPLETE)
849                        .build());
850            }
851            applyBatchIfNeeded(ops, maxOpsPerBatch, false);
852        }
853
854        for (EmailContent.Message msg: newEmails) {
855            msg.addSaveOps(ops);
856            applyBatchIfNeeded(ops, maxOpsPerBatch, false);
857        }
858
859        for (Long id : deletedEmails) {
860            ops.add(ContentProviderOperation.newDelete(
861                    ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, id)).build());
862            AttachmentUtilities.deleteAllAttachmentFiles(mContext, mAccount.mId, id);
863            applyBatchIfNeeded(ops, maxOpsPerBatch, false);
864        }
865
866        if (!changedEmails.isEmpty()) {
867            // Server wins in a conflict...
868            for (ServerChange change : changedEmails) {
869                ContentValues cv = new ContentValues();
870                if (change.read != null) {
871                    cv.put(EmailContent.MessageColumns.FLAG_READ, change.read);
872                }
873                if (change.flag != null) {
874                    cv.put(EmailContent.MessageColumns.FLAG_FAVORITE, change.flag);
875                }
876                if (change.flags != null) {
877                    cv.put(EmailContent.MessageColumns.FLAGS, change.flags);
878                }
879                ops.add(ContentProviderOperation.newUpdate(
880                        ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, change.id))
881                        .withValues(cv)
882                        .build());
883            }
884            applyBatchIfNeeded(ops, maxOpsPerBatch, false);
885        }
886
887        // We only want to update the sync key here
888        ContentValues mailboxValues = new ContentValues();
889        mailboxValues.put(Mailbox.SYNC_KEY, mMailbox.mSyncKey);
890        ops.add(ContentProviderOperation.newUpdate(
891                ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailbox.mId))
892                .withValues(mailboxValues).build());
893
894        applyBatchIfNeeded(ops, maxOpsPerBatch, true);
895        userLog(mMailbox.mDisplayName, " SyncKey saved as: ", mMailbox.mSyncKey);
896    }
897
898    // Check if there at least MAX_OPS_PER_BATCH ops in queue and flush if there are.
899    // If force is true, flush regardless of size.
900    private void applyBatchIfNeeded(ArrayList<ContentProviderOperation> ops, int maxOpsPerBatch,
901            boolean force)
902            throws RemoteException, OperationApplicationException {
903        if (force ||  ops.size() >= maxOpsPerBatch) {
904            mContentResolver.applyBatch(EmailContent.AUTHORITY, ops);
905            ops.clear();
906        }
907    }
908}
909