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