Merge pull request #1649 from philipwhiuk/smtpTesting

SMTP: Testing SmtpTransport using new MockSmtpServer
This commit is contained in:
cketti 2016-10-19 10:04:41 +02:00 committed by GitHub
commit 21d4ceb0d8
8 changed files with 1141 additions and 107 deletions

View file

@ -0,0 +1,67 @@
package com.fsck.k9.mail.helpers;
import java.io.IOException;
import java.io.OutputStream;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.internet.MimeMessage;
import okio.BufferedSink;
import okio.Okio;
class TestMessage extends MimeMessage {
private final long messageSize;
private final Address[] from;
private final Address[] to;
private final boolean hasAttachments;
TestMessage(TestMessageBuilder builder) {
from = toAddressArray(builder.from);
to = toAddressArray(builder.to);
hasAttachments = builder.hasAttachments;
messageSize = builder.messageSize;
}
@Override
public Address[] getFrom() {
return from;
}
@Override
public Address[] getRecipients(RecipientType type) {
switch (type) {
case TO:
return to;
case CC:
return new Address[0];
case BCC:
return new Address[0];
}
throw new AssertionError("Missing switch case: " + type);
}
@Override
public boolean hasAttachments() {
return hasAttachments;
}
@Override
public long calculateSize() {
return messageSize;
}
@Override
public void writeTo(OutputStream out) throws IOException, MessagingException {
BufferedSink bufferedSink = Okio.buffer(Okio.sink(out));
bufferedSink.writeUtf8("[message data]");
bufferedSink.emit();
}
private static Address[] toAddressArray(String email) {
return email == null ? new Address[0] : new Address[] { new Address(email) };
}
}

View file

@ -0,0 +1,37 @@
package com.fsck.k9.mail.helpers;
import com.fsck.k9.mail.Message;
public class TestMessageBuilder {
String from;
String to;
boolean hasAttachments;
long messageSize;
public TestMessageBuilder from(String email) {
from = email;
return this;
}
public TestMessageBuilder to(String email) {
to = email;
return this;
}
public TestMessageBuilder setHasAttachments(boolean hasAttachments) {
this.hasAttachments = hasAttachments;
return this;
}
public TestMessageBuilder messageSize(long messageSize) {
this.messageSize = messageSize;
return this;
}
public Message build() {
return new TestMessage(this);
}
}

View file

@ -0,0 +1,33 @@
package com.fsck.k9.mail.helpers;
import java.io.IOException;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
public class TestTrustedSocketFactory implements TrustedSocketFactory {
@Override
public Socket createSocket(Socket socket, String host, int port, String clientCertificateAlias)
throws NoSuchAlgorithmException, KeyManagementException, MessagingException, IOException {
TrustManager[] trustManagers = new TrustManager[] { new VeryTrustingTrustManager() };
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
return sslSocketFactory.createSocket(
socket,
socket.getInetAddress().getHostAddress(),
socket.getPort(),
true);
}
}

View file

@ -0,0 +1,28 @@
package com.fsck.k9.mail.helpers;
import android.annotation.SuppressLint;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
@SuppressLint("TrustAllX509TrustManager")
class VeryTrustingTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// Accept all certificates
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// Accept all certificates
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}

View file

@ -2,14 +2,8 @@ package com.fsck.k9.mail.store.imap;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import android.annotation.SuppressLint;
import android.net.ConnectivityManager;
import com.fsck.k9.mail.AuthType;
@ -21,10 +15,8 @@ import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import com.fsck.k9.mail.store.imap.mockserver.MockImapServer;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory;
import okio.ByteString;
import org.junit.Before;
import org.junit.Test;
@ -36,7 +28,6 @@ import org.robolectric.shadows.ShadowLog;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -688,41 +679,4 @@ public class ImapConnectionTest {
server.expect("2 LOGIN \"" + USERNAME + "\" \"" + PASSWORD + "\"");
server.output("2 OK LOGIN completed");
}
private static class TestTrustedSocketFactory implements TrustedSocketFactory {
@Override
public Socket createSocket(Socket socket, String host, int port, String clientCertificateAlias)
throws NoSuchAlgorithmException, KeyManagementException, MessagingException, IOException {
TrustManager[] trustManagers = new TrustManager[] { new VeryTrustingTrustManager() };
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
return sslSocketFactory.createSocket(
socket,
socket.getInetAddress().getHostAddress(),
socket.getPort(),
true);
}
}
@SuppressLint("TrustAllX509TrustManager")
private static class VeryTrustingTrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// Accept all certificates
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
// Accept all certificates
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}

View file

@ -1,121 +1,450 @@
package com.fsck.k9.mail.transport;
import java.io.IOException;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.CertificateValidationException;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.ServerSettings.Type;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.helpers.TestMessageBuilder;
import com.fsck.k9.mail.helpers.TestTrustedSocketFactory;
import com.fsck.k9.mail.internet.MimeMessage;
import com.fsck.k9.mail.ssl.TrustedSocketFactory;
import com.fsck.k9.mail.store.StoreConfig;
import com.fsck.k9.mail.transport.mockServer.MockSmtpServer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 21)
public class SmtpTransportTest {
private static final String USERNAME = "user";
private static final String PASSWORD = "password";
private static final String CLIENT_CERTIFICATE_ALIAS = null;
@Test
public void decodeUri_canDecodeAuthType() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
private TrustedSocketFactory socketFactory;
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals(AuthType.PLAIN, result.authenticationType);
@Before
public void before() {
socketFactory = new TestTrustedSocketFactory();
}
@Test
public void decodeUri_canDecodeUsername() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
public void SmtpTransport_withValidTransportUri() throws Exception {
StoreConfig storeConfig = createStoreConfigWithTransportUri("smtp://user:password:CRAM_MD5@server:123456");
ServerSettings result = SmtpTransport.decodeUri(storeUri);
new SmtpTransport(storeConfig, socketFactory);
}
assertEquals("user", result.username);
@Test(expected = MessagingException.class)
public void SmtpTransport_withInvalidTransportUri_shouldThrow() throws Exception {
StoreConfig storeConfig = createStoreConfigWithTransportUri("smpt://");
new SmtpTransport(storeConfig, socketFactory);
}
@Test
public void decodeUri_canDecodePassword() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
public void open_withoutAuthLoginExtension_shouldConnectWithoutAuthentication() 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 OK");
SmtpTransport transport = startServerAndCreateSmtpTransportWithoutPassword(server);
ServerSettings result = SmtpTransport.decodeUri(storeUri);
transport.open();
assertEquals("password", result.password);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void decodeUri_canDecodeHost() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
public void open_withAuthPlainExtension() 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 AUTH PLAIN LOGIN");
server.expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=");
server.output("235 2.7.0 Authentication successful");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
ServerSettings result = SmtpTransport.decodeUri(storeUri);
transport.open();
assertEquals("server", result.host);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void decodeUri_canDecodePort() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
public void open_withAuthLoginExtension() 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 AUTH LOGIN");
server.expect("AUTH LOGIN");
server.output("250 OK");
server.expect("dXNlcg==");
server.output("250 OK");
server.expect("cGFzc3dvcmQ=");
server.output("235 2.7.0 Authentication successful");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
ServerSettings result = SmtpTransport.decodeUri(storeUri);
transport.open();
assertEquals(123456, result.port);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void decodeUri_canDecodeTLS() {
String storeUri = "smtp+tls+://user:password:PLAIN@server:123456";
public void open_withoutLoginAndPlainAuthExtensions_shouldThrow() 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 AUTH");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
ServerSettings result = SmtpTransport.decodeUri(storeUri);
try {
transport.open();
fail("Exception expected");
} catch (MessagingException e) {
assertEquals("Authentication methods SASL PLAIN and LOGIN are unavailable.", e.getMessage());
}
assertEquals(ConnectionSecurity.STARTTLS_REQUIRED, result.connectionSecurity);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void decodeUri_canDecodeSSL() {
String storeUri = "smtp+ssl+://user:password:PLAIN@server:123456";
public void open_withCramMd5AuthExtension() 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 AUTH CRAM-MD5");
server.expect("AUTH CRAM-MD5");
server.output(Base64.encode("<24609.1047914046@localhost>"));
server.expect("dXNlciA3NmYxNWEzZmYwYTNiOGI1NzcxZmNhODZlNTcyMDk2Zg==");
server.output("235 2.7.0 Authentication successful");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.CRAM_MD5, ConnectionSecurity.NONE);
ServerSettings result = SmtpTransport.decodeUri(storeUri);
transport.open();
assertEquals(ConnectionSecurity.SSL_TLS_REQUIRED, result.connectionSecurity);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void decodeUri_canDecodeClientCert() {
String storeUri = "smtp+ssl+://user:clientCert:EXTERNAL@server:123456";
public void open_withoutCramMd5AuthExtension_shouldThrow() 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 AUTH PLAIN LOGIN");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.CRAM_MD5, ConnectionSecurity.NONE);
ServerSettings result = SmtpTransport.decodeUri(storeUri);
try {
transport.open();
fail("Exception expected");
} catch (MessagingException e) {
assertEquals("Authentication method CRAM-MD5 is unavailable.", e.getMessage());
}
assertEquals("clientCert", result.clientCertificateAlias);
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void createUri_canEncodeSmtpSslUri() {
public void open_withAuthExternalExtension() 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 AUTH EXTERNAL");
server.expect("AUTH EXTERNAL dXNlcg==");
server.output("235 2.7.0 Authentication successful");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.EXTERNAL, ConnectionSecurity.NONE);
transport.open();
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void open_withoutAuthExternalExtension_shouldThrow() 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 AUTH");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.EXTERNAL, ConnectionSecurity.NONE);
try {
transport.open();
fail("Exception expected");
} catch (CertificateValidationException e) {
assertEquals(CertificateValidationException.Reason.MissingCapability, e.getReason());
}
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void open_withAutomaticAuthAndNoTransportSecurityAndAuthCramMd5Extension_shouldUseAuthCramMd5()
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 AUTH CRAM-MD5");
server.expect("AUTH CRAM-MD5");
server.output(Base64.encode("<24609.1047914046@localhost>"));
server.expect("dXNlciA3NmYxNWEzZmYwYTNiOGI1NzcxZmNhODZlNTcyMDk2Zg==");
server.output("235 2.7.0 Authentication successful");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.AUTOMATIC,
ConnectionSecurity.NONE);
transport.open();
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void open_withAutomaticAuthAndNoTransportSecurityAndAuthPlainExtension_shouldThrow() 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 AUTH PLAIN LOGIN");
SmtpTransport transport = startServerAndCreateSmtpTransport(server, AuthType.AUTOMATIC,
ConnectionSecurity.NONE);
try {
transport.open();
fail("Exception expected");
} catch (MessagingException e) {
assertEquals("Update your outgoing server authentication setting. AUTOMATIC auth. is unavailable.",
e.getMessage());
}
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void open_withEhloFailing_shouldTryHelo() throws Exception {
MockSmtpServer server = new MockSmtpServer();
server.output("220 localhost Simple Mail Transfer Service Ready");
server.expect("EHLO localhost");
server.output("502 5.5.1, Unrecognized command.");
server.expect("HELO localhost");
server.output("250 localhost");
SmtpTransport transport = startServerAndCreateSmtpTransportWithoutPassword(server);
transport.open();
server.verifyConnectionStillOpen();
server.verifyInteractionCompleted();
}
@Test
public void sendMessage_withoutAddressToSendTo_shouldNotOpenConnection() throws Exception {
MimeMessage message = new MimeMessage();
MockSmtpServer server = createServerAndSetupForPlainAuthentication();
SmtpTransport transport = startServerAndCreateSmtpTransport(server);
transport.sendMessage(message);
server.verifyConnectionNeverCreated();
}
@Test
public void sendMessage_withSingleRecipient() 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 sendMessage_with8BitEncoding() 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()
.setHasAttachments(true)
.messageSize(1234L)
.build();
MockSmtpServer server = createServerAndSetupForPlainAuthentication("SIZE 1000");
SmtpTransport transport = startServerAndCreateSmtpTransport(server);
try {
transport.sendMessage(message);
fail("Expected message too large error");
} catch (MessagingException e) {
assertTrue(e.isPermanentFailure());
assertEquals("Message too large for server", e.getMessage());
}
//FIXME: Make sure connection was closed
//server.verifyConnectionClosed();
}
@Test
public void sendMessage_withNegativeReply_shouldThrow() 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("421 4.7.0 Temporary system problem");
server.expect("QUIT");
server.output("221 BYE");
server.closeConnection();
SmtpTransport transport = startServerAndCreateSmtpTransport(server);
try {
transport.sendMessage(message);
fail("Expected exception");
} catch (SmtpTransport.NegativeSmtpReplyException e) {
assertEquals(421, e.getReplyCode());
assertEquals("4.7.0 Temporary system problem", e.getReplyText());
}
server.verifyConnectionClosed();
server.verifyInteractionCompleted();
}
private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server) throws IOException,
MessagingException {
return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE);
}
private SmtpTransport startServerAndCreateSmtpTransportWithoutPassword(MockSmtpServer server) throws IOException,
MessagingException {
return startServerAndCreateSmtpTransport(server, AuthType.PLAIN, ConnectionSecurity.NONE, null);
}
private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server, AuthType authenticationType,
ConnectionSecurity connectionSecurity) throws IOException, MessagingException {
return startServerAndCreateSmtpTransport(server, authenticationType, connectionSecurity, PASSWORD);
}
private SmtpTransport startServerAndCreateSmtpTransport(MockSmtpServer server, AuthType authenticationType,
ConnectionSecurity connectionSecurity, String password) throws IOException, MessagingException {
server.start();
String host = server.getHost();
int port = server.getPort();
ServerSettings serverSettings = new ServerSettings(
ServerSettings.Type.SMTP, "server", 123456,
ConnectionSecurity.SSL_TLS_REQUIRED, AuthType.EXTERNAL,
"user", "password", "clientCert");
Type.SMTP,
host,
port,
connectionSecurity,
authenticationType,
USERNAME,
password,
CLIENT_CERTIFICATE_ALIAS);
String uri = SmtpTransport.createUri(serverSettings);
StoreConfig storeConfig = createStoreConfigWithTransportUri(uri);
String result = SmtpTransport.createUri(serverSettings);
assertEquals("smtp+ssl+://user:clientCert:EXTERNAL@server:123456", result);
return new SmtpTransport(storeConfig, socketFactory);
}
@Test
public void createUri_canEncodeSmtpTlsUri() {
ServerSettings serverSettings = new ServerSettings(
ServerSettings.Type.SMTP, "server", 123456,
ConnectionSecurity.STARTTLS_REQUIRED, AuthType.PLAIN,
"user", "password", "clientCert");
String result = SmtpTransport.createUri(serverSettings);
assertEquals("smtp+tls+://user:password:PLAIN@server:123456", result);
private StoreConfig createStoreConfigWithTransportUri(String value) {
StoreConfig storeConfig = mock(StoreConfig.class);
when(storeConfig.getTransportUri()).thenReturn(value);
return storeConfig;
}
@Test
public void createUri_canEncodeSmtpUri() {
ServerSettings serverSettings = new ServerSettings(
ServerSettings.Type.SMTP, "server", 123456,
ConnectionSecurity.NONE, AuthType.CRAM_MD5,
"user", "password", "clientCert");
private TestMessageBuilder getDefaultMessageBuilder() {
return new TestMessageBuilder()
.from("user@localhost")
.to("user2@localhost");
}
String result = SmtpTransport.createUri(serverSettings);
private Message getDefaultMessage() {
return getDefaultMessageBuilder().build();
}
assertEquals("smtp://user:password:CRAM_MD5@server:123456", result);
private MockSmtpServer createServerAndSetupForPlainAuthentication(String... extensions) {
MockSmtpServer server = new MockSmtpServer();
server.output("220 localhost Simple Mail Transfer Service Ready");
server.expect("EHLO localhost");
server.output("250-localhost Hello client.localhost");
for (String extension : extensions) {
server.output("250-" + extension);
}
server.output("250 AUTH LOGIN PLAIN CRAM-MD5");
server.expect("AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=");
server.output("235 2.7.0 Authentication successful");
return server;
}
}

View file

@ -0,0 +1,157 @@
package com.fsck.k9.mail.transport;
import android.annotation.SuppressLint;
import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.ConnectionSecurity;
import com.fsck.k9.mail.ServerSettings;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@SuppressLint("AuthLeak")
public class SmtpTransportUriTest {
@Test
public void decodeUri_canDecodeAuthType() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals(AuthType.PLAIN, result.authenticationType);
}
@Test
public void decodeUri_canDecodeUsername() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals("user", result.username);
}
@Test
public void decodeUri_canDecodePassword() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals("password", result.password);
}
@Test
public void decodeUri_canDecodeUsername_withNoAuthType() {
String storeUri = "smtp://user:password@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals("user", result.username);
}
@Test
public void decodeUri_canDecodeUsername_withNoPasswordOrAuthType() {
String storeUri = "smtp://user@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals("user", result.username);
}
@Test
public void decodeUri_canDecodeAuthType_withEmptyPassword() {
String storeUri = "smtp://user::PLAIN@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals(AuthType.PLAIN, result.authenticationType);
}
@Test
public void decodeUri_canDecodeHost() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals("server", result.host);
}
@Test
public void decodeUri_canDecodePort() {
String storeUri = "smtp://user:password:PLAIN@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals(123456, result.port);
}
@Test
public void decodeUri_canDecodeTLS() {
String storeUri = "smtp+tls+://user:password:PLAIN@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals(ConnectionSecurity.STARTTLS_REQUIRED, result.connectionSecurity);
}
@Test
public void decodeUri_canDecodeSSL() {
String storeUri = "smtp+ssl+://user:password:PLAIN@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals(ConnectionSecurity.SSL_TLS_REQUIRED, result.connectionSecurity);
}
@Test
public void decodeUri_canDecodeClientCert() {
String storeUri = "smtp+ssl+://user:clientCert:EXTERNAL@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
assertEquals("clientCert", result.clientCertificateAlias);
}
@Test(expected = IllegalArgumentException.class)
public void decodeUri_forUnknownSchema_throwsIllegalArgumentException() {
String storeUri = "unknown://user:clientCert:EXTERNAL@server:123456";
ServerSettings result = SmtpTransport.decodeUri(storeUri);
}
@Test
public void createUri_canEncodeSmtpSslUri() {
ServerSettings serverSettings = new ServerSettings(
ServerSettings.Type.SMTP, "server", 123456,
ConnectionSecurity.SSL_TLS_REQUIRED, AuthType.EXTERNAL,
"user", "password", "clientCert");
String result = SmtpTransport.createUri(serverSettings);
assertEquals("smtp+ssl+://user:clientCert:EXTERNAL@server:123456", result);
}
@Test
public void createUri_canEncodeSmtpTlsUri() {
ServerSettings serverSettings = new ServerSettings(
ServerSettings.Type.SMTP, "server", 123456,
ConnectionSecurity.STARTTLS_REQUIRED, AuthType.PLAIN,
"user", "password", "clientCert");
String result = SmtpTransport.createUri(serverSettings);
assertEquals("smtp+tls+://user:password:PLAIN@server:123456", result);
}
@Test
public void createUri_canEncodeSmtpUri() {
ServerSettings serverSettings = new ServerSettings(
ServerSettings.Type.SMTP, "server", 123456,
ConnectionSecurity.NONE, AuthType.CRAM_MD5,
"user", "password", "clientCert");
String result = SmtpTransport.createUri(serverSettings);
assertEquals("smtp://user:password:CRAM_MD5@server:123456", result);
}
}

View file

@ -0,0 +1,429 @@
package com.fsck.k9.mail.transport.mockServer;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import android.annotation.SuppressLint;
import com.jcraft.jzlib.JZlib;
import com.jcraft.jzlib.ZOutputStream;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Okio;
import org.apache.commons.io.IOUtils;
@SuppressLint("NewApi")
public class MockSmtpServer {
private static final String KEYSTORE_PASSWORD = "password";
private static final String KEYSTORE_RESOURCE = "/keystore.jks";
private static final byte[] CRLF = { '\r', '\n' };
private final Deque<SmtpInteraction> interactions = new ConcurrentLinkedDeque<>();
private final CountDownLatch waitForConnectionClosed = new CountDownLatch(1);
private final CountDownLatch waitForAllExpectedCommands = new CountDownLatch(1);
private final Logger logger;
private MockServerThread mockServerThread;
private String host;
private int port;
public MockSmtpServer() {
this(new DefaultLogger());
}
public MockSmtpServer(Logger logger) {
this.logger = logger;
}
public void output(String response) {
checkServerNotRunning();
interactions.add(new CannedResponse(response));
}
public void expect(String command) {
checkServerNotRunning();
interactions.add(new ExpectedCommand(command));
}
public void closeConnection() {
checkServerNotRunning();
interactions.add(new CloseConnection());
}
public void start() throws IOException {
checkServerNotRunning();
InetAddress localAddress = InetAddress.getByName(null);
ServerSocket serverSocket = new ServerSocket(0, 1, localAddress);
InetSocketAddress localSocketAddress = (InetSocketAddress) serverSocket.getLocalSocketAddress();
host = localSocketAddress.getHostString();
port = serverSocket.getLocalPort();
mockServerThread = new MockServerThread(serverSocket, interactions, waitForConnectionClosed,
waitForAllExpectedCommands, logger);
mockServerThread.start();
}
public void shutdown() {
checkServerRunning();
mockServerThread.shouldStop();
waitForMockServerThread();
}
private void waitForMockServerThread() {
try {
mockServerThread.join(500L);
} catch (InterruptedException ignored) {
}
}
public String getHost() {
checkServerRunning();
return host;
}
public int getPort() {
checkServerRunning();
return port;
}
public void waitForInteractionToComplete() {
checkServerRunning();
try {
waitForAllExpectedCommands.await(1000L, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
}
}
public void verifyInteractionCompleted() {
shutdown();
if (!interactions.isEmpty()) {
throw new AssertionError("Interactions left: " + interactions.size());
}
UnexpectedCommandException unexpectedCommandException = mockServerThread.getUnexpectedCommandException();
if (unexpectedCommandException != null) {
throw new AssertionError(unexpectedCommandException.getMessage(), unexpectedCommandException);
}
}
public void verifyConnectionNeverCreated() {
checkServerRunning();
if (mockServerThread.clientConnectionCreated()) {
throw new AssertionError("Connection created when it shouldn't have been");
}
}
public void verifyConnectionStillOpen() {
checkServerRunning();
if (mockServerThread.isClientConnectionClosed()) {
throw new AssertionError("Connection closed when it shouldn't be");
}
}
public void verifyConnectionClosed() {
checkServerRunning();
try {
waitForConnectionClosed.await(300L, TimeUnit.MILLISECONDS);
} catch (InterruptedException ignored) {
}
if (!mockServerThread.isClientConnectionClosed()) {
throw new AssertionError("Connection open when is shouldn't be");
}
}
private void checkServerRunning() {
if (mockServerThread == null) {
throw new IllegalStateException("Server was never started");
}
}
private void checkServerNotRunning() {
if (mockServerThread != null) {
throw new IllegalStateException("Server was already started");
}
}
public interface Logger {
void log(String message);
void log(String format, Object... args);
}
private interface SmtpInteraction {
}
private static class ExpectedCommand implements SmtpInteraction {
private final String command;
public ExpectedCommand(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
}
private static class CannedResponse implements SmtpInteraction {
private final String response;
public CannedResponse(String response) {
this.response = response;
}
public String getResponse() {
return response;
}
}
private static class CloseConnection implements SmtpInteraction {
}
private static class UnexpectedCommandException extends Exception {
public UnexpectedCommandException(String expectedCommand, String receivedCommand) {
super("Expected <" + expectedCommand + ">, but received <" + receivedCommand + ">");
}
}
private static class MockServerThread extends Thread {
private final ServerSocket serverSocket;
private final Deque<SmtpInteraction> interactions;
private final CountDownLatch waitForConnectionClosed;
private final CountDownLatch waitForAllExpectedCommands;
private final Logger logger;
private volatile boolean shouldStop = false;
private volatile Socket clientSocket;
private BufferedSource input;
private BufferedSink output;
private volatile UnexpectedCommandException unexpectedCommandException;
public MockServerThread(ServerSocket serverSocket, Deque<SmtpInteraction> interactions,
CountDownLatch waitForConnectionClosed, CountDownLatch waitForAllExpectedCommands, Logger logger) {
super("MockSmtpServer");
this.serverSocket = serverSocket;
this.interactions = interactions;
this.waitForConnectionClosed = waitForConnectionClosed;
this.waitForAllExpectedCommands = waitForAllExpectedCommands;
this.logger = logger;
}
@Override
public void run() {
String hostAddress = serverSocket.getInetAddress().getHostAddress();
int port = serverSocket.getLocalPort();
logger.log("Listening on %s:%d", hostAddress, port);
Socket socket = null;
try {
socket = acceptConnectionAndCloseServerSocket();
clientSocket = socket;
String remoteHostAddress = socket.getInetAddress().getHostAddress();
int remotePort = socket.getPort();
logger.log("Accepted connection from %s:%d", remoteHostAddress, remotePort);
input = Okio.buffer(Okio.source(socket));
output = Okio.buffer(Okio.sink(socket));
while (!shouldStop && !interactions.isEmpty()) {
handleInteractions(socket);
}
waitForAllExpectedCommands.countDown();
while (!shouldStop) {
readAdditionalCommands();
}
waitForConnectionClosed.countDown();
} catch (UnexpectedCommandException e) {
unexpectedCommandException = e;
} catch (IOException e) {
if (!shouldStop) {
logger.log("Exception: %s", e);
}
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException |
NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
IOUtils.closeQuietly(socket);
logger.log("Exiting");
}
private void handleInteractions(Socket socket) throws IOException, KeyStoreException,
NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException,
UnexpectedCommandException {
SmtpInteraction interaction = interactions.pop();
if (interaction instanceof ExpectedCommand) {
readExpectedCommand((ExpectedCommand) interaction);
} else if (interaction instanceof CannedResponse) {
writeCannedResponse((CannedResponse) interaction);
} else if (interaction instanceof CloseConnection) {
clientSocket.close();
}
}
private void readExpectedCommand(ExpectedCommand expectedCommand) throws IOException,
UnexpectedCommandException {
String command = input.readUtf8Line();
if (command == null) {
throw new EOFException();
}
logger.log("C: %s", command);
String expected = expectedCommand.getCommand();
if (!command.equals(expected)) {
logger.log("EXPECTED: %s", expected);
logger.log("ACTUAL: %s", command);
throw new UnexpectedCommandException(expected, command);
}
}
private void writeCannedResponse(CannedResponse cannedResponse) throws IOException {
String response = cannedResponse.getResponse();
logger.log("S: %s", response);
output.writeUtf8(response);
output.write(CRLF);
output.flush();
}
private void enableCompression(Socket socket) throws IOException {
InputStream inputStream = new InflaterInputStream(socket.getInputStream(), new Inflater(true));
input = Okio.buffer(Okio.source(inputStream));
ZOutputStream outputStream = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_SPEED, true);
outputStream.setFlushMode(JZlib.Z_PARTIAL_FLUSH);
output = Okio.buffer(Okio.sink(outputStream));
}
private void upgradeToTls(Socket socket) throws KeyStoreException, IOException, NoSuchAlgorithmException,
CertificateException, UnrecoverableKeyException, KeyManagementException {
KeyStore keyStore = loadKeyStore();
String defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(defaultAlgorithm);
keyManagerFactory.init(keyStore, KEYSTORE_PASSWORD.toCharArray());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true);
sslSocket.setUseClientMode(false);
sslSocket.startHandshake();
input = Okio.buffer(Okio.source(sslSocket.getInputStream()));
output = Okio.buffer(Okio.sink(sslSocket.getOutputStream()));
}
private KeyStore loadKeyStore() throws KeyStoreException, IOException, NoSuchAlgorithmException,
CertificateException {
KeyStore keyStore = KeyStore.getInstance("JKS");
InputStream keyStoreInputStream = getClass().getResourceAsStream(KEYSTORE_RESOURCE);
try {
keyStore.load(keyStoreInputStream, KEYSTORE_PASSWORD.toCharArray());
} finally {
keyStoreInputStream.close();
}
return keyStore;
}
private void readAdditionalCommands() throws IOException {
String command = input.readUtf8Line();
if (command == null) {
throw new EOFException();
}
logger.log("Received additional command: %s", command);
}
private Socket acceptConnectionAndCloseServerSocket() throws IOException {
Socket socket = serverSocket.accept();
serverSocket.close();
return socket;
}
public void shouldStop() {
shouldStop = true;
IOUtils.closeQuietly(clientSocket);
}
public boolean clientConnectionCreated() {
return clientSocket != null;
}
public boolean isClientConnectionClosed() {
return clientSocket.isClosed();
}
public UnexpectedCommandException getUnexpectedCommandException() {
return unexpectedCommandException;
}
}
private static class DefaultLogger implements Logger {
@Override
public void log(String message) {
System.out.println("MockSmtpServer: " + message);
}
@Override
public void log(String format, Object... args) {
log(String.format(format, args));
}
}
}