SMTP pipelining support
This commit is contained in:
parent
66b5154b7d
commit
8a552d46a0
2 changed files with 163 additions and 8 deletions
|
@ -16,9 +16,11 @@ import java.security.GeneralSecurityException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
import android.support.annotation.VisibleForTesting;
|
import android.support.annotation.VisibleForTesting;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -78,6 +80,8 @@ public class SmtpTransport extends Transport {
|
||||||
private boolean isEnhancedStatusCodesProvided;
|
private boolean isEnhancedStatusCodesProvided;
|
||||||
private int largestAcceptableMessage;
|
private int largestAcceptableMessage;
|
||||||
private boolean retryXoauthWithNewToken;
|
private boolean retryXoauthWithNewToken;
|
||||||
|
private boolean isPipeliningSupported;
|
||||||
|
private Queue<String> pipelinedCommand;
|
||||||
|
|
||||||
|
|
||||||
public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
|
public SmtpTransport(StoreConfig storeConfig, TrustedSocketFactory trustedSocketFactory,
|
||||||
|
@ -165,6 +169,7 @@ public class SmtpTransport extends Transport {
|
||||||
|
|
||||||
is8bitEncodingAllowed = extensions.containsKey("8BITMIME");
|
is8bitEncodingAllowed = extensions.containsKey("8BITMIME");
|
||||||
isEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES");
|
isEnhancedStatusCodesProvided = extensions.containsKey("ENHANCEDSTATUSCODES");
|
||||||
|
isPipeliningSupported = extensions.containsKey("PIPELINING");
|
||||||
|
|
||||||
if (connectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) {
|
if (connectionSecurity == ConnectionSecurity.STARTTLS_REQUIRED) {
|
||||||
if (extensions.containsKey("STARTTLS")) {
|
if (extensions.containsKey("STARTTLS")) {
|
||||||
|
@ -434,17 +439,37 @@ 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 (is8bitEncodingAllowed) {
|
if (isPipeliningSupported) {
|
||||||
executeCommand("MAIL FROM:<%s> BODY=8BITMIME", fromAddress);
|
pipelinedCommand = new LinkedList<>();
|
||||||
|
if (is8bitEncodingAllowed) {
|
||||||
|
executeCommandPipelined("MAIL FROM:<%s> BODY=8BITMIME", fromAddress);
|
||||||
|
} else {
|
||||||
|
executeCommandPipelined("MAIL FROM:<%s>", fromAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String address : addresses) {
|
||||||
|
executeCommandPipelined("RCPT TO:<%s>", address);
|
||||||
|
}
|
||||||
|
|
||||||
|
executeCommandPipelined("DATA");
|
||||||
|
CommandResponse commandResponse = readPipelinedResponse();
|
||||||
|
if (commandResponse.replyCode != 354) {
|
||||||
|
String replyText = TextUtils.join(" ", commandResponse.results);
|
||||||
|
throw new NegativeSmtpReplyException(commandResponse.replyCode, replyText);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
executeCommand("MAIL FROM:<%s>", fromAddress);
|
if (is8bitEncodingAllowed) {
|
||||||
}
|
executeCommand("MAIL FROM:<%s> BODY=8BITMIME", fromAddress);
|
||||||
|
} else {
|
||||||
|
executeCommand("MAIL FROM:<%s>", fromAddress);
|
||||||
|
}
|
||||||
|
|
||||||
for (String address : addresses) {
|
for (String address : addresses) {
|
||||||
executeCommand("RCPT TO:<%s>", address);
|
executeCommand("RCPT TO:<%s>", address);
|
||||||
}
|
}
|
||||||
|
|
||||||
executeCommand("DATA");
|
executeCommand("DATA");
|
||||||
|
}
|
||||||
|
|
||||||
EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream(
|
EOLConvertingOutputStream msgOut = new EOLConvertingOutputStream(
|
||||||
new LineWrapOutputStream(new SmtpDataStuffing(outputStream), 1000));
|
new LineWrapOutputStream(new SmtpDataStuffing(outputStream), 1000));
|
||||||
|
@ -622,6 +647,64 @@ public class SmtpTransport extends Transport {
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void executeCommandPipelined(String format, Object... args) throws IOException {
|
||||||
|
if (format != null) {
|
||||||
|
String command = String.format(Locale.ROOT, format, args);
|
||||||
|
pipelinedCommand.add(command);
|
||||||
|
writeLine(command, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandResponse readPipelinedResponse() throws IOException, MessagingException {
|
||||||
|
String responseLine = null;
|
||||||
|
List<String> results = null;
|
||||||
|
int noOfPipelinedResponse = pipelinedCommand.size();
|
||||||
|
while (noOfPipelinedResponse > 0) {
|
||||||
|
results = new ArrayList<>();
|
||||||
|
responseLine = readCommandResponseLine(results);
|
||||||
|
try {
|
||||||
|
responseLineToCommandResponse(responseLine, results);
|
||||||
|
|
||||||
|
} catch (NegativeSmtpReplyException exception) {
|
||||||
|
//continue reading response till DATA response .
|
||||||
|
Timber.d("SMTP <<< " + exception.getReplyCode() + exception.getReplyText());
|
||||||
|
|
||||||
|
} catch (MessagingException exception) {
|
||||||
|
//continue reading response till DATA response .
|
||||||
|
|
||||||
|
}
|
||||||
|
noOfPipelinedResponse-- ;
|
||||||
|
}
|
||||||
|
return responseLineToCommandResponse(responseLine, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandResponse responseLineToCommandResponse(String line, List<String> results) throws MessagingException {
|
||||||
|
int length = line.length();
|
||||||
|
if (length < 1) {
|
||||||
|
throw new MessagingException("SMTP response is 0 length");
|
||||||
|
}
|
||||||
|
|
||||||
|
int replyCode = -1;
|
||||||
|
if (length >= 3) {
|
||||||
|
try {
|
||||||
|
replyCode = Integer.parseInt(line.substring(0, 3));
|
||||||
|
} catch (NumberFormatException e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
char replyCodeCategory = line.charAt(0);
|
||||||
|
boolean isReplyCodeErrorCategory = (replyCodeCategory == '4') || (replyCodeCategory == '5');
|
||||||
|
if (isReplyCodeErrorCategory) {
|
||||||
|
if (isEnhancedStatusCodesProvided) {
|
||||||
|
throw buildEnhancedNegativeSmtpReplyException(replyCode, results);
|
||||||
|
} else {
|
||||||
|
String replyText = TextUtils.join(" ", results);
|
||||||
|
throw new NegativeSmtpReplyException(replyCode, replyText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CommandResponse(replyCode, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void saslAuthLogin() throws MessagingException, IOException {
|
private void saslAuthLogin() throws MessagingException, IOException {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -689,6 +689,78 @@ public class SmtpTransportTest {
|
||||||
server.verifyInteractionCompleted();
|
server.verifyInteractionCompleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sendMessage_withPipelining() throws Exception {
|
||||||
|
Message message = getDefaultMessage();
|
||||||
|
MockSmtpServer server = createServerAndSetupForPlainAuthentication("PIPELINING");
|
||||||
|
server.expect("MAIL FROM:<user@localhost>");
|
||||||
|
server.expect("RCPT TO:<user2@localhost>");
|
||||||
|
server.expect("DATA");
|
||||||
|
server.output("250 OK");
|
||||||
|
server.output("250 OK");
|
||||||
|
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_withoutPipelining() throws Exception {
|
||||||
|
Message message = getDefaultMessage();
|
||||||
|
MockSmtpServer server = createServerAndSetupForPlainAuthentication();
|
||||||
|
server.expect("MAIL FROM:<user@localhost>");
|
||||||
|
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 sendMessagePipelining_withNegativeReply() throws Exception {
|
||||||
|
Message message = getDefaultMessage();
|
||||||
|
MockSmtpServer server = createServerAndSetupForPlainAuthentication("PIPELINING");
|
||||||
|
server.expect("MAIL FROM:<user@localhost>");
|
||||||
|
server.expect("RCPT TO:<user2@localhost>");
|
||||||
|
server.expect("DATA");
|
||||||
|
server.output("250 OK");
|
||||||
|
server.output("421 4.7.0 Temporary system problem");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server) throws IOException,
|
private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server) throws IOException,
|
||||||
MessagingException {
|
MessagingException {
|
||||||
return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
|
return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
|
||||||
|
|
Loading…
Reference in a new issue