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