1 /** 2 * Copyright (c) 2012, Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.mail.providers; 18 19 import android.content.AsyncQueryHandler; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.net.Uri; 24 import android.os.Parcel; 25 import android.os.Parcelable; 26 import android.provider.BaseColumns; 27 import android.text.Html; 28 import android.text.SpannableString; 29 import android.text.TextUtils; 30 import android.text.util.Linkify; 31 import android.text.util.Rfc822Token; 32 import android.text.util.Rfc822Tokenizer; 33 34 import com.android.emailcommon.internet.MimeHeader; 35 import com.android.emailcommon.internet.MimeMessage; 36 import com.android.emailcommon.internet.MimeUtility; 37 import com.android.emailcommon.mail.Address; 38 import com.android.emailcommon.mail.MessagingException; 39 import com.android.emailcommon.mail.Part; 40 import com.android.emailcommon.utility.ConversionUtilities; 41 import com.android.mail.providers.UIProvider.MessageColumns; 42 import com.android.mail.ui.HtmlMessage; 43 import com.android.mail.utils.HtmlSanitizer; 44 import com.android.mail.utils.Utils; 45 import com.google.common.annotations.VisibleForTesting; 46 import com.google.common.base.Objects; 47 import com.google.common.collect.Lists; 48 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.Date; 52 import java.util.List; 53 import java.util.regex.Pattern; 54 55 56 public class Message implements Parcelable, HtmlMessage { 57 /** 58 * Regex pattern used to look for any inline images in message bodies, including Gmail-hosted 59 * relative-URL images, Gmail emoticons, and any external inline images (although we usually 60 * count on the server to detect external images). 61 */ 62 private static Pattern INLINE_IMAGE_PATTERN = Pattern.compile("<img\\s+[^>]*src=", 63 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 64 65 // regex that matches content id surrounded by "<>" optionally. 66 private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$"); 67 68 /** 69 * @see BaseColumns#_ID 70 */ 71 public long id; 72 /** 73 * @see UIProvider.MessageColumns#SERVER_ID 74 */ 75 public String serverId; 76 /** 77 * @see UIProvider.MessageColumns#URI 78 */ 79 public Uri uri; 80 /** 81 * @see UIProvider.MessageColumns#CONVERSATION_ID 82 */ 83 public Uri conversationUri; 84 /** 85 * @see UIProvider.MessageColumns#SUBJECT 86 */ 87 public String subject; 88 /** 89 * @see UIProvider.MessageColumns#SNIPPET 90 */ 91 public String snippet; 92 /** 93 * @see UIProvider.MessageColumns#FROM 94 */ 95 private String mFrom; 96 /** 97 * @see UIProvider.MessageColumns#TO 98 */ 99 private String mTo; 100 /** 101 * @see UIProvider.MessageColumns#CC 102 */ 103 private String mCc; 104 /** 105 * @see UIProvider.MessageColumns#BCC 106 */ 107 private String mBcc; 108 /** 109 * @see UIProvider.MessageColumns#REPLY_TO 110 */ 111 private String mReplyTo; 112 /** 113 * @see UIProvider.MessageColumns#DATE_RECEIVED_MS 114 */ 115 public long dateReceivedMs; 116 /** 117 * @see UIProvider.MessageColumns#BODY_HTML 118 */ 119 public String bodyHtml; 120 /** 121 * @see UIProvider.MessageColumns#BODY_TEXT 122 */ 123 public String bodyText; 124 /** 125 * @see UIProvider.MessageColumns#EMBEDS_EXTERNAL_RESOURCES 126 */ 127 public boolean embedsExternalResources; 128 /** 129 * @see UIProvider.MessageColumns#REF_MESSAGE_ID 130 */ 131 public Uri refMessageUri; 132 /** 133 * @see UIProvider.MessageColumns#DRAFT_TYPE 134 */ 135 public int draftType; 136 /** 137 * @see UIProvider.MessageColumns#APPEND_REF_MESSAGE_CONTENT 138 */ 139 public boolean appendRefMessageContent; 140 /** 141 * @see UIProvider.MessageColumns#HAS_ATTACHMENTS 142 */ 143 public boolean hasAttachments; 144 /** 145 * @see UIProvider.MessageColumns#ATTACHMENT_LIST_URI 146 */ 147 public Uri attachmentListUri; 148 /** 149 * @see UIProvider.MessageColumns#ATTACHMENT_BY_CID_URI 150 */ 151 public Uri attachmentByCidUri; 152 /** 153 * @see UIProvider.MessageColumns#MESSAGE_FLAGS 154 */ 155 public long messageFlags; 156 /** 157 * @see UIProvider.MessageColumns#ALWAYS_SHOW_IMAGES 158 */ 159 public boolean alwaysShowImages; 160 /** 161 * @see UIProvider.MessageColumns#READ 162 */ 163 public boolean read; 164 /** 165 * @see UIProvider.MessageColumns#SEEN 166 */ 167 public boolean seen; 168 /** 169 * @see UIProvider.MessageColumns#STARRED 170 */ 171 public boolean starred; 172 /** 173 * @see UIProvider.MessageColumns#QUOTE_START_POS 174 */ 175 public int quotedTextOffset; 176 /** 177 * @see UIProvider.MessageColumns#ATTACHMENTS 178 *<p> 179 * N.B. this value is NOT immutable and may change during conversation view render. 180 */ 181 public String attachmentsJson; 182 /** 183 * @see UIProvider.MessageColumns#MESSAGE_ACCOUNT_URI 184 */ 185 public Uri accountUri; 186 /** 187 * @see UIProvider.MessageColumns#EVENT_INTENT_URI 188 */ 189 public Uri eventIntentUri; 190 /** 191 * @see UIProvider.MessageColumns#SPAM_WARNING_STRING 192 */ 193 public String spamWarningString; 194 /** 195 * @see UIProvider.MessageColumns#SPAM_WARNING_LEVEL 196 */ 197 public int spamWarningLevel; 198 /** 199 * @see UIProvider.MessageColumns#SPAM_WARNING_LINK_TYPE 200 */ 201 public int spamLinkType; 202 /** 203 * @see UIProvider.MessageColumns#VIA_DOMAIN 204 */ 205 public String viaDomain; 206 /** 207 * @see UIProvider.MessageColumns#SENDING_STATE 208 */ 209 public int sendingState; 210 211 /** 212 * @see UIProvider.MessageColumns#CLIPPED 213 */ 214 public boolean clipped; 215 /** 216 * @see UIProvider.MessageColumns#PERMALINK 217 */ 218 public String permalink; 219 220 private transient String[] mFromAddresses = null; 221 private transient String[] mToAddresses = null; 222 private transient String[] mCcAddresses = null; 223 private transient String[] mBccAddresses = null; 224 private transient String[] mReplyToAddresses = null; 225 226 private transient List<Attachment> mAttachments = null; 227 228 @Override describeContents()229 public int describeContents() { 230 return 0; 231 } 232 233 @Override equals(Object o)234 public boolean equals(Object o) { 235 return this == o || (o != null && o instanceof Message 236 && Objects.equal(uri, ((Message) o).uri)); 237 } 238 239 @Override hashCode()240 public int hashCode() { 241 return uri == null ? 0 : uri.hashCode(); 242 } 243 244 /** 245 * Helper equality function to check if the two Message objects are equal in terms of 246 * the fields that are visible in ConversationView. 247 * 248 * @param o the Message being compared to 249 * @return True if they are equal in fields, false otherwise 250 */ isEqual(Message o)251 public boolean isEqual(Message o) { 252 return TextUtils.equals(this.getFrom(), o.getFrom()) && 253 this.sendingState == o.sendingState && 254 this.starred == o.starred && 255 this.read == o.read && 256 TextUtils.equals(this.getTo(), o.getTo()) && 257 TextUtils.equals(this.getCc(), o.getCc()) && 258 TextUtils.equals(this.getBcc(), o.getBcc()) && 259 TextUtils.equals(this.subject, o.subject) && 260 TextUtils.equals(this.bodyHtml, o.bodyHtml) && 261 TextUtils.equals(this.bodyText, o.bodyText) && 262 Objects.equal(this.attachmentListUri, o.attachmentListUri) && 263 Objects.equal(getAttachments(), o.getAttachments()); 264 } 265 266 @Override writeToParcel(Parcel dest, int flags)267 public void writeToParcel(Parcel dest, int flags) { 268 dest.writeLong(id); 269 dest.writeString(serverId); 270 dest.writeParcelable(uri, 0); 271 dest.writeParcelable(conversationUri, 0); 272 dest.writeString(subject); 273 dest.writeString(snippet); 274 dest.writeString(mFrom); 275 dest.writeString(mTo); 276 dest.writeString(mCc); 277 dest.writeString(mBcc); 278 dest.writeString(mReplyTo); 279 dest.writeLong(dateReceivedMs); 280 dest.writeString(bodyHtml); 281 dest.writeString(bodyText); 282 dest.writeInt(embedsExternalResources ? 1 : 0); 283 dest.writeParcelable(refMessageUri, 0); 284 dest.writeInt(draftType); 285 dest.writeInt(appendRefMessageContent ? 1 : 0); 286 dest.writeInt(hasAttachments ? 1 : 0); 287 dest.writeParcelable(attachmentListUri, 0); 288 dest.writeLong(messageFlags); 289 dest.writeInt(alwaysShowImages ? 1 : 0); 290 dest.writeInt(quotedTextOffset); 291 dest.writeString(attachmentsJson); 292 dest.writeParcelable(accountUri, 0); 293 dest.writeParcelable(eventIntentUri, 0); 294 dest.writeString(spamWarningString); 295 dest.writeInt(spamWarningLevel); 296 dest.writeInt(spamLinkType); 297 dest.writeString(viaDomain); 298 dest.writeInt(sendingState); 299 dest.writeInt(clipped ? 1 : 0); 300 dest.writeString(permalink); 301 } 302 Message(Parcel in)303 private Message(Parcel in) { 304 id = in.readLong(); 305 serverId = in.readString(); 306 uri = in.readParcelable(null); 307 conversationUri = in.readParcelable(null); 308 subject = in.readString(); 309 snippet = in.readString(); 310 mFrom = in.readString(); 311 mTo = in.readString(); 312 mCc = in.readString(); 313 mBcc = in.readString(); 314 mReplyTo = in.readString(); 315 dateReceivedMs = in.readLong(); 316 bodyHtml = in.readString(); 317 bodyText = in.readString(); 318 embedsExternalResources = in.readInt() != 0; 319 refMessageUri = in.readParcelable(null); 320 draftType = in.readInt(); 321 appendRefMessageContent = in.readInt() != 0; 322 hasAttachments = in.readInt() != 0; 323 attachmentListUri = in.readParcelable(null); 324 messageFlags = in.readLong(); 325 alwaysShowImages = in.readInt() != 0; 326 quotedTextOffset = in.readInt(); 327 attachmentsJson = in.readString(); 328 accountUri = in.readParcelable(null); 329 eventIntentUri = in.readParcelable(null); 330 spamWarningString = in.readString(); 331 spamWarningLevel = in.readInt(); 332 spamLinkType = in.readInt(); 333 viaDomain = in.readString(); 334 sendingState = in.readInt(); 335 clipped = in.readInt() != 0; 336 permalink = in.readString(); 337 } 338 Message()339 public Message() { 340 341 } 342 343 @Override toString()344 public String toString() { 345 return "[message id=" + id + "]"; 346 } 347 348 public static final Creator<Message> CREATOR = new Creator<Message>() { 349 350 @Override 351 public Message createFromParcel(Parcel source) { 352 return new Message(source); 353 } 354 355 @Override 356 public Message[] newArray(int size) { 357 return new Message[size]; 358 } 359 360 }; 361 Message(Cursor cursor)362 public Message(Cursor cursor) { 363 if (cursor != null) { 364 id = cursor.getLong(UIProvider.MESSAGE_ID_COLUMN); 365 serverId = cursor.getString(UIProvider.MESSAGE_SERVER_ID_COLUMN); 366 final String messageUriStr = cursor.getString(UIProvider.MESSAGE_URI_COLUMN); 367 uri = !TextUtils.isEmpty(messageUriStr) ? Uri.parse(messageUriStr) : null; 368 final String convUriStr = cursor.getString(UIProvider.MESSAGE_CONVERSATION_URI_COLUMN); 369 conversationUri = !TextUtils.isEmpty(convUriStr) ? Uri.parse(convUriStr) : null; 370 subject = cursor.getString(UIProvider.MESSAGE_SUBJECT_COLUMN); 371 snippet = cursor.getString(UIProvider.MESSAGE_SNIPPET_COLUMN); 372 mFrom = cursor.getString(UIProvider.MESSAGE_FROM_COLUMN); 373 mTo = cursor.getString(UIProvider.MESSAGE_TO_COLUMN); 374 mCc = cursor.getString(UIProvider.MESSAGE_CC_COLUMN); 375 mBcc = cursor.getString(UIProvider.MESSAGE_BCC_COLUMN); 376 mReplyTo = cursor.getString(UIProvider.MESSAGE_REPLY_TO_COLUMN); 377 dateReceivedMs = cursor.getLong(UIProvider.MESSAGE_DATE_RECEIVED_MS_COLUMN); 378 bodyHtml = cursor.getString(UIProvider.MESSAGE_BODY_HTML_COLUMN); 379 bodyText = cursor.getString(UIProvider.MESSAGE_BODY_TEXT_COLUMN); 380 embedsExternalResources = cursor 381 .getInt(UIProvider.MESSAGE_EMBEDS_EXTERNAL_RESOURCES_COLUMN) != 0; 382 final String refMessageUriStr = 383 cursor.getString(UIProvider.MESSAGE_REF_MESSAGE_URI_COLUMN); 384 refMessageUri = !TextUtils.isEmpty(refMessageUriStr) ? 385 Uri.parse(refMessageUriStr) : null; 386 draftType = cursor.getInt(UIProvider.MESSAGE_DRAFT_TYPE_COLUMN); 387 appendRefMessageContent = cursor 388 .getInt(UIProvider.MESSAGE_APPEND_REF_MESSAGE_CONTENT_COLUMN) != 0; 389 hasAttachments = cursor.getInt(UIProvider.MESSAGE_HAS_ATTACHMENTS_COLUMN) != 0; 390 final String attachmentsUri = cursor 391 .getString(UIProvider.MESSAGE_ATTACHMENT_LIST_URI_COLUMN); 392 attachmentListUri = hasAttachments && !TextUtils.isEmpty(attachmentsUri) ? Uri 393 .parse(attachmentsUri) : null; 394 final String attachmentsByCidUri = cursor 395 .getString(UIProvider.MESSAGE_ATTACHMENT_BY_CID_URI_COLUMN); 396 attachmentByCidUri = hasAttachments && !TextUtils.isEmpty(attachmentsByCidUri) ? 397 Uri.parse(attachmentsByCidUri) : null; 398 messageFlags = cursor.getLong(UIProvider.MESSAGE_FLAGS_COLUMN); 399 alwaysShowImages = cursor.getInt(UIProvider.MESSAGE_ALWAYS_SHOW_IMAGES_COLUMN) != 0; 400 read = cursor.getInt(UIProvider.MESSAGE_READ_COLUMN) != 0; 401 seen = cursor.getInt(UIProvider.MESSAGE_SEEN_COLUMN) != 0; 402 starred = cursor.getInt(UIProvider.MESSAGE_STARRED_COLUMN) != 0; 403 quotedTextOffset = cursor.getInt(UIProvider.QUOTED_TEXT_OFFSET_COLUMN); 404 attachmentsJson = cursor.getString(UIProvider.MESSAGE_ATTACHMENTS_COLUMN); 405 String accountUriString = cursor.getString(UIProvider.MESSAGE_ACCOUNT_URI_COLUMN); 406 accountUri = !TextUtils.isEmpty(accountUriString) ? Uri.parse(accountUriString) : null; 407 eventIntentUri = 408 Utils.getValidUri(cursor.getString(UIProvider.MESSAGE_EVENT_INTENT_COLUMN)); 409 spamWarningString = 410 cursor.getString(UIProvider.MESSAGE_SPAM_WARNING_STRING_ID_COLUMN); 411 spamWarningLevel = cursor.getInt(UIProvider.MESSAGE_SPAM_WARNING_LEVEL_COLUMN); 412 spamLinkType = cursor.getInt(UIProvider.MESSAGE_SPAM_WARNING_LINK_TYPE_COLUMN); 413 viaDomain = cursor.getString(UIProvider.MESSAGE_VIA_DOMAIN_COLUMN); 414 sendingState = cursor.getInt(UIProvider.MESSAGE_SENDING_STATE_COLUMN); 415 clipped = cursor.getInt(UIProvider.MESSAGE_CLIPPED_COLUMN) != 0; 416 permalink = cursor.getString(UIProvider.MESSAGE_PERMALINK_COLUMN); 417 } 418 } 419 420 /** 421 * This constructor exists solely to generate Message objects from .eml attachments. 422 */ Message(Context context, MimeMessage mimeMessage, Uri emlFileUri)423 public Message(Context context, MimeMessage mimeMessage, Uri emlFileUri) 424 throws MessagingException { 425 // Set message header values. 426 setFrom(Address.toHeader(mimeMessage.getFrom())); 427 setTo(Address.toHeader(mimeMessage.getRecipients( 428 com.android.emailcommon.mail.Message.RecipientType.TO))); 429 setCc(Address.toHeader(mimeMessage.getRecipients( 430 com.android.emailcommon.mail.Message.RecipientType.CC))); 431 setBcc(Address.toHeader(mimeMessage.getRecipients( 432 com.android.emailcommon.mail.Message.RecipientType.BCC))); 433 setReplyTo(Address.toHeader(mimeMessage.getReplyTo())); 434 subject = mimeMessage.getSubject(); 435 436 final Date sentDate = mimeMessage.getSentDate(); 437 final Date internalDate = mimeMessage.getInternalDate(); 438 if (sentDate != null) { 439 dateReceivedMs = sentDate.getTime(); 440 } else if (internalDate != null) { 441 dateReceivedMs = internalDate.getTime(); 442 } else { 443 dateReceivedMs = System.currentTimeMillis(); 444 } 445 446 // for now, always set defaults 447 alwaysShowImages = false; 448 viaDomain = null; 449 draftType = UIProvider.DraftType.NOT_A_DRAFT; 450 sendingState = UIProvider.ConversationSendingState.OTHER; 451 starred = false; 452 spamWarningString = null; 453 messageFlags = 0; 454 clipped = false; 455 permalink = null; 456 hasAttachments = false; 457 458 // body values (snippet/bodyText/bodyHtml) 459 // Now process body parts & attachments 460 ArrayList<Part> viewables = new ArrayList<Part>(); 461 ArrayList<Part> attachments = new ArrayList<Part>(); 462 MimeUtility.collectParts(mimeMessage, viewables, attachments); 463 464 ConversionUtilities.BodyFieldData data = ConversionUtilities.parseBodyFields(viewables); 465 466 snippet = data.snippet; 467 bodyText = data.textContent; 468 469 // sanitize the HTML found within the .eml file before consuming it 470 bodyHtml = HtmlSanitizer.sanitizeHtml(data.htmlContent); 471 472 // populate mAttachments 473 mAttachments = Lists.newArrayList(); 474 475 final String messageId = mimeMessage.getMessageId(); 476 477 int partId = 0; 478 for (final Part attachmentPart : attachments) { 479 mAttachments.add(new Attachment(context, attachmentPart, 480 emlFileUri, messageId, Integer.toString(partId++), false /* inline */)); 481 } 482 483 // instantiating an Attachment for each viewable will cause it to be registered within the 484 // EmlAttachmentProvider for later access when displaying inline attachments 485 for (final Part viewablePart : viewables) { 486 final String[] cids = viewablePart.getHeader(MimeHeader.HEADER_CONTENT_ID); 487 if (cids != null && cids.length == 1) { 488 final String cid = REMOVE_OPTIONAL_BRACKETS.matcher(cids[0]).replaceAll("$1"); 489 mAttachments.add(new Attachment(context, viewablePart, emlFileUri, messageId, cid, 490 true /* inline */)); 491 } 492 } 493 494 hasAttachments = !mAttachments.isEmpty(); 495 496 attachmentListUri = hasAttachments ? 497 EmlAttachmentProvider.getAttachmentsListUri(emlFileUri, messageId) : null; 498 499 attachmentByCidUri = EmlAttachmentProvider.getAttachmentByCidUri(emlFileUri, messageId); 500 } 501 isFlaggedReplied()502 public boolean isFlaggedReplied() { 503 return (messageFlags & UIProvider.MessageFlags.REPLIED) == 504 UIProvider.MessageFlags.REPLIED; 505 } 506 isFlaggedForwarded()507 public boolean isFlaggedForwarded() { 508 return (messageFlags & UIProvider.MessageFlags.FORWARDED) == 509 UIProvider.MessageFlags.FORWARDED; 510 } 511 isFlaggedCalendarInvite()512 public boolean isFlaggedCalendarInvite() { 513 return (messageFlags & UIProvider.MessageFlags.CALENDAR_INVITE) == 514 UIProvider.MessageFlags.CALENDAR_INVITE; 515 } 516 getFrom()517 public String getFrom() { 518 return mFrom; 519 } 520 setFrom(final String from)521 public synchronized void setFrom(final String from) { 522 mFrom = from; 523 mFromAddresses = null; 524 } 525 getTo()526 public String getTo() { 527 return mTo; 528 } 529 setTo(final String to)530 public synchronized void setTo(final String to) { 531 mTo = to; 532 mToAddresses = null; 533 } 534 getCc()535 public String getCc() { 536 return mCc; 537 } 538 setCc(final String cc)539 public synchronized void setCc(final String cc) { 540 mCc = cc; 541 mCcAddresses = null; 542 } 543 getBcc()544 public String getBcc() { 545 return mBcc; 546 } 547 setBcc(final String bcc)548 public synchronized void setBcc(final String bcc) { 549 mBcc = bcc; 550 mBccAddresses = null; 551 } 552 553 @VisibleForTesting getReplyTo()554 public String getReplyTo() { 555 return mReplyTo; 556 } 557 setReplyTo(final String replyTo)558 public synchronized void setReplyTo(final String replyTo) { 559 mReplyTo = replyTo; 560 mReplyToAddresses = null; 561 } 562 getFromAddresses()563 public synchronized String[] getFromAddresses() { 564 if (mFromAddresses == null) { 565 mFromAddresses = tokenizeAddresses(mFrom); 566 } 567 return mFromAddresses; 568 } 569 getFromAddressesUnescaped()570 public String[] getFromAddressesUnescaped() { 571 return unescapeAddresses(getFromAddresses()); 572 } 573 getToAddresses()574 public synchronized String[] getToAddresses() { 575 if (mToAddresses == null) { 576 mToAddresses = tokenizeAddresses(mTo); 577 } 578 return mToAddresses; 579 } 580 getToAddressesUnescaped()581 public String[] getToAddressesUnescaped() { 582 return unescapeAddresses(getToAddresses()); 583 } 584 getCcAddresses()585 public synchronized String[] getCcAddresses() { 586 if (mCcAddresses == null) { 587 mCcAddresses = tokenizeAddresses(mCc); 588 } 589 return mCcAddresses; 590 } 591 getCcAddressesUnescaped()592 public String[] getCcAddressesUnescaped() { 593 return unescapeAddresses(getCcAddresses()); 594 } 595 getBccAddresses()596 public synchronized String[] getBccAddresses() { 597 if (mBccAddresses == null) { 598 mBccAddresses = tokenizeAddresses(mBcc); 599 } 600 return mBccAddresses; 601 } 602 getBccAddressesUnescaped()603 public String[] getBccAddressesUnescaped() { 604 return unescapeAddresses(getBccAddresses()); 605 } 606 getReplyToAddresses()607 public synchronized String[] getReplyToAddresses() { 608 if (mReplyToAddresses == null) { 609 mReplyToAddresses = tokenizeAddresses(mReplyTo); 610 } 611 return mReplyToAddresses; 612 } 613 getReplyToAddressesUnescaped()614 public String[] getReplyToAddressesUnescaped() { 615 return unescapeAddresses(getReplyToAddresses()); 616 } 617 unescapeAddresses(String[] escaped)618 private static String[] unescapeAddresses(String[] escaped) { 619 final String[] unescaped = new String[escaped.length]; 620 for (int i = 0; i < escaped.length; i++) { 621 final String escapeMore = escaped[i].replace("<", "<").replace(">", ">"); 622 unescaped[i] = Html.fromHtml(escapeMore).toString(); 623 } 624 return unescaped; 625 } 626 tokenizeAddresses(String addresses)627 public static String[] tokenizeAddresses(String addresses) { 628 if (TextUtils.isEmpty(addresses)) { 629 return new String[0]; 630 } 631 632 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addresses); 633 String[] strings = new String[tokens.length]; 634 for (int i = 0; i < tokens.length;i++) { 635 strings[i] = tokens[i].toString(); 636 } 637 return strings; 638 } 639 getAttachments()640 public List<Attachment> getAttachments() { 641 if (mAttachments == null) { 642 if (attachmentsJson != null) { 643 mAttachments = Attachment.fromJSONArray(attachmentsJson); 644 } else { 645 mAttachments = Collections.emptyList(); 646 } 647 } 648 return mAttachments; 649 } 650 651 /** 652 * Returns the number of attachments in the message. 653 * @param includeInline If {@code true}, includes inline attachments in the count. 654 * {@code false}, otherwise. 655 * @return the number of attachments in the message. 656 */ getAttachmentCount(boolean includeInline)657 public int getAttachmentCount(boolean includeInline) { 658 // If include inline, just return the full list count. 659 if (includeInline) { 660 return getAttachments().size(); 661 } 662 663 // Otherwise, iterate through the attachment list, 664 // skipping inline attachments. 665 int numAttachments = 0; 666 final List<Attachment> attachments = getAttachments(); 667 for (int i = 0, size = attachments.size(); i < size; i++) { 668 if (attachments.get(i).isInlineAttachment()) { 669 continue; 670 } 671 numAttachments++; 672 } 673 674 return numAttachments; 675 } 676 677 /** 678 * Returns whether a "Show Pictures" button should initially appear for this message. If the 679 * button is shown, the message must also block all non-local images in the body. Inversely, if 680 * the button is not shown, the message must show all images within (or else the user would be 681 * stuck with no images and no way to reveal them). 682 * 683 * @return true if a "Show Pictures" button should appear. 684 */ shouldShowImagePrompt()685 public boolean shouldShowImagePrompt() { 686 return !alwaysShowImages && (embedsExternalResources || 687 (!TextUtils.isEmpty(bodyHtml) && INLINE_IMAGE_PATTERN.matcher(bodyHtml).find())); 688 } 689 690 @Override embedsExternalResources()691 public boolean embedsExternalResources() { 692 return embedsExternalResources; 693 } 694 695 /** 696 * Helper method to command a provider to mark all messages from this sender with the 697 * {@link MessageColumns#ALWAYS_SHOW_IMAGES} flag set. 698 * 699 * @param handler a caller-provided handler to run the query on 700 * @param token (optional) token to identify the command to the handler 701 * @param cookie (optional) cookie to pass to the handler 702 */ markAlwaysShowImages(AsyncQueryHandler handler, int token, Object cookie)703 public void markAlwaysShowImages(AsyncQueryHandler handler, int token, Object cookie) { 704 alwaysShowImages = true; 705 706 final ContentValues values = new ContentValues(1); 707 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, 1); 708 709 handler.startUpdate(token, cookie, uri, values, null, null); 710 } 711 712 @Override getBodyAsHtml()713 public String getBodyAsHtml() { 714 String body = ""; 715 if (!TextUtils.isEmpty(bodyHtml)) { 716 body = bodyHtml; 717 } else if (!TextUtils.isEmpty(bodyText)) { 718 final SpannableString spannable = new SpannableString(bodyText); 719 Linkify.addLinks(spannable, Linkify.EMAIL_ADDRESSES); 720 body = Html.toHtml(spannable); 721 } 722 return body; 723 } 724 725 @Override getId()726 public long getId() { 727 return id; 728 } 729 isDraft()730 public boolean isDraft() { 731 return draftType != UIProvider.DraftType.NOT_A_DRAFT; 732 } 733 } 734