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