• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
describeContents()229     public int describeContents() {
230         return 0;
231     }
232 
233     @Override
equals(Object o)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
hashCode()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      */
isEqual(Message o)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
writeToParcel(Parcel dest, int flags)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 
Message(Parcel in)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 
Message()337     public Message() {
338 
339     }
340 
341     @Override
toString()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 
Message(Cursor cursor)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      */
Message(Context context, MimeMessage mimeMessage, Uri emlFileUri)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 
isFlaggedReplied()500     public boolean isFlaggedReplied() {
501         return (messageFlags & UIProvider.MessageFlags.REPLIED) ==
502                 UIProvider.MessageFlags.REPLIED;
503     }
504 
isFlaggedForwarded()505     public boolean isFlaggedForwarded() {
506         return (messageFlags & UIProvider.MessageFlags.FORWARDED) ==
507                 UIProvider.MessageFlags.FORWARDED;
508     }
509 
isFlaggedCalendarInvite()510     public boolean isFlaggedCalendarInvite() {
511         return (messageFlags & UIProvider.MessageFlags.CALENDAR_INVITE) ==
512                 UIProvider.MessageFlags.CALENDAR_INVITE;
513     }
514 
getFrom()515     public String getFrom() {
516         return mFrom;
517     }
518 
setFrom(final String from)519     public synchronized void setFrom(final String from) {
520         mFrom = from;
521         mFromAddresses = null;
522     }
523 
getTo()524     public String getTo() {
525         return mTo;
526     }
527 
setTo(final String to)528     public synchronized void setTo(final String to) {
529         mTo = to;
530         mToAddresses = null;
531     }
532 
getCc()533     public String getCc() {
534         return mCc;
535     }
536 
setCc(final String cc)537     public synchronized void setCc(final String cc) {
538         mCc = cc;
539         mCcAddresses = null;
540     }
541 
getBcc()542     public String getBcc() {
543         return mBcc;
544     }
545 
setBcc(final String bcc)546     public synchronized void setBcc(final String bcc) {
547         mBcc = bcc;
548         mBccAddresses = null;
549     }
550 
551     @VisibleForTesting
getReplyTo()552     public String getReplyTo() {
553         return mReplyTo;
554     }
555 
setReplyTo(final String replyTo)556     public synchronized void setReplyTo(final String replyTo) {
557         mReplyTo = replyTo;
558         mReplyToAddresses = null;
559     }
560 
getFromAddresses()561     public synchronized String[] getFromAddresses() {
562         if (mFromAddresses == null) {
563             mFromAddresses = tokenizeAddresses(mFrom);
564         }
565         return mFromAddresses;
566     }
567 
getFromAddressesUnescaped()568     public String[] getFromAddressesUnescaped() {
569         return unescapeAddresses(getFromAddresses());
570     }
571 
getToAddresses()572     public synchronized String[] getToAddresses() {
573         if (mToAddresses == null) {
574             mToAddresses = tokenizeAddresses(mTo);
575         }
576         return mToAddresses;
577     }
578 
getToAddressesUnescaped()579     public String[] getToAddressesUnescaped() {
580         return unescapeAddresses(getToAddresses());
581     }
582 
getCcAddresses()583     public synchronized String[] getCcAddresses() {
584         if (mCcAddresses == null) {
585             mCcAddresses = tokenizeAddresses(mCc);
586         }
587         return mCcAddresses;
588     }
589 
getCcAddressesUnescaped()590     public String[] getCcAddressesUnescaped() {
591         return unescapeAddresses(getCcAddresses());
592     }
593 
getBccAddresses()594     public synchronized String[] getBccAddresses() {
595         if (mBccAddresses == null) {
596             mBccAddresses = tokenizeAddresses(mBcc);
597         }
598         return mBccAddresses;
599     }
600 
getBccAddressesUnescaped()601     public String[] getBccAddressesUnescaped() {
602         return unescapeAddresses(getBccAddresses());
603     }
604 
getReplyToAddresses()605     public synchronized String[] getReplyToAddresses() {
606         if (mReplyToAddresses == null) {
607             mReplyToAddresses = tokenizeAddresses(mReplyTo);
608         }
609         return mReplyToAddresses;
610     }
611 
getReplyToAddressesUnescaped()612     public String[] getReplyToAddressesUnescaped() {
613         return unescapeAddresses(getReplyToAddresses());
614     }
615 
unescapeAddresses(String[] escaped)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 
tokenizeAddresses(String addresses)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 
getAttachments()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      */
getAttachmentCount(boolean includeInline)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      */
shouldShowImagePrompt()683     public boolean shouldShowImagePrompt() {
684         return !alwaysShowImages && (embedsExternalResources ||
685                 (!TextUtils.isEmpty(bodyHtml) && INLINE_IMAGE_PATTERN.matcher(bodyHtml).find()));
686     }
687 
688     @Override
embedsExternalResources()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      */
markAlwaysShowImages(AsyncQueryHandler handler, int token, Object cookie)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
getBodyAsHtml()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
getId()724     public long getId() {
725         return id;
726     }
727 
isDraft()728     public boolean isDraft() {
729         return draftType != UIProvider.DraftType.NOT_A_DRAFT;
730     }
731 }
732