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