• 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.MimeMessage;
35 import com.android.emailcommon.internet.MimeUtility;
36 import com.android.emailcommon.mail.MessagingException;
37 import com.android.emailcommon.mail.Part;
38 import com.android.emailcommon.utility.ConversionUtilities;
39 import com.android.mail.providers.UIProvider.MessageColumns;
40 import com.android.mail.ui.HtmlMessage;
41 import com.android.mail.utils.Utils;
42 import com.google.common.annotations.VisibleForTesting;
43 import com.google.common.base.Objects;
44 import com.google.common.collect.Lists;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.List;
49 import java.util.regex.Pattern;
50 
51 
52 public class Message implements Parcelable, HtmlMessage {
53     /**
54      * Regex pattern used to look for any inline images in message bodies, including Gmail-hosted
55      * relative-URL images, Gmail emoticons, and any external inline images (although we usually
56      * count on the server to detect external images).
57      */
58     private static Pattern INLINE_IMAGE_PATTERN = Pattern.compile("<img\\s+[^>]*src=",
59             Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
60 
61     /**
62      * @see BaseColumns#_ID
63      */
64     public long id;
65     /**
66      * @see UIProvider.MessageColumns#SERVER_ID
67      */
68     public String serverId;
69     /**
70      * @see UIProvider.MessageColumns#URI
71      */
72     public Uri uri;
73     /**
74      * @see UIProvider.MessageColumns#CONVERSATION_ID
75      */
76     public Uri conversationUri;
77     /**
78      * @see UIProvider.MessageColumns#SUBJECT
79      */
80     public String subject;
81     /**
82      * @see UIProvider.MessageColumns#SNIPPET
83      */
84     public String snippet;
85     /**
86      * @see UIProvider.MessageColumns#FROM
87      */
88     private String mFrom;
89     /**
90      * @see UIProvider.MessageColumns#TO
91      */
92     private String mTo;
93     /**
94      * @see UIProvider.MessageColumns#CC
95      */
96     private String mCc;
97     /**
98      * @see UIProvider.MessageColumns#BCC
99      */
100     private String mBcc;
101     /**
102      * @see UIProvider.MessageColumns#REPLY_TO
103      */
104     private String mReplyTo;
105     /**
106      * @see UIProvider.MessageColumns#DATE_RECEIVED_MS
107      */
108     public long dateReceivedMs;
109     /**
110      * @see UIProvider.MessageColumns#BODY_HTML
111      */
112     public String bodyHtml;
113     /**
114      * @see UIProvider.MessageColumns#BODY_TEXT
115      */
116     public String bodyText;
117     /**
118      * @see UIProvider.MessageColumns#EMBEDS_EXTERNAL_RESOURCES
119      */
120     public boolean embedsExternalResources;
121     /**
122      * @see UIProvider.MessageColumns#REF_MESSAGE_ID
123      */
124     public Uri refMessageUri;
125     /**
126      * @see UIProvider.MessageColumns#DRAFT_TYPE
127      */
128     public int draftType;
129     /**
130      * @see UIProvider.MessageColumns#APPEND_REF_MESSAGE_CONTENT
131      */
132     public boolean appendRefMessageContent;
133     /**
134      * @see UIProvider.MessageColumns#HAS_ATTACHMENTS
135      */
136     public boolean hasAttachments;
137     /**
138      * @see UIProvider.MessageColumns#ATTACHMENT_LIST_URI
139      */
140     public Uri attachmentListUri;
141     /**
142      * @see UIProvider.MessageColumns#MESSAGE_FLAGS
143      */
144     public long messageFlags;
145     /**
146      * @see UIProvider.MessageColumns#ALWAYS_SHOW_IMAGES
147      */
148     public boolean alwaysShowImages;
149     /**
150      * @see UIProvider.MessageColumns#READ
151      */
152     public boolean read;
153     /**
154      * @see UIProvider.MessageColumns#SEEN
155      */
156     public boolean seen;
157     /**
158      * @see UIProvider.MessageColumns#STARRED
159      */
160     public boolean starred;
161     /**
162      * @see UIProvider.MessageColumns#QUOTE_START_POS
163      */
164     public int quotedTextOffset;
165     /**
166      * @see UIProvider.MessageColumns#ATTACHMENTS
167      *<p>
168      * N.B. this value is NOT immutable and may change during conversation view render.
169      */
170     public String attachmentsJson;
171     /**
172      * @see UIProvider.MessageColumns#MESSAGE_ACCOUNT_URI
173      */
174     public Uri accountUri;
175     /**
176      * @see UIProvider.MessageColumns#EVENT_INTENT_URI
177      */
178     public Uri eventIntentUri;
179     /**
180      * @see UIProvider.MessageColumns#SPAM_WARNING_STRING
181      */
182     public String spamWarningString;
183     /**
184      * @see UIProvider.MessageColumns#SPAM_WARNING_LEVEL
185      */
186     public int spamWarningLevel;
187     /**
188      * @see UIProvider.MessageColumns#SPAM_WARNING_LINK_TYPE
189      */
190     public int spamLinkType;
191     /**
192      * @see UIProvider.MessageColumns#VIA_DOMAIN
193      */
194     public String viaDomain;
195     /**
196      * @see UIProvider.MessageColumns#IS_SENDING
197      */
198     public boolean isSending;
199 
200     private transient String[] mFromAddresses = null;
201     private transient String[] mToAddresses = null;
202     private transient String[] mCcAddresses = null;
203     private transient String[] mBccAddresses = null;
204     private transient String[] mReplyToAddresses = null;
205 
206     private transient List<Attachment> mAttachments = null;
207 
208     @Override
describeContents()209     public int describeContents() {
210         return 0;
211     }
212 
213     @Override
equals(Object o)214     public boolean equals(Object o) {
215         return this == o || (o != null && o instanceof Message
216                 && Objects.equal(uri, ((Message) o).uri));
217     }
218 
219     @Override
hashCode()220     public int hashCode() {
221         return uri == null ? 0 : uri.hashCode();
222     }
223 
224     @Override
writeToParcel(Parcel dest, int flags)225     public void writeToParcel(Parcel dest, int flags) {
226         dest.writeLong(id);
227         dest.writeString(serverId);
228         dest.writeParcelable(uri, 0);
229         dest.writeParcelable(conversationUri, 0);
230         dest.writeString(subject);
231         dest.writeString(snippet);
232         dest.writeString(mFrom);
233         dest.writeString(mTo);
234         dest.writeString(mCc);
235         dest.writeString(mBcc);
236         dest.writeString(mReplyTo);
237         dest.writeLong(dateReceivedMs);
238         dest.writeString(bodyHtml);
239         dest.writeString(bodyText);
240         dest.writeInt(embedsExternalResources ? 1 : 0);
241         dest.writeParcelable(refMessageUri, 0);
242         dest.writeInt(draftType);
243         dest.writeInt(appendRefMessageContent ? 1 : 0);
244         dest.writeInt(hasAttachments ? 1 : 0);
245         dest.writeParcelable(attachmentListUri, 0);
246         dest.writeLong(messageFlags);
247         dest.writeInt(alwaysShowImages ? 1 : 0);
248         dest.writeInt(quotedTextOffset);
249         dest.writeString(attachmentsJson);
250         dest.writeParcelable(accountUri, 0);
251         dest.writeParcelable(eventIntentUri, 0);
252         dest.writeString(spamWarningString);
253         dest.writeInt(spamWarningLevel);
254         dest.writeInt(spamLinkType);
255         dest.writeString(viaDomain);
256         dest.writeInt(isSending ? 1 : 0);
257     }
258 
Message(Parcel in)259     private Message(Parcel in) {
260         id = in.readLong();
261         serverId = in.readString();
262         uri = in.readParcelable(null);
263         conversationUri = in.readParcelable(null);
264         subject = in.readString();
265         snippet = in.readString();
266         mFrom = in.readString();
267         mTo = in.readString();
268         mCc = in.readString();
269         mBcc = in.readString();
270         mReplyTo = in.readString();
271         dateReceivedMs = in.readLong();
272         bodyHtml = in.readString();
273         bodyText = in.readString();
274         embedsExternalResources = in.readInt() != 0;
275         refMessageUri = in.readParcelable(null);
276         draftType = in.readInt();
277         appendRefMessageContent = in.readInt() != 0;
278         hasAttachments = in.readInt() != 0;
279         attachmentListUri = in.readParcelable(null);
280         messageFlags = in.readLong();
281         alwaysShowImages = in.readInt() != 0;
282         quotedTextOffset = in.readInt();
283         attachmentsJson = in.readString();
284         accountUri = in.readParcelable(null);
285         eventIntentUri = in.readParcelable(null);
286         spamWarningString = in.readString();
287         spamWarningLevel = in.readInt();
288         spamLinkType = in.readInt();
289         viaDomain = in.readString();
290         isSending = in.readInt() != 0;
291     }
292 
Message()293     public Message() {
294 
295     }
296 
297     @Override
toString()298     public String toString() {
299         return "[message id=" + id + "]";
300     }
301 
302     public static final Creator<Message> CREATOR = new Creator<Message>() {
303 
304         @Override
305         public Message createFromParcel(Parcel source) {
306             return new Message(source);
307         }
308 
309         @Override
310         public Message[] newArray(int size) {
311             return new Message[size];
312         }
313 
314     };
315 
Message(Cursor cursor)316     public Message(Cursor cursor) {
317         if (cursor != null) {
318             id = cursor.getLong(UIProvider.MESSAGE_ID_COLUMN);
319             serverId = cursor.getString(UIProvider.MESSAGE_SERVER_ID_COLUMN);
320             final String messageUriStr = cursor.getString(UIProvider.MESSAGE_URI_COLUMN);
321             uri = !TextUtils.isEmpty(messageUriStr) ? Uri.parse(messageUriStr) : null;
322             final String convUriStr = cursor.getString(UIProvider.MESSAGE_CONVERSATION_URI_COLUMN);
323             conversationUri = !TextUtils.isEmpty(convUriStr) ? Uri.parse(convUriStr) : null;
324             subject = cursor.getString(UIProvider.MESSAGE_SUBJECT_COLUMN);
325             snippet = cursor.getString(UIProvider.MESSAGE_SNIPPET_COLUMN);
326             mFrom = cursor.getString(UIProvider.MESSAGE_FROM_COLUMN);
327             mTo = cursor.getString(UIProvider.MESSAGE_TO_COLUMN);
328             mCc = cursor.getString(UIProvider.MESSAGE_CC_COLUMN);
329             mBcc = cursor.getString(UIProvider.MESSAGE_BCC_COLUMN);
330             mReplyTo = cursor.getString(UIProvider.MESSAGE_REPLY_TO_COLUMN);
331             dateReceivedMs = cursor.getLong(UIProvider.MESSAGE_DATE_RECEIVED_MS_COLUMN);
332             bodyHtml = cursor.getString(UIProvider.MESSAGE_BODY_HTML_COLUMN);
333             bodyText = cursor.getString(UIProvider.MESSAGE_BODY_TEXT_COLUMN);
334             embedsExternalResources = cursor
335                     .getInt(UIProvider.MESSAGE_EMBEDS_EXTERNAL_RESOURCES_COLUMN) != 0;
336             final String refMessageUriStr =
337                     cursor.getString(UIProvider.MESSAGE_REF_MESSAGE_URI_COLUMN);
338             refMessageUri = !TextUtils.isEmpty(refMessageUriStr) ?
339                     Uri.parse(refMessageUriStr) : null;
340             draftType = cursor.getInt(UIProvider.MESSAGE_DRAFT_TYPE_COLUMN);
341             appendRefMessageContent = cursor
342                     .getInt(UIProvider.MESSAGE_APPEND_REF_MESSAGE_CONTENT_COLUMN) != 0;
343             hasAttachments = cursor.getInt(UIProvider.MESSAGE_HAS_ATTACHMENTS_COLUMN) != 0;
344             final String attachmentsUri = cursor
345                     .getString(UIProvider.MESSAGE_ATTACHMENT_LIST_URI_COLUMN);
346             attachmentListUri = hasAttachments && !TextUtils.isEmpty(attachmentsUri) ? Uri
347                     .parse(attachmentsUri) : null;
348             messageFlags = cursor.getLong(UIProvider.MESSAGE_FLAGS_COLUMN);
349             alwaysShowImages = cursor.getInt(UIProvider.MESSAGE_ALWAYS_SHOW_IMAGES_COLUMN) != 0;
350             read = cursor.getInt(UIProvider.MESSAGE_READ_COLUMN) != 0;
351             seen = cursor.getInt(UIProvider.MESSAGE_SEEN_COLUMN) != 0;
352             starred = cursor.getInt(UIProvider.MESSAGE_STARRED_COLUMN) != 0;
353             quotedTextOffset = cursor.getInt(UIProvider.QUOTED_TEXT_OFFSET_COLUMN);
354             attachmentsJson = cursor.getString(UIProvider.MESSAGE_ATTACHMENTS_COLUMN);
355             String accountUriString = cursor.getString(UIProvider.MESSAGE_ACCOUNT_URI_COLUMN);
356             accountUri = !TextUtils.isEmpty(accountUriString) ? Uri.parse(accountUriString) : null;
357             eventIntentUri =
358                     Utils.getValidUri(cursor.getString(UIProvider.MESSAGE_EVENT_INTENT_COLUMN));
359             spamWarningString =
360                     cursor.getString(UIProvider.MESSAGE_SPAM_WARNING_STRING_ID_COLUMN);
361             spamWarningLevel = cursor.getInt(UIProvider.MESSAGE_SPAM_WARNING_LEVEL_COLUMN);
362             spamLinkType = cursor.getInt(UIProvider.MESSAGE_SPAM_WARNING_LINK_TYPE_COLUMN);
363             viaDomain = cursor.getString(UIProvider.MESSAGE_VIA_DOMAIN_COLUMN);
364             isSending = cursor.getInt(UIProvider.MESSAGE_IS_SENDING_COLUMN) != 0;
365         }
366     }
367 
Message(Context context, MimeMessage mimeMessage, Uri emlFileUri)368     public Message(Context context, MimeMessage mimeMessage, Uri emlFileUri)
369             throws MessagingException {
370         // Set message header values.
371         setFrom(com.android.emailcommon.mail.Address.pack(mimeMessage.getFrom()));
372         setTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
373                 com.android.emailcommon.mail.Message.RecipientType.TO)));
374         setCc(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
375                 com.android.emailcommon.mail.Message.RecipientType.CC)));
376         setBcc(com.android.emailcommon.mail.Address.pack(mimeMessage.getRecipients(
377                 com.android.emailcommon.mail.Message.RecipientType.BCC)));
378         setReplyTo(com.android.emailcommon.mail.Address.pack(mimeMessage.getReplyTo()));
379         subject = mimeMessage.getSubject();
380         dateReceivedMs = mimeMessage.getSentDate().getTime();
381 
382         // for now, always set defaults
383         alwaysShowImages = false;
384         viaDomain = null;
385         draftType = UIProvider.DraftType.NOT_A_DRAFT;
386         isSending = false;
387         starred = false;
388         spamWarningString = null;
389         messageFlags = 0;
390         hasAttachments = false;
391 
392         // body values (snippet/bodyText/bodyHtml)
393         // Now process body parts & attachments
394         ArrayList<Part> viewables = new ArrayList<Part>();
395         ArrayList<Part> attachments = new ArrayList<Part>();
396         MimeUtility.collectParts(mimeMessage, viewables, attachments);
397 
398         ConversionUtilities.BodyFieldData data =
399                 ConversionUtilities.parseBodyFields(viewables);
400 
401         snippet = data.snippet;
402         bodyText = data.textContent;
403         bodyHtml = data.htmlContent;
404 
405         // populate mAttachments
406         mAttachments = Lists.newArrayList();
407 
408         int partId = 0;
409         final String messageId = mimeMessage.getMessageId();
410         for (final Part attachmentPart : attachments) {
411             mAttachments.add(new Attachment(context, attachmentPart,
412                     emlFileUri, messageId, Integer.toString(partId++)));
413         }
414 
415         hasAttachments = !mAttachments.isEmpty();
416 
417         attachmentListUri =  hasAttachments ?
418                 EmlAttachmentProvider.getAttachmentsListUri(emlFileUri, messageId) : null;
419     }
420 
isFlaggedReplied()421     public boolean isFlaggedReplied() {
422         return (messageFlags & UIProvider.MessageFlags.REPLIED) ==
423                 UIProvider.MessageFlags.REPLIED;
424     }
425 
isFlaggedForwarded()426     public boolean isFlaggedForwarded() {
427         return (messageFlags & UIProvider.MessageFlags.FORWARDED) ==
428                 UIProvider.MessageFlags.FORWARDED;
429     }
430 
isFlaggedCalendarInvite()431     public boolean isFlaggedCalendarInvite() {
432         return (messageFlags & UIProvider.MessageFlags.CALENDAR_INVITE) ==
433                 UIProvider.MessageFlags.CALENDAR_INVITE;
434     }
435 
getFrom()436     public String getFrom() {
437         return mFrom;
438     }
439 
setFrom(final String from)440     public synchronized void setFrom(final String from) {
441         mFrom = from;
442         mFromAddresses = null;
443     }
444 
getTo()445     public String getTo() {
446         return mTo;
447     }
448 
setTo(final String to)449     public synchronized void setTo(final String to) {
450         mTo = to;
451         mToAddresses = null;
452     }
453 
getCc()454     public String getCc() {
455         return mCc;
456     }
457 
setCc(final String cc)458     public synchronized void setCc(final String cc) {
459         mCc = cc;
460         mCcAddresses = null;
461     }
462 
getBcc()463     public String getBcc() {
464         return mBcc;
465     }
466 
setBcc(final String bcc)467     public synchronized void setBcc(final String bcc) {
468         mBcc = bcc;
469         mBccAddresses = null;
470     }
471 
472     @VisibleForTesting
getReplyTo()473     public String getReplyTo() {
474         return mReplyTo;
475     }
476 
setReplyTo(final String replyTo)477     public synchronized void setReplyTo(final String replyTo) {
478         mReplyTo = replyTo;
479         mReplyToAddresses = null;
480     }
481 
getFromAddresses()482     public synchronized String[] getFromAddresses() {
483         if (mFromAddresses == null) {
484             mFromAddresses = tokenizeAddresses(mFrom);
485         }
486         return mFromAddresses;
487     }
488 
getFromAddressesUnescaped()489     public String[] getFromAddressesUnescaped() {
490         return unescapeAddresses(getFromAddresses());
491     }
492 
getToAddresses()493     public synchronized String[] getToAddresses() {
494         if (mToAddresses == null) {
495             mToAddresses = tokenizeAddresses(mTo);
496         }
497         return mToAddresses;
498     }
499 
getToAddressesUnescaped()500     public String[] getToAddressesUnescaped() {
501         return unescapeAddresses(getToAddresses());
502     }
503 
getCcAddresses()504     public synchronized String[] getCcAddresses() {
505         if (mCcAddresses == null) {
506             mCcAddresses = tokenizeAddresses(mCc);
507         }
508         return mCcAddresses;
509     }
510 
getCcAddressesUnescaped()511     public String[] getCcAddressesUnescaped() {
512         return unescapeAddresses(getCcAddresses());
513     }
514 
getBccAddresses()515     public synchronized String[] getBccAddresses() {
516         if (mBccAddresses == null) {
517             mBccAddresses = tokenizeAddresses(mBcc);
518         }
519         return mBccAddresses;
520     }
521 
getBccAddressesUnescaped()522     public String[] getBccAddressesUnescaped() {
523         return unescapeAddresses(getBccAddresses());
524     }
525 
getReplyToAddresses()526     public synchronized String[] getReplyToAddresses() {
527         if (mReplyToAddresses == null) {
528             mReplyToAddresses = tokenizeAddresses(mReplyTo);
529         }
530         return mReplyToAddresses;
531     }
532 
getReplyToAddressesUnescaped()533     public String[] getReplyToAddressesUnescaped() {
534         return unescapeAddresses(getReplyToAddresses());
535     }
536 
unescapeAddresses(String[] escaped)537     private static String[] unescapeAddresses(String[] escaped) {
538         final String[] unescaped = new String[escaped.length];
539         for (int i = 0; i < escaped.length; i++) {
540             final String escapeMore = escaped[i].replace("<", "&lt;").replace(">", "&gt;");
541             unescaped[i] = Html.fromHtml(escapeMore).toString();
542         }
543         return unescaped;
544     }
545 
tokenizeAddresses(String addresses)546     public static String[] tokenizeAddresses(String addresses) {
547         if (TextUtils.isEmpty(addresses)) {
548             return new String[0];
549         }
550 
551         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addresses);
552         String[] strings = new String[tokens.length];
553         for (int i = 0; i < tokens.length;i++) {
554             strings[i] = tokens[i].toString();
555         }
556         return strings;
557     }
558 
getAttachments()559     public List<Attachment> getAttachments() {
560         if (mAttachments == null) {
561             if (attachmentsJson != null) {
562                 mAttachments = Attachment.fromJSONArray(attachmentsJson);
563             } else {
564                 mAttachments = Collections.emptyList();
565             }
566         }
567         return mAttachments;
568     }
569 
570     /**
571      * Returns whether a "Show Pictures" button should initially appear for this message. If the
572      * button is shown, the message must also block all non-local images in the body. Inversely, if
573      * the button is not shown, the message must show all images within (or else the user would be
574      * stuck with no images and no way to reveal them).
575      *
576      * @return true if a "Show Pictures" button should appear.
577      */
shouldShowImagePrompt()578     public boolean shouldShowImagePrompt() {
579         return !alwaysShowImages && (embedsExternalResources ||
580                 (!TextUtils.isEmpty(bodyHtml) && INLINE_IMAGE_PATTERN.matcher(bodyHtml).find()));
581     }
582 
583     @Override
embedsExternalResources()584     public boolean embedsExternalResources() {
585         return embedsExternalResources;
586     }
587 
588     /**
589      * Helper method to command a provider to mark all messages from this sender with the
590      * {@link MessageColumns#ALWAYS_SHOW_IMAGES} flag set.
591      *
592      * @param handler a caller-provided handler to run the query on
593      * @param token (optional) token to identify the command to the handler
594      * @param cookie (optional) cookie to pass to the handler
595      */
markAlwaysShowImages(AsyncQueryHandler handler, int token, Object cookie)596     public void markAlwaysShowImages(AsyncQueryHandler handler, int token, Object cookie) {
597         alwaysShowImages = true;
598 
599         final ContentValues values = new ContentValues(1);
600         values.put(UIProvider.MessageColumns.ALWAYS_SHOW_IMAGES, 1);
601 
602         handler.startUpdate(token, cookie, uri, values, null, null);
603     }
604 
605     @Override
getBodyAsHtml()606     public String getBodyAsHtml() {
607         String body = "";
608         if (!TextUtils.isEmpty(bodyHtml)) {
609             body = bodyHtml;
610         } else if (!TextUtils.isEmpty(bodyText)) {
611             final SpannableString spannable = new SpannableString(bodyText);
612             Linkify.addLinks(spannable, Linkify.EMAIL_ADDRESSES);
613             body = Html.toHtml(spannable);
614         }
615         return body;
616     }
617 
618     @Override
getId()619     public long getId() {
620         return id;
621     }
622 }
623