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