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