(context,
+ android.R.layout.simple_spinner_item, authTypes);
+ authTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ return authTypesAdapter;
+ }
+
+ int mResourceId;
+
+ private AuthType(int id) {
+ mResourceId = id;
+ }
+
+ /**
+ * Used to select an appropriate localized text label for the
+ * {@code AuthType.PLAIN} option presented to users.
+ *
+ * @param insecure
+ *
+ * A value of {@code true} will use "Normal password".
+ *
+ * A value of {@code false} will use
+ * "Password, transmitted insecurely"
+ */
+ public void useInsecureText(boolean insecure, ArrayAdapter authTypesAdapter) {
+ // Do nothing. Overridden in AuthType.PLAIN
+ }
+
+ @Override
+ public String toString() {
+ if (mResourceId == 0) {
+ return name();
+ } else {
+ return K9.app.getString(mResourceId);
+ }
+ }
+}
diff --git a/src/com/fsck/k9/mail/Authentication.java b/src/com/fsck/k9/mail/Authentication.java
index 4b9ae90ae..2872b1b58 100644
--- a/src/com/fsck/k9/mail/Authentication.java
+++ b/src/com/fsck/k9/mail/Authentication.java
@@ -17,21 +17,21 @@ public class Authentication {
* @param b64Nonce The nonce as base64-encoded string.
* @return The CRAM-MD5 response.
*
- * @throws AuthenticationFailedException If something went wrong.
+ * @throws MessagingException If something went wrong.
*
* @see Authentication#computeCramMd5Bytes(String, String, byte[])
*/
public static String computeCramMd5(String username, String password, String b64Nonce)
- throws AuthenticationFailedException {
+ throws MessagingException {
try {
byte[] b64NonceBytes = b64Nonce.getBytes(US_ASCII);
byte[] b64CRAM = computeCramMd5Bytes(username, password, b64NonceBytes);
return new String(b64CRAM, US_ASCII);
- } catch (AuthenticationFailedException e) {
+ } catch (MessagingException e) {
throw e;
} catch (Exception e) {
- throw new AuthenticationFailedException("This shouldn't happen", e);
+ throw new MessagingException("This shouldn't happen", e);
}
}
@@ -44,17 +44,17 @@ public class Authentication {
* @param b64Nonce The nonce as base64-encoded byte array.
* @return The CRAM-MD5 response as byte array.
*
- * @throws AuthenticationFailedException If something went wrong.
+ * @throws MessagingException If something went wrong.
*
* @see RFC 2195
*/
public static byte[] computeCramMd5Bytes(String username, String password, byte[] b64Nonce)
- throws AuthenticationFailedException {
+ throws MessagingException {
try {
byte[] nonce = Base64.decodeBase64(b64Nonce);
- byte[] secretBytes = password.getBytes(US_ASCII);
+ byte[] secretBytes = password.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
if (secretBytes.length > 64) {
secretBytes = md.digest(secretBytes);
@@ -74,12 +74,12 @@ public class Authentication {
byte[] result = md.digest(firstPass);
String plainCRAM = username + " " + new String(Hex.encodeHex(result));
- byte[] b64CRAM = Base64.encodeBase64(plainCRAM.getBytes(US_ASCII));
+ byte[] b64CRAM = Base64.encodeBase64(plainCRAM.getBytes());
return b64CRAM;
} catch (Exception e) {
- throw new AuthenticationFailedException("Something went wrong during CRAM-MD5 computation", e);
+ throw new MessagingException("Something went wrong during CRAM-MD5 computation", e);
}
}
}
diff --git a/src/com/fsck/k9/mail/ConnectionSecurity.java b/src/com/fsck/k9/mail/ConnectionSecurity.java
index 98741303e..510eda039 100644
--- a/src/com/fsck/k9/mail/ConnectionSecurity.java
+++ b/src/com/fsck/k9/mail/ConnectionSecurity.java
@@ -1,19 +1,21 @@
package com.fsck.k9.mail;
-/**
- * The currently available connection security types.
- *
- *
- * Right now this enum is only used by {@link ServerSettings} and converted to store- or
- * transport-specific constants in the different {@link Store} and {@link Transport}
- * implementations. In the future we probably want to change this and use
- * {@code ConnectionSecurity} exclusively.
- *
- */
+import com.fsck.k9.K9;
+import com.fsck.k9.R;
+
public enum ConnectionSecurity {
- NONE,
- STARTTLS_OPTIONAL,
- STARTTLS_REQUIRED,
- SSL_TLS_OPTIONAL,
- SSL_TLS_REQUIRED
+ NONE(R.string.account_setup_incoming_security_none_label),
+ STARTTLS_REQUIRED(R.string.account_setup_incoming_security_tls_label),
+ SSL_TLS_REQUIRED(R.string.account_setup_incoming_security_ssl_label);
+
+ private final int mResourceId;
+
+ private ConnectionSecurity(int id) {
+ mResourceId = id;
+ }
+
+ @Override
+ public String toString() {
+ return K9.app.getString(mResourceId);
+ }
}
diff --git a/src/com/fsck/k9/mail/ServerSettings.java b/src/com/fsck/k9/mail/ServerSettings.java
index 9ba43556e..50207863d 100644
--- a/src/com/fsck/k9/mail/ServerSettings.java
+++ b/src/com/fsck/k9/mail/ServerSettings.java
@@ -48,7 +48,7 @@ public class ServerSettings {
*
* {@code null} if not applicable for the store or transport.
*/
- public final String authenticationType;
+ public final AuthType authenticationType;
/**
* The username part of the credentials needed to authenticate to the server.
@@ -91,7 +91,7 @@ public class ServerSettings {
* see {@link ServerSettings#password}
*/
public ServerSettings(String type, String host, int port,
- ConnectionSecurity connectionSecurity, String authenticationType, String username,
+ ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
String password) {
this.type = type;
this.host = host;
@@ -124,7 +124,7 @@ public class ServerSettings {
* see {@link ServerSettings#extra}
*/
public ServerSettings(String type, String host, int port,
- ConnectionSecurity connectionSecurity, String authenticationType, String username,
+ ConnectionSecurity connectionSecurity, AuthType authenticationType, String username,
String password, Map extra) {
this.type = type;
this.host = host;
diff --git a/src/com/fsck/k9/mail/filter/Hex.java b/src/com/fsck/k9/mail/filter/Hex.java
index 92022203b..049af6e84 100644
--- a/src/com/fsck/k9/mail/filter/Hex.java
+++ b/src/com/fsck/k9/mail/filter/Hex.java
@@ -30,13 +30,13 @@ public class Hex {
};
/**
- * Converts an array of bytes into an array of characters representing the hexidecimal values of each byte in order.
+ * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
* The returned array will be double the length of the passed array, as it takes two characters to represent any
* given byte.
*
* @param data
* a byte[] to convert to Hex characters
- * @return A char[] containing hexidecimal characters
+ * @return A char[] containing lower-case hexadecimal characters
*/
public static char[] encodeHex(byte[] data) {
diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java
index 2363112eb..ca6c5966b 100644
--- a/src/com/fsck/k9/mail/store/ImapStore.java
+++ b/src/com/fsck/k9/mail/store/ImapStore.java
@@ -27,6 +27,7 @@ import java.nio.charset.CodingErrorAction;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.security.Security;
+import java.security.cert.CertificateException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -65,10 +66,12 @@ 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.controller.MessagingController;
import com.fsck.k9.helper.StringUtils;
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.AuthType;
import com.fsck.k9.mail.Authentication;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.Body;
@@ -84,6 +87,7 @@ import com.fsck.k9.mail.PushReceiver;
import com.fsck.k9.mail.Pusher;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store;
+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;
@@ -110,14 +114,6 @@ import com.jcraft.jzlib.ZOutputStream;
public class ImapStore extends Store {
public static final String STORE_TYPE = "IMAP";
- public static final int CONNECTION_SECURITY_NONE = 0;
- public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
- public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
- public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
- public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
-
- public enum AuthType { PLAIN, CRAM_MD5 }
-
private static final int IDLE_READ_TIMEOUT_INCREMENT = 5 * 60 * 1000;
private static final int IDLE_FAILURE_COUNT_LIMIT = 10;
private static int MAX_DELAY_TIME = 5 * 60 * 1000; // 5 minutes
@@ -128,6 +124,10 @@ public class ImapStore extends Store {
private Set mPermanentFlagsIndex = new HashSet();
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_LOGINDISABLED = "LOGINDISABLED";
+ private static final String CAPABILITY_LITERAL_PLUS = "LITERAL+";
private static final String COMMAND_IDLE = "IDLE";
private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
private static final String COMMAND_NAMESPACE = "NAMESPACE";
@@ -147,18 +147,16 @@ public class ImapStore extends Store {
*
* Possible forms:
*
- * imap://auth:user:password@server:port CONNECTION_SECURITY_NONE
- * imap+tls://auth:user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
- * imap+tls+://auth:user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
- * imap+ssl+://auth:user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
- * imap+ssl://auth:user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+ * imap://auth:user:password@server:port ConnectionSecurity.NONE
+ * imap+tls+://auth:user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
+ * imap+ssl+://auth:user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
*
*/
public static ImapStoreSettings decodeUri(String uri) {
String host;
int port;
ConnectionSecurity connectionSecurity;
- String authenticationType = null;
+ AuthType authenticationType = null;
String username = null;
String password = null;
String pathPrefix = null;
@@ -172,21 +170,27 @@ public class ImapStore extends Store {
}
String scheme = imapUri.getScheme();
+ /*
+ * Currently available schemes are:
+ * imap
+ * imap+tls+
+ * imap+ssl+
+ *
+ * The following are obsolete schemes that may be found in pre-existing
+ * settings from earlier versions or that may be found when imported. We
+ * continue to recognize them and re-map them appropriately:
+ * imap+tls
+ * imap+ssl
+ */
if (scheme.equals("imap")) {
connectionSecurity = ConnectionSecurity.NONE;
port = 143;
- } else if (scheme.equals("imap+tls")) {
- connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL;
- port = 143;
- } else if (scheme.equals("imap+tls+")) {
+ } else if (scheme.startsWith("imap+tls")) {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
port = 143;
- } else if (scheme.equals("imap+ssl+")) {
+ } else if (scheme.startsWith("imap+ssl")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
port = 993;
- } else if (scheme.equals("imap+ssl")) {
- connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL;
- port = 993;
} else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
}
@@ -204,14 +208,14 @@ public class ImapStore extends Store {
if (userinfo.endsWith(":")) {
// Password is empty. This can only happen after an account was imported.
- authenticationType = AuthType.valueOf(userInfoParts[0]).name();
+ authenticationType = AuthType.valueOf(userInfoParts[0]);
username = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else if (userInfoParts.length == 2) {
- authenticationType = AuthType.PLAIN.name();
+ authenticationType = AuthType.PLAIN;
username = URLDecoder.decode(userInfoParts[0], "UTF-8");
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
} else {
- authenticationType = AuthType.valueOf(userInfoParts[0]).name();
+ authenticationType = AuthType.valueOf(userInfoParts[0]);
username = URLDecoder.decode(userInfoParts[1], "UTF-8");
password = URLDecoder.decode(userInfoParts[2], "UTF-8");
}
@@ -268,15 +272,9 @@ public class ImapStore extends Store {
String scheme;
switch (server.connectionSecurity) {
- case SSL_TLS_OPTIONAL:
- scheme = "imap+ssl";
- break;
case SSL_TLS_REQUIRED:
scheme = "imap+ssl+";
break;
- case STARTTLS_OPTIONAL:
- scheme = "imap+tls";
- break;
case STARTTLS_REQUIRED:
scheme = "imap+tls+";
break;
@@ -286,15 +284,9 @@ public class ImapStore extends Store {
break;
}
- AuthType authType;
- try {
- authType = AuthType.valueOf(server.authenticationType);
- } catch (Exception e) {
- throw new IllegalArgumentException("Invalid authentication type: " +
- server.authenticationType);
- }
+ AuthType authType = server.authenticationType;
- String userInfo = authType.toString() + ":" + userEnc + ":" + passwordEnc;
+ String userInfo = authType.name() + ":" + userEnc + ":" + passwordEnc;
try {
Map extra = server.getExtra();
String path = null;
@@ -329,7 +321,7 @@ public class ImapStore extends Store {
public final String pathPrefix;
protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
- String authenticationType, String username, String password,
+ AuthType authenticationType, String username, String password,
boolean autodetectNamespace, String pathPrefix) {
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
password);
@@ -357,7 +349,7 @@ public class ImapStore extends Store {
private int mPort;
private String mUsername;
private String mPassword;
- private int mConnectionSecurity;
+ private ConnectionSecurity mConnectionSecurity;
private AuthType mAuthType;
private volatile String mPathPrefix;
private volatile String mCombinedPrefix = null;
@@ -376,7 +368,7 @@ public class ImapStore extends Store {
}
@Override
- public int getConnectionSecurity() {
+ public ConnectionSecurity getConnectionSecurity() {
return mConnectionSecurity;
}
@@ -462,25 +454,9 @@ public class ImapStore extends Store {
mHost = settings.host;
mPort = settings.port;
- switch (settings.connectionSecurity) {
- case NONE:
- mConnectionSecurity = CONNECTION_SECURITY_NONE;
- break;
- case STARTTLS_OPTIONAL:
- mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
- break;
- case STARTTLS_REQUIRED:
- mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
- break;
- case SSL_TLS_OPTIONAL:
- mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
- break;
- case SSL_TLS_REQUIRED:
- mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
- break;
- }
+ mConnectionSecurity = settings.connectionSecurity;
- mAuthType = AuthType.valueOf(settings.authenticationType);
+ mAuthType = settings.authenticationType;
mUsername = settings.username;
mPassword = settings.password;
@@ -2429,7 +2405,7 @@ public class ImapStore extends Store {
}
try {
- int connectionSecurity = mSettings.getConnectionSecurity();
+ ConnectionSecurity connectionSecurity = mSettings.getConnectionSecurity();
// Try all IPv4 and IPv6 addresses of the host
InetAddress[] addresses = InetAddress.getAllByName(mSettings.getHost());
@@ -2443,15 +2419,13 @@ public class ImapStore extends Store {
SocketAddress socketAddress = new InetSocketAddress(addresses[i],
mSettings.getPort());
- if (connectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
- connectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) {
+ if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS");
- boolean secure = connectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
sslContext
.init(null,
new TrustManager[] { TrustManagerFactory.get(
mSettings.getHost(),
- mSettings.getPort(), secure) },
+ mSettings.getPort()) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
} else {
@@ -2476,7 +2450,7 @@ public class ImapStore extends Store {
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
1024));
mParser = new ImapResponseParser(mIn);
- mOut = mSocket.getOutputStream();
+ mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
capabilities.clear();
ImapResponse nullResponse = mParser.readResponse();
@@ -2496,19 +2470,17 @@ public class ImapStore extends Store {
}
}
- if (mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_OPTIONAL
- || mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED) {
+ if (mSettings.getConnectionSecurity() == ConnectionSecurity.STARTTLS_REQUIRED) {
if (hasCapability("STARTTLS")) {
// STARTTLS
executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS");
- boolean secure = mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED;
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(
mSettings.getHost(),
- mSettings.getPort(), secure) },
+ mSettings.getPort()) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket,
mSettings.getHost(), mSettings.getPort(), true);
@@ -2516,37 +2488,55 @@ public class ImapStore extends Store {
mIn = new PeekableInputStream(new BufferedInputStream(mSocket
.getInputStream(), 1024));
mParser = new ImapResponseParser(mIn);
- mOut = mSocket.getOutputStream();
- } else if (mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED) {
- throw new MessagingException("TLS not supported but required");
- }
- }
-
- mOut = new BufferedOutputStream(mOut, 1024);
-
- try {
- if (mSettings.getAuthType() == AuthType.CRAM_MD5) {
- authCramMD5();
- // The authCramMD5 method called on the previous line does not allow for handling updated capabilities
- // sent by the server. So, to make sure we update to the post-authentication capability list
- // we fetch the capabilities here.
+ mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
+ // Per RFC 2595 (3.1): Once TLS has been started, reissue CAPABILITY command
if (K9.DEBUG)
- Log.i(K9.LOG_TAG, "Updating capabilities after CRAM-MD5 authentication for " + getLogId());
+ Log.i(K9.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 if (mSettings.getAuthType() == AuthType.PLAIN) {
- receiveCapabilities(executeSimpleCommand(String.format("LOGIN %s %s", ImapStore.encodeString(mSettings.getUsername()), ImapStore.encodeString(mSettings.getPassword())), true));
+ } 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",
+ new CertificateException());
}
- authSuccess = true;
- } catch (ImapException ie) {
- throw new AuthenticationFailedException(ie.getAlertText(), ie);
-
- } catch (MessagingException me) {
- throw new AuthenticationFailedException(null, me);
}
+
+ 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;
+
+ default:
+ throw new MessagingException(
+ "Unhandled authentication method found in the server settings (bug).");
+ }
+ authSuccess = true;
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, CAPABILITY_COMPRESS_DEFLATE + " = " + hasCapability(CAPABILITY_COMPRESS_DEFLATE));
}
@@ -2664,48 +2654,123 @@ public class ImapStore extends Store {
}
}
- protected void authCramMD5() throws AuthenticationFailedException, MessagingException {
- try {
- String tag = sendCommand("AUTHENTICATE CRAM-MD5", false);
- byte[] buf = new byte[1024];
- int b64NonceLen = 0;
- for (int i = 0; i < buf.length; i++) {
- buf[i] = (byte)mIn.read();
- if (buf[i] == 0x0a) {
- b64NonceLen = i;
- break;
- }
+ protected void login() throws IOException, MessagingException {
+ boolean hasLiteralPlus = hasCapability(CAPABILITY_LITERAL_PLUS);
+ String tag;
+ byte[] username = mSettings.getUsername().getBytes();
+ byte[] password = mSettings.getPassword().getBytes();
+ tag = sendCommand(String.format(Locale.US, "LOGIN {%d%s}",
+ username.length, hasLiteralPlus ? "+" : ""), true);
+ if (!hasLiteralPlus) {
+ readContinuationResponse(tag);
}
- if (b64NonceLen == 0) {
- throw new AuthenticationFailedException("Error negotiating CRAM-MD5: nonce too long.");
+ mOut.write(username);
+ mOut.write(String.format(Locale.US, " {%d%s}\r\n", password.length,
+ hasLiteralPlus ? "+" : "").getBytes());
+ if (!hasLiteralPlus) {
+ mOut.flush();
+ readContinuationResponse(tag);
}
- byte[] b64NonceTrim = new byte[b64NonceLen - 2];
- System.arraycopy(buf, 1, b64NonceTrim, 0, b64NonceLen - 2);
-
- byte[] b64CRAM = Authentication.computeCramMd5Bytes(mSettings.getUsername(),
- mSettings.getPassword(), b64NonceTrim);
-
- mOut.write(b64CRAM);
- mOut.write(new byte[] { 0x0d, 0x0a });
+ mOut.write(password);
+ mOut.write('\r');
+ mOut.write('\n');
mOut.flush();
+ try {
+ receiveCapabilities(readStatusResponse(tag, "LOGIN", null));
+ } catch (MessagingException e) {
+ throw new AuthenticationFailedException(e.getMessage());
+ }
+ }
- int respLen = 0;
- for (int i = 0; i < buf.length; i++) {
- buf[i] = (byte)mIn.read();
- if (buf[i] == 0x0a) {
- respLen = i;
- break;
+ 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());
+ }
+ }
+
+ 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(K9.LOG_TAG, "After sending tag " + tag
+ + ", got tag response from previous command "
+ + response + " for " + getLogId());
}
}
+ } while (!response.mCommandContinuationRequested);
+ return response;
+ }
- String toMatch = tag + " OK";
- String respStr = new String(buf, 0, respLen);
- if (!respStr.startsWith(toMatch)) {
- throw new AuthenticationFailedException("CRAM-MD5 error: " + respStr);
+ protected ArrayList readStatusResponse(String tag,
+ String commandToLog, UntaggedHandler untaggedHandler)
+ throws IOException, MessagingException {
+ ArrayList responses = new ArrayList();
+ ImapResponse response;
+ do {
+ response = mParser.readResponse();
+ if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP)
+ Log.v(K9.LOG_TAG, getLogId() + "<<<" + response);
+
+ if (response.mTag != null && !response.mTag.equalsIgnoreCase(tag)) {
+ Log.w(K9.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;
}
- } catch (IOException ioe) {
- throw new AuthenticationFailedException("CRAM-MD5 Auth Failed.", ioe);
+ 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 {
@@ -2830,35 +2895,7 @@ public class ImapStore extends Store {
//if (K9.DEBUG)
// Log.v(K9.LOG_TAG, "Sent IMAP command " + commandToLog + " with tag " + tag + " for " + getLogId());
- ArrayList responses = new ArrayList();
- ImapResponse response;
- do {
- response = mParser.readResponse();
- if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP)
- Log.v(K9.LOG_TAG, getLogId() + "<<<" + response);
-
- if (response.mTag != null && !response.mTag.equalsIgnoreCase(tag)) {
- Log.w(K9.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;
+ return readStatusResponse(tag, commandToLog, untaggedHandler);
}
}
@@ -3102,6 +3139,7 @@ public class ImapStore extends Store {
if (stop.get()) {
Log.i(K9.LOG_TAG, "Got exception while idling, but stop is set for " + getLogId());
} else {
+ MessagingController.notifyUserIfCertificateProblem(K9.app, e, getAccount(), true);
receiver.pushError("Push error for " + getName(), e);
Log.e(K9.LOG_TAG, "Got exception while idling for " + getLogId(), e);
int delayTimeInt = delayTime.get();
diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java
index ee38e9a08..f50187a36 100644
--- a/src/com/fsck/k9/mail/store/Pop3Store.java
+++ b/src/com/fsck/k9/mail/store/Pop3Store.java
@@ -9,6 +9,8 @@ import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.*;
+import com.fsck.k9.mail.filter.Base64;
+import com.fsck.k9.mail.filter.Hex;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.net.ssl.TrustManagerFactory;
import com.fsck.k9.net.ssl.TrustedSocketFactory;
@@ -16,11 +18,16 @@ import com.fsck.k9.net.ssl.TrustedSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
+
import java.io.*;
import java.net.*;
import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import java.security.cert.CertificateException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.HashMap;
@@ -33,21 +40,11 @@ import java.util.Set;
public class Pop3Store extends Store {
public static final String STORE_TYPE = "POP3";
- public static final int CONNECTION_SECURITY_NONE = 0;
- public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
- public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
- public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
- public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
-
- private enum AuthType {
- PLAIN,
- CRAM_MD5
- }
-
private static final String STLS_COMMAND = "STLS";
private static final String USER_COMMAND = "USER";
private static final String PASS_COMMAND = "PASS";
private static final String CAPA_COMMAND = "CAPA";
+ private static final String AUTH_COMMAND = "AUTH";
private static final String STAT_COMMAND = "STAT";
private static final String LIST_COMMAND = "LIST";
private static final String UIDL_COMMAND = "UIDL";
@@ -58,20 +55,19 @@ public class Pop3Store extends Store {
private static final String STLS_CAPABILITY = "STLS";
private static final String UIDL_CAPABILITY = "UIDL";
- private static final String PIPELINING_CAPABILITY = "PIPELINING";
- private static final String USER_CAPABILITY = "USER";
private static final String TOP_CAPABILITY = "TOP";
+ private static final String SASL_CAPABILITY = "SASL";
+ private static final String AUTH_PLAIN_CAPABILITY = "PLAIN";
+ private static final String AUTH_CRAM_MD5_CAPABILITY = "CRAM-MD5";
/**
* Decodes a Pop3Store URI.
*
* Possible forms:
*
- * pop3://user:password@server:port CONNECTION_SECURITY_NONE
- * pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
- * pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
- * pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
- * pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+ * pop3://user:password@server:port ConnectionSecurity.NONE
+ * pop3+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
+ * pop3+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
*
*/
public static ServerSettings decodeUri(String uri) {
@@ -89,21 +85,27 @@ public class Pop3Store extends Store {
}
String scheme = pop3Uri.getScheme();
+ /*
+ * Currently available schemes are:
+ * pop3
+ * pop3+tls+
+ * pop3+ssl+
+ *
+ * The following are obsolete schemes that may be found in pre-existing
+ * settings from earlier versions or that may be found when imported. We
+ * continue to recognize them and re-map them appropriately:
+ * pop3+tls
+ * pop3+ssl
+ */
if (scheme.equals("pop3")) {
connectionSecurity = ConnectionSecurity.NONE;
port = 110;
- } else if (scheme.equals("pop3+tls")) {
- connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL;
- port = 110;
- } else if (scheme.equals("pop3+tls+")) {
+ } else if (scheme.startsWith("pop3+tls")) {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
port = 110;
- } else if (scheme.equals("pop3+ssl+")) {
+ } else if (scheme.startsWith("pop3+ssl")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
port = 995;
- } else if (scheme.equals("pop3+ssl")) {
- connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL;
- port = 995;
} else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
}
@@ -114,7 +116,7 @@ public class Pop3Store extends Store {
port = pop3Uri.getPort();
}
- String authType = AuthType.PLAIN.name();
+ AuthType authType = AuthType.PLAIN;
if (pop3Uri.getUserInfo() != null) {
try {
int userIndex = 0, passwordIndex = 1;
@@ -125,7 +127,7 @@ public class Pop3Store extends Store {
// after an account was imported (so authType and username are present).
userIndex++;
passwordIndex++;
- authType = userInfoParts[0];
+ authType = AuthType.valueOf(userInfoParts[0]);
}
username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
if (userInfoParts.length > passwordIndex) {
@@ -166,15 +168,9 @@ public class Pop3Store extends Store {
String scheme;
switch (server.connectionSecurity) {
- case SSL_TLS_OPTIONAL:
- scheme = "pop3+ssl";
- break;
case SSL_TLS_REQUIRED:
scheme = "pop3+ssl+";
break;
- case STARTTLS_OPTIONAL:
- scheme = "pop3+tls";
- break;
case STARTTLS_REQUIRED:
scheme = "pop3+tls+";
break;
@@ -184,14 +180,7 @@ public class Pop3Store extends Store {
break;
}
- try {
- AuthType.valueOf(server.authenticationType);
- } catch (Exception e) {
- throw new IllegalArgumentException("Invalid authentication type (" +
- server.authenticationType + ")");
- }
-
- String userInfo = server.authenticationType + ":" + userEnc + ":" + passwordEnc;
+ String userInfo = server.authenticationType.name() + ":" + userEnc + ":" + passwordEnc;
try {
return new URI(scheme, userInfo, server.host, server.port, null, null,
null).toString();
@@ -206,7 +195,7 @@ public class Pop3Store extends Store {
private String mUsername;
private String mPassword;
private AuthType mAuthType;
- private int mConnectionSecurity;
+ private ConnectionSecurity mConnectionSecurity;
private HashMap mFolders = new HashMap();
private Pop3Capabilities mCapabilities;
@@ -231,27 +220,11 @@ public class Pop3Store extends Store {
mHost = settings.host;
mPort = settings.port;
- switch (settings.connectionSecurity) {
- case NONE:
- mConnectionSecurity = CONNECTION_SECURITY_NONE;
- break;
- case STARTTLS_OPTIONAL:
- mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
- break;
- case STARTTLS_REQUIRED:
- mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
- break;
- case SSL_TLS_OPTIONAL:
- mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
- break;
- case SSL_TLS_REQUIRED:
- mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
- break;
- }
+ mConnectionSecurity = settings.connectionSecurity;
mUsername = settings.username;
mPassword = settings.password;
- mAuthType = AuthType.valueOf(settings.authenticationType);
+ mAuthType = settings.authenticationType;
}
@Override
@@ -327,13 +300,11 @@ public class Pop3Store extends Store {
try {
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
- if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
- mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) {
+ if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS");
- final boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(mHost,
- mPort, secure) }, new SecureRandom());
+ mPort) }, new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
} else {
mSocket = new Socket();
@@ -348,21 +319,18 @@ public class Pop3Store extends Store {
throw new MessagingException("Unable to connect socket");
}
- // Eat the banner
- executeSimpleCommand(null);
+ String serverGreeting = executeSimpleCommand(null);
- if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL
- || mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
- mCapabilities = getCapabilities();
+ mCapabilities = getCapabilities();
+ if (mConnectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) {
if (mCapabilities.stls) {
executeSimpleCommand(STLS_COMMAND);
SSLContext sslContext = SSLContext.getInstance("TLS");
- boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED;
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(
- mHost, mPort, secure) },
+ mHost, mPort) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
mPort, true);
@@ -372,31 +340,42 @@ public class Pop3Store extends Store {
if (!isOpen()) {
throw new MessagingException("Unable to connect socket");
}
- } else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
- throw new MessagingException("TLS not supported but required");
+ mCapabilities = getCapabilities();
+ } 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",
+ new CertificateException());
}
}
- if (mAuthType == AuthType.CRAM_MD5) {
- try {
- String b64Nonce = executeSimpleCommand("AUTH CRAM-MD5").replace("+ ", "");
-
- String b64CRAM = Authentication.computeCramMd5(mUsername, mPassword, b64Nonce);
- executeSimpleCommand(b64CRAM);
-
- } catch (MessagingException me) {
- throw new AuthenticationFailedException(null, me);
+ switch (mAuthType) {
+ case PLAIN:
+ if (mCapabilities.authPlain) {
+ authPlain();
+ } else {
+ login();
}
- } else {
- try {
- executeSimpleCommand(USER_COMMAND + " " + mUsername);
- executeSimpleCommand(PASS_COMMAND + " " + mPassword, true);
- } catch (MessagingException me) {
- throw new AuthenticationFailedException(null, me);
+ break;
+
+ case CRAM_MD5:
+ if (mCapabilities.cramMD5) {
+ authCramMD5();
+ } else {
+ authAPOP(serverGreeting);
}
+ break;
+
+ default:
+ throw new MessagingException(
+ "Unhandled authentication method found in the server settings (bug).");
}
-
- mCapabilities = getCapabilities();
} catch (SSLException e) {
throw new CertificateValidationException(e.getMessage(), e);
} catch (GeneralSecurityException gse) {
@@ -415,6 +394,67 @@ public class Pop3Store extends Store {
mUidToMsgNumMap.clear();
}
+ private void login() throws MessagingException {
+ executeSimpleCommand(USER_COMMAND + " " + mUsername);
+ try {
+ executeSimpleCommand(PASS_COMMAND + " " + mPassword, true);
+ } catch (Pop3ErrorResponse e) {
+ throw new AuthenticationFailedException(
+ "POP3 login authentication failed: " + e.getMessage(), e);
+ }
+ }
+
+ private void authPlain() throws MessagingException {
+ executeSimpleCommand("AUTH PLAIN");
+ try {
+ byte[] encodedBytes = Base64.encodeBase64(("\000" + mUsername
+ + "\000" + mPassword).getBytes());
+ executeSimpleCommand(new String(encodedBytes), true);
+ } catch (Pop3ErrorResponse e) {
+ throw new AuthenticationFailedException(
+ "POP3 SASL auth PLAIN authentication failed: "
+ + e.getMessage(), e);
+ }
+ }
+
+ private void authAPOP(String serverGreeting) throws MessagingException {
+ // regex based on RFC 2449 (3.) "Greeting"
+ String timestamp = serverGreeting.replaceFirst(
+ "^\\+OK *(?:\\[[^\\]]+\\])?[^<]*(<[^>]*>)?[^<]*$", "$1");
+ if ("".equals(timestamp)) {
+ throw new MessagingException(
+ "APOP authentication is not supported");
+ }
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("MD5");
+ } catch (NoSuchAlgorithmException e) {
+ throw new MessagingException(
+ "MD5 failure during POP3 auth APOP", e);
+ }
+ byte[] digest = md.digest((timestamp + mPassword).getBytes());
+ String hexDigest = new String(Hex.encodeHex(digest));
+ try {
+ executeSimpleCommand("APOP " + mUsername + " " + hexDigest, true);
+ } catch (Pop3ErrorResponse e) {
+ throw new AuthenticationFailedException(
+ "POP3 APOP authentication failed: " + e.getMessage(), e);
+ }
+ }
+
+ private void authCramMD5() throws MessagingException {
+ String b64Nonce = executeSimpleCommand("AUTH CRAM-MD5").replace("+ ", "");
+
+ String b64CRAM = Authentication.computeCramMd5(mUsername, mPassword, b64Nonce);
+ try {
+ executeSimpleCommand(b64CRAM, true);
+ } catch (Pop3ErrorResponse e) {
+ throw new AuthenticationFailedException(
+ "POP3 CRAM-MD5 authentication failed: "
+ + e.getMessage(), e);
+ }
+ }
+
@Override
public boolean isOpen() {
return (mIn != null && mOut != null && mSocket != null
@@ -984,22 +1024,54 @@ public class Pop3Store extends Store {
private Pop3Capabilities getCapabilities() throws IOException {
Pop3Capabilities capabilities = new Pop3Capabilities();
+ try {
+ /*
+ * Try sending an AUTH command with no arguments.
+ *
+ * The server may respond with a list of supported SASL
+ * authentication mechanisms.
+ *
+ * Ref.: http://tools.ietf.org/html/draft-myers-sasl-pop3-05
+ *
+ * While this never became a standard, there are servers that
+ * support it, and Thunderbird includes this check.
+ */
+ String response = executeSimpleCommand(AUTH_COMMAND);
+ while ((response = readLine()) != null) {
+ if (response.equals(".")) {
+ break;
+ }
+ response = response.toUpperCase(Locale.US);
+ if (response.equals(AUTH_PLAIN_CAPABILITY)) {
+ capabilities.authPlain = true;
+ } else if (response.equals(AUTH_CRAM_MD5_CAPABILITY)) {
+ capabilities.cramMD5 = true;
+ }
+ }
+ } catch (MessagingException e) {
+ // Assume AUTH command with no arguments is not supported.
+ }
try {
String response = executeSimpleCommand(CAPA_COMMAND);
while ((response = readLine()) != null) {
if (response.equals(".")) {
break;
}
- if (response.equalsIgnoreCase(STLS_CAPABILITY)) {
+ response = response.toUpperCase(Locale.US);
+ if (response.equals(STLS_CAPABILITY)) {
capabilities.stls = true;
- } else if (response.equalsIgnoreCase(UIDL_CAPABILITY)) {
+ } else if (response.equals(UIDL_CAPABILITY)) {
capabilities.uidl = true;
- } else if (response.equalsIgnoreCase(PIPELINING_CAPABILITY)) {
- capabilities.pipelining = true;
- } else if (response.equalsIgnoreCase(USER_CAPABILITY)) {
- capabilities.user = true;
- } else if (response.equalsIgnoreCase(TOP_CAPABILITY)) {
+ } else if (response.equals(TOP_CAPABILITY)) {
capabilities.top = true;
+ } else if (response.startsWith(SASL_CAPABILITY)) {
+ List saslAuthMechanisms = Arrays.asList(response.split(" "));
+ if (saslAuthMechanisms.contains(AUTH_PLAIN_CAPABILITY)) {
+ capabilities.authPlain = true;
+ }
+ if (saslAuthMechanisms.contains(AUTH_CRAM_MD5_CAPABILITY)) {
+ capabilities.cramMD5 = true;
+ }
}
}
@@ -1116,20 +1188,20 @@ public class Pop3Store extends Store {
}
static class Pop3Capabilities {
+ public boolean cramMD5;
+ public boolean authPlain;
public boolean stls;
public boolean top;
- public boolean user;
public boolean uidl;
- public boolean pipelining;
@Override
public String toString() {
- return String.format("STLS %b, TOP %b, USER %b, UIDL %b, PIPELINING %b",
+ return String.format("CRAM-MD5 %b, PLAIN %b, STLS %b, TOP %b, UIDL %b",
+ cramMD5,
+ authPlain,
stls,
top,
- user,
- uidl,
- pipelining);
+ uidl);
}
}
diff --git a/src/com/fsck/k9/mail/store/WebDavSocketFactory.java b/src/com/fsck/k9/mail/store/WebDavSocketFactory.java
index 9563f510e..73f110e4e 100644
--- a/src/com/fsck/k9/mail/store/WebDavSocketFactory.java
+++ b/src/com/fsck/k9/mail/store/WebDavSocketFactory.java
@@ -31,7 +31,7 @@ public class WebDavSocketFactory implements LayeredSocketFactory {
public WebDavSocketFactory(String host, int port, boolean secure) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {
- TrustManagerFactory.get(host, port, secure)
+ TrustManagerFactory.get(host, port)
}, new SecureRandom());
mSocketFactory = sslContext.getSocketFactory();
mSchemeSocketFactory = org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory();
diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java
index ea33e917f..7b50984ae 100644
--- a/src/com/fsck/k9/mail/store/WebDavStore.java
+++ b/src/com/fsck/k9/mail/store/WebDavStore.java
@@ -57,13 +57,6 @@ import java.util.zip.GZIPInputStream;
public class WebDavStore extends Store {
public static final String STORE_TYPE = "WebDAV";
- // Security options
- private static final short CONNECTION_SECURITY_NONE = 0;
- private static final short CONNECTION_SECURITY_TLS_OPTIONAL = 1;
- private static final short CONNECTION_SECURITY_TLS_REQUIRED = 2;
- private static final short CONNECTION_SECURITY_SSL_OPTIONAL = 3;
- private static final short CONNECTION_SECURITY_SSL_REQUIRED = 4;
-
// Authentication types
private static final short AUTH_TYPE_NONE = 0;
private static final short AUTH_TYPE_BASIC = 1;
@@ -89,11 +82,8 @@ public class WebDavStore extends Store {
*
* Possible forms:
*
- * webdav://user:password@server:port CONNECTION_SECURITY_NONE
- * webdav+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
- * webdav+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
- * webdav+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
- * webdav+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+ * webdav://user:password@server:port ConnectionSecurity.NONE
+ * webdav+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
*
*/
public static WebDavStoreSettings decodeUri(String uri) {
@@ -116,16 +106,22 @@ public class WebDavStore extends Store {
}
String scheme = webDavUri.getScheme();
+ /*
+ * Currently available schemes are:
+ * webdav
+ * webdav+ssl+
+ *
+ * The following are obsolete schemes that may be found in pre-existing
+ * settings from earlier versions or that may be found when imported. We
+ * continue to recognize them and re-map them appropriately:
+ * webdav+tls
+ * webdav+tls+
+ * webdav+ssl
+ */
if (scheme.equals("webdav")) {
connectionSecurity = ConnectionSecurity.NONE;
- } else if (scheme.equals("webdav+ssl")) {
- connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL;
- } else if (scheme.equals("webdav+ssl+")) {
+ } else if (scheme.startsWith("webdav+")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
- } else if (scheme.equals("webdav+tls")) {
- connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL;
- } else if (scheme.equals("webdav+tls+")) {
- connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
} else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
}
@@ -210,18 +206,9 @@ public class WebDavStore extends Store {
String scheme;
switch (server.connectionSecurity) {
- case SSL_TLS_OPTIONAL:
- scheme = "webdav+ssl";
- break;
case SSL_TLS_REQUIRED:
scheme = "webdav+ssl+";
break;
- case STARTTLS_OPTIONAL:
- scheme = "webdav+tls";
- break;
- case STARTTLS_REQUIRED:
- scheme = "webdav+tls+";
- break;
default:
case NONE:
scheme = "webdav";
@@ -270,7 +257,7 @@ public class WebDavStore extends Store {
public final String mailboxPath;
protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
- String authenticationType, String username, String password, String alias,
+ AuthType authenticationType, String username, String password, String alias,
String path, String authPath, String mailboxPath) {
super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
password);
@@ -298,7 +285,7 @@ public class WebDavStore extends Store {
}
- private short mConnectionSecurity;
+ private ConnectionSecurity mConnectionSecurity;
private String mUsername; /* Stores the username for authentications */
private String mAlias; /* Stores the alias for the user's mailbox */
private String mPassword; /* Stores the password for authentications */
@@ -334,23 +321,7 @@ public class WebDavStore extends Store {
mHost = settings.host;
mPort = settings.port;
- switch (settings.connectionSecurity) {
- case NONE:
- mConnectionSecurity = CONNECTION_SECURITY_NONE;
- break;
- case STARTTLS_OPTIONAL:
- mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
- break;
- case STARTTLS_REQUIRED:
- mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
- break;
- case SSL_TLS_OPTIONAL:
- mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
- break;
- case SSL_TLS_REQUIRED:
- mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
- break;
- }
+ mConnectionSecurity = settings.connectionSecurity;
mUsername = settings.username;
mPassword = settings.password;
@@ -383,16 +354,13 @@ public class WebDavStore extends Store {
// The inbox path would look like: "https://mail.domain.com/Exchange/alias/Inbox".
mUrl = getRoot() + mPath + mMailboxPath;
- mSecure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
+ mSecure = mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED;
mAuthString = "Basic " + Utility.base64Encode(mUsername + ":" + mPassword);
}
private String getRoot() {
String root;
- if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED ||
- mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
- mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL ||
- mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) {
+ if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
root = "https";
} else {
root = "http";
diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java
index d7478c89b..286253b5c 100644
--- a/src/com/fsck/k9/mail/transport/SmtpTransport.java
+++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java
@@ -19,6 +19,7 @@ import com.fsck.k9.net.ssl.TrustedSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManager;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
@@ -27,44 +28,28 @@ import java.io.UnsupportedEncodingException;
import java.net.*;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
+import java.security.cert.CertificateException;
import java.util.*;
public class SmtpTransport extends Transport {
public static final String TRANSPORT_TYPE = "SMTP";
- public static final int CONNECTION_SECURITY_NONE = 0;
- public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
- public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
- public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
- public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
-
- public static final String AUTH_PLAIN = "PLAIN";
-
- public static final String AUTH_CRAM_MD5 = "CRAM_MD5";
-
- public static final String AUTH_LOGIN = "LOGIN";
-
- public static final String AUTH_AUTOMATIC = "AUTOMATIC";
-
-
/**
* Decodes a SmtpTransport URI.
*
* Possible forms:
*
- * smtp://user:password@server:port CONNECTION_SECURITY_NONE
- * smtp+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
- * smtp+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
- * smtp+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
- * smtp+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+ * smtp://user:password@server:port ConnectionSecurity.NONE
+ * smtp+tls+://user:password@server:port ConnectionSecurity.STARTTLS_REQUIRED
+ * smtp+ssl+://user:password@server:port ConnectionSecurity.SSL_TLS_REQUIRED
*
*/
public static ServerSettings decodeUri(String uri) {
String host;
int port;
ConnectionSecurity connectionSecurity;
- String authenticationType = AUTH_AUTOMATIC;
+ AuthType authType = AuthType.PLAIN;
String username = null;
String password = null;
@@ -76,21 +61,27 @@ public class SmtpTransport extends Transport {
}
String scheme = smtpUri.getScheme();
+ /*
+ * Currently available schemes are:
+ * smtp
+ * smtp+tls+
+ * smtp+ssl+
+ *
+ * The following are obsolete schemes that may be found in pre-existing
+ * settings from earlier versions or that may be found when imported. We
+ * continue to recognize them and re-map them appropriately:
+ * smtp+tls
+ * smtp+ssl
+ */
if (scheme.equals("smtp")) {
connectionSecurity = ConnectionSecurity.NONE;
port = 587;
- } else if (scheme.equals("smtp+tls")) {
- connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL;
- port = 587;
- } else if (scheme.equals("smtp+tls+")) {
+ } else if (scheme.startsWith("smtp+tls")) {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
port = 587;
- } else if (scheme.equals("smtp+ssl+")) {
+ } else if (scheme.startsWith("smtp+ssl")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
port = 465;
- } else if (scheme.equals("smtp+ssl")) {
- connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL;
- port = 465;
} else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
}
@@ -109,7 +100,7 @@ public class SmtpTransport extends Transport {
password = URLDecoder.decode(userInfoParts[1], "UTF-8");
}
if (userInfoParts.length > 2) {
- authenticationType = userInfoParts[2];
+ authType = AuthType.valueOf(userInfoParts[2]);
}
} catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8
@@ -118,7 +109,7 @@ public class SmtpTransport extends Transport {
}
return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity,
- authenticationType, username, password);
+ authType, username, password);
}
/**
@@ -147,15 +138,9 @@ public class SmtpTransport extends Transport {
String scheme;
switch (server.connectionSecurity) {
- case SSL_TLS_OPTIONAL:
- scheme = "smtp+ssl";
- break;
case SSL_TLS_REQUIRED:
scheme = "smtp+ssl+";
break;
- case STARTTLS_OPTIONAL:
- scheme = "smtp+tls";
- break;
case STARTTLS_REQUIRED:
scheme = "smtp+tls+";
break;
@@ -165,15 +150,11 @@ public class SmtpTransport extends Transport {
break;
}
- String authType = server.authenticationType;
- if (!(AUTH_AUTOMATIC.equals(authType) ||
- AUTH_LOGIN.equals(authType) ||
- AUTH_PLAIN.equals(authType) ||
- AUTH_CRAM_MD5.equals(authType))) {
- throw new IllegalArgumentException("Invalid authentication type: " + authType);
+ String userInfo = userEnc + ":" + passwordEnc;
+ AuthType authType = server.authenticationType;
+ if (authType != null) {
+ userInfo += ":" + authType.name();
}
-
- String userInfo = userEnc + ":" + passwordEnc + ":" + authType;
try {
return new URI(scheme, userInfo, server.host, server.port, null, null,
null).toString();
@@ -187,8 +168,8 @@ public class SmtpTransport extends Transport {
int mPort;
String mUsername;
String mPassword;
- String mAuthType;
- int mConnectionSecurity;
+ AuthType mAuthType;
+ ConnectionSecurity mConnectionSecurity;
Socket mSocket;
PeekableInputStream mIn;
OutputStream mOut;
@@ -206,23 +187,7 @@ public class SmtpTransport extends Transport {
mHost = settings.host;
mPort = settings.port;
- switch (settings.connectionSecurity) {
- case NONE:
- mConnectionSecurity = CONNECTION_SECURITY_NONE;
- break;
- case STARTTLS_OPTIONAL:
- mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
- break;
- case STARTTLS_REQUIRED:
- mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
- break;
- case SSL_TLS_OPTIONAL:
- mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
- break;
- case SSL_TLS_REQUIRED:
- mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
- break;
- }
+ mConnectionSecurity = settings.connectionSecurity;
mAuthType = settings.authenticationType;
mUsername = settings.username;
@@ -232,20 +197,20 @@ public class SmtpTransport extends Transport {
@Override
public void open() throws MessagingException {
try {
+ boolean secureConnection = false;
InetAddress[] addresses = InetAddress.getAllByName(mHost);
for (int i = 0; i < addresses.length; i++) {
try {
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
- if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
- mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) {
+ if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
SSLContext sslContext = SSLContext.getInstance("TLS");
- boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(
- mHost, mPort, secure) },
+ mHost, mPort) },
new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext);
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
+ secureConnection = true;
} else {
mSocket = new Socket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
@@ -264,7 +229,7 @@ public class SmtpTransport extends Transport {
mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024));
- mOut = mSocket.getOutputStream();
+ mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
// Eat the banner
executeSimpleCommand(null);
@@ -288,104 +253,128 @@ public class SmtpTransport extends Transport {
}
}
- List results = sendHello(localHost);
+ HashMap extensions = sendHello(localHost);
- m8bitEncodingAllowed = results.contains("8BITMIME");
+ m8bitEncodingAllowed = extensions.containsKey("8BITMIME");
- if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL
- || mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
- if (results.contains("STARTTLS")) {
+ if (mConnectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) {
+ if (extensions.containsKey("STARTTLS")) {
executeSimpleCommand("STARTTLS");
SSLContext sslContext = SSLContext.getInstance("TLS");
- boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED;
sslContext.init(null,
new TrustManager[] { TrustManagerFactory.get(mHost,
- mPort, secure) }, new SecureRandom());
+ mPort) }, new SecureRandom());
mSocket = TrustedSocketFactory.createSocket(sslContext, mSocket, mHost,
mPort, true);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
1024));
- mOut = mSocket.getOutputStream();
+ mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
/*
* Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
* Exim.
*/
- results = sendHello(localHost);
- } else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
- throw new MessagingException("TLS not supported but required");
+ extensions = sendHello(localHost);
+ secureConnection = true;
+ } 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",
+ new CertificateException());
}
}
- boolean useAuthLogin = AUTH_LOGIN.equals(mAuthType);
- boolean useAuthPlain = AUTH_PLAIN.equals(mAuthType);
- boolean useAuthCramMD5 = AUTH_CRAM_MD5.equals(mAuthType);
-
- // Automatically choose best authentication method if none was explicitly selected
- boolean useAutomaticAuth = !(useAuthLogin || useAuthPlain || useAuthCramMD5);
-
boolean authLoginSupported = false;
boolean authPlainSupported = false;
boolean authCramMD5Supported = false;
- for (String result : results) {
- if (result.matches(".*AUTH.*LOGIN.*$")) {
- authLoginSupported = true;
- }
- if (result.matches(".*AUTH.*PLAIN.*$")) {
- authPlainSupported = true;
- }
- if (result.matches(".*AUTH.*CRAM-MD5.*$")) {
- authCramMD5Supported = true;
- }
- if (result.matches(".*SIZE \\d*$")) {
- try {
- mLargestAcceptableMessage = Integer.parseInt(result.substring(result.lastIndexOf(' ') + 1));
- } catch (Exception e) {
- if (K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) {
- Log.d(K9.LOG_TAG, "Tried to parse " + result + " and get an int out of the last word", e);
- }
+ if (extensions.containsKey("AUTH")) {
+ List saslMech = Arrays.asList(extensions.get("AUTH").split(" "));
+ authLoginSupported = saslMech.contains("LOGIN");
+ authPlainSupported = saslMech.contains("PLAIN");
+ authCramMD5Supported = saslMech.contains("CRAM-MD5");
+ }
+ if (extensions.containsKey("SIZE")) {
+ try {
+ mLargestAcceptableMessage = Integer.parseInt(extensions.get("SIZE"));
+ } catch (Exception e) {
+ if (K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) {
+ Log.d(K9.LOG_TAG, "Tried to parse " + extensions.get("SIZE") + " and get an int", e);
}
}
}
if (mUsername != null && mUsername.length() > 0 &&
mPassword != null && mPassword.length() > 0) {
- if (useAuthCramMD5 || (useAutomaticAuth && authCramMD5Supported)) {
- if (!authCramMD5Supported && K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) {
- Log.d(K9.LOG_TAG, "Using CRAM_MD5 as authentication method although the " +
- "server didn't advertise support for it in EHLO response.");
- }
- saslAuthCramMD5(mUsername, mPassword);
- } else if (useAuthPlain || (useAutomaticAuth && authPlainSupported)) {
- if (!authPlainSupported && K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) {
- Log.d(K9.LOG_TAG, "Using PLAIN as authentication method although the " +
- "server didn't advertise support for it in EHLO response.");
- }
- try {
+
+ switch (mAuthType) {
+
+ /*
+ * LOGIN is an obsolete option which is unavailable to users,
+ * but it still may exist in a user's settings from a previous
+ * version, or it may have been imported.
+ */
+ case LOGIN:
+ case PLAIN:
+ // try saslAuthPlain first, because it supports UTF-8 explicitly
+ if (authPlainSupported) {
saslAuthPlain(mUsername, mPassword);
- } catch (MessagingException ex) {
- // PLAIN is a special case. Historically, PLAIN has represented both PLAIN and LOGIN; only the
- // protocol being advertised by the server would be used, with PLAIN taking precedence. Instead
- // of using only the requested protocol, we'll try PLAIN and then try LOGIN.
- if (useAuthPlain && authLoginSupported) {
- if (K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) {
- Log.d(K9.LOG_TAG, "Using legacy PLAIN authentication behavior and trying LOGIN.");
- }
+ } else if (authLoginSupported) {
+ saslAuthLogin(mUsername, mPassword);
+ } else {
+ throw new MessagingException("Authentication methods SASL PLAIN and LOGIN are unavailable.");
+ }
+ break;
+
+ case CRAM_MD5:
+ if (authCramMD5Supported) {
+ saslAuthCramMD5(mUsername, mPassword);
+ } else {
+ throw new MessagingException("Authentication method CRAM-MD5 is unavailable.");
+ }
+ break;
+
+ /*
+ * AUTOMATIC is an obsolete option which is unavailable to users,
+ * but it still may exist in a user's settings from a previous
+ * version, or it may have been imported.
+ */
+ case AUTOMATIC:
+ if (secureConnection) {
+ // try saslAuthPlain first, because it supports UTF-8 explicitly
+ if (authPlainSupported) {
+ saslAuthPlain(mUsername, mPassword);
+ } else if (authLoginSupported) {
saslAuthLogin(mUsername, mPassword);
+ } else if (authCramMD5Supported) {
+ saslAuthCramMD5(mUsername, mPassword);
} else {
- // If it was auto detected and failed, continue throwing the exception back up.
- throw ex;
+ throw new MessagingException("No supported authentication methods available.");
+ }
+ } else {
+ if (authCramMD5Supported) {
+ saslAuthCramMD5(mUsername, mPassword);
+ } else {
+ /*
+ * We refuse to insecurely transmit the password
+ * using the obsolete AUTOMATIC setting because of
+ * the potential for a MITM attack. Affected users
+ * must choose a different setting.
+ */
+ throw new MessagingException(
+ "Update your outgoing server authentication setting. AUTOMATIC auth. is unavailable.");
}
}
- } else if (useAuthLogin || (useAutomaticAuth && authLoginSupported)) {
- if (!authPlainSupported && K9.DEBUG && K9.DEBUG_PROTOCOL_SMTP) {
- Log.d(K9.LOG_TAG, "Using LOGIN as authentication method although the " +
- "server didn't advertise support for it in EHLO response.");
- }
- saslAuthLogin(mUsername, mPassword);
- } else {
- throw new MessagingException("No valid authentication mechanism found.");
+ break;
+
+ default:
+ throw new MessagingException("Unhandled authentication method found in the server settings (bug).");
}
}
} catch (SSLException e) {
@@ -410,19 +399,24 @@ public class SmtpTransport extends Transport {
* @param host
* The EHLO/HELO parameter as defined by the RFC.
*
- * @return The list of capabilities as returned by the EHLO command or an empty list.
+ * @return A (possibly empty) {@code HashMap} of extensions (upper case) and
+ * their parameters (possibly 0 length) as returned by the EHLO command
*
* @throws IOException
* In case of a network error.
* @throws MessagingException
* In case of a malformed response.
*/
- private List sendHello(String host) throws IOException, MessagingException {
+ private HashMap sendHello(String host) throws IOException, MessagingException {
+ HashMap extensions = new HashMap();
try {
- //TODO: We currently assume the extension keywords returned by the server are always
- // uppercased. But the RFC allows mixed-case keywords!
-
- return executeSimpleCommand("EHLO " + host);
+ List results = executeSimpleCommand("EHLO " + host);
+ // Remove the EHLO greeting response
+ results.remove(0);
+ for (String result : results) {
+ String[] pair = result.split(" ", 2);
+ extensions.put(pair[0].toUpperCase(Locale.US), pair.length == 1 ? "" : pair[1]);
+ }
} catch (NegativeSmtpReplyException e) {
if (K9.DEBUG) {
Log.v(K9.LOG_TAG, "Server doesn't support the EHLO command. Trying HELO...");
@@ -434,8 +428,7 @@ public class SmtpTransport extends Transport {
Log.w(K9.LOG_TAG, "Server doesn't support the HELO command. Continuing anyway.");
}
}
-
- return new ArrayList(0);
+ return extensions;
}
@Override
@@ -500,8 +493,7 @@ public class SmtpTransport extends Transport {
executeSimpleCommand("DATA");
EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream(
- new LineWrapOutputStream(new SmtpDataStuffing(
- new BufferedOutputStream(mOut, 1024)), 1000));
+ new LineWrapOutputStream(new SmtpDataStuffing(mOut), 1000));
message.writeTo(msgOut);
@@ -714,7 +706,7 @@ public class SmtpTransport extends Transport {
List respList = executeSimpleCommand("AUTH CRAM-MD5");
if (respList.size() != 1) {
- throw new AuthenticationFailedException("Unable to negotiate CRAM-MD5");
+ throw new MessagingException("Unable to negotiate CRAM-MD5");
}
String b64Nonce = respList.get(0);
@@ -722,8 +714,8 @@ public class SmtpTransport extends Transport {
try {
executeSimpleCommand(b64CRAMString, true);
- } catch (MessagingException me) {
- throw new AuthenticationFailedException("Unable to negotiate MD5 CRAM");
+ } catch (NegativeSmtpReplyException exception) {
+ throw new AuthenticationFailedException(exception.getMessage(), exception);
}
}
diff --git a/src/com/fsck/k9/mail/transport/imap/ImapSettings.java b/src/com/fsck/k9/mail/transport/imap/ImapSettings.java
index 63c22f6d9..dc765d54f 100644
--- a/src/com/fsck/k9/mail/transport/imap/ImapSettings.java
+++ b/src/com/fsck/k9/mail/transport/imap/ImapSettings.java
@@ -1,7 +1,8 @@
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.AuthType;
import com.fsck.k9.mail.store.ImapStore.ImapConnection;
/**
@@ -12,7 +13,7 @@ public interface ImapSettings {
int getPort();
- int getConnectionSecurity();
+ ConnectionSecurity getConnectionSecurity();
AuthType getAuthType();
diff --git a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java b/src/com/fsck/k9/net/ssl/TrustManagerFactory.java
index 6b6b54138..27b2c70bb 100644
--- a/src/com/fsck/k9/net/ssl/TrustManagerFactory.java
+++ b/src/com/fsck/k9/net/ssl/TrustManagerFactory.java
@@ -21,23 +21,9 @@ public final class TrustManagerFactory {
private static final String LOG_TAG = "TrustManagerFactory";
private static X509TrustManager defaultTrustManager;
- private static X509TrustManager unsecureTrustManager;
private static LocalKeyStore keyStore;
- private static class SimpleX509TrustManager implements X509TrustManager {
- public void checkClientTrusted(X509Certificate[] chain, String authType)
- throws CertificateException {
- }
-
- public void checkServerTrusted(X509Certificate[] chain, String authType)
- throws CertificateException {
- }
-
- public X509Certificate[] getAcceptedIssuers() {
- return null;
- }
- }
private static class SecureX509TrustManager implements X509TrustManager {
private static final Map mTrustManager =
@@ -126,14 +112,12 @@ public final class TrustManagerFactory {
} catch (KeyStoreException e) {
Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
}
- unsecureTrustManager = new SimpleX509TrustManager();
}
private TrustManagerFactory() {
}
- public static X509TrustManager get(String host, int port, boolean secure) {
- return secure ? SecureX509TrustManager.getInstance(host, port) :
- unsecureTrustManager;
+ public static X509TrustManager get(String host, int port) {
+ return SecureX509TrustManager.getInstance(host, port);
}
}
diff --git a/src/com/fsck/k9/preferences/SettingsExporter.java b/src/com/fsck/k9/preferences/SettingsExporter.java
index 2497376aa..b09ef359d 100644
--- a/src/com/fsck/k9/preferences/SettingsExporter.java
+++ b/src/com/fsck/k9/preferences/SettingsExporter.java
@@ -230,7 +230,7 @@ public class SettingsExporter {
writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port));
}
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name());
- writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType);
+ writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name());
writeElement(serializer, USERNAME_ELEMENT, incoming.username);
// XXX For now we don't export the password
//writeElement(serializer, PASSWORD_ELEMENT, incoming.password);
@@ -257,7 +257,7 @@ public class SettingsExporter {
writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port));
}
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name());
- writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType);
+ writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name());
writeElement(serializer, USERNAME_ELEMENT, outgoing.username);
// XXX For now we don't export the password
//writeElement(serializer, PASSWORD_ELEMENT, outgoing.password);
diff --git a/src/com/fsck/k9/preferences/SettingsImporter.java b/src/com/fsck/k9/preferences/SettingsImporter.java
index 2a774f64f..02458b919 100644
--- a/src/com/fsck/k9/preferences/SettingsImporter.java
+++ b/src/com/fsck/k9/preferences/SettingsImporter.java
@@ -23,6 +23,7 @@ import com.fsck.k9.Identity;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store;
@@ -971,7 +972,8 @@ public class SettingsImporter {
} else if (SettingsExporter.CONNECTION_SECURITY_ELEMENT.equals(element)) {
server.connectionSecurity = getText(xpp);
} else if (SettingsExporter.AUTHENTICATION_TYPE_ELEMENT.equals(element)) {
- server.authenticationType = getText(xpp);
+ String text = getText(xpp);
+ server.authenticationType = AuthType.valueOf(text);
} else if (SettingsExporter.USERNAME_ELEMENT.equals(element)) {
server.username = getText(xpp);
} else if (SettingsExporter.PASSWORD_ELEMENT.equals(element)) {
@@ -1108,6 +1110,16 @@ public class SettingsImporter {
private static ConnectionSecurity convertConnectionSecurity(String connectionSecurity) {
try {
+ /*
+ * TODO:
+ * Add proper settings validation and upgrade capability for server settings.
+ * Once that exists, move this code into a SettingsUpgrader.
+ */
+ if ("SSL_TLS_OPTIONAL".equals(connectionSecurity)) {
+ return ConnectionSecurity.SSL_TLS_REQUIRED;
+ } else if ("STARTTLS_OPTIONAL".equals(connectionSecurity)) {
+ return ConnectionSecurity.STARTTLS_REQUIRED;
+ }
return ConnectionSecurity.valueOf(connectionSecurity);
} catch (Exception e) {
return ConnectionSecurity.NONE;
@@ -1140,7 +1152,7 @@ public class SettingsImporter {
public String host;
public String port;
public String connectionSecurity;
- public String authenticationType;
+ public AuthType authenticationType;
public String username;
public String password;
public ImportedSettings extras;
diff --git a/tests/src/com/fsck/k9/mail/store/ImapStoreUriTest.java b/tests/src/com/fsck/k9/mail/store/ImapStoreUriTest.java
index 4f5f7129e..442b443ce 100644
--- a/tests/src/com/fsck/k9/mail/store/ImapStoreUriTest.java
+++ b/tests/src/com/fsck/k9/mail/store/ImapStoreUriTest.java
@@ -2,6 +2,8 @@ package com.fsck.k9.mail.store;
import java.util.HashMap;
import java.util.Map;
+
+import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store;
@@ -13,7 +15,7 @@ public class ImapStoreUriTest extends TestCase {
String uri = "imap://PLAIN:user:pass@server:143/0%7CcustomPathPrefix";
ServerSettings settings = Store.decodeStoreUri(uri);
- assertEquals("PLAIN", settings.authenticationType);
+ assertEquals(AuthType.PLAIN, settings.authenticationType);
assertEquals("user", settings.username);
assertEquals("pass", settings.password);
assertEquals("server", settings.host);
@@ -26,7 +28,7 @@ public class ImapStoreUriTest extends TestCase {
String uri = "imap://PLAIN:user:pass@server:143/";
ServerSettings settings = Store.decodeStoreUri(uri);
- assertEquals("PLAIN", settings.authenticationType);
+ assertEquals(AuthType.PLAIN, settings.authenticationType);
assertEquals("user", settings.username);
assertEquals("pass", settings.password);
assertEquals("server", settings.host);
@@ -38,7 +40,7 @@ public class ImapStoreUriTest extends TestCase {
String uri = "imap://PLAIN:user:pass@server:143/customPathPrefix";
ServerSettings settings = Store.decodeStoreUri(uri);
- assertEquals("PLAIN", settings.authenticationType);
+ assertEquals(AuthType.PLAIN, settings.authenticationType);
assertEquals("user", settings.username);
assertEquals("pass", settings.password);
assertEquals("server", settings.host);
@@ -51,7 +53,7 @@ public class ImapStoreUriTest extends TestCase {
String uri = "imap://PLAIN:user:pass@server:143/0%7C";
ServerSettings settings = Store.decodeStoreUri(uri);
- assertEquals("PLAIN", settings.authenticationType);
+ assertEquals(AuthType.PLAIN, settings.authenticationType);
assertEquals("user", settings.username);
assertEquals("pass", settings.password);
assertEquals("server", settings.host);
@@ -64,7 +66,7 @@ public class ImapStoreUriTest extends TestCase {
String uri = "imap://PLAIN:user:pass@server:143/1%7CcustomPathPrefix";
ServerSettings settings = Store.decodeStoreUri(uri);
- assertEquals("PLAIN", settings.authenticationType);
+ assertEquals(AuthType.PLAIN, settings.authenticationType);
assertEquals("user", settings.username);
assertEquals("pass", settings.password);
assertEquals("server", settings.host);
@@ -80,7 +82,7 @@ public class ImapStoreUriTest extends TestCase {
extra.put("pathPrefix", "customPathPrefix");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
- ConnectionSecurity.NONE, "PLAIN", "user", "pass", extra);
+ ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
String uri = Store.createStoreUri(settings);
@@ -93,7 +95,7 @@ public class ImapStoreUriTest extends TestCase {
extra.put("pathPrefix", "");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
- ConnectionSecurity.NONE, "PLAIN", "user", "pass", extra);
+ ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
String uri = Store.createStoreUri(settings);
@@ -102,7 +104,7 @@ public class ImapStoreUriTest extends TestCase {
public void testCreateStoreUriImapNoExtra() {
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
- ConnectionSecurity.NONE, "PLAIN", "user", "pass");
+ ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass");
String uri = Store.createStoreUri(settings);
@@ -114,7 +116,7 @@ public class ImapStoreUriTest extends TestCase {
extra.put("autoDetectNamespace", "true");
ServerSettings settings = new ServerSettings(ImapStore.STORE_TYPE, "server", 143,
- ConnectionSecurity.NONE, "PLAIN", "user", "pass", extra);
+ ConnectionSecurity.NONE, AuthType.PLAIN, "user", "pass", extra);
String uri = Store.createStoreUri(settings);
diff --git a/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java b/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java
index 710009eee..ca33ba106 100644
--- a/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java
+++ b/tests/src/com/fsck/k9/net/ssl/TrustManagerFactoryTest.java
@@ -214,27 +214,27 @@ public class TrustManagerFactoryTest extends AndroidTestCase {
mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT1, mCert1);
mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT2, mCert2);
- X509TrustManager trustManager1 = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true);
- X509TrustManager trustManager2 = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT2, true);
+ X509TrustManager trustManager1 = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1);
+ X509TrustManager trustManager2 = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT2);
trustManager2.checkServerTrusted(new X509Certificate[] { mCert2 }, "authType");
trustManager1.checkServerTrusted(new X509Certificate[] { mCert1 }, "authType");
}
public void testSelfSignedCertificateMatchingHost() throws Exception {
mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1);
- X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1);
trustManager.checkServerTrusted(new X509Certificate[] { mCert1 }, "authType");
}
public void testSelfSignedCertificateNotMatchingHost() throws Exception {
mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT1, mCert1);
- X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1);
trustManager.checkServerTrusted(new X509Certificate[] { mCert1 }, "authType");
}
public void testWrongCertificate() throws Exception {
mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1);
- X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1);
assertCertificateRejection(trustManager, new X509Certificate[] { mCert2 });
}
@@ -242,44 +242,44 @@ public class TrustManagerFactoryTest extends AndroidTestCase {
mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert1);
mKeyStore.addCertificate(MATCHING_HOST, PORT2, mCert2);
- X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1);
assertCertificateRejection(trustManager, new X509Certificate[] { mCert2 });
}
public void testUntrustedCertificateChain() throws Exception {
- X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1);
assertCertificateRejection(trustManager, new X509Certificate[] { mCert3, mCaCert });
}
public void testLocallyTrustedCertificateChain() throws Exception {
mKeyStore.addCertificate(MATCHING_HOST, PORT1, mCert3);
- X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1);
trustManager.checkServerTrusted(new X509Certificate[] { mCert3, mCaCert }, "authType");
}
public void testLocallyTrustedCertificateChainNotMatchingHost() throws Exception {
mKeyStore.addCertificate(NOT_MATCHING_HOST, PORT1, mCert3);
- X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1);
trustManager.checkServerTrusted(new X509Certificate[] { mCert3, mCaCert }, "authType");
}
public void testGloballyTrustedCertificateChain() throws Exception {
- X509TrustManager trustManager = TrustManagerFactory.get("www.linux.com", PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get("www.linux.com", PORT1);
X509Certificate[] certificates = new X509Certificate[] { mLinuxComCert, mStarfieldCert };
trustManager.checkServerTrusted(certificates, "authType");
}
public void testGloballyTrustedCertificateNotMatchingHost() throws Exception {
- X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(NOT_MATCHING_HOST, PORT1);
assertCertificateRejection(trustManager, new X509Certificate[] { mLinuxComCert, mStarfieldCert });
}
public void testGloballyTrustedCertificateNotMatchingHostOverride() throws Exception {
mKeyStore.addCertificate(MATCHING_HOST, PORT1, mLinuxComCert);
- X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1, true);
+ X509TrustManager trustManager = TrustManagerFactory.get(MATCHING_HOST, PORT1);
X509Certificate[] certificates = new X509Certificate[] { mLinuxComCert, mStarfieldCert };
trustManager.checkServerTrusted(certificates, "authType");
}