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