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.MimeMessage;
35import com.android.emailcommon.internet.MimeUtility;
36import com.android.emailcommon.mail.MessagingException;
37import com.android.emailcommon.mail.Part;
38import com.android.emailcommon.utility.ConversionUtilities;
39import com.android.mail.providers.UIProvider.MessageColumns;
40import com.android.mail.ui.HtmlMessage;
41import com.android.mail.utils.Utils;
42import com.google.common.annotations.VisibleForTesting;
43import com.google.common.base.Objects;
44import com.google.common.collect.Lists;
45
46import java.util.ArrayList;
47import java.util.Collections;
48import java.util.List;
49import java.util.regex.Pattern;
50
51
52public 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
209    public int describeContents() {
210        return 0;
211    }
212
213    @Override
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
220    public int hashCode() {
221        return uri == null ? 0 : uri.hashCode();
222    }
223
224    @Override
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
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
293    public Message() {
294
295    }
296
297    @Override
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
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
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
421    public boolean isFlaggedReplied() {
422        return (messageFlags & UIProvider.MessageFlags.REPLIED) ==
423                UIProvider.MessageFlags.REPLIED;
424    }
425
426    public boolean isFlaggedForwarded() {
427        return (messageFlags & UIProvider.MessageFlags.FORWARDED) ==
428                UIProvider.MessageFlags.FORWARDED;
429    }
430
431    public boolean isFlaggedCalendarInvite() {
432        return (messageFlags & UIProvider.MessageFlags.CALENDAR_INVITE) ==
433                UIProvider.MessageFlags.CALENDAR_INVITE;
434    }
435
436    public String getFrom() {
437        return mFrom;
438    }
439
440    public synchronized void setFrom(final String from) {
441        mFrom = from;
442        mFromAddresses = null;
443    }
444
445    public String getTo() {
446        return mTo;
447    }
448
449    public synchronized void setTo(final String to) {
450        mTo = to;
451        mToAddresses = null;
452    }
453
454    public String getCc() {
455        return mCc;
456    }
457
458    public synchronized void setCc(final String cc) {
459        mCc = cc;
460        mCcAddresses = null;
461    }
462
463    public String getBcc() {
464        return mBcc;
465    }
466
467    public synchronized void setBcc(final String bcc) {
468        mBcc = bcc;
469        mBccAddresses = null;
470    }
471
472    @VisibleForTesting
473    public String getReplyTo() {
474        return mReplyTo;
475    }
476
477    public synchronized void setReplyTo(final String replyTo) {
478        mReplyTo = replyTo;
479        mReplyToAddresses = null;
480    }
481
482    public synchronized String[] getFromAddresses() {
483        if (mFromAddresses == null) {
484            mFromAddresses = tokenizeAddresses(mFrom);
485        }
486        return mFromAddresses;
487    }
488
489    public String[] getFromAddressesUnescaped() {
490        return unescapeAddresses(getFromAddresses());
491    }
492
493    public synchronized String[] getToAddresses() {
494        if (mToAddresses == null) {
495            mToAddresses = tokenizeAddresses(mTo);
496        }
497        return mToAddresses;
498    }
499
500    public String[] getToAddressesUnescaped() {
501        return unescapeAddresses(getToAddresses());
502    }
503
504    public synchronized String[] getCcAddresses() {
505        if (mCcAddresses == null) {
506            mCcAddresses = tokenizeAddresses(mCc);
507        }
508        return mCcAddresses;
509    }
510
511    public String[] getCcAddressesUnescaped() {
512        return unescapeAddresses(getCcAddresses());
513    }
514
515    public synchronized String[] getBccAddresses() {
516        if (mBccAddresses == null) {
517            mBccAddresses = tokenizeAddresses(mBcc);
518        }
519        return mBccAddresses;
520    }
521
522    public String[] getBccAddressesUnescaped() {
523        return unescapeAddresses(getBccAddresses());
524    }
525
526    public synchronized String[] getReplyToAddresses() {
527        if (mReplyToAddresses == null) {
528            mReplyToAddresses = tokenizeAddresses(mReplyTo);
529        }
530        return mReplyToAddresses;
531    }
532
533    public String[] getReplyToAddressesUnescaped() {
534        return unescapeAddresses(getReplyToAddresses());
535    }
536
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
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
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     */
578    public boolean shouldShowImagePrompt() {
579        return !alwaysShowImages && (embedsExternalResources ||
580                (!TextUtils.isEmpty(bodyHtml) && INLINE_IMAGE_PATTERN.matcher(bodyHtml).find()));
581    }
582
583    @Override
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     */
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
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
619    public long getId() {
620        return id;
621    }
622}
623