Home | History | Annotate | Download | only in internet
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      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.emailcommon.internet;
     18 
     19 import android.content.Context;
     20 import android.net.Uri;
     21 import android.text.TextUtils;
     22 import android.util.Base64;
     23 import android.util.Base64OutputStream;
     24 
     25 import com.android.emailcommon.mail.Address;
     26 import com.android.emailcommon.mail.MessagingException;
     27 import com.android.emailcommon.provider.EmailContent.Attachment;
     28 import com.android.emailcommon.provider.EmailContent.Body;
     29 import com.android.emailcommon.provider.EmailContent.Message;
     30 
     31 import com.android.mail.utils.LogUtils;
     32 
     33 import org.apache.commons.io.IOUtils;
     34 
     35 import java.io.BufferedOutputStream;
     36 import java.io.ByteArrayInputStream;
     37 import java.io.FileNotFoundException;
     38 import java.io.IOException;
     39 import java.io.InputStream;
     40 import java.io.OutputStream;
     41 import java.io.OutputStreamWriter;
     42 import java.io.Writer;
     43 import java.text.SimpleDateFormat;
     44 import java.util.Arrays;
     45 import java.util.Date;
     46 import java.util.List;
     47 import java.util.Locale;
     48 import java.util.regex.Matcher;
     49 import java.util.regex.Pattern;
     50 
     51 /**
     52  * Utility class to output RFC 822 messages from provider email messages
     53  */
     54 public class Rfc822Output {
     55     private static final String TAG = "Email";
     56 
     57     // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
     58     // "Jan", not the other localized format like "Ene" (meaning January in locale es).
     59     private static final SimpleDateFormat DATE_FORMAT =
     60         new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
     61 
     62     /** A less-than-perfect pattern to pull out <body> content */
     63     private static final Pattern BODY_PATTERN = Pattern.compile(
     64                 "(?:<\\s*body[^>]*>)(.*)(?:<\\s*/\\s*body\\s*>)",
     65                 Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
     66     /** Match group in {@code BODY_PATTERN} for the body HTML */
     67     private static final int BODY_PATTERN_GROUP = 1;
     68     /** Index of the plain text version of the message body */
     69     private final static int INDEX_BODY_TEXT = 0;
     70     /** Index of the HTML version of the message body */
     71     private final static int INDEX_BODY_HTML = 1;
     72     /** Single digit [0-9] to ensure uniqueness of the MIME boundary */
     73     /*package*/ static byte sBoundaryDigit;
     74 
     75     /**
     76      * Returns just the content between the <body></body> tags. This is not perfect and breaks
     77      * with malformed HTML or if there happens to be special characters in the attributes of
     78      * the <body> tag (e.g. a '>' in a java script block).
     79      */
     80     /*package*/ static String getHtmlBody(String html) {
     81         Matcher match = BODY_PATTERN.matcher(html);
     82         if (match.find()) {
     83             return match.group(BODY_PATTERN_GROUP);    // Found body; return
     84         } else {
     85             return html;              // Body not found; return the full HTML and hope for the best
     86         }
     87     }
     88 
     89     /**
     90      * Gets both the plain text and HTML versions of the message body.
     91      */
     92     /*package*/ static String[] buildBodyText(Body body, boolean useSmartReply) {
     93         if (body == null) {
     94             return new String[2];
     95         }
     96         String[] messageBody = new String[] { body.mTextContent, body.mHtmlContent };
     97         final int pos = body.mQuotedTextStartPos;
     98         if (useSmartReply && pos > 0) {
     99             if (messageBody[0] != null) {
    100                 if (pos < messageBody[0].length()) {
    101                     messageBody[0] = messageBody[0].substring(0, pos);
    102                 }
    103             } else if (messageBody[1] != null) {
    104                 if (pos < messageBody[1].length()) {
    105                     messageBody[1] = messageBody[1].substring(0, pos);
    106                 }
    107             }
    108         }
    109         return messageBody;
    110     }
    111 
    112     /**
    113      * Write the entire message to an output stream.  This method provides buffering, so it is
    114      * not necessary to pass in a buffered output stream here.
    115      *
    116      * @param context system context for accessing the provider
    117      * @param message the message to write out
    118      * @param out the output stream to write the message to
    119      * @param useSmartReply whether or not quoted text is appended to a reply/forward
    120      * @param sendBcc Whether to add the bcc header
    121      * @param attachments list of attachments to send (or null if retrieved from the message itself)
    122      */
    123     public static void writeTo(Context context, Message message, OutputStream out,
    124             boolean useSmartReply, boolean sendBcc, List<Attachment> attachments)
    125                     throws IOException, MessagingException {
    126         if (message == null) {
    127             // throw something?
    128             return;
    129         }
    130 
    131         OutputStream stream = new BufferedOutputStream(out, 1024);
    132         Writer writer = new OutputStreamWriter(stream);
    133 
    134         // Write the fixed headers.  Ordering is arbitrary (the legacy code iterated through a
    135         // hashmap here).
    136 
    137         String date = DATE_FORMAT.format(new Date(message.mTimeStamp));
    138         writeHeader(writer, "Date", date);
    139 
    140         writeEncodedHeader(writer, "Subject", message.mSubject);
    141 
    142         writeHeader(writer, "Message-ID", message.mMessageId);
    143 
    144         writeAddressHeader(writer, "From", message.mFrom);
    145         writeAddressHeader(writer, "To", message.mTo);
    146         writeAddressHeader(writer, "Cc", message.mCc);
    147         // Address fields.  Note that we skip bcc unless the sendBcc argument is true
    148         // SMTP should NOT send bcc headers, but EAS must send it!
    149         if (sendBcc) {
    150             writeAddressHeader(writer, "Bcc", message.mBcc);
    151         }
    152         writeAddressHeader(writer, "Reply-To", message.mReplyTo);
    153         writeHeader(writer, "MIME-Version", "1.0");
    154 
    155         // Analyze message and determine if we have multiparts
    156         Body body = Body.restoreBodyWithMessageId(context, message.mId);
    157         String[] bodyText = buildBodyText(body, useSmartReply);
    158 
    159         // If a list of attachments hasn't been passed in, build one from the message
    160         if (attachments == null) {
    161             attachments =
    162                     Arrays.asList(Attachment.restoreAttachmentsWithMessageId(context, message.mId));
    163         }
    164 
    165         boolean multipart = attachments.size() > 0;
    166         String multipartBoundary = null;
    167         String multipartType = "mixed";
    168 
    169         // Simplified case for no multipart - just emit text and be done.
    170         if (!multipart) {
    171             writeTextWithHeaders(writer, stream, bodyText);
    172         } else {
    173             // continue with multipart headers, then into multipart body
    174             multipartBoundary = getNextBoundary();
    175 
    176             // Move to the first attachment; this must succeed because multipart is true
    177             if (attachments.size() == 1) {
    178                 // If we've got one attachment and it's an ics "attachment", we want to send
    179                 // this as multipart/alternative instead of multipart/mixed
    180                 int flags = attachments.get(0).mFlags;
    181                 if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) {
    182                     multipartType = "alternative";
    183                 }
    184             }
    185 
    186             writeHeader(writer, "Content-Type",
    187                     "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\"");
    188             // Finish headers and prepare for body section(s)
    189             writer.write("\r\n");
    190 
    191             // first multipart element is the body
    192             if (bodyText[INDEX_BODY_TEXT] != null || bodyText[INDEX_BODY_HTML] != null) {
    193                 writeBoundary(writer, multipartBoundary, false);
    194                 writeTextWithHeaders(writer, stream, bodyText);
    195             }
    196 
    197             // Write out the attachments until we run out
    198             for (Attachment att: attachments) {
    199                 writeBoundary(writer, multipartBoundary, false);
    200                 writeOneAttachment(context, writer, stream, att);
    201                 writer.write("\r\n");
    202             }
    203 
    204             // end of multipart section
    205             writeBoundary(writer, multipartBoundary, true);
    206         }
    207 
    208         writer.flush();
    209         out.flush();
    210     }
    211 
    212     /**
    213      * Write a single attachment and its payload
    214      */
    215     private static void writeOneAttachment(Context context, Writer writer, OutputStream out,
    216             Attachment attachment) throws IOException, MessagingException {
    217         writeHeader(writer, "Content-Type",
    218                 attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\"");
    219         writeHeader(writer, "Content-Transfer-Encoding", "base64");
    220         // Most attachments (real files) will send Content-Disposition.  The suppression option
    221         // is used when sending calendar invites.
    222         if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) {
    223             writeHeader(writer, "Content-Disposition",
    224                     "attachment;"
    225                     + "\n filename=\"" + attachment.mFileName + "\";"
    226                     + "\n size=" + Long.toString(attachment.mSize));
    227         }
    228         if (attachment.mContentId != null) {
    229             writeHeader(writer, "Content-ID", attachment.mContentId);
    230         }
    231         writer.append("\r\n");
    232 
    233         // Set up input stream and write it out via base64
    234         InputStream inStream = null;
    235         try {
    236             // Use content, if provided; otherwise, use the contentUri
    237             if (attachment.mContentBytes != null) {
    238                 inStream = new ByteArrayInputStream(attachment.mContentBytes);
    239             } else {
    240                 // First try the cached file
    241                 final String cachedFile = attachment.getCachedFileUri();
    242                 if (!TextUtils.isEmpty(cachedFile)) {
    243                     final Uri cachedFileUri = Uri.parse(cachedFile);
    244                     try {
    245                         inStream = context.getContentResolver().openInputStream(cachedFileUri);
    246                     } catch (FileNotFoundException e) {
    247                         // Couldn't open the cached file, fall back to the original content uri
    248                         inStream = null;
    249 
    250                         LogUtils.d(TAG, "Rfc822Output#writeOneAttachment(), failed to load" +
    251                                 "cached file, falling back to: %s", attachment.getContentUri());
    252                     }
    253                 }
    254 
    255                 if (inStream == null) {
    256                     // try to open the file
    257                     final Uri fileUri = Uri.parse(attachment.getContentUri());
    258                     inStream = context.getContentResolver().openInputStream(fileUri);
    259                 }
    260             }
    261             // switch to output stream for base64 text output
    262             writer.flush();
    263             Base64OutputStream base64Out = new Base64OutputStream(
    264                 out, Base64.CRLF | Base64.NO_CLOSE);
    265             // copy base64 data and close up
    266             IOUtils.copy(inStream, base64Out);
    267             base64Out.close();
    268 
    269             // The old Base64OutputStream wrote an extra CRLF after
    270             // the output.  It's not required by the base-64 spec; not
    271             // sure if it's required by RFC 822 or not.
    272             out.write('\r');
    273             out.write('\n');
    274             out.flush();
    275         }
    276         catch (FileNotFoundException fnfe) {
    277             // Ignore this - empty file is OK
    278             LogUtils.e(TAG, fnfe, "Rfc822Output#writeOneAttachment(), FileNotFoundException" +
    279                     "when sending attachment");
    280         }
    281         catch (IOException ioe) {
    282             LogUtils.e(TAG, ioe, "Rfc822Output#writeOneAttachment(), IOException" +
    283                     "when sending attachment");
    284             throw new MessagingException("Invalid attachment.", ioe);
    285         }
    286     }
    287 
    288     /**
    289      * Write a single header with no wrapping or encoding
    290      *
    291      * @param writer the output writer
    292      * @param name the header name
    293      * @param value the header value
    294      */
    295     private static void writeHeader(Writer writer, String name, String value) throws IOException {
    296         if (value != null && value.length() > 0) {
    297             writer.append(name);
    298             writer.append(": ");
    299             writer.append(value);
    300             writer.append("\r\n");
    301         }
    302     }
    303 
    304     /**
    305      * Write a single header using appropriate folding & encoding
    306      *
    307      * @param writer the output writer
    308      * @param name the header name
    309      * @param value the header value
    310      */
    311     private static void writeEncodedHeader(Writer writer, String name, String value)
    312             throws IOException {
    313         if (value != null && value.length() > 0) {
    314             writer.append(name);
    315             writer.append(": ");
    316             writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2));
    317             writer.append("\r\n");
    318         }
    319     }
    320 
    321     /**
    322      * Unpack, encode, and fold address(es) into a header
    323      *
    324      * @param writer the output writer
    325      * @param name the header name
    326      * @param value the header value (a packed list of addresses)
    327      */
    328     private static void writeAddressHeader(Writer writer, String name, String value)
    329             throws IOException {
    330         if (value != null && value.length() > 0) {
    331             writer.append(name);
    332             writer.append(": ");
    333             writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2));
    334             writer.append("\r\n");
    335         }
    336     }
    337 
    338     /**
    339      * Write a multipart boundary
    340      *
    341      * @param writer the output writer
    342      * @param boundary the boundary string
    343      * @param end false if inner boundary, true if final boundary
    344      */
    345     private static void writeBoundary(Writer writer, String boundary, boolean end)
    346             throws IOException {
    347         writer.append("--");
    348         writer.append(boundary);
    349         if (end) {
    350             writer.append("--");
    351         }
    352         writer.append("\r\n");
    353     }
    354 
    355     /**
    356      * Write the body text.
    357      *
    358      * Note this always uses base64, even when not required.  Slightly less efficient for
    359      * US-ASCII text, but handles all formats even when non-ascii chars are involved.  A small
    360      * optimization might be to prescan the string for safety and send raw if possible.
    361      *
    362      * @param writer the output writer
    363      * @param out the output stream inside the writer (used for byte[] access)
    364      * @param bodyText Plain text and HTML versions of the original text of the message
    365      */
    366     private static void writeTextWithHeaders(Writer writer, OutputStream out, String[] bodyText)
    367             throws IOException {
    368         boolean html = false;
    369         String text = bodyText[INDEX_BODY_TEXT];
    370         if (text == null) {
    371             text = bodyText[INDEX_BODY_HTML];
    372             html = true;
    373         }
    374         if (text == null) {
    375             writer.write("\r\n");       // a truly empty message
    376         } else {
    377             // first multipart element is the body
    378             String mimeType = "text/" + (html ? "html" : "plain");
    379             writeHeader(writer, "Content-Type", mimeType + "; charset=utf-8");
    380             writeHeader(writer, "Content-Transfer-Encoding", "base64");
    381             writer.write("\r\n");
    382             byte[] textBytes = text.getBytes("UTF-8");
    383             writer.flush();
    384             out.write(Base64.encode(textBytes, Base64.CRLF));
    385         }
    386     }
    387 
    388     /**
    389      * Returns a unique boundary string.
    390      */
    391     /*package*/ static String getNextBoundary() {
    392         StringBuilder boundary = new StringBuilder();
    393         boundary.append("--_com.android.email_").append(System.nanoTime());
    394         synchronized (Rfc822Output.class) {
    395             boundary = boundary.append(sBoundaryDigit);
    396             sBoundaryDigit = (byte)((sBoundaryDigit + 1) % 10);
    397         }
    398         return boundary.toString();
    399     }
    400 }
    401