diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/Transport.java b/k9mail-library/src/main/java/com/fsck/k9/mail/Transport.java index 6b38ec557..daae1f78d 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/Transport.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/Transport.java @@ -1,13 +1,8 @@ package com.fsck.k9.mail; -import android.content.Context; - -import com.fsck.k9.mail.oauth.OAuth2TokenProvider; -import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory; -import com.fsck.k9.mail.store.StoreConfig; import com.fsck.k9.mail.ServerSettings.Type; -import com.fsck.k9.mail.transport.SmtpTransport; +import com.fsck.k9.mail.transport.smtp.SmtpTransport; import com.fsck.k9.mail.transport.WebDavTransport; import java.io.UnsupportedEncodingException; diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/TransportProvider.java b/k9mail-library/src/main/java/com/fsck/k9/mail/TransportProvider.java index 6e70a650a..989296243 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/TransportProvider.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/TransportProvider.java @@ -6,7 +6,7 @@ import android.content.Context; import com.fsck.k9.mail.oauth.OAuth2TokenProvider; import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory; import com.fsck.k9.mail.store.StoreConfig; -import com.fsck.k9.mail.transport.SmtpTransport; +import com.fsck.k9.mail.transport.smtp.SmtpTransport; import com.fsck.k9.mail.transport.WebDavTransport; public class TransportProvider { diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java new file mode 100644 index 000000000..7be1e11f7 --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/EnhancedNegativeSmtpReplyException.java @@ -0,0 +1,18 @@ +package com.fsck.k9.mail.transport.smtp; + + +class EnhancedNegativeSmtpReplyException extends NegativeSmtpReplyException { + private final StatusCodeClass statusCodeClass; + private final StatusCodeSubject statusCodeSubject; + private final StatusCodeDetail statusCodeDetail; + + + EnhancedNegativeSmtpReplyException(int replyCode, StatusCodeClass statusCodeClass, + StatusCodeSubject statusCodeSubject, StatusCodeDetail statusCodeDetail, + String replyText) { + super(replyCode, replyText); + this.statusCodeClass = statusCodeClass; + this.statusCodeSubject = statusCodeSubject; + this.statusCodeDetail = statusCodeDetail; + } +} diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.java b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.java new file mode 100644 index 000000000..4a822030f --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/NegativeSmtpReplyException.java @@ -0,0 +1,41 @@ +package com.fsck.k9.mail.transport.smtp; + + +import android.text.TextUtils; + +import com.fsck.k9.mail.MessagingException; + + +/** + * Exception that is thrown when the server sends a negative reply (reply codes 4xx or 5xx). + */ +class NegativeSmtpReplyException extends MessagingException { + private static final long serialVersionUID = 8696043577357897135L; + + + private final int replyCode; + private final String replyText; + + + public NegativeSmtpReplyException(int replyCode, String replyText) { + super(buildErrorMessage(replyCode, replyText), isPermanentSmtpError(replyCode)); + this.replyCode = replyCode; + this.replyText = replyText; + } + + private static String buildErrorMessage(int replyCode, String replyText) { + return TextUtils.isEmpty(replyText) ? "Negative SMTP reply: " + replyCode : replyText; + } + + private static boolean isPermanentSmtpError(int replyCode) { + return replyCode >= 500 && replyCode <= 599; + } + + public int getReplyCode() { + return replyCode; + } + + public String getReplyText() { + return replyText; + } +} diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/transport/SmtpTransport.java b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java similarity index 86% rename from k9mail-library/src/main/java/com/fsck/k9/mail/transport/SmtpTransport.java rename to k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java index 80fa274ee..5d3635e00 100644 --- a/k9mail-library/src/main/java/com/fsck/k9/mail/transport/SmtpTransport.java +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/SmtpTransport.java @@ -1,5 +1,5 @@ -package com.fsck.k9.mail.transport; +package com.fsck.k9.mail.transport.smtp; import java.io.BufferedInputStream; @@ -49,6 +49,7 @@ import com.fsck.k9.mail.oauth.XOAuth2ChallengeParser; import com.fsck.k9.mail.ssl.TrustedSocketFactory; import com.fsck.k9.mail.store.StoreConfig; import javax.net.ssl.SSLException; +import org.apache.commons.io.IOUtils; import static com.fsck.k9.mail.CertificateValidationException.Reason.MissingCapability; import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_SMTP; @@ -211,6 +212,7 @@ public class SmtpTransport extends Transport { private PeekableInputStream mIn; private OutputStream mOut; private boolean m8bitEncodingAllowed; + private boolean mEnhancedStatusCodesProvided; private int mLargestAcceptableMessage; private boolean retryXoauthWithNewToken; @@ -269,7 +271,7 @@ public class SmtpTransport extends Transport { mOut = new BufferedOutputStream(mSocket.getOutputStream(), 1024); // Eat the banner - executeSimpleCommand(null); + executeCommand(null); InetAddress localAddress = mSocket.getLocalAddress(); String localHost = getCanonicalHostName(localAddress); @@ -293,11 +295,11 @@ public class SmtpTransport extends Transport { Map extensions = sendHello(localHost); m8bitEncodingAllowed = extensions.containsKey("8BITMIME"); - + mEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES"); if (mConnectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) { if (extensions.containsKey("STARTTLS")) { - executeSimpleCommand("STARTTLS"); + executeCommand("STARTTLS"); mSocket = mTrustedSocketFactory.createSocket( mSocket, @@ -488,10 +490,10 @@ public class SmtpTransport extends Transport { * @throws MessagingException * In case of a malformed response. */ - private Map sendHello(String host) throws IOException, MessagingException { + private Map sendHello(String host) throws IOException, MessagingException { Map extensions = new HashMap(); try { - List results = executeSimpleCommand("EHLO " + host); + List results = executeCommand("EHLO %s", host).results; // Remove the EHLO greeting response results.remove(0); for (String result : results) { @@ -504,7 +506,7 @@ public class SmtpTransport extends Transport { } try { - executeSimpleCommand("HELO " + host); + executeCommand("HELO %s", host); } catch (NegativeSmtpReplyException e2) { Log.w(LOG_TAG, "Server doesn't support the HELO command. Continuing anyway."); } @@ -563,12 +565,18 @@ public class SmtpTransport extends Transport { boolean entireMessageSent = false; Address[] from = message.getFrom(); try { - executeSimpleCommand("MAIL FROM:" + "<" + from[0].getAddress() + ">" - + (m8bitEncodingAllowed ? " BODY=8BITMIME" : "")); - for (String address : addresses) { - executeSimpleCommand("RCPT TO:" + "<" + address + ">"); + String fromAddress = from[0].getAddress(); + if (m8bitEncodingAllowed) { + executeCommand("MAIL FROM:<%s> BODY=8BITMIME", fromAddress); + } else { + executeCommand("MAIL FROM:<%s>", fromAddress); } - executeSimpleCommand("DATA"); + + for (String address : addresses) { + executeCommand("RCPT TO:<%s>", address); + } + + executeCommand("DATA"); EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream( new LineWrapOutputStream(new SmtpDataStuffing(mOut), 1000)); @@ -577,7 +585,7 @@ public class SmtpTransport extends Transport { msgOut.endWithCrLfAndFlush(); entireMessageSent = true; // After the "\r\n." is attempted, we may have sent the message - executeSimpleCommand("."); + executeCommand("."); } catch (NegativeSmtpReplyException e) { throw e; } catch (Exception e) { @@ -594,25 +602,13 @@ public class SmtpTransport extends Transport { @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(); + executeCommand("QUIT"); } catch (Exception e) { } + IOUtils.closeQuietly(mIn); + IOUtils.closeQuietly(mOut); + IOUtils.closeQuietly(mSocket); mIn = null; mOut = null; mSocket = null; @@ -660,76 +656,31 @@ public class SmtpTransport extends Transport { 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); - } - - } - @Deprecated - private List executeSimpleCommand(String command) throws IOException, MessagingException { - return executeSimpleCommand(command, false); - } - - /** - * 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. - * - * This should be done using the non-deprecated API below. - */ - @Deprecated - private List executeSimpleCommand(String command, boolean sensitive) - throws IOException, MessagingException { - List results = new ArrayList<>(); - if (command != null) { - writeLine(command, sensitive); - } - - String line = readCommandResponseLine(results); - - // Check if the reply code indicates an error. - checkLine(line); - - return results; - } - private static class CommandResponse { private final int replyCode; - private final String message; + private final List results; - public CommandResponse(int replyCode, String message) { + public CommandResponse(int replyCode, List results) { this.replyCode = replyCode; - this.message = message; + this.results = results; } } - private CommandResponse executeSimpleCommandWithResponse(String command, boolean sensitive) throws IOException, MessagingException { + private CommandResponse executeSensitiveCommand(String format, Object... args) + throws IOException, MessagingException { + return executeCommand(true, format, args); + } + + private CommandResponse executeCommand(String format, Object... args) throws IOException, MessagingException { + return executeCommand(false, format, args); + } + + private CommandResponse executeCommand(boolean sensitive, String format, Object... args) + throws IOException, MessagingException { List results = new ArrayList<>(); - if (command != null) { + if (format != null) { + String command = String.format(Locale.ROOT, format, args); writeLine(command, sensitive); } @@ -741,25 +692,45 @@ public class SmtpTransport extends Transport { } 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); + char replyCodeCategory = line.charAt(0); + boolean isReplyCodeErrorCategory = (replyCodeCategory == '4') || (replyCodeCategory == '5'); + if (isReplyCodeErrorCategory) { + if (mEnhancedStatusCodesProvided) { + throw buildEnhancedNegativeSmtpReplyException(replyCode, results); } else { - message = ""; + String replyText = TextUtils.join(" ", results); + throw new NegativeSmtpReplyException(replyCode, replyText); } } - char c = line.charAt(0); - if ((c == '4') || (c == '5')) { - throw new NegativeSmtpReplyException(replyCode, message); + return new CommandResponse(replyCode, results); + } + + private MessagingException buildEnhancedNegativeSmtpReplyException(int replyCode, List results) { + StatusCodeClass statusCodeClass = null; + StatusCodeSubject statusCodeSubject = null; + StatusCodeDetail statusCodeDetail = null; + + String message = ""; + for (String resultLine : results) { + message += resultLine.split(" ", 2)[1] + " "; + } + if (results.size() > 0) { + String[] statusCodeParts = results.get(0).split(" ", 2)[0].split("\\."); + + statusCodeClass = StatusCodeClass.parse(statusCodeParts[0]); + statusCodeSubject = StatusCodeSubject.parse(statusCodeParts[1]); + statusCodeDetail = StatusCodeDetail.parse(statusCodeSubject, statusCodeParts[2]); } - return new CommandResponse(replyCode, message); + return new EnhancedNegativeSmtpReplyException(replyCode, statusCodeClass, statusCodeSubject, statusCodeDetail, + message.trim()); } @@ -805,9 +776,9 @@ public class SmtpTransport extends Transport { 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); + executeCommand("AUTH LOGIN"); + executeSensitiveCommand(Base64.encode(username)); + executeSensitiveCommand(Base64.encode(password)); } catch (NegativeSmtpReplyException exception) { if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { // Authentication credentials invalid @@ -823,7 +794,7 @@ public class SmtpTransport extends Transport { AuthenticationFailedException, IOException { String data = Base64.encode("\000" + username + "\000" + password); try { - executeSimpleCommand("AUTH PLAIN " + data, true); + executeSensitiveCommand("AUTH PLAIN %s", data); } catch (NegativeSmtpReplyException exception) { if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { // Authentication credentials invalid @@ -838,7 +809,7 @@ public class SmtpTransport extends Transport { private void saslAuthCramMD5(String username, String password) throws MessagingException, AuthenticationFailedException, IOException { - List respList = executeSimpleCommand("AUTH CRAM-MD5"); + List respList = executeCommand("AUTH CRAM-MD5").results; if (respList.size() != 1) { throw new MessagingException("Unable to negotiate CRAM-MD5"); } @@ -847,7 +818,7 @@ public class SmtpTransport extends Transport { String b64CRAMString = Authentication.computeCramMd5(mUsername, mPassword, b64Nonce); try { - executeSimpleCommand(b64CRAMString, true); + executeSensitiveCommand(b64CRAMString); } catch (NegativeSmtpReplyException exception) { if (exception.getReplyCode() == SMTP_AUTHENTICATION_FAILURE_ERROR_CODE) { // Authentication credentials invalid @@ -911,52 +882,23 @@ public class SmtpTransport extends Transport { private void attemptXoauth2(String username) throws MessagingException, IOException { String token = oauthTokenProvider.getToken(username, OAuth2TokenProvider.OAUTH2_TIMEOUT); String authString = Authentication.computeXoauth(username, token); - CommandResponse response = executeSimpleCommandWithResponse("AUTH XOAUTH2 " + authString, true); + CommandResponse response = executeSensitiveCommand("AUTH XOAUTH2 %s", authString); if (response.replyCode == SMTP_CONTINUE_REQUEST) { - retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(response.message, mHost); + String replyText = TextUtils.join("", response.results); + retryXoauthWithNewToken = XOAuth2ChallengeParser.shouldRetry(replyText, mHost); //Per Google spec, respond to challenge with empty response - executeSimpleCommandWithResponse("", false); + executeCommand(""); } } private void saslAuthExternal(String username) throws MessagingException, IOException { - executeSimpleCommand( - String.format("AUTH EXTERNAL %s", - Base64.encode(username)), false); + executeCommand("AUTH EXTERNAL %s", Base64.encode(username)); } @VisibleForTesting protected String getCanonicalHostName(InetAddress localAddress) { return localAddress.getCanonicalHostName(); } - - /** - * 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, isPermanentSmtpError(replyCode)); - mReplyCode = replyCode; - mReplyText = replyText; - } - - private static boolean isPermanentSmtpError(int replyCode) { - return replyCode >= 500 && replyCode <= 599; - } - - public int getReplyCode() { - return mReplyCode; - } - - public String getReplyText() { - return mReplyText; - } - } } diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java new file mode 100644 index 000000000..f56f28942 --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeClass.java @@ -0,0 +1,26 @@ +package com.fsck.k9.mail.transport.smtp; + + +enum StatusCodeClass { + SUCCESS(2), + PERSISTENT_TRANSIENT_FAILURE(4), + PERMANENT_FAILURE(5); + + + private final int codeClass; + + + static StatusCodeClass parse(String statusCodeClassString) { + int value = Integer.parseInt(statusCodeClassString); + for (StatusCodeClass classEnum : StatusCodeClass.values()) { + if (classEnum.codeClass == value) { + return classEnum; + } + } + return null; + } + + StatusCodeClass(int codeClass) { + this.codeClass = codeClass; + } +} diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeDetail.java b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeDetail.java new file mode 100644 index 000000000..92de6967e --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeDetail.java @@ -0,0 +1,80 @@ +package com.fsck.k9.mail.transport.smtp; + + +enum StatusCodeDetail { + UNDEFINED(StatusCodeSubject.UNDEFINED, 0), + OTHER_ADDRESS_STATUS(StatusCodeSubject.ADDRESSING, 0), + BAD_DESTINATION_MAILBOX_ADDRESS(StatusCodeSubject.ADDRESSING, 1), + BAD_DESTINATION_SYSTEM_ADDRESS(StatusCodeSubject.ADDRESSING, 2), + BAD_DESTINATION_MAILBOX_ADDRESS_SYNTAX(StatusCodeSubject.ADDRESSING, 3), + DESTINATION_MAILBOX_ADDRESS_AMBIGUOUS(StatusCodeSubject.ADDRESSING, 4), + DESTINATION_ADDRESS_VALID(StatusCodeSubject.ADDRESSING, 5), + DESTINATION_MAILBOX_MOVED(StatusCodeSubject.ADDRESSING, 6), + BAD_SENDER_MAILBOX_SYNTAX(StatusCodeSubject.ADDRESSING, 7), + BAD_SENDER_SYSTEM_ADDRESS(StatusCodeSubject.ADDRESSING, 8), + + OTHER_MAILBOX_STATUS(StatusCodeSubject.MAILBOX,0), + MAILBOX_DISABLED(StatusCodeSubject.MAILBOX,1), + MAILBOX_FULL(StatusCodeSubject.MAILBOX,2), + MESSAGE_LENGTH_EXCEEDED(StatusCodeSubject.MAILBOX,3), + MAILING_LIST_EXPANSION_PROBLEM(StatusCodeSubject.MAILBOX,4), + + OTHER_MAIL_SYSTEM_STATUS(StatusCodeSubject.MAIL_SYSTEM,0), + MAIL_SYSTEM_FULL(StatusCodeSubject.MAIL_SYSTEM,1), + SYSTEM_NOT_ACCEPTING_MESSAGES(StatusCodeSubject.MAIL_SYSTEM,2), + SYSTEM_INCAPABLE_OF_FEATURE(StatusCodeSubject.MAIL_SYSTEM,3), + MESSAGE_TOO_BIG(StatusCodeSubject.MAIL_SYSTEM,4), + SYSTEM_INCORRECTLY_CONFIGURED(StatusCodeSubject.MAIL_SYSTEM,5), + + OTHER_NETWORK_ROUTING(StatusCodeSubject.NETWORK_ROUTING,0), + NO_ANSWER_FROM_HOST(StatusCodeSubject.NETWORK_ROUTING,1), + BAD_CONNECTION(StatusCodeSubject.NETWORK_ROUTING,2), + DIRECTORY_SERVER_FAILURE(StatusCodeSubject.NETWORK_ROUTING,3), + UNABLE_TO_ROUTE(StatusCodeSubject.NETWORK_ROUTING,4), + MAIL_SYSTEM_CONGESTION(StatusCodeSubject.NETWORK_ROUTING,5), + ROUTING_LOOP_DETECTED(StatusCodeSubject.NETWORK_ROUTING,6), + DELIVERY_TIME_EXPIRED(StatusCodeSubject.NETWORK_ROUTING,7), + + OTHER_MAIL_DELIVERY_PROTOCOL(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL,0), + INVALID_COMMAND(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL,1), + SYNTAX_ERROR(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL,2), + TOO_MANY_RECIPIENTS(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL,3), + INVALID_COMMAND_ARGUMENTS(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL,4), + WRONG_PROTOCOL_VERSION(StatusCodeSubject.MAIL_DELIVERY_PROTOCOL,5), + + OTHER_MESSAGE_CONTENT_OR_MEDIA(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA,0), + MEDIA_NOT_SUPPORTED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA,1), + CONVERSION_REQUIRED_AND_PROHIBITED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA,2), + CONVERSION_REQUIRED_BUT_UNSUPPORTED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA,3), + CONVERSION_WITH_LOSS_PERFORMED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA,4), + CONVERSION_FAILED(StatusCodeSubject.MESSAGE_CONTENT_OR_MEDIA,5), + + OTHER_SECURITY_OR_POLICY_STATUS(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 0), + DELIVERY_NOT_AUTHORIZED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 1), + MAILING_LIST_EXPANSION_PROHIBITED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 2), + SECURITY_CONVERSION_REQUIRED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 3), + SECURITY_FEATURES_UNSUPPORTED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 4), + CRYPTOGRAPHIC_FAILURE(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 5), + CRYPTOGRAPHIC_ALGORITHM_UNSUPPORTED(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 6), + MESSAGE_INTEGRITY_FAILURE(StatusCodeSubject.SECURITY_OR_POLICY_STATUS, 7); + + + private final StatusCodeSubject subject; + private final int detail; + + + public static StatusCodeDetail parse(StatusCodeSubject statusCodeSubject, String statusCodeDetailString) { + int value = Integer.parseInt(statusCodeDetailString); + for (StatusCodeDetail detailEnum : StatusCodeDetail.values()) { + if (detailEnum.subject == statusCodeSubject && detailEnum.detail == value) { + return detailEnum; + } + } + return null; + } + + StatusCodeDetail(StatusCodeSubject subject, int detail) { + this.subject = subject; + this.detail = detail; + } +} diff --git a/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeSubject.java b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeSubject.java new file mode 100644 index 000000000..ff004f306 --- /dev/null +++ b/k9mail-library/src/main/java/com/fsck/k9/mail/transport/smtp/StatusCodeSubject.java @@ -0,0 +1,31 @@ +package com.fsck.k9.mail.transport.smtp; + + +enum StatusCodeSubject { + UNDEFINED(0), + ADDRESSING(1), + MAILBOX(2), + MAIL_SYSTEM(3), + NETWORK_ROUTING(4), + MAIL_DELIVERY_PROTOCOL(5), + MESSAGE_CONTENT_OR_MEDIA(6), + SECURITY_OR_POLICY_STATUS(7); + + + private final int codeSubject; + + + static StatusCodeSubject parse(String statusCodeSubjectString) { + int value = Integer.parseInt(statusCodeSubjectString); + for (StatusCodeSubject classEnum : StatusCodeSubject.values()) { + if (classEnum.codeSubject == value) { + return classEnum; + } + } + return null; + } + + StatusCodeSubject(int codeSubject) { + this.codeSubject = codeSubject; + } +} diff --git a/k9mail-library/src/test/java/com/fsck/k9/mail/transport/SmtpTransportTest.java b/k9mail-library/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java similarity index 87% rename from k9mail-library/src/test/java/com/fsck/k9/mail/transport/SmtpTransportTest.java rename to k9mail-library/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java index bfc8cec31..ed26ae9d4 100644 --- a/k9mail-library/src/test/java/com/fsck/k9/mail/transport/SmtpTransportTest.java +++ b/k9mail-library/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportTest.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.transport; +package com.fsck.k9.mail.transport.smtp; import java.io.IOException; @@ -26,7 +26,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; -import org.robolectric.annotation.Config; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; @@ -227,7 +226,8 @@ public class SmtpTransportTest { fail("Exception expected"); } catch (AuthenticationFailedException e) { assertEquals( - "Negative SMTP reply: 535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68", + "5.7.1 Username and Password not accepted. Learn more at " + + "5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68", e.getMessage()); } @@ -343,7 +343,8 @@ public class SmtpTransportTest { fail("Exception expected"); } catch (AuthenticationFailedException e) { assertEquals( - "Negative SMTP reply: 535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68", + "5.7.1 Username and Password not accepted. Learn more at " + + "5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68", e.getMessage()); } @@ -374,7 +375,6 @@ public class SmtpTransportTest { server.verifyInteractionCompleted(); } - @Test public void open_withoutXoauth2Extension_shouldThrow() throws Exception { MockSmtpServer server = new MockSmtpServer(); @@ -497,6 +497,63 @@ public class SmtpTransportTest { server.verifyInteractionCompleted(); } + @Test + public void open_withSupportWithEnhancedStatusCodesOnAuthFailure_shouldThrowEncodedMessage() + throws Exception { + MockSmtpServer server = new MockSmtpServer(); + server.output("220 localhost Simple Mail Transfer Service Ready"); + server.expect("EHLO localhost"); + server.output("250-localhost Hello client.localhost"); + server.output("250-ENHANCEDSTATUSCODES"); + server.output("250 AUTH XOAUTH2"); + server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); + server.output("334 " + XOAuth2ChallengeParserTest.STATUS_401_RESPONSE); + server.expect(""); + server.output("535-5.7.1 Username and Password not accepted. Learn more at"); + server.output("535 5.7.1 http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68"); + server.expect("QUIT"); + server.output("221 BYE"); + SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); + + try { + transport.open(); + fail("Exception expected"); + } catch (AuthenticationFailedException e) { + assertEquals( + "Username and Password not accepted. Learn more at http://support.google.com/mail/bin/answer.py?answer=14257 hx9sm5317360pbc.68", + e.getMessage()); + } + + InOrder inOrder = inOrder(oAuth2TokenProvider); + inOrder.verify(oAuth2TokenProvider).getToken(eq(USERNAME), anyInt()); + inOrder.verify(oAuth2TokenProvider).invalidateToken(USERNAME); + server.verifyConnectionClosed(); + server.verifyInteractionCompleted(); + } + + @Test + public void open_withManyExtensions_shouldParseAll() throws Exception { + MockSmtpServer server = new MockSmtpServer(); + server.output("220 smtp.gmail.com ESMTP x25sm19117693wrx.27 - gsmtp"); + server.expect("EHLO localhost"); + server.output("250-smtp.gmail.com at your service, [86.147.34.216]"); + server.output("250-SIZE 35882577"); + server.output("250-8BITMIME"); + server.output("250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH"); + server.output("250-ENHANCEDSTATUSCODES"); + server.output("250-PIPELINING"); + server.output("250-CHUNKING"); + server.output("250 SMTPUTF8"); + server.expect("AUTH XOAUTH2 dXNlcj11c2VyAWF1dGg9QmVhcmVyIG9sZFRva2VuAQE="); + server.output("235 2.7.0 Authentication successful"); + SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.XOAUTH2, ConnectionSecurity.NONE); + + transport.open(); + + server.verifyConnectionStillOpen(); + server.verifyInteractionCompleted(); + } + @Test public void sendMessage_withoutAddressToSendTo_shouldNotOpenConnection() throws Exception { MimeMessage message = new MimeMessage(); @@ -556,6 +613,30 @@ public class SmtpTransportTest { server.verifyInteractionCompleted(); } + @Test + public void sendMessage_with8BitEncodingExtensionNotCaseSensitive() throws Exception { + Message message = getDefaultMessage(); + MockSmtpServer server = createServerAndSetupForPlainAuthentication("8bitmime"); + server.expect("MAIL FROM: BODY=8BITMIME"); + server.output("250 OK"); + server.expect("RCPT TO:"); + server.output("250 OK"); + server.expect("DATA"); + server.output("354 End data with ."); + server.expect("[message data]"); + server.expect("."); + server.output("250 OK: queued as 12345"); + server.expect("QUIT"); + server.output("221 BYE"); + server.closeConnection(); + SmtpTransport transport = startServerAndCreateSmtpTransport(server); + + transport.sendMessage(message); + + server.verifyConnectionClosed(); + server.verifyInteractionCompleted(); + } + @Test public void sendMessage_withMessageTooLarge_shouldThrow() throws Exception { Message message = getDefaultMessageBuilder() @@ -598,7 +679,7 @@ public class SmtpTransportTest { try { transport.sendMessage(message); fail("Expected exception"); - } catch (SmtpTransport.NegativeSmtpReplyException e) { + } catch (NegativeSmtpReplyException e) { assertEquals(421, e.getReplyCode()); assertEquals("4.7.0 Temporary system problem", e.getReplyText()); } diff --git a/k9mail-library/src/test/java/com/fsck/k9/mail/transport/SmtpTransportUriTest.java b/k9mail-library/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportUriTest.java similarity index 98% rename from k9mail-library/src/test/java/com/fsck/k9/mail/transport/SmtpTransportUriTest.java rename to k9mail-library/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportUriTest.java index 128edab0b..ecfe17e56 100644 --- a/k9mail-library/src/test/java/com/fsck/k9/mail/transport/SmtpTransportUriTest.java +++ b/k9mail-library/src/test/java/com/fsck/k9/mail/transport/smtp/SmtpTransportUriTest.java @@ -1,4 +1,4 @@ -package com.fsck.k9.mail.transport; +package com.fsck.k9.mail.transport.smtp; import android.annotation.SuppressLint; @@ -6,6 +6,7 @@ import com.fsck.k9.mail.AuthType; import com.fsck.k9.mail.ConnectionSecurity; import com.fsck.k9.mail.ServerSettings; +import com.fsck.k9.mail.transport.smtp.SmtpTransport; import org.junit.Test; import static org.junit.Assert.assertEquals;