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