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 com.android.email.mail.Address; 20 import com.android.email.mail.BodyPart; 21 import com.android.email.mail.Flag; 22 import com.android.email.mail.Message; 23 import com.android.email.mail.MessageTestUtils; 24 import com.android.email.mail.MessagingException; 25 import com.android.email.mail.Part; 26 import com.android.email.mail.Message.RecipientType; 27 import com.android.email.mail.MessageTestUtils.MessageBuilder; 28 import com.android.email.mail.MessageTestUtils.MultipartBuilder; 29 import com.android.email.mail.internet.MimeBodyPart; 30 import com.android.email.mail.internet.MimeHeader; 31 import com.android.email.mail.internet.MimeMessage; 32 import com.android.email.mail.internet.MimeUtility; 33 import com.android.email.mail.internet.TextBody; 34 import com.android.email.provider.EmailContent; 35 import com.android.email.provider.EmailProvider; 36 import com.android.email.provider.ProviderTestUtils; 37 import com.android.email.provider.EmailContent.Attachment; 38 39 import android.content.ContentUris; 40 import android.content.Context; 41 import android.database.Cursor; 42 import android.net.Uri; 43 import android.test.ProviderTestCase2; 44 45 import java.io.IOException; 46 import java.util.ArrayList; 47 import java.util.Date; 48 49 /** 50 * Tests of the Legacy Conversions code (used by MessagingController). 51 * 52 * NOTE: It would probably make sense to rewrite this using a MockProvider, instead of the 53 * ProviderTestCase (which is a real provider running on a temp database). This would be more of 54 * a true "unit test". 55 * 56 * You can run this entire test case with: 57 * runtest -c com.android.email.LegacyConversionsTests email 58 */ 59 public class LegacyConversionsTests extends ProviderTestCase2<EmailProvider> { 60 61 private static final String UID = "UID.12345678"; 62 private static final String SENDER = "sender@android.com"; 63 private static final String RECIPIENT_TO = "recipient-to@android.com"; 64 private static final String RECIPIENT_CC = "recipient-cc@android.com"; 65 private static final String RECIPIENT_BCC = "recipient-bcc@android.com"; 66 private static final String REPLY_TO = "reply-to@android.com"; 67 private static final String SUBJECT = "This is the subject"; 68 private static final String BODY = "This is the body. This is also the body."; 69 private static final String MESSAGE_ID = "Test-Message-ID"; 70 private static final String MESSAGE_ID_2 = "Test-Message-ID-Second"; 71 72 EmailProvider mProvider; 73 Context mProviderContext; 74 Context mContext; 75 LegacyConversionsTests()76 public LegacyConversionsTests() { 77 super(EmailProvider.class, EmailProvider.EMAIL_AUTHORITY); 78 } 79 80 @Override setUp()81 public void setUp() throws Exception { 82 super.setUp(); 83 mProviderContext = getMockContext(); 84 mContext = getContext(); 85 } 86 87 @Override tearDown()88 public void tearDown() throws Exception { 89 super.tearDown(); 90 } 91 92 /** 93 * TODO: basic Legacy -> Provider Message conversions 94 * TODO: basic Legacy -> Provider Body conversions 95 * TODO: rainy day tests of all kinds 96 */ 97 98 /** 99 * Test basic conversion from Store message to Provider message 100 * 101 * TODO: Not a complete test of all fields, and some fields need special tests (e.g. flags) 102 * TODO: There are many special cases in the tested function, that need to be 103 * tested here as well. 104 */ testUpdateMessageFields()105 public void testUpdateMessageFields() throws MessagingException { 106 MimeMessage message = buildTestMessage(RECIPIENT_TO, RECIPIENT_CC, RECIPIENT_BCC, 107 REPLY_TO, SENDER, SUBJECT, null); 108 EmailContent.Message localMessage = new EmailContent.Message(); 109 110 boolean result = LegacyConversions.updateMessageFields(localMessage, message, 1, 1); 111 assertTrue(result); 112 checkProviderMessage("testUpdateMessageFields", message, localMessage); 113 } 114 115 /** 116 * Test basic conversion from Store message to Provider message, when the provider message 117 * does not have a proper message-id. 118 */ testUpdateMessageFieldsNoMessageId()119 public void testUpdateMessageFieldsNoMessageId() throws MessagingException { 120 MimeMessage message = buildTestMessage(RECIPIENT_TO, RECIPIENT_CC, RECIPIENT_BCC, 121 REPLY_TO, SENDER, SUBJECT, null); 122 EmailContent.Message localMessage = new EmailContent.Message(); 123 124 // If the source message-id is null, the target should be left as-is 125 localMessage.mMessageId = MESSAGE_ID_2; 126 message.removeHeader("Message-ID"); 127 128 boolean result = LegacyConversions.updateMessageFields(localMessage, message, 1, 1); 129 assertTrue(result); 130 assertEquals(MESSAGE_ID_2, localMessage.mMessageId); 131 } 132 133 /** 134 * Build a lightweight Store message with simple field population 135 */ buildTestMessage(String to, String cc, String bcc, String replyTo, String sender, String subject, String content)136 private MimeMessage buildTestMessage(String to, String cc, String bcc, String replyTo, 137 String sender, String subject, String content) throws MessagingException { 138 MimeMessage message = new MimeMessage(); 139 140 if (to != null) { 141 Address[] addresses = Address.parse(to); 142 message.setRecipients(RecipientType.TO, addresses); 143 } 144 if (cc != null) { 145 Address[] addresses = Address.parse(cc); 146 message.setRecipients(RecipientType.CC, addresses); 147 } 148 if (bcc != null) { 149 Address[] addresses = Address.parse(bcc); 150 message.setRecipients(RecipientType.BCC, addresses); 151 } 152 if (replyTo != null) { 153 Address[] addresses = Address.parse(replyTo); 154 message.setReplyTo(addresses); 155 } 156 if (sender != null) { 157 Address[] addresses = Address.parse(sender); 158 message.setFrom(Address.parse(sender)[0]); 159 } 160 if (subject != null) { 161 message.setSubject(subject); 162 } 163 if (content != null) { 164 TextBody body = new TextBody(content); 165 message.setBody(body); 166 } 167 168 message.setUid(UID); 169 message.setSentDate(new Date()); 170 message.setInternalDate(new Date()); 171 message.setMessageId(MESSAGE_ID); 172 return message; 173 } 174 175 /** 176 * Basic test of body parts conversion from Store message to Provider message. 177 * This tests that a null body part simply results in null text, and does not crash 178 * or return "null". 179 * 180 * TODO very incomplete, there are many permutations to be explored 181 */ testUpdateBodyFieldsNullText()182 public void testUpdateBodyFieldsNullText() throws MessagingException { 183 EmailContent.Body localBody = new EmailContent.Body(); 184 EmailContent.Message localMessage = new EmailContent.Message(); 185 ArrayList<Part> viewables = new ArrayList<Part>(); 186 Part emptyTextPart = new MimeBodyPart(null, "text/plain"); 187 viewables.add(emptyTextPart); 188 189 // a "null" body part of type text/plain should result in a null mTextContent 190 boolean result = LegacyConversions.updateBodyFields(localBody, localMessage, viewables); 191 assertTrue(result); 192 assertNull(localBody.mTextContent); 193 } 194 195 /** 196 * Sunny day test of adding attachments from an IMAP message. 197 */ testAddAttachments()198 public void testAddAttachments() throws MessagingException, IOException { 199 // Prepare a local message to add the attachments to 200 final long accountId = 1; 201 final long mailboxId = 1; 202 final EmailContent.Message localMessage = ProviderTestUtils.setupMessage( 203 "local-message", accountId, mailboxId, false, true, mProviderContext); 204 205 // Prepare a legacy message with attachments 206 final Message legacyMessage = prepareLegacyMessageWithAttachments(2); 207 208 // Now, convert from legacy to provider and see what happens 209 ArrayList<Part> viewables = new ArrayList<Part>(); 210 ArrayList<Part> attachments = new ArrayList<Part>(); 211 MimeUtility.collectParts(legacyMessage, viewables, attachments); 212 LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments); 213 214 // Read back all attachments for message and check field values 215 Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId); 216 Cursor c = mProviderContext.getContentResolver().query(uri, Attachment.CONTENT_PROJECTION, 217 null, null, null); 218 try { 219 assertEquals(2, c.getCount()); 220 while (c.moveToNext()) { 221 Attachment attachment = Attachment.getContent(c, Attachment.class); 222 if ("101".equals(attachment.mLocation)) { 223 checkAttachment("attachment1Part", attachments.get(0), attachment); 224 } else if ("102".equals(attachment.mLocation)) { 225 checkAttachment("attachment2Part", attachments.get(1), attachment); 226 } else { 227 fail("Unexpected attachment with location " + attachment.mLocation); 228 } 229 } 230 } finally { 231 c.close(); 232 } 233 } 234 235 /** 236 * Test that attachments aren't re-added in the DB. This supports the "partial download" 237 * nature of POP messages. 238 */ testAddDuplicateAttachments()239 public void testAddDuplicateAttachments() throws MessagingException, IOException { 240 // Prepare a local message to add the attachments to 241 final long accountId = 1; 242 final long mailboxId = 1; 243 final EmailContent.Message localMessage = ProviderTestUtils.setupMessage( 244 "local-message", accountId, mailboxId, false, true, mProviderContext); 245 246 // Prepare a legacy message with attachments 247 Message legacyMessage = prepareLegacyMessageWithAttachments(2); 248 249 // Now, convert from legacy to provider and see what happens 250 ArrayList<Part> viewables = new ArrayList<Part>(); 251 ArrayList<Part> attachments = new ArrayList<Part>(); 252 MimeUtility.collectParts(legacyMessage, viewables, attachments); 253 LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments); 254 255 // Confirm two attachment objects created 256 Uri uri = ContentUris.withAppendedId(Attachment.MESSAGE_ID_URI, localMessage.mId); 257 assertEquals(2, EmailContent.count(mProviderContext, uri, null, null)); 258 259 // Now add the attachments again and confirm there are still only two 260 LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments); 261 assertEquals(2, EmailContent.count(mProviderContext, uri, null, null)); 262 263 // Now add a 3rd & 4th attachment and make sure the total is 4, not 2 or 6 264 legacyMessage = prepareLegacyMessageWithAttachments(4); 265 viewables = new ArrayList<Part>(); 266 attachments = new ArrayList<Part>(); 267 MimeUtility.collectParts(legacyMessage, viewables, attachments); 268 LegacyConversions.updateAttachments(mProviderContext, localMessage, attachments); 269 assertEquals(4, EmailContent.count(mProviderContext, uri, null, null)); 270 } 271 272 /** 273 * Prepare a legacy message with 1+ attachments 274 */ prepareLegacyMessageWithAttachments(int numAttachments)275 private static Message prepareLegacyMessageWithAttachments(int numAttachments) 276 throws MessagingException { 277 // First, build one or more attachment parts 278 MultipartBuilder mpBuilder = new MultipartBuilder("multipart/mixed"); 279 for (int i = 1; i <= numAttachments; ++i) { 280 BodyPart attachmentPart = MessageTestUtils.bodyPart("image/jpg", null); 281 282 // name=attachmentN size=N00 location=10N 283 attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, 284 "image/jpg;\n name=\"attachment" + i + "\""); 285 attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); 286 attachmentPart.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, 287 "attachment;\n filename=\"attachment2\";\n size=" + i + "00"); 288 attachmentPart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, "10" + i); 289 290 mpBuilder.addBodyPart(attachmentPart); 291 } 292 293 // Now build a message with them 294 final Message legacyMessage = new MessageBuilder() 295 .setBody(new MultipartBuilder("multipart/mixed") 296 .addBodyPart(MessageTestUtils.bodyPart("text/html", null)) 297 .addBodyPart(mpBuilder.buildBodyPart()) 298 .build()) 299 .build(); 300 301 return legacyMessage; 302 } 303 304 /** 305 * Test the stringInequal helper 306 */ testStringInequal()307 public void testStringInequal() { 308 // Pairs that are "equal" 309 assertFalse(LegacyConversions.stringNotEqual(null, null)); 310 assertFalse(LegacyConversions.stringNotEqual(null, "")); 311 assertFalse(LegacyConversions.stringNotEqual("", null)); 312 assertFalse(LegacyConversions.stringNotEqual("", "")); 313 assertFalse(LegacyConversions.stringNotEqual("string-equal", "string-equal")); 314 // Pairs that are "inequal" 315 assertTrue(LegacyConversions.stringNotEqual(null, "string-inequal")); 316 assertTrue(LegacyConversions.stringNotEqual("", "string-inequal")); 317 assertTrue(LegacyConversions.stringNotEqual("string-inequal", null)); 318 assertTrue(LegacyConversions.stringNotEqual("string-inequal", "")); 319 assertTrue(LegacyConversions.stringNotEqual("string-inequal-a", "string-inequal-b")); 320 } 321 322 /** 323 * Compare attachment that was converted from Part (expected) to Provider Attachment (actual) 324 * 325 * TODO content URI should only be set if we also saved a file 326 * TODO other data encodings 327 */ checkAttachment(String tag, Part expected, EmailContent.Attachment actual)328 private void checkAttachment(String tag, Part expected, EmailContent.Attachment actual) 329 throws MessagingException { 330 String contentType = MimeUtility.unfoldAndDecode(expected.getContentType()); 331 String expectedName = MimeUtility.getHeaderParameter(contentType, "name"); 332 assertEquals(tag, expectedName, actual.mFileName); 333 assertEquals(tag, expected.getMimeType(), actual.mMimeType); 334 String disposition = expected.getDisposition(); 335 String sizeString = MimeUtility.getHeaderParameter(disposition, "size"); 336 long expectedSize = Long.parseLong(sizeString); 337 assertEquals(tag, expectedSize, actual.mSize); 338 assertEquals(tag, expected.getContentId(), actual.mContentId); 339 assertNull(tag, actual.mContentUri); 340 assertTrue(tag, 0 != actual.mMessageKey); 341 String expectedPartId = 342 expected.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA)[0]; 343 assertEquals(tag, expectedPartId, actual.mLocation); 344 assertEquals(tag, "B", actual.mEncoding); 345 } 346 347 /** 348 * TODO: Sunny day test of adding attachments from a POP message. 349 */ 350 351 /** 352 * Sunny day tests of converting an original message to a legacy message 353 */ testMakeLegacyMessage()354 public void testMakeLegacyMessage() throws MessagingException { 355 // Set up and store a message in the provider 356 long account1Id = 1; 357 long mailbox1Id = 1; 358 359 // Test message 1: No body 360 EmailContent.Message localMessage1 = ProviderTestUtils.setupMessage("make-legacy", 361 account1Id, mailbox1Id, false, true, mProviderContext); 362 Message getMessage1 = LegacyConversions.makeMessage(mProviderContext, localMessage1); 363 checkLegacyMessage("no body", localMessage1, getMessage1); 364 365 // Test message 2: Simple body 366 EmailContent.Message localMessage2 = ProviderTestUtils.setupMessage("make-legacy", 367 account1Id, mailbox1Id, true, false, mProviderContext); 368 localMessage2.mTextReply = null; 369 localMessage2.mHtmlReply = null; 370 localMessage2.mIntroText = null; 371 localMessage2.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; 372 localMessage2.save(mProviderContext); 373 Message getMessage2 = LegacyConversions.makeMessage(mProviderContext, localMessage2); 374 checkLegacyMessage("simple body", localMessage2, getMessage2); 375 376 // Test message 3: Body + replied-to text 377 EmailContent.Message localMessage3 = ProviderTestUtils.setupMessage("make-legacy", 378 account1Id, mailbox1Id, true, false, mProviderContext); 379 localMessage3.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; 380 localMessage3.mFlags |= EmailContent.Message.FLAG_TYPE_REPLY; 381 localMessage3.save(mProviderContext); 382 Message getMessage3 = LegacyConversions.makeMessage(mProviderContext, localMessage3); 383 checkLegacyMessage("reply-to", localMessage3, getMessage3); 384 385 // Test message 4: Body + forwarded text 386 EmailContent.Message localMessage4 = ProviderTestUtils.setupMessage("make-legacy", 387 account1Id, mailbox1Id, true, false, mProviderContext); 388 localMessage4.mFlags &= ~EmailContent.Message.FLAG_TYPE_MASK; 389 localMessage4.mFlags |= EmailContent.Message.FLAG_TYPE_FORWARD; 390 localMessage4.save(mProviderContext); 391 Message getMessage4 = LegacyConversions.makeMessage(mProviderContext, localMessage4); 392 checkLegacyMessage("forwarding", localMessage4, getMessage4); 393 } 394 395 /** 396 * Check equality of a pair of converted messages 397 */ checkProviderMessage(String tag, Message expect, EmailContent.Message actual)398 private void checkProviderMessage(String tag, Message expect, EmailContent.Message actual) 399 throws MessagingException { 400 assertEquals(tag, expect.getUid(), actual.mServerId); 401 assertEquals(tag, expect.getSubject(), actual.mSubject); 402 assertEquals(tag, Address.pack(expect.getFrom()), actual.mFrom); 403 assertEquals(tag, expect.getSentDate().getTime(), actual.mTimeStamp); 404 assertEquals(tag, Address.pack(expect.getRecipients(RecipientType.TO)), actual.mTo); 405 assertEquals(tag, Address.pack(expect.getRecipients(RecipientType.CC)), actual.mCc); 406 assertEquals(tag, ((MimeMessage)expect).getMessageId(), actual.mMessageId); 407 assertEquals(tag, expect.isSet(Flag.SEEN), actual.mFlagRead); 408 assertEquals(tag, expect.isSet(Flag.FLAGGED), actual.mFlagFavorite); 409 } 410 411 /** 412 * Check equality of a pair of converted messages 413 */ checkLegacyMessage(String tag, EmailContent.Message expect, Message actual)414 private void checkLegacyMessage(String tag, EmailContent.Message expect, Message actual) 415 throws MessagingException { 416 assertEquals(tag, expect.mServerId, actual.getUid()); 417 assertEquals(tag, expect.mServerTimeStamp, actual.getInternalDate().getTime()); 418 assertEquals(tag, expect.mSubject, actual.getSubject()); 419 assertEquals(tag, expect.mFrom, Address.pack(actual.getFrom())); 420 assertEquals(tag, expect.mTimeStamp, actual.getSentDate().getTime()); 421 assertEquals(tag, expect.mTo, Address.pack(actual.getRecipients(RecipientType.TO))); 422 assertEquals(tag, expect.mCc, Address.pack(actual.getRecipients(RecipientType.CC))); 423 assertEquals(tag, expect.mBcc, Address.pack(actual.getRecipients(RecipientType.BCC))); 424 assertEquals(tag, expect.mReplyTo, Address.pack(actual.getReplyTo())); 425 assertEquals(tag, expect.mMessageId, ((MimeMessage)actual).getMessageId()); 426 // check flags 427 assertEquals(tag, expect.mFlagRead, actual.isSet(Flag.SEEN)); 428 assertEquals(tag, expect.mFlagFavorite, actual.isSet(Flag.FLAGGED)); 429 430 // Check the body of the message 431 ArrayList<Part> viewables = new ArrayList<Part>(); 432 ArrayList<Part> attachments = new ArrayList<Part>(); 433 MimeUtility.collectParts(actual, viewables, attachments); 434 String get1Text = null; 435 String get1Html = null; 436 String get1TextReply = null; 437 String get1HtmlReply = null; 438 String get1TextIntro = null; 439 for (Part viewable : viewables) { 440 String text = MimeUtility.getTextFromPart(viewable); 441 boolean isHtml = viewable.getMimeType().equalsIgnoreCase("text/html"); 442 String[] headers = viewable.getHeader(MimeHeader.HEADER_ANDROID_BODY_QUOTED_PART); 443 if (headers != null) { 444 String header = headers[0]; 445 boolean isReply = LegacyConversions.BODY_QUOTED_PART_REPLY.equalsIgnoreCase(header); 446 boolean isFwd = LegacyConversions.BODY_QUOTED_PART_FORWARD.equalsIgnoreCase(header); 447 boolean isIntro = LegacyConversions.BODY_QUOTED_PART_INTRO.equalsIgnoreCase(header); 448 if (isReply || isFwd) { 449 if (isHtml) { 450 get1HtmlReply = text; 451 } else { 452 get1TextReply = text; 453 } 454 } else if (isIntro) { 455 get1TextIntro = text; 456 } 457 // Check flags 458 int replyTypeFlags = expect.mFlags & EmailContent.Message.FLAG_TYPE_MASK; 459 if (isReply) { 460 assertEquals(tag, EmailContent.Message.FLAG_TYPE_REPLY, replyTypeFlags); 461 } 462 if (isFwd) { 463 assertEquals(tag, EmailContent.Message.FLAG_TYPE_FORWARD, replyTypeFlags); 464 } 465 } else { 466 if (isHtml) { 467 get1Html = text; 468 } else { 469 get1Text = text; 470 } 471 } 472 } 473 assertEquals(tag, expect.mText, get1Text); 474 assertEquals(tag, expect.mHtml, get1Html); 475 assertEquals(tag, expect.mTextReply, get1TextReply); 476 assertEquals(tag, expect.mHtmlReply, get1HtmlReply); 477 assertEquals(tag, expect.mIntroText, get1TextIntro); 478 479 // TODO Check the attachments 480 481 // cv.put("attachment_count", attachments.size()); 482 } 483 } 484