Home | History | Annotate | Download | only in transport
      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.email.mail.transport;
     18 
     19 import com.android.email.mail.Address;
     20 import com.android.email.mail.MessagingException;
     21 import com.android.email.mail.internet.MimeUtility;
     22 import com.android.email.provider.EmailContent.Attachment;
     23 import com.android.email.provider.EmailContent.Body;
     24 import com.android.email.provider.EmailContent.Message;
     25 
     26 import org.apache.commons.io.IOUtils;
     27 
     28 import android.content.ContentUris;
     29 import android.content.Context;
     30 import android.database.Cursor;
     31 import android.net.Uri;
     32 import android.util.Base64;
     33 import android.util.Base64OutputStream;
     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.Date;
     45 import java.util.Locale;
     46 import java.util.regex.Matcher;
     47 import java.util.regex.Pattern;
     48 
     49 /**
     50  * Utility class to output RFC 822 messages from provider email messages
     51  */
     52 public class Rfc822Output {
     53 
     54     private static final Pattern PATTERN_START_OF_LINE = Pattern.compile("(?m)^");
     55     private static final Pattern PATTERN_ENDLINE_CRLF = Pattern.compile("\r\n");
     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     /*package*/ static String buildBodyText(Context context, Message message,
     63             boolean appendQuotedText) {
     64         Body body = Body.restoreBodyWithMessageId(context, message.mId);
     65         if (body == null) {
     66             return null;
     67         }
     68 
     69         String text = body.mTextContent;
     70         int flags = message.mFlags;
     71         boolean isReply = (flags & Message.FLAG_TYPE_REPLY) != 0;
     72         boolean isForward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
     73         // For all forwards/replies, we add the intro text
     74         if (isReply || isForward) {
     75             String intro = body.mIntroText == null ? "" : body.mIntroText;
     76             text += intro;
     77         }
     78         if (!appendQuotedText) {
     79             // appendQuotedText is set to false for use by SmartReply/SmartForward in EAS.
     80             // SmartForward doesn't put a break between the original and new text, so we add an LF
     81             if (isForward) {
     82                 text += "\n";
     83             }
     84             return text;
     85         }
     86 
     87         String quotedText = body.mTextReply;
     88         if (quotedText != null) {
     89             // fix CR-LF line endings to LF-only needed by EditText.
     90             Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText);
     91             quotedText = matcher.replaceAll("\n");
     92         }
     93         if (isReply) {
     94             if (quotedText != null) {
     95                 Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText);
     96                 text += matcher.replaceAll(">");
     97             }
     98         } else if (isForward) {
     99             if (quotedText != null) {
    100                 text += quotedText;
    101             }
    102         }
    103         return text;
    104     }
    105 
    106     /**
    107      * Write the entire message to an output stream.  This method provides buffering, so it is
    108      * not necessary to pass in a buffered output stream here.
    109      *
    110      * @param context system context for accessing the provider
    111      * @param messageId the message to write out
    112      * @param out the output stream to write the message to
    113      * @param appendQuotedText whether or not to append quoted text if this is a reply/forward
    114      *
    115      * TODO alternative parts (e.g. text+html) are not supported here.
    116      */
    117     public static void writeTo(Context context, long messageId, OutputStream out,
    118             boolean appendQuotedText, boolean sendBcc) throws IOException, MessagingException {
    119         Message message = Message.restoreMessageWithId(context, messageId);
    120         if (message == null) {
    121             // throw something?
    122             return;
    123         }
    124 
    125         OutputStream stream = new BufferedOutputStream(out, 1024);
    126         Writer writer = new OutputStreamWriter(stream);
    127 
    128         // Write the fixed headers.  Ordering is arbitrary (the legacy code iterated through a
    129         // hashmap here).
    130 
    131         String date = DATE_FORMAT.format(new Date(message.mTimeStamp));
    132         writeHeader(writer, "Date", date);
    133 
    134         writeEncodedHeader(writer, "Subject", message.mSubject);
    135 
    136         writeHeader(writer, "Message-ID", message.mMessageId);
    137 
    138         writeAddressHeader(writer, "From", message.mFrom);
    139         writeAddressHeader(writer, "To", message.mTo);
    140         writeAddressHeader(writer, "Cc", message.mCc);
    141         // Address fields.  Note that we skip bcc unless the sendBcc argument is true
    142         // SMTP should NOT send bcc headers, but EAS must send it!
    143         if (sendBcc) {
    144             writeAddressHeader(writer, "Bcc", message.mBcc);
    145         }
    146         writeAddressHeader(writer, "Reply-To", message.mReplyTo);
    147         writeHeader(writer, "MIME-Version", "1.0");
    148 
    149         // Analyze message and determine if we have multiparts
    150         String text = buildBodyText(context, message, appendQuotedText);
    151 
    152         Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
    153         Cursor attachmentsCursor = context.getContentResolver().query(uri,
    154                 Attachment.CONTENT_PROJECTION, null, null, null);
    155 
    156         try {
    157             int attachmentCount = attachmentsCursor.getCount();
    158             boolean multipart = attachmentCount > 0;
    159             String multipartBoundary = null;
    160             String multipartType = "mixed";
    161 
    162             // Simplified case for no multipart - just emit text and be done.
    163             if (!multipart) {
    164                 if (text != null) {
    165                     writeTextWithHeaders(writer, stream, text);
    166                 } else {
    167                     writer.write("\r\n");       // a truly empty message
    168                 }
    169             } else {
    170                 // continue with multipart headers, then into multipart body
    171                 multipartBoundary = "--_com.android.email_" + System.nanoTime();
    172 
    173                 // Move to the first attachment; this must succeed because multipart is true
    174                 attachmentsCursor.moveToFirst();
    175                 if (attachmentCount == 1) {
    176                     // If we've got one attachment and it's an ics "attachment", we want to send
    177                     // this as multipart/alternative instead of multipart/mixed
    178                     int flags = attachmentsCursor.getInt(Attachment.CONTENT_FLAGS_COLUMN);
    179                     if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) {
    180                         multipartType = "alternative";
    181                     }
    182                 }
    183 
    184                 writeHeader(writer, "Content-Type",
    185                         "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\"");
    186                 // Finish headers and prepare for body section(s)
    187                 writer.write("\r\n");
    188 
    189                 // first multipart element is the body
    190                 if (text != null) {
    191                     writeBoundary(writer, multipartBoundary, false);
    192                     writeTextWithHeaders(writer, stream, text);
    193                 }
    194 
    195                 // Write out the attachments until we run out
    196                 do {
    197                     writeBoundary(writer, multipartBoundary, false);
    198                     Attachment attachment =
    199                         Attachment.getContent(attachmentsCursor, Attachment.class);
    200                     writeOneAttachment(context, writer, stream, attachment);
    201                     writer.write("\r\n");
    202                 } while (attachmentsCursor.moveToNext());
    203 
    204                 // end of multipart section
    205                 writeBoundary(writer, multipartBoundary, true);
    206             }
    207         } finally {
    208             attachmentsCursor.close();
    209         }
    210 
    211         writer.flush();
    212         out.flush();
    213     }
    214 
    215     /**
    216      * Write a single attachment and its payload
    217      */
    218     private static void writeOneAttachment(Context context, Writer writer, OutputStream out,
    219             Attachment attachment) throws IOException, MessagingException {
    220         writeHeader(writer, "Content-Type",
    221                 attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\"");
    222         writeHeader(writer, "Content-Transfer-Encoding", "base64");
    223         // Most attachments (real files) will send Content-Disposition.  The suppression option
    224         // is used when sending calendar invites.
    225         if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) {
    226             writeHeader(writer, "Content-Disposition",
    227                     "attachment;"
    228                     + "\n filename=\"" + attachment.mFileName + "\";"
    229                     + "\n size=" + Long.toString(attachment.mSize));
    230         }
    231         writeHeader(writer, "Content-ID", attachment.mContentId);
    232         writer.append("\r\n");
    233 
    234         // Set up input stream and write it out via base64
    235         InputStream inStream = null;
    236         try {
    237             // Use content, if provided; otherwise, use the contentUri
    238             if (attachment.mContentBytes != null) {
    239                 inStream = new ByteArrayInputStream(attachment.mContentBytes);
    240             } else {
    241                 // try to open the file
    242                 Uri fileUri = Uri.parse(attachment.mContentUri);
    243                 inStream = context.getContentResolver().openInputStream(fileUri);
    244             }
    245             // switch to output stream for base64 text output
    246             writer.flush();
    247             Base64OutputStream base64Out = new Base64OutputStream(
    248                 out, Base64.CRLF | Base64.NO_CLOSE);
    249             // copy base64 data and close up
    250             IOUtils.copy(inStream, base64Out);
    251             base64Out.close();
    252 
    253             // The old Base64OutputStream wrote an extra CRLF after
    254             // the output.  It's not required by the base-64 spec; not
    255             // sure if it's required by RFC 822 or not.
    256             out.write('\r');
    257             out.write('\n');
    258             out.flush();
    259         }
    260         catch (FileNotFoundException fnfe) {
    261             // Ignore this - empty file is OK
    262         }
    263         catch (IOException ioe) {
    264             throw new MessagingException("Invalid attachment.", ioe);
    265         }
    266     }
    267 
    268     /**
    269      * Write a single header with no wrapping or encoding
    270      *
    271      * @param writer the output writer
    272      * @param name the header name
    273      * @param value the header value
    274      */
    275     private static void writeHeader(Writer writer, String name, String value) throws IOException {
    276         if (value != null && value.length() > 0) {
    277             writer.append(name);
    278             writer.append(": ");
    279             writer.append(value);
    280             writer.append("\r\n");
    281         }
    282     }
    283 
    284     /**
    285      * Write a single header using appropriate folding & encoding
    286      *
    287      * @param writer the output writer
    288      * @param name the header name
    289      * @param value the header value
    290      */
    291     private static void writeEncodedHeader(Writer writer, String name, String value)
    292             throws IOException {
    293         if (value != null && value.length() > 0) {
    294             writer.append(name);
    295             writer.append(": ");
    296             writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2));
    297             writer.append("\r\n");
    298         }
    299     }
    300 
    301     /**
    302      * Unpack, encode, and fold address(es) into a header
    303      *
    304      * @param writer the output writer
    305      * @param name the header name
    306      * @param value the header value (a packed list of addresses)
    307      */
    308     private static void writeAddressHeader(Writer writer, String name, String value)
    309             throws IOException {
    310         if (value != null && value.length() > 0) {
    311             writer.append(name);
    312             writer.append(": ");
    313             writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2));
    314             writer.append("\r\n");
    315         }
    316     }
    317 
    318     /**
    319      * Write a multipart boundary
    320      *
    321      * @param writer the output writer
    322      * @param boundary the boundary string
    323      * @param end false if inner boundary, true if final boundary
    324      */
    325     private static void writeBoundary(Writer writer, String boundary, boolean end)
    326             throws IOException {
    327         writer.append("--");
    328         writer.append(boundary);
    329         if (end) {
    330             writer.append("--");
    331         }
    332         writer.append("\r\n");
    333     }
    334 
    335     /**
    336      * Write text (either as main body or inside a multipart), preceded by appropriate headers.
    337      *
    338      * Note this always uses base64, even when not required.  Slightly less efficient for
    339      * US-ASCII text, but handles all formats even when non-ascii chars are involved.  A small
    340      * optimization might be to prescan the string for safety and send raw if possible.
    341      *
    342      * @param writer the output writer
    343      * @param out the output stream inside the writer (used for byte[] access)
    344      * @param text The original text of the message
    345      */
    346     private static void writeTextWithHeaders(Writer writer, OutputStream out, String text)
    347             throws IOException {
    348         writeHeader(writer, "Content-Type", "text/plain; charset=utf-8");
    349         writeHeader(writer, "Content-Transfer-Encoding", "base64");
    350         writer.write("\r\n");
    351         byte[] bytes = text.getBytes("UTF-8");
    352         writer.flush();
    353         out.write(Base64.encode(bytes, Base64.CRLF));
    354     }
    355 }
    356