Merge pull request #1649 from philipwhiuk/smtpTesting
SMTP: Testing SmtpTransport using new MockSmtpServer
This commit is contained in:
commit
21d4ceb0d8
8 changed files with 1141 additions and 107 deletions
|
@ -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) };
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue