Merge pull request #2344 from philipwhiuk/smtpTransportResponseHandling
SMTP Transport improvements
This commit is contained in:
commit
b56b852b61
10 changed files with 367 additions and 152 deletions
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<String, String> 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<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>();
|
||||
try {
|
||||
List<String> results = executeSimpleCommand("EHLO " + host);
|
||||
List<String> 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<String> 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<String> executeSimpleCommand(String command, boolean sensitive)
|
||||
throws IOException, MessagingException {
|
||||
List<String> 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<String> results;
|
||||
|
||||
public CommandResponse(int replyCode, String message) {
|
||||
public CommandResponse(int replyCode, List<String> 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<String> 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<String> 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<String> respList = executeSimpleCommand("AUTH CRAM-MD5");
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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:<user@localhost> BODY=8BITMIME");
|
||||
server.output("250 OK");
|
||||
server.expect("RCPT TO:<user2@localhost>");
|
||||
server.output("250 OK");
|
||||
server.expect("DATA");
|
||||
server.output("354 End data with <CR><LF>.<CR><LF>");
|
||||
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());
|
||||
}
|
|
@ -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;
|
Loading…
Reference in a new issue