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.email;
18
19import android.content.ContentUris;
20import android.content.Context;
21import android.database.Cursor;
22import android.net.Uri;
23import android.test.ProviderTestCase2;
24import android.test.suitebuilder.annotation.Suppress;
25
26import com.android.email.provider.EmailProvider;
27import com.android.email.provider.ProviderTestUtils;
28import com.android.emailcommon.internet.MimeHeader;
29import com.android.emailcommon.internet.MimeUtility;
30import com.android.emailcommon.mail.Address;
31import com.android.emailcommon.mail.BodyPart;
32import com.android.emailcommon.mail.Flag;
33import com.android.emailcommon.mail.Message;
34import com.android.emailcommon.mail.Message.RecipientType;
35import com.android.emailcommon.mail.MessageTestUtils;
36import com.android.emailcommon.mail.MessageTestUtils.MessageBuilder;
37import com.android.emailcommon.mail.MessageTestUtils.MultipartBuilder;
38import com.android.emailcommon.mail.MessagingException;
39import com.android.emailcommon.mail.Part;
40import com.android.emailcommon.provider.EmailContent;
41import com.android.emailcommon.provider.EmailContent.Attachment;
42
43import java.io.IOException;
44import java.util.ArrayList;
45
46/**
47 * Tests of the Legacy Conversions code (used by MessagingController).
48 *
49 * NOTE:  It would probably make sense to rewrite this using a MockProvider, instead of the
50 * ProviderTestCase (which is a real provider running on a temp database).  This would be more of
51 * a true "unit test".
52 *
53 * You can run this entire test case with:
54 *   runtest -c com.android.email.LegacyConversionsTests email
55 */
56@Suppress
57public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> {
58
59    Context mProviderContext;
60    Context mContext;
61
62    public LegacyConversionsTests() {
63        super(EmailProvider.class, EmailContent.AUTHORITY);
64    }
65
66    @Override
67    public void setUp() throws Exception {
68        super.setUp();
69        mProviderContext = getMockContext();
70        mContext = getContext();
71    }
72
73    /**
74     * TODO: basic Legacy -> Provider Message conversions
75     * TODO: basic Legacy -> Provider Body conversions
76     * TODO: rainy day tests of all kinds
77     */
78
79    /**
80     * Sunny day test of adding attachments from an IMAP/POP message.
81     */
82    public void brokentestAddAttachments() throws MessagingException, IOException {
83        // Prepare a local message to add the attachments to
84        final long accountId = 1;
85        final long mailboxId = 1;
86
87        // test 1: legacy message using content-type:name style for name
88        final EmailContent.Message localMessage = ProviderTestUtils.setupMessage(
89                "local-message", accountId, mailboxId, false, true, mProviderContext);
90        final Message legacyMessage = prepareLegacyMessageWithAttachments(2, false);
91        convertAndCheckcheckAddedAttachments(localMessage, legacyMessage);
92
93        // test 2: legacy message using content-disposition:filename style for name
94        final EmailContent.Message localMessage2 = ProviderTestUtils.setupMessage(
95                "local-message", accountId, mailboxId, false, true, mProviderContext);
96        final Message legacyMessage2 = prepareLegacyMessageWithAttachments(2, true);
97        convertAndCheckcheckAddedAttachments(localMessage2, legacyMessage2);
98    }
99
100    /**
101     * Helper for testAddAttachments
102     */
103    private void convertAndCheckcheckAddedAttachments(final EmailContent.Message localMessage,
104            final Message legacyMessage) throws MessagingException, IOException {
105        // Now, convert from legacy to provider and see what happens
106        ArrayList<Part> viewables = new ArrayList<Part>();
107        ArrayList<Part> attachments = new ArrayList<Part>();
108        MimeUtility.collectParts(legacyMessage, viewables, attachments);
109        LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
110
111        // Read back all attachments for message and check field values
112        Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
113        Cursor c = mProviderContext.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION,
114                null, null, null);
115        try {
116            assertEquals(2, c.getCount());
117            while (c.moveToNext()) {
118                Attachment attachment =
119                        Attachment.getContent(mProviderContext, c, Attachment.class);
120                if ("100".equals(attachment.mLocation)) {
121                    checkAttachment("attachment1Part", attachments.get(0), attachment,
122                            localMessage.mAccountKey);
123                } else if ("101".equals(attachment.mLocation)) {
124                    checkAttachment("attachment2Part", attachments.get(1), attachment,
125                            localMessage.mAccountKey);
126                } else {
127                    fail("Unexpected attachment with location " + attachment.mLocation);
128                }
129            }
130        } finally {
131            c.close();
132        }
133    }
134
135    /**
136     * Test that only "attachment" or "inline" attachments are captured and added.
137     * @throws MessagingException
138     * @throws IOException
139     */
140    public void brokentestAttachmentDispositions() throws MessagingException, IOException {
141        // Prepare a local message to add the attachments to
142        final long accountId = 1;
143        final long mailboxId = 1;
144
145        // Prepare the three attachments we want to test
146        BodyPart[] sourceAttachments = new BodyPart[3];
147        BodyPart attachmentPart;
148
149        // 1. Standard attachment
150        attachmentPart = MessageTestUtils.bodyPart("image/jpg", null);
151        attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "image/jpg");
152        attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
153        attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
154                "attachment;\n filename=\"file-1\";\n size=100");
155        attachmentPart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "100");
156        sourceAttachments[0] = attachmentPart;
157
158        // 2. Inline attachment
159        attachmentPart = MessageTestUtils.bodyPart("image/gif", null);
160        attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "image/gif");
161        attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
162        attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
163                "inline;\n filename=\"file-2\";\n size=200");
164        attachmentPart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "101");
165        sourceAttachments[1] = attachmentPart;
166
167        // 3. Neither (use VCALENDAR)
168        attachmentPart = MessageTestUtils.bodyPart("text/calendar", null);
169        attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE,
170                "text/calendar; charset=UTF-8; method=REQUEST");
171        attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "7bit");
172        attachmentPart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "102");
173        sourceAttachments[2] = attachmentPart;
174
175        // Prepare local message (destination) and legacy message w/attachments (source)
176        final EmailContent.Message localMessage = ProviderTestUtils.setupMessage(
177                "local-message", accountId, mailboxId, false, true, mProviderContext);
178        final Message legacyMessage = prepareLegacyMessageWithAttachments(sourceAttachments);
179        convertAndCheckcheckAddedAttachments(localMessage, legacyMessage);
180
181        // Run the conversion and check for the converted attachments - this test asserts
182        // that there are two attachments numbered 100 & 101 (so will fail if it finds 102)
183        convertAndCheckcheckAddedAttachments(localMessage, legacyMessage);
184    }
185
186    /**
187     * Test that attachments aren't re-added in the DB.  This supports the "partial download"
188     * nature of POP messages.
189     */
190    public void brokentestAddDuplicateAttachments() throws MessagingException, IOException {
191        // Prepare a local message to add the attachments to
192        final long accountId = 1;
193        final long mailboxId = 1;
194        final EmailContent.Message localMessage = ProviderTestUtils.setupMessage(
195                "local-message", accountId, mailboxId, false, true, mProviderContext);
196
197        // Prepare a legacy message with attachments
198        Message legacyMessage = prepareLegacyMessageWithAttachments(2, false);
199
200        // Now, convert from legacy to provider and see what happens
201        ArrayList<Part> viewables = new ArrayList<Part>();
202        ArrayList<Part> attachments = new ArrayList<Part>();
203        MimeUtility.collectParts(legacyMessage, viewables, attachments);
204        LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
205
206        // Confirm two attachment objects created
207        Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId);
208        assertEquals(2, EmailContent.count(mProviderContext, uri, null, null));
209
210        // Now add the attachments again and confirm there are still only two
211        LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
212        assertEquals(2, EmailContent.count(mProviderContext, uri, null, null));
213
214        // Now add a 3rd & 4th attachment and make sure the total is 4, not 2 or 6
215        legacyMessage = prepareLegacyMessageWithAttachments(4, false);
216        viewables = new ArrayList<Part>();
217        attachments = new ArrayList<Part>();
218        MimeUtility.collectParts(legacyMessage, viewables, attachments);
219        LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments);
220        assertEquals(4, EmailContent.count(mProviderContext, uri, null, null));
221    }
222
223    /**
224     * Prepare a legacy message with 1+ attachments
225     * @param numAttachments how many attachments to add
226     * @param filenameInDisposition False: attachment names are sent as content-type:name.  True:
227     *          attachment names are sent as content-disposition:filename.
228     */
229    private Message prepareLegacyMessageWithAttachments(int numAttachments,
230            boolean filenameInDisposition) throws MessagingException {
231        BodyPart[] attachmentParts = new BodyPart[numAttachments];
232        for (int i = 0; i < numAttachments; ++i) {
233            // construct parameter parts for content-type:name or content-disposition:filename.
234            String name = "";
235            String filename = "";
236            String quotedName = "\"test-attachment-" + i + "\"";
237            if (filenameInDisposition) {
238                filename = ";\n filename=" + quotedName;
239            } else {
240                name = ";\n name=" + quotedName;
241            }
242
243            // generate an attachment that came from a server
244            BodyPart attachmentPart = MessageTestUtils.bodyPart("image/jpg", null);
245
246            // name=attachmentN size=N00 location=10N
247            attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "image/jpg" + name);
248            attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
249            attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
250                    "attachment" + filename +  ";\n size=" + (i+1) + "00");
251            attachmentPart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "10" + i);
252
253            attachmentParts[i] = attachmentPart;
254        }
255
256        return prepareLegacyMessageWithAttachments(attachmentParts);
257    }
258
259    /**
260     * Prepare a legacy message with 1+ attachments
261     * @param attachments array containing one or more attachments
262     */
263    private Message prepareLegacyMessageWithAttachments(BodyPart[] attachments)
264            throws MessagingException {
265        // Build the multipart that holds the attachments
266        MultipartBuilder mpBuilder = new MultipartBuilder("multipart/mixed");
267        for (int i = 0; i < attachments.length; ++i) {
268            mpBuilder.addBodyPart(attachments[i]);
269        }
270
271        // Now build a message with them
272        final Message legacyMessage = new MessageBuilder()
273            .setBody(new MultipartBuilder("multipart/mixed")
274                     .addBodyPart(MessageTestUtils.bodyPart("text/html", null))
275                     .addBodyPart(mpBuilder.buildBodyPart())
276                     .build())
277                .build();
278
279        return legacyMessage;
280    }
281
282    /**
283     * Compare attachment that was converted from Part (expected) to Provider Attachment (actual)
284     *
285     * TODO content URI should only be set if we also saved a file
286     * TODO other data encodings
287     */
288    private void checkAttachment(String tag, Part expected, EmailContent.Attachment actual,
289            long accountKey) throws MessagingException {
290        String contentType = MimeUtility.unfoldAndDecode(expected.getContentType());
291        String contentTypeName = MimeUtility.getHeaderParameter(contentType, "name");
292        assertEquals(tag, expected.getMimeType(), actual.mMimeType);
293        String disposition = expected.getDisposition();
294        String sizeString = MimeUtility.getHeaderParameter(disposition, "size");
295        String dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename");
296        long expectedSize = (sizeString != null) ? Long.parseLong(sizeString) : 0;
297        assertEquals(tag, expectedSize, actual.mSize);
298        assertEquals(tag, expected.getContentId(), actual.mContentId);
299
300        // filename is either content-type:name or content-disposition:filename
301        String expectedName = (contentTypeName != null) ? contentTypeName : dispositionFilename;
302        assertEquals(tag, expectedName, actual.mFileName);
303
304        // content URI should be null
305        assertNull(tag, actual.getContentUri());
306
307        assertTrue(tag, 0 != actual.mMessageKey);
308
309        // location is either both null or both matching
310        String expectedPartId = null;
311        String[] storeData = expected.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
312        if (storeData != null && storeData.length > 0) {
313            expectedPartId = storeData[0];
314        }
315        assertEquals(tag, expectedPartId, actual.mLocation);
316        assertEquals(tag, "B", actual.mEncoding);
317        assertEquals(tag, accountKey, actual.mAccountKey);
318    }
319
320    /**
321     * TODO: Sunny day test of adding attachments from a POP message.
322     */
323
324    /**
325     * Sunny day tests of converting an original message to a legacy message
326     */
327    public void brokentestMakeLegacyMessage() throws MessagingException {
328        // Set up and store a message in the provider
329        long account1Id = 1;
330        long mailbox1Id = 1;
331
332        // Test message 1: No body
333        EmailContent.Message localMessage1 = ProviderTestUtils.setupMessage("make-legacy",
334                account1Id, mailbox1Id, false, true, mProviderContext);
335        Message getMessage1 = LegacyConversions.makeMessage(mProviderContext, localMessage1);
336        checkLegacyMessage("no body", localMessage1, getMessage1);
337
338        // Test message 2: Simple body
339        EmailContent.Message localMessage2 = ProviderTestUtils.setupMessage("make-legacy",
340                account1Id, mailbox1Id, true, false, mProviderContext);
341        localMessage2.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
342        localMessage2.save(mProviderContext);
343        Message getMessage2 = LegacyConversions.makeMessage(mProviderContext, localMessage2);
344        checkLegacyMessage("simple body", localMessage2, getMessage2);
345
346        // Test message 3: Body + replied-to text
347        EmailContent.Message localMessage3 = ProviderTestUtils.setupMessage("make-legacy",
348                account1Id, mailbox1Id, true, false, mProviderContext);
349        localMessage3.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
350        localMessage3.mFlags |= EmailContent.Message.FLAG_TYPE_REPLY;
351        localMessage3.save(mProviderContext);
352        Message getMessage3 = LegacyConversions.makeMessage(mProviderContext, localMessage3);
353        checkLegacyMessage("reply-to", localMessage3, getMessage3);
354
355        // Test message 4: Body + forwarded text
356        EmailContent.Message localMessage4 = ProviderTestUtils.setupMessage("make-legacy",
357                account1Id, mailbox1Id, true, false, mProviderContext);
358        localMessage4.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK;
359        localMessage4.mFlags |= EmailContent.Message.FLAG_TYPE_FORWARD;
360        localMessage4.save(mProviderContext);
361        Message getMessage4 = LegacyConversions.makeMessage(mProviderContext, localMessage4);
362        checkLegacyMessage("forwarding", localMessage4, getMessage4);
363    }
364
365    /**
366     * Check equality of a pair of converted messages
367     */
368    private void checkLegacyMessage(String tag, EmailContent.Message expect, Message actual)
369            throws MessagingException {
370        assertEquals(tag, expect.mServerId, actual.getUid());
371        assertEquals(tag, expect.mServerTimeStamp, actual.getInternalDate().getTime());
372        assertEquals(tag, expect.mSubject, actual.getSubject());
373        assertEquals(tag, expect.mFrom, Address.toHeader(actual.getFrom()));
374        assertEquals(tag, expect.mTimeStamp, actual.getSentDate().getTime());
375        assertEquals(tag, expect.mTo, Address.toHeader(actual.getRecipients(RecipientType.TO)));
376        assertEquals(tag, expect.mCc, Address.toHeader(actual.getRecipients(RecipientType.CC)));
377        assertEquals(tag, expect.mBcc, Address.toHeader(actual.getRecipients(RecipientType.BCC)));
378        assertEquals(tag, expect.mReplyTo, Address.toHeader(actual.getReplyTo()));
379        assertEquals(tag, expect.mMessageId, actual.getMessageId());
380        // check flags
381        assertEquals(tag, expect.mFlagRead, actual.isSet(Flag.SEEN));
382        assertEquals(tag, expect.mFlagFavorite, actual.isSet(Flag.FLAGGED));
383
384        // Check the body of the message
385        ArrayList<Part> viewables = new ArrayList<Part>();
386        ArrayList<Part> attachments = new ArrayList<Part>();
387        MimeUtility.collectParts(actual, viewables, attachments);
388        String get1Text = null;
389        String get1Html = null;
390        for (Part viewable : viewables) {
391            String text = MimeUtility.getTextFromPart(viewable);
392            if (viewable.getMimeType().equalsIgnoreCase("text/html")) {
393                get1Html = text;
394            } else {
395                get1Text = text;
396            }
397        }
398        assertEquals(tag, expect.mText, get1Text);
399        assertEquals(tag, expect.mHtml, get1Html);
400
401        // TODO Check the attachments
402
403//      cv.put("attachment_count", attachments.size());
404    }
405}
406