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