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