• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
20 package com.android.exchange.adapter;
21 
22 import android.content.ContentProviderOperation;
23 import android.content.ContentResolver;
24 import android.content.ContentUris;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.OperationApplicationException;
28 import android.database.Cursor;
29 import android.net.Uri;
30 import android.os.RemoteException;
31 import android.os.TransactionTooLargeException;
32 import android.provider.CalendarContract.Events;
33 import android.text.Html;
34 import android.text.SpannedString;
35 import android.text.TextUtils;
36 import android.util.Base64;
37 import android.webkit.MimeTypeMap;
38 
39 import com.android.emailcommon.internet.MimeMessage;
40 import com.android.emailcommon.internet.MimeUtility;
41 import com.android.emailcommon.mail.Address;
42 import com.android.emailcommon.mail.MeetingInfo;
43 import com.android.emailcommon.mail.MessagingException;
44 import com.android.emailcommon.mail.PackedString;
45 import com.android.emailcommon.mail.Part;
46 import com.android.emailcommon.provider.Account;
47 import com.android.emailcommon.provider.EmailContent;
48 import com.android.emailcommon.provider.EmailContent.AccountColumns;
49 import com.android.emailcommon.provider.EmailContent.Attachment;
50 import com.android.emailcommon.provider.EmailContent.Body;
51 import com.android.emailcommon.provider.EmailContent.MailboxColumns;
52 import com.android.emailcommon.provider.EmailContent.Message;
53 import com.android.emailcommon.provider.EmailContent.MessageColumns;
54 import com.android.emailcommon.provider.EmailContent.SyncColumns;
55 import com.android.emailcommon.provider.Mailbox;
56 import com.android.emailcommon.provider.Policy;
57 import com.android.emailcommon.provider.ProviderUnavailableException;
58 import com.android.emailcommon.service.SyncWindow;
59 import com.android.emailcommon.utility.AttachmentUtilities;
60 import com.android.emailcommon.utility.ConversionUtilities;
61 import com.android.emailcommon.utility.TextUtilities;
62 import com.android.emailcommon.utility.Utility;
63 import com.android.exchange.CommandStatusException;
64 import com.android.exchange.Eas;
65 import com.android.exchange.EasResponse;
66 import com.android.exchange.EasSyncService;
67 import com.android.exchange.MessageMoveRequest;
68 import com.android.exchange.R;
69 import com.android.exchange.utility.CalendarUtilities;
70 import com.android.mail.utils.LogUtils;
71 import com.google.common.annotations.VisibleForTesting;
72 
73 import org.apache.http.HttpStatus;
74 import org.apache.http.entity.ByteArrayEntity;
75 
76 import java.io.ByteArrayInputStream;
77 import java.io.IOException;
78 import java.io.InputStream;
79 import java.util.ArrayList;
80 import java.util.Calendar;
81 import java.util.GregorianCalendar;
82 import java.util.TimeZone;
83 
84 /**
85  * Sync adapter for EAS email
86  *
87  */
88 public 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 
EmailSyncAdapter(EasSyncService service)132     public EmailSyncAdapter(EasSyncService service) {
133         super(service);
134     }
135 
136     @Override
wipe()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 
getEmailFilter()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 
FetchRequest(long _messageId, String _serverId)183         FetchRequest(long _messageId, String _serverId) {
184             messageId = _messageId;
185             serverId = _serverId;
186         }
187     }
188 
189     @Override
sendSyncOptions(Double protocolVersion, Serializer s, boolean initialSync)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
parse(InputStream is)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
getAutomaticLookback()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 
GetItemEstimateParser(InputStream in)338         public GetItemEstimateParser(InputStream in) throws IOException {
339             super(in);
340         }
341 
342         @Override
parse()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 
parseGetItemEstimate()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 
parseResponse()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 
parseCollection()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      */
getEstimate(String filter)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();
putFromMeeting(ps, MeetingInfo.MEETING_LOCATION, values, Events.EVENT_LOCATION)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);
values.put(Events.DTSTART, startTime)637                             values.put(Events.DTSTART, startTime);
638                         }
putFromMeeting(ps, MeetingInfo.MEETING_ALL_DAY, values, Events.ALL_DAY)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