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 1731d9acbf0623872f9d4a2b3210b5970854b654c7Marc Blankpackage com.android.emailcommon.internet; 18a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 19bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrookimport android.content.ContentUris; 20f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.content.Context; 21bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrookimport android.database.Cursor; 22f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.net.Uri; 23bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrookimport android.text.Html; 24bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrookimport android.text.TextUtils; 25f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.util.Base64; 26f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.util.Base64OutputStream; 27f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank 282193962ca2b3157e79f731736afa2a0c972e778aMarc Blankimport com.android.emailcommon.mail.Address; 292193962ca2b3157e79f731736afa2a0c972e778aMarc Blankimport com.android.emailcommon.mail.MessagingException; 30a7bc0319a75184ad706bb35c049af107ac3688e6Marc Blankimport com.android.emailcommon.provider.EmailContent.Attachment; 31a7bc0319a75184ad706bb35c049af107ac3688e6Marc Blankimport com.android.emailcommon.provider.EmailContent.Body; 32a7bc0319a75184ad706bb35c049af107ac3688e6Marc Blankimport com.android.emailcommon.provider.EmailContent.Message; 33a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 34c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport org.apache.commons.io.IOUtils; 35c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler 36a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.BufferedOutputStream; 373aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadlerimport java.io.ByteArrayInputStream; 38c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport java.io.FileNotFoundException; 39a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.IOException; 40c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadlerimport java.io.InputStream; 41a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.OutputStream; 42a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.OutputStreamWriter; 43a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.io.Writer; 44a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.text.SimpleDateFormat; 45a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.util.Date; 46a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerimport java.util.Locale; 47f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blankimport java.util.regex.Matcher; 48f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blankimport java.util.regex.Pattern; 49a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 50a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler/** 51a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Utility class to output RFC 822 messages from provider email messages 52a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */ 53a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadlerpublic class Rfc822Output { 54a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 55bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook private static final Pattern PATTERN_START_OF_LINE = Pattern.compile("(?m)^"); 56bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook private static final Pattern PATTERN_ENDLINE_CRLF = Pattern.compile("\r\n"); 57bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 58a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to 59a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler // "Jan", not the other localized format like "Ene" (meaning January in locale es). 60b3f7dd0169a35221184b9327c8ce337b09dc6d1fMakoto Onuki private static final SimpleDateFormat DATE_FORMAT = 61a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); 62a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 63bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook private static final String WHERE_NOT_SMART_FORWARD = "(" + Attachment.FLAGS + "&" + 64bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Attachment.FLAG_SMART_FORWARD + ")=0"; 65bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 669cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /** A less-than-perfect pattern to pull out <body> content */ 679cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy private static final Pattern BODY_PATTERN = Pattern.compile( 689cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy "(?:<\\s*body[^>]*>)(.*)(?:<\\s*/\\s*body\\s*>)", 699cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy Pattern.CASE_INSENSITIVE | Pattern.DOTALL); 709cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /** Match group in {@code BODDY_PATTERN} for the body HTML */ 719cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy private static final int BODY_PATTERN_GROUP = 1; 72bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook /** Pattern to find both dos and unix newlines */ 73bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook private static final Pattern NEWLINE_PATTERN = 74bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Pattern.compile("\\r?\\n"); 75bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook /** HTML string to use when replacing text newlines */ 76bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook private static final String NEWLINE_HTML = "<br>"; 779cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /** Index of the plain text version of the message body */ 789cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy private final static int INDEX_BODY_TEXT = 0; 799cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /** Index of the HTML version of the message body */ 809cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy private final static int INDEX_BODY_HTML = 1; 819cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /** Single digit [0-9] to ensure uniqueness of the MIME boundary */ 829cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /*package*/ static byte sBoundaryDigit; 839cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy 849cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /** 859cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy * Returns just the content between the <body></body> tags. This is not perfect and breaks 869cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy * with malformed HTML or if there happens to be special characters in the attributes of 879cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy * the <body> tag (e.g. a '>' in a java script block). 889cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy */ 899cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /*package*/ static String getHtmlBody(String html) { 909cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy Matcher match = BODY_PATTERN.matcher(html); 919cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy if (match.find()) { 929cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy return match.group(BODY_PATTERN_GROUP); // Found body; return 939cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } else { 949cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy return html; // Body not found; return the full HTML and hope for the best 959cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } 969cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } 979cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy 989cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /** 99bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook * Returns an HTML encoded message alternate 100bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook */ 101bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook /*package*/ static String getHtmlAlternate(Body body, boolean useSmartReply) { 102bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (body.mHtmlReply == null) { 103bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook return null; 104bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 105bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook StringBuffer altMessage = new StringBuffer(); 106bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String htmlContent = TextUtils.htmlEncode(body.mTextContent); // Escape HTML reserved chars 107bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook htmlContent = NEWLINE_PATTERN.matcher(htmlContent).replaceAll(NEWLINE_HTML); 108bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook altMessage.append(htmlContent); 109bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (body.mIntroText != null) { 110bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String htmlIntro = TextUtils.htmlEncode(body.mIntroText); 111bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook htmlIntro = NEWLINE_PATTERN.matcher(htmlIntro).replaceAll(NEWLINE_HTML); 112bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook altMessage.append(htmlIntro); 113bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 114bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (!useSmartReply) { 115bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String htmlBody = getHtmlBody(body.mHtmlReply); 116bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook altMessage.append(htmlBody); 117bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 118bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook return altMessage.toString(); 119bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 120bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 121bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook /** 1229cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy * Gets both the plain text and HTML versions of the message body. 1239cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy */ 1249cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /*package*/ static String[] buildBodyText(Body body, int flags, boolean useSmartReply) { 125bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String[] messageBody = new String[] { null, null }; 1269cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy if (body == null) { 127bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook return messageBody; 128bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 129bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String text = body.mTextContent; 130bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook boolean isReply = (flags & Message.FLAG_TYPE_REPLY) != 0; 131bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook boolean isForward = (flags & Message.FLAG_TYPE_FORWARD) != 0; 132bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // For all forwards/replies, we add the intro text 133bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (isReply || isForward) { 134bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String intro = body.mIntroText == null ? "" : body.mIntroText; 135bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook text += intro; 13680ee607a7cc784993a48741d0dfbd132cd546985Marc Blank } 137bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (useSmartReply) { 138bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // useSmartReply is set to true for use by SmartReply/SmartForward in EAS. 139bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // SmartForward doesn't put a break between the original and new text, so we add an LF 140bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (isForward) { 141bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook text += "\n"; 142bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 143bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } else { 144bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String quotedText = body.mTextReply; 145bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // If there is no plain-text body, use de-tagified HTML as the text body 146bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (quotedText == null && body.mHtmlReply != null) { 147bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook quotedText = Html.fromHtml(body.mHtmlReply).toString(); 148bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 149bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (quotedText != null) { 150bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // fix CR-LF line endings to LF-only needed by EditText. 151bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText); 152bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook quotedText = matcher.replaceAll("\n"); 1539cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } 154bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (isReply) { 155bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (quotedText != null) { 156bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText); 157bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook text += matcher.replaceAll(">"); 158bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 159bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } else if (isForward) { 160bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (quotedText != null) { 161bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook text += quotedText; 162bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 163bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 164bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 165bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook messageBody[INDEX_BODY_TEXT] = text; 166bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // Exchange 2003 doesn't seem to support multipart w/SmartReply and SmartForward, so 167bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // we'll skip this. Really, it would only matter if we could compose HTML replies 168bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (!useSmartReply) { 169bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook messageBody[INDEX_BODY_HTML] = getHtmlAlternate(body, useSmartReply); 17009a071a87938e0c1ed80962d54b6025618dca120Marc Blank } 1719cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy return messageBody; 172f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank } 173f2dded3a2fba83dd3f0d14cce6abe467a4ab66ebMarc Blank 174a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler /** 175a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Write the entire message to an output stream. This method provides buffering, so it is 176a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * not necessary to pass in a buffered output stream here. 177a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * 178a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param context system context for accessing the provider 179a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param messageId the message to write out 180a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param out the output stream to write the message to 1817c87bfc33aaf94e50e4be9ff0b838e4b3a3812f4Todd Kennedy * @param useSmartReply whether or not quoted text is appended to a reply/forward 182a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */ 1831d98989222f2d023ddb08a70d5abb850029f95dcMarc Blank public static void writeTo(Context context, long messageId, OutputStream out, 1847c87bfc33aaf94e50e4be9ff0b838e4b3a3812f4Todd Kennedy boolean useSmartReply, boolean sendBcc) throws IOException, MessagingException { 185a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler Message message = Message.restoreMessageWithId(context, messageId); 186a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler if (message == null) { 187a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler // throw something? 188a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler return; 189a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 190a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 191a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler OutputStream stream = new BufferedOutputStream(out, 1024); 192a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler Writer writer = new OutputStreamWriter(stream); 193a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 194a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler // Write the fixed headers. Ordering is arbitrary (the legacy code iterated through a 195a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler // hashmap here). 196a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 197b3f7dd0169a35221184b9327c8ce337b09dc6d1fMakoto Onuki String date = DATE_FORMAT.format(new Date(message.mTimeStamp)); 198a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writeHeader(writer, "Date", date); 199a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 200a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writeEncodedHeader(writer, "Subject", message.mSubject); 201a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 202a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writeHeader(writer, "Message-ID", message.mMessageId); 203a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 204a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writeAddressHeader(writer, "From", message.mFrom); 205a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writeAddressHeader(writer, "To", message.mTo); 206a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writeAddressHeader(writer, "Cc", message.mCc); 2072d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank // Address fields. Note that we skip bcc unless the sendBcc argument is true 2082d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank // SMTP should NOT send bcc headers, but EAS must send it! 2092d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank if (sendBcc) { 2102d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank writeAddressHeader(writer, "Bcc", message.mBcc); 2112d5691cac1874eb3491353ab608a84c2a75e2b62Marc Blank } 212a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writeAddressHeader(writer, "Reply-To", message.mReplyTo); 2136bcccf628413d40696980d0d86c7ab2b4f831952Andrew Stadler writeHeader(writer, "MIME-Version", "1.0"); 214a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 215a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler // Analyze message and determine if we have multiparts 2169cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy Body body = Body.restoreBodyWithMessageId(context, message.mId); 2179cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy String[] bodyText = buildBodyText(body, message.mFlags, useSmartReply); 218c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler 219bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId); 220bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Cursor attachmentsCursor = context.getContentResolver().query(uri, 221bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Attachment.CONTENT_PROJECTION, WHERE_NOT_SMART_FORWARD, null, null); 222c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler 223bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook try { 224bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook int attachmentCount = attachmentsCursor.getCount(); 225bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook boolean multipart = attachmentCount > 0; 226bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String multipartBoundary = null; 227bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String multipartType = "mixed"; 228a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 229bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // Simplified case for no multipart - just emit text and be done. 230bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (!multipart) { 231bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeTextWithHeaders(writer, stream, bodyText); 232bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } else { 233bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // continue with multipart headers, then into multipart body 234bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook multipartBoundary = getNextBoundary(); 235bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 236bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // Move to the first attachment; this must succeed because multipart is true 237bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook attachmentsCursor.moveToFirst(); 238bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (attachmentCount == 1) { 239bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // If we've got one attachment and it's an ics "attachment", we want to send 240bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // this as multipart/alternative instead of multipart/mixed 241bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook int flags = attachmentsCursor.getInt(Attachment.CONTENT_FLAGS_COLUMN); 242bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if ((flags & Attachment.FLAG_ICS_ALTERNATIVE_PART) != 0) { 243bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook multipartType = "alternative"; 244bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 2450ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank } 246a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 247bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeHeader(writer, "Content-Type", 248bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook "multipart/" + multipartType + "; boundary=\"" + multipartBoundary + "\""); 249bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // Finish headers and prepare for body section(s) 250bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writer.write("\r\n"); 251a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 252bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // first multipart element is the body 253bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (bodyText[INDEX_BODY_TEXT] != null) { 254bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeBoundary(writer, multipartBoundary, false); 255bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeTextWithHeaders(writer, stream, bodyText); 256bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 257a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 258bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // Write out the attachments until we run out 259bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook do { 260bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeBoundary(writer, multipartBoundary, false); 261bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Attachment attachment = 262bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Attachment.getContent(attachmentsCursor, Attachment.class); 263bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeOneAttachment(context, writer, stream, attachment); 264bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writer.write("\r\n"); 265bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } while (attachmentsCursor.moveToNext()); 266bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 267bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // end of multipart section 268bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeBoundary(writer, multipartBoundary, true); 269c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler } 270bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } finally { 271bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook attachmentsCursor.close(); 272a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 273a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 274a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.flush(); 275a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler out.flush(); 276a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 277a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 278a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler /** 279c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler * Write a single attachment and its payload 280c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler */ 281c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler private static void writeOneAttachment(Context context, Writer writer, OutputStream out, 282c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler Attachment attachment) throws IOException, MessagingException { 283c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler writeHeader(writer, "Content-Type", 284c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler attachment.mMimeType + ";\n name=\"" + attachment.mFileName + "\""); 285c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler writeHeader(writer, "Content-Transfer-Encoding", "base64"); 2863aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler // Most attachments (real files) will send Content-Disposition. The suppression option 2873aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler // is used when sending calendar invites. 2880ed690bfb4fdfa86cb6f75c19e24b8ff39a1756cMarc Blank if ((attachment.mFlags & Attachment.FLAG_ICS_ALTERNATIVE_PART) == 0) { 2893aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler writeHeader(writer, "Content-Disposition", 2903aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler "attachment;" 2913aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler + "\n filename=\"" + attachment.mFileName + "\";" 2923aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler + "\n size=" + Long.toString(attachment.mSize)); 2933aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler } 2949cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy if (attachment.mContentId != null) { 2959cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy writeHeader(writer, "Content-ID", attachment.mContentId); 2969cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } 297c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler writer.append("\r\n"); 298c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler 299c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler // Set up input stream and write it out via base64 300c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler InputStream inStream = null; 301c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler try { 3023aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler // Use content, if provided; otherwise, use the contentUri 30320225d57609d6a5e482c088fdad60c29212d31a0Makoto Onuki if (attachment.mContentBytes != null) { 30420225d57609d6a5e482c088fdad60c29212d31a0Makoto Onuki inStream = new ByteArrayInputStream(attachment.mContentBytes); 3053aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler } else { 3063aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler // try to open the file 307bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook Uri fileUri = Uri.parse(attachment.mContentUri); 3083aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler inStream = context.getContentResolver().openInputStream(fileUri); 3093aaba9eb87db34ea0861d70d5c08f84d7ca97ab0Andrew Stadler } 310c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler // switch to output stream for base64 text output 311c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler writer.flush(); 312ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker Base64OutputStream base64Out = new Base64OutputStream( 313ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker out, Base64.CRLF | Base64.NO_CLOSE); 314c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler // copy base64 data and close up 315c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler IOUtils.copy(inStream, base64Out); 316c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler base64Out.close(); 317ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker 318ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker // The old Base64OutputStream wrote an extra CRLF after 319ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker // the output. It's not required by the base-64 spec; not 320ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker // sure if it's required by RFC 822 or not. 321ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker out.write('\r'); 322ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker out.write('\n'); 323ba714999f24697161f1ecd68199c3b330ea64ab9Doug Zongker out.flush(); 324c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler } 325c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler catch (FileNotFoundException fnfe) { 326c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler // Ignore this - empty file is OK 327c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler } 328c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler catch (IOException ioe) { 329c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler throw new MessagingException("Invalid attachment.", ioe); 330c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler } 331c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler } 332c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler 333c640cbbaf385566e1b6de361b2b23156e10f695dAndrew Stadler /** 334a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Write a single header with no wrapping or encoding 335a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * 336a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param writer the output writer 337a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param name the header name 338a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param value the header value 339a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */ 340a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler private static void writeHeader(Writer writer, String name, String value) throws IOException { 341a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler if (value != null && value.length() > 0) { 342a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(name); 343a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(": "); 344a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(value); 345a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append("\r\n"); 346a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 347a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 348a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 349a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler /** 350a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Write a single header using appropriate folding & encoding 351a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * 352a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param writer the output writer 353a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param name the header name 354a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param value the header value 355a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */ 356a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler private static void writeEncodedHeader(Writer writer, String name, String value) 357a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler throws IOException { 358a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler if (value != null && value.length() > 0) { 359a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(name); 360a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(": "); 361a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(MimeUtility.foldAndEncode2(value, name.length() + 2)); 362a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append("\r\n"); 363a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 364a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 365a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 366a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler /** 367a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Unpack, encode, and fold address(es) into a header 368a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * 369a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param writer the output writer 370a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param name the header name 371a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param value the header value (a packed list of addresses) 372a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */ 373a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler private static void writeAddressHeader(Writer writer, String name, String value) 374a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler throws IOException { 375a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler if (value != null && value.length() > 0) { 376a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(name); 377a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(": "); 378a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(MimeUtility.fold(Address.packedToHeader(value), name.length() + 2)); 379a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append("\r\n"); 380a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 381a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 382a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 383a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler /** 384a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Write a multipart boundary 385a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * 386a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param writer the output writer 387a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param boundary the boundary string 388a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param end false if inner boundary, true if final boundary 389a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */ 390a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler private static void writeBoundary(Writer writer, String boundary, boolean end) 391a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler throws IOException { 392a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append("--"); 393a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append(boundary); 394a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler if (end) { 395a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append("--"); 396a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 397a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler writer.append("\r\n"); 398a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 399a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler 400a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler /** 401bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook * Write the body text. If only one version of the body is specified (either plain text 402bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook * or HTML), the text is written directly. Otherwise, the plain text and HTML bodies 403bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook * are both written with the appropriate headers. 404a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * 405a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * Note this always uses base64, even when not required. Slightly less efficient for 406a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * US-ASCII text, but handles all formats even when non-ascii chars are involved. A small 407a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * optimization might be to prescan the string for safety and send raw if possible. 408a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * 409a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param writer the output writer 410a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler * @param out the output stream inside the writer (used for byte[] access) 4119cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy * @param bodyText Plain text and HTML versions of the original text of the message 412a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler */ 4139cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy private static void writeTextWithHeaders(Writer writer, OutputStream out, String[] bodyText) 414a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler throws IOException { 4159cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy String text = bodyText[INDEX_BODY_TEXT]; 416bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String html = bodyText[INDEX_BODY_HTML]; 417bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 4189cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy if (text == null) { 4199cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy writer.write("\r\n"); // a truly empty message 4209cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } else { 421bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook String multipartBoundary = null; 422bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook boolean multipart = html != null; 423bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 424bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // Simplified case for no multipart - just emit text and be done. 425bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (multipart) { 426bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // continue with multipart headers, then into multipart body 427bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook multipartBoundary = getNextBoundary(); 428bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 429bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeHeader(writer, "Content-Type", 430bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook "multipart/alternative; boundary=\"" + multipartBoundary + "\""); 431bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // Finish headers and prepare for body section(s) 432bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writer.write("\r\n"); 433bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeBoundary(writer, multipartBoundary, false); 434bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 435bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 4369cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy // first multipart element is the body 437bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeHeader(writer, "Content-Type", "text/plain; charset=utf-8"); 4389cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy writeHeader(writer, "Content-Transfer-Encoding", "base64"); 4399cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy writer.write("\r\n"); 4409cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy byte[] textBytes = text.getBytes("UTF-8"); 4419cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy writer.flush(); 4429cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy out.write(Base64.encode(textBytes, Base64.CRLF)); 443bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 444bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook if (multipart) { 445bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // next multipart section 446bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeBoundary(writer, multipartBoundary, false); 447bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 448bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeHeader(writer, "Content-Type", "text/html; charset=utf-8"); 449bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeHeader(writer, "Content-Transfer-Encoding", "base64"); 450bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writer.write("\r\n"); 451bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook byte[] htmlBytes = html.getBytes("UTF-8"); 452bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writer.flush(); 453bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook out.write(Base64.encode(htmlBytes, Base64.CRLF)); 454bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook 455bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook // end of multipart section 456bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook writeBoundary(writer, multipartBoundary, true); 457bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook } 4589cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } 4599cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } 4609cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy 4619cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /** 4629cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy * Returns a unique boundary string. 4639cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy */ 4649cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy /*package*/ static String getNextBoundary() { 4659cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy StringBuilder boundary = new StringBuilder(); 4669cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy boundary.append("--_com.android.email_").append(System.nanoTime()); 4679cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy synchronized (Rfc822Output.class) { 4689cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy boundary = boundary.append(sBoundaryDigit); 4699cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy sBoundaryDigit = (byte)((sBoundaryDigit + 1) % 10); 4709cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy } 4719cc51b72c6902b95f65857af64eb38063aa4a42bTodd Kennedy return boundary.toString(); 472a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler } 473a9a0e57436bce15e186bef216564e996ec20ae82Andrew Stadler} 474