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 17 package com.android.emailcommon.internet; 18 19 import android.test.AndroidTestCase; 20 import android.test.suitebuilder.annotation.MediumTest; 21 import android.test.suitebuilder.annotation.SmallTest; 22 23 import com.android.emailcommon.TempDirectory; 24 import com.android.emailcommon.mail.Address; 25 import com.android.emailcommon.mail.Flag; 26 import com.android.emailcommon.mail.Message.RecipientType; 27 import com.android.emailcommon.mail.MessagingException; 28 29 import java.io.ByteArrayInputStream; 30 import java.io.ByteArrayOutputStream; 31 import java.io.IOException; 32 import java.text.ParseException; 33 import java.text.SimpleDateFormat; 34 import java.util.Date; 35 import java.util.Locale; 36 37 /** 38 * This is a series of unit tests for the MimeMessage class. These tests must be locally 39 * complete - no server(s) required. 40 */ 41 @SmallTest 42 public class MimeMessageTest extends AndroidTestCase { 43 44 /** up arrow, down arrow, left arrow, right arrow */ 45 private final String SHORT_UNICODE = "\u2191\u2193\u2190\u2192"; 46 private final String SHORT_UNICODE_ENCODED = "=?UTF-8?B?4oaR4oaT4oaQ4oaS?="; 47 48 /** a string without any unicode */ 49 private final String SHORT_PLAIN = "abcd"; 50 51 /** longer unicode strings */ 52 private final String LONG_UNICODE_16 = SHORT_UNICODE + SHORT_UNICODE + 53 SHORT_UNICODE + SHORT_UNICODE; 54 private final String LONG_UNICODE_64 = LONG_UNICODE_16 + LONG_UNICODE_16 + 55 LONG_UNICODE_16 + LONG_UNICODE_16; 56 57 /** longer plain strings (with fold points) */ 58 private final String LONG_PLAIN_16 = "abcdefgh ijklmno"; 59 private final String LONG_PLAIN_64 = 60 LONG_PLAIN_16 + LONG_PLAIN_16 + LONG_PLAIN_16 + LONG_PLAIN_16; 61 private final String LONG_PLAIN_256 = 62 LONG_PLAIN_64 + LONG_PLAIN_64 + LONG_PLAIN_64 + LONG_PLAIN_64; 63 64 @Override setUp()65 protected void setUp() throws Exception { 66 super.setUp(); 67 TempDirectory.setTempDirectory(getContext()); 68 } 69 70 /** 71 * Confirms that setSentDate() correctly set the "Date" header of a Mime message. 72 * 73 * We tries a same test twice using two locales, Locale.US and the other, since 74 * MimeMessage depends on the date formatter, which may emit wrong date format 75 * in the locale other than Locale.US. 76 * @throws MessagingException 77 * @throws ParseException 78 */ 79 @MediumTest testSetSentDate()80 public void testSetSentDate() throws MessagingException, ParseException { 81 Locale savedLocale = Locale.getDefault(); 82 Locale.setDefault(Locale.US); 83 doTestSetSentDate(); 84 Locale.setDefault(Locale.JAPAN); 85 doTestSetSentDate(); 86 Locale.setDefault(savedLocale); 87 } 88 doTestSetSentDate()89 private void doTestSetSentDate() throws MessagingException, ParseException { 90 // "Thu, 01 Jan 2009 09:00:00 +0000" => 1230800400000L 91 long expectedTime = 1230800400000L; 92 Date date = new Date(expectedTime); 93 MimeMessage message = new MimeMessage(); 94 message.setSentDate(date); 95 String[] headers = message.getHeader("Date"); 96 assertEquals(1, headers.length); 97 // Explicitly specify the locale so that the object does not depend on the default 98 // locale. 99 SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); 100 101 Date result = format.parse(headers[0]); 102 assertEquals(expectedTime, result.getTime()); 103 } 104 105 /** 106 * Simple tests of the new "Message-ID" header 107 */ testMessageId()108 public void testMessageId() throws MessagingException { 109 110 // Test 1. Every message gets a default and unique message-id 111 MimeMessage message1 = new MimeMessage(); 112 MimeMessage message2 = new MimeMessage(); 113 String id1 = message1.getMessageId(); 114 String id2 = message2.getMessageId(); 115 assertNotNull(id1); 116 assertNotNull(id2); 117 assertFalse("Message-ID should be unique", id1.equals(id2)); 118 119 // Test 2. Set and get using API 120 final String testId1 = "test-message-id-one"; 121 message1.setMessageId(testId1); 122 assertEquals("set and get Message-ID", testId1, message1.getMessageId()); 123 124 // Test 3. Should only be one Message-ID per message 125 final String testId2 = "test-message-id-two"; 126 message2.setMessageId(testId1); 127 message2.setMessageId(testId2); 128 assertEquals("set and get Message-ID", testId2, message2.getMessageId()); 129 } 130 131 /** 132 * Confirm getContentID() correctly works. 133 */ testGetContentId()134 public void testGetContentId() throws MessagingException { 135 MimeMessage message = new MimeMessage(); 136 137 // no content-id 138 assertNull(message.getContentId()); 139 140 // normal case 141 final String cid1 = "cid.1@android.com"; 142 message.setHeader(MimeHeader.HEADER_CONTENT_ID, cid1); 143 assertEquals(cid1, message.getContentId()); 144 145 // surrounded by optional bracket 146 message.setHeader(MimeHeader.HEADER_CONTENT_ID, "<" + cid1 + ">"); 147 assertEquals(cid1, message.getContentId()); 148 } 149 150 /** 151 * Confirm that setSubject() works with plain strings 152 */ testSetSubjectPlain()153 public void testSetSubjectPlain() throws MessagingException { 154 MimeMessage message = new MimeMessage(); 155 156 message.setSubject(SHORT_PLAIN); 157 158 // test 1: readback 159 assertEquals("plain subjects", SHORT_PLAIN, message.getSubject()); 160 161 // test 2: raw readback is not escaped 162 String rawHeader = message.getFirstHeader("Subject"); 163 assertEquals("plain subject not encoded", -1, rawHeader.indexOf("=?")); 164 165 // test 3: long subject (shouldn't fold) 166 message.setSubject(LONG_PLAIN_64); 167 rawHeader = message.getFirstHeader("Subject"); 168 String[] split = rawHeader.split("\r\n"); 169 assertEquals("64 shouldn't fold", 1, split.length); 170 171 // test 4: very long subject (should fold) 172 message.setSubject(LONG_PLAIN_256); 173 rawHeader = message.getFirstHeader("Subject"); 174 split = rawHeader.split("\r\n"); 175 assertTrue("long subject should fold", split.length > 1); 176 for (String s : split) { 177 assertTrue("split lines max length 78", s.length() <= 76); // 76+\r\n = 78 178 String trimmed = s.trim(); 179 assertFalse("split lines are not encoded", trimmed.startsWith("=?")); 180 } 181 } 182 183 /** 184 * Confirm that setSubject() works with unicode strings 185 */ testSetSubject()186 public void testSetSubject() throws MessagingException { 187 MimeMessage message = new MimeMessage(); 188 189 message.setSubject(SHORT_UNICODE); 190 191 // test 1: readback in unicode 192 assertEquals("unicode readback", SHORT_UNICODE, message.getSubject()); 193 194 // test 2: raw readback is escaped 195 String rawHeader = message.getFirstHeader("Subject"); 196 assertEquals("raw readback", SHORT_UNICODE_ENCODED, rawHeader); 197 } 198 199 /** 200 * Confirm folding operations on unicode subjects 201 */ testSetLongSubject()202 public void testSetLongSubject() throws MessagingException { 203 MimeMessage message = new MimeMessage(); 204 205 // test 1: long unicode - readback in unicode 206 message.setSubject(LONG_UNICODE_16); 207 assertEquals("unicode readback 16", LONG_UNICODE_16, message.getSubject()); 208 209 // test 2: longer unicode (will fold) 210 message.setSubject(LONG_UNICODE_64); 211 assertEquals("unicode readback 64", LONG_UNICODE_64, message.getSubject()); 212 213 // test 3: check folding & encoding 214 String rawHeader = message.getFirstHeader("Subject"); 215 String[] split = rawHeader.split("\r\n"); 216 assertTrue("long subject should fold", split.length > 1); 217 for (String s : split) { 218 assertTrue("split lines max length 78", s.length() <= 76); // 76+\r\n = 78 219 String trimmed = s.trim(); 220 assertTrue("split lines are encoded", 221 trimmed.startsWith("=?") && trimmed.endsWith("?=")); 222 } 223 } 224 225 /** 226 * Test for encoding address field. 227 */ testEncodingAddressField()228 public void testEncodingAddressField() throws MessagingException { 229 Address noName1 = new Address("noname1@dom1.com"); 230 Address noName2 = new Address("<noname2@dom2.com>", ""); 231 Address simpleName = new Address("address3@dom3.org", "simple long and long long name"); 232 Address dquoteName = new Address("address4@dom4.org", "name,4,long long name"); 233 Address quotedName = new Address("bigG@dom5.net", "big \"G\""); 234 Address utf16Name = new Address("<address6@co.jp>", "\"\u65E5\u672C\u8A9E\""); 235 Address utf32Name = new Address("<address8@ne.jp>", "\uD834\uDF01\uD834\uDF46"); 236 237 MimeMessage message = new MimeMessage(); 238 239 message.setFrom(noName1); 240 message.setRecipient(RecipientType.TO, noName2); 241 message.setRecipients(RecipientType.CC, new Address[] { simpleName, dquoteName }); 242 message.setReplyTo(new Address[] { quotedName, utf16Name, utf32Name }); 243 244 String[] from = message.getHeader("From"); 245 String[] to = message.getHeader("To"); 246 String[] cc = message.getHeader("Cc"); 247 String[] replyTo = message.getHeader("Reply-to"); 248 249 assertEquals("from address count", 1, from.length); 250 assertEquals("no name 1", "noname1@dom1.com", from[0]); 251 252 assertEquals("to address count", 1, to.length); 253 assertEquals("no name 2", "noname2@dom2.com", to[0]); 254 255 // folded. 256 assertEquals("cc address count", 1, cc.length); 257 assertEquals("simple name & double quoted name", 258 "simple long and long long name <address3@dom3.org>, \"name,4,long long\r\n" 259 + " name\" <address4@dom4.org>", 260 cc[0]); 261 262 // folded and encoded. 263 assertEquals("reply-to address count", 1, replyTo.length); 264 assertEquals("quoted name & encoded name", 265 "\"big \\\"G\\\"\" <bigG@dom5.net>, =?UTF-8?B?5pel5pys6Kqe?=\r\n" 266 + " <address6@co.jp>, =?UTF-8?B?8J2MgfCdjYY=?= <address8@ne.jp>", 267 replyTo[0]); 268 } 269 270 /** 271 * Test for parsing address field. 272 */ testParsingAddressField()273 public void testParsingAddressField() throws MessagingException { 274 MimeMessage message = new MimeMessage(); 275 276 message.setHeader("From", "noname1@dom1.com"); 277 message.setHeader("To", "<noname2@dom2.com>"); 278 // folded. 279 message.setHeader("Cc", 280 "simple name <address3@dom3.org>,\r\n" 281 + " \"name,4\" <address4@dom4.org>"); 282 // folded and encoded. 283 message.setHeader("Reply-to", 284 "\"big \\\"G\\\"\" <bigG@dom5.net>,\r\n" 285 + " =?UTF-8?B?5pel5pys6Kqe?=\r\n" 286 + " <address6@co.jp>,\n" 287 + " \"=?UTF-8?B?8J2MgfCdjYY=?=\" <address8@ne.jp>"); 288 289 Address[] from = message.getFrom(); 290 Address[] to = message.getRecipients(RecipientType.TO); 291 Address[] cc = message.getRecipients(RecipientType.CC); 292 Address[] replyTo = message.getReplyTo(); 293 294 assertEquals("from address count", 1, from.length); 295 assertEquals("no name 1 address", "noname1@dom1.com", from[0].getAddress()); 296 assertNull("no name 1 name", from[0].getPersonal()); 297 298 assertEquals("to address count", 1, to.length); 299 assertEquals("no name 2 address", "noname2@dom2.com", to[0].getAddress()); 300 assertNull("no name 2 name", to[0].getPersonal()); 301 302 assertEquals("cc address count", 2, cc.length); 303 assertEquals("simple name address", "address3@dom3.org", cc[0].getAddress()); 304 assertEquals("simple name name", "simple name", cc[0].getPersonal()); 305 assertEquals("double quoted name address", "address4@dom4.org", cc[1].getAddress()); 306 assertEquals("double quoted name name", "name,4", cc[1].getPersonal()); 307 308 assertEquals("reply-to address count", 3, replyTo.length); 309 assertEquals("quoted name address", "bigG@dom5.net", replyTo[0].getAddress()); 310 assertEquals("quoted name name", "big \"G\"", replyTo[0].getPersonal()); 311 assertEquals("utf-16 name address", "address6@co.jp", replyTo[1].getAddress()); 312 assertEquals("utf-16 name name", "\u65E5\u672C\u8A9E", replyTo[1].getPersonal()); 313 assertEquals("utf-32 name address", "address8@ne.jp", replyTo[2].getAddress()); 314 assertEquals("utf-32 name name", "\uD834\uDF01\uD834\uDF46", replyTo[2].getPersonal()); 315 } 316 317 /* 318 * Test setting & getting store-specific flags 319 */ testStoreFlags()320 public void testStoreFlags() throws MessagingException { 321 MimeMessage message = new MimeMessage(); 322 323 // Message should create with no flags 324 Flag[] flags = message.getFlags(); 325 assertEquals(0, flags.length); 326 327 // Set a store flag 328 message.setFlag(Flag.X_STORE_1, true); 329 assertTrue(message.isSet(Flag.X_STORE_1)); 330 assertFalse(message.isSet(Flag.X_STORE_2)); 331 332 // Set another 333 message.setFlag(Flag.X_STORE_2, true); 334 assertTrue(message.isSet(Flag.X_STORE_1)); 335 assertTrue(message.isSet(Flag.X_STORE_2)); 336 337 // Set some and clear some 338 message.setFlag(Flag.X_STORE_1, false); 339 assertFalse(message.isSet(Flag.X_STORE_1)); 340 assertTrue(message.isSet(Flag.X_STORE_2)); 341 342 } 343 344 /* 345 * Test for setExtendedHeader() and getExtendedHeader() 346 */ testExtendedHeader()347 public void testExtendedHeader() throws MessagingException { 348 MimeMessage message = new MimeMessage(); 349 350 assertNull("non existent header", message.getExtendedHeader("X-Non-Existent")); 351 352 message.setExtendedHeader("X-Header1", "value1"); 353 message.setExtendedHeader("X-Header2", "value2\n value3\r\n value4\r\n"); 354 assertEquals("simple value", "value1", 355 message.getExtendedHeader("X-Header1")); 356 assertEquals("multi line value", "value2 value3 value4", 357 message.getExtendedHeader("X-Header2")); 358 assertNull("non existent header 2", message.getExtendedHeader("X-Non-Existent")); 359 360 message.setExtendedHeader("X-Header1", "value4"); 361 assertEquals("over written value", "value4", message.getExtendedHeader("X-Header1")); 362 363 message.setExtendedHeader("X-Header1", null); 364 assertNull("remove header", message.getExtendedHeader("X-Header1")); 365 } 366 367 /* 368 * Test for setExtendedHeaders() and getExtendedheaders() 369 */ testExtendedHeaders()370 public void testExtendedHeaders() throws MessagingException { 371 MimeMessage message = new MimeMessage(); 372 373 assertNull("new message", message.getExtendedHeaders()); 374 message.setExtendedHeaders(null); 375 assertNull("null headers", message.getExtendedHeaders()); 376 message.setExtendedHeaders(""); 377 assertNull("empty headers", message.getExtendedHeaders()); 378 379 message.setExtendedHeaders("X-Header1: value1\r\n"); 380 assertEquals("header 1 value", "value1", message.getExtendedHeader("X-Header1")); 381 assertEquals("header 1", "X-Header1: value1\r\n", message.getExtendedHeaders()); 382 383 message.setExtendedHeaders(null); 384 message.setExtendedHeader("X-Header2", "value2"); 385 message.setExtendedHeader("X-Header3", "value3\n value4\r\n value5\r\n"); 386 assertEquals("headers 2,3", 387 "X-Header2: value2\r\n" + 388 "X-Header3: value3 value4 value5\r\n", 389 message.getExtendedHeaders()); 390 391 message.setExtendedHeaders( 392 "X-Header3: value3 value4 value5\r\n" + 393 "X-Header2: value2\r\n"); 394 assertEquals("header 2", "value2", message.getExtendedHeader("X-Header2")); 395 assertEquals("header 3", "value3 value4 value5", message.getExtendedHeader("X-Header3")); 396 assertEquals("headers 3,2", 397 "X-Header3: value3 value4 value5\r\n" + 398 "X-Header2: value2\r\n", 399 message.getExtendedHeaders()); 400 } 401 402 /* 403 * Test for writeTo(), only for header part. 404 * NOTE: This test is fragile because it assumes headers will be written in a specific order 405 */ testWriteToHeader()406 public void testWriteToHeader() throws Exception { 407 MimeMessage message = new MimeMessage(); 408 409 message.setHeader("Header1", "value1"); 410 message.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "value2"); 411 message.setExtendedHeader("X-Header3", "value3"); 412 message.setHeader("Header4", "value4"); 413 message.setExtendedHeader("X-Header5", "value5"); 414 415 ByteArrayOutputStream out = new ByteArrayOutputStream(); 416 message.writeTo(out); 417 out.close(); 418 String expectedString = 419 "Header1: value1\r\n" + 420 "Header4: value4\r\n" + 421 "Message-ID: " + message.getMessageId() + "\r\n" + 422 "\r\n"; 423 byte[] expected = expectedString.getBytes(); 424 byte[] actual = out.toByteArray(); 425 assertEquals("output length", expected.length, actual.length); 426 for (int i = 0; i < actual.length; ++i) { 427 assertEquals("output byte["+i+"]", expected[i], actual[i]); 428 } 429 } 430 431 /** 432 * Test for parsing headers with extra whitespace and commennts. 433 * 434 * The lines up to Content-Type were copied directly out of RFC 2822 435 * "Section A.5. White space, comments, and other oddities" 436 */ brokentestWhiteSpace()437 public void brokentestWhiteSpace() throws MessagingException, IOException { 438 String entireMessage = 439 "From: Pete(A wonderful \\) chap) <pete(his account)@silly.test(his host)>\r\n"+ 440 "To:A Group(Some people)\r\n"+ 441 " :Chris Jones <c@(Chris's host.)public.example>,\r\n"+ 442 " joe@example.org,\r\n"+ 443 " John <jdoe@one.test> (my dear friend); (the end of the group)\r\n"+ 444 "Cc:(Empty list)(start)Undisclosed recipients :(nobody(that I know)) ;\r\n"+ 445 "Date: Thu,\r\n"+ 446 " 13\r\n"+ 447 " Feb\r\n"+ 448 " 1969\r\n"+ 449 " 23:32\r\n"+ 450 " -0330 (Newfoundland Time)\r\n"+ 451 "Message-ID: <testabcd.1234@silly.test>\r\n"+ 452 "Content-Type: \r\n"+ 453 " TEXT/hTML \r\n"+ 454 " ; x-blah=\"y-blah\" ; \r\n"+ 455 " CHARSET=\"us-ascii\" ; (comment)\r\n"+ 456 "\r\n"+ 457 "<html><body>Testing.</body></html>\r\n"; 458 MimeMessage mm = null; 459 mm = new MimeMessage(new ByteArrayInputStream( 460 entireMessage.getBytes("us-ascii"))); 461 assertTrue(mm.getMimeType(), MimeUtility.mimeTypeMatches("text/html",mm.getMimeType())); 462 assertEquals(new Date(-27723480000L),mm.getSentDate()); 463 assertEquals("<testabcd.1234@silly.test>",mm.getMessageId()); 464 Address[] toAddresses = mm.getRecipients(MimeMessage.RecipientType.TO); 465 assertEquals("joe@example.org", toAddresses[1].getAddress()); 466 assertEquals("jdoe@one.test", toAddresses[2].getAddress()); 467 468 469 // Note: The parentheses in the middle of email addresses are not removed. 470 //assertEquals("c@public.example", toAddresses[0].getAddress()); 471 //assertEquals("pete@silly.test",mm.getFrom()[0].getAddress()); 472 } 473 474 /** 475 * Confirm parser doesn't crash when seeing "Undisclosed recipients:;". 476 */ testUndisclosedRecipients()477 public void testUndisclosedRecipients() throws MessagingException, IOException { 478 String entireMessage = 479 "To:Undisclosed recipients:;\r\n"+ 480 "Cc:Undisclosed recipients:;\r\n"+ 481 "Bcc:Undisclosed recipients:;\r\n"+ 482 "\r\n"; 483 MimeMessage mm = null; 484 mm = new MimeMessage(new ByteArrayInputStream( 485 entireMessage.getBytes("us-ascii"))); 486 487 assertEquals(0, mm.getRecipients(MimeMessage.RecipientType.TO).length); 488 assertEquals(0, mm.getRecipients(MimeMessage.RecipientType.CC).length); 489 assertEquals(0, mm.getRecipients(MimeMessage.RecipientType.BCC).length); 490 } 491 492 /** 493 * Confirm parser doesn't crash when seeing invalid headers/addresses. 494 */ testInvalidHeaders()495 public void testInvalidHeaders() throws MessagingException, IOException { 496 String entireMessage = 497 "To:\r\n"+ 498 "Cc:!invalid!address!, a@b.com\r\n"+ 499 "Bcc:Undisclosed recipients;\r\n"+ // no colon at the end 500 "invalid header\r\n"+ 501 "Message-ID:<testabcd.1234@silly.test>\r\n"+ 502 "\r\n"+ 503 "Testing\r\n"; 504 MimeMessage mm = null; 505 mm = new MimeMessage(new ByteArrayInputStream( 506 entireMessage.getBytes("us-ascii"))); 507 508 assertEquals(0, mm.getRecipients(MimeMessage.RecipientType.TO).length); 509 assertEquals(1, mm.getRecipients(MimeMessage.RecipientType.CC).length); 510 assertEquals("a@b.com", mm.getRecipients(MimeMessage.RecipientType.CC)[0].getAddress()); 511 assertEquals(0, mm.getRecipients(MimeMessage.RecipientType.BCC).length); 512 assertEquals("<testabcd.1234@silly.test>", mm.getMessageId()); 513 } 514 515 /** 516 * Confirm parser w/o a message-id inhibits a local message-id from being generated 517 */ testParseNoMessageId()518 public void testParseNoMessageId() throws MessagingException, IOException { 519 String entireMessage = 520 "To: user@domain.com\r\n" + 521 "\r\n" + 522 "Testing\r\n"; 523 MimeMessage mm = null; 524 mm = new MimeMessage(new ByteArrayInputStream(entireMessage.getBytes("us-ascii"))); 525 526 assertNull(mm.getMessageId()); 527 } 528 529 /** 530 * Make sure the parser accepts the "eBay style" date format. 531 * 532 * Messages from ebay have been seen that they use the wrong date format. 533 * @see com.android.emailcommon.utility.Utility#cleanUpMimeDate 534 */ testEbayDate()535 public void testEbayDate() throws MessagingException, IOException { 536 String entireMessage = 537 "To:a@b.com\r\n" + 538 "Date:Thu, 10 Dec 09 15:08:08 GMT-0700" + 539 "\r\n" + 540 "\r\n"; 541 MimeMessage mm = null; 542 mm = new MimeMessage(new ByteArrayInputStream(entireMessage.getBytes("us-ascii"))); 543 Date actual = mm.getSentDate(); 544 Date expected = new Date(Date.UTC(109, 11, 10, 15, 8, 8) + 7 * 60 * 60 * 1000); 545 assertEquals(expected, actual); 546 } 547 548 // TODO more test for writeTo() 549 } 550