From b36c788ce0a2105aa70717e0f7775638970b75bf Mon Sep 17 00:00:00 2001 From: Jan Berkel Date: Wed, 17 Dec 2014 17:16:18 +0100 Subject: [PATCH] Move ImapConnection out --- src/com/fsck/k9/mail/store/ImapCommands.java | 16 + .../fsck/k9/mail/store/ImapConnection.java | 650 ++++++++++++++++++ src/com/fsck/k9/mail/store/ImapStore.java | 650 +----------------- .../k9/mail/transport/imap/ImapSettings.java | 3 +- 4 files changed, 673 insertions(+), 646 deletions(-) create mode 100644 src/com/fsck/k9/mail/store/ImapCommands.java create mode 100644 src/com/fsck/k9/mail/store/ImapConnection.java diff --git a/src/com/fsck/k9/mail/store/ImapCommands.java b/src/com/fsck/k9/mail/store/ImapCommands.java new file mode 100644 index 000000000..ea986d1b4 --- /dev/null +++ b/src/com/fsck/k9/mail/store/ImapCommands.java @@ -0,0 +1,16 @@ +package com.fsck.k9.mail.store; + +class ImapCommands { + static final String COMMAND_IDLE = "IDLE"; + static final String CAPABILITY_IDLE = "IDLE"; + static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5"; + static final String CAPABILITY_AUTH_PLAIN = "AUTH=PLAIN"; + static final String CAPABILITY_AUTH_EXTERNAL = "AUTH=EXTERNAL"; + static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED"; + static final String CAPABILITY_NAMESPACE = "NAMESPACE"; + static final String COMMAND_NAMESPACE = "NAMESPACE"; + static final String CAPABILITY_CAPABILITY = "CAPABILITY"; + static final String COMMAND_CAPABILITY = "CAPABILITY"; + static final String CAPABILITY_COMPRESS_DEFLATE = "COMPRESS=DEFLATE"; + static final String COMMAND_COMPRESS_DEFLATE = "COMPRESS DEFLATE"; +} diff --git a/src/com/fsck/k9/mail/store/ImapConnection.java b/src/com/fsck/k9/mail/store/ImapConnection.java new file mode 100644 index 000000000..238f2d0d0 --- /dev/null +++ b/src/com/fsck/k9/mail/store/ImapConnection.java @@ -0,0 +1,650 @@ +package com.fsck.k9.mail.store; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import com.fsck.k9.K9; +import com.fsck.k9.mail.Authentication; +import com.fsck.k9.mail.AuthenticationFailedException; +import com.fsck.k9.mail.CertificateValidationException; +import com.fsck.k9.mail.ConnectionSecurity; +import com.fsck.k9.mail.K9MailLib; +import com.fsck.k9.mail.MessagingException; +import com.fsck.k9.mail.filter.Base64; +import com.fsck.k9.mail.filter.PeekableInputStream; +import com.fsck.k9.mail.ssl.TrustedSocketFactory; +import com.fsck.k9.mail.transport.imap.ImapSettings; +import com.jcraft.jzlib.JZlib; +import com.jcraft.jzlib.ZOutputStream; + +import org.apache.commons.io.IOUtils; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.security.GeneralSecurityException; +import java.security.Security; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import javax.net.ssl.SSLException; + +import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_IMAP; +import static com.fsck.k9.mail.K9MailLib.LOG_TAG; + +/** + * A cacheable class that stores the details for a single IMAP connection. + */ +class ImapConnection { + private final TrustedSocketFactory socketFactory; + private Socket mSocket; + private PeekableInputStream mIn; + private OutputStream mOut; + private ImapResponseParser mParser; + private int mNextCommandTag; + private Set capabilities = new HashSet(); + + private ImapSettings mSettings; + + public ImapConnection(final ImapSettings settings, TrustedSocketFactory socketFactory) { + this.mSettings = settings; + this.socketFactory = socketFactory; + } + + public Set getCapabilities() { + return capabilities; + } + + public OutputStream getOutputStream() { + return mOut; + } + + protected String getLogId() { + return "conn" + hashCode(); + } + + private List receiveCapabilities(List responses) { + for (ImapResponseParser.ImapResponse response : responses) { + ImapResponseParser.ImapList capabilityList = null; + if (!response.isEmpty() && ImapResponseParser.equalsIgnoreCase(response.get(0), "OK")) { + for (Object thisPart : response) { + if (thisPart instanceof ImapResponseParser.ImapList) { + ImapResponseParser.ImapList thisList = (ImapResponseParser.ImapList)thisPart; + if (ImapResponseParser.equalsIgnoreCase(thisList.get(0), ImapCommands.CAPABILITY_CAPABILITY)) { + capabilityList = thisList; + break; + } + } + } + } else if (response.mTag == null) { + capabilityList = response; + } + + if (capabilityList != null && !capabilityList.isEmpty() && + ImapResponseParser.equalsIgnoreCase(capabilityList.get(0), ImapCommands.CAPABILITY_CAPABILITY)) { + if (K9MailLib.isDebug()) { + Log.d(LOG_TAG, "Saving " + capabilityList.size() + " capabilities for " + getLogId()); + } + for (Object capability : capabilityList) { + if (capability instanceof String) { +// if (K9MailLib.isDebug()) +// { +// Log.v(LOG_TAG, "Saving capability '" + capability + "' for " + getLogId()); +// } + capabilities.add(((String)capability).toUpperCase(Locale.US)); + } + } + } + } + return responses; + } + + public void open() throws IOException, MessagingException { + if (isOpen()) { + return; + } + + boolean authSuccess = false; + + mNextCommandTag = 1; + try { + Security.setProperty("networkaddress.cache.ttl", "0"); + } catch (Exception e) { + Log.w(LOG_TAG, "Could not set DNS ttl to 0 for " + getLogId(), e); + } + + + try { + Security.setProperty("networkaddress.cache.negative.ttl", "0"); + } catch (Exception e) { + Log.w(LOG_TAG, "Could not set DNS negative ttl to 0 for " + getLogId(), e); + } + + try { + ConnectionSecurity connectionSecurity = mSettings.getConnectionSecurity(); + + // Try all IPv4 and IPv6 addresses of the host + InetAddress[] addresses = InetAddress.getAllByName(mSettings.getHost()); + for (int i = 0; i < addresses.length; i++) { + try { + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { + Log.d(LOG_TAG, "Connecting to " + mSettings.getHost() + " as " + + addresses[i]); + } + + SocketAddress socketAddress = new InetSocketAddress(addresses[i], + mSettings.getPort()); + + if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { + mSocket = socketFactory.createSocket( + null, + mSettings.getHost(), + mSettings.getPort(), + mSettings.getClientCertificateAlias()); + } else { + mSocket = new Socket(); + } + + mSocket.connect(socketAddress, RemoteStore.SOCKET_CONNECT_TIMEOUT); + + // Successfully connected to the server; don't try any other addresses + break; + } catch (SocketException e) { + if (i < (addresses.length - 1)) { + // There are still other addresses for that host to try + continue; + } + throw new MessagingException("Cannot connect to host", e); + } + } + + setReadTimeout(RemoteStore.SOCKET_READ_TIMEOUT); + + mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), + 1024)); + mParser = new ImapResponseParser(mIn); + mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024); + + capabilities.clear(); + ImapResponseParser.ImapResponse nullResponse = mParser.readResponse(); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) + Log.v(LOG_TAG, getLogId() + "<<<" + nullResponse); + + List nullResponses = new LinkedList(); + nullResponses.add(nullResponse); + receiveCapabilities(nullResponses); + + if (!hasCapability(ImapCommands.CAPABILITY_CAPABILITY)) { + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Did not get capabilities in banner, requesting CAPABILITY for " + getLogId()); + List responses = receiveCapabilities(executeSimpleCommand(ImapCommands.COMMAND_CAPABILITY)); + if (responses.size() != 2) { + throw new MessagingException("Invalid CAPABILITY response received"); + } + } + + if (mSettings.getConnectionSecurity() == ConnectionSecurity.STARTTLS_REQUIRED) { + + if (hasCapability("STARTTLS")) { + // STARTTLS + executeSimpleCommand("STARTTLS"); + + mSocket = socketFactory.createSocket( + mSocket, + mSettings.getHost(), + mSettings.getPort(), + mSettings.getClientCertificateAlias()); + mSocket.setSoTimeout(RemoteStore.SOCKET_READ_TIMEOUT); + mIn = new PeekableInputStream(new BufferedInputStream(mSocket + .getInputStream(), 1024)); + mParser = new ImapResponseParser(mIn); + mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024); + // Per RFC 2595 (3.1): Once TLS has been started, reissue CAPABILITY command + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "Updating capabilities after STARTTLS for " + getLogId()); + capabilities.clear(); + List responses = receiveCapabilities(executeSimpleCommand(ImapCommands.COMMAND_CAPABILITY)); + if (responses.size() != 2) { + throw new MessagingException("Invalid CAPABILITY response received"); + } + } else { + /* + * This exception triggers a "Certificate error" + * notification that takes the user to the incoming + * server settings for review. This might be needed if + * the account was configured with an obsolete + * "STARTTLS (if available)" setting. + */ + throw new CertificateValidationException( + "STARTTLS connection security not available"); + } + } + + switch (mSettings.getAuthType()) { + case CRAM_MD5: + if (hasCapability(ImapCommands.CAPABILITY_AUTH_CRAM_MD5)) { + authCramMD5(); + } else { + throw new MessagingException( + "Server doesn't support encrypted passwords using CRAM-MD5."); + } + break; + + case PLAIN: + if (hasCapability(ImapCommands.CAPABILITY_AUTH_PLAIN)) { + saslAuthPlain(); + } else if (!hasCapability(ImapCommands.CAPABILITY_LOGINDISABLED)) { + login(); + } else { + throw new MessagingException( + "Server doesn't support unencrypted passwords using AUTH=PLAIN and LOGIN is disabled."); + } + break; + + case EXTERNAL: + if (hasCapability(ImapCommands.CAPABILITY_AUTH_EXTERNAL)) { + saslAuthExternal(); + } else { + // Provide notification to user of a problem authenticating using client certificates + throw new CertificateValidationException(CertificateValidationException.Reason.MissingCapability); + } + break; + + default: + throw new MessagingException( + "Unhandled authentication method found in the server settings (bug)."); + } + authSuccess = true; + if (K9MailLib.isDebug()) { + Log.d(LOG_TAG, ImapCommands.CAPABILITY_COMPRESS_DEFLATE + " = " + hasCapability(ImapCommands.CAPABILITY_COMPRESS_DEFLATE)); + } + if (hasCapability(ImapCommands.CAPABILITY_COMPRESS_DEFLATE)) { + ConnectivityManager connectivityManager = (ConnectivityManager) K9.app.getSystemService(Context.CONNECTIVITY_SERVICE); + boolean useCompression = true; + + NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo(); + if (netInfo != null) { + int type = netInfo.getType(); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "On network type " + type); + useCompression = mSettings.useCompression(type); + + } + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "useCompression " + useCompression); + if (useCompression) { + try { + executeSimpleCommand(ImapCommands.COMMAND_COMPRESS_DEFLATE); + Inflater inf = new Inflater(true); + InflaterInputStream zInputStream = new InflaterInputStream(mSocket.getInputStream(), inf); + mIn = new PeekableInputStream(new BufferedInputStream(zInputStream, 1024)); + mParser = new ImapResponseParser(mIn); + ZOutputStream zOutputStream = new ZOutputStream(mSocket.getOutputStream(), JZlib.Z_BEST_SPEED, true); + mOut = new BufferedOutputStream(zOutputStream, 1024); + zOutputStream.setFlushMode(JZlib.Z_PARTIAL_FLUSH); + if (K9MailLib.isDebug()) { + Log.i(LOG_TAG, "Compression enabled for " + getLogId()); + } + } catch (Exception e) { + Log.e(LOG_TAG, "Unable to negotiate compression", e); + } + } + } + + + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "NAMESPACE = " + hasCapability(ImapCommands.CAPABILITY_NAMESPACE) + + ", mPathPrefix = " + mSettings.getPathPrefix()); + + if (mSettings.getPathPrefix() == null) { + if (hasCapability(ImapCommands.CAPABILITY_NAMESPACE)) { + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "mPathPrefix is unset and server has NAMESPACE capability"); + List namespaceResponses = + executeSimpleCommand(ImapCommands.COMMAND_NAMESPACE); + for (ImapResponseParser.ImapResponse response : namespaceResponses) { + if (ImapResponseParser.equalsIgnoreCase(response.get(0), ImapCommands.COMMAND_NAMESPACE)) { + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got NAMESPACE response " + response + " on " + getLogId()); + + Object personalNamespaces = response.get(1); + if (personalNamespaces != null && personalNamespaces instanceof ImapResponseParser.ImapList) { + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got personal namespaces: " + personalNamespaces); + ImapResponseParser.ImapList bracketed = (ImapResponseParser.ImapList)personalNamespaces; + Object firstNamespace = bracketed.get(0); + if (firstNamespace != null && firstNamespace instanceof ImapResponseParser.ImapList) { + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got first personal namespaces: " + firstNamespace); + bracketed = (ImapResponseParser.ImapList)firstNamespace; + mSettings.setPathPrefix(bracketed.getString(0)); + mSettings.setPathDelimeter(bracketed.getString(1)); + mSettings.setCombinedPrefix(null); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got path '" + mSettings.getPathPrefix() + "' and separator '" + mSettings.getPathDelimeter() + "'"); + } + } + } + } + } else { + if (K9MailLib.isDebug()) + Log.i(LOG_TAG, "mPathPrefix is unset but server does not have NAMESPACE capability"); + mSettings.setPathPrefix(""); + } + } + if (mSettings.getPathDelimeter() == null) { + try { + List nameResponses = + executeSimpleCommand("LIST \"\" \"\""); + for (ImapResponseParser.ImapResponse response : nameResponses) { + if (ImapResponseParser.equalsIgnoreCase(response.get(0), "LIST")) { + mSettings.setPathDelimeter(response.getString(2)); + mSettings.setCombinedPrefix(null); + if (K9MailLib.isDebug()) + Log.d(LOG_TAG, "Got path delimeter '" + mSettings.getPathDelimeter() + "' for " + getLogId()); + } + } + } catch (Exception e) { + Log.e(LOG_TAG, "Unable to get path delimeter using LIST", e); + } + } + + } catch (SSLException e) { + if (e.getCause() instanceof CertificateException) { + throw new CertificateValidationException(e.getMessage(), e); + } else { + throw e; + } + } catch (GeneralSecurityException gse) { + throw new MessagingException( + "Unable to open connection to IMAP server due to security error.", gse); + } catch (ConnectException ce) { + String ceMess = ce.getMessage(); + String[] tokens = ceMess.split("-"); + if (tokens != null && tokens.length > 1 && tokens[1] != null) { + Log.e(LOG_TAG, "Stripping host/port from ConnectionException for " + getLogId(), ce); + throw new ConnectException(tokens[1].trim()); + } else { + throw ce; + } + } finally { + if (!authSuccess) { + Log.e(LOG_TAG, "Failed to login, closing connection for " + getLogId()); + close(); + } + } + } + + protected void login() throws IOException, MessagingException { + /* + * Use quoted strings which permit spaces and quotes. (Using IMAP + * string literals would be better, but some servers are broken + * and don't parse them correctly.) + */ + + // escape double-quotes and backslash characters with a backslash + Pattern p = Pattern.compile("[\\\\\"]"); + String replacement = "\\\\$0"; + String username = p.matcher(mSettings.getUsername()).replaceAll( + replacement); + String password = p.matcher(mSettings.getPassword()).replaceAll( + replacement); + try { + receiveCapabilities(executeSimpleCommand( + String.format("LOGIN \"%s\" \"%s\"", username, password), true)); + } catch (ImapStore.ImapException e) { + throw new AuthenticationFailedException(e.getMessage()); + } + } + + protected void authCramMD5() throws MessagingException, IOException { + String command = "AUTHENTICATE CRAM-MD5"; + String tag = sendCommand(command, false); + ImapResponseParser.ImapResponse response = readContinuationResponse(tag); + if (response.size() != 1 || !(response.get(0) instanceof String)) { + throw new MessagingException("Invalid Cram-MD5 nonce received"); + } + byte[] b64Nonce = response.getString(0).getBytes(); + byte[] b64CRAM = Authentication.computeCramMd5Bytes( + mSettings.getUsername(), mSettings.getPassword(), b64Nonce); + + mOut.write(b64CRAM); + mOut.write('\r'); + mOut.write('\n'); + mOut.flush(); + try { + receiveCapabilities(readStatusResponse(tag, command, null)); + } catch (MessagingException e) { + throw new AuthenticationFailedException(e.getMessage()); + } + } + + protected void saslAuthPlain() throws IOException, MessagingException { + String command = "AUTHENTICATE PLAIN"; + String tag = sendCommand(command, false); + readContinuationResponse(tag); + mOut.write(Base64.encodeBase64(("\000" + mSettings.getUsername() + + "\000" + mSettings.getPassword()).getBytes())); + mOut.write('\r'); + mOut.write('\n'); + mOut.flush(); + try { + receiveCapabilities(readStatusResponse(tag, command, null)); + } catch (MessagingException e) { + throw new AuthenticationFailedException(e.getMessage()); + } + } + + private void saslAuthExternal() throws IOException, MessagingException { + try { + receiveCapabilities(executeSimpleCommand( + String.format("AUTHENTICATE EXTERNAL %s", + Base64.encode(mSettings.getUsername())), false)); + } catch (ImapStore.ImapException e) { + /* + * Provide notification to the user of a problem authenticating + * using client certificates. We don't use an + * AuthenticationFailedException because that would trigger a + * "Username or password incorrect" notification in + * AccountSetupCheckSettings. + */ + throw new CertificateValidationException(e.getMessage()); + } + } + + protected ImapResponseParser.ImapResponse readContinuationResponse(String tag) + throws IOException, MessagingException { + ImapResponseParser.ImapResponse response; + do { + response = readResponse(); + if (response.mTag != null) { + if (response.mTag.equalsIgnoreCase(tag)) { + throw new MessagingException( + "Command continuation aborted: " + response); + } else { + Log.w(LOG_TAG, "After sending tag " + tag + + ", got tag response from previous command " + + response + " for " + getLogId()); + } + } + } while (!response.mCommandContinuationRequested); + return response; + } + + protected List readStatusResponse(String tag, + String commandToLog, ImapStore.UntaggedHandler untaggedHandler) + throws IOException, MessagingException { + List responses = new ArrayList(); + ImapResponseParser.ImapResponse response; + do { + response = mParser.readResponse(); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) + Log.v(LOG_TAG, getLogId() + "<<<" + response); + + if (response.mTag != null && !response.mTag.equalsIgnoreCase(tag)) { + Log.w(LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response + " for " + getLogId()); + Iterator iter = responses.iterator(); + while (iter.hasNext()) { + ImapResponseParser.ImapResponse delResponse = iter.next(); + if (delResponse.mTag != null || delResponse.size() < 2 + || (!ImapResponseParser.equalsIgnoreCase(delResponse.get(1), "EXISTS") && !ImapResponseParser.equalsIgnoreCase(delResponse.get(1), "EXPUNGE"))) { + iter.remove(); + } + } + response.mTag = null; + continue; + } + if (untaggedHandler != null) { + untaggedHandler.handleAsyncUntaggedResponse(response); + } + responses.add(response); + } while (response.mTag == null); + if (response.size() < 1 || !ImapResponseParser.equalsIgnoreCase(response.get(0), "OK")) { + throw new ImapStore.ImapException("Command: " + commandToLog + "; response: " + response.toString(), response.getAlertText()); + } + return responses; + } + + protected void setReadTimeout(int millis) throws SocketException { + Socket sock = mSocket; + if (sock != null) { + sock.setSoTimeout(millis); + } + } + + protected boolean isIdleCapable() { + if (K9MailLib.isDebug()) + Log.v(LOG_TAG, "Connection " + getLogId() + " has " + capabilities.size() + " capabilities"); + + return capabilities.contains(ImapCommands.CAPABILITY_IDLE); + } + + protected boolean hasCapability(String capability) { + return capabilities.contains(capability.toUpperCase(Locale.US)); + } + + public boolean isOpen() { + return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); + } + + public void close() { +// if (isOpen()) { +// try { +// executeSimpleCommand("LOGOUT"); +// } catch (Exception e) { +// +// } +// } + IOUtils.closeQuietly(mIn); + IOUtils.closeQuietly(mOut); + IOUtils.closeQuietly(mSocket); + mIn = null; + mOut = null; + mSocket = null; + } + + public ImapResponseParser.ImapResponse readResponse() throws IOException, MessagingException { + return readResponse(null); + } + + public ImapResponseParser.ImapResponse readResponse(ImapResponseParser.IImapResponseCallback callback) throws IOException { + try { + ImapResponseParser.ImapResponse response = mParser.readResponse(callback); + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) + Log.v(LOG_TAG, getLogId() + "<<<" + response); + + return response; + } catch (IOException ioe) { + close(); + throw ioe; + } + } + + public void sendContinuation(String continuation) throws IOException { + mOut.write(continuation.getBytes()); + mOut.write('\r'); + mOut.write('\n'); + mOut.flush(); + + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) + Log.v(LOG_TAG, getLogId() + ">>> " + continuation); + + } + + public String sendCommand(String command, boolean sensitive) + throws MessagingException, IOException { + try { + open(); + String tag = Integer.toString(mNextCommandTag++); + String commandToSend = tag + " " + command + "\r\n"; + mOut.write(commandToSend.getBytes()); + mOut.flush(); + + if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) { + if (sensitive && !K9MailLib.isDebugSensitive()) { + Log.v(LOG_TAG, getLogId() + ">>> " + + "[Command Hidden, Enable Sensitive Debug Logging To Show]"); + } else { + Log.v(LOG_TAG, getLogId() + ">>> " + commandToSend); + } + } + + return tag; + } catch (IOException ioe) { + close(); + throw ioe; + } catch (ImapStore.ImapException ie) { + close(); + throw ie; + } catch (MessagingException me) { + close(); + throw me; + } + } + + public List executeSimpleCommand(String command) throws IOException, + MessagingException { + return executeSimpleCommand(command, false, null); + } + + public List executeSimpleCommand(String command, boolean sensitive) throws IOException, + MessagingException { + return executeSimpleCommand(command, sensitive, null); + } + + public List executeSimpleCommand(String command, boolean sensitive, ImapStore.UntaggedHandler untaggedHandler) + throws IOException, MessagingException { + String commandToLog = command; + if (sensitive && !K9MailLib.isDebugSensitive()) { + commandToLog = "*sensitive*"; + } + + + //if (K9MailLib.isDebug()) + // Log.v(LOG_TAG, "Sending IMAP command " + commandToLog + " on connection " + getLogId()); + + String tag = sendCommand(command, sensitive); + //if (K9MailLib.isDebug()) + // Log.v(LOG_TAG, "Sent IMAP command " + commandToLog + " with tag " + tag + " for " + getLogId()); + + return readStatusResponse(tag, commandToLog, untaggedHandler); + } +} diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java index 52e415fcb..06f6d2d03 100644 --- a/src/com/fsck/k9/mail/store/ImapStore.java +++ b/src/com/fsck/k9/mail/store/ImapStore.java @@ -1,18 +1,9 @@ package com.fsck.k9.mail.store; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.net.ConnectException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; @@ -21,9 +12,6 @@ import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CodingErrorAction; -import java.security.GeneralSecurityException; -import java.security.Security; -import java.security.cert.CertificateException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; @@ -43,26 +31,15 @@ import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Pattern; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; -import javax.net.ssl.SSLException; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import android.os.PowerManager; import android.text.TextUtils; import android.util.Log; -import com.fsck.k9.K9; import com.fsck.k9.helper.power.TracingPowerManager; import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import com.fsck.k9.mail.AuthType; -import com.fsck.k9.mail.Authentication; -import com.fsck.k9.mail.AuthenticationFailedException; import com.fsck.k9.mail.Body; -import com.fsck.k9.mail.CertificateValidationException; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Flag; @@ -75,29 +52,21 @@ import com.fsck.k9.mail.Part; import com.fsck.k9.mail.PushReceiver; import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.ServerSettings; -import com.fsck.k9.mail.filter.Base64; import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.FixedLengthInputStream; -import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.internet.MimeBodyPart; import com.fsck.k9.mail.internet.MimeHeader; import com.fsck.k9.mail.internet.MimeMessage; import com.fsck.k9.mail.internet.MimeMultipart; import com.fsck.k9.mail.internet.MimeUtility; -import com.fsck.k9.mail.ssl.TrustedSocketFactory; import com.fsck.k9.mail.store.ImapResponseParser.ImapList; import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse; import com.fsck.k9.mail.transport.imap.ImapSettings; import com.beetstra.jutf7.CharsetProvider; -import com.jcraft.jzlib.JZlib; -import com.jcraft.jzlib.ZOutputStream; -import org.apache.commons.io.IOUtils; -import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_IMAP; import static com.fsck.k9.mail.K9MailLib.LOG_TAG; import static com.fsck.k9.mail.K9MailLib.PUSH_WAKE_LOCK_TIMEOUT; -import static com.fsck.k9.mail.CertificateValidationException.Reason; /** *
@@ -116,22 +85,6 @@ public class ImapStore extends RemoteStore {
     private static final int FETCH_WINDOW_SIZE = 100;
 
     private Set mPermanentFlagsIndex = EnumSet.noneOf(Flag.class);
-
-    private static final String CAPABILITY_IDLE = "IDLE";
-    private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
-    private static final String CAPABILITY_AUTH_PLAIN = "AUTH=PLAIN";
-    private static final String CAPABILITY_AUTH_EXTERNAL = "AUTH=EXTERNAL";
-    private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED";
-    private static final String COMMAND_IDLE = "IDLE";
-    private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
-    private static final String COMMAND_NAMESPACE = "NAMESPACE";
-
-    private static final String CAPABILITY_CAPABILITY = "CAPABILITY";
-    private static final String COMMAND_CAPABILITY = "CAPABILITY";
-
-    private static final String CAPABILITY_COMPRESS_DEFLATE = "COMPRESS=DEFLATE";
-    private static final String COMMAND_COMPRESS_DEFLATE = "COMPRESS DEFLATE";
-
     private static final String[] EMPTY_STRING_ARRAY = new String[0];
 
     /**
@@ -613,7 +566,7 @@ public class ImapStore extends RemoteStore {
      * Attempt to auto-configure folders by attributes if the server advertises that capability.
      *
      * The parsing here is essentially the same as
-     * {@link #listFolders(com.fsck.k9.mail.store.ImapStore.ImapConnection, boolean)}; we should try to consolidate
+     * {@link #listFolders(ImapConnection, boolean)}; we should try to consolidate
      * this at some point. :(
      * @param connection IMAP Connection
      * @throws IOException uh oh!
@@ -623,10 +576,10 @@ public class ImapStore extends RemoteStore {
         String commandResponse = null;
         String commandOptions = "";
 
-        if (connection.capabilities.contains("XLIST")) {
+        if (connection.getCapabilities().contains("XLIST")) {
             if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration: Using XLIST.");
             commandResponse = "XLIST";
-        } else if(connection.capabilities.contains("SPECIAL-USE")) {
+        } else if(connection.getCapabilities().contains("SPECIAL-USE")) {
             if (K9MailLib.isDebug()) Log.d(LOG_TAG, "Folder auto-configuration: Using RFC6154/SPECIAL-USE.");
             commandResponse = "LIST";
             commandOptions = " (SPECIAL-USE)";
@@ -1980,7 +1933,7 @@ public class ImapStore extends RemoteStore {
                         response = mConnection.readResponse();
                         handleUntaggedResponse(response);
                         if (response.mCommandContinuationRequested) {
-                            EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(mConnection.mOut);
+                            EOLConvertingOutputStream eolOut = new EOLConvertingOutputStream(mConnection.getOutputStream());
                             message.writeTo(eolOut);
                             eolOut.write('\r');
                             eolOut.write('\n');
@@ -2311,597 +2264,6 @@ public class ImapStore extends RemoteStore {
         }
     }
 
-    /**
-     * A cacheable class that stores the details for a single IMAP connection.
-     */
-    public static class ImapConnection {
-        private final TrustedSocketFactory socketFactory;
-        private Socket mSocket;
-        private PeekableInputStream mIn;
-        private OutputStream mOut;
-        private ImapResponseParser mParser;
-        private int mNextCommandTag;
-        private Set capabilities = new HashSet();
-
-        private ImapSettings mSettings;
-
-        public ImapConnection(final ImapSettings settings, TrustedSocketFactory socketFactory) {
-            this.mSettings = settings;
-            this.socketFactory = socketFactory;
-        }
-
-        protected String getLogId() {
-            return "conn" + hashCode();
-        }
-
-        private List receiveCapabilities(List responses) {
-            for (ImapResponse response : responses) {
-                ImapList capabilityList = null;
-                if (!response.isEmpty() && ImapResponseParser.equalsIgnoreCase(response.get(0), "OK")) {
-                    for (Object thisPart : response) {
-                        if (thisPart instanceof ImapList) {
-                            ImapList thisList = (ImapList)thisPart;
-                            if (ImapResponseParser.equalsIgnoreCase(thisList.get(0), CAPABILITY_CAPABILITY)) {
-                                capabilityList = thisList;
-                                break;
-                            }
-                        }
-                    }
-                } else if (response.mTag == null) {
-                    capabilityList = response;
-                }
-
-                if (capabilityList != null && !capabilityList.isEmpty() &&
-                        ImapResponseParser.equalsIgnoreCase(capabilityList.get(0), CAPABILITY_CAPABILITY)) {
-                    if (K9MailLib.isDebug()) {
-                        Log.d(LOG_TAG, "Saving " + capabilityList.size() + " capabilities for " + getLogId());
-                    }
-                    for (Object capability : capabilityList) {
-                        if (capability instanceof String) {
-//                            if (K9MailLib.isDebug())
-//                            {
-//                                Log.v(LOG_TAG, "Saving capability '" + capability + "' for " + getLogId());
-//                            }
-                            capabilities.add(((String)capability).toUpperCase(Locale.US));
-                        }
-                    }
-                }
-            }
-            return responses;
-        }
-
-        public void open() throws IOException, MessagingException {
-            if (isOpen()) {
-                return;
-            }
-
-            boolean authSuccess = false;
-
-            mNextCommandTag = 1;
-            try {
-                Security.setProperty("networkaddress.cache.ttl", "0");
-            } catch (Exception e) {
-                Log.w(LOG_TAG, "Could not set DNS ttl to 0 for " + getLogId(), e);
-            }
-
-
-            try {
-                Security.setProperty("networkaddress.cache.negative.ttl", "0");
-            } catch (Exception e) {
-                Log.w(LOG_TAG, "Could not set DNS negative ttl to 0 for " + getLogId(), e);
-            }
-
-            try {
-                ConnectionSecurity connectionSecurity = mSettings.getConnectionSecurity();
-
-                // Try all IPv4 and IPv6 addresses of the host
-                InetAddress[] addresses = InetAddress.getAllByName(mSettings.getHost());
-                for (int i = 0; i < addresses.length; i++) {
-                    try {
-                        if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) {
-                            Log.d(LOG_TAG, "Connecting to " + mSettings.getHost() + " as " +
-                                    addresses[i]);
-                        }
-
-                        SocketAddress socketAddress = new InetSocketAddress(addresses[i],
-                                mSettings.getPort());
-
-                        if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
-                            mSocket = socketFactory.createSocket(
-                                    null,
-                                    mSettings.getHost(),
-                                    mSettings.getPort(),
-                                    mSettings.getClientCertificateAlias());
-                        } else {
-                            mSocket = new Socket();
-                        }
-
-                        mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
-
-                        // Successfully connected to the server; don't try any other addresses
-                        break;
-                    } catch (SocketException e) {
-                        if (i < (addresses.length - 1)) {
-                            // There are still other addresses for that host to try
-                            continue;
-                        }
-                        throw new MessagingException("Cannot connect to host", e);
-                    }
-                }
-
-                setReadTimeout(SOCKET_READ_TIMEOUT);
-
-                mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
-                                              1024));
-                mParser = new ImapResponseParser(mIn);
-                mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
-
-                capabilities.clear();
-                ImapResponse nullResponse = mParser.readResponse();
-                if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP)
-                    Log.v(LOG_TAG, getLogId() + "<<<" + nullResponse);
-
-                List nullResponses = new LinkedList();
-                nullResponses.add(nullResponse);
-                receiveCapabilities(nullResponses);
-
-                if (!hasCapability(CAPABILITY_CAPABILITY)) {
-                    if (K9MailLib.isDebug())
-                        Log.i(LOG_TAG, "Did not get capabilities in banner, requesting CAPABILITY for " + getLogId());
-                    List responses = receiveCapabilities(executeSimpleCommand(COMMAND_CAPABILITY));
-                    if (responses.size() != 2) {
-                        throw new MessagingException("Invalid CAPABILITY response received");
-                    }
-                }
-
-                if (mSettings.getConnectionSecurity() == ConnectionSecurity.STARTTLS_REQUIRED) {
-
-                    if (hasCapability("STARTTLS")) {
-                        // STARTTLS
-                        executeSimpleCommand("STARTTLS");
-
-                        mSocket = socketFactory.createSocket(
-                                mSocket,
-                                mSettings.getHost(),
-                                mSettings.getPort(),
-                                mSettings.getClientCertificateAlias());
-                        mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
-                        mIn = new PeekableInputStream(new BufferedInputStream(mSocket
-                                                      .getInputStream(), 1024));
-                        mParser = new ImapResponseParser(mIn);
-                        mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
-                        // Per RFC 2595 (3.1):  Once TLS has been started, reissue CAPABILITY command
-                        if (K9MailLib.isDebug())
-                            Log.i(LOG_TAG, "Updating capabilities after STARTTLS for " + getLogId());
-                        capabilities.clear();
-                        List responses = receiveCapabilities(executeSimpleCommand(COMMAND_CAPABILITY));
-                        if (responses.size() != 2) {
-                            throw new MessagingException("Invalid CAPABILITY response received");
-                        }
-                    } else {
-                        /*
-                         * This exception triggers a "Certificate error"
-                         * notification that takes the user to the incoming
-                         * server settings for review. This might be needed if
-                         * the account was configured with an obsolete
-                         * "STARTTLS (if available)" setting.
-                         */
-                        throw new CertificateValidationException(
-                                "STARTTLS connection security not available");
-                    }
-                }
-
-                switch (mSettings.getAuthType()) {
-                case CRAM_MD5:
-                    if (hasCapability(CAPABILITY_AUTH_CRAM_MD5)) {
-                        authCramMD5();
-                    } else {
-                        throw new MessagingException(
-                                "Server doesn't support encrypted passwords using CRAM-MD5.");
-                    }
-                    break;
-
-                case PLAIN:
-                    if (hasCapability(CAPABILITY_AUTH_PLAIN)) {
-                        saslAuthPlain();
-                    } else if (!hasCapability(CAPABILITY_LOGINDISABLED)) {
-                        login();
-                    } else {
-                        throw new MessagingException(
-                                "Server doesn't support unencrypted passwords using AUTH=PLAIN and LOGIN is disabled.");
-                    }
-                    break;
-
-                case EXTERNAL:
-                    if (hasCapability(CAPABILITY_AUTH_EXTERNAL)) {
-                        saslAuthExternal();
-                    } else {
-                        // Provide notification to user of a problem authenticating using client certificates
-                        throw new CertificateValidationException(Reason.MissingCapability);
-                    }
-                    break;
-
-                default:
-                    throw new MessagingException(
-                            "Unhandled authentication method found in the server settings (bug).");
-                }
-                authSuccess = true;
-                if (K9MailLib.isDebug()) {
-                    Log.d(LOG_TAG, CAPABILITY_COMPRESS_DEFLATE + " = " + hasCapability(CAPABILITY_COMPRESS_DEFLATE));
-                }
-                if (hasCapability(CAPABILITY_COMPRESS_DEFLATE)) {
-                    ConnectivityManager connectivityManager = (ConnectivityManager)K9.app.getSystemService(Context.CONNECTIVITY_SERVICE);
-                    boolean useCompression = true;
-
-                    NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
-                    if (netInfo != null) {
-                        int type = netInfo.getType();
-                        if (K9MailLib.isDebug())
-                            Log.d(LOG_TAG, "On network type " + type);
-                        useCompression = mSettings.useCompression(type);
-
-                    }
-                    if (K9MailLib.isDebug())
-                        Log.d(LOG_TAG, "useCompression " + useCompression);
-                    if (useCompression) {
-                        try {
-                            executeSimpleCommand(COMMAND_COMPRESS_DEFLATE);
-                            Inflater inf = new Inflater(true);
-                            InflaterInputStream zInputStream = new InflaterInputStream(mSocket.getInputStream(), inf);
-                            mIn = new PeekableInputStream(new BufferedInputStream(zInputStream, 1024));
-                            mParser = new ImapResponseParser(mIn);
-                            ZOutputStream zOutputStream = new ZOutputStream(mSocket.getOutputStream(), JZlib.Z_BEST_SPEED, true);
-                            mOut = new BufferedOutputStream(zOutputStream, 1024);
-                            zOutputStream.setFlushMode(JZlib.Z_PARTIAL_FLUSH);
-                            if (K9MailLib.isDebug()) {
-                                Log.i(LOG_TAG, "Compression enabled for " + getLogId());
-                            }
-                        } catch (Exception e) {
-                            Log.e(LOG_TAG, "Unable to negotiate compression", e);
-                        }
-                    }
-                }
-
-
-                if (K9MailLib.isDebug())
-                    Log.d(LOG_TAG, "NAMESPACE = " + hasCapability(CAPABILITY_NAMESPACE)
-                          + ", mPathPrefix = " + mSettings.getPathPrefix());
-
-                if (mSettings.getPathPrefix() == null) {
-                    if (hasCapability(CAPABILITY_NAMESPACE)) {
-                        if (K9MailLib.isDebug())
-                            Log.i(LOG_TAG, "mPathPrefix is unset and server has NAMESPACE capability");
-                        List namespaceResponses =
-                            executeSimpleCommand(COMMAND_NAMESPACE);
-                        for (ImapResponse response : namespaceResponses) {
-                            if (ImapResponseParser.equalsIgnoreCase(response.get(0), COMMAND_NAMESPACE)) {
-                                if (K9MailLib.isDebug())
-                                    Log.d(LOG_TAG, "Got NAMESPACE response " + response + " on " + getLogId());
-
-                                Object personalNamespaces = response.get(1);
-                                if (personalNamespaces != null && personalNamespaces instanceof ImapList) {
-                                    if (K9MailLib.isDebug())
-                                        Log.d(LOG_TAG, "Got personal namespaces: " + personalNamespaces);
-                                    ImapList bracketed = (ImapList)personalNamespaces;
-                                    Object firstNamespace = bracketed.get(0);
-                                    if (firstNamespace != null && firstNamespace instanceof ImapList) {
-                                        if (K9MailLib.isDebug())
-                                            Log.d(LOG_TAG, "Got first personal namespaces: " + firstNamespace);
-                                        bracketed = (ImapList)firstNamespace;
-                                        mSettings.setPathPrefix(bracketed.getString(0));
-                                        mSettings.setPathDelimeter(bracketed.getString(1));
-                                        mSettings.setCombinedPrefix(null);
-                                        if (K9MailLib.isDebug())
-                                            Log.d(LOG_TAG, "Got path '" + mSettings.getPathPrefix() + "' and separator '" + mSettings.getPathDelimeter() + "'");
-                                    }
-                                }
-                            }
-                        }
-                    } else {
-                        if (K9MailLib.isDebug())
-                            Log.i(LOG_TAG, "mPathPrefix is unset but server does not have NAMESPACE capability");
-                        mSettings.setPathPrefix("");
-                    }
-                }
-                if (mSettings.getPathDelimeter() == null) {
-                    try {
-                        List nameResponses =
-                            executeSimpleCommand("LIST \"\" \"\"");
-                        for (ImapResponse response : nameResponses) {
-                            if (ImapResponseParser.equalsIgnoreCase(response.get(0), "LIST")) {
-                                mSettings.setPathDelimeter(response.getString(2));
-                                mSettings.setCombinedPrefix(null);
-                                if (K9MailLib.isDebug())
-                                    Log.d(LOG_TAG, "Got path delimeter '" + mSettings.getPathDelimeter() + "' for " + getLogId());
-                            }
-                        }
-                    } catch (Exception e) {
-                        Log.e(LOG_TAG, "Unable to get path delimeter using LIST", e);
-                    }
-                }
-
-            } catch (SSLException e) {
-                if (e.getCause() instanceof CertificateException) {
-                    throw new CertificateValidationException(e.getMessage(), e);
-                } else {
-                    throw e;
-                }
-            } catch (GeneralSecurityException gse) {
-                throw new MessagingException(
-                    "Unable to open connection to IMAP server due to security error.", gse);
-            } catch (ConnectException ce) {
-                String ceMess = ce.getMessage();
-                String[] tokens = ceMess.split("-");
-                if (tokens != null && tokens.length > 1 && tokens[1] != null) {
-                    Log.e(LOG_TAG, "Stripping host/port from ConnectionException for " + getLogId(), ce);
-                    throw new ConnectException(tokens[1].trim());
-                } else {
-                    throw ce;
-                }
-            } finally {
-                if (!authSuccess) {
-                    Log.e(LOG_TAG, "Failed to login, closing connection for " + getLogId());
-                    close();
-                }
-            }
-        }
-
-        protected void login() throws IOException, MessagingException {
-            /*
-             * Use quoted strings which permit spaces and quotes. (Using IMAP
-             * string literals would be better, but some servers are broken
-             * and don't parse them correctly.)
-             */
-
-            // escape double-quotes and backslash characters with a backslash
-            Pattern p = Pattern.compile("[\\\\\"]");
-            String replacement = "\\\\$0";
-            String username = p.matcher(mSettings.getUsername()).replaceAll(
-                    replacement);
-            String password = p.matcher(mSettings.getPassword()).replaceAll(
-                    replacement);
-            try {
-                receiveCapabilities(executeSimpleCommand(
-                        String.format("LOGIN \"%s\" \"%s\"", username, password), true));
-            } catch (ImapException e) {
-                throw new AuthenticationFailedException(e.getMessage());
-            }
-        }
-
-        protected void authCramMD5() throws MessagingException, IOException {
-            String command = "AUTHENTICATE CRAM-MD5";
-            String tag = sendCommand(command, false);
-            ImapResponse response = readContinuationResponse(tag);
-            if (response.size() != 1 || !(response.get(0) instanceof String)) {
-                throw new MessagingException("Invalid Cram-MD5 nonce received");
-            }
-            byte[] b64Nonce = response.getString(0).getBytes();
-            byte[] b64CRAM = Authentication.computeCramMd5Bytes(
-                    mSettings.getUsername(), mSettings.getPassword(), b64Nonce);
-
-            mOut.write(b64CRAM);
-            mOut.write('\r');
-            mOut.write('\n');
-            mOut.flush();
-            try {
-                receiveCapabilities(readStatusResponse(tag, command, null));
-            } catch (MessagingException e) {
-                throw new AuthenticationFailedException(e.getMessage());
-            }
-        }
-
-        protected void saslAuthPlain() throws IOException, MessagingException {
-            String command = "AUTHENTICATE PLAIN";
-            String tag = sendCommand(command, false);
-            readContinuationResponse(tag);
-            mOut.write(Base64.encodeBase64(("\000" + mSettings.getUsername()
-                    + "\000" + mSettings.getPassword()).getBytes()));
-            mOut.write('\r');
-            mOut.write('\n');
-            mOut.flush();
-            try {
-                receiveCapabilities(readStatusResponse(tag, command, null));
-            } catch (MessagingException e) {
-                throw new AuthenticationFailedException(e.getMessage());
-            }
-        }
-
-        private void saslAuthExternal() throws IOException, MessagingException {
-            try {
-                receiveCapabilities(executeSimpleCommand(
-                        String.format("AUTHENTICATE EXTERNAL %s",
-                                Base64.encode(mSettings.getUsername())), false));
-            } catch (ImapException e) {
-                /*
-                 * Provide notification to the user of a problem authenticating
-                 * using client certificates. We don't use an
-                 * AuthenticationFailedException because that would trigger a
-                 * "Username or password incorrect" notification in
-                 * AccountSetupCheckSettings.
-                 */
-                throw new CertificateValidationException(e.getMessage());
-            }
-        }
-
-        protected ImapResponse readContinuationResponse(String tag)
-                throws IOException, MessagingException {
-            ImapResponse response;
-            do {
-                response = readResponse();
-                if (response.mTag != null) {
-                    if (response.mTag.equalsIgnoreCase(tag)) {
-                        throw new MessagingException(
-                                "Command continuation aborted: " + response);
-                    } else {
-                        Log.w(LOG_TAG, "After sending tag " + tag
-                                + ", got tag response from previous command "
-                                + response + " for " + getLogId());
-                    }
-                }
-            } while (!response.mCommandContinuationRequested);
-            return response;
-        }
-
-        protected List readStatusResponse(String tag,
-                String commandToLog, UntaggedHandler untaggedHandler)
-                throws IOException, MessagingException {
-            List responses = new ArrayList();
-            ImapResponse response;
-            do {
-                response = mParser.readResponse();
-                if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP)
-                    Log.v(LOG_TAG, getLogId() + "<<<" + response);
-
-                if (response.mTag != null && !response.mTag.equalsIgnoreCase(tag)) {
-                    Log.w(LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response + " for " + getLogId());
-                    Iterator iter = responses.iterator();
-                    while (iter.hasNext()) {
-                        ImapResponse delResponse = iter.next();
-                        if (delResponse.mTag != null || delResponse.size() < 2
-                                || (!ImapResponseParser.equalsIgnoreCase(delResponse.get(1), "EXISTS") && !ImapResponseParser.equalsIgnoreCase(delResponse.get(1), "EXPUNGE"))) {
-                            iter.remove();
-                        }
-                    }
-                    response.mTag = null;
-                    continue;
-                }
-                if (untaggedHandler != null) {
-                    untaggedHandler.handleAsyncUntaggedResponse(response);
-                }
-                responses.add(response);
-            } while (response.mTag == null);
-            if (response.size() < 1 || !ImapResponseParser.equalsIgnoreCase(response.get(0), "OK")) {
-                throw new ImapException("Command: " + commandToLog + "; response: " + response.toString(), response.getAlertText());
-            }
-            return responses;
-        }
-
-        protected void setReadTimeout(int millis) throws SocketException {
-            Socket sock = mSocket;
-            if (sock != null) {
-                sock.setSoTimeout(millis);
-            }
-        }
-
-        protected boolean isIdleCapable() {
-            if (K9MailLib.isDebug())
-                Log.v(LOG_TAG, "Connection " + getLogId() + " has " + capabilities.size() + " capabilities");
-
-            return capabilities.contains(CAPABILITY_IDLE);
-        }
-
-        protected boolean hasCapability(String capability) {
-            return capabilities.contains(capability.toUpperCase(Locale.US));
-        }
-
-        public boolean isOpen() {
-            return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
-        }
-
-        public void close() {
-//            if (isOpen()) {
-//                try {
-//                    executeSimpleCommand("LOGOUT");
-//                } catch (Exception e) {
-//
-//                }
-//            }
-            IOUtils.closeQuietly(mIn);
-            IOUtils.closeQuietly(mOut);
-            IOUtils.closeQuietly(mSocket);
-            mIn = null;
-            mOut = null;
-            mSocket = null;
-        }
-
-        public ImapResponse readResponse() throws IOException, MessagingException {
-            return readResponse(null);
-        }
-
-        public ImapResponse readResponse(ImapResponseParser.IImapResponseCallback callback) throws IOException {
-            try {
-                ImapResponse response = mParser.readResponse(callback);
-                if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP)
-                    Log.v(LOG_TAG, getLogId() + "<<<" + response);
-
-                return response;
-            } catch (IOException ioe) {
-                close();
-                throw ioe;
-            }
-        }
-
-        public void sendContinuation(String continuation) throws IOException {
-            mOut.write(continuation.getBytes());
-            mOut.write('\r');
-            mOut.write('\n');
-            mOut.flush();
-
-            if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP)
-                Log.v(LOG_TAG, getLogId() + ">>> " + continuation);
-
-        }
-
-        public String sendCommand(String command, boolean sensitive)
-        throws MessagingException, IOException {
-            try {
-                open();
-                String tag = Integer.toString(mNextCommandTag++);
-                String commandToSend = tag + " " + command + "\r\n";
-                mOut.write(commandToSend.getBytes());
-                mOut.flush();
-
-                if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) {
-                    if (sensitive && !K9MailLib.isDebugSensitive()) {
-                        Log.v(LOG_TAG, getLogId() + ">>> "
-                              + "[Command Hidden, Enable Sensitive Debug Logging To Show]");
-                    } else {
-                        Log.v(LOG_TAG, getLogId() + ">>> " + commandToSend);
-                    }
-                }
-
-                return tag;
-            } catch (IOException ioe) {
-                close();
-                throw ioe;
-            } catch (ImapException ie) {
-                close();
-                throw ie;
-            } catch (MessagingException me) {
-                close();
-                throw me;
-            }
-        }
-
-        public List executeSimpleCommand(String command) throws IOException,
-            ImapException, MessagingException {
-            return executeSimpleCommand(command, false, null);
-        }
-
-        public List executeSimpleCommand(String command, boolean sensitive) throws IOException,
-            ImapException, MessagingException {
-            return executeSimpleCommand(command, sensitive, null);
-        }
-
-        public List executeSimpleCommand(String command, boolean sensitive, UntaggedHandler untaggedHandler)
-        throws IOException, ImapException, MessagingException {
-            String commandToLog = command;
-            if (sensitive && !K9MailLib.isDebugSensitive()) {
-                commandToLog = "*sensitive*";
-            }
-
-
-            //if (K9MailLib.isDebug())
-            //    Log.v(LOG_TAG, "Sending IMAP command " + commandToLog + " on connection " + getLogId());
-
-            String tag = sendCommand(command, sensitive);
-            //if (K9MailLib.isDebug())
-            //    Log.v(LOG_TAG, "Sent IMAP command " + commandToLog + " with tag " + tag + " for " + getLogId());
-
-            return readStatusResponse(tag, commandToLog, untaggedHandler);
-        }
-    }
-
     static class ImapMessage extends MimeMessage {
         ImapMessage(String uid, Folder folder) {
             this.mUid = uid;
@@ -3107,7 +2469,7 @@ public class ImapStore extends RemoteStore {
                                 doneSent.set(false);
 
                                 conn.setReadTimeout((mStoreConfig.getIdleRefreshMinutes() * 60 * 1000) + IDLE_READ_TIMEOUT_INCREMENT);
-                                untaggedResponses = executeSimpleCommand(COMMAND_IDLE, false, ImapFolderPusher.this);
+                                untaggedResponses = executeSimpleCommand(ImapCommands.COMMAND_IDLE, false, ImapFolderPusher.this);
                                 idling.set(false);
                                 delayTime.set(NORMAL_DELAY_TIME);
                                 idleFailureCount.set(0);
@@ -3516,7 +2878,7 @@ public class ImapStore extends RemoteStore {
         }
 
     }
-    private interface UntaggedHandler {
+    public static interface UntaggedHandler {
         void handleAsyncUntaggedResponse(ImapResponse respose);
     }
 
diff --git a/src/com/fsck/k9/mail/transport/imap/ImapSettings.java b/src/com/fsck/k9/mail/transport/imap/ImapSettings.java
index 848573627..f22d834aa 100644
--- a/src/com/fsck/k9/mail/transport/imap/ImapSettings.java
+++ b/src/com/fsck/k9/mail/transport/imap/ImapSettings.java
@@ -3,10 +3,9 @@ package com.fsck.k9.mail.transport.imap;
 import com.fsck.k9.mail.AuthType;
 import com.fsck.k9.mail.ConnectionSecurity;
 import com.fsck.k9.mail.store.ImapStore;
-import com.fsck.k9.mail.store.ImapStore.ImapConnection;
 
 /**
- * Settings source for IMAP. Implemented in order to remove coupling between {@link ImapStore} and {@link ImapConnection}.
+ * Settings source for IMAP. Implemented in order to remove coupling between {@link ImapStore} and {@link com.fsck.k9.mail.store.ImapConnection}.
  */
 public interface ImapSettings {
     String getHost();