• 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.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 
buildBodyText(Context context, Message message, boolean appendQuotedText)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      */
writeTo(Context context, long messageId, OutputStream out, boolean appendQuotedText, boolean sendBcc)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      */
writeOneAttachment(Context context, Writer writer, OutputStream out, Attachment attachment)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      */
writeHeader(Writer writer, String name, String value)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      */
writeEncodedHeader(Writer writer, String name, String value)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      */
writeAddressHeader(Writer writer, String name, String value)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      */
writeBoundary(Writer writer, String boundary, boolean end)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      */
writeTextWithHeaders(Writer writer, OutputStream out, String text)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