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 android.test.AndroidTestCase;
20import android.test.suitebuilder.annotation.MediumTest;
21import android.test.suitebuilder.annotation.SmallTest;
22
23import com.android.emailcommon.TempDirectory;
24import com.android.emailcommon.mail.Address;
25import com.android.emailcommon.mail.Flag;
26import com.android.emailcommon.mail.Message.RecipientType;
27import com.android.emailcommon.mail.MessagingException;
28
29import java.io.ByteArrayInputStream;
30import java.io.ByteArrayOutputStream;
31import java.io.IOException;
32import java.text.ParseException;
33import java.text.SimpleDateFormat;
34import java.util.Date;
35import 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
42public 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
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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