k-9/k9mail-library/src/main/java/com/fsck/k9/mail/transport/SmtpTransport.java

794 lines
30 KiB
Java
Raw Normal View History

package com.fsck.k9.mail.transport;
import android.util.Log;
2014-05-25 16:45:14 -04:00
import com.fsck.k9.mail.*;
import com.fsck.k9.mail.Message.RecipientType;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.filter.LineWrapOutputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import com.fsck.k9.mail.filter.SmtpDataStuffing;
import com.fsck.k9.mail.internet.CharsetSupport;
import com.fsck.k9.mail.CertificateValidationException;
2014-12-12 07:34:57 -05:00
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
2014-12-17 10:12:55 -05:00
import com.fsck.k9.mail.store.StoreConfig;
import javax.net.ssl.SSLException;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.*;
import java.security.GeneralSecurityException;
2011-02-09 14:06:01 -05:00
import java.util.*;
2014-12-16 06:51:52 -05:00
import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_SMTP;
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability;
2014-12-16 06:51:52 -05:00
public class SmtpTransport extends Transport {
2014-12-17 10:12:55 -05:00
private TrustedSocketFactory mTrustedSocketFactory;
2015-02-18 14:42:33 -05:00
public static final ServerSettings.Type TRANSPORT_TYPE = ServerSettings.Type.SMTP;
/**
* Decodes a SmtpTransport URI.
*
2014-05-25 16:45:14 -04:00
* NOTE: In contrast to ImapStore and Pop3Store, the authType is appended at the end!
*
* <p>Possible forms:</p>
* <pre>
2014-05-25 16:45:14 -04:00
* smtp://user:password:auth@server:port ConnectionSecurity.NONE
* smtp+tls+://user:password:auth@server:port ConnectionSecurity.STARTTLS_REQUIRED
* smtp+ssl+://user:password:auth@server:port ConnectionSecurity.SSL_TLS_REQUIRED
* </pre>
*/
public static ServerSettings decodeUri(String uri) {
String host;
int port;
ConnectionSecurity connectionSecurity;
2014-05-25 16:45:14 -04:00
AuthType authType = null;
String username = null;
String password = null;
2014-05-25 16:45:14 -04:00
String clientCertificateAlias = null;
URI smtpUri;
try {
smtpUri = new URI(uri);
} catch (URISyntaxException use) {
throw new IllegalArgumentException("Invalid SmtpTransport URI", use);
}
String scheme = smtpUri.getScheme();
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
/*
* 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;
2013-04-25 20:29:36 -04:00
port = 587;
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
} else if (scheme.startsWith("smtp+tls")) {
connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
2013-04-20 12:02:30 -04:00
port = 587;
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
} else if (scheme.startsWith("smtp+ssl")) {
connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
port = 465;
} else {
throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
}
host = smtpUri.getHost();
if (smtpUri.getPort() != -1) {
port = smtpUri.getPort();
}
if (smtpUri.getUserInfo() != null) {
2014-09-28 06:59:11 -04:00
String[] userInfoParts = smtpUri.getUserInfo().split(":");
if (userInfoParts.length == 1) {
authType = AuthType.PLAIN;
2014-12-14 10:54:27 -05:00
username = decodeUtf8(userInfoParts[0]);
2014-09-28 06:59:11 -04:00
} else if (userInfoParts.length == 2) {
authType = AuthType.PLAIN;
2014-12-14 10:54:27 -05:00
username = decodeUtf8(userInfoParts[0]);
password = decodeUtf8(userInfoParts[1]);
2014-09-28 06:59:11 -04:00
} else if (userInfoParts.length == 3) {
// NOTE: In SmptTransport URIs, the authType comes last!
authType = AuthType.valueOf(userInfoParts[2]);
2014-12-14 10:54:27 -05:00
username = decodeUtf8(userInfoParts[0]);
2014-09-28 06:59:11 -04:00
if (authType == AuthType.EXTERNAL) {
2014-12-14 10:54:27 -05:00
clientCertificateAlias = decodeUtf8(userInfoParts[1]);
2014-09-28 06:59:11 -04:00
} else {
2014-12-14 10:54:27 -05:00
password = decodeUtf8(userInfoParts[1]);
}
2010-01-17 19:10:49 -05:00
}
}
return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity,
2014-05-25 16:45:14 -04:00
authType, username, password, clientCertificateAlias);
}
/**
* Creates a SmtpTransport URI with the supplied settings.
*
* @param server
* The {@link ServerSettings} object that holds the server settings.
*
* @return A SmtpTransport URI that holds the same information as the {@code server} parameter.
*
* @see com.fsck.k9.mail.store.StoreConfig#getTransportUri()
* @see SmtpTransport#decodeUri(String)
*/
public static String createUri(ServerSettings server) {
2014-09-28 06:59:11 -04:00
String userEnc = (server.username != null) ?
2014-12-14 10:54:27 -05:00
encodeUtf8(server.username) : "";
2014-09-28 06:59:11 -04:00
String passwordEnc = (server.password != null) ?
2014-12-14 10:54:27 -05:00
encodeUtf8(server.password) : "";
2014-09-28 06:59:11 -04:00
String clientCertificateAliasEnc = (server.clientCertificateAlias != null) ?
2014-12-14 10:54:27 -05:00
encodeUtf8(server.clientCertificateAlias) : "";
String scheme;
switch (server.connectionSecurity) {
case SSL_TLS_REQUIRED:
scheme = "smtp+ssl+";
break;
case STARTTLS_REQUIRED:
scheme = "smtp+tls+";
break;
default:
case NONE:
scheme = "smtp";
break;
}
String userInfo;
AuthType authType = server.authenticationType;
2014-05-25 16:45:14 -04:00
// NOTE: authType is append at last item, in contrast to ImapStore and Pop3Store!
if (authType != null) {
if (AuthType.EXTERNAL == authType) {
2014-05-25 16:45:14 -04:00
userInfo = userEnc + ":" + clientCertificateAliasEnc + ":" + authType.name();
} else {
userInfo = userEnc + ":" + passwordEnc + ":" + authType.name();
}
} else {
userInfo = userEnc + ":" + passwordEnc;
}
try {
return new URI(scheme, userInfo, server.host, server.port, null, null,
null).toString();
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Can't create SmtpTransport URI", e);
}
}
private String mHost;
private int mPort;
private String mUsername;
private String mPassword;
private String mClientCertificateAlias;
private AuthType mAuthType;
private ConnectionSecurity mConnectionSecurity;
private Socket mSocket;
private PeekableInputStream mIn;
private OutputStream mOut;
private boolean m8bitEncodingAllowed;
private int mLargestAcceptableMessage;
public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory)
throws MessagingException {
ServerSettings settings;
try {
settings = decodeUri(storeConfig.getTransportUri());
} catch (IllegalArgumentException e) {
throw new MessagingException("Error while decoding transport URI", e);
}
mHost = settings.host;
mPort = settings.port;
mConnectionSecurity = settings.connectionSecurity;
mAuthType = settings.authenticationType;
mUsername = settings.username;
mPassword = settings.password;
2014-05-25 16:45:14 -04:00
mClientCertificateAlias = settings.clientCertificateAlias;
mTrustedSocketFactory = trustedSocketFactory;
}
@Override
public void open() throws MessagingException {
try {
boolean secureConnection = false;
2011-01-31 18:45:23 -05:00
InetAddress[] addresses = InetAddress.getAllByName(mHost);
for (int i = 0; i < addresses.length; i++) {
try {
2011-01-31 18:45:23 -05:00
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort);
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
2014-12-17 10:12:55 -05:00
mSocket = mTrustedSocketFactory.createSocket(null, mHost, mPort, mClientCertificateAlias);
2011-01-31 18:45:23 -05:00
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
secureConnection = true;
} else {
2011-01-31 18:45:23 -05:00
mSocket = new Socket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
}
} catch (SocketException e) {
if (i < (addresses.length - 1)) {
2011-01-31 18:45:23 -05:00
// there are still other addresses for that host to try
continue;
}
throw new MessagingException("Cannot connect to host", e);
2011-01-31 18:45:23 -05:00
}
break; // connection success
}
// RFC 1047
mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024));
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
// Eat the banner
executeSimpleCommand(null);
InetAddress localAddress = mSocket.getLocalAddress();
String localHost = localAddress.getCanonicalHostName();
String ipAddr = localAddress.getHostAddress();
if (localHost.equals("") || localHost.equals(ipAddr) || localHost.contains("_")) {
// We don't have a FQDN or the hostname contains invalid
// characters (see issue 2143), so use IP address.
if (!ipAddr.equals("")) {
if (localAddress instanceof Inet6Address) {
localHost = "[IPv6:" + ipAddr + "]";
} else {
localHost = "[" + ipAddr + "]";
}
} else {
// If the IP address is no good, set a sane default (see issue 2750).
localHost = "android";
}
}
2014-10-04 06:45:45 -04:00
Map<String,String> extensions = sendHello(localHost);
m8bitEncodingAllowed = extensions.containsKey("8BITMIME");
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
if (mConnectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) {
if (extensions.containsKey("STARTTLS")) {
executeSimpleCommand("STARTTLS");
2014-12-17 10:12:55 -05:00
mSocket = mTrustedSocketFactory.createSocket(
mSocket,
mHost,
mPort,
2014-05-25 16:45:14 -04:00
mClientCertificateAlias);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
1024));
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024);
/*
* Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
* Exim.
*/
extensions = sendHello(localHost);
secureConnection = true;
Eliminate the 'if available' connection security options These options originated in the AOSP email client from which K-9 Mail was forked. They provide an odd combination of 2 features: 1. Don't bother to authenticate the server's certificate (applies to both SSL/TLS and STARTTLS); i.e., blindly accept all certificates. This is generally a bad security policy which is susceptible to MITM attacks. 2. If STARTTLS is selected but the server doesn't claim to support STARTTLS, then proceed without using encryption. This, too, is a bad security policy which is susceptible to MITM attacks. Since the time that K-9 Mail was forked, a couple things have changed: > K-9 Mail has implemented the ability for users to review and permanently accept individual certificates that would otherwise fail authentication. With this ability, there is no need for a user to subject themselves to the ongoing risks of feature 1. above. Hence, this commit removes feature 1. > The AOSP email client has changed its behavior and no longer permits a security downgrade to an unencrypted connection if the server doesn't claim to support STARTTLS (i.e., they eliminated feature 2. above). K-9 Mail should do the same. It's unlikely that a server is going to provide STARTTLS on an intermittent basis, so providing a contingency for such unusual behavior is an unnecessary risk. Hence, this commit removes that feature as well. Effect on existing users: If the old connection security setting was "SSL/TLS (if available)" (which now gets remapped to "SSL/TLS"), and the server does not provide a certificate that can be authenticated, then a "Certificate error for <account name>" notification is generated telling the user to check their server settings. Tapping the notification takes the user to the relevant server settings, where the user can tap "Next" to review the certificate and choose to permanently accept it. This process would occur during the first syncing of folders after application upgrade or (in the case of SMTP) during the first attempt to send a message. If the connection security setting was "STARTTLS (if available)" (which now gets remapped to "STARTTLS"), and the server does not provide a certificate that can be authenticated, then the same process as above would occur. If the old connection security setting was "STARTTLS (if available)", and the server doesn't claim to support STARTTLS, then the user would get a certificate error notification which would lead them to the server's settings. There they would need to choose a different connection security -- most likely "NONE". If they didn't change anything but instead just tapped "Next", the server settings would be checked again and a dialog would pop up saying, "Cannot connect to server. (STARTTLS connection security not available)". (The implementation of notifications when STARTTLS is not available is not actually included here -- it's in the commit that follows.) Regarding the changes to providers.xml: in cases where the scheme ended with "+ssl", the schemes were simply updated by appending "+". In cases where the scheme ended with "+tls", a check of the server was made to assure that STARTTLS was available before appending "+" to the scheme. Domains paran.com and nate.com failed the check and were removed because no current information could be found. Domains me.com and mac.com also failed and were updated based on http://support.apple.com/kb/ht4864.
2014-02-26 16:50:21 -05:00
} 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");
}
}
2009-11-07 15:16:15 -05:00
boolean authLoginSupported = false;
boolean authPlainSupported = false;
boolean authCramMD5Supported = false;
boolean authExternalSupported = false;
if (extensions.containsKey("AUTH")) {
List<String> saslMech = Arrays.asList(extensions.get("AUTH").split(" "));
authLoginSupported = saslMech.contains("LOGIN");
authPlainSupported = saslMech.contains("PLAIN");
authCramMD5Supported = saslMech.contains("CRAM-MD5");
authExternalSupported = saslMech.contains("EXTERNAL");
}
if (extensions.containsKey("SIZE")) {
try {
mLargestAcceptableMessage = Integer.parseInt(extensions.get("SIZE"));
} catch (Exception e) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) {
Log.d(LOG_TAG, "Tried to parse " + extensions.get("SIZE") + " and get an int", e);
}
}
2009-11-07 15:16:15 -05:00
}
if (mUsername != null
&& mUsername.length() > 0
&& (mPassword != null && mPassword.length() > 0 || AuthType.EXTERNAL == mAuthType)) {
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);
} 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;
case EXTERNAL:
if (authExternalSupported) {
saslAuthExternal(mUsername);
} else {
/*
* Some SMTP servers are known to provide no error
* indication when a client certificate fails to
* validate, other than to not offer the AUTH EXTERNAL
* capability.
*
* So, we treat it is an error to not offer AUTH
* EXTERNAL when using client certificates. That way, the
* user can be notified of a problem during account setup.
*/
throw new CertificateValidationException(MissingCapability);
}
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 {
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.");
}
}
break;
default:
throw new MessagingException("Unhandled authentication method found in the server settings (bug).");
}
}
} catch (SSLException e) {
throw new CertificateValidationException(e.getMessage(), e);
} catch (GeneralSecurityException gse) {
throw new MessagingException(
"Unable to open connection to SMTP server due to security error.", gse);
} catch (IOException ioe) {
throw new MessagingException("Unable to open connection to SMTP server.", ioe);
}
}
/**
* Send the client "identity" using the EHLO or HELO command.
*
* <p>
* We first try the EHLO command. If the server sends a negative response, it probably doesn't
* support the EHLO command. So we try the older HELO command that all servers need to support.
* And if that fails, too, we pretend everything is fine and continue unimpressed.
* </p>
*
* @param host
* The EHLO/HELO parameter as defined by the RFC.
*
2014-10-04 06:45:45 -04:00
* @return A (possibly empty) {@code Map<String,String>} 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.
*/
2014-10-04 06:45:45 -04:00
private Map<String,String> sendHello(String host) throws IOException, MessagingException {
Map<String, String> extensions = new HashMap<String, String>();
try {
List<String> results = executeSimpleCommand("EHLO " + host);
// Remove the EHLO greeting response
results.remove(0);
for (String result : results) {
String[] pair = result.split(" ", 2);
2014-03-03 10:08:07 -05:00
extensions.put(pair[0].toUpperCase(Locale.US), pair.length == 1 ? "" : pair[1]);
}
} catch (NegativeSmtpReplyException e) {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug()) {
Log.v(LOG_TAG, "Server doesn't support the EHLO command. Trying HELO...");
}
try {
executeSimpleCommand("HELO " + host);
} catch (NegativeSmtpReplyException e2) {
2014-12-16 06:51:52 -05:00
Log.w(LOG_TAG, "Server doesn't support the HELO command. Continuing anyway.");
}
}
return extensions;
}
@Override
public void sendMessage(Message message) throws MessagingException {
2014-10-04 06:45:45 -04:00
List<Address> addresses = new ArrayList<Address>();
{
addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.TO)));
addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.CC)));
addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.BCC)));
}
message.setRecipients(RecipientType.BCC, null);
2014-10-04 06:45:45 -04:00
Map<String, List<String>> charsetAddressesMap =
new HashMap<String, List<String>>();
for (Address address : addresses) {
String addressString = address.getAddress();
String charset = CharsetSupport.getCharsetFromAddress(addressString);
2014-10-04 06:45:45 -04:00
List<String> addressesOfCharset = charsetAddressesMap.get(charset);
if (addressesOfCharset == null) {
addressesOfCharset = new ArrayList<String>();
charsetAddressesMap.put(charset, addressesOfCharset);
}
addressesOfCharset.add(addressString);
}
2014-10-04 06:45:45 -04:00
for (Map.Entry<String, List<String>> charsetAddressesMapEntry :
charsetAddressesMap.entrySet()) {
String charset = charsetAddressesMapEntry.getKey();
2014-10-04 06:45:45 -04:00
List<String> addressesOfCharset = charsetAddressesMapEntry.getValue();
message.setCharset(charset);
sendMessageTo(addressesOfCharset, message);
}
}
2014-10-04 06:45:45 -04:00
private void sendMessageTo(List<String> addresses, Message message)
throws MessagingException {
boolean possibleSend = false;
close();
open();
Recursively convert attachments of type message/rfc822 to 7bit if necessary. The preceding commit resulted in attachments of type message/rfc822 being sent with 8bit encoding even when the SMTP server did not support 8BITMIME. This commit assures that messages will be converted to 7bit when necessary. A new interface CompositeBody was created that extends Body, and classes Message and Multipart were changed from implementing Body to CompositeBody. Additional classes BinaryTempFileMessageBody and LocalAttachmentMessageBody were created (by extending BinaryTempFileBody and LocalAttachmentBody, respectively), and they too implement CompositeBody. A CompositeBody is a Body containing a composite-type that can contain subparts that may require recursive processing when converting from 8bit to 7bit. The Part to which a CompositeBody belongs is only permitted to use 8bit or 7bit encoding for the CompositeBody. Previously, a Message was created so that it was 7bit clean by default (even though that meant base64 encoding all attachments, including messages). Then, if the SMTP server supported 8BITMIME, Message.setEncoding("8bit") was called so that bodies of type TextBody would been transmitted using 8bit encoding rather than quoted-printable. Now, messages are created with 8bit encoding by default. Then, if the SMTP server does not support 8BITMIME, Message.setUsing7bitTransport is called to recursively convert the message and its subparts to 7bit. The method setUsing7bitTransport was added to the interfaces Part and CompositeBody. setEncoding no longer iterates over parts in Multipart. That task belongs to setUsing7bitTransport, which may in turn call setEncoding on the parts. MimeUtility.getEncodingforType was created as a helper function for choosing a default encoding that should be used for a given MIME type when an attachment is added to a message (either while composing or when retrieving from LocalStore). setEncoding was implemented in MimeBodyPart to assure that the encoding set in the Part's headers was the same as set for the Part's Body. (The method already existed in MimeMessage, which has similarities with MimeBodyPart.) MimeMessage.parse(InputStream in, boolean recurse) was implemented so that the parser could be told to recursively process nested messages read from the InputStream, thus giving access to all subparts at any level that may need to be converted from 8bit to 7bit.
2013-09-02 23:49:28 -04:00
if (!m8bitEncodingAllowed) {
message.setUsing7bitTransport();
}
// If the message has attachments and our server has told us about a limit on
// the size of messages, count the message's size before sending it
if (mLargestAcceptableMessage > 0 && message.hasAttachments()) {
if (message.calculateSize() > mLargestAcceptableMessage) {
MessagingException me = new MessagingException("Message too large for server");
//TODO this looks rather suspicious... shouldn't it be true?
me.setPermanentFailure(possibleSend);
throw me;
}
}
Address[] from = message.getFrom();
try {
executeSimpleCommand("MAIL FROM:" + "<" + from[0].getAddress() + ">"
+ (m8bitEncodingAllowed ? " BODY=8BITMIME" : ""));
for (String address : addresses) {
executeSimpleCommand("RCPT TO:" + "<" + address + ">");
}
executeSimpleCommand("DATA");
EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream(
new LineWrapOutputStream(new SmtpDataStuffing(mOut), 1000));
message.writeTo(msgOut);
// We use BufferedOutputStream. So make sure to call flush() !
msgOut.flush();
possibleSend = true; // After the "\r\n." is attempted, we may have sent the message
executeSimpleCommand("\r\n.");
} catch (Exception e) {
MessagingException me = new MessagingException("Unable to send message", e);
// "5xx text" -responses are permanent failures
String msg = e.getMessage();
if (msg != null && msg.startsWith("5")) {
2014-12-16 06:51:52 -05:00
Log.w(LOG_TAG, "handling 5xx SMTP error code as a permanent failure");
2011-06-01 16:03:56 -04:00
possibleSend = false;
}
//TODO this looks rather suspicious... why is possibleSend used, and why are 5xx NOT permanent (in contrast to the log text)
me.setPermanentFailure(possibleSend);
throw me;
} finally {
close();
Complete merge of DAmail functionality into K9mail. Following features are added to K9mail: 1) Show unread message count on each folder 2) Sum unread count of all shown folders in an account to the account display 3) Periodically check selected folders for new mail, not just Inbox 4) Don't refresh folder when opened (unless folder is empty) 5) Show date and time of last sync for each folder 6) Fix timer for automatic periodic sync (use wakelock to assure completion) 7) Optimize local folder queries (speeds up account and folder lists) 8) Show Loading... message in status bar indicating which folder is being synced 9) Eliminate redundant sync of new messages (performance enhancement) 10) Improve notification text for multiple accounts 11) Do not automatically sync folders more often than the account-specific period 12) Use user-configured date and time formats 13) Select which folders are shown, using configurable Classes 14) Select which folders are synced, using configurable Classes 15) Added context (long press) menu to folders, to provide for Refresh and Folder Settings 16) Status light flashes purple when there are unread messages 17) Folder list more quickly eliminates display of deleted and out-of-Class folders. 18) Delete works 19) Mark all messages as read (in the folder context menu) 20) Notifications only for new unread messages 21) One minute synchronization frequency 22) Deleting an unread message decrements unread counter 23) Notifications work for POP3 accounts 24) Message deletes work for POP3 accounts 25) Explicit errors show in folder list 26) Stack traces saved to folder K9mail-errors 27) Clear pending actions (danger, for emergencies only!) 28) Delete policy in Account settings 29) DNS cache in InetAddress disabled 30) Trapped some crash-causing error conditions 31) Eliminate duplicate copies to Sent folder 32) Prevent crashes due to message listener concurrency 33) Empty Trash 34) Nuclear "Mark all messages as read" (marks all messages as read in server-side folder, irrespective of which messages have been downloaded) 35) Forward (alternate) to allow forwarding email through other programs 36) Accept text/plain Intents to allow other programs to send email through K9mail 37) Displays Outbox sending status 38) Manual retry of outbox sending when "Refresh"ing Outbox 39) Folder error status is persisted 40) Ability to log to arbitrary file Fixes K9 issues 11, 23, 24, 65, 69, 71, 79, 81, 82, 83, 87, 101, 104, 107, 120, 148, 154
2008-12-30 22:49:09 -05:00
}
}
@Override
public void close() {
try {
executeSimpleCommand("QUIT");
} catch (Exception e) {
}
try {
mIn.close();
} catch (Exception e) {
}
try {
mOut.close();
} catch (Exception e) {
}
try {
mSocket.close();
} catch (Exception e) {
}
mIn = null;
mOut = null;
mSocket = null;
}
private String readLine() throws IOException {
StringBuilder sb = new StringBuilder();
int d;
while ((d = mIn.read()) != -1) {
if (((char)d) == '\r') {
continue;
} else if (((char)d) == '\n') {
break;
} else {
sb.append((char)d);
}
}
String ret = sb.toString();
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP)
Log.d(LOG_TAG, "SMTP <<< " + ret);
return ret;
}
private void writeLine(String s, boolean sensitive) throws IOException {
2014-12-16 06:51:52 -05:00
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) {
final String commandToLog;
2014-12-16 06:51:52 -05:00
if (sensitive && !K9MailLib.isDebugSensitive()) {
commandToLog = "SMTP >>> *sensitive*";
} else {
commandToLog = "SMTP >>> " + s;
}
2014-12-16 06:51:52 -05:00
Log.d(LOG_TAG, commandToLog);
}
2011-02-26 17:04:49 -05:00
byte[] data = s.concat("\r\n").getBytes();
/*
* Important: Send command + CRLF using just one write() call. Using
* multiple calls will likely result in multiple TCP packets and some
* SMTP servers misbehave if CR and LF arrive in separate pakets.
* See issue 799.
*/
mOut.write(data);
mOut.flush();
}
private void checkLine(String line) throws MessagingException {
int length = line.length();
if (length < 1) {
throw new MessagingException("SMTP response is 0 length");
}
char c = line.charAt(0);
if ((c == '4') || (c == '5')) {
int replyCode = -1;
String message = line;
if (length >= 3) {
try {
replyCode = Integer.parseInt(line.substring(0, 3));
} catch (NumberFormatException e) { /* ignore */ }
if (length > 4) {
message = line.substring(4);
} else {
message = "";
}
}
throw new NegativeSmtpReplyException(replyCode, message);
}
}
private List<String> executeSimpleCommand(String command) throws IOException, MessagingException {
return executeSimpleCommand(command, false);
}
private List<String> executeSimpleCommand(String command, boolean sensitive)
throws IOException, MessagingException {
2009-11-07 15:16:15 -05:00
List<String> results = new ArrayList<String>();
if (command != null) {
writeLine(command, sensitive);
}
/*
* Read lines as long as the length is 4 or larger, e.g. "220-banner text here".
* Shorter lines are either errors of contain only a reply code. Those cases will
* be handled by checkLine() below.
*
* TODO: All responses should be checked to confirm that they start with a valid
* reply code, and that the reply code is appropriate for the command being executed.
* That means it should either be a 2xx code (generally) or a 3xx code in special cases
* (e.g., DATA & AUTH LOGIN commands). Reply codes should be made available as part of
* the returned object.
*/
String line = readLine();
while (line.length() >= 4) {
if (line.length() > 4) {
// Everything after the first four characters goes into the results array.
2009-11-07 15:16:15 -05:00
results.add(line.substring(4));
}
if (line.charAt(3) != '-') {
// If the fourth character isn't "-" this is the last line of the response.
break;
}
line = readLine();
}
2009-11-07 15:16:15 -05:00
// Check if the reply code indicates an error.
checkLine(line);
return results;
}
// C: AUTH LOGIN
// S: 334 VXNlcm5hbWU6
// C: d2VsZG9u
// S: 334 UGFzc3dvcmQ6
// C: dzNsZDBu
// S: 235 2.0.0 OK Authenticated
//
// Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads:
//
//
// C: AUTH LOGIN
// S: 334 Username:
// C: weldon
// S: 334 Password:
// C: w3ld0n
// S: 235 2.0.0 OK Authenticated
private void saslAuthLogin(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException {
try {
executeSimpleCommand("AUTH LOGIN");
executeSimpleCommand(Base64.encode(username), true);
executeSimpleCommand(Base64.encode(password), true);
} catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == 535) {
// Authentication credentials invalid
throw new AuthenticationFailedException("AUTH LOGIN failed ("
+ exception.getMessage() + ")");
} else {
throw exception;
}
}
}
private void saslAuthPlain(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException {
String data = Base64.encode("\000" + username + "\000" + password);
try {
executeSimpleCommand("AUTH PLAIN " + data, true);
} catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == 535) {
// Authentication credentials invalid
throw new AuthenticationFailedException("AUTH PLAIN failed ("
+ exception.getMessage() + ")");
} else {
throw exception;
}
}
}
private void saslAuthCramMD5(String username, String password) throws MessagingException,
AuthenticationFailedException, IOException {
2011-04-12 08:16:22 -04:00
List<String> respList = executeSimpleCommand("AUTH CRAM-MD5");
if (respList.size() != 1) {
throw new MessagingException("Unable to negotiate CRAM-MD5");
}
String b64Nonce = respList.get(0);
String b64CRAMString = Authentication.computeCramMd5(mUsername, mPassword, b64Nonce);
try {
executeSimpleCommand(b64CRAMString, true);
} catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == 535) {
// Authentication credentials invalid
throw new AuthenticationFailedException(exception.getMessage(), exception);
} else {
throw exception;
}
}
}
private void saslAuthExternal(String username) throws MessagingException, IOException {
executeSimpleCommand(
String.format("AUTH EXTERNAL %s",
Base64.encode(username)), false);
}
/**
* Exception that is thrown when the server sends a negative reply (reply codes 4xx or 5xx).
*/
static class NegativeSmtpReplyException extends MessagingException {
private static final long serialVersionUID = 8696043577357897135L;
private final int mReplyCode;
private final String mReplyText;
public NegativeSmtpReplyException(int replyCode, String replyText) {
super("Negative SMTP reply: " + replyCode + " " + replyText);
mReplyCode = replyCode;
mReplyText = replyText;
}
public int getReplyCode() {
return mReplyCode;
}
public String getReplyText() {
return mReplyText;
}
}
}