• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.content.Context;
20 import android.content.ContextWrapper;
21 import android.content.SharedPreferences;
22 import android.content.pm.PackageManager.NameNotFoundException;
23 import android.os.Build;
24 import android.os.Bundle;
25 import android.test.InstrumentationTestCase;
26 import android.test.MoreAsserts;
27 import android.test.suitebuilder.annotation.SmallTest;
28 
29 import com.android.email.DBTestHelper;
30 import com.android.email.MockSharedPreferences;
31 import com.android.email.MockVendorPolicy;
32 import com.android.email.VendorPolicyLoader;
33 import com.android.email.mail.Transport;
34 import com.android.email.mail.store.ImapStore.ImapMessage;
35 import com.android.email.mail.store.imap.ImapResponse;
36 import com.android.email.mail.store.imap.ImapTestUtils;
37 import com.android.email.mail.transport.MockTransport;
38 import com.android.emailcommon.TempDirectory;
39 import com.android.emailcommon.internet.MimeBodyPart;
40 import com.android.emailcommon.internet.MimeMultipart;
41 import com.android.emailcommon.internet.MimeUtility;
42 import com.android.emailcommon.internet.TextBody;
43 import com.android.emailcommon.mail.Address;
44 import com.android.emailcommon.mail.AuthenticationFailedException;
45 import com.android.emailcommon.mail.Body;
46 import com.android.emailcommon.mail.FetchProfile;
47 import com.android.emailcommon.mail.Flag;
48 import com.android.emailcommon.mail.Folder;
49 import com.android.emailcommon.mail.Folder.FolderType;
50 import com.android.emailcommon.mail.Folder.OpenMode;
51 import com.android.emailcommon.mail.Message;
52 import com.android.emailcommon.mail.Message.RecipientType;
53 import com.android.emailcommon.mail.MessagingException;
54 import com.android.emailcommon.mail.Part;
55 import com.android.emailcommon.provider.Account;
56 import com.android.emailcommon.provider.HostAuth;
57 import com.android.emailcommon.provider.Mailbox;
58 import com.android.emailcommon.utility.Utility;
59 
60 import org.apache.commons.io.IOUtils;
61 
62 import java.util.ArrayList;
63 import java.util.HashMap;
64 import java.util.regex.Pattern;
65 
66 /**
67  * This is a series of unit tests for the ImapStore class.  These tests must be locally
68  * complete - no server(s) required.
69  *
70  * To run these tests alone, use:
71  *   $ runtest -c com.android.email.mail.store.ImapStoreUnitTests email
72  *
73  * TODO Check if callback is really called
74  * TODO test for BAD response in various places?
75  * TODO test for BYE response in various places?
76  */
77 @SmallTest
78 public class ImapStoreUnitTests extends InstrumentationTestCase {
79     private final static String[] NO_REPLY = new String[0];
80 
81     /** Default folder name.  In order to test for encoding, we use a non-ascii name. */
82     private final static String FOLDER_NAME = "\u65E5";
83     /** Folder name encoded in UTF-7. */
84     private final static String FOLDER_ENCODED = "&ZeU-";
85     /**
86      * Flag bits to specify whether or not a folder can be selected. This corresponds to
87      * {@link Mailbox#FLAG_ACCEPTS_MOVED_MAIL} and {@link Mailbox#FLAG_HOLDS_MAIL}.
88      */
89     private final static int SELECTABLE_BITS = 0x18;
90 
91     private final static ImapResponse CAPABILITY_RESPONSE = ImapTestUtils.parseResponse(
92             "* CAPABILITY IMAP4rev1 STARTTLS");
93 
94     /* These values are provided by setUp() */
95     private ImapStore mStore = null;
96     private ImapFolder mFolder = null;
97     private Context mTestContext;
98 
99     /** The tag for the current IMAP command; used for mock transport responses */
100     private int mTag;
101     // Fields specific to the CopyMessages tests
102     private MockTransport mCopyMock;
103     private Folder mCopyToFolder;
104     private Message[] mCopyMessages;
105 
106     /**
107      * A wrapper to provide a wrapper to a Context which has already been mocked.
108      * This allows additional methods to delegate to the original, real context, in cases
109      * where the mocked behavior is insufficient.
110      */
111     private class SecondaryMockContext extends ContextWrapper {
112         private final Context mUnderlying;
113 
SecondaryMockContext(Context mocked, Context underlying)114         public SecondaryMockContext(Context mocked, Context underlying) {
115             super(mocked);
116             mUnderlying = underlying;
117         }
118 
119         // TODO: eliminate the need for these method.
120         @Override
createPackageContext(String packageName, int flags)121         public Context createPackageContext(String packageName, int flags)
122                 throws NameNotFoundException {
123             return mUnderlying.createPackageContext(packageName, flags);
124         }
125 
126         @Override
getSharedPreferences(String name, int mode)127         public SharedPreferences getSharedPreferences(String name, int mode) {
128             return new MockSharedPreferences();
129         }
130     }
131 
132     /**
133      * Setup code.  We generate a lightweight ImapStore and ImapStore.ImapFolder.
134      */
135     @Override
setUp()136     protected void setUp() throws Exception {
137         super.setUp();
138         Context realContext = getInstrumentation().getTargetContext();
139         ImapStore.sImapId = ImapStore.makeCommonImapId(realContext.getPackageName(),
140                         Build.VERSION.RELEASE, Build.VERSION.CODENAME,
141                         Build.MODEL, Build.ID, Build.MANUFACTURER,
142                         "FakeNetworkOperator");
143         mTestContext = new SecondaryMockContext(
144                 DBTestHelper.ProviderContextSetupHelper.getProviderContext(realContext),
145                 realContext);
146         MockVendorPolicy.inject(mTestContext);
147 
148         TempDirectory.setTempDirectory(mTestContext);
149 
150         // These are needed so we can get at the inner classes
151         HostAuth testAuth = new HostAuth();
152         Account testAccount = new Account();
153 
154         testAuth.setLogin("user", "password");
155         testAuth.setConnection("imap", "server", 999);
156         testAccount.mHostAuthRecv = testAuth;
157         mStore = (ImapStore) ImapStore.newInstance(testAccount, mTestContext);
158         mFolder = (ImapFolder) mStore.getFolder(FOLDER_NAME);
159         resetTag();
160     }
161 
testJoinMessageUids()162     public void testJoinMessageUids() throws Exception {
163         assertEquals("", ImapStore.joinMessageUids(new Message[] {}));
164         assertEquals("a", ImapStore.joinMessageUids(new Message[] {
165                 mFolder.createMessage("a")
166                 }));
167         assertEquals("a,XX", ImapStore.joinMessageUids(new Message[] {
168                 mFolder.createMessage("a"),
169                 mFolder.createMessage("XX"),
170                 }));
171     }
172 
173     /**
174      * Confirms simple non-SSL non-TLS login
175      */
testSimpleLogin()176     public void testSimpleLogin() throws MessagingException {
177 
178         MockTransport mockTransport = openAndInjectMockTransport();
179 
180         // try to open it
181         setupOpenFolder(mockTransport);
182         mFolder.open(OpenMode.READ_WRITE);
183 
184         // TODO: inject specific facts in the initial folder SELECT and check them here
185     }
186 
187     /**
188      * Test simple login with failed authentication
189      */
testLoginFailure()190     public void testLoginFailure() throws Exception {
191         MockTransport mockTransport = openAndInjectMockTransport();
192         expectLogin(mockTransport, false, false, false, new String[] {"* iD nIL", "oK"},
193                 "nO authentication failed");
194 
195         try {
196             mStore.getConnection().open();
197             fail("Didn't throw AuthenticationFailedException");
198         } catch (AuthenticationFailedException expected) {
199         }
200     }
201 
202     /**
203      * Test simple TLS open
204      */
testTlsOpen()205     public void testTlsOpen() throws MessagingException {
206 
207         MockTransport mockTransport = openAndInjectMockTransport(Transport.CONNECTION_SECURITY_TLS,
208                 false);
209 
210         // try to open it, with STARTTLS
211         expectLogin(mockTransport, true, false, false,
212                 new String[] {"* iD nIL", "oK"}, "oK user authenticated (Success)");
213         mockTransport.expect(
214                 getNextTag(false) + " SELECT \"" + FOLDER_ENCODED + "\"", new String[] {
215                 "* fLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
216                 "* oK [pERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]",
217                 "* 0 eXISTS",
218                 "* 0 rECENT",
219                 "* OK [uNSEEN 0]",
220                 "* OK [uIDNEXT 1]",
221                 getNextTag(true) + " oK [" + "rEAD-wRITE" + "] " +
222                         FOLDER_ENCODED + " selected. (Success)"});
223 
224         mFolder.open(OpenMode.READ_WRITE);
225         assertTrue(mockTransport.isTlsStarted());
226     }
227 
228     /**
229      * TODO: Test with SSL negotiation (faked)
230      * TODO: Test with SSL required but not supported
231      * TODO: Test with TLS required but not supported
232      */
233 
234     /**
235      * Test the generation of the IMAP ID keys
236      */
testImapIdBasic()237     public void testImapIdBasic() {
238         // First test looks at operation of the outer API - we don't control any of the
239         // values;  Just look for basic results.
240 
241         // Strings we'll expect to find:
242         //   name            Android package name of the program
243         //   os              "android"
244         //   os-version      "version; build-id"
245         //   vendor          Vendor of the client/server
246         //   x-android-device-model Model (Optional, so not tested here)
247         //   x-android-net-operator Carrier (Unreliable, so not tested here)
248         //   AGUID           A device+account UID
249         String id = ImapStore.getImapId(mTestContext, "user-name", "host-name",
250                 CAPABILITY_RESPONSE.flatten());
251         HashMap<String, String> map = tokenizeImapId(id);
252         assertEquals(mTestContext.getPackageName(), map.get("name"));
253         assertEquals("android", map.get("os"));
254         assertNotNull(map.get("os-version"));
255         assertNotNull(map.get("vendor"));
256         assertNotNull(map.get("AGUID"));
257 
258         // Next, use the inner API to confirm operation of a couple of
259         // variants for release and non-release devices.
260 
261         // simple API check - non-REL codename, non-empty version
262         id = ImapStore.makeCommonImapId("packageName", "version", "codeName",
263                 "model", "id", "vendor", "network-operator");
264         map = tokenizeImapId(id);
265         assertEquals("packageName", map.get("name"));
266         assertEquals("android", map.get("os"));
267         assertEquals("version; id", map.get("os-version"));
268         assertEquals("vendor", map.get("vendor"));
269         assertEquals(null, map.get("x-android-device-model"));
270         assertEquals("network-operator", map.get("x-android-mobile-net-operator"));
271         assertEquals(null, map.get("AGUID"));
272 
273         // simple API check - codename is REL, so use model name.
274         // also test empty version => 1.0 and empty network operator
275         id = ImapStore.makeCommonImapId("packageName", "", "REL",
276                 "model", "id", "vendor", "");
277         map = tokenizeImapId(id);
278         assertEquals("packageName", map.get("name"));
279         assertEquals("android", map.get("os"));
280         assertEquals("1.0; id", map.get("os-version"));
281         assertEquals("vendor", map.get("vendor"));
282         assertEquals("model", map.get("x-android-device-model"));
283         assertEquals(null, map.get("x-android-mobile-net-operator"));
284         assertEquals(null, map.get("AGUID"));
285     }
286 
287     /**
288      * Test for the interaction between {@link ImapStore#getImapId} and a vendor policy.
289      */
testImapIdWithVendorPolicy()290     public void testImapIdWithVendorPolicy() {
291         try {
292             MockVendorPolicy.inject(mTestContext);
293 
294             // Prepare mock result
295             Bundle result = new Bundle();
296             result.putString("getImapId", "\"test-key\" \"test-value\"");
297             MockVendorPolicy.mockResult = result;
298 
299             // Invoke
300             String id = ImapStore.getImapId(mTestContext, "user-name", "host-name",
301                     ImapTestUtils.parseResponse("* CAPABILITY IMAP4rev1 XXX YYY Z").flatten());
302 
303             // Check the result
304             assertEquals("test-value", tokenizeImapId(id).get("test-key"));
305 
306             // Verify what's passed to the policy
307             assertEquals("getImapId", MockVendorPolicy.passedPolicy);
308             assertEquals("user-name", MockVendorPolicy.passedBundle.getString("getImapId.user"));
309             assertEquals("host-name", MockVendorPolicy.passedBundle.getString("getImapId.host"));
310             assertEquals("[CAPABILITY,IMAP4rev1,XXX,YYY,Z]",
311                     MockVendorPolicy.passedBundle.getString("getImapId.capabilities"));
312         } finally {
313             VendorPolicyLoader.clearInstanceForTest();
314         }
315     }
316 
317     /**
318      * Test of the internal generator for IMAP ID strings, specifically looking for proper
319      * filtering of illegal values.  This is required because we cannot necessarily trust
320      * the external sources of some of this data (e.g. release labels).
321      *
322      * The (somewhat arbitrary) legal values are:  a-z A-Z 0-9 - _ + = ; : . , / <space>
323      * The most important goal of the filters is to keep out control chars, (, ), and "
324      */
testImapIdFiltering()325     public void testImapIdFiltering() {
326         String id = ImapStore.makeCommonImapId(
327                 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
328                 "0123456789", "codeName",
329                 "model", "-_+=;:.,// ",
330                 "v(e)n\"d\ro\nr",           // look for bad chars stripped out, leaving OK chars
331                 "()\"");                    // look for bad chars stripped out, leaving nothing
332         HashMap<String, String> map = tokenizeImapId(id);
333 
334         assertEquals("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", map.get("name"));
335         assertEquals("0123456789; -_+=;:.,// ", map.get("os-version"));
336         assertEquals("vendor", map.get("vendor"));
337         assertNull(map.get("x-android-mobile-net-operator"));
338     }
339 
340     /**
341      * Test that IMAP ID uid's are per-username
342      */
testImapIdDeviceId()343     public void testImapIdDeviceId() throws MessagingException {
344         HostAuth testAuth;
345         Account testAccount;
346 
347         // store 1a
348         testAuth = new HostAuth();
349         testAuth.setLogin("user1", "password");
350         testAuth.setConnection("imap", "server", 999);
351         testAccount = new Account();
352         testAccount.mHostAuthRecv = testAuth;
353         ImapStore testStore1A = (ImapStore) ImapStore.newInstance(testAccount, mTestContext);
354 
355         // store 1b
356         testAuth = new HostAuth();
357         testAuth.setLogin("user1", "password");
358         testAuth.setConnection("imap", "server", 999);
359         testAccount = new Account();
360         testAccount.mHostAuthRecv = testAuth;
361         ImapStore testStore1B = (ImapStore) ImapStore.newInstance(testAccount, mTestContext);
362 
363         // store 2
364         testAuth = new HostAuth();
365         testAuth.setLogin("user2", "password");
366         testAuth.setConnection("imap", "server", 999);
367         testAccount = new Account();
368         testAccount.mHostAuthRecv = testAuth;
369         ImapStore testStore2 = (ImapStore) ImapStore.newInstance(testAccount, mTestContext);
370 
371         String capabilities = CAPABILITY_RESPONSE.flatten();
372         String id1a = ImapStore.getImapId(mTestContext, "user1", "host-name", capabilities);
373         String id1b = ImapStore.getImapId(mTestContext, "user1", "host-name", capabilities);
374         String id2 = ImapStore.getImapId(mTestContext, "user2", "host-name", capabilities);
375 
376         String uid1a = tokenizeImapId(id1a).get("AGUID");
377         String uid1b = tokenizeImapId(id1b).get("AGUID");
378         String uid2 = tokenizeImapId(id2).get("AGUID");
379 
380         assertEquals(uid1a, uid1b);
381         MoreAsserts.assertNotEqual(uid1a, uid2);
382     }
383 
384     /**
385      * Helper to break an IMAP ID string into keys & values
386      * @param id the IMAP Id string (the part inside the parens)
387      * @return a map of key/value pairs
388      */
tokenizeImapId(String id)389     private HashMap<String, String> tokenizeImapId(String id) {
390         // Instead of a true tokenizer, we'll use double-quote as the split.
391         // We can's use " " because there may be spaces inside the values.
392         String[] elements = id.split("\"");
393         HashMap<String, String> map = new HashMap<String, String>();
394         for (int i = 0; i < elements.length; ) {
395             // Because we split at quotes, we expect to find:
396             // [i] = null or one or more spaces
397             // [i+1] = key
398             // [i+2] = one or more spaces
399             // [i+3] = value
400             map.put(elements[i+1], elements[i+3]);
401             i += 4;
402         }
403         return map;
404     }
405 
406     /**
407      * Test non-NIL server response to IMAP ID.  We should simply ignore it.
408      */
testServerId()409     public void testServerId() throws MessagingException {
410         MockTransport mockTransport = openAndInjectMockTransport();
411 
412         // try to open it
413         setupOpenFolder(mockTransport, new String[] {
414                 "* ID (\"name\" \"Cyrus\" \"version\" \"1.5\"" +
415                 " \"os\" \"sunos\" \"os-version\" \"5.5\"" +
416                 " \"support-url\" \"mailto:cyrus-bugs+@andrew.cmu.edu\")",
417                 "oK"}, "rEAD-wRITE");
418         mFolder.open(OpenMode.READ_WRITE);
419     }
420 
421     /**
422      * Test OK response to IMAP ID with crummy text afterwards too.
423      */
testImapIdOkParsing()424     public void testImapIdOkParsing() throws MessagingException {
425         MockTransport mockTransport = openAndInjectMockTransport();
426 
427         // try to open it
428         setupOpenFolder(mockTransport, new String[] {
429                 "* iD nIL",
430                 "oK [iD] bad-char-%"}, "rEAD-wRITE");
431         mFolder.open(OpenMode.READ_WRITE);
432     }
433 
434     /**
435      * Test BAD response to IMAP ID - also with bad parser chars
436      */
testImapIdBad()437     public void testImapIdBad() throws MessagingException {
438         MockTransport mockTransport = openAndInjectMockTransport();
439 
440         // try to open it
441         setupOpenFolder(mockTransport, new String[] {
442                 "bAD unknown command bad-char-%"}, "rEAD-wRITE");
443         mFolder.open(OpenMode.READ_WRITE);
444     }
445 
446     /**
447      * Confirm that when IMAP ID is not in capability, it is not sent/received.
448      * This supports RFC 2971 section 3, and is important because certain servers
449      * (e.g. imap.vodafone.net.nz) do not process the unexpected ID command properly.
450      */
testImapIdNotSupported()451     public void testImapIdNotSupported() throws MessagingException {
452         MockTransport mockTransport = openAndInjectMockTransport();
453 
454         // try to open it
455         setupOpenFolder(mockTransport, null, "rEAD-wRITE");
456         mFolder.open(OpenMode.READ_WRITE);
457     }
458 
459     /**
460      * Confirm that the non-conformant IMAP ID result seen on imap.secureserver.net fails
461      * to properly parse.
462      *   2 ID ("name" "com.google.android.email")
463      *   * ID( "name" "Godaddy IMAP" ... "version" "3.1.0")
464      *   2 OK ID completed
465      */
testImapIdSecureServerParseFail()466     public void testImapIdSecureServerParseFail() {
467         MockTransport mockTransport = openAndInjectMockTransport();
468 
469         // configure mock server to return malformed ID response
470         setupOpenFolder(mockTransport, new String[] {
471                 "* ID( \"name\" \"Godaddy IMAP\" \"version\" \"3.1.0\")",
472                 "oK"}, "rEAD-wRITE");
473         try {
474             mFolder.open(OpenMode.READ_WRITE);
475             fail("Expected MessagingException");
476         } catch (MessagingException expected) {
477         }
478     }
479 
480     /**
481      * Confirm that the connections to *.secureserver.net never send IMAP ID (see
482      * testImapIdSecureServerParseFail() for the reason why.)
483      */
testImapIdSecureServerNotSent()484     public void testImapIdSecureServerNotSent() throws MessagingException {
485         // Note, this is injected into mStore (which we don't use for this test)
486         MockTransport mockTransport = openAndInjectMockTransport();
487         mockTransport.setHost("eMail.sEcurEserVer.nEt");
488 
489         // Prime the expects pump as if the server wants IMAP ID, but we should not actually expect
490         // to send it, because the login code in the store should never actually send it (to this
491         // particular server).  This sequence is a minimized version of expectLogin().
492 
493         // Respond to the initial connection
494         mockTransport.expect(null, "* oK Imap 2000 Ready To Assist You");
495         // Return "ID" in the capability
496         expectCapability(mockTransport, true, false);
497         // No TLS
498         // No ID (the special case for this server)
499         // LOGIN
500         mockTransport.expect(getNextTag(false) + " LOGIN user \"password\"",
501                 getNextTag(true) + " " + "oK user authenticated (Success)");
502         // SELECT
503         expectSelect(mockTransport, FOLDER_ENCODED, "rEAD-wRITE");
504 
505         // Now open the folder.  Although the server indicates ID in the capabilities,
506         // we are not expecting the store to send the ID command (to this particular server).
507         mFolder.open(OpenMode.READ_WRITE);
508     }
509 
510     /**
511      * Test small Folder functions that don't really do anything in Imap
512      */
testSmallFolderFunctions()513     public void testSmallFolderFunctions() {
514         // canCreate() returns true
515         assertTrue(mFolder.canCreate(FolderType.HOLDS_FOLDERS));
516         assertTrue(mFolder.canCreate(FolderType.HOLDS_MESSAGES));
517     }
518 
519     /**
520      * Lightweight test to confirm that IMAP hasn't implemented any folder roles yet.
521      *
522      * TODO: Test this with multiple folders provided by mock server
523      * TODO: Implement XLIST and then support this
524      */
testNoFolderRolesYet()525     public void testNoFolderRolesYet() {
526         assertEquals(Folder.FolderRole.UNKNOWN, mFolder.getRole());
527     }
528 
529     /**
530      * Lightweight test to confirm that IMAP is requesting sent-message-upload.
531      * TODO: Implement Gmail-specific cases and handle this server-side
532      */
testSentUploadRequested()533     public void testSentUploadRequested() {
534         assertTrue(mStore.requireCopyMessageToSentFolder());
535     }
536 
537     /**
538      * TODO: Test the process of opening and indexing a mailbox with one unread message in it.
539      */
540 
541     /**
542      * TODO: Test the scenario where the transport is "open" but not really (e.g. server closed).
543     /**
544      * Set up a basic MockTransport. open it, and inject it into mStore
545      */
openAndInjectMockTransport()546     private MockTransport openAndInjectMockTransport() {
547         return openAndInjectMockTransport(Transport.CONNECTION_SECURITY_NONE, false);
548     }
549 
550     /**
551      * Set up a MockTransport with security settings
552      */
openAndInjectMockTransport(int connectionSecurity, boolean trustAllCertificates)553     private MockTransport openAndInjectMockTransport(int connectionSecurity,
554             boolean trustAllCertificates) {
555         // Create mock transport and inject it into the ImapStore that's already set up
556         MockTransport mockTransport = new MockTransport();
557         mockTransport.setSecurity(connectionSecurity, trustAllCertificates);
558         mockTransport.setHost("mock.server.com");
559         mStore.setTransportForTest(mockTransport);
560         return mockTransport;
561     }
562 
563     /**
564      * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open()
565      *
566      * @param mockTransport the mock transport we're using
567      */
setupOpenFolder(MockTransport mockTransport)568     private void setupOpenFolder(MockTransport mockTransport) {
569         setupOpenFolder(mockTransport, "rEAD-wRITE");
570     }
571 
572     /**
573      * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open()
574      *
575      * @param mockTransport the mock transport we're using
576      */
setupOpenFolder(MockTransport mockTransport, String readWriteMode)577     private void setupOpenFolder(MockTransport mockTransport, String readWriteMode) {
578         setupOpenFolder(mockTransport, new String[] {
579                 "* iD nIL", "oK"}, readWriteMode, false);
580     }
581 
582     /**
583      * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open()
584      * Also allows setting a custom IMAP ID.
585      *
586      * Also sets mNextTag, an int, which is useful if there are additional commands to inject.
587      *
588      * @param mockTransport the mock transport we're using
589      * @param imapIdResponse the expected series of responses to the IMAP ID command.  Non-final
590      *      lines should be tagged with *.  The final response should be untagged (the correct
591      *      tag will be added at runtime).  Pass "null" to test w/o IMAP ID.
592      * @param readWriteMode "READ-WRITE" or "READ-ONLY"
593      */
setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse, String readWriteMode)594     private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse,
595             String readWriteMode) {
596         setupOpenFolder(mockTransport, imapIdResponse, readWriteMode, false);
597     }
598 
setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse, String readWriteMode, boolean withUidPlus)599     private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse,
600             String readWriteMode, boolean withUidPlus) {
601         expectLogin(mockTransport, imapIdResponse, withUidPlus);
602         expectSelect(mockTransport, FOLDER_ENCODED, readWriteMode);
603     }
604 
605     /**
606      * Helper which stuffs the mock with the strings to satisfy a typical SELECT.
607      * @param mockTransport the mock transport we're using
608      * @param readWriteMode "READ-WRITE" or "READ-ONLY"
609      */
expectSelect(MockTransport mockTransport, String folder, String readWriteMode)610     private void expectSelect(MockTransport mockTransport, String folder, String readWriteMode) {
611         mockTransport.expect(
612                 getNextTag(false) + " SELECT \"" + folder + "\"", new String[] {
613                 "* fLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
614                 "* oK [pERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]",
615                 "* 0 eXISTS",
616                 "* 0 rECENT",
617                 "* OK [uNSEEN 0]",
618                 "* OK [uIDNEXT 1]",
619                 getNextTag(true) + " oK [" + readWriteMode + "] " +
620                         folder + " selected. (Success)"});
621     }
622 
expectLogin(MockTransport mockTransport)623     private void expectLogin(MockTransport mockTransport) {
624         expectLogin(mockTransport, new String[] {"* iD nIL", "oK"}, false);
625     }
626 
expectLogin(MockTransport mockTransport, String[] imapIdResponse, boolean withUidPlus)627     private void expectLogin(MockTransport mockTransport, String[] imapIdResponse,
628             boolean withUidPlus) {
629         expectLogin(mockTransport, false, (imapIdResponse != null), withUidPlus, imapIdResponse,
630                 "oK user authenticated (Success)");
631     }
632 
expectLogin(MockTransport mockTransport, boolean startTls, boolean withId, boolean withUidPlus, String[] imapIdResponse, String loginResponse)633     private void expectLogin(MockTransport mockTransport, boolean startTls, boolean withId,
634             boolean withUidPlus, String[] imapIdResponse, String loginResponse) {
635         // inject boilerplate commands that match our typical login
636         mockTransport.expect(null, "* oK Imap 2000 Ready To Assist You");
637 
638         expectCapability(mockTransport, withId, withUidPlus);
639 
640         // TLS (if expected)
641         if (startTls) {
642             mockTransport.expect(getNextTag(false) + " STARTTLS",
643                 getNextTag(true) + " Ok starting TLS");
644             mockTransport.expectStartTls();
645             // After switching to TLS the client must re-query for capability
646             expectCapability(mockTransport, withId, withUidPlus);
647         }
648 
649         // ID
650         if (withId) {
651             String expectedNextTag = getNextTag(false);
652             // Fix the tag # of the ID response
653             String last = imapIdResponse[imapIdResponse.length-1];
654             last = expectedNextTag + " " + last;
655             imapIdResponse[imapIdResponse.length-1] = last;
656             mockTransport.expect(getNextTag(false) + " ID \\(.*\\)", imapIdResponse);
657             getNextTag(true); // Advance the tag for ID response.
658         }
659 
660         // LOGIN
661         mockTransport.expect(getNextTag(false) + " LOGIN user \"password\"",
662                 getNextTag(true) + " " + loginResponse);
663     }
664 
expectCapability(MockTransport mockTransport, boolean withId, boolean withUidPlus)665     private void expectCapability(MockTransport mockTransport, boolean withId,
666             boolean withUidPlus) {
667         String capabilityList = "* cAPABILITY iMAP4rev1 sTARTTLS aUTH=gSSAPI lOGINDISABLED";
668         capabilityList += withId ? " iD" : "";
669         capabilityList += withUidPlus ? " UiDPlUs" : "";
670 
671         mockTransport.expect(getNextTag(false) + " CAPABILITY", new String[] {
672             capabilityList,
673             getNextTag(true) + " oK CAPABILITY completed"});
674     }
675 
expectNoop(MockTransport mockTransport, boolean ok)676     private void expectNoop(MockTransport mockTransport, boolean ok) {
677         String response = ok ? " oK success" : " nO timeout";
678         mockTransport.expect(getNextTag(false) + " NOOP",
679                 new String[] {getNextTag(true) + response});
680     }
681 
682     /**
683      * Return a tag for use in setting up expect strings.  Typically this is called in pairs,
684      * first as getNextTag(false) when emitting the command, then as getNextTag(true) when
685      * emitting the final line of the expected response.
686      * @param advance true to increment mNextTag for the subsequence command
687      * @return a string containing the current tag
688      */
getNextTag(boolean advance)689     public String getNextTag(boolean advance)  {
690         if (advance) ++mTag;
691         return Integer.toString(mTag);
692     }
693 
694     /**
695      * Resets the tag back to it's starting value. Do this after the test connection has been
696      * closed.
697      */
resetTag()698     private int resetTag() {
699         return resetTag(1);
700     }
701 
resetTag(int tag)702     private int resetTag(int tag) {
703         int oldTag = mTag;
704         mTag = tag;
705         return oldTag;
706     }
707 
708     /**
709      * Test that servers reporting READ-WRITE mode are parsed properly
710      * Note: the READ_WRITE mode passed to folder.open() does not affect the test
711      */
testReadWrite()712     public void testReadWrite() throws MessagingException {
713         MockTransport mock = openAndInjectMockTransport();
714         setupOpenFolder(mock, "rEAD-WRITE");
715         mFolder.open(OpenMode.READ_WRITE);
716         assertEquals(OpenMode.READ_WRITE, mFolder.getMode());
717     }
718 
719     /**
720      * Test that servers reporting READ-ONLY mode are parsed properly
721      * Note: the READ_ONLY mode passed to folder.open() does not affect the test
722      */
testReadOnly()723     public void testReadOnly() throws MessagingException {
724         MockTransport mock = openAndInjectMockTransport();
725         setupOpenFolder(mock, "rEAD-ONLY");
726         mFolder.open(OpenMode.READ_ONLY);
727         assertEquals(OpenMode.READ_ONLY, mFolder.getMode());
728     }
729 
730     /**
731      * Test for getUnreadMessageCount with quoted string in the middle of response.
732      */
testGetUnreadMessageCountWithQuotedString()733     public void testGetUnreadMessageCountWithQuotedString() throws Exception {
734         MockTransport mock = openAndInjectMockTransport();
735         setupOpenFolder(mock);
736         mock.expect(
737                 getNextTag(false) + " STATUS \"" + FOLDER_ENCODED + "\" \\(UNSEEN\\)",
738                 new String[] {
739                 "* sTATUS \"" + FOLDER_ENCODED + "\" (uNSEEN 2)",
740                 getNextTag(true) + " oK STATUS completed"});
741         mFolder.open(OpenMode.READ_WRITE);
742         int unreadCount = mFolder.getUnreadMessageCount();
743         assertEquals("getUnreadMessageCount with quoted string", 2, unreadCount);
744     }
745 
746     /**
747      * Test for getUnreadMessageCount with literal string in the middle of response.
748      */
testGetUnreadMessageCountWithLiteralString()749     public void testGetUnreadMessageCountWithLiteralString() throws Exception {
750         MockTransport mock = openAndInjectMockTransport();
751         setupOpenFolder(mock);
752         mock.expect(
753                 getNextTag(false) + " STATUS \"" + FOLDER_ENCODED + "\" \\(UNSEEN\\)",
754                 new String[] {
755                 "* sTATUS {5}",
756                 FOLDER_ENCODED + " (uNSEEN 10)",
757                 getNextTag(true) + " oK STATUS completed"});
758         mFolder.open(OpenMode.READ_WRITE);
759         int unreadCount = mFolder.getUnreadMessageCount();
760         assertEquals("getUnreadMessageCount with literal string", 10, unreadCount);
761     }
762 
testFetchFlagEnvelope()763     public void testFetchFlagEnvelope() throws MessagingException {
764         final MockTransport mock = openAndInjectMockTransport();
765         setupOpenFolder(mock);
766         mFolder.open(OpenMode.READ_WRITE);
767         final Message message = mFolder.createMessage("1");
768 
769         final FetchProfile fp = new FetchProfile();
770         fp.add(FetchProfile.Item.FLAGS);
771         fp.add(FetchProfile.Item.ENVELOPE);
772         mock.expect(getNextTag(false) +
773                 " UID FETCH 1 \\(UID FLAGS INTERNALDATE RFC822\\.SIZE BODY\\.PEEK\\[HEADER.FIELDS" +
774                         " \\(date subject from content-type to cc message-id\\)\\]\\)",
775                 new String[] {
776                 "* 9 fETCH (uID 1 rFC822.sIZE 120626 iNTERNALDATE \"17-may-2010 22:00:15 +0000\"" +
777                         "fLAGS (\\Seen) bODY[hEADER.FIELDS (dAte sUbject fRom cOntent-type tO cC" +
778                         " mEssage-id)]" +
779                         " {279}",
780                 "From: Xxxxxx Yyyyy <userxx@android.com>",
781                 "Date: Mon, 17 May 2010 14:59:52 -0700",
782                 "Message-ID: <x0000000000000000000000000000000000000000000000y@android.com>",
783                 "Subject: ssubject",
784                 "To: android.test01@android.com",
785                 "Content-Type: multipart/mixed; boundary=a00000000000000000000000000b",
786                 "",
787                 ")",
788                 getNextTag(true) + " oK SUCCESS"
789         });
790         mFolder.fetch(new Message[] { message }, fp, null);
791 
792         assertEquals("android.test01@android.com", message.getHeader("to")[0]);
793         assertEquals("Xxxxxx Yyyyy <userxx@android.com>", message.getHeader("from")[0]);
794         assertEquals("multipart/mixed; boundary=a00000000000000000000000000b",
795                 message.getHeader("Content-Type")[0]);
796         assertTrue(message.isSet(Flag.SEEN));
797 
798         // TODO: Test NO response.
799     }
800 
801     /**
802      * Test for fetching simple BODYSTRUCTURE.
803      */
testFetchBodyStructureSimple()804     public void testFetchBodyStructureSimple() throws Exception {
805         final MockTransport mock = openAndInjectMockTransport();
806         setupOpenFolder(mock);
807         mFolder.open(OpenMode.READ_WRITE);
808         final Message message = mFolder.createMessage("1");
809 
810         final FetchProfile fp = new FetchProfile();
811         fp.add(FetchProfile.Item.STRUCTURE);
812         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
813                 new String[] {
814                 "* 9 fETCH (uID 1 bODYSTRUCTURE (\"tEXT\" \"pLAIN\" nIL" +
815                         " nIL nIL nIL 18 3 nIL nIL nIL))",
816                 getNextTag(true) + " oK sUCCESS"
817         });
818         mFolder.fetch(new Message[] { message }, fp, null);
819 
820         // Check mime structure...
821         MoreAsserts.assertEquals(
822                 new String[] {"text/plain"},
823                 message.getHeader("Content-Type")
824                 );
825         assertNull(message.getHeader("Content-Transfer-Encoding"));
826         assertNull(message.getHeader("Content-ID"));
827         MoreAsserts.assertEquals(
828                 new String[] {";\n size=18"},
829                 message.getHeader("Content-Disposition")
830                 );
831 
832         MoreAsserts.assertEquals(
833                 new String[] {"TEXT"},
834                 message.getHeader("X-Android-Attachment-StoreData")
835                 );
836 
837         // TODO: Test NO response.
838     }
839 
840     /**
841      * Test for fetching complex muiltipart BODYSTRUCTURE.
842      */
testFetchBodyStructureMultipart()843     public void testFetchBodyStructureMultipart() throws Exception {
844         final MockTransport mock = openAndInjectMockTransport();
845         setupOpenFolder(mock);
846         mFolder.open(OpenMode.READ_WRITE);
847         final Message message = mFolder.createMessage("1");
848 
849         final FetchProfile fp = new FetchProfile();
850         fp.add(FetchProfile.Item.STRUCTURE);
851         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
852                 new String[] {
853                 "* 9 fETCH (uID 1 bODYSTRUCTURE ((\"tEXT\" \"pLAIN\" () {20}",
854                 "long content id#@!@#" +
855                     " NIL \"7BIT\" 18 3 NIL NIL NIL)" +
856                     "(\"IMAGE\" \"PNG\" (\"NAME\" {10}",
857                 "device.png) NIL NIL \"BASE64\" {6}",
858                 "117840 NIL (\"aTTACHMENT\" (\"fILENAME\" \"device.png\")) NIL)" +
859                     "(\"TEXT\" \"HTML\" () NIL NIL \"7BIT\" 100 NIL 123 (\"aTTACHMENT\"" +
860                     "(\"fILENAME\" {15}",
861                 "attachment.html \"SIZE\" 555)) NIL)" +
862                     "((\"TEXT\" \"HTML\" NIL NIL \"BASE64\")(\"XXX\" \"YYY\"))" + // Nested
863                     "\"mIXED\" (\"bOUNDARY\" \"00032556278a7005e40486d159ca\") NIL NIL))",
864                 getNextTag(true) + " oK SUCCESS"
865         });
866         mFolder.fetch(new Message[] { message }, fp, null);
867 
868         // Check mime structure...
869         final Body body = message.getBody();
870         assertTrue(body instanceof MimeMultipart);
871         MimeMultipart mimeMultipart = (MimeMultipart) body;
872         assertEquals(4, mimeMultipart.getCount());
873         assertEquals("mixed", mimeMultipart.getSubTypeForTest());
874 
875         final Part part1 = mimeMultipart.getBodyPart(0);
876         final Part part2 = mimeMultipart.getBodyPart(1);
877         final Part part3 = mimeMultipart.getBodyPart(2);
878         final Part part4 = mimeMultipart.getBodyPart(3);
879         assertTrue(part1 instanceof MimeBodyPart);
880         assertTrue(part2 instanceof MimeBodyPart);
881         assertTrue(part3 instanceof MimeBodyPart);
882         assertTrue(part4 instanceof MimeBodyPart);
883 
884         final MimeBodyPart mimePart1 = (MimeBodyPart) part1; // text/plain
885         final MimeBodyPart mimePart2 = (MimeBodyPart) part2; // image/png
886         final MimeBodyPart mimePart3 = (MimeBodyPart) part3; // text/html
887         final MimeBodyPart mimePart4 = (MimeBodyPart) part4; // Nested
888 
889         MoreAsserts.assertEquals(
890                 new String[] {"1"},
891                 part1.getHeader("X-Android-Attachment-StoreData")
892                 );
893         MoreAsserts.assertEquals(
894                 new String[] {"2"},
895                 part2.getHeader("X-Android-Attachment-StoreData")
896                 );
897         MoreAsserts.assertEquals(
898                 new String[] {"3"},
899                 part3.getHeader("X-Android-Attachment-StoreData")
900                 );
901 
902         MoreAsserts.assertEquals(
903                 new String[] {"text/plain"},
904                 part1.getHeader("Content-Type")
905                 );
906         MoreAsserts.assertEquals(
907                 new String[] {"image/png;\n NAME=\"device.png\""},
908                 part2.getHeader("Content-Type")
909                 );
910         MoreAsserts.assertEquals(
911                 new String[] {"text/html"},
912                 part3.getHeader("Content-Type")
913                 );
914 
915         MoreAsserts.assertEquals(
916                 new String[] {"long content id#@!@#"},
917                 part1.getHeader("Content-ID")
918                 );
919         assertNull(part2.getHeader("Content-ID"));
920         assertNull(part3.getHeader("Content-ID"));
921 
922         MoreAsserts.assertEquals(
923                 new String[] {"7BIT"},
924                 part1.getHeader("Content-Transfer-Encoding")
925                 );
926         MoreAsserts.assertEquals(
927                 new String[] {"BASE64"},
928                 part2.getHeader("Content-Transfer-Encoding")
929                 );
930         MoreAsserts.assertEquals(
931                 new String[] {"7BIT"},
932                 part3.getHeader("Content-Transfer-Encoding")
933                 );
934 
935         MoreAsserts.assertEquals(
936                 new String[] {";\n size=18"},
937                 part1.getHeader("Content-Disposition")
938                 );
939         MoreAsserts.assertEquals(
940                 new String[] {"attachment;\n filename=\"device.png\";\n size=117840"},
941                 part2.getHeader("Content-Disposition")
942                 );
943         MoreAsserts.assertEquals(
944                 new String[] {"attachment;\n filename=\"attachment.html\";\n size=\"555\""},
945                 part3.getHeader("Content-Disposition")
946                 );
947 
948         // Check the nested parts.
949         final Body part4body = part4.getBody();
950         assertTrue(part4body instanceof MimeMultipart);
951         MimeMultipart mimeMultipartPart4 = (MimeMultipart) part4body;
952         assertEquals(2, mimeMultipartPart4.getCount());
953 
954         final MimeBodyPart mimePart41 = (MimeBodyPart) mimeMultipartPart4.getBodyPart(0);
955         final MimeBodyPart mimePart42 = (MimeBodyPart) mimeMultipartPart4.getBodyPart(1);
956 
957         MoreAsserts.assertEquals(new String[] {"4.1"},
958                 mimePart41.getHeader("X-Android-Attachment-StoreData")
959                 );
960         MoreAsserts.assertEquals(new String[] {"4.2"},
961                 mimePart42.getHeader("X-Android-Attachment-StoreData")
962                 );
963     }
964 
testFetchBodySane()965     public void testFetchBodySane() throws MessagingException {
966         final MockTransport mock = openAndInjectMockTransport();
967         setupOpenFolder(mock);
968         mFolder.open(OpenMode.READ_WRITE);
969         final Message message = mFolder.createMessage("1");
970 
971         final FetchProfile fp = new FetchProfile();
972         fp.add(FetchProfile.Item.BODY_SANE);
973         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[\\]<0.51200>\\)",
974                 new String[] {
975                 "* 9 fETCH (uID 1 bODY[] {23}",
976                 "from: a@b.com", // 15 bytes
977                 "", // 2
978                 "test", // 6
979                 ")",
980                 getNextTag(true) + " oK SUCCESS"
981         });
982         mFolder.fetch(new Message[] { message }, fp, null);
983         assertEquals("a@b.com", message.getHeader("from")[0]);
984 
985         // TODO: Test NO response.
986     }
987 
testFetchBody()988     public void testFetchBody() throws MessagingException {
989         final MockTransport mock = openAndInjectMockTransport();
990         setupOpenFolder(mock);
991         mFolder.open(OpenMode.READ_WRITE);
992         final Message message = mFolder.createMessage("1");
993 
994         final FetchProfile fp = new FetchProfile();
995         fp.add(FetchProfile.Item.BODY);
996         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[\\]\\)",
997                 new String[] {
998                 "* 9 fETCH (uID 1 bODY[] {23}",
999                 "from: a@b.com", // 15 bytes
1000                 "", // 2
1001                 "test", // 6
1002                 ")",
1003                 getNextTag(true) + " oK SUCCESS"
1004         });
1005         mFolder.fetch(new Message[] { message }, fp, null);
1006         assertEquals("a@b.com", message.getHeader("from")[0]);
1007 
1008         // TODO: Test NO response.
1009     }
1010 
testFetchAttachment()1011     public void testFetchAttachment() throws Exception {
1012         MockTransport mock = openAndInjectMockTransport();
1013         setupOpenFolder(mock);
1014         mFolder.open(OpenMode.READ_WRITE);
1015         final Message message = mFolder.createMessage("1");
1016 
1017         final FetchProfile fp = new FetchProfile();
1018         fp.add(FetchProfile.Item.STRUCTURE);
1019         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
1020                 new String[] {
1021                 "* 9 fETCH (uID 1 bODYSTRUCTURE ((\"tEXT\" \"PLAIN\" (\"cHARSET\" \"iSO-8859-1\")" +
1022                         " CID nIL \"7bIT\" 18 3 NIL NIL NIL)" +
1023                         "(\"IMAGE\" \"PNG\"" +
1024                         " (\"nAME\" \"device.png\") NIL NIL \"bASE64\" 117840 NIL (\"aTTACHMENT\"" +
1025                         "(\"fILENAME\" \"device.png\")) NIL)" +
1026                         "\"mIXED\"))",
1027                 getNextTag(true) + " OK SUCCESS"
1028         });
1029         mFolder.fetch(new Message[] { message }, fp, null);
1030 
1031         // Check mime structure, and get the second part.
1032         Body body = message.getBody();
1033         assertTrue(body instanceof MimeMultipart);
1034         MimeMultipart mimeMultipart = (MimeMultipart) body;
1035         assertEquals(2, mimeMultipart.getCount());
1036 
1037         Part part1 = mimeMultipart.getBodyPart(1);
1038         assertTrue(part1 instanceof MimeBodyPart);
1039         MimeBodyPart mimePart1 = (MimeBodyPart) part1;
1040 
1041         // Fetch the second part
1042         fp.clear();
1043         fp.add(mimePart1);
1044         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[2\\]\\)",
1045                 new String[] {
1046                 "* 9 fETCH (uID 1 bODY[2] {4}",
1047                 "YWJj)", // abc in base64
1048                 getNextTag(true) + " oK SUCCESS"
1049         });
1050         mFolder.fetch(new Message[] { message }, fp, null);
1051 
1052         assertEquals("abc",
1053                 Utility.fromUtf8(IOUtils.toByteArray(mimePart1.getBody().getInputStream())));
1054 
1055         // TODO: Test NO response.
1056     }
1057 
1058     /**
1059      * Test for proper operations on servers that return "NIL" for empty message bodies.
1060      */
testNilMessage()1061     public void testNilMessage() throws MessagingException {
1062         MockTransport mock = openAndInjectMockTransport();
1063         setupOpenFolder(mock);
1064         mFolder.open(OpenMode.READ_WRITE);
1065 
1066         // Prepare to pull structure and peek body text - this is like the "large message"
1067         // loop in MessagingController.synchronizeMailboxGeneric()
1068         FetchProfile fp = new FetchProfile();fp.clear();
1069         fp.add(FetchProfile.Item.STRUCTURE);
1070         Message message1 = mFolder.createMessage("1");
1071         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", new String[] {
1072                 "* 1 fETCH (uID 1 bODYSTRUCTURE (tEXT pLAIN nIL nIL nIL 7bIT 0 0 nIL nIL nIL))",
1073                 getNextTag(true) + " oK SUCCESS"
1074         });
1075         mFolder.fetch(new Message[] { message1 }, fp, null);
1076 
1077         // The expected result for an empty body is:
1078         //   * 1 FETCH (UID 1 BODY[TEXT] {0})
1079         // But some servers are returning NIL for the empty body:
1080         //   * 1 FETCH (UID 1 BODY[TEXT] NIL)
1081         // Because this breaks our little parser, fetch() skips over empty parts.
1082         // The rest of this test is confirming that this is the case.
1083 
1084         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[TEXT\\]\\)", new String[] {
1085                 "* 1 fETCH (uID 1 bODY[tEXT] nIL)",
1086                 getNextTag(true) + " oK SUCCESS"
1087         });
1088         ArrayList<Part> viewables = new ArrayList<Part>();
1089         ArrayList<Part> attachments = new ArrayList<Part>();
1090         MimeUtility.collectParts(message1, viewables, attachments);
1091         assertTrue(viewables.size() == 1);
1092         Part emptyBodyPart = viewables.get(0);
1093         fp.clear();
1094         fp.add(emptyBodyPart);
1095         mFolder.fetch(new Message[] { message1 }, fp, null);
1096 
1097         // If this wasn't working properly, there would be an attempted interpretation
1098         // of the empty part's NIL and possibly a crash.
1099 
1100         // If this worked properly, the "empty" body can now be retrieved
1101         viewables = new ArrayList<Part>();
1102         attachments = new ArrayList<Part>();
1103         MimeUtility.collectParts(message1, viewables, attachments);
1104         assertTrue(viewables.size() == 1);
1105         emptyBodyPart = viewables.get(0);
1106         String text = MimeUtility.getTextFromPart(emptyBodyPart);
1107         assertNull(text);
1108     }
1109 
1110     /**
1111      * Confirm the IMAP parser won't crash when seeing an excess FETCH response line without UID.
1112      *
1113      * <p>We've observed that the secure.emailsrvr.com email server returns an excess FETCH response
1114      * for a UID FETCH command.  These excess responses doesn't have the UID field in it, even
1115      * though we request, which led the response parser to crash.  We fixed it by ignoring response
1116      * lines that don't have UID.  This test is to make sure this case.
1117      */
testExcessFetchResult()1118     public void testExcessFetchResult() throws MessagingException {
1119         MockTransport mock = openAndInjectMockTransport();
1120         setupOpenFolder(mock);
1121         mFolder.open(OpenMode.READ_WRITE);
1122 
1123         // Create a message, and make sure it's not "SEEN".
1124         Message message1 = mFolder.createMessage("1");
1125         assertFalse(message1.isSet(Flag.SEEN));
1126 
1127         FetchProfile fp = new FetchProfile();
1128         fp.clear();
1129         fp.add(FetchProfile.Item.FLAGS);
1130         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID FLAGS\\)",
1131                 new String[] {
1132                 "* 1 fETCH (uID 1 fLAGS (\\Seen))",
1133                 "* 2 fETCH (fLAGS (\\Seen))",
1134                 getNextTag(true) + " oK SUCCESS"
1135         });
1136 
1137         // Shouldn't crash
1138         mFolder.fetch(new Message[] { message1 }, fp, null);
1139 
1140         // And the message is "SEEN".
1141         assertTrue(message1.isSet(Flag.SEEN));
1142     }
1143 
1144 
prepareForAppendTest(MockTransport mock, String response)1145     private ImapMessage prepareForAppendTest(MockTransport mock, String response) throws Exception {
1146         ImapMessage message = (ImapMessage) mFolder.createMessage("initial uid");
1147         message.setFrom(new Address("me@test.com"));
1148         message.setRecipient(RecipientType.TO, new Address("you@test.com"));
1149         message.setMessageId("<message.id@test.com>");
1150         message.setFlagDirectlyForTest(Flag.SEEN, true);
1151         message.setBody(new TextBody("Test Body"));
1152 
1153         // + go ahead
1154         // * 12345 EXISTS
1155         // OK [APPENDUID 627684530 17] (Success)
1156 
1157         mock.expect(getNextTag(false) +
1158                 " APPEND \\\"" + FOLDER_ENCODED + "\\\" \\(\\\\SEEN\\) \\{166\\}",
1159                 new String[] {"+ gO aHead"});
1160 
1161         mock.expectLiterally("From: me@test.com", NO_REPLY);
1162         mock.expectLiterally("To: you@test.com", NO_REPLY);
1163         mock.expectLiterally("Message-ID: <message.id@test.com>", NO_REPLY);
1164         mock.expectLiterally("Content-Type: text/plain;", NO_REPLY);
1165         mock.expectLiterally(" charset=utf-8", NO_REPLY);
1166         mock.expectLiterally("Content-Transfer-Encoding: base64", NO_REPLY);
1167         mock.expectLiterally("", NO_REPLY);
1168         mock.expectLiterally("VGVzdCBCb2R5", NO_REPLY);
1169         mock.expectLiterally("", new String[] {
1170                 "* 7 eXISTS",
1171                 getNextTag(true) + " " + response
1172                 });
1173         return message;
1174     }
1175 
1176     /**
1177      * Test for APPEND when the response has APPENDUID.
1178      */
testAppendMessages()1179     public void testAppendMessages() throws Exception {
1180         MockTransport mock = openAndInjectMockTransport();
1181         setupOpenFolder(mock);
1182         mFolder.open(OpenMode.READ_WRITE);
1183 
1184         ImapMessage message = prepareForAppendTest(mock, "oK [aPPENDUID 1234567 13] (Success)");
1185 
1186         mFolder.appendMessages(new Message[] {message});
1187 
1188         assertEquals("13", message.getUid());
1189         assertEquals(7, mFolder.getMessageCount());
1190     }
1191 
1192     /**
1193      * Test for APPEND when the response doesn't have APPENDUID.
1194      */
testAppendMessagesNoAppendUid()1195     public void testAppendMessagesNoAppendUid() throws Exception {
1196         MockTransport mock = openAndInjectMockTransport();
1197         setupOpenFolder(mock);
1198         mFolder.open(OpenMode.READ_WRITE);
1199 
1200         ImapMessage message = prepareForAppendTest(mock, "OK Success");
1201 
1202         // First try w/o parenthesis
1203         mock.expectLiterally(
1204                 getNextTag(false) + " UID SEARCH HEADER MESSAGE-ID <message.id@test.com>",
1205                 new String[] {
1206                     "* sEARCH 321",
1207                     getNextTag(true) + " oK success"
1208                 });
1209         // If that fails, then try w/ parenthesis
1210         mock.expectLiterally(
1211                 getNextTag(false) + " UID SEARCH (HEADER MESSAGE-ID <message.id@test.com>)",
1212                 new String[] {
1213                     "* sEARCH 321",
1214                     getNextTag(true) + " oK success"
1215                 });
1216 
1217         mFolder.appendMessages(new Message[] {message});
1218 
1219         assertEquals("321", message.getUid());
1220     }
1221 
1222     /**
1223      * Test for append failure.
1224      *
1225      * We don't check the response for APPEND.  We just SEARCH for the message-id to get the UID.
1226      * If append has failed, the SEARCH command returns no UID, and the UID of the message is left
1227      * unset.
1228      */
testAppendFailure()1229     public void testAppendFailure() throws Exception {
1230         MockTransport mock = openAndInjectMockTransport();
1231         setupOpenFolder(mock);
1232         mFolder.open(OpenMode.READ_WRITE);
1233 
1234         ImapMessage message = prepareForAppendTest(mock, "NO No space left on the server.");
1235         assertEquals("initial uid", message.getUid());
1236         // First try w/o parenthesis
1237         mock.expectLiterally(
1238                 getNextTag(false) + " UID SEARCH HEADER MESSAGE-ID <message.id@test.com>",
1239                 new String[] {
1240                     "* sEARCH", // not found
1241                     getNextTag(true) + " oK Search completed."
1242                 });
1243         // If that fails, then try w/ parenthesis
1244         mock.expectLiterally(
1245                 getNextTag(false) + " UID SEARCH (HEADER MESSAGE-ID <message.id@test.com>)",
1246                 new String[] {
1247                     "* sEARCH", // not found
1248                     getNextTag(true) + " oK Search completed."
1249                 });
1250 
1251         mFolder.appendMessages(new Message[] {message});
1252 
1253         // Shouldn't have changed
1254         assertEquals("initial uid", message.getUid());
1255     }
1256 
testGetAllFolders()1257     public void testGetAllFolders() throws Exception {
1258         MockTransport mock = openAndInjectMockTransport();
1259         expectLogin(mock);
1260 
1261         expectNoop(mock, true);
1262         mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"",
1263                 new String[] {
1264                 "* lIST (\\HAsNoChildren) \"/\" \"inbox\"",
1265                 "* lIST (\\hAsnochildren) \"/\" \"Drafts\"",
1266                 "* lIST (\\nOselect) \"/\" \"no select\"",
1267                 "* lIST (\\HAsNoChildren) \"/\" \"&ZeVnLIqe-\"", // Japanese folder name
1268                 getNextTag(true) + " oK SUCCESS"
1269                 });
1270         Folder[] folders = mStore.updateFolders();
1271         ImapFolder testFolder;
1272 
1273         testFolder = (ImapFolder) folders[0];
1274         assertEquals("INBOX", testFolder.getName());
1275         assertEquals(SELECTABLE_BITS, testFolder.mMailbox.mFlags & SELECTABLE_BITS);
1276 
1277         testFolder = (ImapFolder) folders[1];
1278         assertEquals("no select", testFolder.getName());
1279         assertEquals(0, testFolder.mMailbox.mFlags & SELECTABLE_BITS);
1280 
1281         testFolder = (ImapFolder) folders[2];
1282         assertEquals("\u65E5\u672C\u8A9E", testFolder.getName());
1283         assertEquals(SELECTABLE_BITS, testFolder.mMailbox.mFlags & SELECTABLE_BITS);
1284 
1285         testFolder = (ImapFolder) folders[3];
1286         assertEquals("Drafts", testFolder.getName());
1287         assertEquals(SELECTABLE_BITS, testFolder.mMailbox.mFlags & SELECTABLE_BITS);
1288         // TODO test with path prefix
1289         // TODO: Test NO response.
1290     }
1291 
testEncodeFolderName()1292     public void testEncodeFolderName() {
1293         // null prefix
1294         assertEquals("",
1295                 ImapStore.encodeFolderName("", null));
1296         assertEquals("a",
1297                 ImapStore.encodeFolderName("a", null));
1298         assertEquals("XYZ",
1299                 ImapStore.encodeFolderName("XYZ", null));
1300         assertEquals("&ZeVnLIqe-",
1301                 ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", null));
1302         assertEquals("!&ZeVnLIqe-!",
1303                 ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", null));
1304         // empty prefix (same as a null prefix)
1305         assertEquals("",
1306                 ImapStore.encodeFolderName("", ""));
1307         assertEquals("a",
1308                 ImapStore.encodeFolderName("a", ""));
1309         assertEquals("XYZ",
1310                 ImapStore.encodeFolderName("XYZ", ""));
1311         assertEquals("&ZeVnLIqe-",
1312                 ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", ""));
1313         assertEquals("!&ZeVnLIqe-!",
1314                 ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", ""));
1315         // defined prefix
1316         assertEquals("[Gmail]/",
1317                 ImapStore.encodeFolderName("", "[Gmail]/"));
1318         assertEquals("[Gmail]/a",
1319                 ImapStore.encodeFolderName("a", "[Gmail]/"));
1320         assertEquals("[Gmail]/XYZ",
1321                 ImapStore.encodeFolderName("XYZ", "[Gmail]/"));
1322         assertEquals("[Gmail]/&ZeVnLIqe-",
1323                 ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", "[Gmail]/"));
1324         assertEquals("[Gmail]/!&ZeVnLIqe-!",
1325                 ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", "[Gmail]/"));
1326         // Add prefix to special mailbox "INBOX" [case insensitive), no affect
1327         assertEquals("INBOX",
1328                 ImapStore.encodeFolderName("INBOX", "[Gmail]/"));
1329         assertEquals("inbox",
1330                 ImapStore.encodeFolderName("inbox", "[Gmail]/"));
1331         assertEquals("InBoX",
1332                 ImapStore.encodeFolderName("InBoX", "[Gmail]/"));
1333     }
1334 
testDecodeFolderName()1335     public void testDecodeFolderName() {
1336         // null prefix
1337         assertEquals("",
1338                 ImapStore.decodeFolderName("", null));
1339         assertEquals("a",
1340                 ImapStore.decodeFolderName("a", null));
1341         assertEquals("XYZ",
1342                 ImapStore.decodeFolderName("XYZ", null));
1343         assertEquals("\u65E5\u672C\u8A9E",
1344                 ImapStore.decodeFolderName("&ZeVnLIqe-", null));
1345         assertEquals("!\u65E5\u672C\u8A9E!",
1346                 ImapStore.decodeFolderName("!&ZeVnLIqe-!", null));
1347         // empty prefix (same as a null prefix)
1348         assertEquals("",
1349                 ImapStore.decodeFolderName("", ""));
1350         assertEquals("a",
1351                 ImapStore.decodeFolderName("a", ""));
1352         assertEquals("XYZ",
1353                 ImapStore.decodeFolderName("XYZ", ""));
1354         assertEquals("\u65E5\u672C\u8A9E",
1355                 ImapStore.decodeFolderName("&ZeVnLIqe-", ""));
1356         assertEquals("!\u65E5\u672C\u8A9E!",
1357                 ImapStore.decodeFolderName("!&ZeVnLIqe-!", ""));
1358         // defined prefix; prefix found, prefix removed
1359         assertEquals("",
1360                 ImapStore.decodeFolderName("[Gmail]/", "[Gmail]/"));
1361         assertEquals("a",
1362                 ImapStore.decodeFolderName("[Gmail]/a", "[Gmail]/"));
1363         assertEquals("XYZ",
1364                 ImapStore.decodeFolderName("[Gmail]/XYZ", "[Gmail]/"));
1365         assertEquals("\u65E5\u672C\u8A9E",
1366                 ImapStore.decodeFolderName("[Gmail]/&ZeVnLIqe-", "[Gmail]/"));
1367         assertEquals("!\u65E5\u672C\u8A9E!",
1368                 ImapStore.decodeFolderName("[Gmail]/!&ZeVnLIqe-!", "[Gmail]/"));
1369         // defined prefix; prefix not found, no affect
1370         assertEquals("INBOX/",
1371                 ImapStore.decodeFolderName("INBOX/", "[Gmail]/"));
1372         assertEquals("INBOX/a",
1373                 ImapStore.decodeFolderName("INBOX/a", "[Gmail]/"));
1374         assertEquals("INBOX/XYZ",
1375                 ImapStore.decodeFolderName("INBOX/XYZ", "[Gmail]/"));
1376         assertEquals("INBOX/\u65E5\u672C\u8A9E",
1377                 ImapStore.decodeFolderName("INBOX/&ZeVnLIqe-", "[Gmail]/"));
1378         assertEquals("INBOX/!\u65E5\u672C\u8A9E!",
1379                 ImapStore.decodeFolderName("INBOX/!&ZeVnLIqe-!", "[Gmail]/"));
1380     }
1381 
testEnsurePrefixIsValid()1382     public void testEnsurePrefixIsValid() {
1383         // Test mPathSeparator == null
1384         mStore.mPathSeparator = null;
1385         mStore.mPathPrefix = null;
1386         mStore.ensurePrefixIsValid();
1387         assertNull(mStore.mPathPrefix);
1388 
1389         mStore.mPathPrefix = "";
1390         mStore.ensurePrefixIsValid();
1391         assertEquals("", mStore.mPathPrefix);
1392 
1393         mStore.mPathPrefix = "foo";
1394         mStore.ensurePrefixIsValid();
1395         assertEquals("foo", mStore.mPathPrefix);
1396 
1397         mStore.mPathPrefix = "foo.";
1398         mStore.ensurePrefixIsValid();
1399         assertEquals("foo.", mStore.mPathPrefix);
1400 
1401         // Test mPathSeparator == ""
1402         mStore.mPathSeparator = "";
1403         mStore.mPathPrefix = null;
1404         mStore.ensurePrefixIsValid();
1405         assertNull(mStore.mPathPrefix);
1406 
1407         mStore.mPathPrefix = "";
1408         mStore.ensurePrefixIsValid();
1409         assertEquals("", mStore.mPathPrefix);
1410 
1411         mStore.mPathPrefix = "foo";
1412         mStore.ensurePrefixIsValid();
1413         assertEquals("foo", mStore.mPathPrefix);
1414 
1415         mStore.mPathPrefix = "foo.";
1416         mStore.ensurePrefixIsValid();
1417         assertEquals("foo.", mStore.mPathPrefix);
1418 
1419         // Test mPathSeparator is non-empty
1420         mStore.mPathSeparator = ".";
1421         mStore.mPathPrefix = null;
1422         mStore.ensurePrefixIsValid();
1423         assertNull(mStore.mPathPrefix);
1424 
1425         mStore.mPathPrefix = "";
1426         mStore.ensurePrefixIsValid();
1427         assertEquals("", mStore.mPathPrefix);
1428 
1429         mStore.mPathPrefix = "foo";
1430         mStore.ensurePrefixIsValid();
1431         assertEquals("foo.", mStore.mPathPrefix);
1432 
1433         // Trailing separator; path separator NOT appended
1434         mStore.mPathPrefix = "foo.";
1435         mStore.ensurePrefixIsValid();
1436         assertEquals("foo.", mStore.mPathPrefix);
1437 
1438         // Trailing punctuation has no affect; path separator still appended
1439         mStore.mPathPrefix = "foo/";
1440         mStore.ensurePrefixIsValid();
1441         assertEquals("foo/.", mStore.mPathPrefix);
1442     }
1443 
testOpen()1444     public void testOpen() throws Exception {
1445         MockTransport mock = openAndInjectMockTransport();
1446         expectLogin(mock);
1447 
1448         final Folder folder = mStore.getFolder("test");
1449 
1450         // Not exist
1451         mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
1452                 new String[] {
1453                 getNextTag(true) + " nO no such mailbox"
1454                 });
1455         try {
1456             folder.open(OpenMode.READ_WRITE);
1457             fail();
1458         } catch (MessagingException expected) {
1459         }
1460 
1461         // READ-WRITE
1462         expectNoop(mock, true); // Need it because we reuse the connection.
1463         mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
1464                 new String[] {
1465                 "* 1 eXISTS",
1466                 getNextTag(true) + " oK [rEAD-wRITE]"
1467                 });
1468 
1469         folder.open(OpenMode.READ_WRITE);
1470         assertTrue(folder.exists());
1471         assertEquals(1, folder.getMessageCount());
1472         assertEquals(OpenMode.READ_WRITE, folder.getMode());
1473 
1474         assertTrue(folder.isOpen());
1475         folder.close(false);
1476         assertFalse(folder.isOpen());
1477 
1478         // READ-ONLY
1479         expectNoop(mock, true); // Need it because we reuse the connection.
1480         mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
1481                 new String[] {
1482                 "* 2 eXISTS",
1483                 getNextTag(true) + " oK [rEAD-oNLY]"
1484                 });
1485 
1486         folder.open(OpenMode.READ_WRITE);
1487         assertTrue(folder.exists());
1488         assertEquals(2, folder.getMessageCount());
1489         assertEquals(OpenMode.READ_ONLY, folder.getMode());
1490 
1491         // Try to re-open as read-write.  Should send SELECT again.
1492         expectNoop(mock, true); // Need it because we reuse the connection.
1493         mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
1494                 new String[] {
1495                 "* 15 eXISTS",
1496                 getNextTag(true) + " oK selected"
1497                 });
1498 
1499         folder.open(OpenMode.READ_WRITE);
1500         assertTrue(folder.exists());
1501         assertEquals(15, folder.getMessageCount());
1502         assertEquals(OpenMode.READ_WRITE, folder.getMode());
1503     }
1504 
testExists()1505     public void testExists() throws Exception {
1506         MockTransport mock = openAndInjectMockTransport();
1507         expectLogin(mock);
1508 
1509         // Folder exists
1510         Folder folder = mStore.getFolder("\u65E5\u672C\u8A9E");
1511         mock.expect(getNextTag(false) + " STATUS \\\"&ZeVnLIqe-\\\" \\(UIDVALIDITY\\)",
1512                 new String[] {
1513                 "* sTATUS \"&ZeVnLIqe-\" (mESSAGES 10)",
1514                 getNextTag(true) + " oK SUCCESS"
1515                 });
1516 
1517         assertTrue(folder.exists());
1518 
1519         // Connection verification
1520         expectNoop(mock, true);
1521 
1522         // Doesn't exist
1523         folder = mStore.getFolder("no such folder");
1524         mock.expect(getNextTag(false) + " STATUS \\\"no such folder\\\" \\(UIDVALIDITY\\)",
1525                 new String[] {
1526                 getNextTag(true) + " NO No such folder!"
1527                 });
1528 
1529         assertFalse(folder.exists());
1530     }
1531 
testCreate()1532     public void testCreate() throws Exception {
1533         MockTransport mock = openAndInjectMockTransport();
1534         expectLogin(mock);
1535 
1536         // Success
1537         Folder folder = mStore.getFolder("\u65E5\u672C\u8A9E");
1538 
1539         assertTrue(folder.canCreate(FolderType.HOLDS_MESSAGES));
1540 
1541         mock.expect(getNextTag(false) + " CREATE \\\"&ZeVnLIqe-\\\"",
1542                 new String[] {
1543                 getNextTag(true) + " oK Success"
1544                 });
1545 
1546         assertTrue(folder.create(FolderType.HOLDS_MESSAGES));
1547 
1548         // Connection verification
1549         expectNoop(mock, true);
1550 
1551         // Failure
1552         mock.expect(getNextTag(false) + " CREATE \\\"&ZeVnLIqe-\\\"",
1553                 new String[] {
1554                 getNextTag(true) + " nO Can't create folder"
1555                 });
1556 
1557         assertFalse(folder.create(FolderType.HOLDS_MESSAGES));
1558     }
1559 
setupCopyMessages(boolean withUidPlus)1560     private void setupCopyMessages(boolean withUidPlus) throws Exception {
1561         mCopyMock = openAndInjectMockTransport();
1562         setupOpenFolder(mCopyMock, new String[] {"* iD nIL", "oK"}, "rEAD-wRITE", withUidPlus);
1563         mFolder.open(OpenMode.READ_WRITE);
1564 
1565         mCopyToFolder = mStore.getFolder("\u65E5\u672C\u8A9E");
1566         Message m1 = mFolder.createMessage("11");
1567         m1.setMessageId("<4D8978AE.0000005D@m58.foo.com>");
1568         Message m2 = mFolder.createMessage("12");
1569         m2.setMessageId("<549373104MSOSI1:145OSIMS@bar.com>");
1570         mCopyMessages = new Message[] { m1, m2 };
1571     }
1572 
1573     /**
1574      * Returns the pattern for the IMAP request to copy messages.
1575      */
getCopyMessagesPattern()1576     private String getCopyMessagesPattern() {
1577         return getNextTag(false) + " UID COPY 11\\,12 \\\"&ZeVnLIqe-\\\"";
1578     }
1579 
1580     /**
1581      * Returns the pattern for the IMAP request to search for messages based on Message-Id.
1582      */
getSearchMessagesPattern(String messageId)1583     private String getSearchMessagesPattern(String messageId) {
1584         return getNextTag(false) + " UID SEARCH HEADER Message-Id \"" + messageId + "\"";
1585     }
1586 
1587     /**
1588      * Counts the number of times the callback methods are invoked.
1589      */
1590     private static class MessageUpdateCallbackCounter implements Folder.MessageUpdateCallbacks {
1591         int messageNotFoundCalled;
1592         int messageUidChangeCalled;
1593 
1594         @Override
onMessageNotFound(Message message)1595         public void onMessageNotFound(Message message) {
1596             ++messageNotFoundCalled;
1597         }
1598         @Override
onMessageUidChange(Message message, String newUid)1599         public void onMessageUidChange(Message message, String newUid) {
1600             ++messageUidChangeCalled;
1601         }
1602     }
1603 
1604     // TODO Test additional degenerate cases; src msg not found, ...
1605     // Golden case; successful copy with UIDCOPY result
testCopyMessages1()1606     public void testCopyMessages1() throws Exception {
1607         setupCopyMessages(true);
1608         mCopyMock.expect(getCopyMessagesPattern(),
1609                 new String[] {
1610                     "* Ok COPY in progress",
1611                     getNextTag(true) + " oK [COPYUID 777 11,12 45,46] UID COPY completed"
1612                 });
1613 
1614         MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
1615         mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);
1616 
1617         assertEquals(0, cb.messageNotFoundCalled);
1618         assertEquals(2, cb.messageUidChangeCalled);
1619     }
1620 
1621     // Degenerate case; NO, un-tagged response works
testCopyMessages2()1622     public void testCopyMessages2() throws Exception {
1623         setupCopyMessages(true);
1624         mCopyMock.expect(getCopyMessagesPattern(),
1625                 new String[] {
1626                     "* No Some error occured during the copy",
1627                     getNextTag(true) + " oK [COPYUID 777 11,12 45,46] UID COPY completed"
1628                 });
1629 
1630         MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
1631         mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);
1632 
1633         assertEquals(0, cb.messageNotFoundCalled);
1634         assertEquals(2, cb.messageUidChangeCalled);
1635     }
1636 
1637     // Degenerate case; NO, tagged response throws MessagingException
testCopyMessages3()1638     public void testCopyMessages3() throws Exception {
1639         try {
1640             setupCopyMessages(false);
1641             mCopyMock.expect(getCopyMessagesPattern(),
1642                     new String[] {
1643                         getNextTag(true) + " No copy did not finish"
1644                     });
1645 
1646             mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);
1647 
1648             fail("MessagingException expected.");
1649         } catch (MessagingException expected) {
1650         }
1651     }
1652 
1653     // Degenerate case; BAD, un-tagged response throws MessagingException
testCopyMessages4()1654     public void testCopyMessages4() throws Exception {
1655         try {
1656             setupCopyMessages(true);
1657             mCopyMock.expect(getCopyMessagesPattern(),
1658                     new String[] {
1659                         "* BAD failed for some reason",
1660                         getNextTag(true) + " Ok copy completed"
1661                     });
1662 
1663             mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);
1664 
1665             fail("MessagingException expected.");
1666         } catch (MessagingException expected) {
1667         }
1668     }
1669 
1670     // Degenerate case; BAD, tagged response throws MessagingException
testCopyMessages5()1671     public void testCopyMessages5() throws Exception {
1672         try {
1673             setupCopyMessages(false);
1674             mCopyMock.expect(getCopyMessagesPattern(),
1675                     new String[] {
1676                         getNextTag(true) + " BaD copy completed"
1677                     });
1678 
1679             mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);
1680 
1681             fail("MessagingException expected.");
1682         } catch (MessagingException expected) {
1683         }
1684     }
1685 
1686     // Golden case; successful copy getting UIDs via search
testCopyMessages6()1687     public void testCopyMessages6() throws Exception {
1688         setupCopyMessages(false);
1689         mCopyMock.expect(getCopyMessagesPattern(),
1690                 new String[] {
1691                     getNextTag(true) + " oK UID COPY completed",
1692                 });
1693         // New connection, so, we need to login again & the tag count gets reset
1694         int saveTag = resetTag();
1695         expectLogin(mCopyMock, new String[] {"* iD nIL", "oK"}, false);
1696         // Select destination folder
1697         expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
1698         // Perform searches
1699         mCopyMock.expect(getSearchMessagesPattern("<4D8978AE.0000005D@m58.foo.com>"),
1700                 new String[] {
1701                     "* SeArCh 777",
1702                     getNextTag(true) + " oK UID SEARCH completed (1 msgs in 3.14159 secs)",
1703                 });
1704         mCopyMock.expect(getSearchMessagesPattern("<549373104MSOSI1:145OSIMS@bar.com>"),
1705                 new String[] {
1706                     "* sEaRcH 1818",
1707                     getNextTag(true) + " oK UID SEARCH completed (1 msgs in 2.71828 secs)",
1708                 });
1709         // Resume commands on the initial connection
1710         resetTag(saveTag);
1711         // Select the original folder
1712         expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");
1713 
1714         MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
1715         mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);
1716 
1717         assertEquals(0, cb.messageNotFoundCalled);
1718         assertEquals(2, cb.messageUidChangeCalled);
1719     }
1720 
1721     // Degenerate case; searches turn up nothing
testCopyMessages7()1722     public void testCopyMessages7() throws Exception {
1723         setupCopyMessages(false);
1724         mCopyMock.expect(getCopyMessagesPattern(),
1725                 new String[] {
1726                     getNextTag(true) + " oK UID COPY completed",
1727                 });
1728         // New connection, so, we need to login again & the tag count gets reset
1729         int saveTag = resetTag();
1730         expectLogin(mCopyMock, new String[] {"* iD nIL", "oK"}, false);
1731         // Select destination folder
1732         expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
1733         // Perform searches
1734         mCopyMock.expect(getSearchMessagesPattern("<4D8978AE.0000005D@m58.foo.com>"),
1735                 new String[] {
1736                     "* SeArCh",
1737                     getNextTag(true) + " oK UID SEARCH completed (0 msgs in 6.02214 secs)",
1738                 });
1739         mCopyMock.expect(getSearchMessagesPattern("<549373104MSOSI1:145OSIMS@bar.com>"),
1740                 new String[] {
1741                     "* sEaRcH",
1742                     getNextTag(true) + " oK UID SEARCH completed (0 msgs in 2.99792 secs)",
1743                 });
1744         // Resume commands on the initial connection
1745         resetTag(saveTag);
1746         // Select the original folder
1747         expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");
1748 
1749         MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
1750         mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);
1751 
1752         assertEquals(0, cb.messageNotFoundCalled);
1753         assertEquals(0, cb.messageUidChangeCalled);
1754     }
1755 
1756     // Degenerate case; search causes an exception; must be eaten
testCopyMessages8()1757     public void testCopyMessages8() throws Exception {
1758         setupCopyMessages(false);
1759         mCopyMock.expect(getCopyMessagesPattern(),
1760                 new String[] {
1761                     getNextTag(true) + " oK UID COPY completed",
1762                 });
1763         // New connection, so, we need to login again & the tag count gets reset
1764         int saveTag = resetTag();
1765         expectLogin(mCopyMock, new String[] {"* iD nIL", "oK"}, false);
1766         // Select destination folder
1767         expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
1768         // Perform searches
1769         mCopyMock.expect(getSearchMessagesPattern("<4D8978AE.0000005D@m58.foo.com>"),
1770                 new String[] {
1771                     getNextTag(true) + " BaD search failed"
1772                 });
1773         mCopyMock.expect(getSearchMessagesPattern("<549373104MSOSI1:145OSIMS@bar.com>"),
1774                 new String[] {
1775                     getNextTag(true) + " BaD search failed"
1776                 });
1777         // Resume commands on the initial connection
1778         resetTag(saveTag);
1779         // Select the original folder
1780         expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");
1781 
1782         MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
1783         mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);
1784 
1785         assertEquals(0, cb.messageNotFoundCalled);
1786         assertEquals(0, cb.messageUidChangeCalled);
1787     }
1788 
testGetUnreadMessageCount()1789     public void testGetUnreadMessageCount() throws Exception {
1790         MockTransport mock = openAndInjectMockTransport();
1791         setupOpenFolder(mock);
1792         mFolder.open(OpenMode.READ_WRITE);
1793 
1794         mock.expect(getNextTag(false) + " STATUS \\\"" + FOLDER_ENCODED + "\\\" \\(UNSEEN\\)",
1795                 new String[] {
1796                 "* sTATUS \"" + FOLDER_ENCODED + "\" (X 1 uNSEEN 123)",
1797                 getNextTag(true) + " oK copy completed"
1798                 });
1799 
1800         assertEquals(123, mFolder.getUnreadMessageCount());
1801     }
1802 
testExpunge()1803     public void testExpunge() throws Exception {
1804         MockTransport mock = openAndInjectMockTransport();
1805         setupOpenFolder(mock);
1806         mFolder.open(OpenMode.READ_WRITE);
1807 
1808         mock.expect(getNextTag(false) + " EXPUNGE",
1809                 new String[] {
1810                 getNextTag(true) + " oK success"
1811                 });
1812 
1813         mFolder.expunge();
1814 
1815         // TODO: Test NO response. (permission denied)
1816     }
1817 
testSetFlags()1818     public void testSetFlags() throws Exception {
1819         MockTransport mock = openAndInjectMockTransport();
1820         setupOpenFolder(mock);
1821         mFolder.open(OpenMode.READ_WRITE);
1822 
1823         Message[] messages = new Message[] {
1824                 mFolder.createMessage("11"),
1825                 mFolder.createMessage("12"),
1826                 };
1827 
1828         // Set
1829         mock.expect(
1830                 getNextTag(false) + " UID STORE 11\\,12 \\+FLAGS.SILENT \\(\\\\FLAGGED \\\\SEEN\\)",
1831                 new String[] {
1832                 getNextTag(true) + " oK success"
1833                 });
1834         mFolder.setFlags(messages, new Flag[] {Flag.FLAGGED, Flag.SEEN}, true);
1835 
1836         // Clear
1837         mock.expect(
1838                 getNextTag(false) + " UID STORE 11\\,12 \\-FLAGS.SILENT \\(\\\\DELETED\\)",
1839                 new String[] {
1840                 getNextTag(true) + " oK success"
1841                 });
1842         mFolder.setFlags(messages, new Flag[] {Flag.DELETED}, false);
1843 
1844         // TODO: Test NO response. (src message not found)
1845     }
1846 
testSearchForUids()1847     public void testSearchForUids() throws Exception {
1848         MockTransport mock = openAndInjectMockTransport();
1849         setupOpenFolder(mock);
1850         mFolder.open(OpenMode.READ_WRITE);
1851 
1852         // Single results
1853         mock.expect(
1854                 getNextTag(false) + " UID SEARCH X",
1855                 new String[] {
1856                         "* sEARCH 1",
1857                         getNextTag(true) + " oK success"
1858                 });
1859         MoreAsserts.assertEquals(new String[] {
1860                 "1"
1861                 }, mFolder.searchForUids("X"));
1862 
1863         // Multiple results, including SEARCH with no UIDs.
1864         mock.expect(
1865                 getNextTag(false) + " UID SEARCH UID 123",
1866                 new String[] {
1867                         "* sEARCH 123 4 567",
1868                         "* search",
1869                         "* sEARCH 0",
1870                         "* SEARCH",
1871                         "* sEARCH 100 200 300",
1872                         getNextTag(true) + " oK success"
1873                 });
1874         MoreAsserts.assertEquals(new String[] {
1875                 "123", "4", "567", "0", "100", "200", "300"
1876                 }, mFolder.searchForUids("UID 123"));
1877 
1878         // NO result
1879         mock.expect(
1880                 getNextTag(false) + " UID SEARCH SOME CRITERIA",
1881                 new String[] {
1882                         getNextTag(true) + " nO not found"
1883                 });
1884         MoreAsserts.assertEquals(new String[] {
1885                 }, mFolder.searchForUids("SOME CRITERIA"));
1886 
1887         // OK result, but result is empty. (Probably against RFC)
1888         mock.expect(
1889                 getNextTag(false) + " UID SEARCH SOME CRITERIA",
1890                 new String[] {
1891                         getNextTag(true) + " oK success"
1892                 });
1893         MoreAsserts.assertEquals(new String[] {
1894                 }, mFolder.searchForUids("SOME CRITERIA"));
1895 
1896         // OK result with empty search response.
1897         mock.expect(
1898                 getNextTag(false) + " UID SEARCH SOME CRITERIA",
1899                 new String[] {
1900                         "* search",
1901                         getNextTag(true) + " oK success"
1902                 });
1903         MoreAsserts.assertEquals(new String[] {
1904                 }, mFolder.searchForUids("SOME CRITERIA"));
1905     }
1906 
1907 
testGetMessage()1908     public void testGetMessage() throws Exception {
1909         MockTransport mock = openAndInjectMockTransport();
1910         setupOpenFolder(mock);
1911         mFolder.open(OpenMode.READ_WRITE);
1912 
1913         // Found
1914         mock.expect(
1915                 getNextTag(false) + " UID SEARCH UID 123",
1916                 new String[] {
1917                     "* sEARCH 123",
1918                 getNextTag(true) + " oK success"
1919                 });
1920         assertEquals("123", mFolder.getMessage("123").getUid());
1921 
1922         // Not found
1923         mock.expect(
1924                 getNextTag(false) + " UID SEARCH UID 123",
1925                 new String[] {
1926                 getNextTag(true) + " nO not found"
1927                 });
1928         assertNull(mFolder.getMessage("123"));
1929     }
1930 
1931     /** Test for getMessages(int, int, MessageRetrievalListener) */
testGetMessages1()1932     public void testGetMessages1() throws Exception {
1933         MockTransport mock = openAndInjectMockTransport();
1934         setupOpenFolder(mock);
1935         mFolder.open(OpenMode.READ_WRITE);
1936 
1937         // Found
1938         mock.expect(
1939                 getNextTag(false) + " UID SEARCH 3:5 NOT DELETED",
1940                 new String[] {
1941                     "* sEARCH 3 4",
1942                 getNextTag(true) + " oK success"
1943                 });
1944 
1945         checkMessageUids(new String[] {"3", "4"}, mFolder.getMessages(3, 5, null));
1946 
1947         // Not found
1948         mock.expect(
1949                 getNextTag(false) + " UID SEARCH 3:5 NOT DELETED",
1950                 new String[] {
1951                 getNextTag(true) + " nO not found"
1952                 });
1953 
1954         checkMessageUids(new String[] {}, mFolder.getMessages(3, 5, null));
1955     }
1956 
1957     /**
1958      * Test for getMessages(String[] uids, MessageRetrievalListener) where uids != null.
1959      * (testGetMessages3() covers the case where uids == null.)
1960      */
testGetMessages2()1961     public void testGetMessages2() throws Exception {
1962         MockTransport mock = openAndInjectMockTransport();
1963         setupOpenFolder(mock);
1964         mFolder.open(OpenMode.READ_WRITE);
1965 
1966         // No command will be sent
1967         checkMessageUids(new String[] {"3", "4", "5"},
1968                 mFolder.getMessages(new String[] {"3", "4", "5"}, null));
1969 
1970         checkMessageUids(new String[] {},
1971                 mFolder.getMessages(new String[] {}, null));
1972     }
1973 
checkMessageUids(String[] expectedUids, Message[] actualMessages)1974     private static void checkMessageUids(String[] expectedUids, Message[] actualMessages) {
1975         ArrayList<String> list = new ArrayList<String>();
1976         for (Message m : actualMessages) {
1977             list.add(m.getUid());
1978         }
1979         MoreAsserts.assertEquals(expectedUids, list.toArray(new String[0]) );
1980     }
1981 
1982     /**
1983      * Test for {@link ImapStore#getConnection}
1984      */
testGetConnection()1985     public void testGetConnection() throws Exception {
1986         MockTransport mock = openAndInjectMockTransport();
1987 
1988         // Start: No pooled connections.
1989         assertEquals(0, mStore.getConnectionPoolForTest().size());
1990 
1991         // Get 1st connection.
1992         final ImapConnection con1 = mStore.getConnection();
1993         assertNotNull(con1);
1994         assertEquals(0, mStore.getConnectionPoolForTest().size()); // Pool size not changed.
1995         assertFalse(con1.isTransportOpenForTest()); // Transport not open yet.
1996 
1997         // Open con1
1998         expectLogin(mock);
1999         con1.open();
2000         assertTrue(con1.isTransportOpenForTest());
2001 
2002         // Get 2nd connection.
2003         final ImapConnection con2 = mStore.getConnection();
2004         assertNotNull(con2);
2005         assertEquals(0, mStore.getConnectionPoolForTest().size()); // Pool size not changed.
2006         assertFalse(con2.isTransportOpenForTest()); // Transport not open yet.
2007 
2008         // con1 != con2
2009         assertNotSame(con1, con2);
2010 
2011         // New connection, so, we need to login again & the tag count gets reset
2012         int saveTag = resetTag();
2013 
2014         // Open con2
2015         expectLogin(mock);
2016         con2.open();
2017         assertTrue(con1.isTransportOpenForTest());
2018 
2019         // Now we have two open connections: con1 and con2
2020 
2021         // Save con1 in the pool.
2022         mStore.poolConnection(con1);
2023         assertEquals(1, mStore.getConnectionPoolForTest().size());
2024 
2025         // Get another connection.  Should get con1, after verifying the connection.
2026         saveTag = resetTag(saveTag);
2027         mock.expect(getNextTag(false) + " NOOP", new String[] {getNextTag(true) + " oK success"});
2028 
2029         final ImapConnection con1b = mStore.getConnection();
2030         assertEquals(0, mStore.getConnectionPoolForTest().size()); // No connections left in pool
2031         assertSame(con1, con1b);
2032         assertTrue(con1.isTransportOpenForTest()); // We opened it.
2033 
2034         // Save con2.
2035         mStore.poolConnection(con2);
2036         assertEquals(1, mStore.getConnectionPoolForTest().size());
2037 
2038         // Resume con2 tags ...
2039         resetTag(saveTag);
2040 
2041         // Try to get connection, but this time, connection gets closed.
2042         mock.expect(getNextTag(false) + " NOOP", new String[] {getNextTag(true) + "* bYE bye"});
2043         final ImapConnection con3 = mStore.getConnection();
2044         assertNotNull(con3);
2045         assertEquals(0, mStore.getConnectionPoolForTest().size()); // No connections left in pool
2046 
2047         // It should be a new connection.
2048         assertNotSame(con1, con3);
2049         assertNotSame(con2, con3);
2050     }
2051 
testCheckSettings()2052     public void testCheckSettings() throws Exception {
2053         MockTransport mock = openAndInjectMockTransport();
2054 
2055         expectLogin(mock);
2056         mStore.checkSettings();
2057 
2058         resetTag();
2059         expectLogin(mock, false, false, false,
2060                 new String[] {"* iD nIL", "oK"}, "nO authentication failed");
2061         try {
2062             mStore.checkSettings();
2063             fail();
2064         } catch (MessagingException expected) {
2065         }
2066     }
2067 
2068     // Compatibility tests...
2069 
2070     /**
2071      * Getting an ALERT with a % mark in the message, which crashed the old parser.
2072      */
testQuotaAlert()2073     public void testQuotaAlert() throws Exception {
2074         MockTransport mock = openAndInjectMockTransport();
2075         expectLogin(mock);
2076 
2077         // Success
2078         Folder folder = mStore.getFolder("INBOX");
2079 
2080         // The following response was copied from an actual bug...
2081         mock.expect(getNextTag(false) + " SELECT \"INBOX\"", new String[] {
2082             "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen NonJunk $Forwarded Junk" +
2083                     " $Label4 $Label1 $Label2 $Label3 $Label5 $MDNSent Old)",
2084             "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen NonJunk" +
2085                     " $Forwarded Junk $Label4 $Label1 $Label2 $Label3 $Label5 $MDNSent Old \\*)]",
2086             "* 6406 EXISTS",
2087             "* 0 RECENT",
2088             "* OK [UNSEEN 5338]",
2089             "* OK [UIDVALIDITY 1055957975]",
2090             "* OK [UIDNEXT 449625]",
2091             "* NO [ALERT] Mailbox is at 98% of quota",
2092             getNextTag(true) + " OK [READ-WRITE] Completed"});
2093         folder.open(OpenMode.READ_WRITE); // shouldn't crash.
2094         assertEquals(6406, folder.getMessageCount());
2095     }
2096 
2097     /**
2098      * Apparently some servers send a size in the wrong format. e.g. 123E
2099      */
testFetchBodyStructureMalformed()2100     public void testFetchBodyStructureMalformed() throws Exception {
2101         final MockTransport mock = openAndInjectMockTransport();
2102         setupOpenFolder(mock);
2103         mFolder.open(OpenMode.READ_WRITE);
2104         final Message message = mFolder.createMessage("1");
2105 
2106         final FetchProfile fp = new FetchProfile();
2107         fp.add(FetchProfile.Item.STRUCTURE);
2108         mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
2109                 new String[] {
2110                 "* 9 FETCH (UID 1 BODYSTRUCTURE (\"TEXT\" \"PLAIN\" ()" +
2111                         " NIL NIL NIL 123E 3))", // 123E isn't a number!
2112                 getNextTag(true) + " OK SUCCESS"
2113         });
2114         mFolder.fetch(new Message[] { message }, fp, null);
2115 
2116         // Check mime structure...
2117         MoreAsserts.assertEquals(
2118                 new String[] {"text/plain"},
2119                 message.getHeader("Content-Type")
2120                 );
2121         assertNull(message.getHeader("Content-Transfer-Encoding"));
2122         assertNull(message.getHeader("Content-ID"));
2123 
2124         // Doesn't have size=xxx
2125         assertNull(message.getHeader("Content-Disposition"));
2126     }
2127 
2128     /**
2129      * Folder name with special chars in it.
2130      *
2131      * Gmail puts the folder name in the OK response, which crashed the old parser if there's a
2132      * special char in the folder name.
2133      */
testFolderNameWithSpecialChars()2134     public void testFolderNameWithSpecialChars() throws Exception {
2135         final String FOLDER_1 = "@u88**%_St";
2136         final String FOLDER_1_QUOTED = Pattern.quote(FOLDER_1);
2137         final String FOLDER_2 = "folder test_06";
2138 
2139         MockTransport mock = openAndInjectMockTransport();
2140         expectLogin(mock);
2141 
2142         // List folders.
2143         expectNoop(mock, true);
2144         mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"",
2145                 new String[] {
2146             "* LIST () \"/\" \"" + FOLDER_1 + "\"",
2147             "* LIST () \"/\" \"" + FOLDER_2 + "\"",
2148             getNextTag(true) + " OK SUCCESS"
2149         });
2150         final Folder[] folders = mStore.updateFolders();
2151 
2152         ArrayList<String> list = new ArrayList<String>();
2153         for (Folder f : folders) {
2154             list.add(f.getName());
2155         }
2156         MoreAsserts.assertEquals(
2157                 new String[] {"INBOX", FOLDER_2, FOLDER_1},
2158                 list.toArray(new String[0])
2159                 );
2160 
2161         // Try to open the folders.
2162         expectNoop(mock, true);
2163         mock.expect(getNextTag(false) + " SELECT \"" + FOLDER_1_QUOTED + "\"", new String[] {
2164             "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
2165             "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]",
2166             "* 0 EXISTS",
2167             "* 0 RECENT",
2168             "* OK [UNSEEN 0]",
2169             "* OK [UIDNEXT 1]",
2170             getNextTag(true) + " OK [READ-WRITE] " + FOLDER_1});
2171         folders[2].open(OpenMode.READ_WRITE);
2172         folders[2].close(false);
2173 
2174         expectNoop(mock, true);
2175         mock.expect(getNextTag(false) + " SELECT \"" + FOLDER_2 + "\"", new String[] {
2176             "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
2177             "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]",
2178             "* 0 EXISTS",
2179             "* 0 RECENT",
2180             "* OK [UNSEEN 0]",
2181             "* OK [UIDNEXT 1]",
2182             getNextTag(true) + " OK [READ-WRITE] " + FOLDER_2});
2183         folders[1].open(OpenMode.READ_WRITE);
2184         folders[1].close(false);
2185     }
2186 
2187     /**
2188      * Callback for {@link #runAndExpectMessagingException}.
2189      */
2190     private interface RunAndExpectMessagingExceptionTarget {
run(MockTransport mockTransport)2191         public void run(MockTransport mockTransport) throws Exception;
2192     }
2193 
2194     /**
2195      * Set up the usual mock transport, open the folder,
2196      * run {@link RunAndExpectMessagingExceptionTarget} and make sure a {@link MessagingException}
2197      * is thrown.
2198      */
runAndExpectMessagingException(RunAndExpectMessagingExceptionTarget target)2199     private void runAndExpectMessagingException(RunAndExpectMessagingExceptionTarget target)
2200             throws Exception {
2201         try {
2202             final MockTransport mockTransport = openAndInjectMockTransport();
2203             setupOpenFolder(mockTransport);
2204             mFolder.open(OpenMode.READ_WRITE);
2205 
2206             target.run(mockTransport);
2207 
2208             fail("MessagingException expected.");
2209         } catch (MessagingException expected) {
2210         }
2211     }
2212 
2213     /**
2214      * Make sure that IOExceptions are always converted to MessagingException.
2215      */
testFetchIOException()2216     public void testFetchIOException() throws Exception {
2217         runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
2218             @Override
2219             public void run(MockTransport mockTransport) throws Exception {
2220                 mockTransport.expectIOException();
2221 
2222                 final Message message = mFolder.createMessage("1");
2223                 final FetchProfile fp = new FetchProfile();
2224                 fp.add(FetchProfile.Item.STRUCTURE);
2225 
2226                 mFolder.fetch(new Message[] { message }, fp, null);
2227             }
2228         });
2229     }
2230 
2231     /**
2232      * Make sure that IOExceptions are always converted to MessagingException.
2233      */
testUnreadMessageCountIOException()2234     public void testUnreadMessageCountIOException() throws Exception {
2235         runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
2236             @Override
2237             public void run(MockTransport mockTransport) throws Exception {
2238                 mockTransport.expectIOException();
2239 
2240                 mFolder.getUnreadMessageCount();
2241             }
2242         });
2243     }
2244 
2245     /**
2246      * Make sure that IOExceptions are always converted to MessagingException.
2247      */
testCopyMessagesIOException()2248     public void testCopyMessagesIOException() throws Exception {
2249         runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
2250             @Override
2251             public void run(MockTransport mockTransport) throws Exception {
2252                 mockTransport.expectIOException();
2253 
2254                 final Message message = mFolder.createMessage("1");
2255                 final Folder folder = mStore.getFolder("test");
2256 
2257                 mFolder.copyMessages(new Message[] { message }, folder, null);
2258             }
2259         });
2260     }
2261 
2262     /**
2263      * Make sure that IOExceptions are always converted to MessagingException.
2264      */
testSearchForUidsIOException()2265     public void testSearchForUidsIOException() throws Exception {
2266         runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
2267             @Override
2268             public void run(MockTransport mockTransport) throws Exception {
2269                 mockTransport.expectIOException();
2270 
2271                 mFolder.getMessage("uid");
2272             }
2273         });
2274     }
2275 
2276     /**
2277      * Make sure that IOExceptions are always converted to MessagingException.
2278      */
testExpungeIOException()2279     public void testExpungeIOException() throws Exception {
2280         runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
2281             @Override
2282             public void run(MockTransport mockTransport) throws Exception {
2283                 mockTransport.expectIOException();
2284 
2285                 mFolder.expunge();
2286             }
2287         });
2288     }
2289 
2290     /**
2291      * Make sure that IOExceptions are always converted to MessagingException.
2292      */
testOpenIOException()2293     public void testOpenIOException() throws Exception {
2294         runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
2295             @Override
2296             public void run(MockTransport mockTransport) throws Exception {
2297                 mockTransport.expectIOException();
2298                 final Folder folder = mStore.getFolder("test");
2299                 folder.open(OpenMode.READ_WRITE);
2300             }
2301         });
2302     }
2303 
2304     /** Creates a folder & mailbox */
createFolder(long id, String displayName, String serverId, char delimiter)2305     private ImapFolder createFolder(long id, String displayName, String serverId, char delimiter) {
2306         ImapFolder folder = new ImapFolder(null, serverId);
2307         Mailbox mailbox = new Mailbox();
2308         mailbox.mId = id;
2309         mailbox.mDisplayName = displayName;
2310         mailbox.mServerId = serverId;
2311         mailbox.mDelimiter = delimiter;
2312         mailbox.mFlags = 0xAAAAAAA8;
2313         folder.mMailbox = mailbox;
2314         return folder;
2315     }
2316 
2317     /** Tests creating folder hierarchies */
testCreateHierarchy()2318     public void testCreateHierarchy() {
2319         HashMap<String, ImapFolder> testMap = new HashMap<String, ImapFolder>();
2320 
2321         // Create hierarchy
2322         //   |-INBOX
2323         //   |  +-b
2324         //   |-a
2325         //   |  |-b
2326         //   |  |-c
2327         //   |  +-d
2328         //   |    +-b
2329         //   |      +-b
2330         //   +-g
2331         ImapFolder[] folders = {
2332             createFolder(1L, "INBOX", "INBOX", '/'),
2333             createFolder(2L, "b", "INBOX/b", '/'),
2334             createFolder(3L, "a", "a", '/'),
2335             createFolder(4L, "b", "a/b", '/'),
2336             createFolder(5L, "c", "a/c", '/'),
2337             createFolder(6L, "d", "a/d", '/'),
2338             createFolder(7L, "b", "a/d/b", '/'),
2339             createFolder(8L, "b", "a/d/b/b", '/'),
2340             createFolder(9L, "g", "g", '/'),
2341         };
2342         for (ImapFolder folder : folders) {
2343             testMap.put(folder.getName(), folder);
2344         }
2345 
2346         ImapStore.createHierarchy(testMap);
2347         // 'INBOX'
2348         assertEquals(-1L, folders[0].mMailbox.mParentKey);
2349         assertEquals(0xAAAAAAAB, folders[0].mMailbox.mFlags);
2350         // 'INBOX/b'
2351         assertEquals(1L, folders[1].mMailbox.mParentKey);
2352         assertEquals(0xAAAAAAA8, folders[1].mMailbox.mFlags);
2353         // 'a'
2354         assertEquals(-1L, folders[2].mMailbox.mParentKey);
2355         assertEquals(0xAAAAAAAB, folders[2].mMailbox.mFlags);
2356         // 'a/b'
2357         assertEquals(3L, folders[3].mMailbox.mParentKey);
2358         assertEquals(0xAAAAAAA8, folders[3].mMailbox.mFlags);
2359         // 'a/c'
2360         assertEquals(3L, folders[4].mMailbox.mParentKey);
2361         assertEquals(0xAAAAAAA8, folders[4].mMailbox.mFlags);
2362         // 'a/d'
2363         assertEquals(3L, folders[5].mMailbox.mParentKey);
2364         assertEquals(0xAAAAAAAB, folders[5].mMailbox.mFlags);
2365         // 'a/d/b'
2366         assertEquals(6L, folders[6].mMailbox.mParentKey);
2367         assertEquals(0xAAAAAAAB, folders[6].mMailbox.mFlags);
2368         // 'a/d/b/b'
2369         assertEquals(7L, folders[7].mMailbox.mParentKey);
2370         assertEquals(0xAAAAAAA8, folders[7].mMailbox.mFlags);
2371         // 'g'
2372         assertEquals(-1L, folders[8].mMailbox.mParentKey);
2373         assertEquals(0xAAAAAAA8, folders[8].mMailbox.mFlags);
2374     }
2375 }
2376