1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-28 04:02:19 -05:00

Remove coupling between ImapStore and ImapConnection by adding an settings indirection interface. Purpose: use the IMAP connection code to validate credentials without having an actual ImapStore instance.

This commit is contained in:
Fiouz 2010-12-30 21:57:59 +00:00
parent a1e059e871
commit 47762a9baa
2 changed files with 253 additions and 84 deletions

View File

@ -1,41 +1,21 @@
package com.fsck.k9.mail.store; package com.fsck.k9.mail.store;
import android.content.Context; import java.io.BufferedInputStream;
import android.net.ConnectivityManager; import java.io.BufferedOutputStream;
import android.net.NetworkInfo; import java.io.ByteArrayInputStream;
import android.os.PowerManager; import java.io.IOException;
import android.util.Log; import java.io.InputStream;
import com.fsck.k9.Account; import java.io.OutputStream;
import com.fsck.k9.K9; import java.io.UnsupportedEncodingException;
import com.fsck.k9.R; import java.net.ConnectException;
import com.fsck.k9.controller.MessageRetrievalListener; import java.net.InetSocketAddress;
import com.fsck.k9.helper.Utility; import java.net.Socket;
import com.fsck.k9.helper.power.TracingPowerManager; import java.net.SocketAddress;
import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock; import java.net.SocketException;
import com.fsck.k9.mail.*; import java.net.URI;
import com.fsck.k9.mail.filter.CountingOutputStream; import java.net.URISyntaxException;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import java.net.URLDecoder;
import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.internet.*;
import com.fsck.k9.mail.store.ImapResponseParser.ImapList;
import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse;
import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZInputStream;
import com.jcraft.jzlib.ZOutputStream;
import com.beetstra.jutf7.CharsetProvider;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import java.io.*;
import java.net.*;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -45,11 +25,73 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.Security; import java.security.Security;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.*; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.PowerManager;
import android.util.Log;
import com.beetstra.jutf7.CharsetProvider;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.R;
import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.helper.power.TracingPowerManager;
import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.Body;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.FetchProfile;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.Part;
import com.fsck.k9.mail.PushReceiver;
import com.fsck.k9.mail.Pusher;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.filter.CountingOutputStream;
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.store.ImapResponseParser.ImapList;
import com.fsck.k9.mail.store.ImapResponseParser.ImapResponse;
import com.fsck.k9.mail.transport.imap.ImapSettings;
import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZInputStream;
import com.jcraft.jzlib.ZOutputStream;
/** /**
* <pre> * <pre>
* TODO Need to start keeping track of UIDVALIDITY * TODO Need to start keeping track of UIDVALIDITY
@ -64,7 +106,7 @@ public class ImapStore extends Store
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3; public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4; public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
private enum AuthType { PLAIN, CRAM_MD5 } public enum AuthType { PLAIN, CRAM_MD5 }
private static final int IDLE_READ_TIMEOUT_INCREMENT = 5 * 60 * 1000; private static final int IDLE_READ_TIMEOUT_INCREMENT = 5 * 60 * 1000;
private static final int IDLE_FAILURE_COUNT_LIMIT = 10; private static final int IDLE_FAILURE_COUNT_LIMIT = 10;
@ -100,6 +142,89 @@ public class ImapStore extends Store
private volatile String mCombinedPrefix = null; private volatile String mCombinedPrefix = null;
private volatile String mPathDelimeter = null; private volatile String mPathDelimeter = null;
public class StoreImapSettings implements ImapSettings
{
@Override
public String getHost()
{
return mHost;
}
@Override
public int getPort()
{
return mPort;
}
@Override
public int getConnectionSecurity()
{
return mConnectionSecurity;
}
@Override
public AuthType getAuthType()
{
return mAuthType;
}
@Override
public String getUsername()
{
return mUsername;
}
@Override
public String getPassword()
{
return mPassword;
}
@Override
public boolean useCompression(final int type)
{
return mAccount.useCompression(type);
}
@Override
public String getPathPrefix()
{
return mPathPrefix;
}
@Override
public void setPathPrefix(String prefix)
{
mPathPrefix = prefix;
}
@Override
public String getPathDelimeter()
{
return mPathDelimeter;
}
@Override
public void setPathDelimeter(String delimeter)
{
mPathDelimeter = delimeter;
}
@Override
public String getCombinedPrefix()
{
return mCombinedPrefix;
}
@Override
public void setCombinedPrefix(String prefix)
{
mCombinedPrefix = prefix;
}
}
private static final SimpleDateFormat RFC3501_DATE = new SimpleDateFormat("dd-MMM-yyyy", Locale.US); private static final SimpleDateFormat RFC3501_DATE = new SimpleDateFormat("dd-MMM-yyyy", Locale.US);
private LinkedList<ImapConnection> mConnections = private LinkedList<ImapConnection> mConnections =
@ -375,7 +500,7 @@ public class ImapStore extends Store
{ {
try try
{ {
ImapConnection connection = new ImapConnection(); ImapConnection connection = new ImapConnection(new StoreImapSettings());
connection.open(); connection.open();
connection.close(); connection.close();
} }
@ -408,7 +533,7 @@ public class ImapStore extends Store
} }
if (connection == null) if (connection == null)
{ {
connection = new ImapConnection(); connection = new ImapConnection(new StoreImapSettings());
} }
return connection; return connection;
} }
@ -2061,16 +2186,23 @@ public class ImapStore extends Store
/** /**
* A cacheable class that stores the details for a single IMAP connection. * A cacheable class that stores the details for a single IMAP connection.
*/ */
class ImapConnection public static class ImapConnection
{ {
private Socket mSocket; protected Socket mSocket;
private PeekableInputStream mIn; protected PeekableInputStream mIn;
private OutputStream mOut; protected OutputStream mOut;
private ImapResponseParser mParser; protected ImapResponseParser mParser;
private int mNextCommandTag; protected int mNextCommandTag;
protected Set<String> capabilities = new HashSet<String>(); protected Set<String> capabilities = new HashSet<String>();
private String getLogId() private ImapSettings mSettings;
public ImapConnection(final ImapSettings settings)
{
this.mSettings = settings;
}
protected String getLogId()
{ {
return "conn" + hashCode(); return "conn" + hashCode();
} }
@ -2126,7 +2258,6 @@ public class ImapStore extends Store
return responses; return responses;
} }
public void open() throws IOException, MessagingException public void open() throws IOException, MessagingException
{ {
if (isOpen()) if (isOpen())
@ -2161,19 +2292,19 @@ public class ImapStore extends Store
try try
{ {
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort); SocketAddress socketAddress = new InetSocketAddress(mSettings.getHost(), mSettings.getPort());
if (K9.DEBUG) if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Connection " + getLogId() + " connecting to " + mHost + " @ IP addr " + socketAddress); Log.i(K9.LOG_TAG, "Connection " + getLogId() + " connecting to " + mSettings.getHost() + " @ IP addr " + socketAddress);
if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED || if (mSettings.getConnectionSecurity() == CONNECTION_SECURITY_SSL_REQUIRED ||
mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) mSettings.getConnectionSecurity() == CONNECTION_SECURITY_SSL_OPTIONAL)
{ {
SSLContext sslContext = SSLContext.getInstance("TLS"); SSLContext sslContext = SSLContext.getInstance("TLS");
final boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED; final boolean secure = mSettings.getConnectionSecurity() == CONNECTION_SECURITY_SSL_REQUIRED;
sslContext.init(null, new TrustManager[] sslContext.init(null, new TrustManager[]
{ {
TrustManagerFactory.get(mHost, secure) TrustManagerFactory.get(mSettings.getHost(), secure)
}, new SecureRandom()); }, new SecureRandom());
mSocket = sslContext.getSocketFactory().createSocket(); mSocket = sslContext.getSocketFactory().createSocket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
@ -2211,8 +2342,8 @@ public class ImapStore extends Store
} }
} }
if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL if (mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_OPTIONAL
|| mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) || mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED)
{ {
if (hasCapability("STARTTLS")) if (hasCapability("STARTTLS"))
@ -2221,12 +2352,12 @@ public class ImapStore extends Store
executeSimpleCommand("STARTTLS"); executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS"); SSLContext sslContext = SSLContext.getInstance("TLS");
boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED; boolean secure = mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED;
sslContext.init(null, new TrustManager[] sslContext.init(null, new TrustManager[]
{ {
TrustManagerFactory.get(mHost, secure) TrustManagerFactory.get(mSettings.getHost(), secure)
}, new SecureRandom()); }, new SecureRandom());
mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort, mSocket = sslContext.getSocketFactory().createSocket(mSocket, mSettings.getHost(), mSettings.getPort(),
true); true);
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT); mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket mIn = new PeekableInputStream(new BufferedInputStream(mSocket
@ -2234,7 +2365,7 @@ public class ImapStore extends Store
mParser = new ImapResponseParser(mIn); mParser = new ImapResponseParser(mIn);
mOut = mSocket.getOutputStream(); mOut = mSocket.getOutputStream();
} }
else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) else if (mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED)
{ {
throw new MessagingException("TLS not supported but required"); throw new MessagingException("TLS not supported but required");
} }
@ -2245,13 +2376,13 @@ public class ImapStore extends Store
try try
{ {
// Yahoo! requires a custom IMAP command to work right over a non-3G network // Yahoo! requires a custom IMAP command to work right over a non-3G network
if (mHost.endsWith("yahoo.com")) if (mSettings.getHost().endsWith("yahoo.com"))
{ {
if (K9.DEBUG) if (K9.DEBUG)
Log.v(K9.LOG_TAG, "Found Yahoo! account. Sending proprietary commands."); Log.v(K9.LOG_TAG, "Found Yahoo! account. Sending proprietary commands.");
executeSimpleCommand("ID (\"GUID\" \"1\")"); executeSimpleCommand("ID (\"GUID\" \"1\")");
} }
if (mAuthType == AuthType.CRAM_MD5) if (mSettings.getAuthType() == AuthType.CRAM_MD5)
{ {
authCramMD5(); authCramMD5();
// The authCramMD5 method called on the previous line does not allow for handling updated capabilities // The authCramMD5 method called on the previous line does not allow for handling updated capabilities
@ -2266,9 +2397,9 @@ public class ImapStore extends Store
} }
} }
else if (mAuthType == AuthType.PLAIN) else if (mSettings.getAuthType() == AuthType.PLAIN)
{ {
receiveCapabilities(executeSimpleCommand("LOGIN \"" + escapeString(mUsername) + "\" \"" + escapeString(mPassword) + "\"", true)); receiveCapabilities(executeSimpleCommand("LOGIN \"" + escapeString(mSettings.getUsername()) + "\" \"" + escapeString(mSettings.getPassword()) + "\"", true));
} }
authSuccess = true; authSuccess = true;
} }
@ -2296,7 +2427,7 @@ public class ImapStore extends Store
int type = netInfo.getType(); int type = netInfo.getType();
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "On network type " + type); Log.d(K9.LOG_TAG, "On network type " + type);
useCompression = mAccount.useCompression(type); useCompression = mSettings.useCompression(type);
} }
if (K9.DEBUG) if (K9.DEBUG)
@ -2328,9 +2459,9 @@ public class ImapStore extends Store
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "NAMESPACE = " + hasCapability(CAPABILITY_NAMESPACE) Log.d(K9.LOG_TAG, "NAMESPACE = " + hasCapability(CAPABILITY_NAMESPACE)
+ ", mPathPrefix = " + mPathPrefix); + ", mPathPrefix = " + mSettings.getPathPrefix());
if (mPathPrefix == null) if (mSettings.getPathPrefix() == null)
{ {
if (hasCapability(CAPABILITY_NAMESPACE)) if (hasCapability(CAPABILITY_NAMESPACE))
{ {
@ -2357,11 +2488,11 @@ public class ImapStore extends Store
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Got first personal namespaces: " + firstNamespace); Log.d(K9.LOG_TAG, "Got first personal namespaces: " + firstNamespace);
bracketed = (ImapList)firstNamespace; bracketed = (ImapList)firstNamespace;
mPathPrefix = bracketed.getString(0); mSettings.setPathPrefix(bracketed.getString(0));
mPathDelimeter = bracketed.getString(1); mSettings.setPathDelimeter(bracketed.getString(1));
mCombinedPrefix = null; mSettings.setCombinedPrefix(null);
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Got path '" + mPathPrefix + "' and separator '" + mPathDelimeter + "'"); Log.d(K9.LOG_TAG, "Got path '" + mSettings.getPathPrefix() + "' and separator '" + mSettings.getPathDelimeter() + "'");
} }
} }
} }
@ -2371,10 +2502,10 @@ public class ImapStore extends Store
{ {
if (K9.DEBUG) if (K9.DEBUG)
Log.i(K9.LOG_TAG, "mPathPrefix is unset but server does not have NAMESPACE capability"); Log.i(K9.LOG_TAG, "mPathPrefix is unset but server does not have NAMESPACE capability");
mPathPrefix = ""; mSettings.setPathPrefix("");
} }
} }
if (mPathDelimeter == null) if (mSettings.getPathDelimeter() == null)
{ {
try try
{ {
@ -2384,10 +2515,10 @@ public class ImapStore extends Store
{ {
if (ImapResponseParser.equalsIgnoreCase(response.get(0), "LIST")) if (ImapResponseParser.equalsIgnoreCase(response.get(0), "LIST"))
{ {
mPathDelimeter = response.getString(2); mSettings.setPathDelimeter(response.getString(2));
mCombinedPrefix = null; mSettings.setCombinedPrefix(null);
if (K9.DEBUG) if (K9.DEBUG)
Log.d(K9.LOG_TAG, "Got path delimeter '" + mPathDelimeter + "' for " + getLogId()); Log.d(K9.LOG_TAG, "Got path delimeter '" + mSettings.getPathDelimeter() + "' for " + getLogId());
} }
} }
} }
@ -2463,7 +2594,7 @@ public class ImapStore extends Store
byte[] ipad = new byte[64]; byte[] ipad = new byte[64];
byte[] opad = new byte[64]; byte[] opad = new byte[64];
byte[] secretBytes = mPassword.getBytes("US-ASCII"); byte[] secretBytes = mSettings.getPassword().getBytes("US-ASCII");
MessageDigest md = MessageDigest.getInstance("MD5"); MessageDigest md = MessageDigest.getInstance("MD5");
if (secretBytes.length > 64) if (secretBytes.length > 64)
{ {
@ -2477,11 +2608,11 @@ public class ImapStore extends Store
byte[] firstPass = md.digest(nonce); byte[] firstPass = md.digest(nonce);
md.update(opad); md.update(opad);
byte[] result = md.digest(firstPass); byte[] result = md.digest(firstPass);
String plainCRAM = mUsername + " " + new String(Hex.encodeHex(result)); String plainCRAM = mSettings.getUsername() + " " + new String(Hex.encodeHex(result));
byte[] b64CRAM = Base64.encodeBase64(plainCRAM.getBytes("US-ASCII")); byte[] b64CRAM = Base64.encodeBase64(plainCRAM.getBytes("US-ASCII"));
if (K9.DEBUG) if (K9.DEBUG)
{ {
Log.d(K9.LOG_TAG, "Username == " + mUsername); Log.d(K9.LOG_TAG, "Username == " + mSettings.getUsername());
Log.d(K9.LOG_TAG, "plainCRAM: " + plainCRAM); Log.d(K9.LOG_TAG, "plainCRAM: " + plainCRAM);
Log.d(K9.LOG_TAG, "b64CRAM: " + new String(b64CRAM, "US-ASCII")); Log.d(K9.LOG_TAG, "b64CRAM: " + new String(b64CRAM, "US-ASCII"));
} }
@ -2538,12 +2669,12 @@ public class ImapStore extends Store
return capabilities.contains(capability.toUpperCase()); return capabilities.contains(capability.toUpperCase());
} }
private boolean isOpen() public boolean isOpen()
{ {
return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
} }
private void close() public void close()
{ {
// if (isOpen()) { // if (isOpen()) {
// try { // try {
@ -2581,12 +2712,12 @@ public class ImapStore extends Store
mSocket = null; mSocket = null;
} }
private ImapResponse readResponse() throws IOException, MessagingException public ImapResponse readResponse() throws IOException, MessagingException
{ {
return readResponse(null); return readResponse(null);
} }
private ImapResponse readResponse(ImapResponseParser.IImapResponseCallback callback) throws IOException public ImapResponse readResponse(ImapResponseParser.IImapResponseCallback callback) throws IOException
{ {
try try
{ {
@ -2614,7 +2745,7 @@ public class ImapStore extends Store
return out; return out;
} }
private void sendContinuation(String continuation) throws IOException public void sendContinuation(String continuation) throws IOException
{ {
mOut.write(continuation.getBytes()); mOut.write(continuation.getBytes());
mOut.write('\r'); mOut.write('\r');
@ -2683,7 +2814,7 @@ public class ImapStore extends Store
return executeSimpleCommand(command, sensitive, null); return executeSimpleCommand(command, sensitive, null);
} }
private List<ImapResponse> executeSimpleCommand(String command, boolean sensitive, UntaggedHandler untaggedHandler) public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive, UntaggedHandler untaggedHandler)
throws IOException, ImapException, MessagingException throws IOException, ImapException, MessagingException
{ {
String commandToLog = command; String commandToLog = command;

View File

@ -0,0 +1,38 @@
package com.fsck.k9.mail.transport.imap;
import com.fsck.k9.mail.store.ImapStore;
import com.fsck.k9.mail.store.ImapStore.AuthType;
import com.fsck.k9.mail.store.ImapStore.ImapConnection;
/**
* Settings source for IMAP. Implemented in order to remove coupling between {@link ImapStore} and {@link ImapConnection}.
*/
public interface ImapSettings
{
String getHost();
int getPort();
int getConnectionSecurity();
AuthType getAuthType();
String getUsername();
String getPassword();
boolean useCompression(int type);
String getPathPrefix();
void setPathPrefix(String prefix);
String getPathDelimeter();
void setPathDelimeter(String delimeter);
String getCombinedPrefix();
void setCombinedPrefix(String prefix);
}