Rfc822Output.java revision 80ee607a7cc784993a48741d0dfbd132cd546985
1a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler/*
2a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Copyright (C) 2009 The Android Open Source Project
3a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler *
4a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Licensed under the Apache License, Version 2.0 (the "License");
5a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * you may not use this file except in compliance with the License.
6a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * You may obtain a copy of the License at
7a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler *
8a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler *      http://www.apache.org/licenses/LICENSE-2.0
9a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler *
10a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Unless required by applicable law or agreed to in writing, software
11a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * distributed under the License is distributed on an "AS IS" BASIS,
12a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * See the License for the specific language governing permissions and
14a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * limitations under the License.
15a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */
16a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
17a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerpackage com.android.email.mail.transport;
18a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
19a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport com.android.email.mail.Address;
20c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport com.android.email.mail.MessagingException;
21a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport com.android.email.mail.internet.MimeUtility;
22c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport com.android.email.provider.EmailContent.Attachment;
23a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport com.android.email.provider.EmailContent.Body;
24a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport com.android.email.provider.EmailContent.Message;
25a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
26c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport org.apache.commons.io.IOUtils;
27c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler
28c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport android.content.ContentUris;
29a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport android.content.Context;
30c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport android.database.Cursor;
31c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport android.net.Uri;
326cec1104fe8863fce2ee86ff5145076e6c436a00Doug Zongkerimport android.util.Base64;
336cec1104fe8863fce2ee86ff5145076e6c436a00Doug Zongkerimport android.util.Base64OutputStream;
34a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
35a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.BufferedOutputStream;
363aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadlerimport java.io.ByteArrayInputStream;
37c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport java.io.FileNotFoundException;
38a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.IOException;
39c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport java.io.InputStream;
40a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.OutputStream;
41a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.OutputStreamWriter;
42a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.Writer;
43a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.text.SimpleDateFormat;
44a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.util.Date;
45a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.util.Locale;
46f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blankimport java.util.regex.Matcher;
47f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blankimport java.util.regex.Pattern;
48a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
49a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler/**
50a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Utility class to output RFC 822 messages from provider email messages
51a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */
52a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerpublic class Rfc822Output {
53a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
54f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank    private static final Pattern PATTERN_START_OF_LINE = Pattern.compile("(?m)^");
55f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank    private static final Pattern PATTERN_ENDLINE_CRLF = Pattern.compile("\r\n");
56f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank
57a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
58a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    // "Jan", not the other localized format like "Ene" (meaning January in locale es).
59b3f7dd0169a35221184b9327c8ce337b09dc6d1fMakoto Onuki    private static final SimpleDateFormat DATE_FORMAT =
60a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
61a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
621d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank    /*package*/ static String buildBodyText(Context context, Message message,
631d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank            boolean appendQuotedText) {
64f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank        Body body = Body.restoreBodyWithMessageId(context, message.mId);
65ffc681a4da85c82c80d819ab8afb4442577c52a0Marc Blank        if (body == null) {
66ffc681a4da85c82c80d819ab8afb4442577c52a0Marc Blank            return null;
67ffc681a4da85c82c80d819ab8afb4442577c52a0Marc Blank        }
682f99314326de0ee3bab9fbf18d511b24c0574ee1Marc Blank
69f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank        String text = body.mTextContent;
702f99314326de0ee3bab9fbf18d511b24c0574ee1Marc Blank        int flags = message.mFlags;
712b4b5e2f26b2fc9e0875a2a1974d8010794f3642Marc Blank        boolean isReply = (flags & Message.FLAG_TYPE_REPLY) != 0;
722b4b5e2f26b2fc9e0875a2a1974d8010794f3642Marc Blank        boolean isForward = (flags & Message.FLAG_TYPE_FORWARD) != 0;
7380ee607a7cc784993a48741d0dfbd132cd546985Marc Blank        // For all forwards/replies, we add the intro text
7480ee607a7cc784993a48741d0dfbd132cd546985Marc Blank        if (isReply || isForward) {
7580ee607a7cc784993a48741d0dfbd132cd546985Marc Blank            String intro = body.mIntroText == null ? "" : body.mIntroText;
7680ee607a7cc784993a48741d0dfbd132cd546985Marc Blank            text += intro;
7780ee607a7cc784993a48741d0dfbd132cd546985Marc Blank        }
781d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank        if (!appendQuotedText) {
792f99314326de0ee3bab9fbf18d511b24c0574ee1Marc Blank            // appendQuotedText is set to false for use by SmartReply/SmartForward in EAS.
8080ee607a7cc784993a48741d0dfbd132cd546985Marc Blank            // SmartForward doesn't put a break between the original and new text, so we add an LF
8180ee607a7cc784993a48741d0dfbd132cd546985Marc Blank            if (isForward) {
8280ee607a7cc784993a48741d0dfbd132cd546985Marc Blank                text += "\n";
832f99314326de0ee3bab9fbf18d511b24c0574ee1Marc Blank            }
841d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank            return text;
851d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank        }
861d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank
87f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank        String quotedText = body.mTextReply;
88f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank        if (quotedText != null) {
89f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank            // fix CR-LF line endings to LF-only needed by EditText.
90f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank            Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText);
91f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank            quotedText = matcher.replaceAll("\n");
92f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank        }
932b4b5e2f26b2fc9e0875a2a1974d8010794f3642Marc Blank        if (isReply) {
94f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank            if (quotedText != null) {
95f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank                Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText);
96f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank                text += matcher.replaceAll(">");
97f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank            }
982b4b5e2f26b2fc9e0875a2a1974d8010794f3642Marc Blank        } else if (isForward) {
99f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank            if (quotedText != null) {
100f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank                text += quotedText;
101f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank            }
102f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank        }
103f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank        return text;
104f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank    }
105f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank
106a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    /**
107a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * Write the entire message to an output stream.  This method provides buffering, so it is
108a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * not necessary to pass in a buffered output stream here.
109a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     *
110a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param context system context for accessing the provider
111a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param messageId the message to write out
112a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param out the output stream to write the message to
1131d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank     * @param appendQuotedText whether or not to append quoted text if this is a reply/forward
114a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     *
115a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * TODO alternative parts (e.g. text+html) are not supported here.
116a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     */
1171d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank    public static void writeTo(Context context, long messageId, OutputStream out,
1182d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank            boolean appendQuotedText, boolean sendBcc) throws IOException, MessagingException {
119a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        Message message = Message.restoreMessageWithId(context, messageId);
120a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        if (message == null) {
121a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            // throw something?
122a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            return;
123a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        }
124a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
125a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        OutputStream stream = new BufferedOutputStream(out, 1024);
126a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        Writer writer = new OutputStreamWriter(stream);
127a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
128a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        // Write the fixed headers.  Ordering is arbitrary (the legacy code iterated through a
129a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        // hashmap here).
130a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
131b3f7dd0169a35221184b9327c8ce337b09dc6d1fMakoto Onuki        String date = DATE_FORMAT.format(new Date(message.mTimeStamp));
132a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeHeader(writer, "Date", date);
133a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
134a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeEncodedHeader(writer, "Subject", message.mSubject);
135a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
136a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeHeader(writer, "Message-ID", message.mMessageId);
137a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
138a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeAddressHeader(writer, "From", message.mFrom);
139a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeAddressHeader(writer, "To", message.mTo);
140a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeAddressHeader(writer, "Cc", message.mCc);
1412d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank        // Address fields.  Note that we skip bcc unless the sendBcc argument is true
1422d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank        // SMTP should NOT send bcc headers, but EAS must send it!
1432d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank        if (sendBcc) {
1442d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank            writeAddressHeader(writer, "Bcc", message.mBcc);
1452d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank        }
146a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeAddressHeader(writer, "Reply-To", message.mReplyTo);
1476bcccf628413d40696980d0d86c7ab2b4f831952Andrew Stadler        writeHeader(writer, "MIME-Version", "1.0");
148a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
149a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        // Analyze message and determine if we have multiparts
1501d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank        String text = buildBodyText(context, message, appendQuotedText);
151c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler
152c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId);
153c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        Cursor attachmentsCursor = context.getContentResolver().query(uri,
154c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                Attachment.CONTENT_PROJECTION, null, null, null);
155c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler
156c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        try {
1570ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank            int attachmentCount = attachmentsCursor.getCount();
1580ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank            boolean multipart = attachmentCount > 0;
1590ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank            String multipartBoundary = null;
1600ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank            String multipartType = "mixed";
161a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
162c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            // Simplified case for no multipart - just emit text and be done.
1630ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank            if (!multipart) {
164c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                if (text != null) {
165c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                    writeTextWithHeaders(writer, stream, text);
166c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                } else {
167c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                    writer.write("\r\n");       // a truly empty message
168c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                }
169c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            } else {
170c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                // continue with multipart headers, then into multipart body
1710ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                multipartBoundary = "--_com.android.email_" + System.nanoTime();
1720ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank
1730ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                // Move to the first attachment; this must succeed because multipart is true
1740ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                attachmentsCursor.moveToFirst();
1750ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                if (attachmentCount == 1) {
1760ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                    // If we've got one attachment and it's an ics "attachment", we want to send
1770ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                    // this as multipart/alternative instead of multipart/mixed
1780ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                    int flags = attachmentsCursor.getInt(Attachment.CONTENT_FLAGS_COLUMN);
1790ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                    if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) {
1800ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                        multipartType = "alternative";
1810ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                    }
1820ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                }
183a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
184c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                writeHeader(writer, "Content-Type",
1850ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                        "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\"");
186c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                // Finish headers and prepare for body section(s)
187c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                writer.write("\r\n");
188a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
189c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                // first multipart element is the body
190c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                if (text != null) {
1910ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                    writeBoundary(writer, multipartBoundary, false);
192c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                    writeTextWithHeaders(writer, stream, text);
193c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                }
194a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
1950ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                // Write out the attachments until we run out
1960ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                do {
1970ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                    writeBoundary(writer, multipartBoundary, false);
198c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                    Attachment attachment =
199c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                        Attachment.getContent(attachmentsCursor, Attachment.class);
200c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                    writeOneAttachment(context, writer, stream, attachment);
201c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                    writer.write("\r\n");
2020ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                } while (attachmentsCursor.moveToNext());
203a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
204c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                // end of multipart section
2050ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank                writeBoundary(writer, multipartBoundary, true);
206c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            }
207c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        } finally {
208c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            attachmentsCursor.close();
209a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        }
210a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
211a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writer.flush();
212a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        out.flush();
213a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    }
214a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
215a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    /**
216c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler     * Write a single attachment and its payload
217c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler     */
218c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler    private static void writeOneAttachment(Context context, Writer writer, OutputStream out,
219c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            Attachment attachment) throws IOException, MessagingException {
220c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        writeHeader(writer, "Content-Type",
221c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler                attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\"");
222c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        writeHeader(writer, "Content-Transfer-Encoding", "base64");
2233aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler        // Most attachments (real files) will send Content-Disposition.  The suppression option
2243aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler        // is used when sending calendar invites.
2250ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank        if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) {
2263aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler            writeHeader(writer, "Content-Disposition",
2273aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler                    "attachment;"
2283aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler                    + "\n filename=\"" + attachment.mFileName + "\";"
2293aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler                    + "\n size=" + Long.toString(attachment.mSize));
2303aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler        }
231c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        writeHeader(writer, "Content-ID", attachment.mContentId);
232c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        writer.append("\r\n");
233c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler
234c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        // Set up input stream and write it out via base64
235c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        InputStream inStream = null;
236c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        try {
2373aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler            // Use content, if provided; otherwise, use the contentUri
23820225d57609d6a5e482c088fdad60c29212d31a0Makoto Onuki            if (attachment.mContentBytes != null) {
23920225d57609d6a5e482c088fdad60c29212d31a0Makoto Onuki                inStream = new ByteArrayInputStream(attachment.mContentBytes);
2403aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler            } else {
2413aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler                // try to open the file
2423aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler                Uri fileUri = Uri.parse(attachment.mContentUri);
2433aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler                inStream = context.getContentResolver().openInputStream(fileUri);
2443aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler            }
245c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            // switch to output stream for base64 text output
246c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            writer.flush();
247ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker            Base64OutputStream base64Out = new Base64OutputStream(
248ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker                out, Base64.CRLF | Base64.NO_CLOSE);
249c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            // copy base64 data and close up
250c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            IOUtils.copy(inStream, base64Out);
251c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            base64Out.close();
252ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker
253ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker            // The old Base64OutputStream wrote an extra CRLF after
254ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker            // the output.  It's not required by the base-64 spec; not
255ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker            // sure if it's required by RFC 822 or not.
256ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker            out.write('\r');
257ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker            out.write('\n');
258ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker            out.flush();
259c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        }
260c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        catch (FileNotFoundException fnfe) {
261c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            // Ignore this - empty file is OK
262c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        }
263c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        catch (IOException ioe) {
264c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler            throw new MessagingException("Invalid attachment.", ioe);
265c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler        }
266c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler    }
267c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler
268c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler    /**
269a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * Write a single header with no wrapping or encoding
270a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     *
271a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param writer the output writer
272a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param name the header name
273a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param value the header value
274a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     */
275a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    private static void writeHeader(Writer writer, String name, String value) throws IOException {
276a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        if (value != null && value.length() > 0) {
277a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(name);
278a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(": ");
279a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(value);
280a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append("\r\n");
281a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        }
282a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    }
283a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
284a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    /**
285a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * Write a single header using appropriate folding & encoding
286a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     *
287a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param writer the output writer
288a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param name the header name
289a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param value the header value
290a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     */
291a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    private static void writeEncodedHeader(Writer writer, String name, String value)
292a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            throws IOException {
293a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        if (value != null && value.length() > 0) {
294a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(name);
295a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(": ");
296a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2));
297a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append("\r\n");
298a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        }
299a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    }
300a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
301a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    /**
302a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * Unpack, encode, and fold address(es) into a header
303a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     *
304a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param writer the output writer
305a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param name the header name
306a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param value the header value (a packed list of addresses)
307a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     */
308a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    private static void writeAddressHeader(Writer writer, String name, String value)
309a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            throws IOException {
310a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        if (value != null && value.length() > 0) {
311a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(name);
312a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(": ");
313a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2));
314a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append("\r\n");
315a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        }
316a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    }
317a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
318a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    /**
319a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * Write a multipart boundary
320a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     *
321a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param writer the output writer
322a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param boundary the boundary string
323a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param end false if inner boundary, true if final boundary
324a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     */
325a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    private static void writeBoundary(Writer writer, String boundary, boolean end)
326a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            throws IOException {
327a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writer.append("--");
328a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writer.append(boundary);
329a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        if (end) {
330a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            writer.append("--");
331a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        }
332a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writer.append("\r\n");
333a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    }
334a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler
335a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    /**
336a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * Write text (either as main body or inside a multipart), preceded by appropriate headers.
337a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     *
338a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * Note this always uses base64, even when not required.  Slightly less efficient for
339a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * US-ASCII text, but handles all formats even when non-ascii chars are involved.  A small
340a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * optimization might be to prescan the string for safety and send raw if possible.
341a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     *
342a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param writer the output writer
343a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param out the output stream inside the writer (used for byte[] access)
344a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     * @param text The original text of the message
345a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler     */
346a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    private static void writeTextWithHeaders(Writer writer, OutputStream out, String text)
347a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler            throws IOException {
348a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeHeader(writer, "Content-Type", "text/plain; charset=utf-8");
349a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writeHeader(writer, "Content-Transfer-Encoding", "base64");
350a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writer.write("\r\n");
351a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        byte[] bytes = text.getBytes("UTF-8");
352a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler        writer.flush();
353f44a40cda14a49d5b02636a3a8a8b2eb8c23fc00Doug Zongker        out.write(Base64.encode(bytes, Base64.CRLF));
354a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler    }
355a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler}
356