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.MimeMessage; 35 import com.android.emailcommon.internet.MimeUtility; 36 import com.android.emailcommon.mail.MessagingException; 37 import com.android.emailcommon.mail.Part; 38 import com.android.emailcommon.utility.ConversionUtilities; 39 import com.android.mail.providers.UIProvider.MessageColumns; 40 import com.android.mail.ui.HtmlMessage; 41 import com.android.mail.utils.Utils; 42 import com.google.common.annotations.VisibleForTesting; 43 import com.google.common.base.Objects; 44 import com.google.common.collect.Lists; 45 46 import java.util.ArrayList; 47 import java.util.Collections; 48 import java.util.List; 49 import java.util.regex.Pattern; 50 51 52 public class Message implements Parcelable, HtmlMessage { 53 /** 54 * Regex pattern used to look for any inline images in message bodies, including Gmail-hosted 55 * relative-URL images, Gmail emoticons, and any external inline images (although we usually 56 * count on the server to detect external images). 57 */ 58 private static Pattern INLINE_IMAGE_PATTERN = Pattern.compile("<img\\s+[^>]*src=", 59 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 60 61 /** 62 * @see BaseColumns#_ID 63 */ 64 public long id; 65 /** 66 * @see UIProvider.MessageColumns#SERVER_ID 67 */ 68 public String serverId; 69 /** 70 * @see UIProvider.MessageColumns#URI 71 */ 72 public Uri uri; 73 /** 74 * @see UIProvider.MessageColumns#CONVERSATION_ID 75 */ 76 public Uri conversationUri; 77 /** 78 * @see UIProvider.MessageColumns#SUBJECT 79 */ 80 public String subject; 81 /** 82 * @see UIProvider.MessageColumns#SNIPPET 83 */ 84 public String snippet; 85 /** 86 * @see UIProvider.MessageColumns#FROM 87 */ 88 private String mFrom; 89 /** 90 * @see UIProvider.MessageColumns#TO 91 */ 92 private String mTo; 93 /** 94 * @see UIProvider.MessageColumns#CC 95 */ 96 private String mCc; 97 /** 98 * @see UIProvider.MessageColumns#BCC 99 */ 100 private String mBcc; 101 /** 102 * @see UIProvider.MessageColumns#REPLY_TO 103 */ 104 private String mReplyTo; 105 /** 106 * @see UIProvider.MessageColumns#DATE_RECEIVED_MS 107 */ 108 public long dateReceivedMs; 109 /** 110 * @see UIProvider.MessageColumns#BODY_HTML 111 */ 112 public String bodyHtml; 113 /** 114 * @see UIProvider.MessageColumns#BODY_TEXT 115 */ 116 public String bodyText; 117 /** 118 * @see UIProvider.MessageColumns#EMBEDS_EXTERNAL_RESOURCES 119 */ 120 public boolean embedsExternalResources; 121 /** 122 * @see UIProvider.MessageColumns#REF_MESSAGE_ID 123 */ 124 public Uri refMessageUri; 125 /** 126 * @see UIProvider.MessageColumns#DRAFT_TYPE 127 */ 128 public int draftType; 129 /** 130 * @see UIProvider.MessageColumns#APPEND_REF_MESSAGE_CONTENT 131 */ 132 public boolean appendRefMessageContent; 133 /** 134 * @see UIProvider.MessageColumns#HAS_ATTACHMENTS 135 */ 136 public boolean hasAttachments; 137 /** 138 * @see UIProvider.MessageColumns#ATTACHMENT_LIST_URI 139 */ 140 public Uri attachmentListUri; 141 /** 142 * @see UIProvider.MessageColumns#MESSAGE_FLAGS 143 */ 144 public long messageFlags; 145 /** 146 * @see UIProvider.MessageColumns#ALWAYS_SHOW_IMAGES 147 */ 148 public boolean alwaysShowImages; 149 /** 150 * @see UIProvider.MessageColumns#READ 151 */ 152 public boolean read; 153 /** 154 * @see UIProvider.MessageColumns#SEEN 155 */ 156 public boolean seen; 157 /** 158 * @see UIProvider.MessageColumns#STARRED 159 */ 160 public boolean starred; 161 /** 162 * @see UIProvider.MessageColumns#QUOTE_START_POS 163 */ 164 public int quotedTextOffset; 165 /** 166 * @see UIProvider.MessageColumns#ATTACHMENTS 167 *<p> 168 * N.B. this value is NOT immutable and may change during conversation view render. 169 */ 170 public String attachmentsJson; 171 /** 172 * @see UIProvider.MessageColumns#MESSAGE_ACCOUNT_URI 173 */ 174 public Uri accountUri; 175 /** 176 * @see UIProvider.MessageColumns#EVENT_INTENT_URI 177 */ 178 public Uri eventIntentUri; 179 /** 180 * @see UIProvider.MessageColumns#SPAM_WARNING_STRING 181 */ 182 public String spamWarningString; 183 /** 184 * @see UIProvider.MessageColumns#SPAM_WARNING_LEVEL 185 */ 186 public int spamWarningLevel; 187 /** 188 * @see UIProvider.MessageColumns#SPAM_WARNING_LINK_TYPE 189 */ 190 public int spamLinkType; 191 /** 192 * @see UIProvider.MessageColumns#VIA_DOMAIN 193 */ 194 public String viaDomain; 195 /** 196 * @see UIProvider.MessageColumns#IS_SENDING 197 */ 198 public boolean isSending; 199 200 private transient String[] mFromAddresses = null; 201 private transient String[] mToAddresses = null; 202 private transient String[] mCcAddresses = null; 203 private transient String[] mBccAddresses = null; 204 private transient String[] mReplyToAddresses = null; 205 206 private transient List<Attachment> mAttachments = null; 207 208 @Override 209 public int describeContents() { 210 return 0; 211 } 212 213 @Override 214 public boolean equals(Object o) { 215 return this == o || (o != null && o instanceof Message 216 && Objects.equal(uri, ((Message) o).uri)); 217 } 218 219 @Override 220 public int hashCode() { 221 return uri == null ? 0 : uri.hashCode(); 222 } 223 224 @Override 225 public void writeToParcel(Parcel dest, int flags) { 226 dest.writeLong(id); 227 dest.writeString(serverId); 228 dest.writeParcelable(uri, 0); 229 dest.writeParcelable(conversationUri, 0); 230 dest.writeString(subject); 231 dest.writeString(snippet); 232 dest.writeString(mFrom); 233 dest.writeString(mTo); 234 dest.writeString(mCc); 235 dest.writeString(mBcc); 236 dest.writeString(mReplyTo); 237 dest.writeLong(dateReceivedMs); 238 dest.writeString(bodyHtml); 239 dest.writeString(bodyText); 240 dest.writeInt(embedsExternalResources ? 1 : 0); 241 dest.writeParcelable(refMessageUri, 0); 242 dest.writeInt(draftType); 243 dest.writeInt(appendRefMessageContent ? 1 : 0); 244 dest.writeInt(hasAttachments ? 1 : 0); 245 dest.writeParcelable(attachmentListUri, 0); 246 dest.writeLong(messageFlags); 247 dest.writeInt(alwaysShowImages ? 1 : 0); 248 dest.writeInt(quotedTextOffset); 249 dest.writeString(attachmentsJson); 250 dest.writeParcelable(accountUri, 0); 251 dest.writeParcelable(eventIntentUri, 0); 252 dest.writeString(spamWarningString); 253 dest.writeInt(spamWarningLevel); 254 dest.writeInt(spamLinkType); 255 dest.writeString(viaDomain); 256 dest.writeInt(isSending ? 1 : 0); 257 } 258 259 private Message(Parcel in) { 260 id = in.readLong(); 261 serverId = in.readString(); 262 uri = in.readParcelable(null); 263 conversationUri = in.readParcelable(null); 264 subject = in.readString(); 265 snippet = in.readString(); 266 mFrom = in.readString(); 267 mTo = in.readString(); 268 mCc = in.readString(); 269 mBcc = in.readString(); 270 mReplyTo = in.readString(); 271 dateReceivedMs = in.readLong(); 272 bodyHtml = in.readString(); 273 bodyText = in.readString(); 274 embedsExternalResources = in.readInt() != 0; 275 refMessageUri = in.readParcelable(null); 276 draftType = in.readInt(); 277 appendRefMessageContent = in.readInt() != 0; 278 hasAttachments = in.readInt() != 0; 279 attachmentListUri = in.readParcelable(null); 280 messageFlags = in.readLong(); 281 alwaysShowImages = in.readInt() != 0; 282 quotedTextOffset = in.readInt(); 283 attachmentsJson = in.readString(); 284 accountUri = in.readParcelable(null); 285 eventIntentUri = in.readParcelable(null); 286 spamWarningString = in.readString(); 287 spamWarningLevel = in.readInt(); 288 spamLinkType = in.readInt(); 289 viaDomain = in.readString(); 290 isSending = in.readInt() != 0; 291 } 292 293 public Message() { 294 295 } 296 297 @Override 298 public String toString() { 299 return "[message id=" + id + "]"; 300 } 301 302 public static final Creator<Message> CREATOR = new Creator<Message>() { 303 304 @Override 305 public Message createFromParcel(Parcel source) { 306 return new Message(source); 307 } 308 309 @Override 310 public Message[] newArray(int size) { 311 return new Message[size]; 312 } 313 314 }; 315 316 public Message(Cursor cursor) { 317 if (cursor != null) { 318 id = cursor.getLong(UIProvider.MESSAGE_ID_COLUMN); 319 serverId = cursor.getString(UIProvider.MESSAGE_SERVER_ID_COLUMN); 320 final String messageUriStr = cursor.getString(UIProvider.MESSAGE_URI_COLUMN); 321 uri = !TextUtils.isEmpty(messageUriStr) ? Uri.parse(messageUriStr) : null; 322 final String convUriStr = cursor.getString(UIProvider.MESSAGE_CONVERSATION_URI_COLUMN); 323 conversationUri = !TextUtils.isEmpty(convUriStr) ? Uri.parse(convUriStr) : null; 324 subject = cursor.getString(UIProvider.MESSAGE_SUBJECT_COLUMN); 325 snippet = cursor.getString(UIProvider.MESSAGE_SNIPPET_COLUMN); 326 mFrom = cursor.getString(UIProvider.MESSAGE_FROM_COLUMN); 327 mTo = cursor.getString(UIProvider.MESSAGE_TO_COLUMN); 328 mCc = cursor.getString(UIProvider.MESSAGE_CC_COLUMN); 329 mBcc = cursor.getString(UIProvider.MESSAGE_BCC_COLUMN); 330 mReplyTo = cursor.getString(UIProvider.MESSAGE_REPLY_TO_COLUMN); 331 dateReceivedMs = cursor.getLong(UIProvider.MESSAGE_DATE_RECEIVED_MS_COLUMN); 332 bodyHtml = cursor.getString(UIProvider.MESSAGE_BODY_HTML_COLUMN); 333 bodyText = cursor.getString(UIProvider.MESSAGE_BODY_TEXT_COLUMN); 334 embedsExternalResources = cursor 335 .getInt(UIProvider.MESSAGE_EMBEDS_EXTERNAL_RESOURCES_COLUMN) != 0; 336 final String refMessageUriStr = 337 cursor.getString(UIProvider.MESSAGE_REF_MESSAGE_URI_COLUMN); 338 refMessageUri = !TextUtils.isEmpty(refMessageUriStr) ? 339 Uri.parse(refMessageUriStr) : null; 340 draftType = cursor.getInt(UIProvider.MESSAGE_DRAFT_TYPE_COLUMN); 341 appendRefMessageContent = cursor 342 .getInt(UIProvider.MESSAGE_APPEND_REF_MESSAGE_CONTENT_COLUMN) != 0; 343 hasAttachments = cursor.getInt(UIProvider.MESSAGE_HAS_ATTACHMENTS_COLUMN) != 0; 344 final String attachmentsUri = cursor 345 .getString(UIProvider.MESSAGE_ATTACHMENT_LIST_URI_COLUMN); 346 attachmentListUri = hasAttachments && !TextUtils.isEmpty(attachmentsUri) ? Uri 347 .parse(attachmentsUri) : null; 348 messageFlags = cursor.getLong(UIProvider.MESSAGE_FLAGS_COLUMN); 349 alwaysShowImages = cursor.getInt(UIProvider.MESSAGE_ALWAYS_SHOW_IMAGES_COLUMN) != 0; 350 read = cursor.getInt(UIProvider.MESSAGE_READ_COLUMN) != 0; 351 seen = cursor.getInt(UIProvider.MESSAGE_SEEN_COLUMN) != 0; 352 starred = cursor.getInt(UIProvider.MESSAGE_STARRED_COLUMN) != 0; 353 quotedTextOffset = cursor.getInt(UIProvider.QUOTED_TEXT_OFFSET_COLUMN); 354 attachmentsJson = cursor.getString(UIProvider.MESSAGE_ATTACHMENTS_COLUMN); 355 String accountUriString = cursor.getString(UIProvider.MESSAGE_ACCOUNT_URI_COLUMN); 356 accountUri = !TextUtils.isEmpty(accountUriString) ? Uri.parse(accountUriString) : null; 357 eventIntentUri = 358 Utils.getValidUri(cursor.getString(UIProvider.MESSAGE_EVENT_INTENT_COLUMN)); 359 spamWarningString = 360 cursor.getString(UIProvider.MESSAGE_SPAM_WARNING_STRING_ID_COLUMN); 361 spamWarningLevel = cursor.getInt(UIProvider.MESSAGE_SPAM_WARNING_LEVEL_COLUMN); 362 spamLinkType = cursor.getInt(UIProvider.MESSAGE_SPAM_WARNING_LINK_TYPE_COLUMN); 363 viaDomain = cursor.getString(UIProvider.MESSAGE_VIA_DOMAIN_COLUMN); 364 isSending = cursor.getInt(UIProvider.MESSAGE_IS_SENDING_COLUMN) != 0; 365 } 366 } 367 368 public Message(Context context, MimeMessage mimeMessage, Uri emlFileUri) 369 throws MessagingException { 370 // Set message header values. 371 setFrom(com.android.emailcommon.mail.Address.pack(mimeMessage.getFrom())); 372 setTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients( 373 com.android.emailcommon.mail.Message.RecipientType.TO))); 374 setCc(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients( 375 com.android.emailcommon.mail.Message.RecipientType.CC))); 376 setBcc(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients( 377 com.android.emailcommon.mail.Message.RecipientType.BCC))); 378 setReplyTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getReplyTo())); 379 subject = mimeMessage.getSubject(); 380 dateReceivedMs = mimeMessage.getSentDate().getTime(); 381 382 // for now, always set defaults 383 alwaysShowImages = false; 384 viaDomain = null; 385 draftType = UIProvider.DraftType.NOT_A_DRAFT; 386 isSending = false; 387 starred = false; 388 spamWarningString = null; 389 messageFlags = 0; 390 hasAttachments = false; 391 392 // body values (snippet/bodyText/bodyHtml) 393 // Now process body parts & attachments 394 ArrayList<Part> viewables = new ArrayList<Part>(); 395 ArrayList<Part> attachments = new ArrayList<Part>(); 396 MimeUtility.collectParts(mimeMessage, viewables, attachments); 397 398 ConversionUtilities.BodyFieldData data = 399 ConversionUtilities.parseBodyFields(viewables); 400 401 snippet = data.snippet; 402 bodyText = data.textContent; 403 bodyHtml = data.htmlContent; 404 405 // populate mAttachments 406 mAttachments = Lists.newArrayList(); 407 408 int partId = 0; 409 final String messageId = mimeMessage.getMessageId(); 410 for (final Part attachmentPart : attachments) { 411 mAttachments.add(new Attachment(context, attachmentPart, 412 emlFileUri, messageId, Integer.toString(partId++))); 413 } 414 415 hasAttachments = !mAttachments.isEmpty(); 416 417 attachmentListUri = hasAttachments ? 418 EmlAttachmentProvider.getAttachmentsListUri(emlFileUri, messageId) : null; 419 } 420 421 public boolean isFlaggedReplied() { 422 return (messageFlags & UIProvider.MessageFlags.REPLIED) == 423 UIProvider.MessageFlags.REPLIED; 424 } 425 426 public boolean isFlaggedForwarded() { 427 return (messageFlags & UIProvider.MessageFlags.FORWARDED) == 428 UIProvider.MessageFlags.FORWARDED; 429 } 430 431 public boolean isFlaggedCalendarInvite() { 432 return (messageFlags & UIProvider.MessageFlags.CALENDAR_INVITE) == 433 UIProvider.MessageFlags.CALENDAR_INVITE; 434 } 435 436 public String getFrom() { 437 return mFrom; 438 } 439 440 public synchronized void setFrom(final String from) { 441 mFrom = from; 442 mFromAddresses = null; 443 } 444 445 public String getTo() { 446 return mTo; 447 } 448 449 public synchronized void setTo(final String to) { 450 mTo = to; 451 mToAddresses = null; 452 } 453 454 public String getCc() { 455 return mCc; 456 } 457 458 public synchronized void setCc(final String cc) { 459 mCc = cc; 460 mCcAddresses = null; 461 } 462 463 public String getBcc() { 464 return mBcc; 465 } 466 467 public synchronized void setBcc(final String bcc) { 468 mBcc = bcc; 469 mBccAddresses = null; 470 } 471 472 @VisibleForTesting 473 public String getReplyTo() { 474 return mReplyTo; 475 } 476 477 public synchronized void setReplyTo(final String replyTo) { 478 mReplyTo = replyTo; 479 mReplyToAddresses = null; 480 } 481 482 public synchronized String[] getFromAddresses() { 483 if (mFromAddresses == null) { 484 mFromAddresses = tokenizeAddresses(mFrom); 485 } 486 return mFromAddresses; 487 } 488 489 public String[] getFromAddressesUnescaped() { 490 return unescapeAddresses(getFromAddresses()); 491 } 492 493 public synchronized String[] getToAddresses() { 494 if (mToAddresses == null) { 495 mToAddresses = tokenizeAddresses(mTo); 496 } 497 return mToAddresses; 498 } 499 500 public String[] getToAddressesUnescaped() { 501 return unescapeAddresses(getToAddresses()); 502 } 503 504 public synchronized String[] getCcAddresses() { 505 if (mCcAddresses == null) { 506 mCcAddresses = tokenizeAddresses(mCc); 507 } 508 return mCcAddresses; 509 } 510 511 public String[] getCcAddressesUnescaped() { 512 return unescapeAddresses(getCcAddresses()); 513 } 514 515 public synchronized String[] getBccAddresses() { 516 if (mBccAddresses == null) { 517 mBccAddresses = tokenizeAddresses(mBcc); 518 } 519 return mBccAddresses; 520 } 521 522 public String[] getBccAddressesUnescaped() { 523 return unescapeAddresses(getBccAddresses()); 524 } 525 526 public synchronized String[] getReplyToAddresses() { 527 if (mReplyToAddresses == null) { 528 mReplyToAddresses = tokenizeAddresses(mReplyTo); 529 } 530 return mReplyToAddresses; 531 } 532 533 public String[] getReplyToAddressesUnescaped() { 534 return unescapeAddresses(getReplyToAddresses()); 535 } 536 537 private static String[] unescapeAddresses(String[] escaped) { 538 final String[] unescaped = new String[escaped.length]; 539 for (int i = 0; i < escaped.length; i++) { 540 final String escapeMore = escaped[i].replace("<", "<").replace(">", ">"); 541 unescaped[i] = Html.fromHtml(escapeMore).toString(); 542 } 543 return unescaped; 544 } 545 546 public static String[] tokenizeAddresses(String addresses) { 547 if (TextUtils.isEmpty(addresses)) { 548 return new String[0]; 549 } 550 551 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addresses); 552 String[] strings = new String[tokens.length]; 553 for (int i = 0; i < tokens.length;i++) { 554 strings[i] = tokens[i].toString(); 555 } 556 return strings; 557 } 558 559 public List<Attachment> getAttachments() { 560 if (mAttachments == null) { 561 if (attachmentsJson != null) { 562 mAttachments = Attachment.fromJSONArray(attachmentsJson); 563 } else { 564 mAttachments = Collections.emptyList(); 565 } 566 } 567 return mAttachments; 568 } 569 570 /** 571 * Returns whether a "Show Pictures" button should initially appear for this message. If the 572 * button is shown, the message must also block all non-local images in the body. Inversely, if 573 * the button is not shown, the message must show all images within (or else the user would be 574 * stuck with no images and no way to reveal them). 575 * 576 * @return true if a "Show Pictures" button should appear. 577 */ 578 public boolean shouldShowImagePrompt() { 579 return !alwaysShowImages && (embedsExternalResources || 580 (!TextUtils.isEmpty(bodyHtml) && INLINE_IMAGE_PATTERN.matcher(bodyHtml).find())); 581 } 582 583 @Override 584 public boolean embedsExternalResources() { 585 return embedsExternalResources; 586 } 587 588 /** 589 * Helper method to command a provider to mark all messages from this sender with the 590 * {@link MessageColumns#ALWAYS_SHOW_IMAGES} flag set. 591 * 592 * @param handler a caller-provided handler to run the query on 593 * @param token (optional) token to identify the command to the handler 594 * @param cookie (optional) cookie to pass to the handler 595 */ 596 public void markAlwaysShowImages(AsyncQueryHandler handler, int token, Object cookie) { 597 alwaysShowImages = true; 598 599 final ContentValues values = new ContentValues(1); 600 values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, 1); 601 602 handler.startUpdate(token, cookie, uri, values, null, null); 603 } 604 605 @Override 606 public String getBodyAsHtml() { 607 String body = ""; 608 if (!TextUtils.isEmpty(bodyHtml)) { 609 body = bodyHtml; 610 } else if (!TextUtils.isEmpty(bodyText)) { 611 final SpannableString spannable = new SpannableString(bodyText); 612 Linkify.addLinks(spannable, Linkify.EMAIL_ADDRESSES); 613 body = Html.toHtml(spannable); 614 } 615 return body; 616 } 617 618 @Override 619 public long getId() { 620 return id; 621 } 622 } 623