Imap client implementation classes and helpers.

Direct interaction
- ImapConnection, ImapFolder, ImapStore, MailTransport
- ImapStore holds the credentials, ImapFolder handles folder-specific
  actions, ImapConnection sets up the connection to the server,
  MailTransport makes the connection and submits the requested actions.

Bug 19236241

Change-Id: Id492353899ec04598735408e45ecf79b17154ea8
diff --git a/src/com/android/phone/common/mail/AuthenticationFailedException.java b/src/com/android/phone/common/mail/AuthenticationFailedException.java
new file mode 100644
index 0000000..f13ab45
--- /dev/null
+++ b/src/com/android/phone/common/mail/AuthenticationFailedException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail;
+
+public class AuthenticationFailedException extends MessagingException {
+    public static final long serialVersionUID = -1;
+
+    public AuthenticationFailedException(String message) {
+        super(MessagingException.AUTHENTICATION_FAILED, message);
+    }
+
+    public AuthenticationFailedException(int exceptionType, String message) {
+        super(exceptionType, message);
+    }
+
+    public AuthenticationFailedException(String message, Throwable throwable) {
+        super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/CertificateValidationException.java b/src/com/android/phone/common/mail/CertificateValidationException.java
new file mode 100644
index 0000000..d8ae9b4
--- /dev/null
+++ b/src/com/android/phone/common/mail/CertificateValidationException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail;
+
+public class CertificateValidationException extends MessagingException {
+    public static final long serialVersionUID = -1;
+
+    public CertificateValidationException(String message) {
+        super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message);
+    }
+
+    public CertificateValidationException(String message, Throwable throwable) {
+        super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/FixedLengthInputStream.java b/src/com/android/phone/common/mail/FixedLengthInputStream.java
new file mode 100644
index 0000000..499feca
--- /dev/null
+++ b/src/com/android/phone/common/mail/FixedLengthInputStream.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that stops allowing reads after the given length has been read. This
+ * is used to allow a client to read directly from an underlying protocol stream without reading
+ * past where the protocol handler intended the client to read.
+ */
+public class FixedLengthInputStream extends InputStream {
+    private final InputStream mIn;
+    private final int mLength;
+    private int mCount;
+
+    public FixedLengthInputStream(InputStream in, int length) {
+        this.mIn = in;
+        this.mLength = length;
+    }
+
+    @Override
+    public int available() throws IOException {
+        return mLength - mCount;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (mCount < mLength) {
+            mCount++;
+            return mIn.read();
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    public int read(byte[] b, int offset, int length) throws IOException {
+        if (mCount < mLength) {
+            int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
+            if (d == -1) {
+                return -1;
+            } else {
+                mCount += d;
+                return d;
+            }
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        return read(b, 0, b.length);
+    }
+
+    public int getLength() {
+        return mLength;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/MailTransport.java b/src/com/android/phone/common/mail/MailTransport.java
new file mode 100644
index 0000000..5e3f7f5
--- /dev/null
+++ b/src/com/android/phone/common/mail/MailTransport.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.phone.common.mail.store.ImapStore;
+
+import java.net.SocketAddress;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+
+/**
+ * Make connection and perform operations on mail server by reading and writing lines.
+ */
+public class MailTransport {
+    private static final String TAG = "MailTransport";
+
+    // TODO protected eventually
+    /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
+    /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
+
+    private static final HostnameVerifier HOSTNAME_VERIFIER =
+            HttpsURLConnection.getDefaultHostnameVerifier();
+
+    private Context mContext;
+    private String mHost;
+    private int mPort;
+    private Socket mSocket;
+    private BufferedInputStream mIn;
+    private BufferedOutputStream mOut;
+    private int mFlags;
+
+    public MailTransport(Context context, String address, int port, int flags) {
+        mContext = context;
+        mHost = address;
+        mPort = port;
+        mFlags = flags;
+    }
+
+    /**
+     * Returns a new transport, using the current transport as a model. The new transport is
+     * configured identically, but not opened or connected in any way.
+     */
+    @Override
+    public MailTransport clone() {
+        return new MailTransport(mContext, mHost, mPort, mFlags);
+    }
+
+    public boolean canTrySslSecurity() {
+        return (mFlags & ImapStore.FLAG_SSL) != 0;
+    }
+
+    public boolean canTrustAllCertificates() {
+        return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
+    }
+
+    /**
+     * Attempts to open a connection using the Uri supplied for connection parameters.  Will attempt
+     * an SSL connection if indicated.
+     */
+    public void open() throws MessagingException, CertificateValidationException {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
+        }
+
+        try {
+            SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
+            if (canTrySslSecurity()) {
+                mSocket = HttpsURLConnection.getDefaultSSLSocketFactory().createSocket();
+            } else {
+                mSocket = new Socket();
+            }
+            mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
+            // After the socket connects to an SSL server, confirm that the hostname is as expected
+            if (canTrySslSecurity() && !canTrustAllCertificates()) {
+                verifyHostname(mSocket, mHost);
+            }
+
+            mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+            mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+            mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+        } catch (SSLException e) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, e.toString());
+            }
+            throw new CertificateValidationException(e.getMessage(), e);
+        } catch (IOException ioe) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, ioe.toString());
+            }
+            throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+        } catch (IllegalArgumentException iae) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, iae.toString());
+            }
+            throw new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION, iae.toString());
+        }
+    }
+
+    /**
+     * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
+     * service but is not in the public API.
+     *
+     * Verify the hostname of the certificate used by the other end of a
+     * connected socket. It is harmless to call this method redundantly if the hostname has already
+     * been verified.
+     *
+     * <p>Wildcard certificates are allowed to verify any matching hostname,
+     * so "foo.bar.example.com" is verified if the peer has a certificate
+     * for "*.example.com".
+     *
+     * @param socket An SSL socket which has been connected to a server
+     * @param hostname The expected hostname of the remote server
+     * @throws IOException if something goes wrong handshaking with the server
+     * @throws SSLPeerUnverifiedException if the server cannot prove its identity
+      */
+    private static void verifyHostname(Socket socket, String hostname) throws IOException {
+        // The code at the start of OpenSSLSocketImpl.startHandshake()
+        // ensures that the call is idempotent, so we can safely call it.
+        SSLSocket ssl = (SSLSocket) socket;
+        ssl.startHandshake();
+
+        SSLSession session = ssl.getSession();
+        if (session == null) {
+            throw new SSLException("Cannot verify SSL socket without session");
+        }
+        // TODO: Instead of reporting the name of the server we think we're connecting to,
+        // we should be reporting the bad name in the certificate.  Unfortunately this is buried
+        // in the verifier code and is not available in the verifier API, and extracting the
+        // CN & alts is beyond the scope of this patch.
+        if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
+            throw new SSLPeerUnverifiedException(
+                    "Certificate hostname not useable for server: " + hostname);
+        }
+    }
+
+    public boolean isOpen() {
+        return (mIn != null && mOut != null &&
+                mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
+    }
+
+    /**
+     * Close the connection.  MUST NOT return any exceptions - must be "best effort" and safe.
+     */
+    public void close() {
+        try {
+            mIn.close();
+        } catch (Exception e) {
+            // May fail if the connection is already closed.
+        }
+        try {
+            mOut.close();
+        } catch (Exception e) {
+            // May fail if the connection is already closed.
+        }
+        try {
+            mSocket.close();
+        } catch (Exception e) {
+            // May fail if the connection is already closed.
+        }
+        mIn = null;
+        mOut = null;
+        mSocket = null;
+    }
+
+    public InputStream getInputStream() {
+        return mIn;
+    }
+
+    public OutputStream getOutputStream() {
+        return mOut;
+    }
+
+    /**
+     * Writes a single line to the server using \r\n termination.
+     */
+    public void writeLine(String s, String sensitiveReplacement) throws IOException {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            if (sensitiveReplacement != null) {
+                Log.d(TAG, ">>> " + sensitiveReplacement);
+            } else {
+                Log.d(TAG, ">>> " + s);
+            }
+        }
+
+        OutputStream out = getOutputStream();
+        out.write(s.getBytes());
+        out.write('\r');
+        out.write('\n');
+        out.flush();
+    }
+
+    /**
+     * Reads a single line from the server, using either \r\n or \n as the delimiter.  The
+     * delimiter char(s) are not included in the result.
+     */
+    public String readLine(boolean loggable) throws IOException {
+        StringBuffer sb = new StringBuffer();
+        InputStream in = getInputStream();
+        int d;
+        while ((d = in.read()) != -1) {
+            if (((char)d) == '\r') {
+                continue;
+            } else if (((char)d) == '\n') {
+                break;
+            } else {
+                sb.append((char)d);
+            }
+        }
+        if (d == -1 && Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "End of stream reached while trying to read line.");
+        }
+        String ret = sb.toString();
+        if (loggable && Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "<<< " + ret);
+        }
+        return ret;
+    }
+}
diff --git a/src/com/android/phone/common/mail/Message.java b/src/com/android/phone/common/mail/Message.java
new file mode 100644
index 0000000..cd5a44d
--- /dev/null
+++ b/src/com/android/phone/common/mail/Message.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail;
+
+/**
+ * Object to represent an email message.
+ */
+public class Message {
+    public static final String FLAG_SEEN = "seen";
+    public static final String FLAG_DELETED = "deleted";
+}
diff --git a/src/com/android/phone/common/mail/MessagingException.java b/src/com/android/phone/common/mail/MessagingException.java
new file mode 100644
index 0000000..e4c674a
--- /dev/null
+++ b/src/com/android/phone/common/mail/MessagingException.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail;
+
+/**
+ * This exception is used for most types of failures that occur during server interactions.
+ *
+ * Data passed through this exception should be considered non-localized.  Any strings should
+ * either be internal-only (for debugging) or server-generated.
+ *
+ * TO DO: Does it make sense to further collapse AuthenticationFailedException and
+ * CertificateValidationException and any others into this?
+ */
+public class MessagingException extends Exception {
+    public static final long serialVersionUID = -1;
+
+    public static final int NO_ERROR = -1;
+    /** Any exception that does not specify a specific issue */
+    public static final int UNSPECIFIED_EXCEPTION = 0;
+    /** Connection or IO errors */
+    public static final int IOERROR = 1;
+    /** The configuration requested TLS but the server did not support it. */
+    public static final int TLS_REQUIRED = 2;
+    /** Authentication is required but the server did not support it. */
+    public static final int AUTH_REQUIRED = 3;
+    /** General security failures */
+    public static final int GENERAL_SECURITY = 4;
+    /** Authentication failed */
+    public static final int AUTHENTICATION_FAILED = 5;
+    /** Attempt to create duplicate account */
+    public static final int DUPLICATE_ACCOUNT = 6;
+    /** Required security policies reported - advisory only */
+    public static final int SECURITY_POLICIES_REQUIRED = 7;
+   /** Required security policies not supported */
+    public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
+   /** The protocol (or protocol version) isn't supported */
+    public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
+    /** The server's SSL certificate couldn't be validated */
+    public static final int CERTIFICATE_VALIDATION_ERROR = 10;
+    /** Authentication failed during autodiscover */
+    public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
+    /** Autodiscover completed with a result (non-error) */
+    public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
+    /** Ambiguous failure; server error or bad credentials */
+    public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
+    /** The server refused access */
+    public static final int ACCESS_DENIED = 14;
+    /** The server refused access */
+    public static final int ATTACHMENT_NOT_FOUND = 15;
+    /** A client SSL certificate is required for connections to the server */
+    public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
+    /** The client SSL certificate specified is invalid */
+    public static final int CLIENT_CERTIFICATE_ERROR = 17;
+    /** The server indicates it does not support OAuth authentication */
+    public static final int OAUTH_NOT_SUPPORTED = 18;
+    /** The server indicates it experienced an internal error */
+    public static final int SERVER_ERROR = 19;
+
+    protected int mExceptionType;
+    // Exception type-specific data
+    protected Object mExceptionData;
+
+    public MessagingException(String message, Throwable throwable) {
+        this(UNSPECIFIED_EXCEPTION, message, throwable);
+    }
+
+    public MessagingException(int exceptionType, String message, Throwable throwable) {
+        super(message, throwable);
+        mExceptionType = exceptionType;
+        mExceptionData = null;
+    }
+
+    /**
+     * Constructs a MessagingException with an exceptionType and a null message.
+     * @param exceptionType The exception type to set for this exception.
+     */
+    public MessagingException(int exceptionType) {
+        this(exceptionType, null, null);
+    }
+
+    /**
+     * Constructs a MessagingException with a message.
+     * @param message the message for this exception
+     */
+    public MessagingException(String message) {
+        this(UNSPECIFIED_EXCEPTION, message, null);
+    }
+
+    /**
+     * Constructs a MessagingException with an exceptionType and a message.
+     * @param exceptionType The exception type to set for this exception.
+     */
+    public MessagingException(int exceptionType, String message) {
+        this(exceptionType, message, null);
+    }
+
+    /**
+     * Constructs a MessagingException with an exceptionType, a message, and data
+     * @param exceptionType The exception type to set for this exception.
+     * @param message the message for the exception (or null)
+     * @param data exception-type specific data for the exception (or null)
+     */
+    public MessagingException(int exceptionType, String message, Object data) {
+        super(message);
+        mExceptionType = exceptionType;
+        mExceptionData = data;
+    }
+
+    /**
+     * Return the exception type.  Will be OTHER_EXCEPTION if not explicitly set.
+     *
+     * @return Returns the exception type.
+     */
+    public int getExceptionType() {
+        return mExceptionType;
+    }
+    /**
+     * Return the exception data.  Will be null if not explicitly set.
+     *
+     * @return Returns the exception data.
+     */
+    public Object getExceptionData() {
+        return mExceptionData;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/PeekableInputStream.java b/src/com/android/phone/common/mail/PeekableInputStream.java
new file mode 100644
index 0000000..d29fc6b
--- /dev/null
+++ b/src/com/android/phone/common/mail/PeekableInputStream.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that allows single byte "peeks" without consuming the byte. The
+ * client of this stream can call peek() to see the next available byte in the stream
+ * and a subsequent read will still return the peeked byte.
+ */
+public class PeekableInputStream extends InputStream {
+    private final InputStream mIn;
+    private boolean mPeeked;
+    private int mPeekedByte;
+
+    public PeekableInputStream(InputStream in) {
+        this.mIn = in;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (!mPeeked) {
+            return mIn.read();
+        } else {
+            mPeeked = false;
+            return mPeekedByte;
+        }
+    }
+
+    public int peek() throws IOException {
+        if (!mPeeked) {
+            mPeekedByte = read();
+            mPeeked = true;
+        }
+        return mPeekedByte;
+    }
+
+    @Override
+    public int read(byte[] b, int offset, int length) throws IOException {
+        if (!mPeeked) {
+            return mIn.read(b, offset, length);
+        } else {
+            b[0] = (byte)mPeekedByte;
+            mPeeked = false;
+            int r = mIn.read(b, offset + 1, length - 1);
+            if (r == -1) {
+                return 1;
+            } else {
+                return r + 1;
+            }
+        }
+    }
+
+    @Override
+    public int read(byte[] b) throws IOException {
+        return read(b, 0, b.length);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
+                mIn.toString(), mPeeked, mPeekedByte);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/ImapConnection.java b/src/com/android/phone/common/mail/store/ImapConnection.java
new file mode 100644
index 0000000..3cae5f8
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/ImapConnection.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.phone.common.mail.AuthenticationFailedException;
+import com.android.phone.common.mail.CertificateValidationException;
+import com.android.phone.common.mail.MailTransport;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.store.imap.ImapConstants;
+import com.android.phone.common.mail.store.imap.ImapResponse;
+import com.android.phone.common.mail.store.imap.ImapResponseParser;
+import com.android.phone.common.mail.store.imap.ImapUtility;
+import com.android.phone.common.mail.store.ImapStore.ImapException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.ssl.SSLException;
+
+/**
+ * A cacheable class that stores the details for a single IMAP connection.
+ */
+public class ImapConnection {
+    private final String TAG = "ImapConnection";
+
+    private String mLoginPhrase;
+    private ImapStore mImapStore;
+    private MailTransport mTransport;
+    private ImapResponseParser mParser;
+
+    static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
+
+    /**
+     * Next tag to use.  All connections associated to the same ImapStore instance share the same
+     * counter to make tests simpler.
+     * (Some of the tests involve multiple connections but only have a single counter to track the
+     * tag.)
+     */
+    private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
+
+    ImapConnection(ImapStore store) {
+        setStore(store);
+    }
+
+    void setStore(ImapStore store) {
+        // TODO: maybe we should throw an exception if the connection is not closed here,
+        // if it's not currently closed, then we won't reopen it, so if the credentials have
+        // changed, the connection will not be reestablished.
+        mImapStore = store;
+        mLoginPhrase = null;
+    }
+
+    /**
+     * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
+     * username and password.
+     *
+     * @return the login command string to sent to the IMAP server
+     */
+    String getLoginPhrase() {
+        if (mLoginPhrase == null) {
+            if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
+                // build the LOGIN string once (instead of over-and-over again.)
+                // apply the quoting here around the built-up password
+                mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " "
+                        + ImapUtility.imapQuoted(mImapStore.getPassword());
+            }
+        }
+        return mLoginPhrase;
+    }
+
+    void open() throws IOException, MessagingException {
+        if (mTransport != null && mTransport.isOpen()) {
+            return;
+        }
+
+        try {
+            // copy configuration into a clean transport, if necessary
+            if (mTransport == null) {
+                mTransport = mImapStore.cloneTransport();
+            }
+
+            mTransport.open();
+
+            createParser();
+
+            // LOGIN
+            doLogin();
+        } catch (SSLException e) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "SSLException ", e);
+            }
+            throw new CertificateValidationException(e.getMessage(), e);
+        } catch (IOException ioe) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "IOException", ioe);
+            }
+            throw ioe;
+        } finally {
+            destroyResponses();
+        }
+    }
+
+    /**
+     * Closes the connection and releases all resources. This connection can not be used again
+     * until {@link #setStore(ImapStore)} is called.
+     */
+    void close() {
+        if (mTransport != null) {
+            mTransport.close();
+            mTransport = null;
+        }
+        destroyResponses();
+        mParser = null;
+        mImapStore = null;
+    }
+
+    /**
+     * Logs into the IMAP server
+     */
+    private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
+        try {
+            executeSimpleCommand(getLoginPhrase(), true);
+        } catch (ImapException ie) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "ImapException", ie);
+            }
+            final String status = ie.getStatus();
+            final String code = ie.getResponseCode();
+            final String alertText = ie.getAlertText();
+
+            // if the response code indicates expired or bad credentials, throw a special exception
+            if (ImapConstants.AUTHENTICATIONFAILED.equals(code) ||
+                    ImapConstants.EXPIRED.equals(code) ||
+                    (ImapConstants.NO.equals(status) && TextUtils.isEmpty(code))) {
+                throw new AuthenticationFailedException(alertText, ie);
+            }
+
+            throw new MessagingException(alertText, ie);
+        }
+    }
+
+    /**
+     * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
+     * set it to {@link #mParser}.
+     *
+     * If we already have an {@link ImapResponseParser}, we
+     * {@link #destroyResponses()} and throw it away.
+     */
+    private void createParser() {
+        destroyResponses();
+        mParser = new ImapResponseParser(mTransport.getInputStream());
+    }
+
+
+    void destroyResponses() {
+        if (mParser != null) {
+            mParser.destroyResponses();
+        }
+    }
+
+    List<ImapResponse> executeSimpleCommand(String command)
+            throws IOException, MessagingException{
+        return executeSimpleCommand(command, false);
+    }
+
+    /**
+     * Send a single command to the server.  The command will be preceded by an IMAP command
+     * tag and followed by \r\n (caller need not supply them).
+     * Execute a simple command at the server, a simple command being one that is sent in a single
+     * line of text
+     *
+     * @param command the command to send to the server
+     * @param sensitive whether the command should be redacted in logs (used for login)
+     * @return a list of ImapResponses
+     * @throws IOException
+     * @throws MessagingException
+     */
+    List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
+            throws IOException, MessagingException {
+        // TODO: It may be nice to catch IOExceptions and close the connection here.
+        // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
+        sendCommand(command, sensitive);
+        return getCommandResponses();
+    }
+
+    String sendCommand(String command, boolean sensitive) throws IOException, MessagingException {
+        open();
+
+        if (mTransport == null) {
+            throw new IOException("Null transport");
+        }
+        String tag = Integer.toString(mNextCommandTag.incrementAndGet());
+        String commandToSend = tag + " " + command;
+        mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
+
+        return tag;
+    }
+
+    /**
+     * Read and return all of the responses from the most recent command sent to the server
+     *
+     * @return a list of ImapResponses
+     * @throws IOException
+     * @throws MessagingException
+     */
+    List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
+        final List<ImapResponse> responses = new ArrayList<ImapResponse>();
+        ImapResponse response;
+        do {
+            response = mParser.readResponse();
+            responses.add(response);
+        } while (!response.isTagged());
+
+        if (!response.isOk()) {
+            final String toString = response.toString();
+            final String status = response.getStatusOrEmpty().getString();
+            final String alert = response.getAlertTextOrEmpty().getString();
+            final String responseCode = response.getResponseCodeOrEmpty().getString();
+            destroyResponses();
+
+            // if the response code indicates an error occurred within the server, indicate that
+            if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
+                throw new MessagingException(MessagingException.SERVER_ERROR, alert);
+            }
+
+            throw new ImapException(toString, status, alert, responseCode);
+        }
+        return responses;
+    }
+}
diff --git a/src/com/android/phone/common/mail/store/ImapFolder.java b/src/com/android/phone/common/mail/store/ImapFolder.java
new file mode 100644
index 0000000..9f7db4a
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/ImapFolder.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store;
+
+import android.util.Log;
+
+import com.android.phone.common.mail.AuthenticationFailedException;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.store.imap.ImapConstants;
+import com.android.phone.common.mail.store.imap.ImapResponse;
+import com.android.phone.common.mail.store.imap.ImapString;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Represents one folder on the IMAP server.
+ */
+public class ImapFolder {
+    private final String TAG = "ImapFolder";
+    private ImapStore mStore;
+    private String mName;
+    private String mMode;
+    private boolean mExists;
+    private ImapConnection mConnection;
+    private int mMessageCount;
+
+    public static final String MODE_READ_ONLY = "mode_read_only";
+    public static final String MODE_READ_WRITE = "mode_read_write";
+
+    public ImapFolder(ImapStore store, String name) {
+        mStore = store;
+        mName = name;
+    }
+
+    private void destroyResponses() {
+        if (mConnection != null) {
+            mConnection.destroyResponses();
+        }
+    }
+
+    public void open(String mode)
+            throws MessagingException {
+        try {
+            if (isOpen()) {
+                if (mMode == mode) {
+                    // Make sure the connection is valid.
+                    // If it's not we'll close it down and continue on to get a new one.
+                    try {
+                        mConnection.executeSimpleCommand(ImapConstants.NOOP);
+                        return;
+
+                    } catch (IOException ioe) {
+                        ioExceptionHandler(mConnection, ioe);
+                    } finally {
+                        destroyResponses();
+                    }
+                } else {
+                    // Return the connection to the pool, if exists.
+                    close(false);
+                }
+            }
+            synchronized (this) {
+                mConnection = mStore.getConnection();
+            }
+            // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
+            // $MDNSent)
+            // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
+            // NonJunk $MDNSent \*)] Flags permitted.
+            // * 23 EXISTS
+            // * 0 RECENT
+            // * OK [UIDVALIDITY 1125022061] UIDs valid
+            // * OK [UIDNEXT 57576] Predicted next UID
+            // 2 OK [READ-WRITE] Select completed.
+            try {
+                doSelect();
+            } catch (IOException ioe) {
+                throw ioExceptionHandler(mConnection, ioe);
+            } finally {
+                destroyResponses();
+            }
+        } catch (AuthenticationFailedException e) {
+            // Don't cache this connection, so we're forced to try connecting/login again
+            mConnection = null;
+            close(false);
+            throw e;
+        } catch (MessagingException e) {
+            mExists = false;
+            close(false);
+            throw e;
+        }
+    }
+
+    public boolean isOpen() {
+        return mExists && mConnection != null;
+    }
+
+    /**
+     * Selects the folder for use. Before performing any operations on this folder, it
+     * must be selected.
+     */
+    private void doSelect() throws IOException, MessagingException {
+        final List<ImapResponse> responses = mConnection.executeSimpleCommand(
+                String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName));
+
+        // Assume the folder is opened read-write; unless we are notified otherwise
+        mMode = MODE_READ_WRITE;
+        int messageCount = -1;
+        for (ImapResponse response : responses) {
+            if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+                messageCount = response.getStringOrEmpty(0).getNumberOrZero();
+            } else if (response.isOk()) {
+                final ImapString responseCode = response.getResponseCodeOrEmpty();
+                if (responseCode.is(ImapConstants.READ_ONLY)) {
+                    mMode = MODE_READ_ONLY;
+                } else if (responseCode.is(ImapConstants.READ_WRITE)) {
+                    mMode = MODE_READ_WRITE;
+                }
+            } else if (response.isTagged()) { // Not OK
+                throw new MessagingException("Can't open mailbox: "
+                        + response.getStatusResponseTextOrEmpty());
+            }
+        }
+        if (messageCount == -1) {
+            throw new MessagingException("Did not find message count during select");
+        }
+        mMessageCount = messageCount;
+        mExists = true;
+    }
+
+    public void close(boolean expunge) {
+        // TODO implement expunge
+        mMessageCount = -1;
+        synchronized (this) {
+            mStore.closeConnection();
+            mConnection = null;
+        }
+    }
+
+    private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, "IO Exception detected: ", ioe);
+        }
+        connection.close();
+        if (connection == mConnection) {
+            mConnection = null; // To prevent close() from returning the connection to the pool.
+            close(false);
+        }
+        return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
+    }
+}
diff --git a/src/com/android/phone/common/mail/store/ImapStore.java b/src/com/android/phone/common/mail/store/ImapStore.java
new file mode 100644
index 0000000..38bb62f
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/ImapStore.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store;
+
+import android.content.Context;
+
+import com.android.phone.common.mail.MailTransport;
+import com.android.phone.common.mail.MessagingException;
+import com.android.phone.common.mail.store.ImapFolder;
+
+public class ImapStore {
+    /**
+     * A global suggestion to Store implementors on how much of the body
+     * should be returned on FetchProfile.Item.BODY_SANE requests. We'll use 125k now.
+     */
+    public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024);
+    private Context mContext;
+    private String mUsername;
+    private String mPassword;
+    private MailTransport mTransport;
+    private ImapConnection mConnection;
+
+    public static final int FLAG_NONE         = 0x00;    // No flags
+    public static final int FLAG_SSL          = 0x01;    // Use SSL
+    public static final int FLAG_TLS          = 0x02;    // Use TLS
+    public static final int FLAG_AUTHENTICATE = 0x04;    // Use name/password for authentication
+    public static final int FLAG_TRUST_ALL    = 0x08;    // Trust all certificates
+    public static final int FLAG_OAUTH        = 0x10;    // Use OAuth for authentication
+
+    /**
+     * Contains all the information necessary to log into an imap server
+     */
+    public ImapStore(Context context, String username, String password, int port,
+            String serverName, int flags) {
+        mContext = context;
+        mUsername = username;
+        mPassword = password;
+        mTransport = new MailTransport(context, serverName, port, flags);
+    }
+
+    public ImapFolder getFolder(String name) {
+        return new ImapFolder(this, name);
+    }
+
+    public String getUsername() {
+        return mUsername;
+    }
+
+    public String getPassword() {
+        return mPassword;
+    }
+
+    /** Returns a clone of the transport associated with this store. */
+    MailTransport cloneTransport() {
+        return mTransport.clone();
+    }
+
+    static class ImapException extends MessagingException {
+        private static final long serialVersionUID = 1L;
+
+        private final String mStatus;
+        private final String mAlertText;
+        private final String mResponseCode;
+
+        public ImapException(String message, String status, String alertText,
+                String responseCode) {
+            super(message);
+            mStatus = status;
+            mAlertText = alertText;
+            mResponseCode = responseCode;
+        }
+
+        public String getStatus() {
+            return mStatus;
+        }
+
+        public String getAlertText() {
+            return mAlertText;
+        }
+
+        public String getResponseCode() {
+            return mResponseCode;
+        }
+    }
+
+    public void closeConnection() {
+        if (mConnection != null) {
+            mConnection.close();
+            mConnection = null;
+        }
+    }
+
+    public ImapConnection getConnection() {
+        if (mConnection == null) {
+            mConnection = new ImapConnection(this);
+        }
+        return mConnection;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapConstants.java b/src/com/android/phone/common/mail/store/imap/ImapConstants.java
new file mode 100644
index 0000000..0f1f526
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapConstants.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store.imap;
+
+import com.android.phone.common.mail.store.ImapStore;
+
+public final class ImapConstants {
+    private ImapConstants() {}
+
+    public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
+    public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
+    public static final String FETCH_FIELD_BODY_PEEK_SANE
+            = String.format("BODY.PEEK[]<0.%d>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE);
+    public static final String FETCH_FIELD_HEADERS =
+            "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
+
+    public static final String ALERT = "ALERT";
+    public static final String APPEND = "APPEND";
+    public static final String BAD = "BAD";
+    public static final String BADCHARSET = "BADCHARSET";
+    public static final String BODY = "BODY";
+    public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
+    public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
+    public static final String BYE = "BYE";
+    public static final String CAPABILITY = "CAPABILITY";
+    public static final String CHECK = "CHECK";
+    public static final String CLOSE = "CLOSE";
+    public static final String COPY = "COPY";
+    public static final String CREATE = "CREATE";
+    public static final String DELETE = "DELETE";
+    public static final String EXAMINE = "EXAMINE";
+    public static final String EXISTS = "EXISTS";
+    public static final String EXPUNGE = "EXPUNGE";
+    public static final String FETCH = "FETCH";
+    public static final String FLAG_ANSWERED = "\\ANSWERED";
+    public static final String FLAG_DELETED = "\\DELETED";
+    public static final String FLAG_FLAGGED = "\\FLAGGED";
+    public static final String FLAG_NO_SELECT = "\\NOSELECT";
+    public static final String FLAG_SEEN = "\\SEEN";
+    public static final String FLAGS = "FLAGS";
+    public static final String FLAGS_SILENT = "FLAGS.SILENT";
+    public static final String ID = "ID";
+    public static final String INBOX = "INBOX";
+    public static final String INTERNALDATE = "INTERNALDATE";
+    public static final String LIST = "LIST";
+    public static final String LOGIN = "LOGIN";
+    public static final String LOGOUT = "LOGOUT";
+    public static final String LSUB = "LSUB";
+    public static final String NO = "NO";
+    public static final String NOOP = "NOOP";
+    public static final String OK = "OK";
+    public static final String PARSE = "PARSE";
+    public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
+    public static final String PREAUTH = "PREAUTH";
+    public static final String READ_ONLY = "READ-ONLY";
+    public static final String READ_WRITE = "READ-WRITE";
+    public static final String RENAME = "RENAME";
+    public static final String RFC822_SIZE = "RFC822.SIZE";
+    public static final String SEARCH = "SEARCH";
+    public static final String SELECT = "SELECT";
+    public static final String STARTTLS = "STARTTLS";
+    public static final String STATUS = "STATUS";
+    public static final String STORE = "STORE";
+    public static final String SUBSCRIBE = "SUBSCRIBE";
+    public static final String TEXT = "TEXT";
+    public static final String TRYCREATE = "TRYCREATE";
+    public static final String UID = "UID";
+    public static final String UID_COPY = "UID COPY";
+    public static final String UID_FETCH = "UID FETCH";
+    public static final String UID_SEARCH = "UID SEARCH";
+    public static final String UID_STORE = "UID STORE";
+    public static final String UIDNEXT = "UIDNEXT";
+    public static final String UIDVALIDITY = "UIDVALIDITY";
+    public static final String UNSEEN = "UNSEEN";
+    public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
+    public static final String APPENDUID = "APPENDUID";
+    public static final String NIL = "NIL";
+
+    /** response codes within IMAP responses */
+    public static final String EXPIRED = "EXPIRED";
+    public static final String AUTHENTICATIONFAILED = "AUTHENTICATIONFAILED";
+    public static final String UNAVAILABLE = "UNAVAILABLE";
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapElement.java b/src/com/android/phone/common/mail/store/imap/ImapElement.java
new file mode 100644
index 0000000..2d1824e
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapElement.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store.imap;
+
+/**
+ * Class representing "element"s in IMAP responses.
+ *
+ * <p>Class hierarchy:
+ * <pre>
+ * ImapElement
+ *   |
+ *   |-- ImapElement.NONE (for 'index out of range')
+ *   |
+ *   |-- ImapList (isList() == true)
+ *   |   |
+ *   |   |-- ImapList.EMPTY
+ *   |   |
+ *   |   --- ImapResponse
+ *   |
+ *   --- ImapString (isString() == true)
+ *       |
+ *       |-- ImapString.EMPTY
+ *       |
+ *       |-- ImapSimpleString
+ *       |
+ *       |-- ImapMemoryLiteral
+ *       |
+ *       --- ImapTempFileLiteral
+ * </pre>
+ */
+public abstract class ImapElement {
+    /**
+     * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index
+     * is out of range.
+     */
+    public static final ImapElement NONE = new ImapElement() {
+        @Override public void destroy() {
+            // Don't call super.destroy().
+            // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override public boolean isList() {
+            return false;
+        }
+
+        @Override public boolean isString() {
+            return false;
+        }
+
+        @Override public String toString() {
+            return "[NO ELEMENT]";
+        }
+
+        @Override
+        public boolean equalsForTest(ImapElement that) {
+            return super.equalsForTest(that);
+        }
+    };
+
+    private boolean mDestroyed = false;
+
+    public abstract boolean isList();
+
+    public abstract boolean isString();
+
+    protected boolean isDestroyed() {
+        return mDestroyed;
+    }
+
+    /**
+     * Clean up the resources used by the instance.
+     * It's for removing a temp file used by {@link ImapTempFileLiteral}.
+     */
+    public void destroy() {
+        mDestroyed = true;
+    }
+
+    /**
+     * Throws {@link RuntimeException} if it's already destroyed.
+     */
+    protected final void checkNotDestroyed() {
+        if (mDestroyed) {
+            throw new RuntimeException("Already destroyed");
+        }
+    }
+
+    /**
+     * Return a string that represents this object; it's purely for the debug purpose.  Don't
+     * mistake it for {@link ImapString#getString}.
+     *
+     * Abstract to force subclasses to implement it.
+     */
+    @Override
+    public abstract String toString();
+
+    /**
+     * The equals implementation that is intended to be used only for unit testing.
+     * (Because it may be heavy and has a special sense of "equal" for testing.)
+     */
+    public boolean equalsForTest(ImapElement that) {
+        if (that == null) {
+            return false;
+        }
+        return this.getClass() == that.getClass(); // Has to be the same class.
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapList.java b/src/com/android/phone/common/mail/store/imap/ImapList.java
new file mode 100644
index 0000000..93adc4f
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapList.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store.imap;
+
+import java.util.ArrayList;
+
+/**
+ * Class represents an IMAP list.
+ */
+public class ImapList extends ImapElement {
+    /**
+     * {@link ImapList} representing an empty list.
+     */
+    public static final ImapList EMPTY = new ImapList() {
+        @Override public void destroy() {
+            // Don't call super.destroy().
+            // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override void add(ImapElement e) {
+            throw new RuntimeException();
+        }
+    };
+
+    private ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
+
+    /* package */ void add(ImapElement e) {
+        if (e == null) {
+            throw new RuntimeException("Can't add null");
+        }
+        mList.add(e);
+    }
+
+    @Override
+    public final boolean isString() {
+        return false;
+    }
+
+    @Override
+    public final boolean isList() {
+        return true;
+    }
+
+    public final int size() {
+        return mList.size();
+    }
+
+    public final boolean isEmpty() {
+        return size() == 0;
+    }
+
+    /**
+     * Return true if the element at {@code index} exists, is string, and equals to {@code s}.
+     * (case insensitive)
+     */
+    public final boolean is(int index, String s) {
+        return is(index, s, false);
+    }
+
+    /**
+     * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}.
+     */
+    public final boolean is(int index, String s, boolean prefixMatch) {
+        if (!prefixMatch) {
+            return getStringOrEmpty(index).is(s);
+        } else {
+            return getStringOrEmpty(index).startsWith(s);
+        }
+    }
+
+    /**
+     * Return the element at {@code index}.
+     * If {@code index} is out of range, returns {@link ImapElement#NONE}.
+     */
+    public final ImapElement getElementOrNone(int index) {
+        return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
+    }
+
+    /**
+     * Return the element at {@code index} if it's a list.
+     * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}.
+     */
+    public final ImapList getListOrEmpty(int index) {
+        ImapElement el = getElementOrNone(index);
+        return el.isList() ? (ImapList) el : EMPTY;
+    }
+
+    /**
+     * Return the element at {@code index} if it's a string.
+     * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}.
+     */
+    public final ImapString getStringOrEmpty(int index) {
+        ImapElement el = getElementOrNone(index);
+        return el.isString() ? (ImapString) el : ImapString.EMPTY;
+    }
+
+    /**
+     * Return an element keyed by {@code key}.  Return null if not found.  {@code key} has to be
+     * at an even index.
+     */
+    /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
+        for (int i = 1; i < size(); i += 2) {
+            if (is(i-1, key, prefixMatch)) {
+                return mList.get(i);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return an {@link ImapList} keyed by {@code key}.
+     * Return {@link ImapList#EMPTY} if not found.
+     */
+    public final ImapList getKeyedListOrEmpty(String key) {
+        return getKeyedListOrEmpty(key, false);
+    }
+
+    /**
+     * Return an {@link ImapList} keyed by {@code key}.
+     * Return {@link ImapList#EMPTY} if not found.
+     */
+    public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
+        ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+        return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
+    }
+
+    /**
+     * Return an {@link ImapString} keyed by {@code key}.
+     * Return {@link ImapString#EMPTY} if not found.
+     */
+    public final ImapString getKeyedStringOrEmpty(String key) {
+        return getKeyedStringOrEmpty(key, false);
+    }
+
+    /**
+     * Return an {@link ImapString} keyed by {@code key}.
+     * Return {@link ImapString#EMPTY} if not found.
+     */
+    public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
+        ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+        return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
+    }
+
+    /**
+     * Return true if it contains {@code s}.
+     */
+    public final boolean contains(String s) {
+        for (int i = 0; i < size(); i++) {
+            if (getStringOrEmpty(i).is(s)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void destroy() {
+        if (mList != null) {
+            for (ImapElement e : mList) {
+                e.destroy();
+            }
+            mList = null;
+        }
+        super.destroy();
+    }
+
+    @Override
+    public String toString() {
+        return mList.toString();
+    }
+
+    /**
+     * Return the text representations of the contents concatenated with ",".
+     */
+    public final String flatten() {
+        return flatten(new StringBuilder()).toString();
+    }
+
+    /**
+     * Returns text representations (i.e. getString()) of contents joined together with
+     * "," as the separator.
+     *
+     * Only used for building the capability string passed to vendor policies.
+     *
+     * We can't use toString(), because it's for debugging (meaning the format may change any time),
+     * and it won't expand literals.
+     */
+    private final StringBuilder flatten(StringBuilder sb) {
+        sb.append('[');
+        for (int i = 0; i < mList.size(); i++) {
+            if (i > 0) {
+                sb.append(',');
+            }
+            final ImapElement e = getElementOrNone(i);
+            if (e.isList()) {
+                getListOrEmpty(i).flatten(sb);
+            } else if (e.isString()) {
+                sb.append(getStringOrEmpty(i).getString());
+            }
+        }
+        sb.append(']');
+        return sb;
+    }
+
+    @Override
+    public boolean equalsForTest(ImapElement that) {
+        if (!super.equalsForTest(that)) {
+            return false;
+        }
+        ImapList thatList = (ImapList) that;
+        if (size() != thatList.size()) {
+            return false;
+        }
+        for (int i = 0; i < size(); i++) {
+            if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapMemoryLiteral.java b/src/com/android/phone/common/mail/store/imap/ImapMemoryLiteral.java
new file mode 100644
index 0000000..aac66c2
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapMemoryLiteral.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2010 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.android.phone.common.mail.store.imap;
+
+import android.util.Log;
+
+import com.android.phone.common.mail.FixedLengthInputStream;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Subclass of {@link ImapString} used for literals backed by an in-memory byte array.
+ */
+public class ImapMemoryLiteral extends ImapString {
+    private final String TAG = "ImapMemoryLiteral";
+    private byte[] mData;
+
+    /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
+        // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
+        // copy....
+        mData = new byte[in.getLength()];
+        int pos = 0;
+        while (pos < mData.length) {
+            int read = in.read(mData, pos, mData.length - pos);
+            if (read < 0) {
+                break;
+            }
+            pos += read;
+        }
+        if (pos != mData.length) {
+            Log.w(TAG, "");
+        }
+    }
+
+    @Override
+    public void destroy() {
+        mData = null;
+        super.destroy();
+    }
+
+    @Override
+    public String getString() {
+        try {
+            return new String(mData, "US-ASCII");
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "Unsupported encoding: ", e);
+        }
+        return null;
+    }
+
+    @Override
+    public InputStream getAsStream() {
+        return new ByteArrayInputStream(mData);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{%d byte literal(memory)}", mData.length);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapResponse.java b/src/com/android/phone/common/mail/store/imap/ImapResponse.java
new file mode 100644
index 0000000..4891966
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapResponse.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store.imap;
+
+/**
+ * Class represents an IMAP response.
+ */
+public class ImapResponse extends ImapList {
+    private final String mTag;
+    private final boolean mIsContinuationRequest;
+
+    /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
+        mTag = tag;
+        mIsContinuationRequest = isContinuationRequest;
+    }
+
+    /* package */ static boolean isStatusResponse(String symbol) {
+        return     ImapConstants.OK.equalsIgnoreCase(symbol)
+                || ImapConstants.NO.equalsIgnoreCase(symbol)
+                || ImapConstants.BAD.equalsIgnoreCase(symbol)
+                || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
+                || ImapConstants.BYE.equalsIgnoreCase(symbol);
+    }
+
+    /**
+     * @return whether it's a tagged response.
+     */
+    public boolean isTagged() {
+        return mTag != null;
+    }
+
+    /**
+     * @return whether it's a continuation request.
+     */
+    public boolean isContinuationRequest() {
+        return mIsContinuationRequest;
+    }
+
+    public boolean isStatusResponse() {
+        return isStatusResponse(getStringOrEmpty(0).getString());
+    }
+
+    /**
+     * @return whether it's an OK response.
+     */
+    public boolean isOk() {
+        return is(0, ImapConstants.OK);
+    }
+
+    /**
+     * @return whether it's an BAD response.
+     */
+    public boolean isBad() {
+        return is(0, ImapConstants.BAD);
+    }
+
+    /**
+     * @return whether it's an NO response.
+     */
+    public boolean isNo() {
+        return is(0, ImapConstants.NO);
+    }
+
+    /**
+     * @return whether it's an {@code responseType} data response.  (i.e. not tagged).
+     * @param index where {@code responseType} should appear.  e.g. 1 for "FETCH"
+     * @param responseType e.g. "FETCH"
+     */
+    public final boolean isDataResponse(int index, String responseType) {
+        return !isTagged() && getStringOrEmpty(index).is(responseType);
+    }
+
+    /**
+     * @return Response code (RFC 3501 7.1) if it's a status response.
+     *
+     * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
+     */
+    public ImapString getResponseCodeOrEmpty() {
+        if (!isStatusResponse()) {
+            return ImapString.EMPTY; // Not a status response.
+        }
+        return getListOrEmpty(1).getStringOrEmpty(0);
+    }
+
+    /**
+     * @return Alert message it it has ALERT response code.
+     *
+     * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
+     */
+    public ImapString getAlertTextOrEmpty() {
+        if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
+            return ImapString.EMPTY; // Not an ALERT
+        }
+        // The 3rd element contains all the rest of line.
+        return getStringOrEmpty(2);
+    }
+
+    /**
+     * @return Response text in a status response.
+     */
+    public ImapString getStatusResponseTextOrEmpty() {
+        if (!isStatusResponse()) {
+            return ImapString.EMPTY;
+        }
+        return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
+    }
+
+    public ImapString getStatusOrEmpty() {
+        if (!isStatusResponse()) {
+            return ImapString.EMPTY;
+        }
+        return getStringOrEmpty(0);
+    }
+
+    @Override
+    public String toString() {
+        String tag = mTag;
+        if (isContinuationRequest()) {
+            tag = "+";
+        }
+        return "#" + tag + "# " + super.toString();
+    }
+
+    @Override
+    public boolean equalsForTest(ImapElement that) {
+        if (!super.equalsForTest(that)) {
+            return false;
+        }
+        final ImapResponse thatResponse = (ImapResponse) that;
+        if (mTag == null) {
+            if (thatResponse.mTag != null) {
+                return false;
+            }
+        } else {
+            if (!mTag.equals(thatResponse.mTag)) {
+                return false;
+            }
+        }
+        if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
+            return false;
+        }
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapResponseParser.java b/src/com/android/phone/common/mail/store/imap/ImapResponseParser.java
new file mode 100644
index 0000000..1bf1565
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapResponseParser.java
@@ -0,0 +1,434 @@
+/*
+ * Copyright (C) 2010 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.android.phone.common.mail.store.imap;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.phone.common.mail.FixedLengthInputStream;
+import com.android.phone.common.mail.PeekableInputStream;
+import com.android.phone.common.mail.MessagingException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * IMAP response parser.
+ */
+public class ImapResponseParser {
+    private static final String TAG = "ImapResponseParser";
+
+    /**
+     * Literal larger than this will be stored in temp file.
+     */
+    public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
+
+    /** Input stream */
+    private final PeekableInputStream mIn;
+
+    private final int mLiteralKeepInMemoryThreshold;
+
+    /** StringBuilder used by readUntil() */
+    private final StringBuilder mBufferReadUntil = new StringBuilder();
+
+    /** StringBuilder used by parseBareString() */
+    private final StringBuilder mParseBareString = new StringBuilder();
+
+    /**
+     * We store all {@link ImapResponse} in it.  {@link #destroyResponses()} must be called from
+     * time to time to destroy them and clear it.
+     */
+    private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
+
+    /**
+     * Exception thrown when we receive BYE.  It derives from IOException, so it'll be treated
+     * in the same way EOF does.
+     */
+    public static class ByeException extends IOException {
+        public static final String MESSAGE = "Received BYE";
+        public ByeException() {
+            super(MESSAGE);
+        }
+    }
+
+    /**
+     * Public constructor for normal use.
+     */
+    public ImapResponseParser(InputStream in) {
+        this(in, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
+    }
+
+    /**
+     * Constructor for testing to override the literal size threshold.
+     */
+    /* package for test */ ImapResponseParser(InputStream in, int literalKeepInMemoryThreshold) {
+        mIn = new PeekableInputStream(in);
+        mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
+    }
+
+    private static IOException newEOSException() {
+        final String message = "End of stream reached";
+        if (Log.isLoggable(TAG, Log.DEBUG)) {
+            Log.d(TAG, message);
+        }
+        return new IOException(message);
+    }
+
+    /**
+     * Peek next one byte.
+     *
+     * Throws IOException() if reaches EOF.  As long as logical response lines end with \r\n,
+     * we shouldn't see EOF during parsing.
+     */
+    private int peek() throws IOException {
+        final int next = mIn.peek();
+        if (next == -1) {
+            throw newEOSException();
+        }
+        return next;
+    }
+
+    /**
+     * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
+     *
+     * Throws IOException() if reaches EOF.  As long as logical response lines end with \r\n,
+     * we shouldn't see EOF during parsing.
+     */
+    private int readByte() throws IOException {
+        int next = mIn.read();
+        if (next == -1) {
+            throw newEOSException();
+        }
+        return next;
+    }
+
+    /**
+     * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
+     *
+     * @see #readResponse()
+     */
+    public void destroyResponses() {
+        for (ImapResponse r : mResponsesToDestroy) {
+            r.destroy();
+        }
+        mResponsesToDestroy.clear();
+    }
+
+    /**
+     * Reads the next response available on the stream and returns an
+     * {@link ImapResponse} object that represents it.
+     *
+     * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse}
+     * is stored in the internal storage.  When the {@link ImapResponse} is no longer used
+     * {@link #destroyResponses} should be called to destroy all the responses in the array.
+     *
+     * @return the parsed {@link ImapResponse} object.
+     * @exception ByeException when detects BYE.
+     */
+    public ImapResponse readResponse() throws IOException, MessagingException {
+        ImapResponse response = null;
+        try {
+            response = parseResponse();
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "<<< " + response.toString());
+            }
+        } catch (RuntimeException e) {
+            // Parser crash -- log network activities.
+            onParseError(e);
+            throw e;
+        } catch (IOException e) {
+            // Network error, or received an unexpected char.
+            onParseError(e);
+            throw e;
+        }
+
+        // Handle this outside of try-catch.  We don't have to dump protocol log when getting BYE.
+        if (response.is(0, ImapConstants.BYE)) {
+            Log.w(TAG, ByeException.MESSAGE);
+            response.destroy();
+            throw new ByeException();
+        }
+        mResponsesToDestroy.add(response);
+        return response;
+    }
+
+    private void onParseError(Exception e) {
+        // Read a few more bytes, so that the log will contain some more context, even if the parser
+        // crashes in the middle of a response.
+        // This also makes sure the byte in question will be logged, no matter where it crashes.
+        // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
+        // before actually reading it.
+        // However, we don't want to read too much, because then it may get into an email message.
+        try {
+            for (int i = 0; i < 4; i++) {
+                int b = readByte();
+                if (b == -1 || b == '\n') {
+                    break;
+                }
+            }
+        } catch (IOException ignore) {
+        }
+        Log.w(TAG, "Exception detected: " + e.getMessage());
+    }
+
+    /**
+     * Read next byte from stream and throw it away.  If the byte is different from {@code expected}
+     * throw {@link MessagingException}.
+     */
+    /* package for test */ void expect(char expected) throws IOException {
+        final int next = readByte();
+        if (expected != next) {
+            throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
+                    (int) expected, expected, next, (char) next));
+        }
+    }
+
+    /**
+     * Read bytes until we find {@code end}, and return all as string.
+     * The {@code end} will be read (rather than peeked) and won't be included in the result.
+     */
+    /* package for test */ String readUntil(char end) throws IOException {
+        mBufferReadUntil.setLength(0);
+        for (;;) {
+            final int ch = readByte();
+            if (ch != end) {
+                mBufferReadUntil.append((char) ch);
+            } else {
+                return mBufferReadUntil.toString();
+            }
+        }
+    }
+
+    /**
+     * Read all bytes until \r\n.
+     */
+    /* package */ String readUntilEol() throws IOException {
+        String ret = readUntil('\r');
+        expect('\n'); // TODO Should this really be error?
+        return ret;
+    }
+
+    /**
+     * Parse and return the response line.
+     */
+    private ImapResponse parseResponse() throws IOException, MessagingException {
+        // We need to destroy the response if we get an exception.
+        // So, we first store the response that's being built in responseToDestroy, until it's
+        // completely built, at which point we copy it into responseToReturn and null out
+        // responseToDestroyt.
+        // If responseToDestroy is not null in finally, we destroy it because that means
+        // we got an exception somewhere.
+        ImapResponse responseToDestroy = null;
+        final ImapResponse responseToReturn;
+
+        try {
+            final int ch = peek();
+            if (ch == '+') { // Continuation request
+                readByte(); // skip +
+                expect(' ');
+                responseToDestroy = new ImapResponse(null, true);
+
+                // If it's continuation request, we don't really care what's in it.
+                responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+
+                // Response has successfully been built.  Let's return it.
+                responseToReturn = responseToDestroy;
+                responseToDestroy = null;
+            } else {
+                // Status response or response data
+                final String tag;
+                if (ch == '*') {
+                    tag = null;
+                    readByte(); // skip *
+                    expect(' ');
+                } else {
+                    tag = readUntil(' ');
+                }
+                responseToDestroy = new ImapResponse(tag, false);
+
+                final ImapString firstString = parseBareString();
+                responseToDestroy.add(firstString);
+
+                // parseBareString won't eat a space after the string, so we need to skip it,
+                // if exists.
+                // If the next char is not ' ', it should be EOL.
+                if (peek() == ' ') {
+                    readByte(); // skip ' '
+
+                    if (responseToDestroy.isStatusResponse()) { // It's a status response
+
+                        // Is there a response code?
+                        final int next = peek();
+                        if (next == '[') {
+                            responseToDestroy.add(parseList('[', ']'));
+                            if (peek() == ' ') { // Skip following space
+                                readByte();
+                            }
+                        }
+
+                        String rest = readUntilEol();
+                        if (!TextUtils.isEmpty(rest)) {
+                            // The rest is free-form text.
+                            responseToDestroy.add(new ImapSimpleString(rest));
+                        }
+                    } else { // It's a response data.
+                        parseElements(responseToDestroy, '\0');
+                    }
+                } else {
+                    expect('\r');
+                    expect('\n');
+                }
+
+                // Response has successfully been built.  Let's return it.
+                responseToReturn = responseToDestroy;
+                responseToDestroy = null;
+            }
+        } finally {
+            if (responseToDestroy != null) {
+                // We get an exception.
+                responseToDestroy.destroy();
+            }
+        }
+
+        return responseToReturn;
+    }
+
+    private ImapElement parseElement() throws IOException, MessagingException {
+        final int next = peek();
+        switch (next) {
+            case '(':
+                return parseList('(', ')');
+            case '[':
+                return parseList('[', ']');
+            case '"':
+                readByte(); // Skip "
+                return new ImapSimpleString(readUntil('"'));
+            case '{':
+                return parseLiteral();
+            case '\r':  // CR
+                readByte(); // Consume \r
+                expect('\n'); // Should be followed by LF.
+                return null;
+            case '\n': // LF // There shouldn't be a bare LF, but just in case.
+                readByte(); // Consume \n
+                return null;
+            default:
+                return parseBareString();
+        }
+    }
+
+    /**
+     * Parses an atom.
+     *
+     * Special case: If an atom contains '[', everything until the next ']' will be considered
+     * a part of the atom.
+     * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
+     *
+     * If the value is "NIL", returns an empty string.
+     */
+    private ImapString parseBareString() throws IOException, MessagingException {
+        mParseBareString.setLength(0);
+        for (;;) {
+            final int ch = peek();
+
+            // TODO Can we clean this up?  (This condition is from the old parser.)
+            if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
+                    // ']' is not part of atom (it's in resp-specials)
+                    ch == ']' ||
+                    // docs claim that flags are \ atom but atom isn't supposed to
+                    // contain
+                    // * and some flags contain *
+                    // ch == '%' || ch == '*' ||
+                    ch == '%' ||
+                    // TODO probably should not allow \ and should recognize
+                    // it as a flag instead
+                    // ch == '"' || ch == '\' ||
+                    ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) {
+                if (mParseBareString.length() == 0) {
+                    throw new MessagingException("Expected string, none found.");
+                }
+                String s = mParseBareString.toString();
+
+                // NIL will be always converted into the empty string.
+                if (ImapConstants.NIL.equalsIgnoreCase(s)) {
+                    return ImapString.EMPTY;
+                }
+                return new ImapSimpleString(s);
+            } else if (ch == '[') {
+                // Eat all until next ']'
+                mParseBareString.append((char) readByte());
+                mParseBareString.append(readUntil(']'));
+                mParseBareString.append(']'); // readUntil won't include the end char.
+            } else {
+                mParseBareString.append((char) readByte());
+            }
+        }
+    }
+
+    private void parseElements(ImapList list, char end)
+            throws IOException, MessagingException {
+        for (;;) {
+            for (;;) {
+                final int next = peek();
+                if (next == end) {
+                    return;
+                }
+                if (next != ' ') {
+                    break;
+                }
+                // Skip space
+                readByte();
+            }
+            final ImapElement el = parseElement();
+            if (el == null) { // EOL
+                return;
+            }
+            list.add(el);
+        }
+    }
+
+    private ImapList parseList(char opening, char closing)
+            throws IOException, MessagingException {
+        expect(opening);
+        final ImapList list = new ImapList();
+        parseElements(list, closing);
+        expect(closing);
+        return list;
+    }
+
+    private ImapString parseLiteral() throws IOException, MessagingException {
+        expect('{');
+        final int size;
+        try {
+            size = Integer.parseInt(readUntil('}'));
+        } catch (NumberFormatException nfe) {
+            throw new MessagingException("Invalid length in literal");
+        }
+        if (size < 0) {
+            throw new MessagingException("Invalid negative length in literal");
+        }
+        expect('\r');
+        expect('\n');
+        FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
+        if (size > mLiteralKeepInMemoryThreshold) {
+            return new ImapTempFileLiteral(in);
+        } else {
+            return new ImapMemoryLiteral(in);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapSimpleString.java b/src/com/android/phone/common/mail/store/imap/ImapSimpleString.java
new file mode 100644
index 0000000..3d5263b
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapSimpleString.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store.imap;
+
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Subclass of {@link ImapString} used for non literals.
+ */
+public class ImapSimpleString extends ImapString {
+    private final String TAG = "ImapSimpleString";
+    private String mString;
+
+    /* package */  ImapSimpleString(String string) {
+        mString = (string != null) ? string : "";
+    }
+
+    @Override
+    public void destroy() {
+        mString = null;
+        super.destroy();
+    }
+
+    @Override
+    public String getString() {
+        return mString;
+    }
+
+    @Override
+    public InputStream getAsStream() {
+        try {
+            return new ByteArrayInputStream(mString.getBytes("US-ASCII"));
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "Unsupported encoding: ", e);
+        }
+        return null;
+    }
+
+    @Override
+    public String toString() {
+        // Purposefully not return just mString, in order to prevent using it instead of getString.
+        return "\"" + mString + "\"";
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapString.java b/src/com/android/phone/common/mail/store/imap/ImapString.java
new file mode 100644
index 0000000..f38a993
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapString.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store.imap;
+
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Class represents an IMAP "element" that is not a list.
+ *
+ * An atom, quoted string, literal, are all represented by this.  Values like OK, STATUS are too.
+ * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]".
+ * See {@link ImapResponseParser}.
+ */
+public abstract class ImapString extends ImapElement {
+    private static final byte[] EMPTY_BYTES = new byte[0];
+
+    public static final ImapString EMPTY = new ImapString() {
+        @Override public void destroy() {
+            // Don't call super.destroy().
+            // It's a shared object.  We don't want the mDestroyed to be set on this.
+        }
+
+        @Override public String getString() {
+            return "";
+        }
+
+        @Override public InputStream getAsStream() {
+            return new ByteArrayInputStream(EMPTY_BYTES);
+        }
+
+        @Override public String toString() {
+            return "";
+        }
+    };
+
+    // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
+    // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
+    // handled by Locale.US
+    private final static SimpleDateFormat DATE_TIME_FORMAT =
+            new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
+
+    private boolean mIsInteger;
+    private int mParsedInteger;
+    private Date mParsedDate;
+
+    @Override
+    public final boolean isList() {
+        return false;
+    }
+
+    @Override
+    public final boolean isString() {
+        return true;
+    }
+
+    /**
+     * @return true if and only if the length of the string is larger than 0.
+     *
+     * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
+     * #parseBareString}.
+     * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is
+     * treated literally.
+     */
+    public final boolean isEmpty() {
+        return getString().length() == 0;
+    }
+
+    public abstract String getString();
+
+    public abstract InputStream getAsStream();
+
+    /**
+     * @return whether it can be parsed as a number.
+     */
+    public final boolean isNumber() {
+        if (mIsInteger) {
+            return true;
+        }
+        try {
+            mParsedInteger = Integer.parseInt(getString());
+            mIsInteger = true;
+            return true;
+        } catch (NumberFormatException e) {
+            return false;
+        }
+    }
+
+    /**
+     * @return value parsed as a number.
+     */
+    public final int getNumberOrZero() {
+        if (!isNumber()) {
+            return 0;
+        }
+        return mParsedInteger;
+    }
+
+    /**
+     * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}.
+     */
+    public final boolean isDate() {
+        if (mParsedDate != null) {
+            return true;
+        }
+        if (isEmpty()) {
+            return false;
+        }
+        try {
+            mParsedDate = DATE_TIME_FORMAT.parse(getString());
+            return true;
+        } catch (ParseException e) {
+            Log.w("ImapString", getString() + " can't be parsed as a date.");
+            return false;
+        }
+    }
+
+    /**
+     * @return value it can be parsed as a {@link Date}, or null otherwise.
+     */
+    public final Date getDateOrNull() {
+        if (!isDate()) {
+            return null;
+        }
+        return mParsedDate;
+    }
+
+    /**
+     * @return whether the value case-insensitively equals to {@code s}.
+     */
+    public final boolean is(String s) {
+        if (s == null) {
+            return false;
+        }
+        return getString().equalsIgnoreCase(s);
+    }
+
+
+    /**
+     * @return whether the value case-insensitively starts with {@code s}.
+     */
+    public final boolean startsWith(String prefix) {
+        if (prefix == null) {
+            return false;
+        }
+        final String me = this.getString();
+        if (me.length() < prefix.length()) {
+            return false;
+        }
+        return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
+    }
+
+    // To force subclasses to implement it.
+    @Override
+    public abstract String toString();
+
+    @Override
+    public final boolean equalsForTest(ImapElement that) {
+        if (!super.equalsForTest(that)) {
+            return false;
+        }
+        ImapString thatString = (ImapString) that;
+        return getString().equals(thatString.getString());
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapTempFileLiteral.java b/src/com/android/phone/common/mail/store/imap/ImapTempFileLiteral.java
new file mode 100644
index 0000000..2b1113e
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapTempFileLiteral.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store.imap;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.phone.common.mail.FixedLengthInputStream;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Subclass of {@link ImapString} used for literals backed by a temp file.
+ */
+public class ImapTempFileLiteral extends ImapString {
+    private final String TAG = "ImapTempFileLiteral";
+
+    /* package for test */ final File mFile;
+
+    /** Size is purely for toString() */
+    private final int mSize;
+
+    /* package */  ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
+        mSize = stream.getLength();
+        mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
+
+        // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
+        // so it'd simply cause a memory leak.
+        // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
+        // mFile.deleteOnExit();
+        OutputStream out = new FileOutputStream(mFile);
+        StreamUtils.copy(stream, out);
+        out.close();
+    }
+
+    /**
+     * Copies utility methods for working with byte arrays and I/O streams from guava library.
+     */
+    public static class StreamUtils {
+        private static final int BUF_SIZE = 0x1000; // 4K
+        /**
+         * Copies all bytes from the input stream to the output stream.
+         * Does not close or flush either stream.
+         *
+         * @param from the input stream to read from
+         * @param to the output stream to write to
+         * @return the number of bytes copied
+         * @throws IOException if an I/O error occurs
+         */
+        public static long copy(InputStream from, OutputStream to) throws IOException {
+            checkNotNull(from);
+            checkNotNull(to);
+            byte[] buf = new byte[BUF_SIZE];
+            long total = 0;
+            while (true) {
+              int r = from.read(buf);
+              if (r == -1) {
+                break;
+              }
+              to.write(buf, 0, r);
+              total += r;
+            }
+            return total;
+        }
+
+        /**
+         * Reads all bytes from an input stream into a byte array.
+         * Does not close the stream.
+         *
+         * @param in the input stream to read from
+         * @return a byte array containing all the bytes from the stream
+         * @throws IOException if an I/O error occurs
+         */
+        public static byte[] toByteArray(InputStream in) throws IOException {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            copy(in, out);
+            return out.toByteArray();
+        }
+
+        /**
+         * Ensures that an object reference passed as a parameter to the calling method is not null.
+         *
+         * @param reference an object reference
+         * @return the non-null reference that was validated
+         * @throws NullPointerException if {@code reference} is null
+         */
+        public static <T> T checkNotNull(T reference) {
+            if (reference == null) {
+                throw new NullPointerException();
+            }
+            return reference;
+        }
+    }
+
+    /**
+     * Make sure we delete the temp file.
+     *
+     * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
+     */
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            destroy();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    @Override
+    public InputStream getAsStream() {
+        checkNotDestroyed();
+        try {
+            return new FileInputStream(mFile);
+        } catch (FileNotFoundException e) {
+            // It's probably possible if we're low on storage and the system clears the cache dir.
+            Log.w(TAG, "ImapTempFileLiteral: Temp file not found");
+
+            // Return 0 byte stream as a dummy...
+            return new ByteArrayInputStream(new byte[0]);
+        }
+    }
+
+    @Override
+    public String getString() {
+        checkNotDestroyed();
+        try {
+            byte[] bytes = StreamUtils.toByteArray(getAsStream());
+            // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
+            if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
+                throw new IOException();
+            }
+            return new String(bytes, "US-ASCII");
+        } catch (IOException e) {
+            Log.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+            return "";
+        }
+    }
+
+    @Override
+    public void destroy() {
+        try {
+            if (!isDestroyed() && mFile.exists()) {
+                mFile.delete();
+            }
+        } catch (RuntimeException re) {
+            // Just log and ignore.
+            Log.w(TAG, "Failed to remove temp file: " + re.getMessage());
+        }
+        super.destroy();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("{%d byte literal(file)}", mSize);
+    }
+
+    public boolean tempFileExistsForTest() {
+        return mFile.exists();
+    }
+
+   /**
+    * TempDirectory caches the directory used for caching file.  It is set up during application
+    * initialization.
+    */
+   public static class TempDirectory {
+       private static File sTempDirectory = null;
+
+       public static void setTempDirectory(Context context) {
+           sTempDirectory = context.getCacheDir();
+       }
+
+       public static File getTempDirectory() {
+           if (sTempDirectory == null) {
+               throw new RuntimeException(
+                       "TempDirectory not set.  " +
+                       "If in a unit test, call Email.setTempDirectory(context) in setUp().");
+           }
+           return sTempDirectory;
+       }
+   }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/common/mail/store/imap/ImapUtility.java b/src/com/android/phone/common/mail/store/imap/ImapUtility.java
new file mode 100644
index 0000000..28c8b6f
--- /dev/null
+++ b/src/com/android/phone/common/mail/store/imap/ImapUtility.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.android.phone.common.mail.store.imap;
+
+/**
+ * Utility methods for use with IMAP.
+ */
+public class ImapUtility {
+    /**
+     * Apply quoting rules per IMAP RFC,
+     * quoted          = DQUOTE *QUOTED-CHAR DQUOTE
+     * QUOTED-CHAR     = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
+     * quoted-specials = DQUOTE / "\"
+     *
+     * This is used primarily for IMAP login, but might be useful elsewhere.
+     *
+     * NOTE:  Not very efficient - you may wish to preflight this, or perhaps it should check
+     * for trouble chars before calling the replace functions.
+     *
+     * @param s The string to be quoted.
+     * @return A copy of the string, having undergone quoting as described above
+     */
+    public static String imapQuoted(String s) {
+
+        // First, quote any backslashes by replacing \ with \\
+        // regex Pattern:  \\    (Java string const = \\\\)
+        // Substitute:     \\\\  (Java string const = \\\\\\\\)
+        String result = s.replaceAll("\\\\", "\\\\\\\\");
+
+        // Then, quote any double-quotes by replacing " with \"
+        // regex Pattern:  "    (Java string const = \")
+        // Substitute:     \\"  (Java string const = \\\\\")
+        result = result.replaceAll("\"", "\\\\\"");
+
+        // return string with quotes around it
+        return "\"" + result + "\"";
+    }
+}
\ No newline at end of file