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.ContentUris;
     20 import android.content.Context;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.text.Html;
     24 import android.text.TextUtils;
     25 import android.util.Base64;
     26 import android.util.Base64OutputStream;
     27 
     28 import com.android.emailcommon.mail.Address;
     29 import com.android.emailcommon.mail.MessagingException;
     30 import com.android.emailcommon.provider.EmailContent.Attachment;
     31 import com.android.emailcommon.provider.EmailContent.Body;
     32 import com.android.emailcommon.provider.EmailContent.Message;
     33 
     34 import org.apache.commons.io.IOUtils;
     35 
     36 import java.io.BufferedOutputStream;
     37 import java.io.ByteArrayInputStream;
     38 import java.io.FileNotFoundException;
     39 import java.io.IOException;
     40 import java.io.InputStream;
     41 import java.io.OutputStream;
     42 import java.io.OutputStreamWriter;
     43 import java.io.Writer;
     44 import java.text.SimpleDateFormat;
     45 import java.util.Date;
     46 import java.util.Locale;
     47 import java.util.regex.Matcher;
     48 import java.util.regex.Pattern;
     49 
     50 /**
     51  * Utility class to output RFC 822 messages from provider email messages
     52  */
     53 public class Rfc822Output {
     54 
     55     private static final Pattern PATTERN_START_OF_LINE = Pattern.compile("(?m)^");
     56     private static final Pattern PATTERN_ENDLINE_CRLF = Pattern.compile("\r\n");
     57 
     58     // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
     59     // "Jan", not the other localized format like "Ene" (meaning January in locale es).
     60     private static final SimpleDateFormat DATE_FORMAT =
     61         new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
     62 
     63     private static final String WHERE_NOT_SMART_FORWARD = "(" + Attachment.FLAGS + "&" +
     64         Attachment.FLAG_SMART_FORWARD + ")=0";
     65 
     66     /** A less-than-perfect pattern to pull out <body> content */
     67     private static final Pattern BODY_PATTERN = Pattern.compile(
     68                 "(?:<\\s*body[^>]*>)(.*)(?:<\\s*/\\s*body\\s*>)",
     69                 Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
     70     /** Match group in {@code BODDY_PATTERN} for the body HTML */
     71     private static final int BODY_PATTERN_GROUP = 1;
     72     /** Pattern to find both dos and unix newlines */
     73     private static final Pattern NEWLINE_PATTERN =
     74         Pattern.compile("\\r?\\n");
     75     /** HTML string to use when replacing text newlines */
     76     private static final String NEWLINE_HTML = "<br>";
     77     /** Index of the plain text version of the message body */
     78     private final static int INDEX_BODY_TEXT = 0;
     79     /** Index of the HTML version of the message body */
     80     private final static int INDEX_BODY_HTML = 1;
     81     /** Single digit [0-9] to ensure uniqueness of the MIME boundary */
     82     /*package*/ static byte sBoundaryDigit;
     83 
     84     /**
     85      * Returns just the content between the <body></body> tags. This is not perfect and breaks
     86      * with malformed HTML or if there happens to be special characters in the attributes of
     87      * the <body> tag (e.g. a '>' in a java script block).
     88      */
     89     /*package*/ static String getHtmlBody(String html) {
     90         Matcher match = BODY_PATTERN.matcher(html);
     91         if (match.find()) {
     92             return match.group(BODY_PATTERN_GROUP);    // Found body; return
     93         } else {
     94             return html;              // Body not found; return the full HTML and hope for the best
     95         }
     96     }
     97 
     98     /**
     99      * Returns an HTML encoded message alternate
    100      */
    101     /*package*/ static String getHtmlAlternate(Body body, boolean useSmartReply) {
    102         if (body.mHtmlReply == null) {
    103             return null;
    104         }
    105         StringBuffer altMessage = new StringBuffer();
    106         String htmlContent = TextUtils.htmlEncode(body.mTextContent); // Escape HTML reserved chars
    107         htmlContent = NEWLINE_PATTERN.matcher(htmlContent).replaceAll(NEWLINE_HTML);
    108         altMessage.append(htmlContent);
    109         if (body.mIntroText != null) {
    110             String htmlIntro = TextUtils.htmlEncode(body.mIntroText);
    111             htmlIntro = NEWLINE_PATTERN.matcher(htmlIntro).replaceAll(NEWLINE_HTML);
    112             altMessage.append(htmlIntro);
    113         }
    114         if (!useSmartReply) {
    115             String htmlBody = getHtmlBody(body.mHtmlReply);
    116             altMessage.append(htmlBody);
    117         }
    118         return altMessage.toString();
    119     }
    120 
    121     /**
    122      * Gets both the plain text and HTML versions of the message body.
    123      */
    124     /*package*/ static String[] buildBodyText(Body body, int flags, boolean useSmartReply) {
    125         String[] messageBody = new String[] { null, null };
    126         if (body == null) {
    127             return messageBody;
    128         }
    129         String text = body.mTextContent;
    130         boolean isReply = (flags & Message.FLAG_TYPE_REPLY) != 0;
    131         boolean isForward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
    132         // For all forwards/replies, we add the intro text
    133         if (isReply || isForward) {
    134             String intro = body.mIntroText == null ? "" : body.mIntroText;
    135             text += intro;
    136         }
    137         if (useSmartReply) {
    138             // useSmartReply is set to true for use by SmartReply/SmartForward in EAS.
    139             // SmartForward doesn't put a break between the original and new text, so we add an LF
    140             if (isForward) {
    141                 text += "\n";
    142             }
    143         } else {
    144             String quotedText = body.mTextReply;
    145             // If there is no plain-text body, use de-tagified HTML as the text body
    146             if (quotedText == null && body.mHtmlReply != null) {
    147                 quotedText = Html.fromHtml(body.mHtmlReply).toString();
    148             }
    149             if (quotedText != null) {
    150                 // fix CR-LF line endings to LF-only needed by EditText.
    151                 Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText);
    152                 quotedText = matcher.replaceAll("\n");
    153             }
    154             if (isReply) {
    155                 if (quotedText != null) {
    156                     Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText);
    157                     text += matcher.replaceAll(">");
    158                 }
    159             } else if (isForward) {
    160                 if (quotedText != null) {
    161                     text += quotedText;
    162                 }
    163             }
    164         }
    165         messageBody[INDEX_BODY_TEXT] = text;
    166         // Exchange 2003 doesn't seem to support multipart w/SmartReply and SmartForward, so
    167         // we'll skip this.  Really, it would only matter if we could compose HTML replies
    168         if (!useSmartReply) {
    169             messageBody[INDEX_BODY_HTML] = getHtmlAlternate(body, useSmartReply);
    170         }
    171         return messageBody;
    172     }
    173 
    174     /**
    175      * Write the entire message to an output stream.  This method provides buffering, so it is
    176      * not necessary to pass in a buffered output stream here.
    177      *
    178      * @param context system context for accessing the provider
    179      * @param messageId the message to write out
    180      * @param out the output stream to write the message to
    181      * @param useSmartReply whether or not quoted text is appended to a reply/forward
    182      */
    183     public static void writeTo(Context context, long messageId, OutputStream out,
    184             boolean useSmartReply, boolean sendBcc) throws IOException, MessagingException {
    185         Message message = Message.restoreMessageWithId(context, messageId);
    186         if (message == null) {
    187             // throw something?
    188             return;
    189         }
    190 
    191         OutputStream stream = new BufferedOutputStream(out, 1024);
    192         Writer writer = new OutputStreamWriter(stream);
    193 
    194         // Write the fixed headers.  Ordering is arbitrary (the legacy code iterated through a
    195         // hashmap here).
    196 
    197         String date = DATE_FORMAT.format(new Date(message.mTimeStamp));
    198         writeHeader(writer, "Date", date);
    199 
    200         writeEncodedHeader(writer, "Subject", message.mSubject);
    201 
    202         writeHeader(writer, "Message-ID", message.mMessageId);
    203 
    204         writeAddressHeader(writer, "From", message.mFrom);
    205         writeAddressHeader(writer, "To", message.mTo);
    206         writeAddressHeader(writer, "Cc", message.mCc);
    207         // Address fields.  Note that we skip bcc unless the sendBcc argument is true
    208         // SMTP should NOT send bcc headers, but EAS must send it!
    209         if (sendBcc) {
    210             writeAddressHeader(writer, "Bcc", message.mBcc);
    211         }
    212         writeAddressHeader(writer, "Reply-To", message.mReplyTo);
    213         writeHeader(writer, "MIME-Version", "1.0");
    214 
    215         // Analyze message and determine if we have multiparts
    216         Body body = Body.restoreBodyWithMessageId(context, message.mId);
    217         String[] bodyText = buildBodyText(body, message.mFlags, useSmartReply);
    218 
    219         Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
    220         Cursor attachmentsCursor = context.getContentResolver().query(uri,
    221                 Attachment.CONTENT_PROJECTION, WHERE_NOT_SMART_FORWARD, null, null);
    222 
    223         try {
    224             int attachmentCount = attachmentsCursor.getCount();
    225             boolean multipart = attachmentCount > 0;
    226             String multipartBoundary = null;
    227             String multipartType = "mixed";
    228 
    229             // Simplified case for no multipart - just emit text and be done.
    230             if (!multipart) {
    231                 writeTextWithHeaders(writer, stream, bodyText);
    232             } else {
    233                 // continue with multipart headers, then into multipart body
    234                 multipartBoundary = getNextBoundary();
    235 
    236                 // Move to the first attachment; this must succeed because multipart is true
    237                 attachmentsCursor.moveToFirst();
    238                 if (attachmentCount == 1) {
    239                     // If we've got one attachment and it's an ics "attachment", we want to send
    240                     // this as multipart/alternative instead of multipart/mixed
    241                     int flags = attachmentsCursor.getInt(Attachment.CONTENT_FLAGS_COLUMN);
    242                     if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) {
    243                         multipartType = "alternative";
    244                     }
    245                 }
    246 
    247                 writeHeader(writer, "Content-Type",
    248                         "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\"");
    249                 // Finish headers and prepare for body section(s)
    250                 writer.write("\r\n");
    251 
    252                 // first multipart element is the body
    253                 if (bodyText[INDEX_BODY_TEXT] != null) {
    254                     writeBoundary(writer, multipartBoundary, false);
    255                     writeTextWithHeaders(writer, stream, bodyText);
    256                 }
    257 
    258                 // Write out the attachments until we run out
    259                 do {
    260                     writeBoundary(writer, multipartBoundary, false);
    261                     Attachment attachment =
    262                         Attachment.getContent(attachmentsCursor, Attachment.class);
    263                     writeOneAttachment(context, writer, stream, attachment);
    264                     writer.write("\r\n");
    265                 } while (attachmentsCursor.moveToNext());
    266 
    267                 // end of multipart section
    268                 writeBoundary(writer, multipartBoundary, true);
    269             }
    270         } finally {
    271             attachmentsCursor.close();
    272         }
    273 
    274         writer.flush();
    275         out.flush();
    276     }
    277 
    278     /**
    279      * Write a single attachment and its payload
    280      */
    281     private static void writeOneAttachment(Context context, Writer writer, OutputStream out,
    282             Attachment attachment) throws IOException, MessagingException {
    283         writeHeader(writer, "Content-Type",
    284                 attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\"");
    285         writeHeader(writer, "Content-Transfer-Encoding", "base64");
    286         // Most attachments (real files) will send Content-Disposition.  The suppression option
    287         // is used when sending calendar invites.
    288         if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) {
    289             writeHeader(writer, "Content-Disposition",
    290                     "attachment;"
    291                     + "\n filename=\"" + attachment.mFileName + "\";"
    292                     + "\n size=" + Long.toString(attachment.mSize));
    293         }
    294         if (attachment.mContentId != null) {
    295             writeHeader(writer, "Content-ID", attachment.mContentId);
    296         }
    297         writer.append("\r\n");
    298 
    299         // Set up input stream and write it out via base64
    300         InputStream inStream = null;
    301         try {
    302             // Use content, if provided; otherwise, use the contentUri
    303             if (attachment.mContentBytes != null) {
    304                 inStream = new ByteArrayInputStream(attachment.mContentBytes);
    305             } else {
    306                 // try to open the file
    307                 Uri fileUri = Uri.parse(attachment.mContentUri);
    308                 inStream = context.getContentResolver().openInputStream(fileUri);
    309             }
    310             // switch to output stream for base64 text output
    311             writer.flush();
    312             Base64OutputStream base64Out = new Base64OutputStream(
    313                 out, Base64.CRLF | Base64.NO_CLOSE);
    314             // copy base64 data and close up
    315             IOUtils.copy(inStream, base64Out);
    316             base64Out.close();
    317 
    318             // The old Base64OutputStream wrote an extra CRLF after
    319             // the output.  It's not required by the base-64 spec; not
    320             // sure if it's required by RFC 822 or not.
    321             out.write('\r');
    322             out.write('\n');
    323             out.flush();
    324         }
    325         catch (FileNotFoundException fnfe) {
    326             // Ignore this - empty file is OK
    327         }
    328         catch (IOException ioe) {
    329             throw new MessagingException("Invalid attachment.", ioe);
    330         }
    331     }
    332 
    333     /**
    334      * Write a single header with no wrapping or encoding
    335      *
    336      * @param writer the output writer
    337      * @param name the header name
    338      * @param value the header value
    339      */
    340     private static void writeHeader(Writer writer, String name, String value) throws IOException {
    341         if (value != null && value.length() > 0) {
    342             writer.append(name);
    343             writer.append(": ");
    344             writer.append(value);
    345             writer.append("\r\n");
    346         }
    347     }
    348 
    349     /**
    350      * Write a single header using appropriate folding & encoding
    351      *
    352      * @param writer the output writer
    353      * @param name the header name
    354      * @param value the header value
    355      */
    356     private static void writeEncodedHeader(Writer writer, String name, String value)
    357             throws IOException {
    358         if (value != null && value.length() > 0) {
    359             writer.append(name);
    360             writer.append(": ");
    361             writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2));
    362             writer.append("\r\n");
    363         }
    364     }
    365 
    366     /**
    367      * Unpack, encode, and fold address(es) into a header
    368      *
    369      * @param writer the output writer
    370      * @param name the header name
    371      * @param value the header value (a packed list of addresses)
    372      */
    373     private static void writeAddressHeader(Writer writer, String name, String value)
    374             throws IOException {
    375         if (value != null && value.length() > 0) {
    376             writer.append(name);
    377             writer.append(": ");
    378             writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2));
    379             writer.append("\r\n");
    380         }
    381     }
    382 
    383     /**
    384      * Write a multipart boundary
    385      *
    386      * @param writer the output writer
    387      * @param boundary the boundary string
    388      * @param end false if inner boundary, true if final boundary
    389      */
    390     private static void writeBoundary(Writer writer, String boundary, boolean end)
    391             throws IOException {
    392         writer.append("--");
    393         writer.append(boundary);
    394         if (end) {
    395             writer.append("--");
    396         }
    397         writer.append("\r\n");
    398     }
    399 
    400     /**
    401      * Write the body text. If only one version of the body is specified (either plain text
    402      * or HTML), the text is written directly. Otherwise, the plain text and HTML bodies
    403      * are both written with the appropriate headers.
    404      *
    405      * Note this always uses base64, even when not required.  Slightly less efficient for
    406      * US-ASCII text, but handles all formats even when non-ascii chars are involved.  A small
    407      * optimization might be to prescan the string for safety and send raw if possible.
    408      *
    409      * @param writer the output writer
    410      * @param out the output stream inside the writer (used for byte[] access)
    411      * @param bodyText Plain text and HTML versions of the original text of the message
    412      */
    413     private static void writeTextWithHeaders(Writer writer, OutputStream out, String[] bodyText)
    414             throws IOException {
    415         String text = bodyText[INDEX_BODY_TEXT];
    416         String html = bodyText[INDEX_BODY_HTML];
    417 
    418         if (text == null) {
    419             writer.write("\r\n");       // a truly empty message
    420         } else {
    421             String multipartBoundary = null;
    422             boolean multipart = html != null;
    423 
    424             // Simplified case for no multipart - just emit text and be done.
    425             if (multipart) {
    426                 // continue with multipart headers, then into multipart body
    427                 multipartBoundary = getNextBoundary();
    428 
    429                 writeHeader(writer, "Content-Type",
    430                         "multipart/alternative; boundary=\"" + multipartBoundary + "\"");
    431                 // Finish headers and prepare for body section(s)
    432                 writer.write("\r\n");
    433                 writeBoundary(writer, multipartBoundary, false);
    434             }
    435 
    436             // first multipart element is the body
    437             writeHeader(writer, "Content-Type", "text/plain; charset=utf-8");
    438             writeHeader(writer, "Content-Transfer-Encoding", "base64");
    439             writer.write("\r\n");
    440             byte[] textBytes = text.getBytes("UTF-8");
    441             writer.flush();
    442             out.write(Base64.encode(textBytes, Base64.CRLF));
    443 
    444             if (multipart) {
    445                 // next multipart section
    446                 writeBoundary(writer, multipartBoundary, false);
    447 
    448                 writeHeader(writer, "Content-Type", "text/html; charset=utf-8");
    449                 writeHeader(writer, "Content-Transfer-Encoding", "base64");
    450                 writer.write("\r\n");
    451                 byte[] htmlBytes = html.getBytes("UTF-8");
    452                 writer.flush();
    453                 out.write(Base64.encode(htmlBytes, Base64.CRLF));
    454 
    455                 // end of multipart section
    456                 writeBoundary(writer, multipartBoundary, true);
    457             }
    458         }
    459     }
    460 
    461     /**
    462      * Returns a unique boundary string.
    463      */
    464     /*package*/ static String getNextBoundary() {
    465         StringBuilder boundary = new StringBuilder();
    466         boundary.append("--_com.android.email_").append(System.nanoTime());
    467         synchronized (Rfc822Output.class) {
    468             boundary = boundary.append(sBoundaryDigit);
    469             sBoundaryDigit = (byte)((sBoundaryDigit + 1) % 10);
    470         }
    471         return boundary.toString();
    472     }
    473 }
    474