• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
getHtmlBody(String html)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      */
getHtmlAlternate(Body body, boolean useSmartReply)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      */
buildBodyText(Body body, int flags, boolean useSmartReply)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      */
writeTo(Context context, long messageId, OutputStream out, boolean useSmartReply, boolean sendBcc)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      */
writeOneAttachment(Context context, Writer writer, OutputStream out, Attachment attachment)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      */
writeHeader(Writer writer, String name, String value)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      */
writeEncodedHeader(Writer writer, String name, String value)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      */
writeAddressHeader(Writer writer, String name, String value)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      */
writeBoundary(Writer writer, String boundary, boolean end)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      */
writeTextWithHeaders(Writer writer, OutputStream out, String[] bodyText)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      */
getNextBoundary()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