1 /* 2 * Copyright (C) 2008 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.mail.store; 18 19 import com.android.email.Email; 20 import com.android.email.MockVendorPolicy; 21 import com.android.email.Utility; 22 import com.android.email.VendorPolicyLoader; 23 import com.android.email.mail.Address; 24 import com.android.email.mail.AuthenticationFailedException; 25 import com.android.email.mail.Body; 26 import com.android.email.mail.FetchProfile; 27 import com.android.email.mail.Flag; 28 import com.android.email.mail.Folder; 29 import com.android.email.mail.Message; 30 import com.android.email.mail.MessagingException; 31 import com.android.email.mail.Part; 32 import com.android.email.mail.Transport; 33 import com.android.email.mail.Folder.FolderType; 34 import com.android.email.mail.Folder.OpenMode; 35 import com.android.email.mail.Message.RecipientType; 36 import com.android.email.mail.internet.MimeBodyPart; 37 import com.android.email.mail.internet.MimeMultipart; 38 import com.android.email.mail.internet.MimeUtility; 39 import com.android.email.mail.internet.TextBody; 40 import com.android.email.mail.store.ImapStore.ImapConnection; 41 import com.android.email.mail.store.ImapStore.ImapMessage; 42 import com.android.email.mail.store.imap.ImapResponse; 43 import com.android.email.mail.store.imap.ImapTestUtils; 44 import com.android.email.mail.transport.MockTransport; 45 46 import org.apache.commons.io.IOUtils; 47 48 import android.os.Bundle; 49 import android.test.AndroidTestCase; 50 import android.test.MoreAsserts; 51 import android.test.suitebuilder.annotation.SmallTest; 52 53 import java.io.IOException; 54 import java.util.ArrayList; 55 import java.util.HashMap; 56 import java.util.regex.Pattern; 57 58 /** 59 * This is a series of unit tests for the ImapStore class. These tests must be locally 60 * complete - no server(s) required. 61 * 62 * To run these tests alone, use: 63 * $ runtest -c com.android.email.mail.store.ImapStoreUnitTests email 64 * 65 * TODO Check if callback is really called 66 * TODO test for BAD response in various places? 67 * TODO test for BYE response in various places? 68 */ 69 @SmallTest 70 public class ImapStoreUnitTests extends AndroidTestCase { 71 private final static String[] NO_REPLY = new String[0]; 72 73 /** 74 * Default folder name. In order to test for encoding, we use a non-ascii name. 75 */ 76 private final static String FOLDER_NAME = "\u65E5"; 77 78 /** 79 * Folder name encoded in UTF-7. 80 */ 81 private final static String FOLDER_ENCODED = "&ZeU-"; 82 83 private static ImapResponse CAPABILITY_RESPONSE = ImapTestUtils.parseResponse( 84 "* CAPABILITY IMAP4rev1 STARTTLS"); 85 86 /* These values are provided by setUp() */ 87 private ImapStore mStore = null; 88 private ImapStore.ImapFolder mFolder = null; 89 90 private int mNextTag; 91 92 /** 93 * Setup code. We generate a lightweight ImapStore and ImapStore.ImapFolder. 94 */ 95 @Override 96 protected void setUp() throws Exception { 97 super.setUp(); 98 Email.setTempDirectory(getContext()); 99 100 // These are needed so we can get at the inner classes 101 mStore = (ImapStore) ImapStore.newInstance("imap://user:password@server:999", 102 getContext(), null); 103 mFolder = (ImapStore.ImapFolder) mStore.getFolder(FOLDER_NAME); 104 mNextTag = 1; 105 } 106 107 public void testJoinMessageUids() throws Exception { 108 assertEquals("", ImapStore.joinMessageUids(new Message[] {})); 109 assertEquals("a", ImapStore.joinMessageUids(new Message[] { 110 mFolder.createMessage("a") 111 })); 112 assertEquals("a,XX", ImapStore.joinMessageUids(new Message[] { 113 mFolder.createMessage("a"), 114 mFolder.createMessage("XX"), 115 })); 116 } 117 118 /** 119 * Confirms simple non-SSL non-TLS login 120 */ 121 public void testSimpleLogin() throws MessagingException { 122 123 MockTransport mockTransport = openAndInjectMockTransport(); 124 125 // try to open it 126 setupOpenFolder(mockTransport); 127 mFolder.open(OpenMode.READ_WRITE, null); 128 129 // TODO: inject specific facts in the initial folder SELECT and check them here 130 } 131 132 public void testLoginFailure() throws Exception { 133 MockTransport mockTransport = openAndInjectMockTransport(); 134 expectLogin(mockTransport, new String[] {"* iD nIL", "oK"}, "nO authentication failed"); 135 136 try { 137 mStore.getConnection().open(); 138 fail("Didn't throw AuthenticationFailedException"); 139 } catch (AuthenticationFailedException expected) { 140 } 141 } 142 143 /** 144 * TODO: Test with SSL negotiation (faked) 145 * TODO: Test with SSL required but not supported 146 * TODO: Test with TLS negotiation (faked) 147 * TODO: Test with TLS required but not supported 148 */ 149 150 /** 151 * Test the generation of the IMAP ID keys 152 */ 153 public void testImapIdBasic() { 154 // First test looks at operation of the outer API - we don't control any of the 155 // values; Just look for basic results. 156 157 // Strings we'll expect to find: 158 // name Android package name of the program 159 // os "android" 160 // os-version "version; build-id" 161 // vendor Vendor of the client/server 162 // x-android-device-model Model (Optional, so not tested here) 163 // x-android-net-operator Carrier (Unreliable, so not tested here) 164 // AGUID A device+account UID 165 String id = ImapStore.getImapId(getContext(), "user-name", "host-name", 166 CAPABILITY_RESPONSE); 167 HashMap<String, String> map = tokenizeImapId(id); 168 assertEquals(getContext().getPackageName(), map.get("name")); 169 assertEquals("android", map.get("os")); 170 assertNotNull(map.get("os-version")); 171 assertNotNull(map.get("vendor")); 172 assertNotNull(map.get("AGUID")); 173 174 // Next, use the inner API to confirm operation of a couple of 175 // variants for release and non-release devices. 176 177 // simple API check - non-REL codename, non-empty version 178 id = ImapStore.makeCommonImapId("packageName", "version", "codeName", 179 "model", "id", "vendor", "network-operator"); 180 map = tokenizeImapId(id); 181 assertEquals("packageName", map.get("name")); 182 assertEquals("android", map.get("os")); 183 assertEquals("version; id", map.get("os-version")); 184 assertEquals("vendor", map.get("vendor")); 185 assertEquals(null, map.get("x-android-device-model")); 186 assertEquals("network-operator", map.get("x-android-mobile-net-operator")); 187 assertEquals(null, map.get("AGUID")); 188 189 // simple API check - codename is REL, so use model name. 190 // also test empty version => 1.0 and empty network operator 191 id = ImapStore.makeCommonImapId("packageName", "", "REL", 192 "model", "id", "vendor", ""); 193 map = tokenizeImapId(id); 194 assertEquals("packageName", map.get("name")); 195 assertEquals("android", map.get("os")); 196 assertEquals("1.0; id", map.get("os-version")); 197 assertEquals("vendor", map.get("vendor")); 198 assertEquals("model", map.get("x-android-device-model")); 199 assertEquals(null, map.get("x-android-mobile-net-operator")); 200 assertEquals(null, map.get("AGUID")); 201 } 202 203 /** 204 * Test for the interaction between {@link ImapStore#getImapId} and a vendor policy. 205 */ 206 public void testImapIdWithVendorPolicy() { 207 try { 208 MockVendorPolicy.inject(getContext()); 209 210 // Prepare mock result 211 Bundle result = new Bundle(); 212 result.putString("getImapId", "\"test-key\" \"test-value\""); 213 MockVendorPolicy.mockResult = result; 214 215 // Invoke 216 String id = ImapStore.getImapId(getContext(), "user-name", "host-name", 217 ImapTestUtils.parseResponse("* CAPABILITY IMAP4rev1 XXX YYY Z")); 218 219 // Check the result 220 assertEquals("test-value", tokenizeImapId(id).get("test-key")); 221 222 // Verify what's passed to the policy 223 assertEquals("getImapId", MockVendorPolicy.passedPolicy); 224 assertEquals("user-name", MockVendorPolicy.passedBundle.getString("getImapId.user")); 225 assertEquals("host-name", MockVendorPolicy.passedBundle.getString("getImapId.host")); 226 assertEquals("[CAPABILITY,IMAP4rev1,XXX,YYY,Z]", 227 MockVendorPolicy.passedBundle.getString("getImapId.capabilities")); 228 } finally { 229 VendorPolicyLoader.clearInstanceForTest(); 230 } 231 } 232 233 /** 234 * Test of the internal generator for IMAP ID strings, specifically looking for proper 235 * filtering of illegal values. This is required because we cannot necessarily trust 236 * the external sources of some of this data (e.g. release labels). 237 * 238 * The (somewhat arbitrary) legal values are: a-z A-Z 0-9 - _ + = ; : . , / <space> 239 * The most important goal of the filters is to keep out control chars, (, ), and " 240 */ 241 public void testImapIdFiltering() { 242 String id = ImapStore.makeCommonImapId( 243 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", 244 "0123456789", "codeName", 245 "model", "-_+=;:.,// ", 246 "v(e)n\"d\ro\nr", // look for bad chars stripped out, leaving OK chars 247 "()\""); // look for bad chars stripped out, leaving nothing 248 HashMap<String, String> map = tokenizeImapId(id); 249 250 assertEquals("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", map.get("name")); 251 assertEquals("0123456789; -_+=;:.,// ", map.get("os-version")); 252 assertEquals("vendor", map.get("vendor")); 253 assertNull(map.get("x-android-mobile-net-operator")); 254 } 255 256 /** 257 * Test that IMAP ID uid's are per-username 258 */ 259 public void testImapIdDeviceId() throws MessagingException { 260 ImapStore store1a = (ImapStore) ImapStore.newInstance("imap://user1:password@server:999", 261 getContext(), null); 262 ImapStore store1b = (ImapStore) ImapStore.newInstance("imap://user1:password@server:999", 263 getContext(), null); 264 ImapStore store2 = (ImapStore) ImapStore.newInstance("imap://user2:password@server:999", 265 getContext(), null); 266 267 String id1a = ImapStore.getImapId(getContext(), "user1", "host-name", CAPABILITY_RESPONSE); 268 String id1b = ImapStore.getImapId(getContext(), "user1", "host-name", CAPABILITY_RESPONSE); 269 String id2 = ImapStore.getImapId(getContext(), "user2", "host-name", CAPABILITY_RESPONSE); 270 271 String uid1a = tokenizeImapId(id1a).get("AGUID"); 272 String uid1b = tokenizeImapId(id1b).get("AGUID"); 273 String uid2 = tokenizeImapId(id2).get("AGUID"); 274 275 assertEquals(uid1a, uid1b); 276 MoreAsserts.assertNotEqual(uid1a, uid2); 277 } 278 279 /** 280 * Helper to break an IMAP ID string into keys & values 281 * @param id the IMAP Id string (the part inside the parens) 282 * @return a map of key/value pairs 283 */ 284 private HashMap<String, String> tokenizeImapId(String id) { 285 // Instead of a true tokenizer, we'll use double-quote as the split. 286 // We can's use " " because there may be spaces inside the values. 287 String[] elements = id.split("\""); 288 HashMap<String, String> map = new HashMap<String, String>(); 289 for (int i = 0; i < elements.length; ) { 290 // Because we split at quotes, we expect to find: 291 // [i] = null or one or more spaces 292 // [i+1] = key 293 // [i+2] = one or more spaces 294 // [i+3] = value 295 map.put(elements[i+1], elements[i+3]); 296 i += 4; 297 } 298 return map; 299 } 300 301 /** 302 * Test non-NIL server response to IMAP ID. We should simply ignore it. 303 */ 304 public void testServerId() throws MessagingException { 305 MockTransport mockTransport = openAndInjectMockTransport(); 306 307 // try to open it 308 setupOpenFolder(mockTransport, new String[] { 309 "* ID (\"name\" \"Cyrus\" \"version\" \"1.5\"" + 310 " \"os\" \"sunos\" \"os-version\" \"5.5\"" + 311 " \"support-url\" \"mailto:cyrus-bugs+@andrew.cmu.edu\")", 312 "oK"}, "rEAD-wRITE"); 313 mFolder.open(OpenMode.READ_WRITE, null); 314 } 315 316 /** 317 * Test OK response to IMAP ID with crummy text afterwards too. 318 */ 319 public void testImapIdOkParsing() throws MessagingException { 320 MockTransport mockTransport = openAndInjectMockTransport(); 321 322 // try to open it 323 setupOpenFolder(mockTransport, new String[] { 324 "* iD nIL", 325 "oK [iD] bad-char-%"}, "rEAD-wRITE"); 326 mFolder.open(OpenMode.READ_WRITE, null); 327 } 328 329 /** 330 * Test BAD response to IMAP ID - also with bad parser chars 331 */ 332 public void testImapIdBad() throws MessagingException { 333 MockTransport mockTransport = openAndInjectMockTransport(); 334 335 // try to open it 336 setupOpenFolder(mockTransport, new String[] { 337 "bAD unknown command bad-char-%"}, "rEAD-wRITE"); 338 mFolder.open(OpenMode.READ_WRITE, null); 339 } 340 341 /** 342 * Test small Folder functions that don't really do anything in Imap 343 */ 344 public void testSmallFolderFunctions() throws MessagingException { 345 // getPermanentFlags() returns { Flag.DELETED, Flag.SEEN, Flag.FLAGGED } 346 Flag[] flags = mFolder.getPermanentFlags(); 347 assertEquals(3, flags.length); 348 // TODO: Write flags into hashset and compare them to a hashset and compare them 349 assertEquals(Flag.DELETED, flags[0]); 350 assertEquals(Flag.SEEN, flags[1]); 351 assertEquals(Flag.FLAGGED, flags[2]); 352 353 // canCreate() returns true 354 assertTrue(mFolder.canCreate(FolderType.HOLDS_FOLDERS)); 355 assertTrue(mFolder.canCreate(FolderType.HOLDS_MESSAGES)); 356 } 357 358 /** 359 * Lightweight test to confirm that IMAP hasn't implemented any folder roles yet. 360 * 361 * TODO: Test this with multiple folders provided by mock server 362 * TODO: Implement XLIST and then support this 363 */ 364 public void testNoFolderRolesYet() { 365 assertEquals(Folder.FolderRole.UNKNOWN, mFolder.getRole()); 366 } 367 368 /** 369 * Lightweight test to confirm that IMAP isn't requesting structure prefetch. 370 */ 371 public void testNoStructurePrefetch() { 372 assertFalse(mStore.requireStructurePrefetch()); 373 } 374 375 /** 376 * Lightweight test to confirm that IMAP is requesting sent-message-upload. 377 * TODO: Implement Gmail-specific cases and handle this server-side 378 */ 379 public void testSentUploadRequested() { 380 assertTrue(mStore.requireCopyMessageToSentFolder()); 381 } 382 383 /** 384 * TODO: Test the process of opening and indexing a mailbox with one unread message in it. 385 */ 386 387 /** 388 * TODO: Test the scenario where the transport is "open" but not really (e.g. server closed). 389 /** 390 * Set up a basic MockTransport. open it, and inject it into mStore 391 */ 392 private MockTransport openAndInjectMockTransport() { 393 // Create mock transport and inject it into the ImapStore that's already set up 394 MockTransport mockTransport = new MockTransport(); 395 mockTransport.setSecurity(Transport.CONNECTION_SECURITY_NONE, false); 396 mockTransport.setMockHost("mock.server.com"); 397 mStore.setTransport(mockTransport); 398 return mockTransport; 399 } 400 401 /** 402 * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open() 403 * 404 * @param mockTransport the mock transport we're using 405 */ 406 private void setupOpenFolder(MockTransport mockTransport) { 407 setupOpenFolder(mockTransport, "rEAD-wRITE"); 408 } 409 410 /** 411 * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open() 412 * 413 * @param mockTransport the mock transport we're using 414 */ 415 private void setupOpenFolder(MockTransport mockTransport, String readWriteMode) { 416 setupOpenFolder(mockTransport, new String[] { 417 "* iD nIL", "oK"}, readWriteMode); 418 } 419 420 /** 421 * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open() 422 * Also allows setting a custom IMAP ID. 423 * 424 * Also sets mNextTag, an int, which is useful if there are additional commands to inject. 425 * 426 * @param mockTransport the mock transport we're using 427 * @param imapIdResponse the expected series of responses to the IMAP ID command. Non-final 428 * lines should be tagged with *. The final response should be untagged (the correct 429 * tag will be added at runtime). 430 * @param "READ-WRITE" or "READ-ONLY" 431 * @return the next tag# to use 432 */ 433 private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse, 434 String readWriteMode) { 435 expectLogin(mockTransport, imapIdResponse); 436 mockTransport.expect( 437 getNextTag(false) + " SELECT \"" + FOLDER_ENCODED + "\"", new String[] { 438 "* fLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)", 439 "* oK [pERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]", 440 "* 0 eXISTS", 441 "* 0 rECENT", 442 "* OK [uNSEEN 0]", 443 "* OK [uIDNEXT 1]", 444 getNextTag(true) + " oK [" + readWriteMode + "] " + 445 FOLDER_ENCODED + " selected. (Success)"}); 446 } 447 448 private void expectLogin(MockTransport mockTransport) { 449 expectLogin(mockTransport, new String[] {"* iD nIL", "oK"}); 450 } 451 452 private void expectLogin(MockTransport mockTransport, String[] imapIdResponse) { 453 expectLogin(mockTransport, imapIdResponse, "oK user authenticated (Success)"); 454 } 455 456 private void expectLogin(MockTransport mockTransport, String[] imapIdResponse, 457 String loginResponse) { 458 // inject boilerplate commands that match our typical login 459 mockTransport.expect(null, "* oK Imap 2000 Ready To Assist You"); 460 461 mockTransport.expect(getNextTag(false) + " CAPABILITY", new String[] { 462 "* cAPABILITY iMAP4rev1 sTARTTLS aUTH=gSSAPI lOGINDISABLED", 463 getNextTag(true) + " oK CAPABILITY completed"}); 464 465 String expectedNextTag = getNextTag(false); 466 // Fix the tag # of the ID response 467 String last = imapIdResponse[imapIdResponse.length-1]; 468 last = expectedNextTag + " " + last; 469 imapIdResponse[imapIdResponse.length-1] = last; 470 mockTransport.expect(getNextTag(false) + " ID \\(.*\\)", imapIdResponse); 471 472 getNextTag(true); // Advance the tag for ID response. 473 474 mockTransport.expect(getNextTag(false) + " LOGIN user \"password\"", 475 getNextTag(true) + " " + loginResponse); 476 } 477 478 private void expectNoop(MockTransport mockTransport, boolean ok) { 479 String response = ok ? " oK success" : " nO timeout"; 480 mockTransport.expect(getNextTag(false) + " NOOP", 481 new String[] {getNextTag(true) + response}); 482 } 483 484 /** 485 * Return a tag for use in setting up expect strings. Typically this is called in pairs, 486 * first as getNextTag(false) when emitting the command, then as getNextTag(true) when 487 * emitting the final line of the expected response. 488 * @param advance true to increment mNextTag for the subsequence command 489 * @return a string containing the current tag 490 */ 491 public String getNextTag(boolean advance) { 492 if (advance) ++mNextTag; 493 return Integer.toString(mNextTag); 494 } 495 496 /** 497 * Test that servers reporting READ-WRITE mode are parsed properly 498 * Note: the READ_WRITE mode passed to folder.open() does not affect the test 499 */ 500 public void testReadWrite() throws MessagingException { 501 MockTransport mock = openAndInjectMockTransport(); 502 setupOpenFolder(mock, "rEAD-WRITE"); 503 mFolder.open(OpenMode.READ_WRITE, null); 504 assertEquals(OpenMode.READ_WRITE, mFolder.getMode()); 505 } 506 507 /** 508 * Test that servers reporting READ-ONLY mode are parsed properly 509 * Note: the READ_ONLY mode passed to folder.open() does not affect the test 510 */ 511 public void testReadOnly() throws MessagingException { 512 MockTransport mock = openAndInjectMockTransport(); 513 setupOpenFolder(mock, "rEAD-ONLY"); 514 mFolder.open(OpenMode.READ_ONLY, null); 515 assertEquals(OpenMode.READ_ONLY, mFolder.getMode()); 516 } 517 518 /** 519 * Test for getUnreadMessageCount with quoted string in the middle of response. 520 */ 521 public void testGetUnreadMessageCountWithQuotedString() throws Exception { 522 MockTransport mock = openAndInjectMockTransport(); 523 setupOpenFolder(mock); 524 mock.expect( 525 getNextTag(false) + " STATUS \"" + FOLDER_ENCODED + "\" \\(UNSEEN\\)", 526 new String[] { 527 "* sTATUS \"" + FOLDER_ENCODED + "\" (uNSEEN 2)", 528 getNextTag(true) + " oK STATUS completed"}); 529 mFolder.open(OpenMode.READ_WRITE, null); 530 int unreadCount = mFolder.getUnreadMessageCount(); 531 assertEquals("getUnreadMessageCount with quoted string", 2, unreadCount); 532 } 533 534 /** 535 * Test for getUnreadMessageCount with literal string in the middle of response. 536 */ 537 public void testGetUnreadMessageCountWithLiteralString() throws Exception { 538 MockTransport mock = openAndInjectMockTransport(); 539 setupOpenFolder(mock); 540 mock.expect( 541 getNextTag(false) + " STATUS \"" + FOLDER_ENCODED + "\" \\(UNSEEN\\)", 542 new String[] { 543 "* sTATUS {5}", 544 FOLDER_ENCODED + " (uNSEEN 10)", 545 getNextTag(true) + " oK STATUS completed"}); 546 mFolder.open(OpenMode.READ_WRITE, null); 547 int unreadCount = mFolder.getUnreadMessageCount(); 548 assertEquals("getUnreadMessageCount with literal string", 10, unreadCount); 549 } 550 551 public void testFetchFlagEnvelope() throws MessagingException { 552 final MockTransport mock = openAndInjectMockTransport(); 553 setupOpenFolder(mock); 554 mFolder.open(OpenMode.READ_WRITE, null); 555 final Message message = mFolder.createMessage("1"); 556 557 final FetchProfile fp = new FetchProfile(); 558 fp.add(FetchProfile.Item.FLAGS); 559 fp.add(FetchProfile.Item.ENVELOPE); 560 mock.expect(getNextTag(false) + 561 " UID FETCH 1 \\(UID FLAGS INTERNALDATE RFC822\\.SIZE BODY\\.PEEK\\[HEADER.FIELDS" + 562 " \\(date subject from content-type to cc message-id\\)\\]\\)", 563 new String[] { 564 "* 9 fETCH (uID 1 rFC822.sIZE 120626 iNTERNALDATE \"17-may-2010 22:00:15 +0000\"" + 565 "fLAGS (\\Seen) bODY[hEADER.FIELDS (dAte sUbject fRom cOntent-type tO cC" + 566 " mEssage-id)]" + 567 " {279}", 568 "From: Xxxxxx Yyyyy <userxx (at) android.com>", 569 "Date: Mon, 17 May 2010 14:59:52 -0700", 570 "Message-ID: <x0000000000000000000000000000000000000000000000y (at) android.com>", 571 "Subject: ssubject", 572 "To: android.test01 (at) android.com", 573 "Content-Type: multipart/mixed; boundary=a00000000000000000000000000b", 574 "", 575 ")", 576 getNextTag(true) + " oK SUCCESS" 577 }); 578 mFolder.fetch(new Message[] { message }, fp, null); 579 580 assertEquals("android.test01 (at) android.com", message.getHeader("to")[0]); 581 assertEquals("Xxxxxx Yyyyy <userxx (at) android.com>", message.getHeader("from")[0]); 582 assertEquals("multipart/mixed; boundary=a00000000000000000000000000b", 583 message.getHeader("Content-Type")[0]); 584 assertTrue(message.isSet(Flag.SEEN)); 585 586 // TODO: Test NO response. 587 } 588 589 /** 590 * Test for fetching simple BODYSTRUCTURE. 591 */ 592 public void testFetchBodyStructureSimple() throws Exception { 593 final MockTransport mock = openAndInjectMockTransport(); 594 setupOpenFolder(mock); 595 mFolder.open(OpenMode.READ_WRITE, null); 596 final Message message = mFolder.createMessage("1"); 597 598 final FetchProfile fp = new FetchProfile(); 599 fp.add(FetchProfile.Item.STRUCTURE); 600 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", 601 new String[] { 602 "* 9 fETCH (uID 1 bODYSTRUCTURE (\"tEXT\" \"pLAIN\" nIL" + 603 " nIL nIL nIL 18 3 nIL nIL nIL))", 604 getNextTag(true) + " oK sUCCESS" 605 }); 606 mFolder.fetch(new Message[] { message }, fp, null); 607 608 // Check mime structure... 609 MoreAsserts.assertEquals( 610 new String[] {"text/plain"}, 611 message.getHeader("Content-Type") 612 ); 613 assertNull(message.getHeader("Content-Transfer-Encoding")); 614 assertNull(message.getHeader("Content-ID")); 615 MoreAsserts.assertEquals( 616 new String[] {";\n size=18"}, 617 message.getHeader("Content-Disposition") 618 ); 619 620 MoreAsserts.assertEquals( 621 new String[] {"TEXT"}, 622 message.getHeader("X-Android-Attachment-StoreData") 623 ); 624 625 // TODO: Test NO response. 626 } 627 628 /** 629 * Test for fetching complex muiltipart BODYSTRUCTURE. 630 */ 631 public void testFetchBodyStructureMultipart() throws Exception { 632 final MockTransport mock = openAndInjectMockTransport(); 633 setupOpenFolder(mock); 634 mFolder.open(OpenMode.READ_WRITE, null); 635 final Message message = mFolder.createMessage("1"); 636 637 final FetchProfile fp = new FetchProfile(); 638 fp.add(FetchProfile.Item.STRUCTURE); 639 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", 640 new String[] { 641 "* 9 fETCH (uID 1 bODYSTRUCTURE ((\"tEXT\" \"pLAIN\" () {20}", 642 "long content id#@!@#" + 643 " NIL \"7BIT\" 18 3 NIL NIL NIL)" + 644 "(\"IMAGE\" \"PNG\" (\"NAME\" {10}", 645 "device.png) NIL NIL \"BASE64\" {6}", 646 "117840 NIL (\"aTTACHMENT\" (\"fILENAME\" \"device.png\")) NIL)" + 647 "(\"TEXT\" \"HTML\" () NIL NIL \"7BIT\" 100 NIL 123 (\"aTTACHMENT\"" + 648 "(\"fILENAME\" {15}", 649 "attachment.html \"SIZE\" 555)) NIL)" + 650 "((\"TEXT\" \"HTML\" NIL NIL \"BASE64\")(\"XXX\" \"YYY\"))" + // Nested 651 "\"mIXED\" (\"bOUNDARY\" \"00032556278a7005e40486d159ca\") NIL NIL))", 652 getNextTag(true) + " oK SUCCESS" 653 }); 654 mFolder.fetch(new Message[] { message }, fp, null); 655 656 // Check mime structure... 657 final Body body = message.getBody(); 658 assertTrue(body instanceof MimeMultipart); 659 MimeMultipart mimeMultipart = (MimeMultipart) body; 660 assertEquals(4, mimeMultipart.getCount()); 661 assertEquals("mixed", mimeMultipart.getSubTypeForTest()); 662 663 final Part part1 = mimeMultipart.getBodyPart(0); 664 final Part part2 = mimeMultipart.getBodyPart(1); 665 final Part part3 = mimeMultipart.getBodyPart(2); 666 final Part part4 = mimeMultipart.getBodyPart(3); 667 assertTrue(part1 instanceof MimeBodyPart); 668 assertTrue(part2 instanceof MimeBodyPart); 669 assertTrue(part3 instanceof MimeBodyPart); 670 assertTrue(part4 instanceof MimeBodyPart); 671 672 final MimeBodyPart mimePart1 = (MimeBodyPart) part1; // text/plain 673 final MimeBodyPart mimePart2 = (MimeBodyPart) part2; // image/png 674 final MimeBodyPart mimePart3 = (MimeBodyPart) part3; // text/html 675 final MimeBodyPart mimePart4 = (MimeBodyPart) part4; // Nested 676 677 MoreAsserts.assertEquals( 678 new String[] {"1"}, 679 part1.getHeader("X-Android-Attachment-StoreData") 680 ); 681 MoreAsserts.assertEquals( 682 new String[] {"2"}, 683 part2.getHeader("X-Android-Attachment-StoreData") 684 ); 685 MoreAsserts.assertEquals( 686 new String[] {"3"}, 687 part3.getHeader("X-Android-Attachment-StoreData") 688 ); 689 690 MoreAsserts.assertEquals( 691 new String[] {"text/plain"}, 692 part1.getHeader("Content-Type") 693 ); 694 MoreAsserts.assertEquals( 695 new String[] {"image/png;\n NAME=\"device.png\""}, 696 part2.getHeader("Content-Type") 697 ); 698 MoreAsserts.assertEquals( 699 new String[] {"text/html"}, 700 part3.getHeader("Content-Type") 701 ); 702 703 MoreAsserts.assertEquals( 704 new String[] {"long content id#@!@#"}, 705 part1.getHeader("Content-ID") 706 ); 707 assertNull(part2.getHeader("Content-ID")); 708 assertNull(part3.getHeader("Content-ID")); 709 710 MoreAsserts.assertEquals( 711 new String[] {"7BIT"}, 712 part1.getHeader("Content-Transfer-Encoding") 713 ); 714 MoreAsserts.assertEquals( 715 new String[] {"BASE64"}, 716 part2.getHeader("Content-Transfer-Encoding") 717 ); 718 MoreAsserts.assertEquals( 719 new String[] {"7BIT"}, 720 part3.getHeader("Content-Transfer-Encoding") 721 ); 722 723 MoreAsserts.assertEquals( 724 new String[] {";\n size=18"}, 725 part1.getHeader("Content-Disposition") 726 ); 727 MoreAsserts.assertEquals( 728 new String[] {"attachment;\n filename=\"device.png\";\n size=117840"}, 729 part2.getHeader("Content-Disposition") 730 ); 731 MoreAsserts.assertEquals( 732 new String[] {"attachment;\n filename=\"attachment.html\";\n size=\"555\""}, 733 part3.getHeader("Content-Disposition") 734 ); 735 736 // Check the nested parts. 737 final Body part4body = part4.getBody(); 738 assertTrue(part4body instanceof MimeMultipart); 739 MimeMultipart mimeMultipartPart4 = (MimeMultipart) part4body; 740 assertEquals(2, mimeMultipartPart4.getCount()); 741 742 final MimeBodyPart mimePart41 = (MimeBodyPart) mimeMultipartPart4.getBodyPart(0); 743 final MimeBodyPart mimePart42 = (MimeBodyPart) mimeMultipartPart4.getBodyPart(1); 744 745 MoreAsserts.assertEquals(new String[] {"4.1"}, 746 mimePart41.getHeader("X-Android-Attachment-StoreData") 747 ); 748 MoreAsserts.assertEquals(new String[] {"4.2"}, 749 mimePart42.getHeader("X-Android-Attachment-StoreData") 750 ); 751 } 752 753 public void testFetchBodySane() throws MessagingException { 754 final MockTransport mock = openAndInjectMockTransport(); 755 setupOpenFolder(mock); 756 mFolder.open(OpenMode.READ_WRITE, null); 757 final Message message = mFolder.createMessage("1"); 758 759 final FetchProfile fp = new FetchProfile(); 760 fp.add(FetchProfile.Item.BODY_SANE); 761 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[\\]<0.51200>\\)", 762 new String[] { 763 "* 9 fETCH (uID 1 bODY[] {23}", 764 "from: a (at) b.com", // 15 bytes 765 "", // 2 766 "test", // 6 767 ")", 768 getNextTag(true) + " oK SUCCESS" 769 }); 770 mFolder.fetch(new Message[] { message }, fp, null); 771 assertEquals("a (at) b.com", message.getHeader("from")[0]); 772 773 // TODO: Test NO response. 774 } 775 776 public void testFetchBody() throws MessagingException { 777 final MockTransport mock = openAndInjectMockTransport(); 778 setupOpenFolder(mock); 779 mFolder.open(OpenMode.READ_WRITE, null); 780 final Message message = mFolder.createMessage("1"); 781 782 final FetchProfile fp = new FetchProfile(); 783 fp.add(FetchProfile.Item.BODY); 784 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[\\]\\)", 785 new String[] { 786 "* 9 fETCH (uID 1 bODY[] {23}", 787 "from: a (at) b.com", // 15 bytes 788 "", // 2 789 "test", // 6 790 ")", 791 getNextTag(true) + " oK SUCCESS" 792 }); 793 mFolder.fetch(new Message[] { message }, fp, null); 794 assertEquals("a (at) b.com", message.getHeader("from")[0]); 795 796 // TODO: Test NO response. 797 } 798 799 public void testFetchAttachment() throws Exception { 800 MockTransport mock = openAndInjectMockTransport(); 801 setupOpenFolder(mock); 802 mFolder.open(OpenMode.READ_WRITE, null); 803 final Message message = mFolder.createMessage("1"); 804 805 final FetchProfile fp = new FetchProfile(); 806 fp.add(FetchProfile.Item.STRUCTURE); 807 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", 808 new String[] { 809 "* 9 fETCH (uID 1 bODYSTRUCTURE ((\"tEXT\" \"PLAIN\" (\"cHARSET\" \"iSO-8859-1\")" + 810 " CID nIL \"7bIT\" 18 3 NIL NIL NIL)" + 811 "(\"IMAGE\" \"PNG\"" + 812 " (\"nAME\" \"device.png\") NIL NIL \"bASE64\" 117840 NIL (\"aTTACHMENT\"" + 813 "(\"fILENAME\" \"device.png\")) NIL)" + 814 "\"mIXED\"))", 815 getNextTag(true) + " OK SUCCESS" 816 }); 817 mFolder.fetch(new Message[] { message }, fp, null); 818 819 // Check mime structure, and get the second part. 820 Body body = message.getBody(); 821 assertTrue(body instanceof MimeMultipart); 822 MimeMultipart mimeMultipart = (MimeMultipart) body; 823 assertEquals(2, mimeMultipart.getCount()); 824 825 Part part1 = mimeMultipart.getBodyPart(1); 826 assertTrue(part1 instanceof MimeBodyPart); 827 MimeBodyPart mimePart1 = (MimeBodyPart) part1; 828 829 // Fetch the second part 830 fp.clear(); 831 fp.add(mimePart1); 832 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[2\\]\\)", 833 new String[] { 834 "* 9 fETCH (uID 1 bODY[2] {4}", 835 "YWJj)", // abc in base64 836 getNextTag(true) + " oK SUCCESS" 837 }); 838 mFolder.fetch(new Message[] { message }, fp, null); 839 840 assertEquals("abc", 841 Utility.fromUtf8(IOUtils.toByteArray(mimePart1.getBody().getInputStream()))); 842 843 // TODO: Test NO response. 844 } 845 846 /** 847 * Test for proper operations on servers that return "NIL" for empty message bodies. 848 */ 849 public void testNilMessage() throws MessagingException { 850 MockTransport mock = openAndInjectMockTransport(); 851 setupOpenFolder(mock); 852 mFolder.open(OpenMode.READ_WRITE, null); 853 854 // Prepare to pull structure and peek body text - this is like the "large message" 855 // loop in MessagingController.synchronizeMailboxGeneric() 856 FetchProfile fp = new FetchProfile();fp.clear(); 857 fp.add(FetchProfile.Item.STRUCTURE); 858 Message message1 = mFolder.createMessage("1"); 859 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", new String[] { 860 "* 1 fETCH (uID 1 bODYSTRUCTURE (tEXT pLAIN nIL nIL nIL 7bIT 0 0 nIL nIL nIL))", 861 getNextTag(true) + " oK SUCCESS" 862 }); 863 mFolder.fetch(new Message[] { message1 }, fp, null); 864 865 // The expected result for an empty body is: 866 // * 1 FETCH (UID 1 BODY[TEXT] {0}) 867 // But some servers are returning NIL for the empty body: 868 // * 1 FETCH (UID 1 BODY[TEXT] NIL) 869 // Because this breaks our little parser, fetch() skips over empty parts. 870 // The rest of this test is confirming that this is the case. 871 872 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[TEXT\\]\\)", new String[] { 873 "* 1 fETCH (uID 1 bODY[tEXT] nIL)", 874 getNextTag(true) + " oK SUCCESS" 875 }); 876 ArrayList<Part> viewables = new ArrayList<Part>(); 877 ArrayList<Part> attachments = new ArrayList<Part>(); 878 MimeUtility.collectParts(message1, viewables, attachments); 879 assertTrue(viewables.size() == 1); 880 Part emptyBodyPart = viewables.get(0); 881 fp.clear(); 882 fp.add(emptyBodyPart); 883 mFolder.fetch(new Message[] { message1 }, fp, null); 884 885 // If this wasn't working properly, there would be an attempted interpretation 886 // of the empty part's NIL and possibly a crash. 887 888 // If this worked properly, the "empty" body can now be retrieved 889 viewables = new ArrayList<Part>(); 890 attachments = new ArrayList<Part>(); 891 MimeUtility.collectParts(message1, viewables, attachments); 892 assertTrue(viewables.size() == 1); 893 emptyBodyPart = viewables.get(0); 894 String text = MimeUtility.getTextFromPart(emptyBodyPart); 895 assertNull(text); 896 } 897 898 /** 899 * Confirm the IMAP parser won't crash when seeing an excess FETCH response line without UID. 900 * 901 * <p>We've observed that the secure.emailsrvr.com email server returns an excess FETCH response 902 * for a UID FETCH command. These excess responses doesn't have the UID field in it, even 903 * though we request, which led the response parser to crash. We fixed it by ignoring response 904 * lines that don't have UID. This test is to make sure this case. 905 */ 906 public void testExcessFetchResult() throws MessagingException { 907 MockTransport mock = openAndInjectMockTransport(); 908 setupOpenFolder(mock); 909 mFolder.open(OpenMode.READ_WRITE, null); 910 911 // Create a message, and make sure it's not "SEEN". 912 Message message1 = mFolder.createMessage("1"); 913 assertFalse(message1.isSet(Flag.SEEN)); 914 915 FetchProfile fp = new FetchProfile(); 916 fp.clear(); 917 fp.add(FetchProfile.Item.FLAGS); 918 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID FLAGS\\)", 919 new String[] { 920 "* 1 fETCH (uID 1 fLAGS (\\Seen))", 921 "* 2 fETCH (fLAGS (\\Seen))", 922 getNextTag(true) + " oK SUCCESS" 923 }); 924 925 // Shouldn't crash 926 mFolder.fetch(new Message[] { message1 }, fp, null); 927 928 // And the message is "SEEN". 929 assertTrue(message1.isSet(Flag.SEEN)); 930 } 931 932 933 private ImapMessage prepareForAppendTest(MockTransport mock, String response) throws Exception { 934 ImapMessage message = (ImapMessage) mFolder.createMessage("initial uid"); 935 message.setFrom(new Address("me (at) test.com")); 936 message.setRecipient(RecipientType.TO, new Address("you (at) test.com")); 937 message.setMessageId("<message.id (at) test.com>"); 938 message.setFlagDirectlyForTest(Flag.SEEN, true); 939 message.setBody(new TextBody("Test Body")); 940 941 // + go ahead 942 // * 12345 EXISTS 943 // OK [APPENDUID 627684530 17] (Success) 944 945 mock.expect(getNextTag(false) + 946 " APPEND \\\"" + FOLDER_ENCODED + "\\\" \\(\\\\SEEN\\) \\{166\\}", 947 new String[] {"+ gO aHead"}); 948 949 mock.expectLiterally("From: me (at) test.com", NO_REPLY); 950 mock.expectLiterally("To: you (at) test.com", NO_REPLY); 951 mock.expectLiterally("Message-ID: <message.id (at) test.com>", NO_REPLY); 952 mock.expectLiterally("Content-Type: text/plain;", NO_REPLY); 953 mock.expectLiterally(" charset=utf-8", NO_REPLY); 954 mock.expectLiterally("Content-Transfer-Encoding: base64", NO_REPLY); 955 mock.expectLiterally("", NO_REPLY); 956 mock.expectLiterally("VGVzdCBCb2R5", NO_REPLY); 957 mock.expectLiterally("", new String[] { 958 "* 7 eXISTS", 959 getNextTag(true) + " " + response 960 }); 961 return message; 962 } 963 964 /** 965 * Test for APPEND when the response has APPENDUID. 966 */ 967 public void testAppendMessages() throws Exception { 968 MockTransport mock = openAndInjectMockTransport(); 969 setupOpenFolder(mock); 970 mFolder.open(OpenMode.READ_WRITE, null); 971 972 ImapMessage message = prepareForAppendTest(mock, "oK [aPPENDUID 1234567 13] (Success)"); 973 974 mFolder.appendMessages(new Message[] {message}); 975 976 assertEquals("13", message.getUid()); 977 assertEquals(7, mFolder.getMessageCount()); 978 } 979 980 /** 981 * Test for APPEND when the response doesn't have APPENDUID. 982 */ 983 public void testAppendMessagesNoAppendUid() throws Exception { 984 MockTransport mock = openAndInjectMockTransport(); 985 setupOpenFolder(mock); 986 mFolder.open(OpenMode.READ_WRITE, null); 987 988 ImapMessage message = prepareForAppendTest(mock, "OK Success"); 989 990 mock.expectLiterally( 991 getNextTag(false) + " UID SEARCH (HEADER MESSAGE-ID <message.id (at) test.com>)", 992 new String[] { 993 "* sEARCH 321", 994 getNextTag(true) + " oK success" 995 }); 996 997 mFolder.appendMessages(new Message[] {message}); 998 999 assertEquals("321", message.getUid()); 1000 } 1001 1002 /** 1003 * Test for append failure. 1004 * 1005 * We don't check the response for APPEND. We just SEARCH for the message-id to get the UID. 1006 * If append has failed, the SEARCH command returns no UID, and the UID of the message is left 1007 * unset. 1008 */ 1009 public void testAppendFailure() throws Exception { 1010 MockTransport mock = openAndInjectMockTransport(); 1011 setupOpenFolder(mock); 1012 mFolder.open(OpenMode.READ_WRITE, null); 1013 1014 ImapMessage message = prepareForAppendTest(mock, "NO No space left on the server."); 1015 assertEquals("initial uid", message.getUid()); 1016 1017 mock.expectLiterally( 1018 getNextTag(false) + " UID SEARCH (HEADER MESSAGE-ID <message.id (at) test.com>)", 1019 new String[] { 1020 "* sEARCH", // not found 1021 getNextTag(true) + " oK Search completed." 1022 }); 1023 1024 mFolder.appendMessages(new Message[] {message}); 1025 1026 // Shouldn't have changed 1027 assertEquals("initial uid", message.getUid()); 1028 } 1029 1030 public void testGetPersonalNamespaces() throws Exception { 1031 MockTransport mock = openAndInjectMockTransport(); 1032 expectLogin(mock); 1033 1034 mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"", 1035 new String[] { 1036 "* lIST (\\HAsNoChildren) \"/\" \"inbox\"", 1037 "* lIST (\\hAsnochildren) \"/\" \"Drafts\"", 1038 "* lIST (\\nOselect) \"/\" \"no select\"", 1039 "* lIST (\\HAsNoChildren) \"/\" \"&ZeVnLIqe-\"", // Japanese folder name 1040 getNextTag(true) + " oK SUCCESS" 1041 }); 1042 Folder[] folders = mStore.getPersonalNamespaces(); 1043 1044 ArrayList<String> list = new ArrayList<String>(); 1045 for (Folder f : folders) { 1046 list.add(f.getName()); 1047 } 1048 MoreAsserts.assertEquals( 1049 new String[] {"Drafts", "\u65E5\u672C\u8A9E", "INBOX"}, 1050 list.toArray(new String[0]) 1051 ); 1052 1053 // TODO test with path prefix 1054 // TODO: Test NO response. 1055 } 1056 1057 public void testEncodeFolderName() { 1058 assertEquals("", ImapStore.encodeFolderName("")); 1059 assertEquals("a", ImapStore.encodeFolderName("a")); 1060 assertEquals("XYZ", ImapStore.encodeFolderName("XYZ")); 1061 assertEquals("&ZeVnLIqe-", ImapStore.encodeFolderName("\u65E5\u672C\u8A9E")); 1062 assertEquals("!&ZeVnLIqe-!", ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!")); 1063 } 1064 1065 public void testDecodeFolderName() { 1066 assertEquals("", ImapStore.decodeFolderName("")); 1067 assertEquals("a", ImapStore.decodeFolderName("a")); 1068 assertEquals("XYZ", ImapStore.decodeFolderName("XYZ")); 1069 assertEquals("\u65E5\u672C\u8A9E", ImapStore.decodeFolderName("&ZeVnLIqe-")); 1070 assertEquals("!\u65E5\u672C\u8A9E!", ImapStore.decodeFolderName("!&ZeVnLIqe-!")); 1071 } 1072 1073 public void testOpen() throws Exception { 1074 MockTransport mock = openAndInjectMockTransport(); 1075 expectLogin(mock); 1076 1077 final Folder folder = mStore.getFolder("test"); 1078 1079 // Not exist 1080 mock.expect(getNextTag(false) + " SELECT \\\"test\\\"", 1081 new String[] { 1082 getNextTag(true) + " nO no such mailbox" 1083 }); 1084 try { 1085 folder.open(OpenMode.READ_WRITE, null); 1086 fail(); 1087 } catch (MessagingException expected) { 1088 } 1089 1090 // READ-WRITE 1091 expectNoop(mock, true); // Need it because we reuse the connection. 1092 mock.expect(getNextTag(false) + " SELECT \\\"test\\\"", 1093 new String[] { 1094 "* 1 eXISTS", 1095 getNextTag(true) + " oK [rEAD-wRITE]" 1096 }); 1097 1098 folder.open(OpenMode.READ_WRITE, null); 1099 assertTrue(folder.exists()); 1100 assertEquals(1, folder.getMessageCount()); 1101 assertEquals(OpenMode.READ_WRITE, folder.getMode()); 1102 1103 assertTrue(folder.isOpen()); 1104 folder.close(false); 1105 assertFalse(folder.isOpen()); 1106 1107 // READ-ONLY 1108 expectNoop(mock, true); // Need it because we reuse the connection. 1109 mock.expect(getNextTag(false) + " SELECT \\\"test\\\"", 1110 new String[] { 1111 "* 2 eXISTS", 1112 getNextTag(true) + " oK [rEAD-oNLY]" 1113 }); 1114 1115 folder.open(OpenMode.READ_WRITE, null); 1116 assertTrue(folder.exists()); 1117 assertEquals(2, folder.getMessageCount()); 1118 assertEquals(OpenMode.READ_ONLY, folder.getMode()); 1119 1120 // Try to re-open as read-write. Should send SELECT again. 1121 expectNoop(mock, true); // Need it because we reuse the connection. 1122 mock.expect(getNextTag(false) + " SELECT \\\"test\\\"", 1123 new String[] { 1124 "* 15 eXISTS", 1125 getNextTag(true) + " oK selected" 1126 }); 1127 1128 folder.open(OpenMode.READ_WRITE, null); 1129 assertTrue(folder.exists()); 1130 assertEquals(15, folder.getMessageCount()); 1131 assertEquals(OpenMode.READ_WRITE, folder.getMode()); 1132 } 1133 1134 public void testExists() throws Exception { 1135 MockTransport mock = openAndInjectMockTransport(); 1136 expectLogin(mock); 1137 1138 // Folder exists 1139 Folder folder = mStore.getFolder("\u65E5\u672C\u8A9E"); 1140 mock.expect(getNextTag(false) + " STATUS \\\"&ZeVnLIqe-\\\" \\(UIDVALIDITY\\)", 1141 new String[] { 1142 "* sTATUS \"&ZeVnLIqe-\" (mESSAGES 10)", 1143 getNextTag(true) + " oK SUCCESS" 1144 }); 1145 1146 assertTrue(folder.exists()); 1147 1148 // Connection verification 1149 expectNoop(mock, true); 1150 1151 // Doesn't exist 1152 folder = mStore.getFolder("no such folder"); 1153 mock.expect(getNextTag(false) + " STATUS \\\"no such folder\\\" \\(UIDVALIDITY\\)", 1154 new String[] { 1155 getNextTag(true) + " NO No such folder!" 1156 }); 1157 1158 assertFalse(folder.exists()); 1159 } 1160 1161 public void testCreate() throws Exception { 1162 MockTransport mock = openAndInjectMockTransport(); 1163 expectLogin(mock); 1164 1165 // Success 1166 Folder folder = mStore.getFolder("\u65E5\u672C\u8A9E"); 1167 1168 assertTrue(folder.canCreate(FolderType.HOLDS_MESSAGES)); 1169 1170 mock.expect(getNextTag(false) + " CREATE \\\"&ZeVnLIqe-\\\"", 1171 new String[] { 1172 getNextTag(true) + " oK Success" 1173 }); 1174 1175 assertTrue(folder.create(FolderType.HOLDS_MESSAGES)); 1176 1177 // Connection verification 1178 expectNoop(mock, true); 1179 1180 // Failure 1181 mock.expect(getNextTag(false) + " CREATE \\\"&ZeVnLIqe-\\\"", 1182 new String[] { 1183 getNextTag(true) + " nO Can't create folder" 1184 }); 1185 1186 assertFalse(folder.create(FolderType.HOLDS_MESSAGES)); 1187 } 1188 1189 public void testCopy() throws Exception { 1190 MockTransport mock = openAndInjectMockTransport(); 1191 setupOpenFolder(mock); 1192 mFolder.open(OpenMode.READ_WRITE, null); 1193 1194 Folder folderTo = mStore.getFolder("\u65E5\u672C\u8A9E"); 1195 Message[] messages = new Message[] { 1196 mFolder.createMessage("11"), 1197 mFolder.createMessage("12"), 1198 }; 1199 1200 mock.expect(getNextTag(false) + " UID COPY 11\\,12 \\\"&ZeVnLIqe-\\\"", 1201 new String[] { 1202 getNextTag(true) + " oK copy completed" 1203 }); 1204 1205 mFolder.copyMessages(messages, folderTo, null); 1206 1207 // TODO: Test NO response. (src message not found) 1208 } 1209 1210 public void testGetUnreadMessageCount() throws Exception { 1211 MockTransport mock = openAndInjectMockTransport(); 1212 setupOpenFolder(mock); 1213 mFolder.open(OpenMode.READ_WRITE, null); 1214 1215 mock.expect(getNextTag(false) + " STATUS \\\"" + FOLDER_ENCODED + "\\\" \\(UNSEEN\\)", 1216 new String[] { 1217 "* sTATUS \"" + FOLDER_ENCODED + "\" (X 1 uNSEEN 123)", 1218 getNextTag(true) + " oK copy completed" 1219 }); 1220 1221 assertEquals(123, mFolder.getUnreadMessageCount()); 1222 } 1223 1224 public void testExpunge() throws Exception { 1225 MockTransport mock = openAndInjectMockTransport(); 1226 setupOpenFolder(mock); 1227 mFolder.open(OpenMode.READ_WRITE, null); 1228 1229 mock.expect(getNextTag(false) + " EXPUNGE", 1230 new String[] { 1231 getNextTag(true) + " oK success" 1232 }); 1233 1234 mFolder.expunge(); 1235 1236 // TODO: Test NO response. (permission denied) 1237 } 1238 1239 public void testSetFlags() throws Exception { 1240 MockTransport mock = openAndInjectMockTransport(); 1241 setupOpenFolder(mock); 1242 mFolder.open(OpenMode.READ_WRITE, null); 1243 1244 Message[] messages = new Message[] { 1245 mFolder.createMessage("11"), 1246 mFolder.createMessage("12"), 1247 }; 1248 1249 // Set 1250 mock.expect( 1251 getNextTag(false) + " UID STORE 11\\,12 \\+FLAGS.SILENT \\(\\\\FLAGGED \\\\SEEN\\)", 1252 new String[] { 1253 getNextTag(true) + " oK success" 1254 }); 1255 mFolder.setFlags(messages, new Flag[] {Flag.FLAGGED, Flag.SEEN}, true); 1256 1257 // Clear 1258 mock.expect( 1259 getNextTag(false) + " UID STORE 11\\,12 \\-FLAGS.SILENT \\(\\\\DELETED\\)", 1260 new String[] { 1261 getNextTag(true) + " oK success" 1262 }); 1263 mFolder.setFlags(messages, new Flag[] {Flag.DELETED}, false); 1264 1265 // TODO: Test NO response. (src message not found) 1266 } 1267 1268 public void testSearchForUids() throws Exception { 1269 MockTransport mock = openAndInjectMockTransport(); 1270 setupOpenFolder(mock); 1271 mFolder.open(OpenMode.READ_WRITE, null); 1272 1273 // Single results 1274 mock.expect( 1275 getNextTag(false) + " UID SEARCH X", 1276 new String[] { 1277 "* sEARCH 1", 1278 getNextTag(true) + " oK success" 1279 }); 1280 MoreAsserts.assertEquals(new String[] { 1281 "1" 1282 }, mFolder.searchForUids("X")); 1283 1284 // Multiple results, including SEARCH with no UIDs. 1285 mock.expect( 1286 getNextTag(false) + " UID SEARCH UID 123", 1287 new String[] { 1288 "* sEARCH 123 4 567", 1289 "* search", 1290 "* sEARCH 0", 1291 "* SEARCH", 1292 "* sEARCH 100 200 300", 1293 getNextTag(true) + " oK success" 1294 }); 1295 MoreAsserts.assertEquals(new String[] { 1296 "123", "4", "567", "0", "100", "200", "300" 1297 }, mFolder.searchForUids("UID 123")); 1298 1299 // NO result 1300 mock.expect( 1301 getNextTag(false) + " UID SEARCH SOME CRITERIA", 1302 new String[] { 1303 getNextTag(true) + " nO not found" 1304 }); 1305 MoreAsserts.assertEquals(new String[] { 1306 }, mFolder.searchForUids("SOME CRITERIA")); 1307 1308 // OK result, but result is empty. (Probably against RFC) 1309 mock.expect( 1310 getNextTag(false) + " UID SEARCH SOME CRITERIA", 1311 new String[] { 1312 getNextTag(true) + " oK success" 1313 }); 1314 MoreAsserts.assertEquals(new String[] { 1315 }, mFolder.searchForUids("SOME CRITERIA")); 1316 1317 // OK result with empty search response. 1318 mock.expect( 1319 getNextTag(false) + " UID SEARCH SOME CRITERIA", 1320 new String[] { 1321 "* search", 1322 getNextTag(true) + " oK success" 1323 }); 1324 MoreAsserts.assertEquals(new String[] { 1325 }, mFolder.searchForUids("SOME CRITERIA")); 1326 } 1327 1328 1329 public void testGetMessage() throws Exception { 1330 MockTransport mock = openAndInjectMockTransport(); 1331 setupOpenFolder(mock); 1332 mFolder.open(OpenMode.READ_WRITE, null); 1333 1334 // Found 1335 mock.expect( 1336 getNextTag(false) + " UID SEARCH UID 123", 1337 new String[] { 1338 "* sEARCH 123", 1339 getNextTag(true) + " oK success" 1340 }); 1341 assertEquals("123", mFolder.getMessage("123").getUid()); 1342 1343 // Not found 1344 mock.expect( 1345 getNextTag(false) + " UID SEARCH UID 123", 1346 new String[] { 1347 getNextTag(true) + " nO not found" 1348 }); 1349 assertNull(mFolder.getMessage("123")); 1350 } 1351 1352 /** Test for getMessages(int, int, MessageRetrievalListener) */ 1353 public void testGetMessages1() throws Exception { 1354 MockTransport mock = openAndInjectMockTransport(); 1355 setupOpenFolder(mock); 1356 mFolder.open(OpenMode.READ_WRITE, null); 1357 1358 // Found 1359 mock.expect( 1360 getNextTag(false) + " UID SEARCH 3:5 NOT DELETED", 1361 new String[] { 1362 "* sEARCH 3 4", 1363 getNextTag(true) + " oK success" 1364 }); 1365 1366 checkMessageUids(new String[] {"3", "4"}, mFolder.getMessages(3, 5, null)); 1367 1368 // Not found 1369 mock.expect( 1370 getNextTag(false) + " UID SEARCH 3:5 NOT DELETED", 1371 new String[] { 1372 getNextTag(true) + " nO not found" 1373 }); 1374 1375 checkMessageUids(new String[] {}, mFolder.getMessages(3, 5, null)); 1376 } 1377 1378 /** 1379 * Test for getMessages(String[] uids, MessageRetrievalListener) where uids != null. 1380 * (testGetMessages3() covers the case where uids == null.) 1381 */ 1382 public void testGetMessages2() throws Exception { 1383 MockTransport mock = openAndInjectMockTransport(); 1384 setupOpenFolder(mock); 1385 mFolder.open(OpenMode.READ_WRITE, null); 1386 1387 // No command will be sent 1388 checkMessageUids(new String[] {"3", "4", "5"}, 1389 mFolder.getMessages(new String[] {"3", "4", "5"}, null)); 1390 1391 checkMessageUids(new String[] {}, 1392 mFolder.getMessages(new String[] {}, null)); 1393 } 1394 1395 /** 1396 * Test for getMessages(MessageRetrievalListener), which is the same as 1397 * getMessages(String[] uids, MessageRetrievalListener) where uids == null. 1398 */ 1399 public void testGetMessages3() throws Exception { 1400 MockTransport mock = openAndInjectMockTransport(); 1401 setupOpenFolder(mock); 1402 mFolder.open(OpenMode.READ_WRITE, null); 1403 1404 mock.expect( 1405 getNextTag(false) + " UID SEARCH 1:\\* NOT DELETED", 1406 new String[] { 1407 "* sEARCH 3 4 5", 1408 getNextTag(true) + " OK success" 1409 }); 1410 checkMessageUids(new String[] {"3", "4", "5"}, 1411 mFolder.getMessages(null)); 1412 } 1413 1414 private static void checkMessageUids(String[] expectedUids, Message[] actualMessages) { 1415 ArrayList<String> list = new ArrayList<String>(); 1416 for (Message m : actualMessages) { 1417 list.add(m.getUid()); 1418 } 1419 MoreAsserts.assertEquals(expectedUids, list.toArray(new String[0]) ); 1420 } 1421 1422 /** 1423 * Test for {@link ImapStore#getConnection} and {@link ImapStore#keepConnectionForReuse} 1424 */ 1425 public void testGetConnection() throws Exception { 1426 MockTransport mock = openAndInjectMockTransport(); 1427 1428 // Start: No pooled connections. 1429 assertEquals(0, mStore.getConnectionPoolForTest().size()); 1430 1431 // Get 1st connection. 1432 final ImapConnection con1 = mStore.getConnection(); 1433 assertNotNull(con1); 1434 assertEquals(0, mStore.getConnectionPoolForTest().size()); // Pool size not changed. 1435 assertFalse(con1.isTransportOpenForTest()); // Transport not open yet. 1436 1437 // Open con1 1438 expectLogin(mock); 1439 con1.open(); 1440 assertTrue(con1.isTransportOpenForTest()); 1441 1442 // Get 2nd connection. 1443 final ImapConnection con2 = mStore.getConnection(); 1444 assertNotNull(con2); 1445 assertEquals(0, mStore.getConnectionPoolForTest().size()); // Pool size not changed. 1446 assertFalse(con2.isTransportOpenForTest()); // Transport not open yet. 1447 1448 // con1 != con2 1449 assertNotSame(con1, con2); 1450 1451 // Open con2 1452 expectLogin(mock); 1453 con2.open(); 1454 assertTrue(con1.isTransportOpenForTest()); 1455 1456 // Now we have two open connections: con1 and con2 1457 1458 // Save con1 in the pool. 1459 mStore.poolConnection(con1); 1460 assertEquals(1, mStore.getConnectionPoolForTest().size()); 1461 1462 // Get another connection. Should get con1, after verifying the connection. 1463 mock.expect(getNextTag(false) + " NOOP", new String[] {getNextTag(true) + " oK success"}); 1464 1465 final ImapConnection con1b = mStore.getConnection(); 1466 assertEquals(0, mStore.getConnectionPoolForTest().size()); // No connections left in pool 1467 assertSame(con1, con1b); 1468 assertTrue(con1.isTransportOpenForTest()); // We opened it. 1469 1470 // Save con2. 1471 mStore.poolConnection(con2); 1472 assertEquals(1, mStore.getConnectionPoolForTest().size()); 1473 1474 // Try to get connection, but this time, connection gets closed. 1475 mock.expect(getNextTag(false) + " NOOP", new String[] {getNextTag(true) + "* bYE bye"}); 1476 final ImapConnection con3 = mStore.getConnection(); 1477 assertNotNull(con3); 1478 assertEquals(0, mStore.getConnectionPoolForTest().size()); // No connections left in pool 1479 1480 // It should be a new connection. 1481 assertNotSame(con1, con3); 1482 assertNotSame(con2, con3); 1483 } 1484 1485 public void testCheckSettings() throws Exception { 1486 MockTransport mock = openAndInjectMockTransport(); 1487 1488 expectLogin(mock); 1489 mStore.checkSettings(); 1490 1491 expectLogin(mock, new String[] {"* iD nIL", "oK"}, "nO authentication failed"); 1492 try { 1493 mStore.checkSettings(); 1494 fail(); 1495 } catch (MessagingException expected) { 1496 } 1497 } 1498 1499 // Compatibility tests... 1500 1501 /** 1502 * Getting an ALERT with a % mark in the message, which crashed the old parser. 1503 */ 1504 public void testQuotaAlert() throws Exception { 1505 MockTransport mock = openAndInjectMockTransport(); 1506 expectLogin(mock); 1507 1508 // Success 1509 Folder folder = mStore.getFolder("INBOX"); 1510 1511 // The following response was copied from an actual bug... 1512 mock.expect(getNextTag(false) + " SELECT \"INBOX\"", new String[] { 1513 "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen NonJunk $Forwarded Junk" + 1514 " $Label4 $Label1 $Label2 $Label3 $Label5 $MDNSent Old)", 1515 "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen NonJunk" + 1516 " $Forwarded Junk $Label4 $Label1 $Label2 $Label3 $Label5 $MDNSent Old \\*)]", 1517 "* 6406 EXISTS", 1518 "* 0 RECENT", 1519 "* OK [UNSEEN 5338]", 1520 "* OK [UIDVALIDITY 1055957975]", 1521 "* OK [UIDNEXT 449625]", 1522 "* NO [ALERT] Mailbox is at 98% of quota", 1523 getNextTag(true) + " OK [READ-WRITE] Completed"}); 1524 folder.open(OpenMode.READ_WRITE, null); // shouldn't crash. 1525 assertEquals(6406, folder.getMessageCount()); 1526 } 1527 1528 /** 1529 * Apparently some servers send a size in the wrong format. e.g. 123E 1530 */ 1531 public void testFetchBodyStructureMalformed() throws Exception { 1532 final MockTransport mock = openAndInjectMockTransport(); 1533 setupOpenFolder(mock); 1534 mFolder.open(OpenMode.READ_WRITE, null); 1535 final Message message = mFolder.createMessage("1"); 1536 1537 final FetchProfile fp = new FetchProfile(); 1538 fp.add(FetchProfile.Item.STRUCTURE); 1539 mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", 1540 new String[] { 1541 "* 9 FETCH (UID 1 BODYSTRUCTURE (\"TEXT\" \"PLAIN\" ()" + 1542 " NIL NIL NIL 123E 3))", // 123E isn't a number! 1543 getNextTag(true) + " OK SUCCESS" 1544 }); 1545 mFolder.fetch(new Message[] { message }, fp, null); 1546 1547 // Check mime structure... 1548 MoreAsserts.assertEquals( 1549 new String[] {"text/plain"}, 1550 message.getHeader("Content-Type") 1551 ); 1552 assertNull(message.getHeader("Content-Transfer-Encoding")); 1553 assertNull(message.getHeader("Content-ID")); 1554 1555 // Doesn't have size=xxx 1556 assertNull(message.getHeader("Content-Disposition")); 1557 } 1558 1559 /** 1560 * Folder name with special chars in it. 1561 * 1562 * Gmail puts the folder name in the OK response, which crashed the old parser if there's a 1563 * special char in the folder name. 1564 */ 1565 public void testFolderNameWithSpecialChars() throws Exception { 1566 final String FOLDER_1 = "@u88**%_St"; 1567 final String FOLDER_1_QUOTED = Pattern.quote(FOLDER_1); 1568 final String FOLDER_2 = "folder test_06"; 1569 1570 MockTransport mock = openAndInjectMockTransport(); 1571 expectLogin(mock); 1572 1573 // List folders. 1574 mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"", 1575 new String[] { 1576 "* LIST () \"/\" \"" + FOLDER_1 + "\"", 1577 "* LIST () \"/\" \"" + FOLDER_2 + "\"", 1578 getNextTag(true) + " OK SUCCESS" 1579 }); 1580 final Folder[] folders = mStore.getPersonalNamespaces(); 1581 1582 ArrayList<String> list = new ArrayList<String>(); 1583 for (Folder f : folders) { 1584 list.add(f.getName()); 1585 } 1586 MoreAsserts.assertEquals( 1587 new String[] {FOLDER_1, FOLDER_2, "INBOX"}, 1588 list.toArray(new String[0]) 1589 ); 1590 1591 // Try to open the folders. 1592 expectNoop(mock, true); 1593 mock.expect(getNextTag(false) + " SELECT \"" + FOLDER_1_QUOTED + "\"", new String[] { 1594 "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)", 1595 "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]", 1596 "* 0 EXISTS", 1597 "* 0 RECENT", 1598 "* OK [UNSEEN 0]", 1599 "* OK [UIDNEXT 1]", 1600 getNextTag(true) + " OK [READ-WRITE] " + FOLDER_1}); 1601 folders[0].open(OpenMode.READ_WRITE, null); 1602 folders[0].close(false); 1603 1604 expectNoop(mock, true); 1605 mock.expect(getNextTag(false) + " SELECT \"" + FOLDER_2 + "\"", new String[] { 1606 "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)", 1607 "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]", 1608 "* 0 EXISTS", 1609 "* 0 RECENT", 1610 "* OK [UNSEEN 0]", 1611 "* OK [UIDNEXT 1]", 1612 getNextTag(true) + " OK [READ-WRITE] " + FOLDER_2}); 1613 folders[1].open(OpenMode.READ_WRITE, null); 1614 folders[1].close(false); 1615 } 1616 1617 /** 1618 * Callback for {@link #runAndExpectMessagingException}. 1619 */ 1620 private interface RunAndExpectMessagingExceptionTarget { 1621 public void run(MockTransport mockTransport) throws Exception; 1622 } 1623 1624 /** 1625 * Set up the usual mock transport, open the folder, 1626 * run {@link RunAndExpectMessagingExceptionTarget} and make sure a {@link MessagingException} 1627 * is thrown. 1628 */ 1629 private void runAndExpectMessagingException(RunAndExpectMessagingExceptionTarget target) 1630 throws Exception { 1631 try { 1632 final MockTransport mockTransport = openAndInjectMockTransport(); 1633 setupOpenFolder(mockTransport); 1634 mFolder.open(OpenMode.READ_WRITE, null); 1635 1636 target.run(mockTransport); 1637 1638 fail("MessagingException expected."); 1639 } catch (MessagingException expected) { 1640 } 1641 } 1642 1643 /** 1644 * Make sure that IOExceptions are always converted to MessagingException. 1645 */ 1646 public void testFetchIOException() throws Exception { 1647 runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() { 1648 public void run(MockTransport mockTransport) throws Exception { 1649 mockTransport.expectIOException(); 1650 1651 final Message message = mFolder.createMessage("1"); 1652 final FetchProfile fp = new FetchProfile(); 1653 fp.add(FetchProfile.Item.STRUCTURE); 1654 1655 mFolder.fetch(new Message[] { message }, fp, null); 1656 } 1657 }); 1658 } 1659 1660 /** 1661 * Make sure that IOExceptions are always converted to MessagingException. 1662 */ 1663 public void testUnreadMessageCountIOException() throws Exception { 1664 runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() { 1665 public void run(MockTransport mockTransport) throws Exception { 1666 mockTransport.expectIOException(); 1667 1668 mFolder.getUnreadMessageCount(); 1669 } 1670 }); 1671 } 1672 1673 /** 1674 * Make sure that IOExceptions are always converted to MessagingException. 1675 */ 1676 public void testCopyMessagesIOException() throws Exception { 1677 runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() { 1678 public void run(MockTransport mockTransport) throws Exception { 1679 mockTransport.expectIOException(); 1680 1681 final Message message = mFolder.createMessage("1"); 1682 final Folder folder = mStore.getFolder("test"); 1683 1684 mFolder.copyMessages(new Message[] { message }, folder, null); 1685 } 1686 }); 1687 } 1688 1689 /** 1690 * Make sure that IOExceptions are always converted to MessagingException. 1691 */ 1692 public void testSearchForUidsIOException() throws Exception { 1693 runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() { 1694 public void run(MockTransport mockTransport) throws Exception { 1695 mockTransport.expectIOException(); 1696 1697 mFolder.getMessage("uid"); 1698 } 1699 }); 1700 } 1701 1702 /** 1703 * Make sure that IOExceptions are always converted to MessagingException. 1704 */ 1705 public void testExpungeIOException() throws Exception { 1706 runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() { 1707 public void run(MockTransport mockTransport) throws Exception { 1708 mockTransport.expectIOException(); 1709 1710 mFolder.expunge(); 1711 } 1712 }); 1713 } 1714 1715 /** 1716 * Make sure that IOExceptions are always converted to MessagingException. 1717 */ 1718 public void testOpenIOException() throws Exception { 1719 runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() { 1720 public void run(MockTransport mockTransport) throws Exception { 1721 mockTransport.expectIOException(); 1722 final Folder folder = mStore.getFolder("test"); 1723 folder.open(OpenMode.READ_WRITE, null); 1724 } 1725 }); 1726 } 1727 } 1728