summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorNancy Chen <nancychen@google.com>2015-03-19 16:28:55 -0700
committerNancy Chen <nancychen@google.com>2015-03-25 12:49:57 -0700
commit98750f5774070527403035d1713ea59abd71ec08 (patch)
tree9286ec4e2ea1225e3b087d9aaab5d3a1f058fcaa /src
parente36994bef9400604210d81c98598cab7c256a6f6 (diff)
downloadandroid_packages_apps_PhoneCommon-98750f5774070527403035d1713ea59abd71ec08.tar.gz
android_packages_apps_PhoneCommon-98750f5774070527403035d1713ea59abd71ec08.tar.bz2
android_packages_apps_PhoneCommon-98750f5774070527403035d1713ea59abd71ec08.zip
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
Diffstat (limited to 'src')
-rw-r--r--src/com/android/phone/common/mail/AuthenticationFailedException.java33
-rw-r--r--src/com/android/phone/common/mail/CertificateValidationException.java29
-rw-r--r--src/com/android/phone/common/mail/FixedLengthInputStream.java79
-rw-r--r--src/com/android/phone/common/mail/MailTransport.java247
-rw-r--r--src/com/android/phone/common/mail/Message.java24
-rw-r--r--src/com/android/phone/common/mail/MessagingException.java139
-rw-r--r--src/com/android/phone/common/mail/PeekableInputStream.java80
-rw-r--r--src/com/android/phone/common/mail/store/ImapConnection.java248
-rw-r--r--src/com/android/phone/common/mail/store/ImapFolder.java165
-rw-r--r--src/com/android/phone/common/mail/store/ImapStore.java113
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapConstants.java97
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapElement.java120
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapList.java235
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapMemoryLiteral.java77
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapResponse.java158
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapResponseParser.java434
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapSimpleString.java62
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapString.java185
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapTempFileLiteral.java203
-rw-r--r--src/com/android/phone/common/mail/store/imap/ImapUtility.java51
20 files changed, 2779 insertions, 0 deletions
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