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