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