From a9a0e57436bce15e186bef216564e996ec20ae82 Mon Sep 17 00:00:00 2001 From: Andrew Stadler Date: Tue, 11 Aug 2009 14:27:36 -0700 Subject: [PATCH] An RFC822 streamer for sending provider messages. * Message headers with proper encoding * Should handle all non-US encodings properly * Sends body text only (no HTML planned) This is implemented but not fully tested - I'm submitting now so Marc can evaluate & test in EAS environment. TODO: Unit tests TODO: Attachments --- src/com/android/email/mail/Address.java | 9 + .../email/mail/transport/Rfc822Output.java | 208 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 src/com/android/email/mail/transport/Rfc822Output.java diff --git a/src/com/android/email/mail/Address.java b/src/com/android/email/mail/Address.java index b411e65a0..32e5ef5be 100644 --- a/src/com/android/email/mail/Address.java +++ b/src/com/android/email/mail/Address.java @@ -327,6 +327,15 @@ public class Address { return array.length > 0 ? array[0] : null; } + /** + * Convert a packed list of addresses to a form suitable for use in an RFC822 header. + * This implementation is brute-force, and could be replaced with a more efficient version + * if desired. + */ + public static String packedToHeader(String packedList) { + return toHeader(unpack(packedList)); + } + /** * Unpacks an address list previously packed with pack() * @param addressList String with packed addresses as returned by pack() diff --git a/src/com/android/email/mail/transport/Rfc822Output.java b/src/com/android/email/mail/transport/Rfc822Output.java new file mode 100644 index 000000000..60099b23c --- /dev/null +++ b/src/com/android/email/mail/transport/Rfc822Output.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.email.mail.transport; + +import com.android.email.codec.binary.Base64; +import com.android.email.mail.Address; +import com.android.email.mail.internet.MimeUtility; +import com.android.email.provider.EmailContent.Body; +import com.android.email.provider.EmailContent.Message; + +import android.content.Context; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Utility class to output RFC 822 messages from provider email messages + */ +public class Rfc822Output { + + // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to + // "Jan", not the other localized format like "Ene" (meaning January in locale es). + static final SimpleDateFormat mDateFormat = + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); + + /** + * Write the entire message to an output stream. This method provides buffering, so it is + * not necessary to pass in a buffered output stream here. + * + * @param context system context for accessing the provider + * @param messageId the message to write out + * @param out the output stream to write the message to + * + * TODO is there anything in the flags fields we need to look at? + * TODO alternative parts (e.g. text+html) are not supported here. + */ + public static void writeTo(Context context, long messageId, OutputStream out) + throws IOException { + Message message = Message.restoreMessageWithId(context, messageId); + if (message == null) { + // throw something? + return; + } + + OutputStream stream = new BufferedOutputStream(out, 1024); + Writer writer = new OutputStreamWriter(stream); + + // Write the fixed headers. Ordering is arbitrary (the legacy code iterated through a + // hashmap here). + + String date = mDateFormat.format(new Date(message.mTimeStamp)); + writeHeader(writer, "Date", date); + + writeEncodedHeader(writer, "Subject", message.mSubject); + + writeHeader(writer, "Message-ID", message.mMessageId); + + // Address fields. Note, obviously, we skip bcc here + writeAddressHeader(writer, "From", message.mFrom); + writeAddressHeader(writer, "To", message.mTo); + writeAddressHeader(writer, "Cc", message.mCc); + writeAddressHeader(writer, "Reply-To", message.mReplyTo); + + // Analyze message and determine if we have multiparts + // TODO count attachments + boolean mixedParts = false; + String mixedBoundary = null; + + // Simplified case for no multipart - just emit text and be done. + if (!mixedParts) { + String text = Body.restoreBodyTextWithMessageId(context, messageId); + writeTextWithHeaders(writer, stream, text); + } else { + // continue with multipart headers, then into multipart body + writeHeader(writer, "MIME-Version", "1.0"); + + mixedBoundary = "--_com.android.email_" + System.nanoTime(); + writeHeader(writer, "Content-Type", + "multipart/mixed; boundary=\"" + mixedBoundary + "\""); + + // Finish headers and prepare for body section(s) + writer.write("\r\n"); + + // first multipart element is the body + String text = Body.restoreBodyTextWithMessageId(context, messageId); + writeTextWithHeaders(writer, stream, text); + + // TODO: attachments here + + // end of multipart section + writeBoundary(writer, mixedBoundary, true); + } + + writer.flush(); + out.flush(); + } + + /** + * Write a single header with no wrapping or encoding + * + * @param writer the output writer + * @param name the header name + * @param value the header value + */ + private static void writeHeader(Writer writer, String name, String value) throws IOException { + if (value != null && value.length() > 0) { + writer.append(name); + writer.append(": "); + writer.append(value); + writer.append("\r\n"); + } + } + + /** + * Write a single header using appropriate folding & encoding + * + * @param writer the output writer + * @param name the header name + * @param value the header value + */ + private static void writeEncodedHeader(Writer writer, String name, String value) + throws IOException { + if (value != null && value.length() > 0) { + writer.append(name); + writer.append(": "); + writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2)); + writer.append("\r\n"); + } + } + + /** + * Unpack, encode, and fold address(es) into a header + * + * @param writer the output writer + * @param name the header name + * @param value the header value (a packed list of addresses) + */ + private static void writeAddressHeader(Writer writer, String name, String value) + throws IOException { + if (value != null && value.length() > 0) { + writer.append(name); + writer.append(": "); + writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2)); + writer.append("\r\n"); + } + } + + /** + * Write a multipart boundary + * + * @param writer the output writer + * @param boundary the boundary string + * @param end false if inner boundary, true if final boundary + */ + private static void writeBoundary(Writer writer, String boundary, boolean end) + throws IOException { + writer.append("--"); + writer.append(boundary); + if (end) { + writer.append("--"); + } + writer.append("\r\n"); + } + + /** + * Write text (either as main body or inside a multipart), preceded by appropriate headers. + * + * Note this always uses base64, even when not required. Slightly less efficient for + * US-ASCII text, but handles all formats even when non-ascii chars are involved. A small + * optimization might be to prescan the string for safety and send raw if possible. + * + * @param writer the output writer + * @param out the output stream inside the writer (used for byte[] access) + * @param text The original text of the message + */ + private static void writeTextWithHeaders(Writer writer, OutputStream out, String text) + throws IOException { + writeHeader(writer, "Content-Type", "text/plain; charset=utf-8"); + writeHeader(writer, "Content-Transfer-Encoding", "base64"); + writer.write("\r\n"); + + byte[] bytes = text.getBytes("UTF-8"); + writer.flush(); + out.write(Base64.encodeBase64Chunked(bytes)); + writer.write(text); + writer.write("\r\n"); + } +}