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         final 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         final 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         final OutputStream stream = new BufferedOutputStream(out, 1024);
    132         final 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         final 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         final Body body = Body.restoreBodyWithMessageId(context, message.mId);
    157         final 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         final boolean multipart = attachments.size() > 0;
    166 
    167         // Simplified case for no multipart - just emit text and be done.
    168         if (!multipart) {
    169             writeTextWithHeaders(writer, stream, bodyText);
    170         } else {
    171             // continue with multipart headers, then into multipart body
    172             final String multipartBoundary = getNextBoundary();
    173             String multipartType = "mixed";
    174 
    175             // Move to the first attachment; this must succeed because multipart is true
    176             if (attachments.size() == 1) {
    177                 // If we've got one attachment and it's an ics "attachment", we want to send
    178                 // this as multipart/alternative instead of multipart/mixed
    179                 final int flags = attachments.get(0).mFlags;
    180                 if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) {
    181                     multipartType = "alternative";
    182                 }
    183             }
    184 
    185             writeHeader(writer, "Content-Type",
    186                     "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\"");
    187             // Finish headers and prepare for body section(s)
    188             writer.write("\r\n");
    189 
    190             // first multipart element is the body
    191             if (bodyText[INDEX_BODY_TEXT] != null || bodyText[INDEX_BODY_HTML] != null) {
    192                 writeBoundary(writer, multipartBoundary, false);
    193                 writeTextWithHeaders(writer, stream, bodyText);
    194             }
    195 
    196             // Write out the attachments until we run out
    197             for (final Attachment att: attachments) {
    198                 writeBoundary(writer, multipartBoundary, false);
    199                 writeOneAttachment(context, writer, stream, att);
    200                 writer.write("\r\n");
    201             }
    202 
    203             // end of multipart section
    204             writeBoundary(writer, multipartBoundary, true);
    205         }
    206 
    207         writer.flush();
    208         out.flush();
    209     }
    210 
    211     /**
    212      * Write a single attachment and its payload
    213      */
    214     private static void writeOneAttachment(Context context, Writer writer, OutputStream out,
    215             Attachment attachment) throws IOException, MessagingException {
    216         writeHeader(writer, "Content-Type",
    217                 attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\"");
    218         writeHeader(writer, "Content-Transfer-Encoding", "base64");
    219         // Most attachments (real files) will send Content-Disposition.  The suppression option
    220         // is used when sending calendar invites.
    221         if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) {
    222             writeHeader(writer, "Content-Disposition",
    223                     "attachment;"
    224                     + "\n filename=\"" + attachment.mFileName + "\";"
    225                     + "\n size=" + Long.toString(attachment.mSize));
    226         }
    227         if (attachment.mContentId != null) {
    228             writeHeader(writer, "Content-ID", attachment.mContentId);
    229         }
    230         writer.append("\r\n");
    231 
    232         // Set up input stream and write it out via base64
    233         InputStream inStream = null;
    234         try {
    235             // Use content, if provided; otherwise, use the contentUri
    236             if (attachment.mContentBytes != null) {
    237                 inStream = new ByteArrayInputStream(attachment.mContentBytes);
    238             } else {
    239                 // First try the cached file
    240                 final String cachedFile = attachment.getCachedFileUri();
    241                 if (!TextUtils.isEmpty(cachedFile)) {
    242                     final Uri cachedFileUri = Uri.parse(cachedFile);
    243                     try {
    244                         inStream = context.getContentResolver().openInputStream(cachedFileUri);
    245                     } catch (FileNotFoundException e) {
    246                         // Couldn't open the cached file, fall back to the original content uri
    247                         inStream = null;
    248 
    249                         LogUtils.d(TAG, "Rfc822Output#writeOneAttachment(), failed to load" +
    250                                 "cached file, falling back to: %s", attachment.getContentUri());
    251                     }
    252                 }
    253 
    254                 if (inStream == null) {
    255                     // try to open the file
    256                     final Uri fileUri = Uri.parse(attachment.getContentUri());
    257                     inStream = context.getContentResolver().openInputStream(fileUri);
    258                 }
    259             }
    260             // switch to output stream for base64 text output
    261             writer.flush();
    262             Base64OutputStream base64Out = new Base64OutputStream(
    263                 out, Base64.CRLF | Base64.NO_CLOSE);
    264             // copy base64 data and close up
    265             IOUtils.copy(inStream, base64Out);
    266             base64Out.close();
    267 
    268             // The old Base64OutputStream wrote an extra CRLF after
    269             // the output.  It's not required by the base-64 spec; not
    270             // sure if it's required by RFC 822 or not.
    271             out.write('\r');
    272             out.write('\n');
    273             out.flush();
    274         }
    275         catch (FileNotFoundException fnfe) {
    276             // Ignore this - empty file is OK
    277             LogUtils.e(TAG, fnfe, "Rfc822Output#writeOneAttachment(), FileNotFoundException" +
    278                     "when sending attachment");
    279         }
    280         catch (IOException ioe) {
    281             LogUtils.e(TAG, ioe, "Rfc822Output#writeOneAttachment(), IOException" +
    282                     "when sending attachment");
    283             throw new MessagingException("Invalid attachment.", ioe);
    284         }
    285     }
    286 
    287     /**
    288      * Write a single header with no wrapping or encoding
    289      *
    290      * @param writer the output writer
    291      * @param name the header name
    292      * @param value the header value
    293      */
    294     private static void writeHeader(Writer writer, String name, String value) throws IOException {
    295         if (value != null && value.length() > 0) {
    296             writer.append(name);
    297             writer.append(": ");
    298             writer.append(value);
    299             writer.append("\r\n");
    300         }
    301     }
    302 
    303     /**
    304      * Write a single header using appropriate folding & encoding
    305      *
    306      * @param writer the output writer
    307      * @param name the header name
    308      * @param value the header value
    309      */
    310     private static void writeEncodedHeader(Writer writer, String name, String value)
    311             throws IOException {
    312         if (value != null && value.length() > 0) {
    313             writer.append(name);
    314             writer.append(": ");
    315             writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2));
    316             writer.append("\r\n");
    317         }
    318     }
    319 
    320     /**
    321      * Unpack, encode, and fold address(es) into a header
    322      *
    323      * @param writer the output writer
    324      * @param name the header name
    325      * @param value the header value (a packed list of addresses)
    326      */
    327     private static void writeAddressHeader(Writer writer, String name, String value)
    328             throws IOException {
    329         if (value != null && value.length() > 0) {
    330             writer.append(name);
    331             writer.append(": ");
    332             writer.append(MimeUtility.fold(Address.reformatToHeader(value), name.length() + 2));
    333             writer.append("\r\n");
    334         }
    335     }
    336 
    337     /**
    338      * Write a multipart boundary
    339      *
    340      * @param writer the output writer
    341      * @param boundary the boundary string
    342      * @param end false if inner boundary, true if final boundary
    343      */
    344     private static void writeBoundary(Writer writer, String boundary, boolean end)
    345             throws IOException {
    346         writer.append("--");
    347         writer.append(boundary);
    348         if (end) {
    349             writer.append("--");
    350         }
    351         writer.append("\r\n");
    352     }
    353 
    354     /**
    355      * Write the body text.
    356      *
    357      * Note this always uses base64, even when not required.  Slightly less efficient for
    358      * US-ASCII text, but handles all formats even when non-ascii chars are involved.  A small
    359      * optimization might be to prescan the string for safety and send raw if possible.
    360      *
    361      * @param writer the output writer
    362      * @param out the output stream inside the writer (used for byte[] access)
    363      * @param bodyText Plain text and HTML versions of the original text of the message
    364      */
    365     private static void writeTextWithHeaders(Writer writer, OutputStream out, String[] bodyText)
    366             throws IOException {
    367         boolean html = false;
    368         String text = bodyText[INDEX_BODY_TEXT];
    369         if (TextUtils.isEmpty(text)) {
    370             text = bodyText[INDEX_BODY_HTML];
    371             html = true;
    372         }
    373         if (TextUtils.isEmpty(text)) {
    374             writer.write("\r\n");       // a truly empty message
    375         } else {
    376             // first multipart element is the body
    377             final String mimeType = "text/" + (html ? "html" : "plain");
    378             writeHeader(writer, "Content-Type", mimeType + "; charset=utf-8");
    379             writeHeader(writer, "Content-Transfer-Encoding", "base64");
    380             writer.write("\r\n");
    381             final byte[] textBytes = text.getBytes("UTF-8");
    382             writer.flush();
    383             out.write(Base64.encode(textBytes, Base64.CRLF));
    384         }
    385     }
    386 
    387     /**
    388      * Returns a unique boundary string.
    389      */
    390     /*package*/ static String getNextBoundary() {
    391         final StringBuilder boundary = new StringBuilder();
    392         boundary.append("--_com.android.email_").append(System.nanoTime());
    393         synchronized (Rfc822Output.class) {
    394             boundary.append(sBoundaryDigit);
    395             sBoundaryDigit = (byte)((sBoundaryDigit + 1) % 10);
    396         }
    397         return boundary.toString();
    398     }
    399 }
    400