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