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
This commit is contained in:
parent
45e161bb5e
commit
9cc51b72c6
|
@ -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 <body> 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 = "<br>";
|
||||
/** 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 <body></body> tags. This is not perfect and breaks
|
||||
* with malformed HTML or if there happens to be special characters in the attributes of
|
||||
* the <body> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
<init>(java.lang.String);
|
||||
<init>(java.lang.String,java.lang.String);
|
||||
|
@ -60,7 +65,7 @@
|
|||
-keep class com.android.emailcommon.service.PolicySet {
|
||||
<init>(com.android.emailcommon.provider.EmailContent$Account);
|
||||
<init>(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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<EmailProvider> {
|
|||
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 =
|
||||
"<a href=\"m.google.com\">This</a> is the body.<br>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 = "<html><head><title>MyTitle</title></head>"
|
||||
+ "<body bgcolor=\"#ffffff\" text=\"#000000\">"
|
||||
+ "<a href=\"google.com\">test1</a></body></html>";
|
||||
private static String HTML_FULL_RESULT = "<a href=\"google.com\">test1</a>";
|
||||
// <body/> element w/ content
|
||||
private static String HTML_BODY_BODY =
|
||||
"<body bgcolor=\"#ffffff\" text=\"#000000\"><a href=\"google.com\">test2</a></body>";
|
||||
private static String HTML_BODY_RESULT = "<a href=\"google.com\">test2</a>";
|
||||
// No <body/> tag; just content
|
||||
private static String HTML_NO_BODY_BODY =
|
||||
"<a href=\"google.com\">test3</a>";
|
||||
private static String HTML_NO_BODY_RESULT = "<a href=\"google.com\">test3</a>";
|
||||
|
||||
private static String REPLY_INTRO_TEXT = "\n\n" + SENDER + " wrote:\n\n";
|
||||
private static String REPLY_INTRO_HTML = "<br><br>" + SENDER + " wrote:<br><br>";
|
||||
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<EmailProvider> {
|
|||
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<EmailProvider> {
|
|||
// 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<EmailProvider> {
|
|||
*/
|
||||
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<EmailProvider> {
|
|||
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<EmailProvider> {
|
|||
assertNotNull(header.getField("content-disposition"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests various types of HTML reply text -- with full <html/> tags,
|
||||
* with just the <body/> 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<br>unix<br>three<br><br><br>" + 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"
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue