1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.emailcommon.internet; 18 19import com.android.email.R; 20import com.android.email.provider.EmailProvider; 21import com.android.emailcommon.internet.Rfc822Output; 22import com.android.emailcommon.mail.MessagingException; 23import com.android.emailcommon.provider.EmailContent; 24import com.android.emailcommon.provider.EmailContent.Attachment; 25import com.android.emailcommon.provider.EmailContent.Body; 26import com.android.emailcommon.provider.EmailContent.Message; 27 28import org.apache.james.mime4j.field.Field; 29import org.apache.james.mime4j.message.BodyPart; 30import org.apache.james.mime4j.message.Entity; 31import org.apache.james.mime4j.message.Header; 32import org.apache.james.mime4j.message.Multipart; 33 34import android.content.Context; 35import android.test.ProviderTestCase2; 36 37import java.io.ByteArrayInputStream; 38import java.io.ByteArrayOutputStream; 39import java.io.IOException; 40import java.util.ArrayList; 41import java.util.List; 42 43 44/** 45 * Tests of the Rfc822Output (used for sending mail) 46 * 47 * You can run this entire test case with: 48 * runtest -c com.android.email.mail.transport.Rfc822OutputTests email 49 */ 50public class Rfc822OutputTests extends ProviderTestCase2<EmailProvider> { 51 private static final String SENDER = "sender@android.com"; 52 private static final String RECIPIENT_TO = "recipient-to@android.com"; 53 private static final String RECIPIENT_CC = "recipient-cc@android.com"; 54 private static final String SUBJECT = "This is the subject"; 55 private static final String REPLY_TEXT_BODY = "This is the body. This is also the body."; 56 /** HTML reply body */ 57 private static final String BODY_HTML_REPLY = 58 "<a href=\"m.google.com\">This</a> is the body.<br>This is also the body."; 59 /** Text-only version of the HTML reply body */ 60 private static final String BODY_TEXT_REPLY_HTML = 61 ">This is the body.\n>This is also the body."; 62 private static final String TEXT = "Here is some new text."; 63 64 // Full HTML document 65 private static String HTML_FULL_BODY = "<html><head><title>MyTitle</title></head>" 66 + "<body bgcolor=\"#ffffff\" text=\"#000000\">" 67 + "<a href=\"google.com\">test1</a></body></html>"; 68 private static String HTML_FULL_RESULT = "<a href=\"google.com\">test1</a>"; 69 // <body/> element w/ content 70 private static String HTML_BODY_BODY = 71 "<body bgcolor=\"#ffffff\" text=\"#000000\"><a href=\"google.com\">test2</a></body>"; 72 private static String HTML_BODY_RESULT = "<a href=\"google.com\">test2</a>"; 73 // No <body/> tag; just content 74 private static String HTML_NO_BODY_BODY = 75 "<a href=\"google.com\">test3</a>"; 76 private static String HTML_NO_BODY_RESULT = "<a href=\"google.com\">test3</a>"; 77 78 private static String REPLY_INTRO_TEXT = "\n\n" + SENDER + " wrote:\n\n"; 79 private static String REPLY_INTRO_HTML = "<br><br>" + SENDER + " wrote:<br><br>"; 80 private Context mMockContext; 81 private String mForwardIntro; 82 83 public Rfc822OutputTests () { 84 super(EmailProvider.class, EmailContent.AUTHORITY); 85 } 86 87 @Override 88 public void setUp() throws Exception { 89 super.setUp(); 90 mMockContext = getMockContext(); 91 mForwardIntro = mMockContext.getString(R.string.message_compose_fwd_header_fmt, SUBJECT, 92 SENDER, RECIPIENT_TO, RECIPIENT_CC); 93 } 94 95 // TODO Create more tests here. Specifically, we should test to make sure that forward works 96 // properly instead of just reply 97 98 // TODO Write test that ensures that bcc is handled properly (i.e. sent/not send depending 99 // on the flag passed to writeTo 100 101 private Message createTestMessage(String text, boolean save) { 102 Message message = new Message(); 103 message.mText = text; 104 message.mFrom = SENDER; 105 message.mFlags = Message.FLAG_TYPE_REPLY; 106 message.mTextReply = REPLY_TEXT_BODY; 107 message.mHtmlReply = BODY_HTML_REPLY; 108 message.mIntroText = REPLY_INTRO_TEXT; 109 if (save) { 110 message.save(mMockContext); 111 } 112 return message; 113 } 114 115 private Body createTestBody(Message message) { 116 Body body = Body.restoreBodyWithMessageId(mMockContext, message.mId); 117 return body; 118 } 119 120 /** 121 * Test for buildBodyText(). 122 * Compare with expected values. 123 * Also test the situation where the message has no body. 124 */ 125 public void testBuildBodyText() { 126 // Test sending a message *without* using smart reply 127 Message message1 = createTestMessage("", true); 128 Body body1 = createTestBody(message1); 129 String[] bodyParts; 130 131 bodyParts = Rfc822Output.buildBodyText(body1, message1.mFlags, false); 132 assertEquals(REPLY_INTRO_TEXT + ">" + REPLY_TEXT_BODY, bodyParts[0]); 133 134 message1.mId = -1; // Changing the message; need to reset the id 135 message1.mText = TEXT; 136 message1.save(mMockContext); 137 body1 = createTestBody(message1); 138 139 bodyParts = Rfc822Output.buildBodyText(body1, message1.mFlags, false); 140 assertEquals(TEXT + REPLY_INTRO_TEXT + ">" + REPLY_TEXT_BODY, bodyParts[0]); 141 142 // We have an HTML reply and no text reply; use the HTML reply 143 message1.mId = -1; // Changing the message; need to reset the id 144 message1.mTextReply = null; 145 message1.save(mMockContext); 146 body1 = createTestBody(message1); 147 148 bodyParts = Rfc822Output.buildBodyText(body1, message1.mFlags, false); 149 assertEquals(TEXT + REPLY_INTRO_TEXT + BODY_TEXT_REPLY_HTML, bodyParts[0]); 150 151 // We have no HTML or text reply; use nothing 152 message1.mId = -1; // Changing the message; need to reset the id 153 message1.mHtmlReply = null; 154 message1.save(mMockContext); 155 body1 = createTestBody(message1); 156 157 bodyParts = Rfc822Output.buildBodyText(body1, message1.mFlags, false); 158 assertEquals(TEXT + REPLY_INTRO_TEXT, bodyParts[0]); 159 160 // Test sending a message *with* using smart reply 161 Message message2 = createTestMessage("", true); 162 Body body2 = createTestBody(message2); 163 164 bodyParts = Rfc822Output.buildBodyText(body2, message2.mFlags, true); 165 assertEquals(REPLY_INTRO_TEXT, bodyParts[0]); 166 167 message2.mId = -1; // Changing the message; need to reset the id 168 message2.mText = TEXT; 169 message2.save(mMockContext); 170 body2 = createTestBody(message2); 171 172 bodyParts = Rfc822Output.buildBodyText(body2, message2.mFlags, true); 173 assertEquals(TEXT + REPLY_INTRO_TEXT, bodyParts[0]); 174 175 // We have an HTML reply and no text reply; use nothing (smart reply) 176 message2.mId = -1; // Changing the message; need to reset the id 177 message2.mTextReply = null; 178 message2.save(mMockContext); 179 body2 = createTestBody(message2); 180 181 bodyParts = Rfc822Output.buildBodyText(body2, message2.mFlags, true); 182 assertEquals(TEXT + REPLY_INTRO_TEXT, bodyParts[0]); 183 184 // We have no HTML or text reply; use nothing 185 message2.mId = -1; // Changing the message; need to reset the id 186 message2.mTextReply = null; 187 message2.mHtmlReply = null; 188 message2.save(mMockContext); 189 body2 = createTestBody(message2); 190 191 bodyParts = Rfc822Output.buildBodyText(body2, message2.mFlags, true); 192 assertEquals(TEXT + REPLY_INTRO_TEXT, bodyParts[0]); 193 } 194 195 /** 196 * Test for buildBodyText(). 197 * Compare with expected values. 198 */ 199 public void testBuildBodyTextWithForward() { 200 Message msg = new Message(); 201 msg.mText = TEXT; 202 msg.mFrom = SENDER; 203 msg.mTo = RECIPIENT_TO; 204 msg.mCc = RECIPIENT_CC; 205 msg.mSubject = SUBJECT; 206 msg.mFlags = Message.FLAG_TYPE_FORWARD; 207 msg.mTextReply = REPLY_TEXT_BODY; 208 msg.mIntroText = mForwardIntro; 209 msg.save(mMockContext); 210 Body body = createTestBody(msg); 211 String[] bodyParts = Rfc822Output.buildBodyText(body, msg.mFlags, false); 212 assertEquals(TEXT + mForwardIntro + REPLY_TEXT_BODY, bodyParts[0]); 213 } 214 215 public void testWriteToText() throws IOException, MessagingException { 216 // Create a simple text message 217 Message msg = new Message(); 218 msg.mText = TEXT; 219 msg.mFrom = SENDER; 220 // Save this away 221 msg.save(mMockContext); 222 223 // Write out an Rfc822 message 224 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 225 Rfc822Output.writeTo(mMockContext, msg.mId, byteStream, true, false); 226 227 // Get the message and create a mime4j message from it 228 // We'll take advantage of its parsing capabilities 229 ByteArrayInputStream messageInputStream = 230 new ByteArrayInputStream(byteStream.toByteArray()); 231 org.apache.james.mime4j.message.Message mimeMessage = 232 new org.apache.james.mime4j.message.Message(messageInputStream); 233 234 // Make sure its structure is correct 235 checkMimeVersion(mimeMessage); 236 assertFalse(mimeMessage.isMultipart()); 237 assertEquals("text/plain", mimeMessage.getMimeType()); 238 } 239 240 @SuppressWarnings("unchecked") 241 public void testWriteToAlternativePart() throws IOException, MessagingException { 242 // Create a message with alternative part 243 Message msg = new Message(); 244 msg.mText = TEXT; 245 msg.mFrom = SENDER; 246 msg.mAttachments = new ArrayList<Attachment>(); 247 // Attach a meeting invitation, which needs to be sent as multipart/alternative 248 Attachment att = new Attachment(); 249 att.mContentBytes = "__CONTENT__".getBytes("UTF-8"); 250 att.mFlags = Attachment.FLAG_ICS_ALTERNATIVE_PART; 251 att.mMimeType = "text/calendar"; 252 att.mFileName = "invite.ics"; 253 msg.mAttachments.add(att); 254 // Save this away 255 msg.save(mMockContext); 256 257 // Write out an Rfc822 message 258 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 259 Rfc822Output.writeTo(mMockContext, msg.mId, byteStream, true, false); 260 261 // Get the message and create a mime4j message from it 262 // We'll take advantage of its parsing capabilities 263 ByteArrayInputStream messageInputStream = 264 new ByteArrayInputStream(byteStream.toByteArray()); 265 org.apache.james.mime4j.message.Message mimeMessage = 266 new org.apache.james.mime4j.message.Message(messageInputStream); 267 268 // Make sure its structure is correct 269 checkMimeVersion(mimeMessage); 270 assertTrue(mimeMessage.isMultipart()); 271 Header header = mimeMessage.getHeader(); 272 Field contentType = header.getField("content-type"); 273 assertTrue(contentType.getBody().contains("multipart/alternative")); 274 Multipart multipart = (Multipart)mimeMessage.getBody(); 275 List<BodyPart> partList = multipart.getBodyParts(); 276 assertEquals(2, partList.size()); 277 Entity part = partList.get(0); 278 assertEquals("text/plain", part.getMimeType()); 279 part = partList.get(1); 280 assertEquals("text/calendar", part.getMimeType()); 281 header = part.getHeader(); 282 assertNull(header.getField("content-disposition")); 283 } 284 285 @SuppressWarnings("unchecked") 286 public void testWriteToMixedPart() throws IOException, MessagingException { 287 // Create a message with a mixed part 288 Message msg = new Message(); 289 msg.mText = TEXT; 290 msg.mFrom = SENDER; 291 msg.mAttachments = new ArrayList<Attachment>(); 292 // Attach a simple html "file" 293 Attachment att = new Attachment(); 294 att.mContentBytes = "<html>Hi</html>".getBytes("UTF-8"); 295 att.mMimeType = "text/html"; 296 att.mFileName = "test.html"; 297 msg.mAttachments.add(att); 298 // Save this away 299 msg.save(mMockContext); 300 301 // Write out an Rfc822 message 302 ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 303 Rfc822Output.writeTo(mMockContext, msg.mId, byteStream, true, false); 304 305 // Get the message and create a mime4j message from it 306 // We'll take advantage of its parsing capabilities 307 ByteArrayInputStream messageInputStream = 308 new ByteArrayInputStream(byteStream.toByteArray()); 309 org.apache.james.mime4j.message.Message mimeMessage = 310 new org.apache.james.mime4j.message.Message(messageInputStream); 311 312 // Make sure its structure is correct 313 checkMimeVersion(mimeMessage); 314 assertTrue(mimeMessage.isMultipart()); 315 Header header = mimeMessage.getHeader(); 316 Field contentType = header.getField("content-type"); 317 assertTrue(contentType.getBody().contains("multipart/mixed")); 318 Multipart multipart = (Multipart)mimeMessage.getBody(); 319 List<BodyPart> partList = multipart.getBodyParts(); 320 assertEquals(2, partList.size()); 321 Entity part = partList.get(0); 322 assertEquals("text/plain", part.getMimeType()); 323 part = partList.get(1); 324 assertEquals("text/html", part.getMimeType()); 325 header = part.getHeader(); 326 assertNotNull(header.getField("content-disposition")); 327 } 328 329 /** 330 * Tests various types of HTML reply text -- with full <html/> tags, 331 * with just the <body/> tags and without any surrounding tags. 332 */ 333 public void testGetHtmlBody() { 334 String actual; 335 actual = Rfc822Output.getHtmlBody(HTML_FULL_BODY); 336 assertEquals(HTML_FULL_RESULT, actual); 337 actual = Rfc822Output.getHtmlBody(HTML_BODY_BODY); 338 assertEquals(HTML_BODY_RESULT, actual); 339 actual = Rfc822Output.getHtmlBody(HTML_NO_BODY_BODY); 340 assertEquals(HTML_NO_BODY_RESULT, actual); 341 } 342 343 /** 344 * Tests that the entire HTML alternate string is valid for text entered by 345 * the user. We don't test all permutations of forwarded HTML here because 346 * that is verified by testGetHtmlBody(). 347 */ 348 public void testGetHtmlAlternate() { 349 Message message = createTestMessage(TEXT, true); 350 Body body = createTestBody(message); 351 String html; 352 353 // Generic case 354 html = Rfc822Output.getHtmlAlternate(body, false); 355 assertEquals(TEXT + REPLY_INTRO_HTML + BODY_HTML_REPLY, html); 356 357 // "smart reply" enabled; html body should not be added 358 html = Rfc822Output.getHtmlAlternate(body, true); 359 assertEquals(TEXT + REPLY_INTRO_HTML, html); 360 361 // HTML special characters; dependent upon TextUtils#htmlEncode() 362 message.mId = -1; // Changing the message; need to reset the id 363 message.mText = "<>&'\""; 364 message.save(mMockContext); 365 body = createTestBody(message); 366 367 html = Rfc822Output.getHtmlAlternate(body, false); 368 assertEquals("<>&'"" + REPLY_INTRO_HTML + BODY_HTML_REPLY, html); 369 370 // Newlines in user text 371 message.mId = -1; // Changing the message; need to reset the id 372 message.mText = "dos\r\nunix\nthree\r\n\n\n"; 373 message.save(mMockContext); 374 body = createTestBody(message); 375 376 html = Rfc822Output.getHtmlAlternate(body, false); 377 assertEquals("dos<br>unix<br>three<br><br><br>" + REPLY_INTRO_HTML + BODY_HTML_REPLY, html); 378 379 // Null HTML reply 380 message.mId = -1; // Changing the message; need to reset the id 381 message.mHtmlReply = null; 382 message.save(mMockContext); 383 body = createTestBody(message); 384 385 html = Rfc822Output.getHtmlAlternate(body, false); 386 assertNull(html); 387 } 388 389 /** 390 * Test the boundary digit. We modify it indirectly. 391 */ 392 public void testBoundaryDigit() { 393 // Use getBoundary() to update the boundary digit 394 Rfc822Output.sBoundaryDigit = 0; // ensure it starts at a known value 395 396 Rfc822Output.getNextBoundary(); 397 assertEquals(1, Rfc822Output.sBoundaryDigit); 398 Rfc822Output.getNextBoundary(); 399 assertEquals(2, Rfc822Output.sBoundaryDigit); 400 Rfc822Output.getNextBoundary(); 401 assertEquals(3, Rfc822Output.sBoundaryDigit); 402 Rfc822Output.getNextBoundary(); 403 assertEquals(4, Rfc822Output.sBoundaryDigit); 404 Rfc822Output.getNextBoundary(); 405 assertEquals(5, Rfc822Output.sBoundaryDigit); 406 Rfc822Output.getNextBoundary(); 407 assertEquals(6, Rfc822Output.sBoundaryDigit); 408 Rfc822Output.getNextBoundary(); 409 assertEquals(7, Rfc822Output.sBoundaryDigit); 410 Rfc822Output.getNextBoundary(); 411 assertEquals(8, Rfc822Output.sBoundaryDigit); 412 Rfc822Output.getNextBoundary(); 413 assertEquals(9, Rfc822Output.sBoundaryDigit); 414 Rfc822Output.getNextBoundary(); // roll over 415 assertEquals(0, Rfc822Output.sBoundaryDigit); 416 } 417 418 private final int BOUNDARY_COUNT = 12; 419 public void testGetNextBoundary() { 420 String[] resultArray = new String[BOUNDARY_COUNT]; 421 for (int i = 0; i < BOUNDARY_COUNT; i++) { 422 resultArray[i] = Rfc822Output.getNextBoundary(); 423 } 424 for (int i = 0; i < BOUNDARY_COUNT; i++) { 425 final String result1 = resultArray[i]; 426 for (int j = 0; j < BOUNDARY_COUNT; j++) { 427 if (i == j) { 428 continue; // Don't verify the same result 429 } 430 final String result2 = resultArray[j]; 431 assertFalse(result1.equals(result2)); 432 } 433 } 434 } 435 436 /** 437 * Confirm that the constructed message includes "MIME-VERSION: 1.0" 438 */ 439 private void checkMimeVersion(org.apache.james.mime4j.message.Message mimeMessage) { 440 Header header = mimeMessage.getHeader(); 441 Field contentType = header.getField("MIME-VERSION"); 442 assertTrue(contentType.getBody().equals("1.0")); 443 } 444} 445