Make ImapConnect a top-level class

Split out ImapConnection to its own class. This allows us to update ImapStore
without worrying about links between it and the connection.

Also, added a bit more safety to the classes in terms of correctly freeing
resources. Whenever the connection is closed, it now releases all resources.
Additionally, if the connection is ever put back in the pool, any response
data is released.

Change-Id: Ie3bda40d677707a0d6655f57175e58dece539e19
This commit is contained in:
Todd Kennedy 2011-05-16 13:49:27 -07:00
parent b9c2e0d5e6
commit ebece4dbdc
7 changed files with 570 additions and 434 deletions

View File

@ -38,7 +38,7 @@ import java.net.SocketException;
* Interpretation of URI
* Support for SSL and TLS wireline security
*/
public interface Transport {
public interface Transport extends Cloneable {
/**
* Connection security options for transport that supports SSL and/or TLS
@ -52,7 +52,7 @@ public interface Transport {
* setUri() and setSecurity() have been called, but not opened or connected in any way.
* @return a new Transport ready to open()
*/
public Transport newInstanceWithConfiguration();
public Transport clone();
/**
* Sets the host

View File

@ -0,0 +1,446 @@
/*
* Copyright (C) 2011 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.email.mail.store;
import com.android.email.Email;
import com.android.email.mail.Transport;
import com.android.email.mail.store.ImapStore.ImapException;
import com.android.email.mail.store.imap.ImapConstants;
import com.android.email.mail.store.imap.ImapList;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapResponseParser;
import com.android.email.mail.store.imap.ImapUtility;
import com.android.email.mail.transport.DiscourseLogger;
import com.android.email.mail.transport.MailTransport;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
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.
*/
class ImapConnection {
/** ID capability per RFC 2971*/
public static final int CAPABILITY_ID = 1 << 0;
/** NAMESPACE capability per RFC 2342 */
public static final int CAPABILITY_NAMESPACE = 1 << 1;
/** STARTTLS capability per RFC 3501 */
public static final int CAPABILITY_STARTTLS = 1 << 2;
/** UIDPLUS capability per RFC 4315 */
public static final int CAPABILITY_UIDPLUS = 1 << 3;
/** The capabilities supported; a set of CAPABILITY_* values. */
private int mCapabilities;
private static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
Transport mTransport;
private ImapResponseParser mParser;
private ImapStore mImapStore;
private String mUsername;
private String mLoginPhrase;
private String mIdPhrase = null;
/** # of command/response lines to log upon crash. */
private static final int DISCOURSE_LOGGER_SIZE = 64;
private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
/**
* 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);
// Keep others from instantiating directly
ImapConnection(ImapStore store, String username, String password) {
setStore(store, username, password);
}
void setStore(ImapStore store, String username, String password) {
if (username != null && password != null) {
mUsername = username;
// build the LOGIN string once (instead of over-and-over again.)
// apply the quoting here around the built-up password
mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
+ ImapUtility.imapQuoted(password);
}
mImapStore = store;
}
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.mRootTransport.clone();
}
mTransport.open();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
createParser();
// BANNER
mParser.readResponse();
// CAPABILITY
ImapResponse capabilities = queryCapabilities();
boolean hasStartTlsCapability =
capabilities.contains(ImapConstants.STARTTLS);
// TLS
ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
if (newCapabilities != null) {
capabilities = newCapabilities;
}
// NOTE: An IMAP response MUST be processed before issuing any new IMAP
// requests. Subsequent requests may destroy previous response data. As
// such, we save away capability information here for future use.
setCapabilities(capabilities);
String capabilityString = capabilities.flatten();
// ID
doSendId(isCapable(CAPABILITY_ID), capabilityString);
// LOGIN
doLogin();
// NAMESPACE (only valid in the Authenticated state)
doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
// Gets the path separator from the server
doGetPathSeparator();
mImapStore.ensurePrefixIsValid();
} catch (SSLException e) {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
// NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot
// of other code here that catches IOException and I don't want to break it.
// This catch is only here to enhance logging of connection-time issues.
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw ioe;
} finally {
destroyResponses();
}
}
/**
* Closes the connection and releases all resources. This connection can not be used again
* until {@link #setStore(ImapStore, String, String)} is called.
*/
void close() {
if (mTransport != null) {
mTransport.close();
mTransport = null;
}
destroyResponses();
mParser = null;
mImapStore = null;
}
/**
* Returns whether or not the specified capability is supported by the server.
*/
private boolean isCapable(int capability) {
return (mCapabilities & capability) != 0;
}
/**
* Sets the capability flags according to the response provided by the server.
* Note: We only set the capability flags that we are interested in. There are many IMAP
* capabilities that we do not track.
*/
private void setCapabilities(ImapResponse capabilities) {
if (capabilities.contains(ImapConstants.ID)) {
mCapabilities |= CAPABILITY_ID;
}
if (capabilities.contains(ImapConstants.NAMESPACE)) {
mCapabilities |= CAPABILITY_NAMESPACE;
}
if (capabilities.contains(ImapConstants.UIDPLUS)) {
mCapabilities |= CAPABILITY_UIDPLUS;
}
if (capabilities.contains(ImapConstants.STARTTLS)) {
mCapabilities |= CAPABILITY_STARTTLS;
}
}
/**
* 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(), mDiscourse);
}
void destroyResponses() {
if (mParser != null) {
mParser.destroyResponses();
}
}
boolean isTransportOpenForTest() {
return mTransport != null ? mTransport.isOpen() : false;
}
ImapResponse readResponse() throws IOException, MessagingException {
return mParser.readResponse();
}
/**
* 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).
*
* @param command The command to send to the server
* @param sensitive If true, the command will not be logged
* @return Returns the command tag that was sent
*/
String sendCommand(String command, boolean sensitive)
throws MessagingException, IOException {
open();
String tag = Integer.toString(mNextCommandTag.incrementAndGet());
String commandToSend = tag + " " + command;
mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
return tag;
}
List<ImapResponse> executeSimpleCommand(String command) throws IOException,
MessagingException {
return executeSimpleCommand(command, false);
}
List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
throws IOException, MessagingException {
String tag = sendCommand(command, sensitive);
ArrayList<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 alert = response.getAlertTextOrEmpty().getString();
destroyResponses();
throw new ImapException(toString, alert);
}
return responses;
}
/**
* Query server for capabilities.
*/
private ImapResponse queryCapabilities() throws IOException, MessagingException {
ImapResponse capabilityResponse = null;
for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
if (r.is(0, ImapConstants.CAPABILITY)) {
capabilityResponse = r;
break;
}
}
if (capabilityResponse == null) {
throw new MessagingException("Invalid CAPABILITY response received");
}
return capabilityResponse;
}
/**
* Sends client identification information to the IMAP server per RFC 2971. If
* the server does not support the ID command, this will perform no operation.
*
* Interoperability hack: Never send ID to *.secureserver.net, which sends back a
* malformed response that our parser can't deal with.
*/
private void doSendId(boolean hasIdCapability, String capabilities)
throws MessagingException {
if (!hasIdCapability) return;
// Never send ID to *.secureserver.net
String host = mTransport.getHost();
if (host.toLowerCase().endsWith(".secureserver.net")) return;
// Assign user-agent string (for RFC2971 ID command)
String mUserAgent =
ImapStore.getImapId(mImapStore.getContext(), mUsername, host, capabilities);
if (mUserAgent != null) {
mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
} else if (ImapStore.DEBUG_FORCE_SEND_ID) {
mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
}
// else: mIdPhrase = null, no ID will be emitted
// Send user-agent in an RFC2971 ID command
if (mIdPhrase != null) {
try {
executeSimpleCommand(mIdPhrase);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
// A true IOException will recur on the following login steps
// This can go away after the parser is fixed - see bug 2138981
}
}
}
/**
* Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
* explicitly sets a namespace (using setup UI) or if the server does not support the
* namespace command, this will perform no operation.
*/
private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
// user did not specify a hard-coded prefix; try to get it from the server
if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
List<ImapResponse> responseList = Collections.emptyList();
try {
responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
}
for (ImapResponse response: responseList) {
if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
ImapList namespaceList = response.getListOrEmpty(1);
ImapList namespace = namespaceList.getListOrEmpty(0);
String namespaceString = namespace.getStringOrEmpty(0).getString();
if (!TextUtils.isEmpty(namespaceString)) {
mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
}
}
}
}
}
/**
* Logs into the IMAP server
*/
private void doLogin()
throws IOException, MessagingException, AuthenticationFailedException {
try {
// TODO eventually we need to add additional authentication
// options such as SASL
executeSimpleCommand(mLoginPhrase, true);
} catch (ImapException ie) {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
throw new AuthenticationFailedException(ie.getAlertText(), ie);
} catch (MessagingException me) {
throw new AuthenticationFailedException(null, me);
}
}
/**
* Gets the path separator per the LIST command in RFC 3501. If the path separator
* was obtained while obtaining the namespace or there is no prefix defined, this
* will perform no operation.
*/
private void doGetPathSeparator() throws MessagingException {
// user did not specify a hard-coded prefix; try to get it from the server
if (mImapStore.isUserPrefixSet()) {
List<ImapResponse> responseList = Collections.emptyList();
try {
responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
}
for (ImapResponse response: responseList) {
if (response.isDataResponse(0, ImapConstants.LIST)) {
mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
}
}
}
}
/**
* Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
* to use TLS or the server does not support the TLS capability, this will perform
* no operation.
*/
private ImapResponse doStartTls(boolean hasStartTlsCapability)
throws IOException, MessagingException {
if (mTransport.canTryTlsSecurity()) {
if (hasStartTlsCapability) {
// STARTTLS
executeSimpleCommand(ImapConstants.STARTTLS);
mTransport.reopenTls();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
createParser();
// Per RFC requirement (3501-6.2.1) gather new capabilities
return(queryCapabilities());
} else {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
}
}
return null;
}
/** @see DiscourseLogger#logLastDiscourse() */
void logLastDiscourse() {
mDiscourse.logLastDiscourse();
}
}

View File

@ -22,7 +22,6 @@ import android.util.Base64DataException;
import android.util.Log;
import com.android.email.Email;
import com.android.email.mail.store.ImapStore.ImapConnection;
import com.android.email.mail.store.ImapStore.ImapException;
import com.android.email.mail.store.ImapStore.ImapMessage;
import com.android.email.mail.store.imap.ImapConstants;
@ -151,7 +150,6 @@ class ImapFolder extends Folder {
// TODO implement expunge
mMessageCount = -1;
synchronized (this) {
destroyResponses();
mStore.poolConnection(mConnection);
mConnection = null;
}
@ -1040,7 +1038,6 @@ class ImapFolder extends Folder {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "IO Exception detected: ", ioe);
}
connection.destroyResponses();
connection.close();
if (connection == mConnection) {
mConnection = null; // To prevent close() from returning the connection to the pool.

View File

@ -16,24 +16,18 @@
package com.android.email.mail.store;
import com.android.email.Email;
import com.android.email.LegacyConversions;
import com.android.email.Preferences;
import com.android.email.VendorPolicyLoader;
import com.android.email.mail.Store;
import com.android.email.mail.Transport;
import com.android.email.mail.store.imap.ImapConstants;
import com.android.email.mail.store.imap.ImapList;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapResponseParser;
import com.android.email.mail.store.imap.ImapString;
import com.android.email.mail.store.imap.ImapUtility;
import com.android.email.mail.transport.DiscourseLogger;
import com.android.email.mail.transport.MailTransport;
import com.android.emailcommon.Logging;
import com.android.emailcommon.internet.MimeMessage;
import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Message;
@ -60,17 +54,13 @@ import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import javax.net.ssl.SSLException;
/**
* <pre>
@ -91,21 +81,19 @@ import javax.net.ssl.SSLException;
public class ImapStore extends Store {
// Always check in FALSE
private static final boolean DEBUG_FORCE_SEND_ID = false;
static final boolean DEBUG_FORCE_SEND_ID = false;
static final int COPY_BUFFER_SIZE = 16*1024;
static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
private final Context mContext;
final Context mContext;
private final Account mAccount;
private Transport mRootTransport;
Transport mRootTransport;
@VisibleForTesting static String sImapId = null;
@VisibleForTesting String mPathPrefix;
@VisibleForTesting String mPathSeparator;
private String mUsername;
private String mPassword;
private String mLoginPhrase;
private String mIdPhrase = null;
@VisibleForTesting static String sImapId = null;
/*package*/ String mPathPrefix;
/*package*/ String mPathSeparator;
private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
new ConcurrentLinkedQueue<ImapConnection>();
@ -124,14 +112,6 @@ public class ImapStore extends Store {
*/
private final HashMap<String, ImapFolder> mFolderCache = new HashMap<String, ImapFolder>();
/**
* 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);
/**
* Static named constructor.
*/
@ -172,20 +152,19 @@ public class ImapStore extends Store {
mRootTransport.setPort(port);
mRootTransport.setSecurity(connectionSecurity, trustCertificates);
String[] userInfoParts = recvAuth.getLogin();
if (userInfoParts != null) {
mUsername = userInfoParts[0];
mPassword = userInfoParts[1];
// build the LOGIN string once (instead of over-and-over again.)
// apply the quoting here around the built-up password
mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
+ ImapUtility.imapQuoted(mPassword);
String[] userInfo = recvAuth.getLogin();
if (userInfo != null) {
mUsername = userInfo[0];
mPassword = userInfo[1];
} else {
mUsername = null;
mPassword = null;
}
mPathPrefix = recvAuth.mDomain;
}
/* package */ Collection<ImapConnection> getConnectionPoolForTest() {
@VisibleForTesting
Collection<ImapConnection> getConnectionPoolForTest() {
return mConnectionPool;
}
@ -195,7 +174,8 @@ public class ImapStore extends Store {
* should already be set up and ready to use. Do not use for real code.
* @param testTransport The Transport to inject and use for all future communication.
*/
/* package */ void setTransport(Transport testTransport) {
@VisibleForTesting
void setTransportForTest(Transport testTransport) {
mRootTransport = testTransport;
}
@ -223,8 +203,8 @@ public class ImapStore extends Store {
* @param capabilities a list of the capabilities from the server
* @return a String for use in an IMAP ID message.
*/
@VisibleForTesting static String getImapId(Context context, String userName, String host,
String capabilities) {
@VisibleForTesting
static String getImapId(Context context, String userName, String host, String capabilities) {
// The first section is global to all IMAP connections, and generates the fixed
// values in any IMAP ID message
synchronized (ImapStore.class) {
@ -284,7 +264,8 @@ public class ImapStore extends Store {
* @param networkOperator TelephonyManager.getNetworkOperatorName()
* @return the static (never changes) portion of the IMAP ID
*/
@VisibleForTesting static String makeCommonImapId(String packageName, String version,
@VisibleForTesting
static String makeCommonImapId(String packageName, String version,
String codeName, String model, String id, String vendor, String networkOperator) {
// Before building up IMAP ID string, pre-filter the input strings for "legal" chars
@ -484,7 +465,6 @@ public class ImapStore extends Store {
throw afe;
} finally {
if (connection != null) {
connection.destroyResponses();
poolConnection(connection);
}
}
@ -494,7 +474,7 @@ public class ImapStore extends Store {
public Bundle checkSettings() throws MessagingException {
int result = MessagingException.NO_ERROR;
Bundle bundle = new Bundle();
ImapConnection connection = new ImapConnection();
ImapConnection connection = new ImapConnection(this, mUsername, mPassword);
try {
connection.open();
connection.close();
@ -508,11 +488,34 @@ public class ImapStore extends Store {
return bundle;
}
/**
* Returns whether or not the prefix has been set by the user. This can be determined by
* the fact that the prefix is set, but, the path separator is not set.
*/
boolean isUserPrefixSet() {
return TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix);
}
/** Sets the path separator */
void setPathSeparator(String pathSeparator) {
mPathSeparator = pathSeparator;
}
/** Sets the prefix */
void setPathPrefix(String pathPrefix) {
mPathPrefix = pathPrefix;
}
/** Gets the context for this store */
Context getContext() {
return mContext;
}
/**
* Fixes the path prefix, if necessary. The path prefix must always end with the
* path separator.
*/
/*package*/ void ensurePrefixIsValid() {
void ensurePrefixIsValid() {
// Make sure the path prefix ends with the path separator
if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) {
if (!mPathPrefix.endsWith(mPathSeparator)) {
@ -524,24 +527,23 @@ public class ImapStore extends Store {
/**
* Gets a connection if one is available from the pool, or creates a new one if not.
*/
/* package */ ImapConnection getConnection() {
ImapConnection getConnection() {
ImapConnection connection = null;
while ((connection = mConnectionPool.poll()) != null) {
try {
connection.setStore(this, mUsername, mPassword);
connection.executeSimpleCommand(ImapConstants.NOOP);
break;
} catch (MessagingException e) {
// Fall through
} catch (IOException e) {
// Fall through
} finally {
connection.destroyResponses();
}
connection.close();
connection = null;
}
if (connection == null) {
connection = new ImapConnection();
connection = new ImapConnection(this, mUsername, mPassword);
}
return connection;
}
@ -549,8 +551,9 @@ public class ImapStore extends Store {
/**
* Save a {@link ImapConnection} in the pool for reuse.
*/
/* package */ void poolConnection(ImapConnection connection) {
void poolConnection(ImapConnection connection) {
if (connection != null) {
connection.destroyResponses();
mConnectionPool.add(connection);
}
}
@ -604,373 +607,6 @@ public class ImapStore extends Store {
return sb.toString();
}
/**
* A cacheable class that stores the details for a single IMAP connection.
*/
class ImapConnection {
/** ID capability per RFC 2971*/
public static final int CAPABILITY_ID = 1 << 0;
/** NAMESPACE capability per RFC 2342 */
public static final int CAPABILITY_NAMESPACE = 1 << 1;
/** STARTTLS capability per RFC 3501 */
public static final int CAPABILITY_STARTTLS = 1 << 2;
/** UIDPLUS capability per RFC 4315 */
public static final int CAPABILITY_UIDPLUS = 1 << 3;
/** The capabilities supported; a set of CAPABILITY_* values. */
private int mCapabilities;
private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]";
Transport mTransport;
private ImapResponseParser mParser;
/** # of command/response lines to log upon crash. */
private static final int DISCOURSE_LOGGER_SIZE = 64;
private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
public void open() throws IOException, MessagingException {
if (mTransport != null && mTransport.isOpen()) {
return;
}
try {
// copy configuration into a clean transport, if necessary
if (mTransport == null) {
mTransport = mRootTransport.newInstanceWithConfiguration();
}
mTransport.open();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
createParser();
// BANNER
mParser.readResponse();
// CAPABILITY
ImapResponse capabilities = queryCapabilities();
boolean hasStartTlsCapability =
capabilities.contains(ImapConstants.STARTTLS);
// TLS
ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
if (newCapabilities != null) {
capabilities = newCapabilities;
}
// NOTE: An IMAP response MUST be processed before issuing any new IMAP
// requests. Subsequent requests may destroy previous response data. As
// such, we save away capability information here for future use.
setCapabilities(capabilities);
String capabilityString = capabilities.flatten();
// ID
doSendId(isCapable(CAPABILITY_ID), capabilityString);
// LOGIN
doLogin();
// NAMESPACE (only valid in the Authenticated state)
doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
// Gets the path separator from the server
doGetPathSeparator();
ensurePrefixIsValid();
} catch (SSLException e) {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, e.toString());
}
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
// NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot
// of other code here that catches IOException and I don't want to break it.
// This catch is only here to enhance logging of connection-time issues.
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ioe.toString());
}
throw ioe;
} finally {
destroyResponses();
}
}
public void close() {
if (mTransport != null) {
mTransport.close();
mTransport = null;
}
}
/**
* Returns whether or not the specified capability is supported by the server.
*/
public boolean isCapable(int capability) {
return (mCapabilities & capability) != 0;
}
/**
* Sets the capability flags according to the response provided by the server.
* Note: We only set the capability flags that we are interested in. There are many IMAP
* capabilities that we do not track.
*/
private void setCapabilities(ImapResponse capabilities) {
if (capabilities.contains(ImapConstants.ID)) {
mCapabilities |= CAPABILITY_ID;
}
if (capabilities.contains(ImapConstants.NAMESPACE)) {
mCapabilities |= CAPABILITY_NAMESPACE;
}
if (capabilities.contains(ImapConstants.UIDPLUS)) {
mCapabilities |= CAPABILITY_UIDPLUS;
}
if (capabilities.contains(ImapConstants.STARTTLS)) {
mCapabilities |= CAPABILITY_STARTTLS;
}
}
/**
* 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(), mDiscourse);
}
public void destroyResponses() {
if (mParser != null) {
mParser.destroyResponses();
}
}
/* package */ boolean isTransportOpenForTest() {
return mTransport != null ? mTransport.isOpen() : false;
}
public ImapResponse readResponse() throws IOException, MessagingException {
return mParser.readResponse();
}
/**
* 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).
*
* @param command The command to send to the server
* @param sensitive If true, the command will not be logged
* @return Returns the command tag that was sent
*/
public String sendCommand(String command, boolean sensitive)
throws MessagingException, IOException {
open();
String tag = Integer.toString(mNextCommandTag.incrementAndGet());
String commandToSend = tag + " " + command;
mTransport.writeLine(commandToSend, sensitive ? IMAP_DEDACTED_LOG : null);
mDiscourse.addSentCommand(sensitive ? IMAP_DEDACTED_LOG : commandToSend);
return tag;
}
/*package*/ List<ImapResponse> executeSimpleCommand(String command) throws IOException,
MessagingException {
return executeSimpleCommand(command, false);
}
/*package*/ List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
throws IOException, MessagingException {
String tag = sendCommand(command, sensitive);
ArrayList<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 alert = response.getAlertTextOrEmpty().getString();
destroyResponses();
throw new ImapException(toString, alert);
}
return responses;
}
/**
* Query server for capabilities.
*/
private ImapResponse queryCapabilities() throws IOException, MessagingException {
ImapResponse capabilityResponse = null;
for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
if (r.is(0, ImapConstants.CAPABILITY)) {
capabilityResponse = r;
break;
}
}
if (capabilityResponse == null) {
throw new MessagingException("Invalid CAPABILITY response received");
}
return capabilityResponse;
}
/**
* Sends client identification information to the IMAP server per RFC 2971. If
* the server does not support the ID command, this will perform no operation.
*
* Interoperability hack: Never send ID to *.secureserver.net, which sends back a
* malformed response that our parser can't deal with.
*/
private void doSendId(boolean hasIdCapability, String capabilities)
throws MessagingException {
if (!hasIdCapability) return;
// Never send ID to *.secureserver.net
String host = mRootTransport.getHost();
if (host.toLowerCase().endsWith(".secureserver.net")) return;
// Assign user-agent string (for RFC2971 ID command)
String mUserAgent = getImapId(mContext, mUsername, host, capabilities);
if (mUserAgent != null) {
mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
} else if (DEBUG_FORCE_SEND_ID) {
mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
}
// else: mIdPhrase = null, no ID will be emitted
// Send user-agent in an RFC2971 ID command
if (mIdPhrase != null) {
try {
executeSimpleCommand(mIdPhrase);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
// A true IOException will recur on the following login steps
// This can go away after the parser is fixed - see bug 2138981
}
}
}
/**
* Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
* explicitly sets a namespace (using setup UI) or if the server does not support the
* namespace command, this will perform no operation.
*/
private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
// user did not specify a hard-coded prefix; try to get it from the server
if (hasNamespaceCapability && TextUtils.isEmpty(mPathPrefix)) {
List<ImapResponse> responseList = Collections.emptyList();
try {
responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
}
for (ImapResponse response: responseList) {
if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
ImapList namespaceList = response.getListOrEmpty(1);
ImapList namespace = namespaceList.getListOrEmpty(0);
String namespaceString = namespace.getStringOrEmpty(0).getString();
if (!TextUtils.isEmpty(namespaceString)) {
mPathPrefix = decodeFolderName(namespaceString, null);
mPathSeparator = namespace.getStringOrEmpty(1).getString();
}
}
}
}
}
/**
* Logs into the IMAP server
*/
private void doLogin()
throws IOException, MessagingException, AuthenticationFailedException {
try {
// TODO eventually we need to add additional authentication
// options such as SASL
executeSimpleCommand(mLoginPhrase, true);
} catch (ImapException ie) {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
throw new AuthenticationFailedException(ie.getAlertText(), ie);
} catch (MessagingException me) {
throw new AuthenticationFailedException(null, me);
}
}
/**
* Gets the path separator per the LIST command in RFC 3501. If the path separator
* was obtained while obtaining the namespace or there is no prefix defined, this
* will perform no operation.
*/
private void doGetPathSeparator() throws MessagingException {
// user did not specify a hard-coded prefix; try to get it from the server
if (TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix)) {
List<ImapResponse> responseList = Collections.emptyList();
try {
responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
}
for (ImapResponse response: responseList) {
if (response.isDataResponse(0, ImapConstants.LIST)) {
mPathSeparator = response.getStringOrEmpty(2).getString();
}
}
}
}
/**
* Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
* to use TLS or the server does not support the TLS capability, this will perform
* no operation.
*/
private ImapResponse doStartTls(boolean hasStartTlsCapability)
throws IOException, MessagingException {
if (mTransport.canTryTlsSecurity()) {
if (hasStartTlsCapability) {
// STARTTLS
executeSimpleCommand(ImapConstants.STARTTLS);
mTransport.reopenTls();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
createParser();
// Per RFC requirement (3501-6.2.1) gather new capabilities
return(queryCapabilities());
} else {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
}
}
return null;
}
/** @see DiscourseLogger#logLastDiscourse() */
public void logLastDiscourse() {
mDiscourse.logLastDiscourse();
}
}
static class ImapMessage extends MimeMessage {
ImapMessage(String uid, ImapFolder folder) {
this.mUid = uid;

View File

@ -79,11 +79,12 @@ public class MailTransport implements Transport {
}
/**
* Get a new transport, using an existing one as a model. The new transport is configured as if
* setUri() and setSecurity() have been called, but not opened or connected in any way.
* @return a new Transport ready to open()
* Returns a new transport, using the current transport as a model. The new transport is
* configured identically (as if {@link #setSecurity(int, boolean)}, {@link #setPort(int)}
* and {@link #setHost(String)} were invoked), but not opened or connected in any way.
*/
public Transport newInstanceWithConfiguration() {
@Override
public Transport clone() {
MailTransport newObject = new MailTransport(mDebugLabel);
newObject.mDebugLabel = mDebugLabel;
@ -107,31 +108,38 @@ public class MailTransport implements Transport {
mPort = port;
}
@Override
public String getHost() {
return mHost;
}
@Override
public int getPort() {
return mPort;
}
@Override
public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {
mConnectionSecurity = connectionSecurity;
mTrustCertificates = trustAllCertificates;
}
@Override
public int getSecurity() {
return mConnectionSecurity;
}
@Override
public boolean canTrySslSecurity() {
return mConnectionSecurity == CONNECTION_SECURITY_SSL;
}
@Override
public boolean canTryTlsSecurity() {
return mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS;
}
@Override
public boolean canTrustAllCertificates() {
return mTrustCertificates;
}
@ -140,6 +148,7 @@ public class MailTransport implements Transport {
* Attempts to open a connection using the Uri supplied for connection parameters. Will attempt
* an SSL connection if indicated.
*/
@Override
public void open() throws MessagingException, CertificateValidationException {
if (Email.DEBUG) {
Log.d(Logging.LOG_TAG, "*** " + mDebugLabel + " open " +
@ -182,6 +191,7 @@ public class MailTransport implements Transport {
*
* TODO should we explicitly close the old socket? This seems funky to abandon it.
*/
@Override
public void reopenTls() throws MessagingException {
try {
mSocket = SSLUtils.getSSLSocketFactory(canTrustAllCertificates())
@ -246,10 +256,12 @@ public class MailTransport implements Transport {
* @param timeoutMilliseconds the read timeout value if greater than {@code 0}, or
* {@code 0} for an infinite timeout.
*/
@Override
public void setSoTimeout(int timeoutMilliseconds) throws SocketException {
mSocket.setSoTimeout(timeoutMilliseconds);
}
@Override
public boolean isOpen() {
return (mIn != null && mOut != null &&
mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
@ -258,6 +270,7 @@ public class MailTransport implements Transport {
/**
* Close the connection. MUST NOT return any exceptions - must be "best effort" and safe.
*/
@Override
public void close() {
try {
mIn.close();
@ -279,10 +292,12 @@ public class MailTransport implements Transport {
mSocket = null;
}
@Override
public InputStream getInputStream() {
return mIn;
}
@Override
public OutputStream getOutputStream() {
return mOut;
}
@ -290,6 +305,7 @@ public class MailTransport implements Transport {
/**
* Writes a single line to the server using \r\n termination.
*/
@Override
public void writeLine(String s, String sensitiveReplacement) throws IOException {
if (Email.DEBUG) {
if (sensitiveReplacement != null && !Logging.DEBUG_SENSITIVE) {
@ -310,6 +326,7 @@ public class MailTransport implements Transport {
* 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.
*/
@Override
public String readLine() throws IOException {
StringBuffer sb = new StringBuffer();
InputStream in = getInputStream();
@ -333,6 +350,7 @@ public class MailTransport implements Transport {
return ret;
}
@Override
public InetAddress getLocalAddress() {
if (isOpen()) {
return mSocket.getLocalAddress();

View File

@ -21,7 +21,6 @@ import com.android.email.MockSharedPreferences;
import com.android.email.MockVendorPolicy;
import com.android.email.VendorPolicyLoader;
import com.android.email.mail.Transport;
import com.android.email.mail.store.ImapStore.ImapConnection;
import com.android.email.mail.store.ImapStore.ImapMessage;
import com.android.email.mail.store.imap.ImapResponse;
import com.android.email.mail.store.imap.ImapTestUtils;
@ -97,7 +96,8 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
private ImapFolder mFolder = null;
private Context mTestContext;
private int mNextTag;
/** The tag for the current IMAP command; used for mock transport responses */
private int mTag;
// Fields specific to the CopyMessages tests
private MockTransport mCopyMock;
private Folder mCopyToFolder;
@ -156,7 +156,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
testAccount.mHostAuthRecv = testAuth;
mStore = (ImapStore) ImapStore.newInstance(testAccount, mTestContext, null);
mFolder = (ImapFolder) mStore.getFolder(FOLDER_NAME);
mNextTag = 1;
resetTag();
}
public void testJoinMessageUids() throws Exception {
@ -571,7 +571,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
MockTransport mockTransport = new MockTransport();
mockTransport.setSecurity(connectionSecurity, trustAllCertificates);
mockTransport.setHost("mock.server.com");
mStore.setTransport(mockTransport);
mStore.setTransportForTest(mockTransport);
return mockTransport;
}
@ -702,8 +702,22 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
* @return a string containing the current tag
*/
public String getNextTag(boolean advance) {
if (advance) ++mNextTag;
return Integer.toString(mNextTag);
if (advance) ++mTag;
return Integer.toString(mTag);
}
/**
* Resets the tag back to it's starting value. Do this after the test connection has been
* closed.
*/
private int resetTag() {
return resetTag(1);
}
private int resetTag(int tag) {
int oldTag = mTag;
mTag = tag;
return oldTag;
}
/**
@ -1676,7 +1690,8 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
new String[] {
getNextTag(true) + " oK UID COPY completed",
});
// New connection, so, we need to login again
// New connection, so, we need to login again & the tag count gets reset
int saveTag = resetTag();
expectLogin(mCopyMock, new String[] {"* iD nIL", "oK"}, false);
// Select destination folder
expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
@ -1691,6 +1706,8 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
"* sEaRcH 1818",
getNextTag(true) + " oK UID SEARCH completed (1 msgs in 2.71828 secs)",
});
// Resume commands on the initial connection
resetTag(saveTag);
// Select the original folder
expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");
@ -1708,7 +1725,8 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
new String[] {
getNextTag(true) + " oK UID COPY completed",
});
// New connection, so, we need to login again
// New connection, so, we need to login again & the tag count gets reset
int saveTag = resetTag();
expectLogin(mCopyMock, new String[] {"* iD nIL", "oK"}, false);
// Select destination folder
expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
@ -1723,6 +1741,8 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
"* sEaRcH",
getNextTag(true) + " oK UID SEARCH completed (0 msgs in 2.99792 secs)",
});
// Resume commands on the initial connection
resetTag(saveTag);
// Select the original folder
expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");
@ -1740,7 +1760,8 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
new String[] {
getNextTag(true) + " oK UID COPY completed",
});
// New connection, so, we need to login again
// New connection, so, we need to login again & the tag count gets reset
int saveTag = resetTag();
expectLogin(mCopyMock, new String[] {"* iD nIL", "oK"}, false);
// Select destination folder
expectSelect(mCopyMock, "&ZeVnLIqe-", "rEAD-wRITE");
@ -1753,6 +1774,8 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
new String[] {
getNextTag(true) + " BaD search failed"
});
// Resume commands on the initial connection
resetTag(saveTag);
// Select the original folder
expectSelect(mCopyMock, FOLDER_ENCODED, "rEAD-wRITE");
@ -2004,6 +2027,9 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
// con1 != con2
assertNotSame(con1, con2);
// New connection, so, we need to login again & the tag count gets reset
int saveTag = resetTag();
// Open con2
expectLogin(mock);
con2.open();
@ -2016,6 +2042,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
assertEquals(1, mStore.getConnectionPoolForTest().size());
// Get another connection. Should get con1, after verifying the connection.
saveTag = resetTag(saveTag);
mock.expect(getNextTag(false) + " NOOP", new String[] {getNextTag(true) + " oK success"});
final ImapConnection con1b = mStore.getConnection();
@ -2027,6 +2054,9 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
mStore.poolConnection(con2);
assertEquals(1, mStore.getConnectionPoolForTest().size());
// Resume con2 tags ...
resetTag(saveTag);
// Try to get connection, but this time, connection gets closed.
mock.expect(getNextTag(false) + " NOOP", new String[] {getNextTag(true) + "* bYE bye"});
final ImapConnection con3 = mStore.getConnection();
@ -2044,6 +2074,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
expectLogin(mock);
mStore.checkSettings();
resetTag();
expectLogin(mock, false, false, false,
new String[] {"* iD nIL", "oK"}, "nO authentication failed");
try {
@ -2203,6 +2234,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
*/
public void testFetchIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
@Override
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
@ -2220,6 +2252,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
*/
public void testUnreadMessageCountIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
@Override
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
@ -2233,6 +2266,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
*/
public void testCopyMessagesIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
@Override
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
@ -2249,6 +2283,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
*/
public void testSearchForUidsIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
@Override
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
@ -2262,6 +2297,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
*/
public void testExpungeIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
@Override
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
@ -2275,6 +2311,7 @@ public class ImapStoreUnitTests extends InstrumentationTestCase {
*/
public void testOpenIOException() throws Exception {
runAndExpectMessagingException(new RunAndExpectMessagingExceptionTarget() {
@Override
public void run(MockTransport mockTransport) throws Exception {
mockTransport.expectIOException();
final Folder folder = mStore.getFolder("test");

View File

@ -192,6 +192,7 @@ public class MockTransport implements Transport {
mInputOpen = false;
}
@Override
public void close() {
mOpen = false;
mInputOpen = false;
@ -232,7 +233,7 @@ public class MockTransport implements Transport {
* don't have to worry about dealing with test metadata like the expects list or socket state.
*/
@Override
public Transport newInstanceWithConfiguration() {
public Transport clone() {
return this;
}
@ -322,6 +323,7 @@ public class MockTransport implements Transport {
mTrustCertificates = trustAllCertificates;
}
@Override
public void setSoTimeout(int timeoutMilliseconds) /* throws SocketException */ {
}