Home | History | Annotate | Download | only in providers
      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
    229     public int describeContents() {
    230         return 0;
    231     }
    232 
    233     @Override
    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
    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 the user can input from ComposeActivity. This is primarily used to
    247      * ensure draft preview/composition are synced.
    248      * @param o the Message being compared to
    249      * @return True if they are equal in fields, false otherwise
    250      */
    251     public boolean isEqual(Message o) {
    252         return TextUtils.equals(this.getFrom(), o.getFrom()) &&
    253                 this.sendingState == o.sendingState &&
    254                 TextUtils.equals(this.getTo(), o.getTo()) &&
    255                 TextUtils.equals(this.getCc(), o.getCc()) &&
    256                 TextUtils.equals(this.getBcc(), o.getBcc()) &&
    257                 TextUtils.equals(this.subject, o.subject) &&
    258                 TextUtils.equals(this.bodyHtml, o.bodyHtml) &&
    259                 TextUtils.equals(this.bodyText, o.bodyText) &&
    260                 Objects.equal(this.attachmentListUri, o.attachmentListUri) &&
    261                 Objects.equal(getAttachments(), o.getAttachments());
    262     }
    263 
    264     @Override
    265     public void writeToParcel(Parcel dest, int flags) {
    266         dest.writeLong(id);
    267         dest.writeString(serverId);
    268         dest.writeParcelable(uri, 0);
    269         dest.writeParcelable(conversationUri, 0);
    270         dest.writeString(subject);
    271         dest.writeString(snippet);
    272         dest.writeString(mFrom);
    273         dest.writeString(mTo);
    274         dest.writeString(mCc);
    275         dest.writeString(mBcc);
    276         dest.writeString(mReplyTo);
    277         dest.writeLong(dateReceivedMs);
    278         dest.writeString(bodyHtml);
    279         dest.writeString(bodyText);
    280         dest.writeInt(embedsExternalResources ? 1 : 0);
    281         dest.writeParcelable(refMessageUri, 0);
    282         dest.writeInt(draftType);
    283         dest.writeInt(appendRefMessageContent ? 1 : 0);
    284         dest.writeInt(hasAttachments ? 1 : 0);
    285         dest.writeParcelable(attachmentListUri, 0);
    286         dest.writeLong(messageFlags);
    287         dest.writeInt(alwaysShowImages ? 1 : 0);
    288         dest.writeInt(quotedTextOffset);
    289         dest.writeString(attachmentsJson);
    290         dest.writeParcelable(accountUri, 0);
    291         dest.writeParcelable(eventIntentUri, 0);
    292         dest.writeString(spamWarningString);
    293         dest.writeInt(spamWarningLevel);
    294         dest.writeInt(spamLinkType);
    295         dest.writeString(viaDomain);
    296         dest.writeInt(sendingState);
    297         dest.writeInt(clipped ? 1 : 0);
    298         dest.writeString(permalink);
    299     }
    300 
    301     private Message(Parcel in) {
    302         id = in.readLong();
    303         serverId = in.readString();
    304         uri = in.readParcelable(null);
    305         conversationUri = in.readParcelable(null);
    306         subject = in.readString();
    307         snippet = in.readString();
    308         mFrom = in.readString();
    309         mTo = in.readString();
    310         mCc = in.readString();
    311         mBcc = in.readString();
    312         mReplyTo = in.readString();
    313         dateReceivedMs = in.readLong();
    314         bodyHtml = in.readString();
    315         bodyText = in.readString();
    316         embedsExternalResources = in.readInt() != 0;
    317         refMessageUri = in.readParcelable(null);
    318         draftType = in.readInt();
    319         appendRefMessageContent = in.readInt() != 0;
    320         hasAttachments = in.readInt() != 0;
    321         attachmentListUri = in.readParcelable(null);
    322         messageFlags = in.readLong();
    323         alwaysShowImages = in.readInt() != 0;
    324         quotedTextOffset = in.readInt();
    325         attachmentsJson = in.readString();
    326         accountUri = in.readParcelable(null);
    327         eventIntentUri = in.readParcelable(null);
    328         spamWarningString = in.readString();
    329         spamWarningLevel = in.readInt();
    330         spamLinkType = in.readInt();
    331         viaDomain = in.readString();
    332         sendingState = in.readInt();
    333         clipped = in.readInt() != 0;
    334         permalink = in.readString();
    335     }
    336 
    337     public Message() {
    338 
    339     }
    340 
    341     @Override
    342     public String toString() {
    343         return "[message id=" + id + "]";
    344     }
    345 
    346     public static final Creator<Message> CREATOR = new Creator<Message>() {
    347 
    348         @Override
    349         public Message createFromParcel(Parcel source) {
    350             return new Message(source);
    351         }
    352 
    353         @Override
    354         public Message[] newArray(int size) {
    355             return new Message[size];
    356         }
    357 
    358     };
    359 
    360     public Message(Cursor cursor) {
    361         if (cursor != null) {
    362             id = cursor.getLong(UIProvider.MESSAGE_ID_COLUMN);
    363             serverId = cursor.getString(UIProvider.MESSAGE_SERVER_ID_COLUMN);
    364             final String messageUriStr = cursor.getString(UIProvider.MESSAGE_URI_COLUMN);
    365             uri = !TextUtils.isEmpty(messageUriStr) ? Uri.parse(messageUriStr) : null;
    366             final String convUriStr = cursor.getString(UIProvider.MESSAGE_CONVERSATION_URI_COLUMN);
    367             conversationUri = !TextUtils.isEmpty(convUriStr) ? Uri.parse(convUriStr) : null;
    368             subject = cursor.getString(UIProvider.MESSAGE_SUBJECT_COLUMN);
    369             snippet = cursor.getString(UIProvider.MESSAGE_SNIPPET_COLUMN);
    370             mFrom = cursor.getString(UIProvider.MESSAGE_FROM_COLUMN);
    371             mTo = cursor.getString(UIProvider.MESSAGE_TO_COLUMN);
    372             mCc = cursor.getString(UIProvider.MESSAGE_CC_COLUMN);
    373             mBcc = cursor.getString(UIProvider.MESSAGE_BCC_COLUMN);
    374             mReplyTo = cursor.getString(UIProvider.MESSAGE_REPLY_TO_COLUMN);
    375             dateReceivedMs = cursor.getLong(UIProvider.MESSAGE_DATE_RECEIVED_MS_COLUMN);
    376             bodyHtml = cursor.getString(UIProvider.MESSAGE_BODY_HTML_COLUMN);
    377             bodyText = cursor.getString(UIProvider.MESSAGE_BODY_TEXT_COLUMN);
    378             embedsExternalResources = cursor
    379                     .getInt(UIProvider.MESSAGE_EMBEDS_EXTERNAL_RESOURCES_COLUMN) != 0;
    380             final String refMessageUriStr =
    381                     cursor.getString(UIProvider.MESSAGE_REF_MESSAGE_URI_COLUMN);
    382             refMessageUri = !TextUtils.isEmpty(refMessageUriStr) ?
    383                     Uri.parse(refMessageUriStr) : null;
    384             draftType = cursor.getInt(UIProvider.MESSAGE_DRAFT_TYPE_COLUMN);
    385             appendRefMessageContent = cursor
    386                     .getInt(UIProvider.MESSAGE_APPEND_REF_MESSAGE_CONTENT_COLUMN) != 0;
    387             hasAttachments = cursor.getInt(UIProvider.MESSAGE_HAS_ATTACHMENTS_COLUMN) != 0;
    388             final String attachmentsUri = cursor
    389                     .getString(UIProvider.MESSAGE_ATTACHMENT_LIST_URI_COLUMN);
    390             attachmentListUri = hasAttachments && !TextUtils.isEmpty(attachmentsUri) ? Uri
    391                     .parse(attachmentsUri) : null;
    392             final String attachmentsByCidUri = cursor
    393                     .getString(UIProvider.MESSAGE_ATTACHMENT_BY_CID_URI_COLUMN);
    394             attachmentByCidUri = hasAttachments && !TextUtils.isEmpty(attachmentsByCidUri) ?
    395                     Uri.parse(attachmentsByCidUri) : null;
    396             messageFlags = cursor.getLong(UIProvider.MESSAGE_FLAGS_COLUMN);
    397             alwaysShowImages = cursor.getInt(UIProvider.MESSAGE_ALWAYS_SHOW_IMAGES_COLUMN) != 0;
    398             read = cursor.getInt(UIProvider.MESSAGE_READ_COLUMN) != 0;
    399             seen = cursor.getInt(UIProvider.MESSAGE_SEEN_COLUMN) != 0;
    400             starred = cursor.getInt(UIProvider.MESSAGE_STARRED_COLUMN) != 0;
    401             quotedTextOffset = cursor.getInt(UIProvider.QUOTED_TEXT_OFFSET_COLUMN);
    402             attachmentsJson = cursor.getString(UIProvider.MESSAGE_ATTACHMENTS_COLUMN);
    403             String accountUriString = cursor.getString(UIProvider.MESSAGE_ACCOUNT_URI_COLUMN);
    404             accountUri = !TextUtils.isEmpty(accountUriString) ? Uri.parse(accountUriString) : null;
    405             eventIntentUri =
    406                     Utils.getValidUri(cursor.getString(UIProvider.MESSAGE_EVENT_INTENT_COLUMN));
    407             spamWarningString =
    408                     cursor.getString(UIProvider.MESSAGE_SPAM_WARNING_STRING_ID_COLUMN);
    409             spamWarningLevel = cursor.getInt(UIProvider.MESSAGE_SPAM_WARNING_LEVEL_COLUMN);
    410             spamLinkType = cursor.getInt(UIProvider.MESSAGE_SPAM_WARNING_LINK_TYPE_COLUMN);
    411             viaDomain = cursor.getString(UIProvider.MESSAGE_VIA_DOMAIN_COLUMN);
    412             sendingState = cursor.getInt(UIProvider.MESSAGE_SENDING_STATE_COLUMN);
    413             clipped = cursor.getInt(UIProvider.MESSAGE_CLIPPED_COLUMN) != 0;
    414             permalink = cursor.getString(UIProvider.MESSAGE_PERMALINK_COLUMN);
    415         }
    416     }
    417 
    418     /**
    419      * This constructor exists solely to generate Message objects from .eml attachments.
    420      */
    421     public Message(Context context, MimeMessage mimeMessage, Uri emlFileUri)
    422             throws MessagingException {
    423         // Set message header values.
    424         setFrom(Address.toHeader(mimeMessage.getFrom()));
    425         setTo(Address.toHeader(mimeMessage.getRecipients(
    426                 com.android.emailcommon.mail.Message.RecipientType.TO)));
    427         setCc(Address.toHeader(mimeMessage.getRecipients(
    428                 com.android.emailcommon.mail.Message.RecipientType.CC)));
    429         setBcc(Address.toHeader(mimeMessage.getRecipients(
    430                 com.android.emailcommon.mail.Message.RecipientType.BCC)));
    431         setReplyTo(Address.toHeader(mimeMessage.getReplyTo()));
    432         subject = mimeMessage.getSubject();
    433 
    434         final Date sentDate = mimeMessage.getSentDate();
    435         final Date internalDate = mimeMessage.getInternalDate();
    436         if (sentDate != null) {
    437             dateReceivedMs = sentDate.getTime();
    438         } else if (internalDate != null) {
    439             dateReceivedMs = internalDate.getTime();
    440         } else {
    441             dateReceivedMs = System.currentTimeMillis();
    442         }
    443 
    444         // for now, always set defaults
    445         alwaysShowImages = false;
    446         viaDomain = null;
    447         draftType = UIProvider.DraftType.NOT_A_DRAFT;
    448         sendingState = UIProvider.ConversationSendingState.OTHER;
    449         starred = false;
    450         spamWarningString = null;
    451         messageFlags = 0;
    452         clipped = false;
    453         permalink = null;
    454         hasAttachments = false;
    455 
    456         // body values (snippet/bodyText/bodyHtml)
    457         // Now process body parts & attachments
    458         ArrayList<Part> viewables = new ArrayList<Part>();
    459         ArrayList<Part> attachments = new ArrayList<Part>();
    460         MimeUtility.collectParts(mimeMessage, viewables, attachments);
    461 
    462         ConversionUtilities.BodyFieldData data = ConversionUtilities.parseBodyFields(viewables);
    463 
    464         snippet = data.snippet;
    465         bodyText = data.textContent;
    466 
    467         // sanitize the HTML found within the .eml file before consuming it
    468         bodyHtml = HtmlSanitizer.sanitizeHtml(data.htmlContent);
    469 
    470         // populate mAttachments
    471         mAttachments = Lists.newArrayList();
    472 
    473         final String messageId = mimeMessage.getMessageId();
    474 
    475         int partId = 0;
    476         for (final Part attachmentPart : attachments) {
    477             mAttachments.add(new Attachment(context, attachmentPart,
    478                     emlFileUri, messageId, Integer.toString(partId++), false /* inline */));
    479         }
    480 
    481         // instantiating an Attachment for each viewable will cause it to be registered within the
    482         // EmlAttachmentProvider for later access when displaying inline attachments
    483         for (final Part viewablePart : viewables) {
    484             final String[] cids = viewablePart.getHeader(MimeHeader.HEADER_CONTENT_ID);
    485             if (cids != null && cids.length == 1) {
    486                 final String cid = REMOVE_OPTIONAL_BRACKETS.matcher(cids[0]).replaceAll("$1");
    487                 mAttachments.add(new Attachment(context, viewablePart, emlFileUri, messageId, cid,
    488                         true /* inline */));
    489             }
    490         }
    491 
    492         hasAttachments = !mAttachments.isEmpty();
    493 
    494         attachmentListUri = hasAttachments ?
    495                 EmlAttachmentProvider.getAttachmentsListUri(emlFileUri, messageId) : null;
    496 
    497         attachmentByCidUri = EmlAttachmentProvider.getAttachmentByCidUri(emlFileUri, messageId);
    498     }
    499 
    500     public boolean isFlaggedReplied() {
    501         return (messageFlags & UIProvider.MessageFlags.REPLIED) ==
    502                 UIProvider.MessageFlags.REPLIED;
    503     }
    504 
    505     public boolean isFlaggedForwarded() {
    506         return (messageFlags & UIProvider.MessageFlags.FORWARDED) ==
    507                 UIProvider.MessageFlags.FORWARDED;
    508     }
    509 
    510     public boolean isFlaggedCalendarInvite() {
    511         return (messageFlags & UIProvider.MessageFlags.CALENDAR_INVITE) ==
    512                 UIProvider.MessageFlags.CALENDAR_INVITE;
    513     }
    514 
    515     public String getFrom() {
    516         return mFrom;
    517     }
    518 
    519     public synchronized void setFrom(final String from) {
    520         mFrom = from;
    521         mFromAddresses = null;
    522     }
    523 
    524     public String getTo() {
    525         return mTo;
    526     }
    527 
    528     public synchronized void setTo(final String to) {
    529         mTo = to;
    530         mToAddresses = null;
    531     }
    532 
    533     public String getCc() {
    534         return mCc;
    535     }
    536 
    537     public synchronized void setCc(final String cc) {
    538         mCc = cc;
    539         mCcAddresses = null;
    540     }
    541 
    542     public String getBcc() {
    543         return mBcc;
    544     }
    545 
    546     public synchronized void setBcc(final String bcc) {
    547         mBcc = bcc;
    548         mBccAddresses = null;
    549     }
    550 
    551     @VisibleForTesting
    552     public String getReplyTo() {
    553         return mReplyTo;
    554     }
    555 
    556     public synchronized void setReplyTo(final String replyTo) {
    557         mReplyTo = replyTo;
    558         mReplyToAddresses = null;
    559     }
    560 
    561     public synchronized String[] getFromAddresses() {
    562         if (mFromAddresses == null) {
    563             mFromAddresses = tokenizeAddresses(mFrom);
    564         }
    565         return mFromAddresses;
    566     }
    567 
    568     public String[] getFromAddressesUnescaped() {
    569         return unescapeAddresses(getFromAddresses());
    570     }
    571 
    572     public synchronized String[] getToAddresses() {
    573         if (mToAddresses == null) {
    574             mToAddresses = tokenizeAddresses(mTo);
    575         }
    576         return mToAddresses;
    577     }
    578 
    579     public String[] getToAddressesUnescaped() {
    580         return unescapeAddresses(getToAddresses());
    581     }
    582 
    583     public synchronized String[] getCcAddresses() {
    584         if (mCcAddresses == null) {
    585             mCcAddresses = tokenizeAddresses(mCc);
    586         }
    587         return mCcAddresses;
    588     }
    589 
    590     public String[] getCcAddressesUnescaped() {
    591         return unescapeAddresses(getCcAddresses());
    592     }
    593 
    594     public synchronized String[] getBccAddresses() {
    595         if (mBccAddresses == null) {
    596             mBccAddresses = tokenizeAddresses(mBcc);
    597         }
    598         return mBccAddresses;
    599     }
    600 
    601     public String[] getBccAddressesUnescaped() {
    602         return unescapeAddresses(getBccAddresses());
    603     }
    604 
    605     public synchronized String[] getReplyToAddresses() {
    606         if (mReplyToAddresses == null) {
    607             mReplyToAddresses = tokenizeAddresses(mReplyTo);
    608         }
    609         return mReplyToAddresses;
    610     }
    611 
    612     public String[] getReplyToAddressesUnescaped() {
    613         return unescapeAddresses(getReplyToAddresses());
    614     }
    615 
    616     private static String[] unescapeAddresses(String[] escaped) {
    617         final String[] unescaped = new String[escaped.length];
    618         for (int i = 0; i < escaped.length; i++) {
    619             final String escapeMore = escaped[i].replace("<", "&lt;").replace(">", "&gt;");
    620             unescaped[i] = Html.fromHtml(escapeMore).toString();
    621         }
    622         return unescaped;
    623     }
    624 
    625     public static String[] tokenizeAddresses(String addresses) {
    626         if (TextUtils.isEmpty(addresses)) {
    627             return new String[0];
    628         }
    629 
    630         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addresses);
    631         String[] strings = new String[tokens.length];
    632         for (int i = 0; i < tokens.length;i++) {
    633             strings[i] = tokens[i].toString();
    634         }
    635         return strings;
    636     }
    637 
    638     public List<Attachment> getAttachments() {
    639         if (mAttachments == null) {
    640             if (attachmentsJson != null) {
    641                 mAttachments = Attachment.fromJSONArray(attachmentsJson);
    642             } else {
    643                 mAttachments = Collections.emptyList();
    644             }
    645         }
    646         return mAttachments;
    647     }
    648 
    649     /**
    650      * Returns the number of attachments in the message.
    651      * @param includeInline If {@code true}, includes inline attachments in the count.
    652      *                      {@code false}, otherwise.
    653      * @return the number of attachments in the message.
    654      */
    655     public int getAttachmentCount(boolean includeInline) {
    656         // If include inline, just return the full list count.
    657         if (includeInline) {
    658             return mAttachments.size();
    659         }
    660 
    661         // Otherwise, iterate through the attachment list,
    662         // skipping inline attachments.
    663         int numAttachments = 0;
    664         final List<Attachment> attachments = getAttachments();
    665         for (int i = 0, size = attachments.size(); i < size; i++) {
    666             if (attachments.get(i).isInlineAttachment()) {
    667                 continue;
    668             }
    669             numAttachments++;
    670         }
    671 
    672         return numAttachments;
    673     }
    674 
    675     /**
    676      * Returns whether a "Show Pictures" button should initially appear for this message. If the
    677      * button is shown, the message must also block all non-local images in the body. Inversely, if
    678      * the button is not shown, the message must show all images within (or else the user would be
    679      * stuck with no images and no way to reveal them).
    680      *
    681      * @return true if a "Show Pictures" button should appear.
    682      */
    683     public boolean shouldShowImagePrompt() {
    684         return !alwaysShowImages && (embedsExternalResources ||
    685                 (!TextUtils.isEmpty(bodyHtml) && INLINE_IMAGE_PATTERN.matcher(bodyHtml).find()));
    686     }
    687 
    688     @Override
    689     public boolean embedsExternalResources() {
    690         return embedsExternalResources;
    691     }
    692 
    693     /**
    694      * Helper method to command a provider to mark all messages from this sender with the
    695      * {@link MessageColumns#ALWAYS_SHOW_IMAGES} flag set.
    696      *
    697      * @param handler a caller-provided handler to run the query on
    698      * @param token (optional) token to identify the command to the handler
    699      * @param cookie (optional) cookie to pass to the handler
    700      */
    701     public void markAlwaysShowImages(AsyncQueryHandler handler, int token, Object cookie) {
    702         alwaysShowImages = true;
    703 
    704         final ContentValues values = new ContentValues(1);
    705         values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, 1);
    706 
    707         handler.startUpdate(token, cookie, uri, values, null, null);
    708     }
    709 
    710     @Override
    711     public String getBodyAsHtml() {
    712         String body = "";
    713         if (!TextUtils.isEmpty(bodyHtml)) {
    714             body = bodyHtml;
    715         } else if (!TextUtils.isEmpty(bodyText)) {
    716             final SpannableString spannable = new SpannableString(bodyText);
    717             Linkify.addLinks(spannable, Linkify.EMAIL_ADDRESSES);
    718             body = Html.toHtml(spannable);
    719         }
    720         return body;
    721     }
    722 
    723     @Override
    724     public long getId() {
    725         return id;
    726     }
    727 
    728     public boolean isDraft() {
    729         return draftType != UIProvider.DraftType.NOT_A_DRAFT;
    730     }
    731 }
    732