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