clean up SmtpTransport

This commit is contained in:
Vincent Breitmoser 2017-08-23 01:23:31 +02:00
parent c4f68b873a
commit 9cb7712142

View file

@ -58,26 +58,30 @@ public class SmtpTransport extends Transport {
private static final int SMTP_CONTINUE_REQUEST = 334; private static final int SMTP_CONTINUE_REQUEST = 334;
private static final int SMTP_AUTHENTICATION_FAILURE_ERROR_CODE = 535; private static final int SMTP_AUTHENTICATION_FAILURE_ERROR_CODE = 535;
private TrustedSocketFactory mTrustedSocketFactory;
private OAuth2TokenProvider oauthTokenProvider;
private String mHost; private final TrustedSocketFactory trustedSocketFactory;
private int mPort; private final OAuth2TokenProvider oauthTokenProvider;
private String mUsername;
private String mPassword; private final String host;
private String mClientCertificateAlias; private final int port;
private AuthType mAuthType; private final String username;
private ConnectionSecurity mConnectionSecurity; private final String password;
private Socket mSocket; private final String clientCertificateAlias;
private PeekableInputStream mIn; private final AuthType authType;
private OutputStream mOut; private final ConnectionSecurity connectionSecurity;
private boolean m8bitEncodingAllowed;
private boolean mEnhancedStatusCodesProvided;
private int mLargestAcceptableMessage; private Socket socket;
private PeekableInputStream inputStream;
private OutputStream outputStream;
private boolean is8bitEncodingAllowed;
private boolean isEnhancedStatusCodesProvided;
private int largestAcceptableMessage;
private boolean retryXoauthWithNewToken; private boolean retryXoauthWithNewToken;
public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory, public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
OAuth2TokenProvider oauth2TokenProvider) throws MessagingException { OAuth2TokenProvider oauthTokenProvider) throws MessagingException {
ServerSettings settings; ServerSettings settings;
try { try {
settings = TransportUris.decodeTransportUri(storeConfig.getTransportUri()); settings = TransportUris.decodeTransportUri(storeConfig.getTransportUri());
@ -85,38 +89,39 @@ public class SmtpTransport extends Transport {
throw new MessagingException("Error while decoding transport URI", e); throw new MessagingException("Error while decoding transport URI", e);
} }
if (settings.type == Type.SMTP) { if (settings.type != Type.SMTP) {
throw new IllegalArgumentException("Expected SMTP StoreConfig!"); throw new IllegalArgumentException("Expected SMTP StoreConfig!");
} }
mHost = settings.host; host = settings.host;
mPort = settings.port; port = settings.port;
mConnectionSecurity = settings.connectionSecurity; connectionSecurity = settings.connectionSecurity;
mAuthType = settings.authenticationType; authType = settings.authenticationType;
mUsername = settings.username; username = settings.username;
mPassword = settings.password; password = settings.password;
mClientCertificateAlias = settings.clientCertificateAlias; clientCertificateAlias = settings.clientCertificateAlias;
mTrustedSocketFactory = trustedSocketFactory;
oauthTokenProvider = oauth2TokenProvider; this.trustedSocketFactory = trustedSocketFactory;
this.oauthTokenProvider = oauthTokenProvider;
} }
@Override @Override
public void open() throws MessagingException { public void open() throws MessagingException {
try { try {
boolean secureConnection = false; boolean secureConnection = false;
InetAddress[] addresses = InetAddress.getAllByName(mHost); InetAddress[] addresses = InetAddress.getAllByName(host);
for (int i = 0; i < addresses.length; i++) { for (int i = 0; i < addresses.length; i++) {
try { try {
SocketAddress socketAddress = new InetSocketAddress(addresses[i], mPort); SocketAddress socketAddress = new InetSocketAddress(addresses[i], port);
if (mConnectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) { if (connectionSecurity == ConnectionSecurity.SSL_TLS_REQUIRED) {
mSocket = mTrustedSocketFactory.createSocket(null, mHost, mPort, mClientCertificateAlias); socket = trustedSocketFactory.createSocket(null, host, port, clientCertificateAlias);
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); socket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
secureConnection = true; secureConnection = true;
} else { } else {
mSocket = new Socket(); socket = new Socket();
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT); socket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
} }
} catch (SocketException e) { } catch (SocketException e) {
if (i < (addresses.length - 1)) { if (i < (addresses.length - 1)) {
@ -129,15 +134,15 @@ public class SmtpTransport extends Transport {
} }
// RFC 1047 // RFC 1047
mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); socket.setSoTimeout(SOCKET_READ_TIMEOUT);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024)); inputStream = new PeekableInputStream(new BufferedInputStream(socket.getInputStream(), 1024));
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024); outputStream = new BufferedOutputStream(socket.getOutputStream(), 1024);
// Eat the banner // Eat the banner
executeCommand(null); executeCommand(null);
InetAddress localAddress = mSocket.getLocalAddress(); InetAddress localAddress = socket.getLocalAddress();
String localHost = getCanonicalHostName(localAddress); String localHost = getCanonicalHostName(localAddress);
String ipAddr = localAddress.getHostAddress(); String ipAddr = localAddress.getHostAddress();
@ -158,22 +163,22 @@ public class SmtpTransport extends Transport {
Map<String, String> extensions = sendHello(localHost); Map<String, String> extensions = sendHello(localHost);
m8bitEncodingAllowed = extensions.containsKey("8BITMIME"); is8bitEncodingAllowed = extensions.containsKey("8BITMIME");
mEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES"); isEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES");
if (mConnectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) { if (connectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) {
if (extensions.containsKey("STARTTLS")) { if (extensions.containsKey("STARTTLS")) {
executeCommand("STARTTLS"); executeCommand("STARTTLS");
mSocket = mTrustedSocketFactory.createSocket( socket = trustedSocketFactory.createSocket(
mSocket, socket,
mHost, host,
mPort, port,
mClientCertificateAlias); clientCertificateAlias);
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), inputStream = new PeekableInputStream(new BufferedInputStream(socket.getInputStream(),
1024)); 1024));
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024); outputStream = new BufferedOutputStream(socket.getOutputStream(), 1024);
/* /*
* Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically, * Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
* Exim. * Exim.
@ -208,12 +213,12 @@ public class SmtpTransport extends Transport {
} }
parseOptionalSizeValue(extensions); parseOptionalSizeValue(extensions);
if (!TextUtils.isEmpty(mUsername) if (!TextUtils.isEmpty(username)
&& (!TextUtils.isEmpty(mPassword) || && (!TextUtils.isEmpty(password) ||
AuthType.EXTERNAL == mAuthType || AuthType.EXTERNAL == authType ||
AuthType.XOAUTH2 == mAuthType)) { AuthType.XOAUTH2 == authType)) {
switch (mAuthType) { switch (authType) {
/* /*
* LOGIN is an obsolete option which is unavailable to users, * LOGIN is an obsolete option which is unavailable to users,
@ -224,9 +229,9 @@ public class SmtpTransport extends Transport {
case PLAIN: case PLAIN:
// try saslAuthPlain first, because it supports UTF-8 explicitly // try saslAuthPlain first, because it supports UTF-8 explicitly
if (authPlainSupported) { if (authPlainSupported) {
saslAuthPlain(mUsername, mPassword); saslAuthPlain();
} else if (authLoginSupported) { } else if (authLoginSupported) {
saslAuthLogin(mUsername, mPassword); saslAuthLogin();
} else { } else {
throw new MessagingException( throw new MessagingException(
"Authentication methods SASL PLAIN and LOGIN are unavailable."); "Authentication methods SASL PLAIN and LOGIN are unavailable.");
@ -235,21 +240,21 @@ public class SmtpTransport extends Transport {
case CRAM_MD5: case CRAM_MD5:
if (authCramMD5Supported) { if (authCramMD5Supported) {
saslAuthCramMD5(mUsername, mPassword); saslAuthCramMD5();
} else { } else {
throw new MessagingException("Authentication method CRAM-MD5 is unavailable."); throw new MessagingException("Authentication method CRAM-MD5 is unavailable.");
} }
break; break;
case XOAUTH2: case XOAUTH2:
if (authXoauth2Supported && oauthTokenProvider != null) { if (authXoauth2Supported && oauthTokenProvider != null) {
saslXoauth2(mUsername); saslXoauth2();
} else { } else {
throw new MessagingException("Authentication method XOAUTH2 is unavailable."); throw new MessagingException("Authentication method XOAUTH2 is unavailable.");
} }
break; break;
case EXTERNAL: case EXTERNAL:
if (authExternalSupported) { if (authExternalSupported) {
saslAuthExternal(mUsername); saslAuthExternal();
} else { } else {
/* /*
* Some SMTP servers are known to provide no error * Some SMTP servers are known to provide no error
@ -274,17 +279,17 @@ public class SmtpTransport extends Transport {
if (secureConnection) { if (secureConnection) {
// try saslAuthPlain first, because it supports UTF-8 explicitly // try saslAuthPlain first, because it supports UTF-8 explicitly
if (authPlainSupported) { if (authPlainSupported) {
saslAuthPlain(mUsername, mPassword); saslAuthPlain();
} else if (authLoginSupported) { } else if (authLoginSupported) {
saslAuthLogin(mUsername, mPassword); saslAuthLogin();
} else if (authCramMD5Supported) { } else if (authCramMD5Supported) {
saslAuthCramMD5(mUsername, mPassword); saslAuthCramMD5();
} else { } else {
throw new MessagingException("No supported authentication methods available."); throw new MessagingException("No supported authentication methods available.");
} }
} else { } else {
if (authCramMD5Supported) { if (authCramMD5Supported) {
saslAuthCramMD5(mUsername, mPassword); saslAuthCramMD5();
} else { } else {
/* /*
* We refuse to insecurely transmit the password * We refuse to insecurely transmit the password
@ -322,9 +327,9 @@ public class SmtpTransport extends Transport {
private void parseOptionalSizeValue(Map<String, String> extensions) { private void parseOptionalSizeValue(Map<String, String> extensions) {
if (extensions.containsKey("SIZE")) { if (extensions.containsKey("SIZE")) {
String optionalsizeValue = extensions.get("SIZE"); String optionalsizeValue = extensions.get("SIZE");
if (optionalsizeValue != null && optionalsizeValue != "") { if (optionalsizeValue != null && !"".equals(optionalsizeValue)) {
try { try {
mLargestAcceptableMessage = Integer.parseInt(optionalsizeValue); largestAcceptableMessage = Integer.parseInt(optionalsizeValue);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) { if (K9MailLib.isDebug() && DEBUG_PROTOCOL_SMTP) {
Timber.d(e, "Tried to parse %s and get an int", optionalsizeValue); Timber.d(e, "Tried to parse %s and get an int", optionalsizeValue);
@ -355,7 +360,7 @@ public class SmtpTransport extends Transport {
* In case of a malformed response. * In case of a malformed response.
*/ */
private Map<String, String> sendHello(String host) throws IOException, MessagingException { private Map<String, String> sendHello(String host) throws IOException, MessagingException {
Map<String, String> extensions = new HashMap<String, String>(); Map<String, String> extensions = new HashMap<>();
try { try {
List<String> results = executeCommand("EHLO %s", host).results; List<String> results = executeCommand("EHLO %s", host).results;
// Remove the EHLO greeting response // Remove the EHLO greeting response
@ -380,7 +385,7 @@ public class SmtpTransport extends Transport {
@Override @Override
public void sendMessage(Message message) throws MessagingException { public void sendMessage(Message message) throws MessagingException {
List<Address> addresses = new ArrayList<Address>(); List<Address> addresses = new ArrayList<>();
{ {
addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.TO))); addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.TO)));
addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.CC))); addresses.addAll(Arrays.asList(message.getRecipients(RecipientType.CC)));
@ -388,14 +393,13 @@ public class SmtpTransport extends Transport {
} }
message.setRecipients(RecipientType.BCC, null); message.setRecipients(RecipientType.BCC, null);
Map<String, List<String>> charsetAddressesMap = Map<String, List<String>> charsetAddressesMap = new HashMap<>();
new HashMap<String, List<String>>();
for (Address address : addresses) { for (Address address : addresses) {
String addressString = address.getAddress(); String addressString = address.getAddress();
String charset = CharsetSupport.getCharsetFromAddress(addressString); String charset = CharsetSupport.getCharsetFromAddress(addressString);
List<String> addressesOfCharset = charsetAddressesMap.get(charset); List<String> addressesOfCharset = charsetAddressesMap.get(charset);
if (addressesOfCharset == null) { if (addressesOfCharset == null) {
addressesOfCharset = new ArrayList<String>(); addressesOfCharset = new ArrayList<>();
charsetAddressesMap.put(charset, addressesOfCharset); charsetAddressesMap.put(charset, addressesOfCharset);
} }
addressesOfCharset.add(addressString); addressesOfCharset.add(addressString);
@ -415,13 +419,13 @@ public class SmtpTransport extends Transport {
close(); close();
open(); open();
if (!m8bitEncodingAllowed) { if (!is8bitEncodingAllowed) {
Timber.d("Server does not support 8bit transfer encoding"); Timber.d("Server does not support 8bit transfer encoding");
} }
// If the message has attachments and our server has told us about a limit on // 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 // the size of messages, count the message's size before sending it
if (mLargestAcceptableMessage > 0 && message.hasAttachments()) { if (largestAcceptableMessage > 0 && message.hasAttachments()) {
if (message.calculateSize() > mLargestAcceptableMessage) { if (message.calculateSize() > largestAcceptableMessage) {
throw new MessagingException("Message too large for server", true); throw new MessagingException("Message too large for server", true);
} }
} }
@ -430,7 +434,7 @@ public class SmtpTransport extends Transport {
Address[] from = message.getFrom(); Address[] from = message.getFrom();
try { try {
String fromAddress = from[0].getAddress(); String fromAddress = from[0].getAddress();
if (m8bitEncodingAllowed) { if (is8bitEncodingAllowed) {
executeCommand("MAIL FROM:<%s> BODY=8BITMIME", fromAddress); executeCommand("MAIL FROM:<%s> BODY=8BITMIME", fromAddress);
} else { } else {
executeCommand("MAIL FROM:<%s>", fromAddress); executeCommand("MAIL FROM:<%s>", fromAddress);
@ -443,7 +447,7 @@ public class SmtpTransport extends Transport {
executeCommand("DATA"); executeCommand("DATA");
EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream( EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream(
new LineWrapOutputStream(new SmtpDataStuffing(mOut), 1000)); new LineWrapOutputStream(new SmtpDataStuffing(outputStream), 1000));
message.writeTo(msgOut); message.writeTo(msgOut);
msgOut.endWithCrLfAndFlush(); msgOut.endWithCrLfAndFlush();
@ -468,26 +472,25 @@ public class SmtpTransport extends Transport {
try { try {
executeCommand("QUIT"); executeCommand("QUIT");
} catch (Exception e) { } catch (Exception e) {
// don't care
} }
IOUtils.closeQuietly(mIn); IOUtils.closeQuietly(inputStream);
IOUtils.closeQuietly(mOut); IOUtils.closeQuietly(outputStream);
IOUtils.closeQuietly(mSocket); IOUtils.closeQuietly(socket);
mIn = null; inputStream = null;
mOut = null; outputStream = null;
mSocket = null; socket = null;
} }
private String readLine() throws IOException { private String readLine() throws IOException {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
int d; int d;
while ((d = mIn.read()) != -1) { while ((d = inputStream.read()) != -1) {
if (((char)d) == '\r') { char c = (char) d;
continue; if (c == '\n') {
} else if (((char)d) == '\n') {
break; break;
} else { } else if (c != '\r') {
sb.append((char)d); sb.append(c);
} }
} }
String ret = sb.toString(); String ret = sb.toString();
@ -516,8 +519,8 @@ public class SmtpTransport extends Transport {
* SMTP servers misbehave if CR and LF arrive in separate pakets. * SMTP servers misbehave if CR and LF arrive in separate pakets.
* See issue 799. * See issue 799.
*/ */
mOut.write(data); outputStream.write(data);
mOut.flush(); outputStream.flush();
} }
private static class CommandResponse { private static class CommandResponse {
@ -525,7 +528,7 @@ public class SmtpTransport extends Transport {
private final int replyCode; private final int replyCode;
private final List<String> results; private final List<String> results;
public CommandResponse(int replyCode, List<String> results) { CommandResponse(int replyCode, List<String> results) {
this.replyCode = replyCode; this.replyCode = replyCode;
this.results = results; this.results = results;
} }
@ -565,7 +568,7 @@ public class SmtpTransport extends Transport {
char replyCodeCategory = line.charAt(0); char replyCodeCategory = line.charAt(0);
boolean isReplyCodeErrorCategory = (replyCodeCategory == '4') || (replyCodeCategory == '5'); boolean isReplyCodeErrorCategory = (replyCodeCategory == '4') || (replyCodeCategory == '5');
if (isReplyCodeErrorCategory) { if (isReplyCodeErrorCategory) {
if (mEnhancedStatusCodesProvided) { if (isEnhancedStatusCodesProvided) {
throw buildEnhancedNegativeSmtpReplyException(replyCode, results); throw buildEnhancedNegativeSmtpReplyException(replyCode, results);
} else { } else {
String replyText = TextUtils.join(" ", results); String replyText = TextUtils.join(" ", results);
@ -620,48 +623,26 @@ public class SmtpTransport extends Transport {
} }
// C: AUTH LOGIN private void saslAuthLogin() throws MessagingException, IOException {
// 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 { try {
executeCommand("AUTH LOGIN"); executeCommand("AUTH LOGIN");
executeSensitiveCommand(Base64.encode(username)); executeSensitiveCommand(Base64.encode(username));
executeSensitiveCommand(Base64.encode(password)); executeSensitiveCommand(Base64.encode(password));
} catch (NegativeSmtpReplyException exception) { } catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
// Authentication credentials invalid throw new AuthenticationFailedException("AUTH LOGIN failed (" + exception.getMessage() + ")");
throw new AuthenticationFailedException("AUTH LOGIN failed ("
+ exception.getMessage() + ")");
} else { } else {
throw exception; throw exception;
} }
} }
} }
private void saslAuthPlain(String username, String password) throws MessagingException, private void saslAuthPlain() throws MessagingException, IOException {
AuthenticationFailedException, IOException {
String data = Base64.encode("\000" + username + "\000" + password); String data = Base64.encode("\000" + username + "\000" + password);
try { try {
executeSensitiveCommand("AUTH PLAIN %s", data); executeSensitiveCommand("AUTH PLAIN %s", data);
} catch (NegativeSmtpReplyException exception) { } catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
// Authentication credentials invalid
throw new AuthenticationFailedException("AUTH PLAIN failed (" throw new AuthenticationFailedException("AUTH PLAIN failed ("
+ exception.getMessage() + ")"); + exception.getMessage() + ")");
} else { } else {
@ -670,8 +651,7 @@ public class SmtpTransport extends Transport {
} }
} }
private void saslAuthCramMD5(String username, String password) throws MessagingException, private void saslAuthCramMD5() throws MessagingException, IOException {
AuthenticationFailedException, IOException {
List<String> respList = executeCommand("AUTH CRAM-MD5").results; List<String> respList = executeCommand("AUTH CRAM-MD5").results;
if (respList.size() != 1) { if (respList.size() != 1) {
@ -679,13 +659,12 @@ public class SmtpTransport extends Transport {
} }
String b64Nonce = respList.get(0); String b64Nonce = respList.get(0);
String b64CRAMString = Authentication.computeCramMd5(mUsername, mPassword, b64Nonce); String b64CRAMString = Authentication.computeCramMd5(username, password, b64Nonce);
try { try {
executeSensitiveCommand(b64CRAMString); executeSensitiveCommand(b64CRAMString);
} catch (NegativeSmtpReplyException exception) { } catch (NegativeSmtpReplyException exception) {
if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) {
// Authentication credentials invalid
throw new AuthenticationFailedException(exception.getMessage(), exception); throw new AuthenticationFailedException(exception.getMessage(), exception);
} else { } else {
throw exception; throw exception;
@ -693,7 +672,7 @@ public class SmtpTransport extends Transport {
} }
} }
private void saslXoauth2(String username) throws MessagingException, IOException { private void saslXoauth2() throws MessagingException, IOException {
retryXoauthWithNewToken = true; retryXoauthWithNewToken = true;
try { try {
attemptXoauth2(username); attemptXoauth2(username);
@ -749,14 +728,14 @@ public class SmtpTransport extends Transport {
if (response.replyCode == SMTP_CONTINUE_REQUEST) { if (response.replyCode == SMTP_CONTINUE_REQUEST) {
String replyText = TextUtils.join("", response.results); String replyText = TextUtils.join("", response.results);
retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, mHost); retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, host);
//Per Google spec, respond to challenge with empty response //Per Google spec, respond to challenge with empty response
executeCommand(""); executeCommand("");
} }
} }
private void saslAuthExternal(String username) throws MessagingException, IOException { private void saslAuthExternal() throws MessagingException, IOException {
executeCommand("AUTH EXTERNAL %s", Base64.encode(username)); executeCommand("AUTH EXTERNAL %s", Base64.encode(username));
} }