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