From 9cc51b72c6902b95f65857af64eb38063aa4a42b Mon Sep 17 00:00:00 2001 From: Todd Kennedy Date: Tue, 1 Mar 2011 14:20:19 -0800 Subject: [PATCH] Attach original HTML message on forward/reply When replying or fowarding an HTML message, we now send both plain text and HTML bodies as a multi-part mime message. We take special care to ensure the message bodies are in their own multi-part block and do not interfere with any additional attachments to the message. bug 3060920 Change-Id: I2fc3cb4e1f65bcc28486a62731b44b0ee0a99719 --- .../emailcommon/internet/Rfc822Output.java | 194 +++++++++++---- proguard.flags | 16 +- .../email/activity/MessageCompose.java | 31 +-- .../internet/Rfc822OutputTests.java | 235 +++++++++++++++--- 4 files changed, 374 insertions(+), 102 deletions(-) diff --git a/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java b/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java index 7382d30b2..00de12681 100644 --- a/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java +++ b/emailcommon/src/com/android/emailcommon/internet/Rfc822Output.java @@ -28,6 +28,8 @@ import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; +import android.text.Html; +import android.text.TextUtils; import android.util.Base64; import android.util.Base64OutputStream; @@ -61,15 +63,68 @@ public class Rfc822Output { private static final String WHERE_NOT_SMART_FORWARD = "(" + Attachment.FLAGS + "&" + Attachment.FLAG_SMART_FORWARD + ")=0"; - /*package*/ static String buildBodyText(Context context, Message message, - boolean useSmartReply) { - Body body = Body.restoreBodyWithMessageId(context, message.mId); - if (body == null) { + /** A less-than-perfect pattern to pull out content */ + private static final Pattern BODY_PATTERN = Pattern.compile( + "(?:<\\s*body[^>]*>)(.*)(?:<\\s*/\\s*body\\s*>)", + Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + /** Match group in {@code BODDY_PATTERN} for the body HTML */ + private static final int BODY_PATTERN_GROUP = 1; + /** Pattern to find both dos and unix newlines */ + private static final Pattern NEWLINE_PATTERN = + Pattern.compile("\\r?\\n"); + /** HTML string to use when replacing text newlines */ + private static final String NEWLINE_HTML = "
"; + /** Index of the plain text version of the message body */ + private final static int INDEX_BODY_TEXT = 0; + /** Index of the HTML version of the message body */ + private final static int INDEX_BODY_HTML = 1; + /** Single digit [0-9] to ensure uniqueness of the MIME boundary */ + /*package*/ static byte sBoundaryDigit; + + /** + * Returns just the content between the tags. This is not perfect and breaks + * with malformed HTML or if there happens to be special characters in the attributes of + * the tag (e.g. a '>' in a java script block). + */ + /*package*/ static String getHtmlBody(String html) { + Matcher match = BODY_PATTERN.matcher(html); + if (match.find()) { + return match.group(BODY_PATTERN_GROUP); // Found body; return + } else { + return html; // Body not found; return the full HTML and hope for the best + } + } + + /** + * Returns an HTML encoded message alternate + */ + /*package*/ static String getHtmlAlternate(Body body) { + if (body.mHtmlReply == null) { return null; } + StringBuffer altMessage = new StringBuffer(); + String htmlContent = TextUtils.htmlEncode(body.mTextContent); // Escape HTML reserved chars + htmlContent = NEWLINE_PATTERN.matcher(htmlContent).replaceAll(NEWLINE_HTML); + altMessage.append(htmlContent); + if (body.mIntroText != null) { + String htmlIntro = TextUtils.htmlEncode(body.mIntroText); + htmlIntro = NEWLINE_PATTERN.matcher(htmlIntro).replaceAll(NEWLINE_HTML); + altMessage.append(htmlIntro); + } + String htmlBody = getHtmlBody(body.mHtmlReply); + altMessage.append(htmlBody); + return altMessage.toString(); + } + /** + * Gets both the plain text and HTML versions of the message body. + */ + /*package*/ static String[] buildBodyText(Body body, int flags, boolean useSmartReply) { + String[] messageBody = new String[] { null, null }; + if (body == null) { + return messageBody; + } String text = body.mTextContent; - int flags = message.mFlags; boolean isReply = (flags & Message.FLAG_TYPE_REPLY) != 0; boolean isForward = (flags & Message.FLAG_TYPE_FORWARD) != 0; // For all forwards/replies, we add the intro text @@ -83,26 +138,31 @@ public class Rfc822Output { if (isForward) { text += "\n"; } - return text; - } - - String quotedText = body.mTextReply; - if (quotedText != null) { - // fix CR-LF line endings to LF-only needed by EditText. - Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText); - quotedText = matcher.replaceAll("\n"); - } - if (isReply) { - if (quotedText != null) { - Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText); - text += matcher.replaceAll(">"); + } else { + String quotedText = body.mTextReply; + // If there is no plain-text body, use de-tagified HTML as the text body + if (quotedText == null && body.mHtmlReply != null) { + quotedText = Html.fromHtml(body.mHtmlReply).toString(); } - } else if (isForward) { if (quotedText != null) { - text += quotedText; + // fix CR-LF line endings to LF-only needed by EditText. + Matcher matcher = PATTERN_ENDLINE_CRLF.matcher(quotedText); + quotedText = matcher.replaceAll("\n"); + } + if (isReply) { + if (quotedText != null) { + Matcher matcher = PATTERN_START_OF_LINE.matcher(quotedText); + text += matcher.replaceAll(">"); + } + } else if (isForward) { + if (quotedText != null) { + text += quotedText; + } } } - return text; + messageBody[INDEX_BODY_TEXT] = text; + messageBody[INDEX_BODY_HTML] = getHtmlAlternate(body); + return messageBody; } /** @@ -113,8 +173,6 @@ public class Rfc822Output { * @param messageId the message to write out * @param out the output stream to write the message to * @param useSmartReply whether or not quoted text is appended to a reply/forward - * - * TODO alternative parts (e.g. text+html) are not supported here. */ public static void writeTo(Context context, long messageId, OutputStream out, boolean useSmartReply, boolean sendBcc) throws IOException, MessagingException { @@ -149,7 +207,8 @@ public class Rfc822Output { writeHeader(writer, "MIME-Version", "1.0"); // Analyze message and determine if we have multiparts - String text = buildBodyText(context, message, useSmartReply); + Body body = Body.restoreBodyWithMessageId(context, message.mId); + String[] bodyText = buildBodyText(body, message.mFlags, useSmartReply); Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, messageId); Cursor attachmentsCursor = context.getContentResolver().query(uri, @@ -163,14 +222,10 @@ public class Rfc822Output { // Simplified case for no multipart - just emit text and be done. if (!multipart) { - if (text != null) { - writeTextWithHeaders(writer, stream, text); - } else { - writer.write("\r\n"); // a truly empty message - } + writeTextWithHeaders(writer, stream, bodyText); } else { // continue with multipart headers, then into multipart body - multipartBoundary = "--_com.android.email_" + System.nanoTime(); + multipartBoundary = getNextBoundary(); // Move to the first attachment; this must succeed because multipart is true attachmentsCursor.moveToFirst(); @@ -189,9 +244,9 @@ public class Rfc822Output { writer.write("\r\n"); // first multipart element is the body - if (text != null) { + if (bodyText[INDEX_BODY_TEXT] != null) { writeBoundary(writer, multipartBoundary, false); - writeTextWithHeaders(writer, stream, text); + writeTextWithHeaders(writer, stream, bodyText); } // Write out the attachments until we run out @@ -230,7 +285,9 @@ public class Rfc822Output { + "\n filename=\"" + attachment.mFileName + "\";" + "\n size=" + Long.toString(attachment.mSize)); } - writeHeader(writer, "Content-ID", attachment.mContentId); + if (attachment.mContentId != null) { + writeHeader(writer, "Content-ID", attachment.mContentId); + } writer.append("\r\n"); // Set up input stream and write it out via base64 @@ -335,7 +392,9 @@ public class Rfc822Output { } /** - * Write text (either as main body or inside a multipart), preceded by appropriate headers. + * Write the body text. If only one version of the body is specified (either plain text + * or HTML), the text is written directly. Otherwise, the plain text and HTML bodies + * are both written with the 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 @@ -343,15 +402,66 @@ public class Rfc822Output { * * @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 + * @param bodyText Plain text and HTML versions of the original text of the message */ - private static void writeTextWithHeaders(Writer writer, OutputStream out, String text) + private static void writeTextWithHeaders(Writer writer, OutputStream out, String[] bodyText) 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.encode(bytes, Base64.CRLF)); + String text = bodyText[INDEX_BODY_TEXT]; + String html = bodyText[INDEX_BODY_HTML]; + + if (text == null) { + writer.write("\r\n"); // a truly empty message + } else { + String multipartBoundary = null; + boolean multipart = html != null; + + // Simplified case for no multipart - just emit text and be done. + if (multipart) { + // continue with multipart headers, then into multipart body + multipartBoundary = getNextBoundary(); + + writeHeader(writer, "Content-Type", + "multipart/alternative; boundary=\"" + multipartBoundary + "\""); + // Finish headers and prepare for body section(s) + writer.write("\r\n"); + writeBoundary(writer, multipartBoundary, false); + } + + // first multipart element is the body + writeHeader(writer, "Content-Type", "text/plain; charset=utf-8"); + writeHeader(writer, "Content-Transfer-Encoding", "base64"); + writer.write("\r\n"); + byte[] textBytes = text.getBytes("UTF-8"); + writer.flush(); + out.write(Base64.encode(textBytes, Base64.CRLF)); + + if (multipart) { + // next multipart section + writeBoundary(writer, multipartBoundary, false); + + writeHeader(writer, "Content-Type", "text/html; charset=utf-8"); + writeHeader(writer, "Content-Transfer-Encoding", "base64"); + writer.write("\r\n"); + byte[] htmlBytes = html.getBytes("UTF-8"); + writer.flush(); + out.write(Base64.encode(htmlBytes, Base64.CRLF)); + + // end of multipart section + writeBoundary(writer, multipartBoundary, true); + } + } + } + + /** + * Returns a unique boundary string. + */ + /*package*/ static String getNextBoundary() { + StringBuilder boundary = new StringBuilder(); + boundary.append("--_com.android.email_").append(System.nanoTime()); + synchronized (Rfc822Output.class) { + boundary = boundary.append(sBoundaryDigit); + sBoundaryDigit = (byte)((sBoundaryDigit + 1) % 10); + } + return boundary.toString(); } } diff --git a/proguard.flags b/proguard.flags index f6355d7c0..d1ebcb8f8 100644 --- a/proguard.flags +++ b/proguard.flags @@ -46,6 +46,11 @@ *** setProviderContext(android.content.Context); } +-keepclasseswithmembers class com.android.emailcommon.internet.Rfc822Output { + *** getHtmlBody(java.lang.String); + *** buildBodyText(android.content.Context, com.android.emailcommon.provider.EmailContent$Message, boolean); +} + -keepclasseswithmembers class com.android.emailcommon.mail.Address { (java.lang.String); (java.lang.String,java.lang.String); @@ -60,7 +65,7 @@ -keep class com.android.emailcommon.service.PolicySet { (com.android.emailcommon.provider.EmailContent$Account); (int, int, int, int, boolean, int, int, boolean); - *** writeAccount(...); + *** writeAccount(com.android.emailcommon.provider.EmailContent$Account, java.lang.String, boolean, android.content.Context); } -keepclasseswithmembers class com.android.email.MessagingController { @@ -69,7 +74,7 @@ -keepclasseswithmembers class com.android.emailcommon.utility.Utility { *** dumpCursor(android.database.Cursor); - *** fromUtf8(...); + *** fromUtf8(byte[]); *** isFirstUtf8Byte(byte); *** replaceBareLfWithCrlf(java.lang.String); } @@ -185,7 +190,7 @@ } -keep class org.apache.james.mime4j.field.Field { - *** getBody(...); + *** getBody(); } # The following classes are used only by unit tests. @@ -196,5 +201,8 @@ } -keepclasseswithmembers class org.apache.commons.io.IOUtils { - *** toByteArray(...); + *** toByteArray(java.io.InputStream); + *** toByteArray(java.io.Reader); + *** toByteArray(java.io.Reader, java.lang.String); + *** toByteArray(java.lang.String); } diff --git a/src/com/android/email/activity/MessageCompose.java b/src/com/android/email/activity/MessageCompose.java index 2ca634c80..57eb3a8a1 100644 --- a/src/com/android/email/activity/MessageCompose.java +++ b/src/com/android/email/activity/MessageCompose.java @@ -141,7 +141,6 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus */ private boolean mSourceMessageProcessed = false; - private ActionBar mActionBar; private TextView mFromView; private MultiAutoCompleteTextView mToView; private MultiAutoCompleteTextView mCcView; @@ -472,7 +471,6 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus } private void initViews() { - mActionBar = getActionBar(); mFromView = (TextView)findViewById(R.id.from); mToView = (MultiAutoCompleteTextView)findViewById(R.id.to); mCcView = (MultiAutoCompleteTextView)findViewById(R.id.cc); @@ -481,7 +479,7 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus mSubjectView = (EditText)findViewById(R.id.subject); mMessageContentView = (EditText)findViewById(R.id.message_content); mAttachments = (LinearLayout)findViewById(R.id.attachments); - mAttachmentContainer = (LinearLayout)findViewById(R.id.attachment_container); + mAttachmentContainer = findViewById(R.id.attachment_container); mQuotedTextBar = findViewById(R.id.quoted_text_bar); mIncludeQuotedTextCheckBox = (CheckBox) findViewById(R.id.include_quoted_text); mQuotedText = (WebView)findViewById(R.id.quoted_text); @@ -814,6 +812,8 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus } /** + * Updates the given message using values from the compose UI. + * * @param message The message to be updated. * @param account the account (used to obtain From: address). * @param hasAttachments true if it has one or more attachment. @@ -840,14 +840,11 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus // Use the Intent to set flags saying this message is a reply or a forward and save the // unique id of the source message if (mSource != null && mQuotedTextBar.getVisibility() == View.VISIBLE) { - if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction) - || ACTION_FORWARD.equals(mAction)) { - message.mSourceKey = mSource.mId; - // Get the body of the source message here - message.mHtmlReply = mSource.mHtml; - message.mTextReply = mSource.mText; - } - + // If the quote bar is visible; this must either be a reply or forward + message.mSourceKey = mSource.mId; + // Get the body of the source message here + message.mHtmlReply = mSource.mHtml; + message.mTextReply = mSource.mText; String fromAsString = Address.unpackToString(mSource.mFrom); if (ACTION_FORWARD.equals(mAction)) { message.mFlags |= Message.FLAG_TYPE_FORWARD; @@ -949,7 +946,6 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus // For any unloaded attachment, set the flag saying we need it loaded boolean hasUnloadedAttachments = false; for (Attachment attachment : attachments) { - if (attachment.mContentUri == null && ((attachment.mFlags & Attachment.FLAG_SMART_FORWARD) != 0)) { attachment.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD; @@ -1481,13 +1477,12 @@ public class MessageCompose extends Activity implements OnClickListener, OnFocus return URLDecoder.decode(s, "UTF-8"); } - // used by processSourceMessage() + /** + * Displays quoted text from the original email + */ private void displayQuotedText(String textBody, String htmlBody) { - /* Use plain-text body if available, otherwise use HTML body. - * This matches the desired behavior for IMAP/POP where we only send plain-text, - * and for EAS which sends HTML and has no plain-text body. - */ - boolean plainTextFlag = textBody != null; + // Only use plain text if there is no HTML body + boolean plainTextFlag = TextUtils.isEmpty(htmlBody); String text = plainTextFlag ? textBody : htmlBody; if (text != null) { text = plainTextFlag ? EmailHtmlUtil.escapeCharacterToDisplay(text) : text; diff --git a/tests/src/com/android/emailcommon/internet/Rfc822OutputTests.java b/tests/src/com/android/emailcommon/internet/Rfc822OutputTests.java index 1d9253f95..51684d513 100644 --- a/tests/src/com/android/emailcommon/internet/Rfc822OutputTests.java +++ b/tests/src/com/android/emailcommon/internet/Rfc822OutputTests.java @@ -22,6 +22,7 @@ import com.android.emailcommon.internet.Rfc822Output; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.EmailContent.Attachment; +import com.android.emailcommon.provider.EmailContent.Body; import com.android.emailcommon.provider.EmailContent.Message; import org.apache.james.mime4j.field.Field; @@ -51,12 +52,33 @@ public class Rfc822OutputTests extends ProviderTestCase2 { private static final String RECIPIENT_TO = "recipient-to@android.com"; private static final String RECIPIENT_CC = "recipient-cc@android.com"; private static final String SUBJECT = "This is the subject"; - private static final String BODY = "This is the body. This is also the body."; + private static final String REPLY_TEXT_BODY = "This is the body. This is also the body."; + /** HTML reply body */ + private static final String BODY_HTML_REPLY = + "This is the body.
This is also the body."; + /** Text-only version of the HTML reply body */ + private static final String BODY_TEXT_REPLY_HTML = + ">This is the body.\n>This is also the body."; private static final String TEXT = "Here is some new text."; + // Full HTML document + private static String HTML_FULL_BODY = "MyTitle" + + "" + + "test1"; + private static String HTML_FULL_RESULT = "test1"; + // element w/ content + private static String HTML_BODY_BODY = + "test2"; + private static String HTML_BODY_RESULT = "test2"; + // No tag; just content + private static String HTML_NO_BODY_BODY = + "test3"; + private static String HTML_NO_BODY_RESULT = "test3"; + + private static String REPLY_INTRO_TEXT = "\n\n" + SENDER + " wrote:\n\n"; + private static String REPLY_INTRO_HTML = "

" + SENDER + " wrote:

"; private Context mMockContext; private String mForwardIntro; - private String mReplyIntro; public Rfc822OutputTests () { super(EmailProvider.class, EmailContent.AUTHORITY); @@ -68,7 +90,6 @@ public class Rfc822OutputTests extends ProviderTestCase2 { mMockContext = getMockContext(); mForwardIntro = mMockContext.getString(R.string.message_compose_fwd_header_fmt, SUBJECT, SENDER, RECIPIENT_TO, RECIPIENT_CC); - mReplyIntro = mMockContext.getString(R.string.message_compose_reply_header_fmt, SENDER); } // TODO Create more tests here. Specifically, we should test to make sure that forward works @@ -77,6 +98,25 @@ public class Rfc822OutputTests extends ProviderTestCase2 { // TODO Write test that ensures that bcc is handled properly (i.e. sent/not send depending // on the flag passed to writeTo + private Message createTestMessage(String text, boolean save) { + Message message = new Message(); + message.mText = text; + message.mFrom = SENDER; + message.mFlags = Message.FLAG_TYPE_REPLY; + message.mTextReply = REPLY_TEXT_BODY; + message.mHtmlReply = BODY_HTML_REPLY; + message.mIntroText = REPLY_INTRO_TEXT; + if (save) { + message.save(mMockContext); + } + return message; + } + + private Body createTestBody(Message message) { + Body body = Body.restoreBodyWithMessageId(mMockContext, message.mId); + return body; + } + /** * Test for buildBodyText(). * Compare with expected values. @@ -84,56 +124,72 @@ public class Rfc822OutputTests extends ProviderTestCase2 { */ public void testBuildBodyText() { // Test sending a message *without* using smart reply - Message message1 = new Message(); - message1.mText = ""; - message1.mFrom = SENDER; - message1.mFlags = Message.FLAG_TYPE_REPLY; - message1.mTextReply = BODY; - message1.mIntroText = mReplyIntro; - message1.save(mMockContext); + Message message1 = createTestMessage("", true); + Body body1 = createTestBody(message1); + String[] bodyParts; - String body1 = Rfc822Output.buildBodyText(mMockContext, message1, false); - assertEquals(mReplyIntro + ">" + BODY, body1); + bodyParts = Rfc822Output.buildBodyText(body1, message1.mFlags, false); + assertEquals(REPLY_INTRO_TEXT + ">" + REPLY_TEXT_BODY, bodyParts[0]); - message1.mId = -1; + message1.mId = -1; // Changing the message; need to reset the id message1.mText = TEXT; message1.save(mMockContext); + body1 = createTestBody(message1); - body1 = Rfc822Output.buildBodyText(mMockContext, message1, false); - assertEquals(TEXT + mReplyIntro + ">" + BODY, body1); + bodyParts = Rfc822Output.buildBodyText(body1, message1.mFlags, false); + assertEquals(TEXT + REPLY_INTRO_TEXT + ">" + REPLY_TEXT_BODY, bodyParts[0]); - // Save a different message with no reply body (so we reset the id) - message1.mId = -1; + // We have an HTML reply and no text reply; use the HTML reply + message1.mId = -1; // Changing the message; need to reset the id message1.mTextReply = null; message1.save(mMockContext); - body1 = Rfc822Output.buildBodyText(mMockContext, message1, false); - assertEquals(TEXT + mReplyIntro, body1); + body1 = createTestBody(message1); + + bodyParts = Rfc822Output.buildBodyText(body1, message1.mFlags, false); + assertEquals(TEXT + REPLY_INTRO_TEXT + BODY_TEXT_REPLY_HTML, bodyParts[0]); + + // We have no HTML or text reply; use nothing + message1.mId = -1; // Changing the message; need to reset the id + message1.mHtmlReply = null; + message1.save(mMockContext); + body1 = createTestBody(message1); + + bodyParts = Rfc822Output.buildBodyText(body1, message1.mFlags, false); + assertEquals(TEXT + REPLY_INTRO_TEXT, bodyParts[0]); // Test sending a message *with* using smart reply - Message message2 = new Message(); - message2.mText = ""; - message2.mFrom = SENDER; - message2.mFlags = Message.FLAG_TYPE_REPLY; - message2.mTextReply = BODY; - message2.mIntroText = mReplyIntro; - message2.save(mMockContext); + Message message2 = createTestMessage("", true); + Body body2 = createTestBody(message2); - String body2 = Rfc822Output.buildBodyText(mMockContext, message2, true); - assertEquals(mReplyIntro, body2); + bodyParts = Rfc822Output.buildBodyText(body2, message2.mFlags, true); + assertEquals(REPLY_INTRO_TEXT, bodyParts[0]); - message2.mId = -1; + message2.mId = -1; // Changing the message; need to reset the id message2.mText = TEXT; message2.save(mMockContext); + body2 = createTestBody(message2); - body2 = Rfc822Output.buildBodyText(mMockContext, message2, true); - assertEquals(TEXT + mReplyIntro, body2); + bodyParts = Rfc822Output.buildBodyText(body2, message2.mFlags, true); + assertEquals(TEXT + REPLY_INTRO_TEXT, bodyParts[0]); - // Save a different message with no reply body (so we reset the id) - message2.mId = -1; + // We have an HTML reply and no text reply; use nothing (smart reply) + message2.mId = -1; // Changing the message; need to reset the id message2.mTextReply = null; message2.save(mMockContext); - body2 = Rfc822Output.buildBodyText(mMockContext, message2, true); - assertEquals(TEXT + mReplyIntro, body2); + body2 = createTestBody(message2); + + bodyParts = Rfc822Output.buildBodyText(body2, message2.mFlags, true); + assertEquals(TEXT + REPLY_INTRO_TEXT, bodyParts[0]); + + // We have no HTML or text reply; use nothing + message2.mId = -1; // Changing the message; need to reset the id + message2.mTextReply = null; + message2.mHtmlReply = null; + message2.save(mMockContext); + body2 = createTestBody(message2); + + bodyParts = Rfc822Output.buildBodyText(body2, message2.mFlags, true); + assertEquals(TEXT + REPLY_INTRO_TEXT, bodyParts[0]); } /** @@ -148,11 +204,12 @@ public class Rfc822OutputTests extends ProviderTestCase2 { msg.mCc = RECIPIENT_CC; msg.mSubject = SUBJECT; msg.mFlags = Message.FLAG_TYPE_FORWARD; - msg.mTextReply = BODY; + msg.mTextReply = REPLY_TEXT_BODY; msg.mIntroText = mForwardIntro; msg.save(mMockContext); - String body = Rfc822Output.buildBodyText(mMockContext, msg, false); - assertEquals(TEXT + mForwardIntro + BODY, body); + Body body = createTestBody(msg); + String[] bodyParts = Rfc822Output.buildBodyText(body, msg.mFlags, false); + assertEquals(TEXT + mForwardIntro + REPLY_TEXT_BODY, bodyParts[0]); } public void testWriteToText() throws IOException, MessagingException { @@ -269,6 +326,108 @@ public class Rfc822OutputTests extends ProviderTestCase2 { assertNotNull(header.getField("content-disposition")); } + /** + * Tests various types of HTML reply text -- with full tags, + * with just the tags and without any surrounding tags. + */ + public void testGetHtmlBody() { + String actual; + actual = Rfc822Output.getHtmlBody(HTML_FULL_BODY); + assertEquals(HTML_FULL_RESULT, actual); + actual = Rfc822Output.getHtmlBody(HTML_BODY_BODY); + assertEquals(HTML_BODY_RESULT, actual); + actual = Rfc822Output.getHtmlBody(HTML_NO_BODY_BODY); + assertEquals(HTML_NO_BODY_RESULT, actual); + } + + /** + * Tests that the entire HTML alternate string is valid for text entered by + * the user. We don't test all permutations of forwarded HTML here because + * that is verified by testGetHtmlBody(). + */ + public void testGetHtmlAlternate() { + Message message = createTestMessage(TEXT, true); + Body body = createTestBody(message); + String html; + + html = Rfc822Output.getHtmlAlternate(body); + assertEquals(TEXT + REPLY_INTRO_HTML + BODY_HTML_REPLY, html); + + // HTML special characters; dependent upon TextUtils#htmlEncode() + message.mId = -1; // Changing the message; need to reset the id + message.mText = "<>&'\""; + message.save(mMockContext); + body = createTestBody(message); + + html = Rfc822Output.getHtmlAlternate(body); + assertEquals("<>&'"" + REPLY_INTRO_HTML + BODY_HTML_REPLY, html); + + // Newlines in user text + message.mId = -1; // Changing the message; need to reset the id + message.mText = "dos\r\nunix\nthree\r\n\n\n"; + message.save(mMockContext); + body = createTestBody(message); + + html = Rfc822Output.getHtmlAlternate(body); + assertEquals("dos
unix
three


" + REPLY_INTRO_HTML + BODY_HTML_REPLY, html); + + // Null HTML reply + message.mId = -1; // Changing the message; need to reset the id + message.mHtmlReply = null; + message.save(mMockContext); + body = createTestBody(message); + + html = Rfc822Output.getHtmlAlternate(body); + assertNull(html); + } + + /** + * Test the boundary digit. We modify it indirectly. + */ + public void testBoundaryDigit() { + // Use getBoundary() to update the boundary digit + Rfc822Output.sBoundaryDigit = 0; // ensure it starts at a known value + + Rfc822Output.getNextBoundary(); + assertEquals(1, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); + assertEquals(2, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); + assertEquals(3, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); + assertEquals(4, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); + assertEquals(5, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); + assertEquals(6, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); + assertEquals(7, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); + assertEquals(8, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); + assertEquals(9, Rfc822Output.sBoundaryDigit); + Rfc822Output.getNextBoundary(); // roll over + assertEquals(0, Rfc822Output.sBoundaryDigit); + } + + private final int BOUNDARY_COUNT = 12; + public void testGetNextBoundary() { + String[] resultArray = new String[BOUNDARY_COUNT]; + for (int i = 0; i < BOUNDARY_COUNT; i++) { + resultArray[i] = Rfc822Output.getNextBoundary(); + } + for (int i = 0; i < BOUNDARY_COUNT; i++) { + final String result1 = resultArray[i]; + for (int j = 0; j < BOUNDARY_COUNT; j++) { + if (i == j) { + continue; // Don't verify the same result + } + final String result2 = resultArray[j]; + assertFalse(result1.equals(result2)); + } + } + } + /** * Confirm that the constructed message includes "MIME-VERSION: 1.0" */