com.appaholics.email.mail.store.ImapStoreUnitTests.java Source code

Java tutorial

Introduction

Here is the source code for com.appaholics.email.mail.store.ImapStoreUnitTests.java

Source

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.appaholics.email.mail.store;

import android.content.Context;
import android.content.ContextWrapper;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Bundle;
import android.test.InstrumentationTestCase;
import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.SmallTest;

import com.appaholics.email.DBTestHelper;
import com.appaholics.email.MockSharedPreferences;
import com.appaholics.email.MockVendorPolicy;
import com.appaholics.email.VendorPolicyLoader;
import com.appaholics.email.mail.Transport;
import com.appaholics.email.mail.store.ImapStore.ImapMessage;
import com.appaholics.email.mail.store.imap.ImapResponse;
import com.appaholics.email.mail.store.imap.ImapTestUtils;
import com.appaholics.email.mail.transport.MockTransport;
import com.android.emailcommon.TempDirectory;
import com.android.emailcommon.internet.MimeBodyPart;
import com.android.emailcommon.internet.MimeMultipart;
import com.android.emailcommon.internet.MimeUtility;
import com.android.emailcommon.internet.TextBody;
import com.android.emailcommon.mail.Address;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.Body;
import com.android.emailcommon.mail.FetchProfile;
import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Folder.FolderType;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.Message.RecipientType;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.Utility;

import org.apache.commons.io.IOUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Pattern;

/**
 * This is a series of unit tests for the ImapStore class.  These tests must be locally
 * complete - no server(s) required.
 *
 * To run these tests alone, use:
 *   $ runtest -c com.appaholics.email.mail.store.ImapStoreUnitTests email
 *
 * TODO Check if callback is really called
 * TODO test for BAD response in various places?
 * TODO test for BYE response in various places?
 */
@SmallTest
public class ImapStoreUnitTests extends InstrumentationTestCase {
    private final static String[] NO_REPLY = new String[0];

    /** Default folder name.  In order to test for encoding, we use a non-ascii name. */
    private final static String FOLDER_NAME = "\u65E5";
    /** Folder name encoded in UTF-7. */
    private final static String FOLDER_ENCODED = "&ZeU-";
    /**
     * Flag bits to specify whether or not a folder can be selected. This corresponds to
     * {@link Mailbox#FLAG_ACCEPTS_MOVED_MAIL} and {@link Mailbox#FLAG_HOLDS_MAIL}.
     */
    private final static int SELECTABLE_BITS = 0x18;

    private final static ImapResponse CAPABILITY_RESPONSE = ImapTestUtils
            .parseResponse("* CAPABILITY IMAP4rev1 STARTTLS");

    /* These values are provided by setUp() */
    private ImapStore mStore = null;
    private ImapFolder mFolder = null;
    private Context mTestContext;

    /** The tag for the current IMAP command; used for mock transport responses */
    private int mTag;
    // Fields specific to the CopyMessages tests
    private MockTransport mCopyMock;
    private Folder mCopyToFolder;
    private Message[] mCopyMessages;

    /**
     * A wrapper to provide a wrapper to a Context which has already been mocked.
     * This allows additional methods to delegate to the original, real context, in cases
     * where the mocked behavior is insufficient.
     */
    private class SecondaryMockContext extends ContextWrapper {
        private final Context mUnderlying;

        public SecondaryMockContext(Context mocked, Context underlying) {
            super(mocked);
            mUnderlying = underlying;
        }

        // TODO: eliminate the need for these method.
        @Override
        public Context createPackageContext(String packageName, int flags) throws NameNotFoundException {
            return mUnderlying.createPackageContext(packageName, flags);
        }

        @Override
        public SharedPreferences getSharedPreferences(String name, int mode) {
            return new MockSharedPreferences();
        }
    }

    /**
     * Setup code.  We generate a lightweight ImapStore and ImapStore.ImapFolder.
     */
    @Override
    protected void setUp() throws Exception {
        super.setUp();
        Context realContext = getInstrumentation().getTargetContext();
        ImapStore.sImapId = ImapStore.makeCommonImapId(realContext.getPackageName(), Build.VERSION.RELEASE,
                Build.VERSION.CODENAME, Build.MODEL, Build.ID, Build.MANUFACTURER, "FakeNetworkOperator");
        mTestContext = new SecondaryMockContext(
                DBTestHelper.ProviderContextSetupHelper.getProviderContext(realContext), realContext);
        MockVendorPolicy.inject(mTestContext);

        TempDirectory.setTempDirectory(mTestContext);

        // These are needed so we can get at the inner classes
        HostAuth testAuth = new HostAuth();
        Account testAccount = new Account();

        testAuth.setLogin("user", "password");
        testAuth.setConnection("imap", "server", 999);
        testAccount.mHostAuthRecv = testAuth;
        mStore = (ImapStore) ImapStore.newInstance(testAccount, mTestContext);
        mFolder = (ImapFolder) mStore.getFolder(FOLDER_NAME);
        resetTag();
    }

    public void testJoinMessageUids() throws Exception {
        assertEquals("", ImapStore.joinMessageUids(new Message[] {}));
        assertEquals("a", ImapStore.joinMessageUids(new Message[] { mFolder.createMessage("a") }));
        assertEquals("a,XX", ImapStore
                .joinMessageUids(new Message[] { mFolder.createMessage("a"), mFolder.createMessage("XX"), }));
    }

    /**
     * Confirms simple non-SSL non-TLS login
     */
    public void testSimpleLogin() throws MessagingException {

        MockTransport mockTransport = openAndInjectMockTransport();

        // try to open it
        setupOpenFolder(mockTransport);
        mFolder.open(OpenMode.READ_WRITE);

        // TODO: inject specific facts in the initial folder SELECT and check them here
    }

    /**
     * Test simple login with failed authentication
     */
    public void testLoginFailure() throws Exception {
        MockTransport mockTransport = openAndInjectMockTransport();
        expectLogin(mockTransport, false, false, false, new String[] { "* iD nIL", "oK" },
                "nO authentication failed");

        try {
            mStore.getConnection().open();
            fail("Didn't throw AuthenticationFailedException");
        } catch (AuthenticationFailedException expected) {
        }
    }

    /**
     * Test simple TLS open
     */
    public void testTlsOpen() throws MessagingException {

        MockTransport mockTransport = openAndInjectMockTransport(Transport.CONNECTION_SECURITY_TLS, false);

        // try to open it, with STARTTLS
        expectLogin(mockTransport, true, false, false, new String[] { "* iD nIL", "oK" },
                "oK user authenticated (Success)");
        mockTransport.expect(getNextTag(false) + " SELECT \"" + FOLDER_ENCODED + "\"", new String[] {
                "* fLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
                "* oK [pERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]", "* 0 eXISTS",
                "* 0 rECENT", "* OK [uNSEEN 0]", "* OK [uIDNEXT 1]",
                getNextTag(true) + " oK [" + "rEAD-wRITE" + "] " + FOLDER_ENCODED + " selected. (Success)" });

        mFolder.open(OpenMode.READ_WRITE);
        assertTrue(mockTransport.isTlsStarted());
    }

    /**
     * TODO: Test with SSL negotiation (faked)
     * TODO: Test with SSL required but not supported
     * TODO: Test with TLS required but not supported
     */

    /**
     * Test the generation of the IMAP ID keys
     */
    public void testImapIdBasic() {
        // First test looks at operation of the outer API - we don't control any of the
        // values;  Just look for basic results.

        // Strings we'll expect to find:
        //   name            Android package name of the program
        //   os              "android"
        //   os-version      "version; build-id"
        //   vendor          Vendor of the client/server
        //   x-android-device-model Model (Optional, so not tested here)
        //   x-android-net-operator Carrier (Unreliable, so not tested here)
        //   AGUID           A device+account UID
        String id = ImapStore.getImapId(mTestContext, "user-name", "host-name", CAPABILITY_RESPONSE.flatten());
        HashMap<String, String> map = tokenizeImapId(id);
        assertEquals(mTestContext.getPackageName(), map.get("name"));
        assertEquals("android", map.get("os"));
        assertNotNull(map.get("os-version"));
        assertNotNull(map.get("vendor"));
        assertNotNull(map.get("AGUID"));

        // Next, use the inner API to confirm operation of a couple of
        // variants for release and non-release devices.

        // simple API check - non-REL codename, non-empty version
        id = ImapStore.makeCommonImapId("packageName", "version", "codeName", "model", "id", "vendor",
                "network-operator");
        map = tokenizeImapId(id);
        assertEquals("packageName", map.get("name"));
        assertEquals("android", map.get("os"));
        assertEquals("version; id", map.get("os-version"));
        assertEquals("vendor", map.get("vendor"));
        assertEquals(null, map.get("x-android-device-model"));
        assertEquals("network-operator", map.get("x-android-mobile-net-operator"));
        assertEquals(null, map.get("AGUID"));

        // simple API check - codename is REL, so use model name.
        // also test empty version => 1.0 and empty network operator
        id = ImapStore.makeCommonImapId("packageName", "", "REL", "model", "id", "vendor", "");
        map = tokenizeImapId(id);
        assertEquals("packageName", map.get("name"));
        assertEquals("android", map.get("os"));
        assertEquals("1.0; id", map.get("os-version"));
        assertEquals("vendor", map.get("vendor"));
        assertEquals("model", map.get("x-android-device-model"));
        assertEquals(null, map.get("x-android-mobile-net-operator"));
        assertEquals(null, map.get("AGUID"));
    }

    /**
     * Test for the interaction between {@link ImapStore#getImapId} and a vendor policy.
     */
    public void testImapIdWithVendorPolicy() {
        try {
            MockVendorPolicy.inject(mTestContext);

            // Prepare mock result
            Bundle result = new Bundle();
            result.putString("getImapId", "\"test-key\" \"test-value\"");
            MockVendorPolicy.mockResult = result;

            // Invoke
            String id = ImapStore.getImapId(mTestContext, "user-name", "host-name",
                    ImapTestUtils.parseResponse("* CAPABILITY IMAP4rev1 XXX YYY Z").flatten());

            // Check the result
            assertEquals("test-value", tokenizeImapId(id).get("test-key"));

            // Verify what's passed to the policy
            assertEquals("getImapId", MockVendorPolicy.passedPolicy);
            assertEquals("user-name", MockVendorPolicy.passedBundle.getString("getImapId.user"));
            assertEquals("host-name", MockVendorPolicy.passedBundle.getString("getImapId.host"));
            assertEquals("[CAPABILITY,IMAP4rev1,XXX,YYY,Z]",
                    MockVendorPolicy.passedBundle.getString("getImapId.capabilities"));
        } finally {
            VendorPolicyLoader.clearInstanceForTest();
        }
    }

    /**
     * Test of the internal generator for IMAP ID strings, specifically looking for proper
     * filtering of illegal values.  This is required because we cannot necessarily trust
     * the external sources of some of this data (e.g. release labels).
     *
     * The (somewhat arbitrary) legal values are:  a-z A-Z 0-9 - _ + = ; : . , / <space>
     * The most important goal of the filters is to keep out control chars, (, ), and "
     */
    public void testImapIdFiltering() {
        String id = ImapStore.makeCommonImapId("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789",
                "codeName", "model", "-_+=;:.,// ", "v(e)n\"d\ro\nr", // look for bad chars stripped out, leaving OK chars
                "()\""); // look for bad chars stripped out, leaving nothing
        HashMap<String, String> map = tokenizeImapId(id);

        assertEquals("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", map.get("name"));
        assertEquals("0123456789; -_+=;:.,// ", map.get("os-version"));
        assertEquals("vendor", map.get("vendor"));
        assertNull(map.get("x-android-mobile-net-operator"));
    }

    /**
     * Test that IMAP ID uid's are per-username
     */
    public void testImapIdDeviceId() throws MessagingException {
        HostAuth testAuth;
        Account testAccount;

        // store 1a
        testAuth = new HostAuth();
        testAuth.setLogin("user1", "password");
        testAuth.setConnection("imap", "server", 999);
        testAccount = new Account();
        testAccount.mHostAuthRecv = testAuth;
        ImapStore testStore1A = (ImapStore) ImapStore.newInstance(testAccount, mTestContext);

        // store 1b
        testAuth = new HostAuth();
        testAuth.setLogin("user1", "password");
        testAuth.setConnection("imap", "server", 999);
        testAccount = new Account();
        testAccount.mHostAuthRecv = testAuth;
        ImapStore testStore1B = (ImapStore) ImapStore.newInstance(testAccount, mTestContext);

        // store 2
        testAuth = new HostAuth();
        testAuth.setLogin("user2", "password");
        testAuth.setConnection("imap", "server", 999);
        testAccount = new Account();
        testAccount.mHostAuthRecv = testAuth;
        ImapStore testStore2 = (ImapStore) ImapStore.newInstance(testAccount, mTestContext);

        String capabilities = CAPABILITY_RESPONSE.flatten();
        String id1a = ImapStore.getImapId(mTestContext, "user1", "host-name", capabilities);
        String id1b = ImapStore.getImapId(mTestContext, "user1", "host-name", capabilities);
        String id2 = ImapStore.getImapId(mTestContext, "user2", "host-name", capabilities);

        String uid1a = tokenizeImapId(id1a).get("AGUID");
        String uid1b = tokenizeImapId(id1b).get("AGUID");
        String uid2 = tokenizeImapId(id2).get("AGUID");

        assertEquals(uid1a, uid1b);
        MoreAsserts.assertNotEqual(uid1a, uid2);
    }

    /**
     * Helper to break an IMAP ID string into keys & values
     * @param id the IMAP Id string (the part inside the parens)
     * @return a map of key/value pairs
     */
    private HashMap<String, String> tokenizeImapId(String id) {
        // Instead of a true tokenizer, we'll use double-quote as the split.
        // We can's use " " because there may be spaces inside the values.
        String[] elements = id.split("\"");
        HashMap<String, String> map = new HashMap<String, String>();
        for (int i = 0; i < elements.length;) {
            // Because we split at quotes, we expect to find:
            // [i] = null or one or more spaces
            // [i+1] = key
            // [i+2] = one or more spaces
            // [i+3] = value
            map.put(elements[i + 1], elements[i + 3]);
            i += 4;
        }
        return map;
    }

    /**
     * Test non-NIL server response to IMAP ID.  We should simply ignore it.
     */
    public void testServerId() throws MessagingException {
        MockTransport mockTransport = openAndInjectMockTransport();

        // try to open it
        setupOpenFolder(mockTransport,
                new String[] {
                        "* ID (\"name\" \"Cyrus\" \"version\" \"1.5\"" + " \"os\" \"sunos\" \"os-version\" \"5.5\""
                                + " \"support-url\" \"mailto:cyrus-bugs+@andrew.cmu.edu\")",
                        "oK" },
                "rEAD-wRITE");
        mFolder.open(OpenMode.READ_WRITE);
    }

    /**
     * Test OK response to IMAP ID with crummy text afterwards too.
     */
    public void testImapIdOkParsing() throws MessagingException {
        MockTransport mockTransport = openAndInjectMockTransport();

        // try to open it
        setupOpenFolder(mockTransport, new String[] { "* iD nIL", "oK [iD] bad-char-%" }, "rEAD-wRITE");
        mFolder.open(OpenMode.READ_WRITE);
    }

    /**
     * Test BAD response to IMAP ID - also with bad parser chars
     */
    public void testImapIdBad() throws MessagingException {
        MockTransport mockTransport = openAndInjectMockTransport();

        // try to open it
        setupOpenFolder(mockTransport, new String[] { "bAD unknown command bad-char-%" }, "rEAD-wRITE");
        mFolder.open(OpenMode.READ_WRITE);
    }

    /**
     * Confirm that when IMAP ID is not in capability, it is not sent/received.
     * This supports RFC 2971 section 3, and is important because certain servers
     * (e.g. imap.vodafone.net.nz) do not process the unexpected ID command properly.
     */
    public void testImapIdNotSupported() throws MessagingException {
        MockTransport mockTransport = openAndInjectMockTransport();

        // try to open it
        setupOpenFolder(mockTransport, null, "rEAD-wRITE");
        mFolder.open(OpenMode.READ_WRITE);
    }

    /**
     * Confirm that the non-conformant IMAP ID result seen on imap.secureserver.net fails
     * to properly parse.
     *   2 ID ("name" "com.google.android.email")
     *   * ID( "name" "Godaddy IMAP" ... "version" "3.1.0")
     *   2 OK ID completed
     */
    public void testImapIdSecureServerParseFail() {
        MockTransport mockTransport = openAndInjectMockTransport();

        // configure mock server to return malformed ID response
        setupOpenFolder(mockTransport,
                new String[] { "* ID( \"name\" \"Godaddy IMAP\" \"version\" \"3.1.0\")", "oK" }, "rEAD-wRITE");
        try {
            mFolder.open(OpenMode.READ_WRITE);
            fail("Expected MessagingException");
        } catch (MessagingException expected) {
        }
    }

    /**
     * Confirm that the connections to *.secureserver.net never send IMAP ID (see
     * testImapIdSecureServerParseFail() for the reason why.)
     */
    public void testImapIdSecureServerNotSent() throws MessagingException {
        // Note, this is injected into mStore (which we don't use for this test)
        MockTransport mockTransport = openAndInjectMockTransport();
        mockTransport.setHost("eMail.sEcurEserVer.nEt");

        // Prime the expects pump as if the server wants IMAP ID, but we should not actually expect
        // to send it, because the login code in the store should never actually send it (to this
        // particular server).  This sequence is a minimized version of expectLogin().

        // Respond to the initial connection
        mockTransport.expect(null, "* oK Imap 2000 Ready To Assist You");
        // Return "ID" in the capability
        expectCapability(mockTransport, true, false);
        // No TLS
        // No ID (the special case for this server)
        // LOGIN
        mockTransport.expect(getNextTag(false) + " LOGIN user \"password\"",
                getNextTag(true) + " " + "oK user authenticated (Success)");
        // SELECT
        expectSelect(mockTransport, FOLDER_ENCODED, "rEAD-wRITE");

        // Now open the folder.  Although the server indicates ID in the capabilities,
        // we are not expecting the store to send the ID command (to this particular server).
        mFolder.open(OpenMode.READ_WRITE);
    }

    /**
     * Test small Folder functions that don't really do anything in Imap
     */
    public void testSmallFolderFunctions() {
        // canCreate() returns true
        assertTrue(mFolder.canCreate(FolderType.HOLDS_FOLDERS));
        assertTrue(mFolder.canCreate(FolderType.HOLDS_MESSAGES));
    }

    /**
     * Lightweight test to confirm that IMAP hasn't implemented any folder roles yet.
     *
     * TODO: Test this with multiple folders provided by mock server
     * TODO: Implement XLIST and then support this
     */
    public void testNoFolderRolesYet() {
        assertEquals(Folder.FolderRole.UNKNOWN, mFolder.getRole());
    }

    /**
     * Lightweight test to confirm that IMAP is requesting sent-message-upload.
     * TODO: Implement Gmail-specific cases and handle this server-side
     */
    public void testSentUploadRequested() {
        assertTrue(mStore.requireCopyMessageToSentFolder());
    }

    /**
     * TODO: Test the process of opening and indexing a mailbox with one unread message in it.
     */

    /**
     * TODO: Test the scenario where the transport is "open" but not really (e.g. server closed).
    /**
     * Set up a basic MockTransport. open it, and inject it into mStore
     */
    private MockTransport openAndInjectMockTransport() {
        return openAndInjectMockTransport(Transport.CONNECTION_SECURITY_NONE, false);
    }

    /**
     * Set up a MockTransport with security settings
     */
    private MockTransport openAndInjectMockTransport(int connectionSecurity, boolean trustAllCertificates) {
        // Create mock transport and inject it into the ImapStore that's already set up
        MockTransport mockTransport = new MockTransport();
        mockTransport.setSecurity(connectionSecurity, trustAllCertificates);
        mockTransport.setHost("mock.server.com");
        mStore.setTransportForTest(mockTransport);
        return mockTransport;
    }

    /**
     * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open()
     *
     * @param mockTransport the mock transport we're using
     */
    private void setupOpenFolder(MockTransport mockTransport) {
        setupOpenFolder(mockTransport, "rEAD-wRITE");
    }

    /**
     * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open()
     *
     * @param mockTransport the mock transport we're using
     */
    private void setupOpenFolder(MockTransport mockTransport, String readWriteMode) {
        setupOpenFolder(mockTransport, new String[] { "* iD nIL", "oK" }, readWriteMode, false);
    }

    /**
     * Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open()
     * Also allows setting a custom IMAP ID.
     *
     * Also sets mNextTag, an int, which is useful if there are additional commands to inject.
     *
     * @param mockTransport the mock transport we're using
     * @param imapIdResponse the expected series of responses to the IMAP ID command.  Non-final
     *      lines should be tagged with *.  The final response should be untagged (the correct
     *      tag will be added at runtime).  Pass "null" to test w/o IMAP ID.
     * @param readWriteMode "READ-WRITE" or "READ-ONLY"
     */
    private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse, String readWriteMode) {
        setupOpenFolder(mockTransport, imapIdResponse, readWriteMode, false);
    }

    private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse, String readWriteMode,
            boolean withUidPlus) {
        expectLogin(mockTransport, imapIdResponse, withUidPlus);
        expectSelect(mockTransport, FOLDER_ENCODED, readWriteMode);
    }

    /**
     * Helper which stuffs the mock with the strings to satisfy a typical SELECT.
     * @param mockTransport the mock transport we're using
     * @param readWriteMode "READ-WRITE" or "READ-ONLY"
     */
    private void expectSelect(MockTransport mockTransport, String folder, String readWriteMode) {
        mockTransport.expect(getNextTag(false) + " SELECT \"" + folder + "\"",
                new String[] { "* fLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
                        "* oK [pERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]", "* 0 eXISTS",
                        "* 0 rECENT", "* OK [uNSEEN 0]", "* OK [uIDNEXT 1]",
                        getNextTag(true) + " oK [" + readWriteMode + "] " + folder + " selected. (Success)" });
    }

    private void expectLogin(MockTransport mockTransport) {
        expectLogin(mockTransport, new String[] { "* iD nIL", "oK" }, false);
    }

    private void expectLogin(MockTransport mockTransport, String[] imapIdResponse, boolean withUidPlus) {
        expectLogin(mockTransport, false, (imapIdResponse != null), withUidPlus, imapIdResponse,
                "oK user authenticated (Success)");
    }

    private void expectLogin(MockTransport mockTransport, boolean startTls, boolean withId, boolean withUidPlus,
            String[] imapIdResponse, String loginResponse) {
        // inject boilerplate commands that match our typical login
        mockTransport.expect(null, "* oK Imap 2000 Ready To Assist You");

        expectCapability(mockTransport, withId, withUidPlus);

        // TLS (if expected)
        if (startTls) {
            mockTransport.expect(getNextTag(false) + " STARTTLS", getNextTag(true) + " Ok starting TLS");
            mockTransport.expectStartTls();
            // After switching to TLS the client must re-query for capability
            expectCapability(mockTransport, withId, withUidPlus);
        }

        // ID
        if (withId) {
            String expectedNextTag = getNextTag(false);
            // Fix the tag # of the ID response
            String last = imapIdResponse[imapIdResponse.length - 1];
            last = expectedNextTag + " " + last;
            imapIdResponse[imapIdResponse.length - 1] = last;
            mockTransport.expect(getNextTag(false) + " ID \\(.*\\)", imapIdResponse);
            getNextTag(true); // Advance the tag for ID response.
        }

        // LOGIN
        mockTransport.expect(getNextTag(false) + " LOGIN user \"password\"",
                getNextTag(true) + " " + loginResponse);
    }

    private void expectCapability(MockTransport mockTransport, boolean withId, boolean withUidPlus) {
        String capabilityList = "* cAPABILITY iMAP4rev1 sTARTTLS aUTH=gSSAPI lOGINDISABLED";
        capabilityList += withId ? " iD" : "";
        capabilityList += withUidPlus ? " UiDPlUs" : "";

        mockTransport.expect(getNextTag(false) + " CAPABILITY",
                new String[] { capabilityList, getNextTag(true) + " oK CAPABILITY completed" });
    }

    private void expectNoop(MockTransport mockTransport, boolean ok) {
        String response = ok ? " oK success" : " nO timeout";
        mockTransport.expect(getNextTag(false) + " NOOP", new String[] { getNextTag(true) + response });
    }

    /**
     * Return a tag for use in setting up expect strings.  Typically this is called in pairs,
     * first as getNextTag(false) when emitting the command, then as getNextTag(true) when
     * emitting the final line of the expected response.
     * @param advance true to increment mNextTag for the subsequence command
     * @return a string containing the current tag
     */
    public String getNextTag(boolean advance) {
        if (advance)
            ++mTag;
        return Integer.toString(mTag);
    }

    /**
     * Resets the tag back to it's starting value. Do this after the test connection has been
     * closed.
     */
    private int resetTag() {
        return resetTag(1);
    }

    private int resetTag(int tag) {
        int oldTag = mTag;
        mTag = tag;
        return oldTag;
    }

    /**
     * Test that servers reporting READ-WRITE mode are parsed properly
     * Note: the READ_WRITE mode passed to folder.open() does not affect the test
     */
    public void testReadWrite() throws MessagingException {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock, "rEAD-WRITE");
        mFolder.open(OpenMode.READ_WRITE);
        assertEquals(OpenMode.READ_WRITE, mFolder.getMode());
    }

    /**
     * Test that servers reporting READ-ONLY mode are parsed properly
     * Note: the READ_ONLY mode passed to folder.open() does not affect the test
     */
    public void testReadOnly() throws MessagingException {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock, "rEAD-ONLY");
        mFolder.open(OpenMode.READ_ONLY);
        assertEquals(OpenMode.READ_ONLY, mFolder.getMode());
    }

    /**
     * Test for getUnreadMessageCount with quoted string in the middle of response.
     */
    public void testGetUnreadMessageCountWithQuotedString() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mock.expect(getNextTag(false) + " STATUS \"" + FOLDER_ENCODED + "\" \\(UNSEEN\\)", new String[] {
                "* sTATUS \"" + FOLDER_ENCODED + "\" (uNSEEN 2)", getNextTag(true) + " oK STATUS completed" });
        mFolder.open(OpenMode.READ_WRITE);
        int unreadCount = mFolder.getUnreadMessageCount();
        assertEquals("getUnreadMessageCount with quoted string", 2, unreadCount);
    }

    /**
     * Test for getUnreadMessageCount with literal string in the middle of response.
     */
    public void testGetUnreadMessageCountWithLiteralString() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mock.expect(getNextTag(false) + " STATUS \"" + FOLDER_ENCODED + "\" \\(UNSEEN\\)", new String[] {
                "* sTATUS {5}", FOLDER_ENCODED + " (uNSEEN 10)", getNextTag(true) + " oK STATUS completed" });
        mFolder.open(OpenMode.READ_WRITE);
        int unreadCount = mFolder.getUnreadMessageCount();
        assertEquals("getUnreadMessageCount with literal string", 10, unreadCount);
    }

    public void testFetchFlagEnvelope() throws MessagingException {
        final MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);
        final Message message = mFolder.createMessage("1");

        final FetchProfile fp = new FetchProfile();
        fp.add(FetchProfile.Item.FLAGS);
        fp.add(FetchProfile.Item.ENVELOPE);
        mock.expect(
                getNextTag(false)
                        + " UID FETCH 1 \\(UID FLAGS INTERNALDATE RFC822\\.SIZE BODY\\.PEEK\\[HEADER.FIELDS"
                        + " \\(date subject from content-type to cc message-id\\)\\]\\)",
                new String[] {
                        "* 9 fETCH (uID 1 rFC822.sIZE 120626 iNTERNALDATE \"17-may-2010 22:00:15 +0000\""
                                + "fLAGS (\\Seen) bODY[hEADER.FIELDS (dAte sUbject fRom cOntent-type tO cC"
                                + " mEssage-id)]" + " {279}",
                        "From: Xxxxxx Yyyyy <userxx@android.com>", "Date: Mon, 17 May 2010 14:59:52 -0700",
                        "Message-ID: <x0000000000000000000000000000000000000000000000y@android.com>",
                        "Subject: ssubject", "To: android.test01@android.com",
                        "Content-Type: multipart/mixed; boundary=a00000000000000000000000000b", "", ")",
                        getNextTag(true) + " oK SUCCESS" });
        mFolder.fetch(new Message[] { message }, fp, null);

        assertEquals("android.test01@android.com", message.getHeader("to")[0]);
        assertEquals("Xxxxxx Yyyyy <userxx@android.com>", message.getHeader("from")[0]);
        assertEquals("multipart/mixed; boundary=a00000000000000000000000000b",
                message.getHeader("Content-Type")[0]);
        assertTrue(message.isSet(Flag.SEEN));

        // TODO: Test NO response.
    }

    /**
     * Test for fetching simple BODYSTRUCTURE.
     */
    public void testFetchBodyStructureSimple() throws Exception {
        final MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);
        final Message message = mFolder.createMessage("1");

        final FetchProfile fp = new FetchProfile();
        fp.add(FetchProfile.Item.STRUCTURE);
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", new String[] {
                "* 9 fETCH (uID 1 bODYSTRUCTURE (\"tEXT\" \"pLAIN\" nIL" + " nIL nIL nIL 18 3 nIL nIL nIL))",
                getNextTag(true) + " oK sUCCESS" });
        mFolder.fetch(new Message[] { message }, fp, null);

        // Check mime structure...
        MoreAsserts.assertEquals(new String[] { "text/plain" }, message.getHeader("Content-Type"));
        assertNull(message.getHeader("Content-Transfer-Encoding"));
        assertNull(message.getHeader("Content-ID"));
        MoreAsserts.assertEquals(new String[] { ";\n size=18" }, message.getHeader("Content-Disposition"));

        MoreAsserts.assertEquals(new String[] { "TEXT" }, message.getHeader("X-Android-Attachment-StoreData"));

        // TODO: Test NO response.
    }

    /**
     * Test for fetching complex muiltipart BODYSTRUCTURE.
     */
    public void testFetchBodyStructureMultipart() throws Exception {
        final MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);
        final Message message = mFolder.createMessage("1");

        final FetchProfile fp = new FetchProfile();
        fp.add(FetchProfile.Item.STRUCTURE);
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
                new String[] { "* 9 fETCH (uID 1 bODYSTRUCTURE ((\"tEXT\" \"pLAIN\" () {20}",
                        "long content id#@!@#"
                                + " NIL \"7BIT\" 18 3 NIL NIL NIL)" + "(\"IMAGE\" \"PNG\" (\"NAME\" {10}",
                        "device.png) NIL NIL \"BASE64\" {6}",
                        "117840 NIL (\"aTTACHMENT\" (\"fILENAME\" \"device.png\")) NIL)"
                                + "(\"TEXT\" \"HTML\" () NIL NIL \"7BIT\" 100 NIL 123 (\"aTTACHMENT\""
                                + "(\"fILENAME\" {15}",
                        "attachment.html \"SIZE\" 555)) NIL)"
                                + "((\"TEXT\" \"HTML\" NIL NIL \"BASE64\")(\"XXX\" \"YYY\"))" + // Nested
                                "\"mIXED\" (\"bOUNDARY\" \"00032556278a7005e40486d159ca\") NIL NIL))",
                        getNextTag(true) + " oK SUCCESS" });
        mFolder.fetch(new Message[] { message }, fp, null);

        // Check mime structure...
        final Body body = message.getBody();
        assertTrue(body instanceof MimeMultipart);
        MimeMultipart mimeMultipart = (MimeMultipart) body;
        assertEquals(4, mimeMultipart.getCount());
        assertEquals("mixed", mimeMultipart.getSubTypeForTest());

        final Part part1 = mimeMultipart.getBodyPart(0);
        final Part part2 = mimeMultipart.getBodyPart(1);
        final Part part3 = mimeMultipart.getBodyPart(2);
        final Part part4 = mimeMultipart.getBodyPart(3);
        assertTrue(part1 instanceof MimeBodyPart);
        assertTrue(part2 instanceof MimeBodyPart);
        assertTrue(part3 instanceof MimeBodyPart);
        assertTrue(part4 instanceof MimeBodyPart);

        final MimeBodyPart mimePart1 = (MimeBodyPart) part1; // text/plain
        final MimeBodyPart mimePart2 = (MimeBodyPart) part2; // image/png
        final MimeBodyPart mimePart3 = (MimeBodyPart) part3; // text/html
        final MimeBodyPart mimePart4 = (MimeBodyPart) part4; // Nested

        MoreAsserts.assertEquals(new String[] { "1" }, part1.getHeader("X-Android-Attachment-StoreData"));
        MoreAsserts.assertEquals(new String[] { "2" }, part2.getHeader("X-Android-Attachment-StoreData"));
        MoreAsserts.assertEquals(new String[] { "3" }, part3.getHeader("X-Android-Attachment-StoreData"));

        MoreAsserts.assertEquals(new String[] { "text/plain" }, part1.getHeader("Content-Type"));
        MoreAsserts.assertEquals(new String[] { "image/png;\n NAME=\"device.png\"" },
                part2.getHeader("Content-Type"));
        MoreAsserts.assertEquals(new String[] { "text/html" }, part3.getHeader("Content-Type"));

        MoreAsserts.assertEquals(new String[] { "long content id#@!@#" }, part1.getHeader("Content-ID"));
        assertNull(part2.getHeader("Content-ID"));
        assertNull(part3.getHeader("Content-ID"));

        MoreAsserts.assertEquals(new String[] { "7BIT" }, part1.getHeader("Content-Transfer-Encoding"));
        MoreAsserts.assertEquals(new String[] { "BASE64" }, part2.getHeader("Content-Transfer-Encoding"));
        MoreAsserts.assertEquals(new String[] { "7BIT" }, part3.getHeader("Content-Transfer-Encoding"));

        MoreAsserts.assertEquals(new String[] { ";\n size=18" }, part1.getHeader("Content-Disposition"));
        MoreAsserts.assertEquals(new String[] { "attachment;\n filename=\"device.png\";\n size=117840" },
                part2.getHeader("Content-Disposition"));
        MoreAsserts.assertEquals(new String[] { "attachment;\n filename=\"attachment.html\";\n size=\"555\"" },
                part3.getHeader("Content-Disposition"));

        // Check the nested parts.
        final Body part4body = part4.getBody();
        assertTrue(part4body instanceof MimeMultipart);
        MimeMultipart mimeMultipartPart4 = (MimeMultipart) part4body;
        assertEquals(2, mimeMultipartPart4.getCount());

        final MimeBodyPart mimePart41 = (MimeBodyPart) mimeMultipartPart4.getBodyPart(0);
        final MimeBodyPart mimePart42 = (MimeBodyPart) mimeMultipartPart4.getBodyPart(1);

        MoreAsserts.assertEquals(new String[] { "4.1" }, mimePart41.getHeader("X-Android-Attachment-StoreData"));
        MoreAsserts.assertEquals(new String[] { "4.2" }, mimePart42.getHeader("X-Android-Attachment-StoreData"));
    }

    public void testFetchBodySane() throws MessagingException {
        final MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);
        final Message message = mFolder.createMessage("1");

        final FetchProfile fp = new FetchProfile();
        fp.add(FetchProfile.Item.BODY_SANE);
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[\\]<0.51200>\\)",
                new String[] { "* 9 fETCH (uID 1 bODY[] {23}", "from: a@b.com", // 15 bytes
                        "", // 2
                        "test", // 6
                        ")", getNextTag(true) + " oK SUCCESS" });
        mFolder.fetch(new Message[] { message }, fp, null);
        assertEquals("a@b.com", message.getHeader("from")[0]);

        // TODO: Test NO response.
    }

    public void testFetchBody() throws MessagingException {
        final MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);
        final Message message = mFolder.createMessage("1");

        final FetchProfile fp = new FetchProfile();
        fp.add(FetchProfile.Item.BODY);
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[\\]\\)",
                new String[] { "* 9 fETCH (uID 1 bODY[] {23}", "from: a@b.com", // 15 bytes
                        "", // 2
                        "test", // 6
                        ")", getNextTag(true) + " oK SUCCESS" });
        mFolder.fetch(new Message[] { message }, fp, null);
        assertEquals("a@b.com", message.getHeader("from")[0]);

        // TODO: Test NO response.
    }

    public void testFetchAttachment() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);
        final Message message = mFolder.createMessage("1");

        final FetchProfile fp = new FetchProfile();
        fp.add(FetchProfile.Item.STRUCTURE);
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
                new String[] {
                        "* 9 fETCH (uID 1 bODYSTRUCTURE ((\"tEXT\" \"PLAIN\" (\"cHARSET\" \"iSO-8859-1\")"
                                + " CID nIL \"7bIT\" 18 3 NIL NIL NIL)" + "(\"IMAGE\" \"PNG\""
                                + " (\"nAME\" \"device.png\") NIL NIL \"bASE64\" 117840 NIL (\"aTTACHMENT\""
                                + "(\"fILENAME\" \"device.png\")) NIL)" + "\"mIXED\"))",
                        getNextTag(true) + " OK SUCCESS" });
        mFolder.fetch(new Message[] { message }, fp, null);

        // Check mime structure, and get the second part.
        Body body = message.getBody();
        assertTrue(body instanceof MimeMultipart);
        MimeMultipart mimeMultipart = (MimeMultipart) body;
        assertEquals(2, mimeMultipart.getCount());

        Part part1 = mimeMultipart.getBodyPart(1);
        assertTrue(part1 instanceof MimeBodyPart);
        MimeBodyPart mimePart1 = (MimeBodyPart) part1;

        // Fetch the second part
        fp.clear();
        fp.add(mimePart1);
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[2\\]\\)",
                new String[] { "* 9 fETCH (uID 1 bODY[2] {4}", "YWJj)", // abc in base64
                        getNextTag(true) + " oK SUCCESS" });
        mFolder.fetch(new Message[] { message }, fp, null);

        assertEquals("abc", Utility.fromUtf8(IOUtils.toByteArray(mimePart1.getBody().getInputStream())));

        // TODO: Test NO response.
    }

    /**
     * Test for proper operations on servers that return "NIL" for empty message bodies.
     */
    public void testNilMessage() throws MessagingException {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        // Prepare to pull structure and peek body text - this is like the "large message"
        // loop in MessagingController.synchronizeMailboxGeneric()
        FetchProfile fp = new FetchProfile();
        fp.clear();
        fp.add(FetchProfile.Item.STRUCTURE);
        Message message1 = mFolder.createMessage("1");
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
                new String[] { "* 1 fETCH (uID 1 bODYSTRUCTURE (tEXT pLAIN nIL nIL nIL 7bIT 0 0 nIL nIL nIL))",
                        getNextTag(true) + " oK SUCCESS" });
        mFolder.fetch(new Message[] { message1 }, fp, null);

        // The expected result for an empty body is:
        //   * 1 FETCH (UID 1 BODY[TEXT] {0})
        // But some servers are returning NIL for the empty body:
        //   * 1 FETCH (UID 1 BODY[TEXT] NIL)
        // Because this breaks our little parser, fetch() skips over empty parts.
        // The rest of this test is confirming that this is the case.

        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[TEXT\\]\\)",
                new String[] { "* 1 fETCH (uID 1 bODY[tEXT] nIL)", getNextTag(true) + " oK SUCCESS" });
        ArrayList<Part> viewables = new ArrayList<Part>();
        ArrayList<Part> attachments = new ArrayList<Part>();
        MimeUtility.collectParts(message1, viewables, attachments);
        assertTrue(viewables.size() == 1);
        Part emptyBodyPart = viewables.get(0);
        fp.clear();
        fp.add(emptyBodyPart);
        mFolder.fetch(new Message[] { message1 }, fp, null);

        // If this wasn't working properly, there would be an attempted interpretation
        // of the empty part's NIL and possibly a crash.

        // If this worked properly, the "empty" body can now be retrieved
        viewables = new ArrayList<Part>();
        attachments = new ArrayList<Part>();
        MimeUtility.collectParts(message1, viewables, attachments);
        assertTrue(viewables.size() == 1);
        emptyBodyPart = viewables.get(0);
        String text = MimeUtility.getTextFromPart(emptyBodyPart);
        assertNull(text);
    }

    /**
     * Confirm the IMAP parser won't crash when seeing an excess FETCH response line without UID.
     *
     * <p>We've observed that the secure.emailsrvr.com email server returns an excess FETCH response
     * for a UID FETCH command.  These excess responses doesn't have the UID field in it, even
     * though we request, which led the response parser to crash.  We fixed it by ignoring response
     * lines that don't have UID.  This test is to make sure this case.
     */
    public void testExcessFetchResult() throws MessagingException {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        // Create a message, and make sure it's not "SEEN".
        Message message1 = mFolder.createMessage("1");
        assertFalse(message1.isSet(Flag.SEEN));

        FetchProfile fp = new FetchProfile();
        fp.clear();
        fp.add(FetchProfile.Item.FLAGS);
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID FLAGS\\)",
                new String[] { "* 1 fETCH (uID 1 fLAGS (\\Seen))", "* 2 fETCH (fLAGS (\\Seen))",
                        getNextTag(true) + " oK SUCCESS" });

        // Shouldn't crash
        mFolder.fetch(new Message[] { message1 }, fp, null);

        // And the message is "SEEN".
        assertTrue(message1.isSet(Flag.SEEN));
    }

    private ImapMessage prepareForAppendTest(MockTransport mock, String response) throws Exception {
        ImapMessage message = (ImapMessage) mFolder.createMessage("initial uid");
        message.setFrom(new Address("me@test.com"));
        message.setRecipient(RecipientType.TO, new Address("you@test.com"));
        message.setMessageId("<message.id@test.com>");
        message.setFlagDirectlyForTest(Flag.SEEN, true);
        message.setBody(new TextBody("Test Body"));

        // + go ahead
        // * 12345 EXISTS
        // OK [APPENDUID 627684530 17] (Success)

        mock.expect(getNextTag(false) + " APPEND \\\"" + FOLDER_ENCODED + "\\\" \\(\\\\SEEN\\) \\{166\\}",
                new String[] { "+ gO aHead" });

        mock.expectLiterally("From: me@test.com", NO_REPLY);
        mock.expectLiterally("To: you@test.com", NO_REPLY);
        mock.expectLiterally("Message-ID: <message.id@test.com>", NO_REPLY);
        mock.expectLiterally("Content-Type: text/plain;", NO_REPLY);
        mock.expectLiterally(" charset=utf-8", NO_REPLY);
        mock.expectLiterally("Content-Transfer-Encoding: base64", NO_REPLY);
        mock.expectLiterally("", NO_REPLY);
        mock.expectLiterally("VGVzdCBCb2R5", NO_REPLY);
        mock.expectLiterally("", new String[] { "* 7 eXISTS", getNextTag(true) + " " + response });
        return message;
    }

    /**
     * Test for APPEND when the response has APPENDUID.
     */
    public void testAppendMessages() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        ImapMessage message = prepareForAppendTest(mock, "oK [aPPENDUID 1234567 13] (Success)");

        mFolder.appendMessages(new Message[] { message });

        assertEquals("13", message.getUid());
        assertEquals(7, mFolder.getMessageCount());
    }

    /**
     * Test for APPEND when the response doesn't have APPENDUID.
     */
    public void testAppendMessagesNoAppendUid() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        ImapMessage message = prepareForAppendTest(mock, "OK Success");

        // First try w/o parenthesis
        mock.expectLiterally(getNextTag(false) + " UID SEARCH HEADER MESSAGE-ID <message.id@test.com>",
                new String[] { "* sEARCH 321", getNextTag(true) + " oK success" });
        // If that fails, then try w/ parenthesis
        mock.expectLiterally(getNextTag(false) + " UID SEARCH (HEADER MESSAGE-ID <message.id@test.com>)",
                new String[] { "* sEARCH 321", getNextTag(true) + " oK success" });

        mFolder.appendMessages(new Message[] { message });

        assertEquals("321", message.getUid());
    }

    /**
     * Test for append failure.
     *
     * We don't check the response for APPEND.  We just SEARCH for the message-id to get the UID.
     * If append has failed, the SEARCH command returns no UID, and the UID of the message is left
     * unset.
     */
    public void testAppendFailure() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        ImapMessage message = prepareForAppendTest(mock, "NO No space left on the server.");
        assertEquals("initial uid", message.getUid());
        // First try w/o parenthesis
        mock.expectLiterally(getNextTag(false) + " UID SEARCH HEADER MESSAGE-ID <message.id@test.com>",
                new String[] { "* sEARCH", // not found
                        getNextTag(true) + " oK Search completed." });
        // If that fails, then try w/ parenthesis
        mock.expectLiterally(getNextTag(false) + " UID SEARCH (HEADER MESSAGE-ID <message.id@test.com>)",
                new String[] { "* sEARCH", // not found
                        getNextTag(true) + " oK Search completed." });

        mFolder.appendMessages(new Message[] { message });

        // Shouldn't have changed
        assertEquals("initial uid", message.getUid());
    }

    public void testGetAllFolders() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        expectLogin(mock);

        expectNoop(mock, true);
        mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"",
                new String[] { "* lIST (\\HAsNoChildren) \"/\" \"inbox\"",
                        "* lIST (\\hAsnochildren) \"/\" \"Drafts\"", "* lIST (\\nOselect) \"/\" \"no select\"",
                        "* lIST (\\HAsNoChildren) \"/\" \"&ZeVnLIqe-\"", // Japanese folder name
                        getNextTag(true) + " oK SUCCESS" });
        Folder[] folders = mStore.updateFolders();
        ImapFolder testFolder;

        testFolder = (ImapFolder) folders[0];
        assertEquals("INBOX", testFolder.getName());
        assertEquals(SELECTABLE_BITS, testFolder.mMailbox.mFlags & SELECTABLE_BITS);

        testFolder = (ImapFolder) folders[1];
        assertEquals("no select", testFolder.getName());
        assertEquals(0, testFolder.mMailbox.mFlags & SELECTABLE_BITS);

        testFolder = (ImapFolder) folders[2];
        assertEquals("\u65E5\u672C\u8A9E", testFolder.getName());
        assertEquals(SELECTABLE_BITS, testFolder.mMailbox.mFlags & SELECTABLE_BITS);

        testFolder = (ImapFolder) folders[3];
        assertEquals("Drafts", testFolder.getName());
        assertEquals(SELECTABLE_BITS, testFolder.mMailbox.mFlags & SELECTABLE_BITS);
        // TODO test with path prefix
        // TODO: Test NO response.
    }

    public void testEncodeFolderName() {
        // null prefix
        assertEquals("", ImapStore.encodeFolderName("", null));
        assertEquals("a", ImapStore.encodeFolderName("a", null));
        assertEquals("XYZ", ImapStore.encodeFolderName("XYZ", null));
        assertEquals("&ZeVnLIqe-", ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", null));
        assertEquals("!&ZeVnLIqe-!", ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", null));
        // empty prefix (same as a null prefix)
        assertEquals("", ImapStore.encodeFolderName("", ""));
        assertEquals("a", ImapStore.encodeFolderName("a", ""));
        assertEquals("XYZ", ImapStore.encodeFolderName("XYZ", ""));
        assertEquals("&ZeVnLIqe-", ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", ""));
        assertEquals("!&ZeVnLIqe-!", ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", ""));
        // defined prefix
        assertEquals("[Gmail]/", ImapStore.encodeFolderName("", "[Gmail]/"));
        assertEquals("[Gmail]/a", ImapStore.encodeFolderName("a", "[Gmail]/"));
        assertEquals("[Gmail]/XYZ", ImapStore.encodeFolderName("XYZ", "[Gmail]/"));
        assertEquals("[Gmail]/&ZeVnLIqe-", ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", "[Gmail]/"));
        assertEquals("[Gmail]/!&ZeVnLIqe-!", ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", "[Gmail]/"));
        // Add prefix to special mailbox "INBOX" [case insensitive), no affect
        assertEquals("INBOX", ImapStore.encodeFolderName("INBOX", "[Gmail]/"));
        assertEquals("inbox", ImapStore.encodeFolderName("inbox", "[Gmail]/"));
        assertEquals("InBoX", ImapStore.encodeFolderName("InBoX", "[Gmail]/"));
    }

    public void testDecodeFolderName() {
        // null prefix
        assertEquals("", ImapStore.decodeFolderName("", null));
        assertEquals("a", ImapStore.decodeFolderName("a", null));
        assertEquals("XYZ", ImapStore.decodeFolderName("XYZ", null));
        assertEquals("\u65E5\u672C\u8A9E", ImapStore.decodeFolderName("&ZeVnLIqe-", null));
        assertEquals("!\u65E5\u672C\u8A9E!", ImapStore.decodeFolderName("!&ZeVnLIqe-!", null));
        // empty prefix (same as a null prefix)
        assertEquals("", ImapStore.decodeFolderName("", ""));
        assertEquals("a", ImapStore.decodeFolderName("a", ""));
        assertEquals("XYZ", ImapStore.decodeFolderName("XYZ", ""));
        assertEquals("\u65E5\u672C\u8A9E", ImapStore.decodeFolderName("&ZeVnLIqe-", ""));
        assertEquals("!\u65E5\u672C\u8A9E!", ImapStore.decodeFolderName("!&ZeVnLIqe-!", ""));
        // defined prefix; prefix found, prefix removed
        assertEquals("", ImapStore.decodeFolderName("[Gmail]/", "[Gmail]/"));
        assertEquals("a", ImapStore.decodeFolderName("[Gmail]/a", "[Gmail]/"));
        assertEquals("XYZ", ImapStore.decodeFolderName("[Gmail]/XYZ", "[Gmail]/"));
        assertEquals("\u65E5\u672C\u8A9E", ImapStore.decodeFolderName("[Gmail]/&ZeVnLIqe-", "[Gmail]/"));
        assertEquals("!\u65E5\u672C\u8A9E!", ImapStore.decodeFolderName("[Gmail]/!&ZeVnLIqe-!", "[Gmail]/"));
        // defined prefix; prefix not found, no affect
        assertEquals("INBOX/", ImapStore.decodeFolderName("INBOX/", "[Gmail]/"));
        assertEquals("INBOX/a", ImapStore.decodeFolderName("INBOX/a", "[Gmail]/"));
        assertEquals("INBOX/XYZ", ImapStore.decodeFolderName("INBOX/XYZ", "[Gmail]/"));
        assertEquals("INBOX/\u65E5\u672C\u8A9E", ImapStore.decodeFolderName("INBOX/&ZeVnLIqe-", "[Gmail]/"));
        assertEquals("INBOX/!\u65E5\u672C\u8A9E!", ImapStore.decodeFolderName("INBOX/!&ZeVnLIqe-!", "[Gmail]/"));
    }

    public void testEnsurePrefixIsValid() {
        // Test mPathSeparator == null
        mStore.mPathSeparator = null;
        mStore.mPathPrefix = null;
        mStore.ensurePrefixIsValid();
        assertNull(mStore.mPathPrefix);

        mStore.mPathPrefix = "";
        mStore.ensurePrefixIsValid();
        assertEquals("", mStore.mPathPrefix);

        mStore.mPathPrefix = "foo";
        mStore.ensurePrefixIsValid();
        assertEquals("foo", mStore.mPathPrefix);

        mStore.mPathPrefix = "foo.";
        mStore.ensurePrefixIsValid();
        assertEquals("foo.", mStore.mPathPrefix);

        // Test mPathSeparator == ""
        mStore.mPathSeparator = "";
        mStore.mPathPrefix = null;
        mStore.ensurePrefixIsValid();
        assertNull(mStore.mPathPrefix);

        mStore.mPathPrefix = "";
        mStore.ensurePrefixIsValid();
        assertEquals("", mStore.mPathPrefix);

        mStore.mPathPrefix = "foo";
        mStore.ensurePrefixIsValid();
        assertEquals("foo", mStore.mPathPrefix);

        mStore.mPathPrefix = "foo.";
        mStore.ensurePrefixIsValid();
        assertEquals("foo.", mStore.mPathPrefix);

        // Test mPathSeparator is non-empty
        mStore.mPathSeparator = ".";
        mStore.mPathPrefix = null;
        mStore.ensurePrefixIsValid();
        assertNull(mStore.mPathPrefix);

        mStore.mPathPrefix = "";
        mStore.ensurePrefixIsValid();
        assertEquals("", mStore.mPathPrefix);

        mStore.mPathPrefix = "foo";
        mStore.ensurePrefixIsValid();
        assertEquals("foo.", mStore.mPathPrefix);

        // Trailing separator; path separator NOT appended
        mStore.mPathPrefix = "foo.";
        mStore.ensurePrefixIsValid();
        assertEquals("foo.", mStore.mPathPrefix);

        // Trailing punctuation has no affect; path separator still appended
        mStore.mPathPrefix = "foo/";
        mStore.ensurePrefixIsValid();
        assertEquals("foo/.", mStore.mPathPrefix);
    }

    public void testOpen() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        expectLogin(mock);

        final Folder folder = mStore.getFolder("test");

        // Not exist
        mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
                new String[] { getNextTag(true) + " nO no such mailbox" });
        try {
            folder.open(OpenMode.READ_WRITE);
            fail();
        } catch (MessagingException expected) {
        }

        // READ-WRITE
        expectNoop(mock, true); // Need it because we reuse the connection.
        mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
                new String[] { "* 1 eXISTS", getNextTag(true) + " oK [rEAD-wRITE]" });

        folder.open(OpenMode.READ_WRITE);
        assertTrue(folder.exists());
        assertEquals(1, folder.getMessageCount());
        assertEquals(OpenMode.READ_WRITE, folder.getMode());

        assertTrue(folder.isOpen());
        folder.close(false);
        assertFalse(folder.isOpen());

        // READ-ONLY
        expectNoop(mock, true); // Need it because we reuse the connection.
        mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
                new String[] { "* 2 eXISTS", getNextTag(true) + " oK [rEAD-oNLY]" });

        folder.open(OpenMode.READ_WRITE);
        assertTrue(folder.exists());
        assertEquals(2, folder.getMessageCount());
        assertEquals(OpenMode.READ_ONLY, folder.getMode());

        // Try to re-open as read-write.  Should send SELECT again.
        expectNoop(mock, true); // Need it because we reuse the connection.
        mock.expect(getNextTag(false) + " SELECT \\\"test\\\"",
                new String[] { "* 15 eXISTS", getNextTag(true) + " oK selected" });

        folder.open(OpenMode.READ_WRITE);
        assertTrue(folder.exists());
        assertEquals(15, folder.getMessageCount());
        assertEquals(OpenMode.READ_WRITE, folder.getMode());
    }

    public void testExists() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        expectLogin(mock);

        // Folder exists
        Folder folder = mStore.getFolder("\u65E5\u672C\u8A9E");
        mock.expect(getNextTag(false) + " STATUS \\\"&ZeVnLIqe-\\\" \\(UIDVALIDITY\\)",
                new String[] { "* sTATUS \"&ZeVnLIqe-\" (mESSAGES 10)", getNextTag(true) + " oK SUCCESS" });

        assertTrue(folder.exists());

        // Connection verification
        expectNoop(mock, true);

        // Doesn't exist
        folder = mStore.getFolder("no such folder");
        mock.expect(getNextTag(false) + " STATUS \\\"no such folder\\\" \\(UIDVALIDITY\\)",
                new String[] { getNextTag(true) + " NO No such folder!" });

        assertFalse(folder.exists());
    }

    public void testCreate() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        expectLogin(mock);

        // Success
        Folder folder = mStore.getFolder("\u65E5\u672C\u8A9E");

        assertTrue(folder.canCreate(FolderType.HOLDS_MESSAGES));

        mock.expect(getNextTag(false) + " CREATE \\\"&ZeVnLIqe-\\\"",
                new String[] { getNextTag(true) + " oK Success" });

        assertTrue(folder.create(FolderType.HOLDS_MESSAGES));

        // Connection verification
        expectNoop(mock, true);

        // Failure
        mock.expect(getNextTag(false) + " CREATE \\\"&ZeVnLIqe-\\\"",
                new String[] { getNextTag(true) + " nO Can't create folder" });

        assertFalse(folder.create(FolderType.HOLDS_MESSAGES));
    }

    private void setupCopyMessages(boolean withUidPlus) throws Exception {
        mCopyMock = openAndInjectMockTransport();
        setupOpenFolder(mCopyMock, new String[] { "* iD nIL", "oK" }, "rEAD-wRITE", withUidPlus);
        mFolder.open(OpenMode.READ_WRITE);

        mCopyToFolder = mStore.getFolder("\u65E5\u672C\u8A9E");
        Message m1 = mFolder.createMessage("11");
        m1.setMessageId("<4D8978AE.0000005D@m58.foo.com>");
        Message m2 = mFolder.createMessage("12");
        m2.setMessageId("<549373104MSOSI1:145OSIMS@bar.com>");
        mCopyMessages = new Message[] { m1, m2 };
    }

    /**
     * Returns the pattern for the IMAP request to copy messages.
     */
    private String getCopyMessagesPattern() {
        return getNextTag(false) + " UID COPY 11\\,12 \\\"&ZeVnLIqe-\\\"";
    }

    /**
     * Returns the pattern for the IMAP request to search for messages based on Message-Id.
     */
    private String getSearchMessagesPattern(String messageId) {
        return getNextTag(false) + " UID SEARCH HEADER Message-Id \"" + messageId + "\"";
    }

    /**
     * Counts the number of times the callback methods are invoked.
     */
    private static class MessageUpdateCallbackCounter implements Folder.MessageUpdateCallbacks {
        int messageNotFoundCalled;
        int messageUidChangeCalled;

        @Override
        public void onMessageNotFound(Message message) {
            ++messageNotFoundCalled;
        }

        @Override
        public void onMessageUidChange(Message message, String newUid) {
            ++messageUidChangeCalled;
        }
    }

    // TODO Test additional degenerate cases; src msg not found, ...
    // Golden case; successful copy with UIDCOPY result
    public void testCopyMessages1() throws Exception {
        setupCopyMessages(true);
        mCopyMock.expect(getCopyMessagesPattern(), new String[] { "* Ok COPY in progress",
                getNextTag(true) + " oK [COPYUID 777 11,12 45,46] UID COPY completed" });

        MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
        mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);

        assertEquals(0, cb.messageNotFoundCalled);
        assertEquals(2, cb.messageUidChangeCalled);
    }

    // Degenerate case; NO, un-tagged response works
    public void testCopyMessages2() throws Exception {
        setupCopyMessages(true);
        mCopyMock.expect(getCopyMessagesPattern(), new String[] { "* No Some error occured during the copy",
                getNextTag(true) + " oK [COPYUID 777 11,12 45,46] UID COPY completed" });

        MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
        mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);

        assertEquals(0, cb.messageNotFoundCalled);
        assertEquals(2, cb.messageUidChangeCalled);
    }

    // Degenerate case; NO, tagged response throws MessagingException
    public void testCopyMessages3() throws Exception {
        try {
            setupCopyMessages(false);
            mCopyMock.expect(getCopyMessagesPattern(),
                    new String[] { getNextTag(true) + " No copy did not finish" });

            mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);

            fail("MessagingException expected.");
        } catch (MessagingException expected) {
        }
    }

    // Degenerate case; BAD, un-tagged response throws MessagingException
    public void testCopyMessages4() throws Exception {
        try {
            setupCopyMessages(true);
            mCopyMock.expect(getCopyMessagesPattern(),
                    new String[] { "* BAD failed for some reason", getNextTag(true) + " Ok copy completed" });

            mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);

            fail("MessagingException expected.");
        } catch (MessagingException expected) {
        }
    }

    // Degenerate case; BAD, tagged response throws MessagingException
    public void testCopyMessages5() throws Exception {
        try {
            setupCopyMessages(false);
            mCopyMock.expect(getCopyMessagesPattern(), new String[] { getNextTag(true) + " BaD copy completed" });

            mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);

            fail("MessagingException expected.");
        } catch (MessagingException expected) {
        }
    }

    // Golden case; successful copy getting UIDs via search
    public void testCopyMessages6() throws Exception {
        setupCopyMessages(false);
        mCopyMock.expect(getCopyMessagesPattern(), new String[] { getNextTag(true) + " oK UID COPY completed", });
        // New connection, so, we need to login again & the tag count gets reset
        int saveTag = resetTag();
        expectLogin(mCopyMock, new String[] { "* iD nIL", "oK" }, false);
        // Select destination folder
        expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
        // Perform searches
        mCopyMock.expect(getSearchMessagesPattern("<4D8978AE.0000005D@m58.foo.com>"), new String[] { "* SeArCh 777",
                getNextTag(true) + " oK UID SEARCH completed (1 msgs in 3.14159 secs)", });
        mCopyMock.expect(getSearchMessagesPattern("<549373104MSOSI1:145OSIMS@bar.com>"), new String[] {
                "* sEaRcH 1818", getNextTag(true) + " oK UID SEARCH completed (1 msgs in 2.71828 secs)", });
        // Resume commands on the initial connection
        resetTag(saveTag);
        // Select the original folder
        expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");

        MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
        mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);

        assertEquals(0, cb.messageNotFoundCalled);
        assertEquals(2, cb.messageUidChangeCalled);
    }

    // Degenerate case; searches turn up nothing
    public void testCopyMessages7() throws Exception {
        setupCopyMessages(false);
        mCopyMock.expect(getCopyMessagesPattern(), new String[] { getNextTag(true) + " oK UID COPY completed", });
        // New connection, so, we need to login again & the tag count gets reset
        int saveTag = resetTag();
        expectLogin(mCopyMock, new String[] { "* iD nIL", "oK" }, false);
        // Select destination folder
        expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
        // Perform searches
        mCopyMock.expect(getSearchMessagesPattern("<4D8978AE.0000005D@m58.foo.com>"), new String[] { "* SeArCh",
                getNextTag(true) + " oK UID SEARCH completed (0 msgs in 6.02214 secs)", });
        mCopyMock.expect(getSearchMessagesPattern("<549373104MSOSI1:145OSIMS@bar.com>"), new String[] { "* sEaRcH",
                getNextTag(true) + " oK UID SEARCH completed (0 msgs in 2.99792 secs)", });
        // Resume commands on the initial connection
        resetTag(saveTag);
        // Select the original folder
        expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");

        MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
        mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);

        assertEquals(0, cb.messageNotFoundCalled);
        assertEquals(0, cb.messageUidChangeCalled);
    }

    // Degenerate case; search causes an exception; must be eaten
    public void testCopyMessages8() throws Exception {
        setupCopyMessages(false);
        mCopyMock.expect(getCopyMessagesPattern(), new String[] { getNextTag(true) + " oK UID COPY completed", });
        // New connection, so, we need to login again & the tag count gets reset
        int saveTag = resetTag();
        expectLogin(mCopyMock, new String[] { "* iD nIL", "oK" }, false);
        // Select destination folder
        expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
        // Perform searches
        mCopyMock.expect(getSearchMessagesPattern("<4D8978AE.0000005D@m58.foo.com>"),
                new String[] { getNextTag(true) + " BaD search failed" });
        mCopyMock.expect(getSearchMessagesPattern("<549373104MSOSI1:145OSIMS@bar.com>"),
                new String[] { getNextTag(true) + " BaD search failed" });
        // Resume commands on the initial connection
        resetTag(saveTag);
        // Select the original folder
        expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");

        MessageUpdateCallbackCounter cb = new MessageUpdateCallbackCounter();
        mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);

        assertEquals(0, cb.messageNotFoundCalled);
        assertEquals(0, cb.messageUidChangeCalled);
    }

    public void testGetUnreadMessageCount() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        mock.expect(getNextTag(false) + " STATUS \\\"" + FOLDER_ENCODED + "\\\" \\(UNSEEN\\)", new String[] {
                "* sTATUS \"" + FOLDER_ENCODED + "\" (X 1 uNSEEN 123)", getNextTag(true) + " oK copy completed" });

        assertEquals(123, mFolder.getUnreadMessageCount());
    }

    public void testExpunge() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        mock.expect(getNextTag(false) + " EXPUNGE", new String[] { getNextTag(true) + " oK success" });

        mFolder.expunge();

        // TODO: Test NO response. (permission denied)
    }

    public void testSetFlags() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        Message[] messages = new Message[] { mFolder.createMessage("11"), mFolder.createMessage("12"), };

        // Set
        mock.expect(getNextTag(false) + " UID STORE 11\\,12 \\+FLAGS.SILENT \\(\\\\FLAGGED \\\\SEEN\\)",
                new String[] { getNextTag(true) + " oK success" });
        mFolder.setFlags(messages, new Flag[] { Flag.FLAGGED, Flag.SEEN }, true);

        // Clear
        mock.expect(getNextTag(false) + " UID STORE 11\\,12 \\-FLAGS.SILENT \\(\\\\DELETED\\)",
                new String[] { getNextTag(true) + " oK success" });
        mFolder.setFlags(messages, new Flag[] { Flag.DELETED }, false);

        // TODO: Test NO response. (src message not found)
    }

    public void testSearchForUids() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        // Single results
        mock.expect(getNextTag(false) + " UID SEARCH X",
                new String[] { "* sEARCH 1", getNextTag(true) + " oK success" });
        MoreAsserts.assertEquals(new String[] { "1" }, mFolder.searchForUids("X"));

        // Multiple results, including SEARCH with no UIDs.
        mock.expect(getNextTag(false) + " UID SEARCH UID 123", new String[] { "* sEARCH 123 4 567", "* search",
                "* sEARCH 0", "* SEARCH", "* sEARCH 100 200 300", getNextTag(true) + " oK success" });
        MoreAsserts.assertEquals(new String[] { "123", "4", "567", "0", "100", "200", "300" },
                mFolder.searchForUids("UID 123"));

        // NO result
        mock.expect(getNextTag(false) + " UID SEARCH SOME CRITERIA",
                new String[] { getNextTag(true) + " nO not found" });
        MoreAsserts.assertEquals(new String[] {}, mFolder.searchForUids("SOME CRITERIA"));

        // OK result, but result is empty. (Probably against RFC)
        mock.expect(getNextTag(false) + " UID SEARCH SOME CRITERIA",
                new String[] { getNextTag(true) + " oK success" });
        MoreAsserts.assertEquals(new String[] {}, mFolder.searchForUids("SOME CRITERIA"));

        // OK result with empty search response.
        mock.expect(getNextTag(false) + " UID SEARCH SOME CRITERIA",
                new String[] { "* search", getNextTag(true) + " oK success" });
        MoreAsserts.assertEquals(new String[] {}, mFolder.searchForUids("SOME CRITERIA"));
    }

    public void testGetMessage() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        // Found
        mock.expect(getNextTag(false) + " UID SEARCH UID 123",
                new String[] { "* sEARCH 123", getNextTag(true) + " oK success" });
        assertEquals("123", mFolder.getMessage("123").getUid());

        // Not found
        mock.expect(getNextTag(false) + " UID SEARCH UID 123", new String[] { getNextTag(true) + " nO not found" });
        assertNull(mFolder.getMessage("123"));
    }

    /** Test for getMessages(int, int, MessageRetrievalListener) */
    public void testGetMessages1() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        // Found
        mock.expect(getNextTag(false) + " UID SEARCH 3:5 NOT DELETED",
                new String[] { "* sEARCH 3 4", getNextTag(true) + " oK success" });

        checkMessageUids(new String[] { "3", "4" }, mFolder.getMessages(3, 5, null));

        // Not found
        mock.expect(getNextTag(false) + " UID SEARCH 3:5 NOT DELETED",
                new String[] { getNextTag(true) + " nO not found" });

        checkMessageUids(new String[] {}, mFolder.getMessages(3, 5, null));
    }

    /**
     * Test for getMessages(String[] uids, MessageRetrievalListener) where uids != null.
     * (testGetMessages3() covers the case where uids == null.)
     */
    public void testGetMessages2() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);

        // No command will be sent
        checkMessageUids(new String[] { "3", "4", "5" }, mFolder.getMessages(new String[] { "3", "4", "5" }, null));

        checkMessageUids(new String[] {}, mFolder.getMessages(new String[] {}, null));
    }

    private static void checkMessageUids(String[] expectedUids, Message[] actualMessages) {
        ArrayList<String> list = new ArrayList<String>();
        for (Message m : actualMessages) {
            list.add(m.getUid());
        }
        MoreAsserts.assertEquals(expectedUids, list.toArray(new String[0]));
    }

    /**
     * Test for {@link ImapStore#getConnection}
     */
    public void testGetConnection() throws Exception {
        MockTransport mock = openAndInjectMockTransport();

        // Start: No pooled connections.
        assertEquals(0, mStore.getConnectionPoolForTest().size());

        // Get 1st connection.
        final ImapConnection con1 = mStore.getConnection();
        assertNotNull(con1);
        assertEquals(0, mStore.getConnectionPoolForTest().size()); // Pool size not changed.
        assertFalse(con1.isTransportOpenForTest()); // Transport not open yet.

        // Open con1
        expectLogin(mock);
        con1.open();
        assertTrue(con1.isTransportOpenForTest());

        // Get 2nd connection.
        final ImapConnection con2 = mStore.getConnection();
        assertNotNull(con2);
        assertEquals(0, mStore.getConnectionPoolForTest().size()); // Pool size not changed.
        assertFalse(con2.isTransportOpenForTest()); // Transport not open yet.

        // con1 != con2
        assertNotSame(con1, con2);

        // New connection, so, we need to login again & the tag count gets reset
        int saveTag = resetTag();

        // Open con2
        expectLogin(mock);
        con2.open();
        assertTrue(con1.isTransportOpenForTest());

        // Now we have two open connections: con1 and con2

        // Save con1 in the pool.
        mStore.poolConnection(con1);
        assertEquals(1, mStore.getConnectionPoolForTest().size());

        // Get another connection.  Should get con1, after verifying the connection.
        saveTag = resetTag(saveTag);
        mock.expect(getNextTag(false) + " NOOP", new String[] { getNextTag(true) + " oK success" });

        final ImapConnection con1b = mStore.getConnection();
        assertEquals(0, mStore.getConnectionPoolForTest().size()); // No connections left in pool
        assertSame(con1, con1b);
        assertTrue(con1.isTransportOpenForTest()); // We opened it.

        // Save con2.
        mStore.poolConnection(con2);
        assertEquals(1, mStore.getConnectionPoolForTest().size());

        // Resume con2 tags ...
        resetTag(saveTag);

        // Try to get connection, but this time, connection gets closed.
        mock.expect(getNextTag(false) + " NOOP", new String[] { getNextTag(true) + "* bYE bye" });
        final ImapConnection con3 = mStore.getConnection();
        assertNotNull(con3);
        assertEquals(0, mStore.getConnectionPoolForTest().size()); // No connections left in pool

        // It should be a new connection.
        assertNotSame(con1, con3);
        assertNotSame(con2, con3);
    }

    public void testCheckSettings() throws Exception {
        MockTransport mock = openAndInjectMockTransport();

        expectLogin(mock);
        mStore.checkSettings();

        resetTag();
        expectLogin(mock, false, false, false, new String[] { "* iD nIL", "oK" }, "nO authentication failed");
        try {
            mStore.checkSettings();
            fail();
        } catch (MessagingException expected) {
        }
    }

    // Compatibility tests...

    /**
     * Getting an ALERT with a % mark in the message, which crashed the old parser.
     */
    public void testQuotaAlert() throws Exception {
        MockTransport mock = openAndInjectMockTransport();
        expectLogin(mock);

        // Success
        Folder folder = mStore.getFolder("INBOX");

        // The following response was copied from an actual bug...
        mock.expect(getNextTag(false) + " SELECT \"INBOX\"",
                new String[] {
                        "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen NonJunk $Forwarded Junk"
                                + " $Label4 $Label1 $Label2 $Label3 $Label5 $MDNSent Old)",
                        "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen NonJunk"
                                + " $Forwarded Junk $Label4 $Label1 $Label2 $Label3 $Label5 $MDNSent Old \\*)]",
                        "* 6406 EXISTS", "* 0 RECENT", "* OK [UNSEEN 5338]", "* OK [UIDVALIDITY 1055957975]",
                        "* OK [UIDNEXT 449625]", "* NO [ALERT] Mailbox is at 98% of quota",
                        getNextTag(true) + " OK [READ-WRITE] Completed" });
        folder.open(OpenMode.READ_WRITE); // shouldn't crash.
        assertEquals(6406, folder.getMessageCount());
    }

    /**
     * Apparently some servers send a size in the wrong format. e.g. 123E
     */
    public void testFetchBodyStructureMalformed() throws Exception {
        final MockTransport mock = openAndInjectMockTransport();
        setupOpenFolder(mock);
        mFolder.open(OpenMode.READ_WRITE);
        final Message message = mFolder.createMessage("1");

        final FetchProfile fp = new FetchProfile();
        fp.add(FetchProfile.Item.STRUCTURE);
        mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)",
                new String[] { "* 9 FETCH (UID 1 BODYSTRUCTURE (\"TEXT\" \"PLAIN\" ()" + " NIL NIL NIL 123E 3))", // 123E isn't a number!
                        getNextTag(true) + " OK SUCCESS" });
        mFolder.fetch(new Message[] { message }, fp, null);

        // Check mime structure...
        MoreAsserts.assertEquals(new String[] { "text/plain" }, message.getHeader("Content-Type"));
        assertNull(message.getHeader("Content-Transfer-Encoding"));
        assertNull(message.getHeader("Content-ID"));

        // Doesn't have size=xxx
        assertNull(message.getHeader("Content-Disposition"));
    }

    /**
     * Folder name with special chars in it.
     *
     * Gmail puts the folder name in the OK response, which crashed the old parser if there's a
     * special char in the folder name.
     */
    public void testFolderNameWithSpecialChars() throws Exception {
        final String FOLDER_1 = "@u88**%_St";
        final String FOLDER_1_QUOTED = Pattern.quote(FOLDER_1);
        final String FOLDER_2 = "folder test_06";

        MockTransport mock = openAndInjectMockTransport();
        expectLogin(mock);

        // List folders.
        expectNoop(mock, true);
        mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"", new String[] { "* LIST () \"/\" \"" + FOLDER_1 + "\"",
                "* LIST () \"/\" \"" + FOLDER_2 + "\"", getNextTag(true) + " OK SUCCESS" });
        final Folder[] folders = mStore.updateFolders();

        ArrayList<String> list = new ArrayList<String>();
        for (Folder f : folders) {
            list.add(f.getName());
        }
        MoreAsserts.assertEquals(new String[] { "INBOX", FOLDER_2, FOLDER_1 }, list.toArray(new String[0]));

        // Try to open the folders.
        expectNoop(mock, true);
        mock.expect(getNextTag(false) + " SELECT \"" + FOLDER_1_QUOTED + "\"",
                new String[] { "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
                        "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]", "* 0 EXISTS",
                        "* 0 RECENT", "* OK [UNSEEN 0]", "* OK [UIDNEXT 1]",
                        getNextTag(true) + " OK [READ-WRITE] " + FOLDER_1 });
        folders[2].open(OpenMode.READ_WRITE);
        folders[2].close(false);

        expectNoop(mock, true);
        mock.expect(getNextTag(false) + " SELECT \"" + FOLDER_2 + "\"",
                new String[] { "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
                        "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]", "* 0 EXISTS",
                        "* 0 RECENT", "* OK [UNSEEN 0]", "* OK [UIDNEXT 1]",
                        getNextTag(true) + " OK [READ-WRITE] " + FOLDER_2 });
        folders[1].open(OpenMode.READ_WRITE);
        folders[1].close(false);
    }

    /**
     * Callback for {@link #runAndExpectMessagingException}.
     */
    private interface RunAndExpectMessagingExceptionTarget {
        public void run(MockTransport mockTransport) throws Exception;
    }

    /**
     * Set up the usual mock transport, open the folder,
     * run {@link RunAndExpectMessagingExceptionTarget} and make sure a {@link MessagingException}
     * is thrown.
     */
    private void runAndExpectMessagingException(RunAndExpectMessagingExceptionTarget target) throws Exception {
        try {
            final MockTransport mockTransport = openAndInjectMockTransport();
            setupOpenFolder(mockTransport);
            mFolder.open(OpenMode.READ_WRITE);

            target.run(mockTransport);

            fail("MessagingException expected.");
        } catch (MessagingException expected) {
        }
    }

    /**
     * Make sure that IOExceptions are always converted to MessagingException.
     */
    public void testFetchIOException() throws Exception {
        runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
            @Override
            public void run(MockTransport mockTransport) throws Exception {
                mockTransport.expectIOException();

                final Message message = mFolder.createMessage("1");
                final FetchProfile fp = new FetchProfile();
                fp.add(FetchProfile.Item.STRUCTURE);

                mFolder.fetch(new Message[] { message }, fp, null);
            }
        });
    }

    /**
     * Make sure that IOExceptions are always converted to MessagingException.
     */
    public void testUnreadMessageCountIOException() throws Exception {
        runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
            @Override
            public void run(MockTransport mockTransport) throws Exception {
                mockTransport.expectIOException();

                mFolder.getUnreadMessageCount();
            }
        });
    }

    /**
     * Make sure that IOExceptions are always converted to MessagingException.
     */
    public void testCopyMessagesIOException() throws Exception {
        runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
            @Override
            public void run(MockTransport mockTransport) throws Exception {
                mockTransport.expectIOException();

                final Message message = mFolder.createMessage("1");
                final Folder folder = mStore.getFolder("test");

                mFolder.copyMessages(new Message[] { message }, folder, null);
            }
        });
    }

    /**
     * Make sure that IOExceptions are always converted to MessagingException.
     */
    public void testSearchForUidsIOException() throws Exception {
        runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
            @Override
            public void run(MockTransport mockTransport) throws Exception {
                mockTransport.expectIOException();

                mFolder.getMessage("uid");
            }
        });
    }

    /**
     * Make sure that IOExceptions are always converted to MessagingException.
     */
    public void testExpungeIOException() throws Exception {
        runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
            @Override
            public void run(MockTransport mockTransport) throws Exception {
                mockTransport.expectIOException();

                mFolder.expunge();
            }
        });
    }

    /**
     * Make sure that IOExceptions are always converted to MessagingException.
     */
    public void testOpenIOException() throws Exception {
        runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
            @Override
            public void run(MockTransport mockTransport) throws Exception {
                mockTransport.expectIOException();
                final Folder folder = mStore.getFolder("test");
                folder.open(OpenMode.READ_WRITE);
            }
        });
    }

    /** Creates a folder & mailbox */
    private ImapFolder createFolder(long id, String displayName, String serverId, char delimiter) {
        ImapFolder folder = new ImapFolder(null, serverId);
        Mailbox mailbox = new Mailbox();
        mailbox.mId = id;
        mailbox.mDisplayName = displayName;
        mailbox.mServerId = serverId;
        mailbox.mDelimiter = delimiter;
        mailbox.mFlags = 0xAAAAAAA8;
        folder.mMailbox = mailbox;
        return folder;
    }

    /** Tests creating folder hierarchies */
    public void testCreateHierarchy() {
        HashMap<String, ImapFolder> testMap = new HashMap<String, ImapFolder>();

        // Create hierarchy
        //   |-INBOX
        //   |  +-b
        //   |-a
        //   |  |-b
        //   |  |-c
        //   |  +-d
        //   |    +-b
        //   |      +-b
        //   +-g
        ImapFolder[] folders = { createFolder(1L, "INBOX", "INBOX", '/'), createFolder(2L, "b", "INBOX/b", '/'),
                createFolder(3L, "a", "a", '/'), createFolder(4L, "b", "a/b", '/'),
                createFolder(5L, "c", "a/c", '/'), createFolder(6L, "d", "a/d", '/'),
                createFolder(7L, "b", "a/d/b", '/'), createFolder(8L, "b", "a/d/b/b", '/'),
                createFolder(9L, "g", "g", '/'), };
        for (ImapFolder folder : folders) {
            testMap.put(folder.getName(), folder);
        }

        ImapStore.createHierarchy(testMap);
        // 'INBOX'
        assertEquals(-1L, folders[0].mMailbox.mParentKey);
        assertEquals(0xAAAAAAAB, folders[0].mMailbox.mFlags);
        // 'INBOX/b'
        assertEquals(1L, folders[1].mMailbox.mParentKey);
        assertEquals(0xAAAAAAA8, folders[1].mMailbox.mFlags);
        // 'a'
        assertEquals(-1L, folders[2].mMailbox.mParentKey);
        assertEquals(0xAAAAAAAB, folders[2].mMailbox.mFlags);
        // 'a/b'
        assertEquals(3L, folders[3].mMailbox.mParentKey);
        assertEquals(0xAAAAAAA8, folders[3].mMailbox.mFlags);
        // 'a/c'
        assertEquals(3L, folders[4].mMailbox.mParentKey);
        assertEquals(0xAAAAAAA8, folders[4].mMailbox.mFlags);
        // 'a/d'
        assertEquals(3L, folders[5].mMailbox.mParentKey);
        assertEquals(0xAAAAAAAB, folders[5].mMailbox.mFlags);
        // 'a/d/b'
        assertEquals(6L, folders[6].mMailbox.mParentKey);
        assertEquals(0xAAAAAAAB, folders[6].mMailbox.mFlags);
        // 'a/d/b/b'
        assertEquals(7L, folders[7].mMailbox.mParentKey);
        assertEquals(0xAAAAAAA8, folders[7].mMailbox.mFlags);
        // 'g'
        assertEquals(-1L, folders[8].mMailbox.mParentKey);
        assertEquals(0xAAAAAAA8, folders[8].mMailbox.mFlags);
    }
}