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("&lt;&gt;&amp;&apos;&quot;" + 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