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