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.SpannableString;
29import android.text.TextUtils;
30import android.text.util.Linkify;
31import android.text.util.Rfc822Token;
32import android.text.util.Rfc822Tokenizer;
33
34import com.android.emailcommon.internet.MimeHeader;
35import com.android.emailcommon.internet.MimeMessage;
36import com.android.emailcommon.internet.MimeUtility;
37import com.android.emailcommon.mail.Address;
38import com.android.emailcommon.mail.MessagingException;
39import com.android.emailcommon.mail.Part;
40import com.android.emailcommon.utility.ConversionUtilities;
41import com.android.mail.providers.UIProvider.MessageColumns;
42import com.android.mail.ui.HtmlMessage;
43import com.android.mail.utils.HtmlSanitizer;
44import com.android.mail.utils.Utils;
45import com.google.common.annotations.VisibleForTesting;
46import com.google.common.base.Objects;
47import com.google.common.collect.Lists;
48
49import java.util.ArrayList;
50import java.util.Collections;
51import java.util.Date;
52import java.util.List;
53import java.util.regex.Pattern;
54
55
56public 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