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