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.email; 18 19 import android.content.ContentUris; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.net.Uri; 23 import android.test.ProviderTestCase2; 24 import android.test.suitebuilder.annotation.Suppress; 25 26 import com.android.email.provider.EmailProvider; 27 import com.android.email.provider.ProviderTestUtils; 28 import com.android.emailcommon.internet.MimeHeader; 29 import com.android.emailcommon.internet.MimeUtility; 30 import com.android.emailcommon.mail.Address; 31 import com.android.emailcommon.mail.BodyPart; 32 import com.android.emailcommon.mail.Flag; 33 import com.android.emailcommon.mail.Message; 34 import com.android.emailcommon.mail.Message.RecipientType; 35 import com.android.emailcommon.mail.MessageTestUtils; 36 import com.android.emailcommon.mail.MessageTestUtils.MessageBuilder; 37 import com.android.emailcommon.mail.MessageTestUtils.MultipartBuilder; 38 import com.android.emailcommon.mail.MessagingException; 39 import com.android.emailcommon.mail.Part; 40 import com.android.emailcommon.provider.EmailContent; 41 import com.android.emailcommon.provider.EmailContent.Attachment; 42 43 import java.io.IOException; 44 import 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 57 public 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