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