├── .gitignore ├── doc ├── logo.jpg ├── cli.md ├── introduction.md └── plugin.md ├── src ├── test │ ├── resources │ │ ├── META-INF │ │ │ └── services │ │ │ │ └── org.junit.platform.launcher.LauncherSessionListener │ │ ├── keystore.jks │ │ ├── mime │ │ │ ├── logo.jpg │ │ │ ├── selfie.jpg │ │ │ └── robin.article.pdf │ │ ├── properties.json5 │ │ ├── cases │ │ │ └── config │ │ │ │ ├── request │ │ │ │ ├── get.json5 │ │ │ │ ├── delete.json5 │ │ │ │ ├── put-json.json5 │ │ │ │ ├── post-json.json5 │ │ │ │ ├── post-files.json5 │ │ │ │ └── put-files.json5 │ │ │ │ ├── pangrams.json5 │ │ │ │ ├── xclient.json5 │ │ │ │ ├── behaviour.json5 │ │ │ │ └── dynamic │ │ │ │ └── dynamic.json5 │ │ ├── mapper.json5 │ │ ├── sample.log │ │ ├── client.json5 │ │ ├── log4j2.xml │ │ ├── lipsum.plain.eml │ │ ├── case.json5 │ │ └── server.json5 │ └── java │ │ ├── com │ │ └── mimecast │ │ │ └── robin │ │ │ ├── smtp │ │ │ ├── EmailDeliveryMock.java │ │ │ ├── auth │ │ │ │ ├── NotRandomTest.java │ │ │ │ ├── SecureRandomTest.java │ │ │ │ ├── PlainTest.java │ │ │ │ ├── DigestDataTest.java │ │ │ │ └── LoginTest.java │ │ │ ├── security │ │ │ │ └── PermissiveTrustManagerTest.java │ │ │ ├── session │ │ │ │ ├── XclientSessionTest.java │ │ │ │ └── ConfigMapperTest.java │ │ │ ├── extension │ │ │ │ ├── client │ │ │ │ │ ├── ClientQuitTest.java │ │ │ │ │ ├── ClientRsetTest.java │ │ │ │ │ ├── ClientMailTest.java │ │ │ │ │ ├── ClientXclientTest.java │ │ │ │ │ └── ClientHelpTest.java │ │ │ │ └── server │ │ │ │ │ ├── ServerRsetTest.java │ │ │ │ │ ├── ServerQuitTest.java │ │ │ │ │ ├── ServerStartTlsTest.java │ │ │ │ │ ├── ServerRcptTest.java │ │ │ │ │ ├── ServerXclientTest.java │ │ │ │ │ ├── ServerHelpTest.java │ │ │ │ │ ├── ServerEhloTest.java │ │ │ │ │ └── ServerMailTest.java │ │ │ ├── io │ │ │ │ ├── LineInputStreamTest.java │ │ │ │ ├── SlowInputStreamTest.java │ │ │ │ └── SlowOutputStreamTest.java │ │ │ └── connection │ │ │ │ └── ConnectionTest.java │ │ │ ├── assertion │ │ │ └── client │ │ │ │ ├── humio │ │ │ │ ├── HumioExternalClientMock.java │ │ │ │ └── HumioClientMock.java │ │ │ │ └── logs │ │ │ │ └── LogsExternalClientMock.java │ │ │ ├── annotation │ │ │ └── AnnotationLoaderTest.java │ │ │ ├── http │ │ │ ├── MockHttpClient.java │ │ │ └── MockOkHttpClient.java │ │ │ ├── config │ │ │ ├── BasicConfigTest.java │ │ │ ├── ConfigLoaderTest.java │ │ │ ├── server │ │ │ │ ├── UserConfigTest.java │ │ │ │ ├── ScenarioConfigTest.java │ │ │ │ └── ServerConfigTest.java │ │ │ ├── client │ │ │ │ ├── ClientConfigTest.java │ │ │ │ └── RouteConfigTest.java │ │ │ ├── assertion │ │ │ │ └── AssertConfigTest.java │ │ │ └── PropertiesTest.java │ │ │ ├── util │ │ │ ├── RandomTest.java │ │ │ ├── PathUtilsTest.java │ │ │ ├── MapUtilsTest.java │ │ │ └── MagicTest.java │ │ │ ├── MainMock.java │ │ │ └── mime │ │ │ └── parts │ │ │ └── PdfMimePartTest.java │ │ ├── benchmark │ │ ├── Benchmarks.java │ │ ├── EmailBuilderBench.java │ │ └── EmailParserBench.java │ │ ├── cases │ │ └── ExampleSend.java │ │ └── SetupListener.java └── main │ └── java │ └── com │ └── mimecast │ └── robin │ ├── util │ ├── package-info.java │ ├── CharsetDetector.java │ ├── Sleep.java │ ├── StreamUtils.java │ ├── Random.java │ ├── UIDExtractor.java │ └── MapUtils.java │ ├── smtp │ ├── io │ │ ├── package-info.java │ │ ├── SlowOutputStream.java │ │ └── SlowInputStream.java │ ├── security │ │ ├── package-info.java │ │ ├── TLSSocket.java │ │ └── PermissiveTrustManager.java │ ├── extension │ │ ├── package-info.java │ │ ├── client │ │ │ ├── package-info.java │ │ │ ├── Behaviour.java │ │ │ ├── ClientQuit.java │ │ │ ├── ClientHelp.java │ │ │ ├── ClientProcessor.java │ │ │ ├── ClientRset.java │ │ │ ├── XclientBehaviour.java │ │ │ ├── ClientXclient.java │ │ │ ├── ClientRcpt.java │ │ │ └── ClientStartTls.java │ │ ├── server │ │ │ ├── package-info.java │ │ │ ├── ServerRset.java │ │ │ ├── ServerQuit.java │ │ │ ├── ServerHelp.java │ │ │ ├── ServerXclient.java │ │ │ ├── ServerProcessor.java │ │ │ ├── ServerRcpt.java │ │ │ └── ServerStartTls.java │ │ └── Extension.java │ ├── auth │ │ ├── package-info.java │ │ ├── Random.java │ │ ├── SecureRandom.java │ │ ├── NotRandom.java │ │ ├── InstanceDigestCache.java │ │ ├── StaticDigestCache.java │ │ ├── Login.java │ │ ├── Plain.java │ │ └── DigestCache.java │ ├── verb │ │ ├── package-info.java │ │ ├── EhloVerb.java │ │ └── BdatVerb.java │ ├── session │ │ ├── package-info.java │ │ └── XclientSession.java │ ├── connection │ │ ├── package-info.java │ │ └── SmtpException.java │ └── transaction │ │ ├── package-info.java │ │ ├── SessionTransactionList.java │ │ └── EnvelopeTransactionList.java │ ├── config │ ├── client │ │ ├── package-info.java │ │ ├── LoggingConfig.java │ │ ├── ClientConfig.java │ │ └── RouteConfig.java │ ├── server │ │ ├── package-info.java │ │ └── UserConfig.java │ ├── assertion │ │ ├── package-info.java │ │ └── external │ │ │ ├── logs │ │ │ └── LogsExternalClientConfig.java │ │ │ └── ExternalConfig.java │ ├── package-info.java │ └── BasicConfig.java │ ├── annotation │ ├── plugin │ │ ├── package-info.java │ │ ├── RequestPlugin.java │ │ ├── HumioPlugin.java │ │ └── XclientPlugin.java │ ├── Plugin.java │ ├── package-info.java │ └── AnnotationLoader.java │ ├── mime │ ├── parts │ │ ├── MultipartMimePart.java │ │ └── TextMimePart.java │ ├── HashType.java │ └── headers │ │ └── MimeHeaders.java │ ├── assertion │ ├── client │ │ └── package-info.java │ ├── AssertException.java │ └── AssertExternalGroup.java │ ├── storage │ ├── package-info.java │ └── StorageClient.java │ ├── http │ ├── HttpMethod.java │ └── HttpResponse.java │ ├── main │ ├── Foundation.java │ └── ServerCLI.java │ └── package-info.java ├── .github ├── dependabot.yml └── workflows │ └── maven.yml ├── inventory.yaml ├── cfg ├── properties.json5 ├── log4j2.xml └── client.json5 └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | log/ 4 | target/ 5 | -------------------------------------------------------------------------------- /doc/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimecast/robin/HEAD/doc/logo.jpg -------------------------------------------------------------------------------- /src/test/resources/META-INF/services/org.junit.platform.launcher.LauncherSessionListener: -------------------------------------------------------------------------------- 1 | SetupListener 2 | -------------------------------------------------------------------------------- /src/test/resources/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimecast/robin/HEAD/src/test/resources/keystore.jks -------------------------------------------------------------------------------- /src/test/resources/mime/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimecast/robin/HEAD/src/test/resources/mime/logo.jpg -------------------------------------------------------------------------------- /src/test/resources/mime/selfie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimecast/robin/HEAD/src/test/resources/mime/selfie.jpg -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/util/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Utilities. 3 | */ 4 | package com.mimecast.robin.util; 5 | -------------------------------------------------------------------------------- /src/test/resources/mime/robin.article.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mimecast/robin/HEAD/src/test/resources/mime/robin.article.pdf -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/io/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP I/O streams. 3 | */ 4 | package com.mimecast.robin.smtp.io; 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/security/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP security package. 3 | */ 4 | package com.mimecast.robin.smtp.security; 5 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP extensions package. 3 | */ 4 | package com.mimecast.robin.smtp.extension; 5 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/client/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Client configuration accessors. 3 | */ 4 | package com.mimecast.robin.config.client; 5 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/server/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Server configuration accessors. 3 | */ 4 | package com.mimecast.robin.config.server; 5 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP authentication mechanisms package. 3 | */ 4 | package com.mimecast.robin.smtp.auth; 5 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/assertion/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Assertion configuration accessors. 3 | */ 4 | package com.mimecast.robin.config.assertion; 5 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP client extensions package. 3 | */ 4 | package com.mimecast.robin.smtp.extension.client; 5 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/server/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP server extensions package. 3 | */ 4 | package com.mimecast.robin.smtp.extension.server; 5 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/verb/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP verbs package. 3 | * 4 | *

Provides classes to parse generic and custom verbs. 5 | */ 6 | package com.mimecast.robin.smtp.verb; 7 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/annotation/plugin/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugin interface and container package. 3 | * 4 | *

Plugins are only loaded from this package.

5 | */ 6 | package com.mimecast.robin.annotation.plugin; 7 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/session/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP session package. 3 | * 4 | *

XclientSession extends Session to provide XCLIENT specific configuration meta. 5 | */ 6 | package com.mimecast.robin.smtp.session; 7 | -------------------------------------------------------------------------------- /inventory.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | component: Robin 3 | project_name: Robin 4 | full_path: https://github.com/mimecast/robin 5 | component_usage: OPENSOURCE 6 | owning_team: Vlad Marian 7 | risk_category: low 8 | application_purpose: testing 9 | secrets_scan_date: '2023-01-10' 10 | dev_language: 11 | - Java 12 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/connection/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP connection and foundation. 3 | * 4 | *

Connection and SmtpFoundation work together to provide the core networking and containers mostly common to both server and client. 5 | */ 6 | package com.mimecast.robin.smtp.connection; 7 | -------------------------------------------------------------------------------- /src/test/resources/properties.json5: -------------------------------------------------------------------------------- 1 | { 2 | "digestmd5.random": "64", 3 | 4 | boolean: true, 5 | long: 7, 6 | string: "string", 7 | sub: { 8 | string: "substring" 9 | }, 10 | list: [ 11 | "monkey", 12 | "weasel", 13 | "dragon" 14 | ], 15 | map: { 16 | string: "map" 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/mime/parts/MultipartMimePart.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.mime.parts; 2 | 3 | /** 4 | * MIME part container from multiparts. 5 | */ 6 | public class MultipartMimePart extends MimePart { 7 | 8 | /** 9 | * Constructs a new FileMimePart instance. 10 | */ 11 | public MultipartMimePart() { 12 | // Do nothing. 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/EmailDeliveryMock.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | 5 | public class EmailDeliveryMock extends EmailDelivery { 6 | 7 | public EmailDeliveryMock(Connection connection) { 8 | super(connection.getSession()); 9 | this.connection = connection; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/assertion/client/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * External logs client. 3 | * 4 | *

Interface for implementing external logs client. 5 | * 6 | *

Ideally any such clients would reside in this package. 7 | * 8 | *

The plugins system is meant to be used for registering among other the external clients. 9 | */ 10 | package com.mimecast.robin.assertion.client; 11 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/auth/NotRandomTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class NotRandomTest { 8 | 9 | @Test 10 | void generate() { 11 | Random random = new NotRandom("whatever"); 12 | assertEquals("whatever", random.generate(1)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/Random.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | /** 4 | * Digest-MD5 authentication mechanism random generator. 5 | */ 6 | interface Random { 7 | 8 | /** 9 | * Generates random bytes and HEX encodes them 10 | * 11 | * @param size Random bytes size prior to encoding. 12 | * @return Random. 13 | */ 14 | String generate(int size); 15 | } -------------------------------------------------------------------------------- /src/test/resources/cases/config/request/get.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | request: { 5 | url: "https://robin.requestcatcher.com/", 6 | type: "GET", 7 | headers: [ 8 | { 9 | name: "Content-Type", 10 | value: "text/html" 11 | } 12 | ] 13 | }, 14 | 15 | assertions: { 16 | external: [ 17 | // External assertions. 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /src/test/resources/cases/config/request/delete.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | retry: 1, 5 | delay: 2, 6 | 7 | request: { 8 | url: "https://robin.requestcatcher.com/", 9 | type: "DELETE", 10 | headers: [ 11 | { 12 | name: "Content-Type", 13 | value: "text/html" 14 | } 15 | ] 16 | }, 17 | 18 | assertions: { 19 | external: [ 20 | // External assertions. 21 | ] 22 | } 23 | } -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/assertion/client/humio/HumioExternalClientMock.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.assertion.client.humio; 2 | 3 | class HumioExternalClientMock extends HumioExternalClient { 4 | 5 | /** 6 | * Gets new HumioClient. 7 | * 8 | * @return HumioClientMock instance. 9 | */ 10 | @Override 11 | protected HumioClient getClient() { 12 | return new HumioClientMock(connection, config, transactionId); 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/transaction/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * SMTP transactions containers. 3 | * 4 | *

Every SMTP exchange is a transaction that gets recorded in it's place. 5 | *
These can be at session level or envelope level. 6 | *
Session reffers to the overall connection while envelope strictly to each message sent. 7 | *
Envelope SMTP extensions are: MAIL, RCPT, DATA, BDAT (also known as CHUNKING extension). 8 | */ 9 | package com.mimecast.robin.smtp.transaction; 10 | -------------------------------------------------------------------------------- /src/test/resources/cases/config/request/put-json.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | request: { 5 | url: "https://robin.requestcatcher.com/", 6 | type: "PUT", 7 | headers: [ 8 | { 9 | name: "Cache-Control", 10 | value: "no-cache" 11 | } 12 | ], 13 | content: { 14 | payload: "{\"name\": \"Robin\"}", 15 | mimeType: "application/json" 16 | } 17 | }, 18 | 19 | assertions: { 20 | external: [ 21 | // External assertions. 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up JDK 21 17 | uses: actions/setup-java@v4 18 | with: 19 | java-version: '21' 20 | distribution: 'temurin' 21 | cache: maven 22 | - name: Build with Maven 23 | run: mvn -B package --file pom.xml 24 | -------------------------------------------------------------------------------- /src/test/resources/cases/config/request/post-json.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | request: { 5 | url: "https://robin.requestcatcher.com/", 6 | type: "POST", 7 | headers: [ 8 | { 9 | name: "Cache-Control", 10 | value: "no-cache" 11 | } 12 | ], 13 | content: { 14 | payload: "{\"name\": \"Robin\"}", 15 | mimeType: "application/json" 16 | } 17 | }, 18 | 19 | assertions: { 20 | external: [ 21 | // External assertions. 22 | ] 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/Behaviour.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * Client behaviour interface. 9 | */ 10 | public interface Behaviour { 11 | 12 | /** 13 | * Process connection. 14 | * 15 | * @param connection Connection instance. 16 | * @throws IOException Unable to communicate. 17 | */ 18 | void process(Connection connection) throws IOException; 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/auth/SecureRandomTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import org.apache.geronimo.mail.util.Hex; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | class SecureRandomTest { 9 | 10 | @Test 11 | void generate() { 12 | Random random = new SecureRandom(); 13 | String base64 = random.generate(42); 14 | byte[] bytes = Hex.decode(base64.getBytes()); 15 | assertEquals(42, bytes.length); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/storage/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Server Storage. 3 | * 4 | *

Provides an interface and local disk implementation for server incoming email storage. 5 | *

The default LocalStorageClient can be replaced with another implementation via Factories. 6 | *
Ideally this would be done in a plugin. 7 | * 8 | *

Example setting new storage client: 9 | *

10 |  *     Factories.setStorageClient(RemoteStorageClient::new);
11 |  * 
12 | * 13 | *

Read more on plugins here: {@link com.mimecast.robin.annotation} 14 | */ 15 | package com.mimecast.robin.storage; 16 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/assertion/client/logs/LogsExternalClientMock.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.assertion.client.logs; 2 | 3 | import java.nio.file.Paths; 4 | 5 | public class LogsExternalClientMock extends LogsExternalClient { 6 | 7 | /** 8 | * Constructs a new LogsExternalClientMock instance. 9 | */ 10 | public LogsExternalClientMock() { 11 | this.dir = "src/test/resources/"; 12 | this.path = Paths.get(dir, "sample.log").toString(); 13 | } 14 | 15 | @Override 16 | protected void setPath(String fileName) { 17 | // Do nothing 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/annotation/Plugin.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Plugin annotation interface. 7 | * 8 | *

Classes using this annotation should be placed inside the plugin package or will not be loaded. 9 | * 10 | * @see AnnotationLoader 11 | */ 12 | @Documented 13 | @Inherited 14 | @Target(ElementType.TYPE) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface Plugin { 17 | 18 | /** 19 | * Execution priority. 20 | * 21 | * @return Annotation priority if given or default 100. 22 | */ 23 | int priority() default 100; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/verb/EhloVerb.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.verb; 2 | 3 | /** 4 | * EHLO verb. 5 | * 6 | *

This is used for parsing HELO/EHLO commands. 7 | */ 8 | public class EhloVerb extends Verb { 9 | 10 | /** 11 | * Constructs a new EhloVerb instance with given Verb. 12 | * 13 | * @param verb Verb instance. 14 | */ 15 | public EhloVerb(Verb verb) { 16 | super(verb); 17 | } 18 | 19 | /** 20 | * Gets EHLO domain. 21 | * 22 | * @return Domain. 23 | */ 24 | public String getDomain() { 25 | return getCount() > 1 ? getPart(1) : ""; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/resources/mapper.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "/schema/case.schema.json", 3 | 4 | "route": "net", 5 | "timeout": 30000, 6 | 7 | "envelopes": [ 8 | { 9 | "file": "src/test/resources/cases/sources/lipsum.eml", 10 | 11 | "mail": "tony@example.com", 12 | "rcpt": [ 13 | "pepper@example.com", 14 | "happy@example.com" 15 | ], 16 | 17 | "headers": { 18 | "from": "{$mail}", 19 | "to": "{$rcpt}" 20 | } 21 | }, 22 | { 23 | "subject": "lost in space", 24 | "message": "Rescue me", 25 | 26 | "headers": { 27 | "from": "{$blnk}", 28 | "to": "{$blank}" 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration core. 3 | * 4 | *

Provides the configuration foundation and utilities. 5 | * 6 | *

Also provides accessors for components. 7 | * 8 | *

The Log4j2 XML filename can be configured via properties.json or a system property called log4j2. 9 | * Example: 10 | *

java -jar robin.jar --server config/ -Dlog4j2=log4j2custom.xml
11 | * 12 | *

The properties.json filename can be configured via a system property called properties. 13 | * Example: 14 | *

java -jar robin.jar --server config/ -Dproperties=properties-new.json
15 | */ 16 | package com.mimecast.robin.config; 17 | -------------------------------------------------------------------------------- /src/test/resources/cases/config/pangrams.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | retry: 1, 5 | delay: 2, 6 | 7 | envelopes: [ 8 | { 9 | chunkSize: 8192, 10 | 11 | file: "src/test/resources/cases/sources/pangrams.eml", 12 | 13 | assertions: { 14 | protocol: [ 15 | ["MAIL", "250 Sender OK"], 16 | ["RCPT", "250 Recipient OK"], 17 | ["BDAT", "^250"], 18 | ["BDAT", "Received OK"] 19 | ] 20 | } 21 | } 22 | ], 23 | 24 | assertions: { 25 | protocol: [ 26 | [ "SMTP", "^220" ], 27 | [ "EHLO", "STARTTLS" ], 28 | [ "SHLO", "250 HELP" ], 29 | [ "QUIT" ] 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/java/benchmark/Benchmarks.java: -------------------------------------------------------------------------------- 1 | package benchmark; 2 | 3 | import org.openjdk.jmh.runner.Runner; 4 | import org.openjdk.jmh.runner.RunnerException; 5 | import org.openjdk.jmh.runner.options.Options; 6 | import org.openjdk.jmh.runner.options.OptionsBuilder; 7 | 8 | public class Benchmarks { 9 | 10 | public static void main(String[] args) throws RunnerException { 11 | final Options options = new OptionsBuilder() 12 | .include(EmailBuilderBench.class.getSimpleName()) 13 | .include(EmailParserBench.class.getSimpleName()) 14 | .threads(4) 15 | .forks(1) 16 | .build(); 17 | 18 | new Runner(options).run(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/client/LoggingConfig.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.client; 2 | 3 | import com.mimecast.robin.config.ConfigFoundation; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Configuration foundation. 9 | */ 10 | public class LoggingConfig extends ConfigFoundation { 11 | 12 | /** 13 | * Constructs a new MtaUid instance with given map. 14 | * 15 | * @param map Map. 16 | */ 17 | public LoggingConfig(Map map) { 18 | super(map); 19 | } 20 | 21 | /** 22 | * Gets data boolean. 23 | * 24 | * @return Boolean. 25 | */ 26 | public boolean getData() { 27 | return getBooleanProperty("data", true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/ClientQuit.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * QUIT extension processor. 9 | */ 10 | public class ClientQuit extends ClientRset { 11 | 12 | /** 13 | * QUIT processor. 14 | * 15 | * @param connection Connection instance. 16 | * @return Boolean. 17 | * @throws IOException Unable to communicate. 18 | */ 19 | @Override 20 | public boolean process(Connection connection) throws IOException { 21 | verb = "QUIT"; 22 | code = "221"; 23 | return super.process(connection); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/annotation/AnnotationLoaderTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.annotation; 2 | 3 | import com.mimecast.robin.main.Extensions; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | class AnnotationLoaderTest { 13 | 14 | @BeforeAll 15 | static void before() throws ConfigurationException { 16 | Foundation.init("src/test/resources/"); 17 | } 18 | 19 | @Test 20 | void load() { 21 | AnnotationLoader.load(); 22 | assertTrue(Extensions.isExtension("xclient")); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/http/MockHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.http; 2 | 3 | import com.mimecast.robin.config.BasicConfig; 4 | import okhttp3.OkHttpClient; 5 | import okhttp3.Response; 6 | 7 | import javax.net.ssl.SSLSocketFactory; 8 | import javax.net.ssl.X509TrustManager; 9 | 10 | class MockHttpClient extends HttpClient{ 11 | 12 | Response mockResponse; 13 | public MockHttpClient(BasicConfig config, X509TrustManager trustManager, Response mockResponse) { 14 | super(config, trustManager); 15 | this.mockResponse = mockResponse; 16 | } 17 | 18 | @Override 19 | protected OkHttpClient getClient(SSLSocketFactory socketFactory) { 20 | return new MockOkHttpClient(mockResponse); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/cases/config/request/post-files.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | request: { 5 | url: "https://robin.requestcatcher.com/", 6 | type: "POST", 7 | headers: [ 8 | { 9 | name: "Content-Type", 10 | value: "text/html" 11 | } 12 | ], 13 | params: [ 14 | { 15 | name: "name", 16 | value: "Robin" 17 | } 18 | ], 19 | files: [ 20 | { 21 | name: "logo", 22 | value: "src/test/resources/mime/logo.jpg" 23 | }, 24 | { 25 | name: "photo", 26 | value: "src/test/resources/mime/selfie.jpg" 27 | } 28 | ] 29 | }, 30 | 31 | assertions: { 32 | external: [ 33 | // External assertions. 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /src/test/resources/cases/config/request/put-files.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | request: { 5 | url: "https://robin.requestcatcher.com/", 6 | type: "PUT", 7 | headers: [ 8 | { 9 | name: "Content-Type", 10 | value: "text/html" 11 | } 12 | ], 13 | params: [ 14 | { 15 | name: "name", 16 | value: "Robin" 17 | } 18 | ], 19 | files: [ 20 | { 21 | name: "logo", 22 | value: "src/test/resources/mime/logo.jpg" 23 | }, 24 | { 25 | name: "photo", 26 | value: "src/test/resources/mime/selfie.jpg" 27 | } 28 | ] 29 | }, 30 | 31 | assertions: { 32 | external: [ 33 | // External assertions. 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /src/test/resources/cases/config/xclient.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | retry: 1, 5 | delay: 2, 6 | 7 | xclient: { 8 | name: "example.com", 9 | helo: "example.com", 10 | addr: "1.2.3.4" 11 | }, 12 | 13 | envelopes: [ 14 | { 15 | file: "src/test/resources/cases/sources/lipsum.eml", 16 | 17 | assertions: { 18 | protocol: [ 19 | ["MAIL", "250 Sender OK"], 20 | ["RCPT", "250 Recipient OK"], 21 | ["DATA", "^250"], 22 | ["DATA", "Received OK"] 23 | ] 24 | } 25 | } 26 | ], 27 | 28 | assertions: { 29 | protocol: [ 30 | [ "SMTP", "^220" ], 31 | [ "EHLO", "STARTTLS" ], 32 | [ "SHLO", "250 HELP" ], 33 | [ "QUIT" ] 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/SecureRandom.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import org.apache.geronimo.mail.util.Hex; 4 | 5 | /** 6 | * Digest-MD5 authentication mechanism random generator. 7 | * 8 | *

This is the default implementation of Random. 9 | * 10 | * @see DigestMD5 11 | * @see Random 12 | */ 13 | public class SecureRandom implements Random { 14 | 15 | /** 16 | * Generates random bytes and HEX encodes them. 17 | * 18 | * @param size Random bytes size prior to encoding. 19 | * @return Random. 20 | */ 21 | public String generate(int size) { 22 | byte[] bytes = new byte[size]; 23 | new java.security.SecureRandom().nextBytes(bytes); 24 | 25 | return new String(Hex.encode(bytes)); 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/resources/sample.log: -------------------------------------------------------------------------------- 1 | DEBUG|0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||Closing Transmission Channel 2 | INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||Accepted connection from 8.8.8.8:7575 3 | INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||> EHLO example.com 4 | INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||> MAIL FROM: SIZE=294 5 | INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||> RCPT TO: 6 | INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||> DATA 7 | INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||> . 8 | INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||> QUIT 9 | DEBUG|0810-110152743|SmtpThread-30307|smtp.Receipt|rescueMe|||||Closing Transmission Channel -------------------------------------------------------------------------------- /cfg/properties.json5: -------------------------------------------------------------------------------- 1 | { 2 | // Path to MTA logs if one running on local host. 3 | localLogsDir: "/home/robin/log/", 4 | 5 | // Pattern to match UID out of SMT responses. 6 | uidPattern: "\\s\\[([a-z0-9\\-_]+)]", 7 | 8 | // Send RSET command before aditional envelopes. 9 | rsetBetweenEnvelopes: false, 10 | 11 | // Logging config. 12 | logging: { 13 | data: false, // Log email DATA sent and received. 14 | textPartBody: false // Log MIME build case text/* type part contents. 15 | }, 16 | 17 | // HTTP Requests configuration. 18 | request: { 19 | connectTimeout: 20, 20 | writeTimeout: 20, 21 | readTimeout: 90 22 | }, 23 | 24 | // Humio configuration. 25 | humio: { 26 | auth: "YOUR_API_KEY", 27 | url: "https://humio.example.com/", 28 | connectTimeout: 20, 29 | writeTimeout: 20, 30 | readTimeout: 90 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/BasicConfig.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config; 2 | 3 | import java.io.IOException; 4 | import java.util.Map; 5 | 6 | /** 7 | * Config generic implemetation. 8 | */ 9 | public class BasicConfig extends ConfigFoundation { 10 | 11 | /** 12 | * Constructs a new BasicConfig instance with given path. 13 | * 14 | * @param path Configuration pathj. 15 | * @throws IOException Unable to read file. 16 | */ 17 | @SuppressWarnings("rawtypes") 18 | public BasicConfig(String path) throws IOException { 19 | super(path); 20 | } 21 | 22 | /** 23 | * Constructs a new BasicConfig instance with given map. 24 | * 25 | * @param map Configuration map. 26 | */ 27 | @SuppressWarnings("rawtypes") 28 | public BasicConfig(Map map) { 29 | super(map); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/http/HttpMethod.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.http; 2 | 3 | /** 4 | * HTTP/S method. 5 | */ 6 | public enum HttpMethod { 7 | 8 | /** 9 | * DELETE method. 10 | */ 11 | DELETE("DELETE"), 12 | 13 | /** 14 | * POST method. 15 | */ 16 | POST("POST"), 17 | 18 | /** 19 | * PUT method. 20 | */ 21 | PUT("PUT"), 22 | 23 | /** 24 | * GET method. 25 | */ 26 | GET("GET"); 27 | 28 | /** 29 | * Method. 30 | */ 31 | private final String method; 32 | 33 | /** 34 | * @param method String. 35 | */ 36 | HttpMethod(final String method) { 37 | this.method = method; 38 | } 39 | 40 | /** 41 | * @see java.lang.Enum#toString() 42 | */ 43 | @Override 44 | public String toString() { 45 | return method; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/server/ServerRset.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import com.mimecast.robin.smtp.verb.Verb; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * RSET extension processor. 10 | */ 11 | public class ServerRset extends ServerProcessor { 12 | 13 | /** 14 | * RSET processor. 15 | * 16 | * @param connection Connection instance. 17 | * @param verb Verb instance. 18 | * @return Boolean. 19 | * @throws IOException Unable to communicate. 20 | */ 21 | @Override 22 | public boolean process(Connection connection, Verb verb) throws IOException { 23 | super.process(connection, verb); 24 | 25 | connection.write("250 2.1.5 All clear"); 26 | connection.reset(); 27 | 28 | return true; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/mime/HashType.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.mime; 2 | 3 | /** 4 | * Hash type container. 5 | */ 6 | public enum HashType { 7 | 8 | /** 9 | * SHA-256 hash type. 10 | */ 11 | SHA_256("SHA-256"), 12 | 13 | /** 14 | * SHA-1 hash type. 15 | */ 16 | SHA_1("SHA-1"), 17 | 18 | /** 19 | * MD5 hash type. 20 | */ 21 | MD_5("MD5"); 22 | 23 | /** 24 | * Key container. 25 | */ 26 | private final String key; 27 | 28 | /** 29 | * Constructs new hash type with given string. 30 | * 31 | * @param key String name of hash type. 32 | */ 33 | HashType(final String key) { 34 | this.key = key; 35 | } 36 | 37 | /** 38 | * Gets key. 39 | * 40 | * @return String name of hash type. 41 | */ 42 | public String getKey() { 43 | return key; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/server/ServerQuit.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import com.mimecast.robin.smtp.verb.Verb; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * QUIT extension processor. 10 | */ 11 | public class ServerQuit extends ServerProcessor { 12 | 13 | /** 14 | * QUIT processor. 15 | * 16 | * @param connection Connection instance. 17 | * @param verb Verb instance. 18 | * @return Boolean. 19 | * @throws IOException Unable to communicate. 20 | */ 21 | @Override 22 | public boolean process(Connection connection, Verb verb) throws IOException { 23 | super.process(connection, verb); 24 | 25 | connection.write("221 2.0.0 Closing connection"); 26 | connection.close(); 27 | 28 | return false; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/util/CharsetDetector.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import org.mozilla.universalchardet.UniversalDetector; 4 | 5 | /** 6 | * Charset detector. 7 | */ 8 | public class CharsetDetector { 9 | 10 | /** 11 | * Protected constructor. 12 | */ 13 | private CharsetDetector() { 14 | throw new IllegalStateException("Static class"); 15 | } 16 | 17 | /** 18 | * Gets charset. 19 | * 20 | * @param bytes Text to guess as bytes. 21 | * @return Charset name. 22 | */ 23 | public static String getCharset(byte[] bytes) { 24 | UniversalDetector detector = new UniversalDetector(null); 25 | detector.handleData(bytes, 0, bytes.length); 26 | detector.dataEnd(); 27 | 28 | String charset = detector.getDetectedCharset(); 29 | detector.reset(); 30 | 31 | return charset; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/util/Sleep.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | 6 | /** 7 | * Sleep utility. 8 | */ 9 | public class Sleep { 10 | private static final Logger log = LogManager.getLogger(Sleep.class); 11 | 12 | /** 13 | * Protected constructor. 14 | */ 15 | private Sleep() { 16 | throw new IllegalStateException("Static class"); 17 | } 18 | 19 | /** 20 | * Take a nap. 21 | * 22 | * @param delay Time in miliseconds. 23 | */ 24 | public static void nap(int delay) { 25 | try { 26 | log.info("Napping {} miliseconds.", delay); 27 | Thread.sleep(delay); // Take a nap. 28 | } catch (InterruptedException e) { 29 | log.info("Nap interrupted."); 30 | Thread.currentThread().interrupt(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/NotRandom.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | /** 4 | * Digest-MD5 authentication mechanism returning predefined string instead of a random one. 5 | * 6 | *

This is used for verifying challenge responses. 7 | * 8 | * @see DigestMD5 9 | * @see Random 10 | */ 11 | public class NotRandom implements Random { 12 | 13 | /** 14 | * Source container. 15 | */ 16 | private final String source; 17 | 18 | /** 19 | * Constructs a new NotRandom instance with given source. 20 | * 21 | * @param source Source string. 22 | */ 23 | public NotRandom(String source) { 24 | this.source = source; 25 | } 26 | 27 | /** 28 | * Returns constructor given source. 29 | * 30 | * @param size This is ignored. 31 | * @return Given string. 32 | */ 33 | public String generate(int size) { 34 | return source; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/security/PermissiveTrustManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.security; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import javax.naming.ConfigurationException; 8 | import java.security.cert.CertificateException; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | class PermissiveTrustManagerTest { 13 | 14 | @BeforeAll 15 | static void before() throws ConfigurationException { 16 | Foundation.init("src/test/resources/"); 17 | } 18 | 19 | @Test 20 | void use() throws CertificateException { 21 | PermissiveTrustManager tm = new PermissiveTrustManager(); 22 | tm.checkClientTrusted(null, null); 23 | tm.checkServerTrusted(null, null); 24 | assertTrue(tm.isClientTrusted(null)); 25 | assertTrue(tm.isHostTrusted(null)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/BasicConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | class BasicConfigTest { 11 | 12 | @Test 13 | void numbers() { 14 | Map map = new HashMap<>(); 15 | map.put("integer", 7); 16 | map.put("double", 7D); 17 | map.put("short", (short) 7); 18 | map.put("long", 7L); 19 | map.put("string", "7"); 20 | 21 | BasicConfig config = new BasicConfig(map); 22 | assertEquals(7, config.getLongProperty("integer")); 23 | assertEquals(7, config.getLongProperty("double")); 24 | assertEquals(7, config.getLongProperty("short")); 25 | assertEquals(7, config.getLongProperty("long")); 26 | assertEquals(7, config.getLongProperty("string")); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/connection/SmtpException.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.connection; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Smtp exception. 7 | * 8 | *

This is thrown when there was a problem with the SMTP exchange. 9 | */ 10 | public class SmtpException extends IOException { 11 | 12 | /** 13 | * Constructs a new SmtpException instance. 14 | */ 15 | public SmtpException() { 16 | super(); 17 | } 18 | 19 | /** 20 | * Constructs a new SmtpException instance with given Exception. 21 | * 22 | * @param exception Exception instance. 23 | */ 24 | public SmtpException(Exception exception) { 25 | super(exception); 26 | } 27 | 28 | /** 29 | * Constructs a new SmtpException instance with given message. 30 | * 31 | * @param message Message string. 32 | */ 33 | public SmtpException(String message) { 34 | super(message); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/resources/client.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "/schema/client.schema.json", 3 | 4 | "mx": [ 5 | "example.com" 6 | ], 7 | "port": 25, 8 | 9 | "tls": true, 10 | "protocols": [ 11 | "TLSv1.1", "TLSv1.2" 12 | ], 13 | "ciphers": [ 14 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 15 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", 16 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 17 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384" 18 | ], 19 | 20 | "ehlo": "example.com", 21 | "mail": "tony@example.com", 22 | "rcpt": [ 23 | "pepper@example.com" 24 | ], 25 | 26 | "routes": [ 27 | { 28 | "name": "com", 29 | "mx": [ 30 | "example.com" 31 | ], 32 | "port": 25 33 | }, 34 | 35 | { 36 | "name": "net", 37 | "mx": [ 38 | "example.net" 39 | ], 40 | "port": 465, 41 | "auth": true, 42 | "user": "tony@example.com", 43 | "pass": "giveHerTheRing" 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/ClientHelp.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * HELP extension processor. 9 | */ 10 | public class ClientHelp extends ClientProcessor { 11 | 12 | /** 13 | * HELP processor. 14 | * 15 | * @param connection Connection instance. 16 | * @return Boolean. 17 | * @throws IOException Unable to communicate. 18 | */ 19 | @Override 20 | public boolean process(Connection connection) throws IOException { 21 | super.process(connection); 22 | 23 | String write = "HELP"; 24 | connection.write(write); 25 | 26 | String read = connection.read("214"); 27 | connection.getSession().getSessionTransactionList().addTransaction(write, write, read, !read.startsWith("250")); 28 | 29 | return read.startsWith("214"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/ClientProcessor.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * Client extension processor abstract. 11 | */ 12 | public abstract class ClientProcessor { 13 | protected static final Logger log = LogManager.getLogger(ClientProcessor.class); 14 | 15 | /** 16 | * Connection. 17 | */ 18 | protected Connection connection; 19 | 20 | /** 21 | * Blank client processor. 22 | * 23 | * @param connection Connection instance. 24 | * @return Boolean. 25 | * @throws IOException Unable to communicate. 26 | */ 27 | public boolean process(Connection connection) throws IOException { 28 | this.connection = connection; 29 | 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/ConfigLoaderTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config; 2 | 3 | import com.mimecast.robin.main.Config; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.apache.logging.log4j.core.LoggerContext; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | class ConfigLoaderTest { 15 | 16 | @BeforeAll 17 | static void before() throws ConfigurationException { 18 | Foundation.init("src/test/resources/"); 19 | } 20 | 21 | @Test 22 | void load() { 23 | assertNotNull(Config.getServer()); 24 | assertNotNull(Config.getClient()); 25 | assertNotNull(Config.getProperties()); 26 | assertTrue(LoggerContext.getContext().getConfiguration().getName().endsWith("/log4j2.xml")); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cfg/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/assertion/AssertException.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.assertion; 2 | 3 | /** 4 | * Assertion exception. 5 | * 6 | *

Thrown by Assert and AssertExternal if an assertion doesn't match anything. 7 | * 8 | * @see Assert 9 | */ 10 | public class AssertException extends Exception { 11 | 12 | /** 13 | * Constructs a new AssertException instance without message. 14 | */ 15 | public AssertException() { 16 | super(); 17 | } 18 | 19 | /** 20 | * Constructs a new AssertException instance with message. 21 | * 22 | * @param message Message string. 23 | */ 24 | public AssertException(String message) { 25 | super(Thread.currentThread().getName() + " - " + message); 26 | } 27 | 28 | /** 29 | * Constructs a new AssertException instance with given Throwable. 30 | * 31 | * @param cause Throwable cause. 32 | */ 33 | public AssertException(Throwable cause) { 34 | super(cause); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/server/UserConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.server; 2 | 3 | import com.mimecast.robin.main.Config; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | @SuppressWarnings("OptionalGetWithoutIsPresent") 13 | class UserConfigTest { 14 | 15 | private static UserConfig userConfig; 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | 21 | userConfig = Config.getServer().getUser("tony@example.com").get(); 22 | } 23 | 24 | @Test 25 | void getName() { 26 | assertEquals("tony@example.com", userConfig.getName()); 27 | } 28 | 29 | @Test 30 | void getPass() { 31 | assertEquals("giveHerTheRing", userConfig.getPass()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/util/RandomTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import javax.naming.ConfigurationException; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | class RandomTest { 13 | 14 | @BeforeAll 15 | static void before() throws ConfigurationException { 16 | Foundation.init("src/test/resources/"); 17 | } 18 | 19 | @Test 20 | void chFixedLength() { 21 | assertEquals(20, Random.ch().length()); 22 | } 23 | 24 | @Test 25 | void chVariableLength() { 26 | assertEquals(99, Random.ch(99).length()); 27 | } 28 | 29 | @Test 30 | void noFixedLength() { 31 | assertTrue(Random.no() <= 10); 32 | } 33 | 34 | @Test 35 | void noVariableLength() { 36 | assertTrue(Random.no(20) <= 20); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/client/ClientConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.client; 2 | 3 | import com.mimecast.robin.main.Config; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class ClientConfigTest { 13 | 14 | @BeforeAll 15 | static void before() throws ConfigurationException { 16 | Foundation.init("src/test/resources/"); 17 | } 18 | 19 | @Test 20 | void getMail() { 21 | assertEquals("tony@example.com", Config.getClient().getMail()); 22 | } 23 | 24 | @Test 25 | void getRcpt() { 26 | assertTrue(String.join(", ", Config.getClient().getRcpt()).contains("pepper@example.com")); 27 | } 28 | 29 | @Test 30 | void getRoute() { 31 | // Tested in RouteConfigTest 32 | assertNotNull( Config.getClient().getRoute("com")); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/benchmark/EmailBuilderBench.java: -------------------------------------------------------------------------------- 1 | package benchmark; 2 | 3 | import com.mimecast.robin.mime.EmailBuilder; 4 | import com.mimecast.robin.smtp.MessageEnvelope; 5 | import com.mimecast.robin.smtp.session.Session; 6 | import org.openjdk.jmh.annotations.*; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.IOException; 10 | 11 | @State(Scope.Benchmark) 12 | public class EmailBuilderBench { 13 | 14 | final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 15 | 16 | @Setup(Level.Iteration) 17 | public void setup() { 18 | outputStream.reset(); 19 | } 20 | 21 | @TearDown(Level.Trial) 22 | public void tearDown() throws IOException { 23 | outputStream.close(); 24 | } 25 | 26 | @Benchmark 27 | @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime}) 28 | public void defaultHeaders() throws IOException { 29 | EmailBuilder emailBuilder = new EmailBuilder(new Session(), new MessageEnvelope()) 30 | .writeTo(outputStream); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/resources/lipsum.plain.eml: -------------------------------------------------------------------------------- 1 | MIME-Version: 1.0 2 | From: <{$MAILFROM}> 3 | To: <{$RCPTTO}> 4 | Date: {$DATE} 5 | Message-ID: <{$MSGID}{$MAILFROM}> 6 | Subject: Lipsum 7 | Content-Type: text/plain; charset=UTF-8 8 | Content-Transfer-Encoding: 8bit 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 11 | Aliquam in quam id orci finibus luctus. 12 | Integer at finibus orci. 13 | Vestibulum nunc massa, porttitor vel justo vitae, dictum fermentum augue. 14 | Integer ut lorem consectetur, feugiat lectus quis, mattis sem. 15 | Phasellus quis eros eget felis ornare accumsan. Suspendisse potenti. 16 | Proin ornare vestibulum purus, a pharetra enim tempor at. 17 | Vestibulum a augue rutrum purus bibendum rutrum. 18 | Aliquam nec lacus dui. 19 | Sed et nisi vel sem tristique finibus sit amet nec tortor. 20 | Ut finibus, eros id mollis maximus, felis ante imperdiet mauris, sit amet vehicula justo lectus id ipsum. 21 | Donec vitae dapibus lacus. 22 | Curabitur eu orci iaculis, pretium augue aliquam, vestibulum nulla. 23 | Sed ultricies justo nec justo tempor viverra. 24 | -------------------------------------------------------------------------------- /src/test/resources/case.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | route: "net", 5 | timeout: 30, 6 | 7 | xclient: { 8 | name: "example.com", 9 | helo: "example.net", 10 | addr: "127.0.0.10" 11 | }, 12 | 13 | auth: true, 14 | authBeforeTls: true, 15 | user: "tony@example.com", 16 | pass: "giveHerTheRing", 17 | 18 | envelopes: [ 19 | { 20 | chunkSize: 2048, 21 | chunkBdat: true, 22 | chunkWrite: true, 23 | 24 | file: "src/test/resources/lipsum.eml", 25 | folder: "src/test/resources/", 26 | 27 | mail: "tony@example.com", 28 | rcpt: [ 29 | "pepper@example.com", 30 | "happy@example.com" 31 | ], 32 | headers: { 33 | from: "{$mail}", 34 | to: ["{$rcpt}"] 35 | } 36 | }, 37 | { 38 | subject: "Lost in space", 39 | message: "Rescue me!", 40 | 41 | mail: "", 42 | rcpt: [ 43 | "journalling@example.com" 44 | ], 45 | headers: { 46 | from: "tony@example.com", 47 | to: ["pepper@example.com"] 48 | } 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/server/ServerHelp.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Extensions; 4 | import com.mimecast.robin.smtp.connection.Connection; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * HELP extension processor. 11 | */ 12 | public class ServerHelp extends ServerProcessor { 13 | 14 | /** 15 | * HELP advert. 16 | * 17 | * @return Advert string. 18 | */ 19 | @Override 20 | public String getAdvert() { 21 | return "HELP"; 22 | } 23 | 24 | /** 25 | * HELP processor. 26 | * 27 | * @param connection Connection instance. 28 | * @param verb Verb instance. 29 | * @return Boolean. 30 | * @throws IOException Unable to communicate. 31 | */ 32 | @Override 33 | public boolean process(Connection connection, Verb verb) throws IOException { 34 | super.process(connection, verb); 35 | 36 | connection.write("214 " + Extensions.getHelp()); 37 | 38 | return true; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/ClientRset.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * RSET extension processor. 9 | */ 10 | public class ClientRset extends ClientProcessor { 11 | 12 | String verb = "RSET"; 13 | String code = "250"; 14 | 15 | /** 16 | * RSET processor. 17 | * 18 | * @param connection Connection instance. 19 | * @return Boolean. 20 | * @throws IOException Unable to communicate. 21 | */ 22 | @Override 23 | public boolean process(Connection connection) throws IOException { 24 | super.process(connection); 25 | 26 | try { 27 | connection.write(verb); 28 | 29 | String read = connection.read(code); 30 | connection.getSession().getSessionTransactionList().addTransaction(verb, verb, read); 31 | } catch (IOException e) { 32 | log.info("Error reading/writing for {}: {}", verb, e.getMessage()); 33 | } 34 | 35 | return true; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/session/XclientSessionTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.session; 2 | 3 | import com.mimecast.robin.config.client.CaseConfig; 4 | import com.mimecast.robin.main.Factories; 5 | import com.mimecast.robin.main.Foundation; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | class XclientSessionTest { 15 | 16 | @BeforeAll 17 | static void before() throws ConfigurationException { 18 | Foundation.init("src/test/resources/"); 19 | } 20 | 21 | @Test 22 | void map() throws IOException { 23 | XclientSession session = (XclientSession) Factories.getSession(); 24 | session.map(new CaseConfig("src/test/resources/case.json5")); 25 | 26 | assertEquals("example.com", session.getXclient().get("name")); 27 | assertEquals("example.net", session.getXclient().get("helo")); 28 | assertEquals("127.0.0.10", session.getXclient().get("addr")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/auth/PlainTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.Connection; 5 | import com.mimecast.robin.smtp.session.Session; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | class PlainTest { 14 | 15 | @BeforeAll 16 | static void before() throws ConfigurationException { 17 | Foundation.init("src/test/resources/"); 18 | } 19 | 20 | @Test 21 | void getResponse() { 22 | Session session = new Session(); 23 | session.setUsername("tony@example.com"); 24 | session.setUsername("giveHerTheRing"); 25 | 26 | Plain plain = new Plain(new Connection(session)); 27 | Plain plainNoSession = new Plain(new Connection(new Session())); 28 | assertEquals("Z2l2ZUhlclRoZVJpbmcAZ2l2ZUhlclRoZVJpbmcA", plain.getLogin()); 29 | assertEquals("AAA=", plainNoSession.getLogin()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/server/ServerXclient.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import com.mimecast.robin.smtp.verb.Verb; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * XCLIENT extension processor. 10 | * 11 | * @see Postfix XCLIENT 12 | */ 13 | public class ServerXclient extends ServerProcessor { 14 | 15 | /** 16 | * XCLIENT processor. 17 | * 18 | * @return Boolean. 19 | * @throws IOException Unable to communicate. 20 | */ 21 | @Override 22 | public boolean process(Connection connection, Verb verb) throws IOException { 23 | super.process(connection, verb); 24 | 25 | connection.getSession().setEhlo(verb.getParam("helo")); 26 | connection.getSession().setFriendRdns(verb.getParam("name")); 27 | connection.getSession().setFriendAddr(verb.getParam("addr")); 28 | connection.write("220 " + connection.getSession().getRdns() + " ESMTP; " + connection.getSession().getDate()); 29 | 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/assertion/external/logs/LogsExternalClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.assertion.external.logs; 2 | 3 | import com.mimecast.robin.config.assertion.external.MatchExternalClientConfig; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Logs assertions config. 9 | */ 10 | public final class LogsExternalClientConfig extends MatchExternalClientConfig { 11 | 12 | /** 13 | * Constructs a new LogsExternalClientConfig instance. 14 | * 15 | * @param map Configuration map. 16 | */ 17 | @SuppressWarnings("rawtypes") 18 | public LogsExternalClientConfig(Map map) { 19 | super(map); 20 | } 21 | 22 | /** 23 | * Get log file name precedence. 24 | * 25 | * @return String. 26 | */ 27 | public String getLogPrecedence() { 28 | return hasProperty("logPrecedence") ? getStringProperty("logPrecedence") : ""; 29 | } 30 | 31 | /** 32 | * Get service name. 33 | * 34 | * @return String. 35 | */ 36 | public String getService() { 37 | return hasProperty("serviceName") ? getStringProperty("serviceName") : "mta"; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/annotation/plugin/RequestPlugin.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.annotation.plugin; 2 | 3 | import com.mimecast.robin.annotation.Plugin; 4 | import com.mimecast.robin.assertion.client.request.RequestExternalClient; 5 | import com.mimecast.robin.main.Factories; 6 | 7 | /** 8 | * Request plugin. 9 | * 10 | *

This is a basic HTTP request client.

11 | *

Example configuration:

12 | *
13 |  * {
14 |  *   "type": "request",
15 |  *   "wait": 5,
16 |  *   "delay": 5,
17 |  *   "retry": 3,
18 |  *   url: "https://robin.requestcatcher.com/",
19 |  *   headers: {
20 |  *     "Content-Type": "application/json",
21 |  *     "Cache-Control": "no-cache",
22 |  *     "Authorization": "Basic {$requestAuth}"
23 |  *   },
24 |  *   contentType: "application/json",
25 |  *   file: "src/test/resources/cases/config/request/example.json5"
26 |  * }
27 |  * 
28 | */ 29 | @SuppressWarnings("WeakerAccess") 30 | @Plugin(priority = 103) 31 | public final class RequestPlugin { 32 | 33 | /** 34 | * Constructs a new RequestPlugin instance. 35 | */ 36 | public RequestPlugin() { 37 | Factories.putExternalClient("request", RequestExternalClient::new); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/client/ClientQuitTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | import java.io.IOException; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | class ClientQuitTest { 15 | 16 | @BeforeAll 17 | static void before() throws ConfigurationException { 18 | Foundation.init("src/test/resources/"); 19 | } 20 | 21 | @Test 22 | void process() throws IOException { 23 | StringBuilder stringBuilder = new StringBuilder(); 24 | stringBuilder.append("250 OK\r\n"); 25 | ConnectionMock connection = new ConnectionMock(stringBuilder); 26 | 27 | ClientQuit quit = new ClientQuit(); 28 | boolean process = quit.process(connection); 29 | 30 | assertTrue(process); 31 | 32 | connection.parseLines(); 33 | assertEquals("QUIT\r\n", connection.getLine(1)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/client/ClientRsetTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | import java.io.IOException; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | class ClientRsetTest { 15 | 16 | @BeforeAll 17 | static void before() throws ConfigurationException { 18 | Foundation.init("src/test/resources/"); 19 | } 20 | 21 | @Test 22 | void process() throws IOException { 23 | StringBuilder stringBuilder = new StringBuilder(); 24 | stringBuilder.append("250 OK\r\n"); 25 | ConnectionMock connection = new ConnectionMock(stringBuilder); 26 | 27 | ClientRset rset = new ClientRset(); 28 | boolean process = rset.process(connection); 29 | 30 | assertTrue(process); 31 | 32 | connection.parseLines(); 33 | assertEquals("RSET\r\n", connection.getLine(1)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/server/UserConfig.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.server; 2 | 3 | import com.mimecast.robin.config.ConfigFoundation; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Server user configuration container. 9 | * 10 | *

This is a container for users defined in the server configuration. 11 | *

One instance will be made for every user defined. 12 | *

This can be used to authenticate users when testing clients. 13 | *

The server supports AUTH PLAIN LOGIN. 14 | * 15 | * @see ServerConfig 16 | */ 17 | public class UserConfig extends ConfigFoundation { 18 | 19 | /** 20 | * Constructs a new UserConfig instance with given map. 21 | * 22 | * @param map Properties map. 23 | */ 24 | @SuppressWarnings("rawtypes") 25 | public UserConfig(Map map) { 26 | super(map); 27 | } 28 | 29 | /** 30 | * Gets username. 31 | * 32 | * @return Username string. 33 | */ 34 | public String getName() { 35 | return getStringProperty("name"); 36 | } 37 | 38 | /** 39 | * Gets password. 40 | * 41 | * @return Password string. 42 | */ 43 | public String getPass() { 44 | return getStringProperty("pass"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/annotation/plugin/HumioPlugin.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.annotation.plugin; 2 | 3 | import com.mimecast.robin.annotation.Plugin; 4 | import com.mimecast.robin.assertion.client.humio.HumioExternalClient; 5 | import com.mimecast.robin.main.Factories; 6 | 7 | /** 8 | * Humio plugin. 9 | * 10 | *

Humio is a platform for consuming and monitoring event data ingested from logs. 11 | * The software is designed to be used by system administrators and DevOps to monitor large 12 | * quantities of data from logs, derive the individual events within those logs, and parse the 13 | * content in order to extract key data points from the source. The extracted and identified data 14 | * can be used to build graphs, identify anomalies, and create alerts to monitor your systems.

15 | * 16 | * @see Humio Getting Started 17 | */ 18 | @SuppressWarnings("WeakerAccess") 19 | @Plugin(priority = 102) 20 | public final class HumioPlugin { 21 | 22 | /** 23 | * Constructs a new HumioPlugin instance. 24 | *

Sets extensions, behaviour and logs client. 25 | */ 26 | public HumioPlugin() { 27 | Factories.putExternalClient("humio", HumioExternalClient::new); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/server/ServerRsetTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | class ServerRsetTest { 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | } 21 | 22 | @Test 23 | void process() throws IOException { 24 | StringBuilder stringBuilder = new StringBuilder(); 25 | ConnectionMock connection = new ConnectionMock(stringBuilder); 26 | 27 | Verb verb = new Verb("RSET"); 28 | 29 | ServerRset rset = new ServerRset(); 30 | boolean process = rset.process(connection, verb); 31 | 32 | assertTrue(process); 33 | 34 | connection.parseLines(); 35 | assertEquals("250 2.1.5 All clear\r\n", connection.getLine(1)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/MainMock.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Main runnable. 8 | */ 9 | public final class MainMock extends Main { 10 | 11 | /** 12 | * Logs list. 13 | */ 14 | private List logs; 15 | 16 | /** 17 | * Main runnable. 18 | * 19 | * @param argv List of String. 20 | */ 21 | public static List main(List argv) { 22 | MainMock main = new MainMock(argv.toArray(new String[0])); 23 | return main.getLogs(); 24 | } 25 | 26 | /** 27 | * Constructs a new Main instance. 28 | * 29 | * @param args String array. 30 | */ 31 | private MainMock(String[] args) { 32 | super(args); 33 | } 34 | 35 | /** 36 | * Logging wrapper. 37 | * 38 | * @param string String. 39 | */ 40 | @Override 41 | public void log(String string) { 42 | super.log(string); 43 | if (logs == null) { 44 | logs = new ArrayList<>(); 45 | } 46 | logs.add(string); 47 | } 48 | 49 | /** 50 | * Gets logs. 51 | * 52 | * @return List of String. 53 | */ 54 | private List getLogs() { 55 | return logs; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/server/ServerQuitTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertFalse; 14 | 15 | class ServerQuitTest { 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | } 21 | 22 | @Test 23 | void process() throws IOException { 24 | StringBuilder stringBuilder = new StringBuilder(); 25 | ConnectionMock connection = new ConnectionMock(stringBuilder); 26 | 27 | Verb verb = new Verb("QUIT"); 28 | 29 | ServerQuit quit = new ServerQuit(); 30 | boolean process = quit.process(connection, verb); 31 | 32 | assertFalse(process); 33 | 34 | connection.parseLines(); 35 | assertEquals("221 2.0.0 Closing connection\r\n", connection.getLine(1)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/verb/BdatVerb.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.verb; 2 | 3 | /** 4 | * BDAT verb. 5 | * 6 | *

This is used for parsing BDAT commands for CHUNKING implementation. 7 | * 8 | * @see RFC 3030 9 | */ 10 | public class BdatVerb extends Verb { 11 | 12 | /** 13 | * BDAT size. 14 | */ 15 | private int size = 0; 16 | 17 | /** 18 | * BDAT LAST. 19 | */ 20 | private boolean last = false; 21 | 22 | /** 23 | * Constructs a new BdatVerb instance with given Verb. 24 | * 25 | * @param verb Verb instance. 26 | */ 27 | public BdatVerb(Verb verb) { 28 | super(verb); 29 | } 30 | 31 | /** 32 | * Gets BDAT size. 33 | * 34 | * @return Size. 35 | */ 36 | public int getSize() { 37 | if (size == 0) { 38 | size = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; 39 | } 40 | 41 | return size; 42 | } 43 | 44 | /** 45 | * Is BDAT last. 46 | * 47 | * @return True if found. 48 | */ 49 | public boolean isLast() { 50 | if (!last) { 51 | last = parts.length > 2 && parts[2].equalsIgnoreCase("last"); 52 | } 53 | 54 | return last; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/InstanceDigestCache.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Digest-MD5 authentication mechanism database. 8 | * 9 | *

Designed for testing purposes only. 10 | */ 11 | public class InstanceDigestCache extends DigestCache { 12 | 13 | /** 14 | * Instance Deque cache. 15 | */ 16 | private final LinkedHashMap> map = new LinkedHashMap>() { 17 | @Override 18 | protected boolean removeEldestEntry(Map.Entry> eldest) { 19 | return this.size() > 100; // Limit. 20 | } 21 | }; 22 | 23 | /** 24 | * Adds a DigestData instance. 25 | * 26 | * @param token Lookup token string. 27 | * @param data DigestData instance. 28 | */ 29 | @Override 30 | void add(String token, DigestData data) { 31 | map.put(token, data.getMap()); 32 | } 33 | 34 | /** 35 | * Lookup DigestData in cache by token. 36 | * 37 | * @param token Token string. 38 | * @return DigestData instance. 39 | */ 40 | @Override 41 | Map lookup(String token) { 42 | return map.get(token); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/storage/StorageClient.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.storage; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | 5 | import java.io.FileNotFoundException; 6 | import java.io.OutputStream; 7 | 8 | /** 9 | * Server file storage interface. 10 | * 11 | *

The instanciation of this will be done via Factories. 12 | *

Connection is required to allow customisation based on sender/recipient. 13 | */ 14 | public interface StorageClient { 15 | 16 | /** 17 | * Sets connection. 18 | * 19 | * @param connection Connection instance. 20 | * @return Self. 21 | */ 22 | StorageClient setConnection(Connection connection); 23 | 24 | /** 25 | * Sets extension. 26 | * 27 | * @param extension File extension. 28 | * @return Self. 29 | */ 30 | StorageClient setExtension(String extension); 31 | 32 | /** 33 | * Gets file output stream. 34 | * 35 | * @return OutputStream instance. 36 | * @throws FileNotFoundException File not found. 37 | */ 38 | OutputStream getStream() throws FileNotFoundException; 39 | 40 | /** 41 | * Gets file token. 42 | * 43 | * @return String. 44 | */ 45 | String getToken(); 46 | 47 | /** 48 | * Saves file. 49 | */ 50 | void save(); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/annotation/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugin core. 3 | * 4 | *

Provides an annotation interface for plugin loading. 5 | * 6 | *

Plugins can be used to overide primary components to add functionality. 7 | *
A plugin is provided that provides SMTP XCLIENT support like in Postfix. 8 | * 9 | *

New plugins may be added in the package com.mimecast.robin.annotation.plugin. 10 | *
Plugins are loaded in order of priority thus any plugin depending on another must have a higher priority. 11 | *
Plugins of the same priority are loaded in random order. 12 | * 13 | *

Example plugin with custom session, storage client, behaviour and server and client extension: 14 | *

15 |  *     @Plugin(priority = 102)
16 |  *     public class CustomPlugin {
17 |  *
18 |  *         public CustomPlugin() {
19 |  *             Factories.setSession(CustomSession::new);
20 |  *             Factories.setStorageClient(CustomStorageClient::new);
21 |  *             Factories.setBehaviour(CustomBehaviour::new);
22 |  *             Extensions.addExtension("custom", new Extension(ServerCustom::new, ClientCustom::new));
23 |  *         }
24 |  *     }
25 |  * 
26 | * 27 | * @see Postfix XCLIENT 28 | */ 29 | package com.mimecast.robin.annotation; 30 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/server/ServerStartTlsTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertThrows; 14 | 15 | class ServerStartTlsTest { 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | } 21 | 22 | @Test 23 | void getAdvert() { 24 | ServerStartTls tls = new ServerStartTls(); 25 | assertEquals("STARTTLS", tls.getAdvert()); 26 | } 27 | 28 | @Test 29 | void process() { 30 | StringBuilder stringBuilder = new StringBuilder(); 31 | ConnectionMock connection = new ConnectionMock(stringBuilder); 32 | 33 | Verb verb = new Verb("STARTTLS"); 34 | 35 | // Testing exception as the flow is covered by ClientStartTlsTest.java. 36 | assertThrows(IOException.class, () -> new ServerStartTls().process(connection, verb)); 37 | } 38 | } -------------------------------------------------------------------------------- /doc/cli.md: -------------------------------------------------------------------------------- 1 | Command line usage 2 | ================== 3 | 4 | java -jar robin.jar 5 | MTA development, debug and testing tool 6 | 7 | usage: 8 | --client Run as client 9 | --server Run as server 10 | 11 | Client 12 | ------ 13 | 14 | java -jar robin.jar --client 15 | Email delivery client 16 | 17 | usage: 18 | -c,--conf Path to configuration dir (Default: cfg/) 19 | -f,--file EML file to send 20 | -h,--help Show usage help 21 | -j,--gson Path to case file JSON 22 | -m,--mail MAIL FROM address 23 | -p,--port Port to connect to 24 | -r,--rcpt RCPT TO address 25 | -x,--mx Server to connect to 26 | 27 | Server 28 | ------ 29 | 30 | java -jar robin.jar --server 31 | Debug MTA server 32 | 33 | usage: 34 | Path to configuration directory 35 | 36 | example: 37 | java -jar robin.jar --server cfg/ 38 | 39 | Common 40 | ------ 41 | 42 | The Log4j2 XML filename can be configured via properties.json5 or a system property called `log4j2`. 43 | 44 | example: 45 | java -jar robin.jar --server cfg/ -Dlog4j2=log4j2custom.xml 46 | 47 | The properties.json5 filename can be configured via a system property called `properties`. 48 | 49 | example: 50 | java -jar robin.jar --server cfg/ -Dproperties=properties-new.json5 51 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/XclientBehaviour.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import com.mimecast.robin.smtp.session.XclientSession; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * Client behaviour with XCLIENT support. 10 | * 11 | * @see Postfix XCLIENT 12 | */ 13 | public class XclientBehaviour extends DefaultBehaviour { 14 | 15 | /** 16 | * Executes delivery. 17 | */ 18 | @Override 19 | public void process(Connection connection) throws IOException { 20 | this.connection = connection; 21 | 22 | if (!ehlo()) return; 23 | if (!startTls()) return; 24 | 25 | // XCLIENT 26 | if (connection.getSession() instanceof XclientSession) { 27 | XclientSession session = (XclientSession) connection.getSession(); 28 | if (session.getXclient() != null && session.getXclient().size() > 0) { 29 | if (!process("xclient", connection)) return; 30 | 31 | // Post XCLIENT hello. 32 | if (!ehlo()) return; 33 | } 34 | } 35 | 36 | if (!auth()) return; 37 | if (!connection.getSession().getEnvelopes().isEmpty()) { 38 | data(); 39 | } 40 | quit(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/auth/DigestDataTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | class DigestDataTest { 8 | 9 | @Test 10 | void all() { 11 | DigestData digestData = new DigestData(); 12 | 13 | digestData.setHost("example.com"); 14 | digestData.setUsername("tony@example.com"); 15 | digestData.setRealm("example.net"); 16 | 17 | String nonce = new SecureRandom().generate(16); 18 | digestData.setNonce(nonce); 19 | 20 | String cnonce = new SecureRandom().generate(32); 21 | digestData.setCnonce(cnonce); 22 | 23 | assertEquals("example.com", digestData.getHost()); 24 | assertEquals("tony@example.com", digestData.getUsername()); 25 | assertEquals("example.net", digestData.getRealm()); 26 | assertEquals(nonce, digestData.getNonce()); 27 | assertEquals(cnonce, digestData.getCnonce()); 28 | } 29 | 30 | @Test 31 | void none() { 32 | DigestData digestData = new DigestData(); 33 | assertEquals("", digestData.getHost()); 34 | assertEquals("", digestData.getUsername()); 35 | assertEquals("", digestData.getRealm()); 36 | assertEquals("", digestData.getNonce()); 37 | assertEquals("", digestData.getCnonce()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/server/ServerRcptTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | class ServerRcptTest { 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | } 21 | 22 | @Test 23 | void processWithScenario() throws IOException { 24 | StringBuilder stringBuilder = new StringBuilder(); 25 | ConnectionMock connection = new ConnectionMock(stringBuilder); 26 | 27 | Verb verb = new Verb("RCPT TO: "); 28 | 29 | ServerRcpt rcpt = new ServerRcpt(); 30 | boolean process = rcpt.process(connection, verb); 31 | 32 | assertTrue(process); 33 | assertEquals("friday-123@example.com", rcpt.getAddress().getAddress()); 34 | assertEquals("friday-123@example.com", connection.getSession().getRcpts().get(0).getAddress()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /doc/introduction.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | Before you start the server or run the example clients you should start by checking the config files 4 | and make any required changes according to your environment. 5 | 6 | 7 | Robin client 8 | ------------ 9 | Is designed to use minimal config and as such it loads defaults from [client.json5](../cfg/client.json5). 10 | You may want to adjust the following values: mx, port, ehlo, main and rcpt. 11 | Additionaly you may want to configure your own routes for even more streamlined case config. 12 | 13 | - See ExampleSend.java for examples on how to craft JSON5 cases to send emails. 14 | - Read [case.md](case.md), [magic.md](magic.md) and [mime.md](mime.md). 15 | - See ExampleHttp.java for examples on how to craft JSON5 cases to do HTTP requests. 16 | - Read [request.md](request.md). 17 | - See ExampleProgramatic.java for examples on how to use it as a Java library. 18 | - Read [client.md](client.md) and to extend its capabilities [plugin.md](plugin.md). 19 | - Read [cli.md](cli.md) for commandline usage. 20 | 21 | 22 | Robin server 23 | ------------ 24 | It might need some configuring as well in [server.json5](../cfg/server.json5) in order to be able to load a keystore. One is provided in test resources. 25 | Additionaly you may want to update the email store path. 26 | 27 | - Read [cli.md](cli.md) for commandline usage. 28 | 29 | 30 | _Use it wisely!_ 31 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/server/ServerProcessor.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import com.mimecast.robin.smtp.verb.Verb; 5 | import org.apache.logging.log4j.LogManager; 6 | import org.apache.logging.log4j.Logger; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * Server extension processor abstract. 12 | */ 13 | public abstract class ServerProcessor { 14 | protected static final Logger log = LogManager.getLogger(ServerProcessor.class); 15 | 16 | /** 17 | * Connection instance. 18 | */ 19 | protected Connection connection; 20 | 21 | /** 22 | * Verb instance. 23 | */ 24 | protected Verb verb; 25 | 26 | /** 27 | * Advert getter. 28 | * 29 | * @return Advert string. 30 | */ 31 | @SuppressWarnings("squid:S3400") 32 | protected String getAdvert() { 33 | return ""; 34 | } 35 | 36 | /** 37 | * ClientProcessor. 38 | * 39 | * @param connection Connection instance. 40 | * @param verb Verb instance. 41 | * @return Boolean. 42 | * @throws IOException Unable to communicate. 43 | */ 44 | public boolean process(Connection connection, Verb verb) throws IOException { 45 | this.connection = connection; 46 | this.verb = verb; 47 | 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/assertion/external/ExternalConfig.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.assertion.external; 2 | 3 | import com.mimecast.robin.config.BasicConfig; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * External assertions config. 9 | * 10 | *

This may be present at envelope level only. 11 | */ 12 | public class ExternalConfig extends BasicConfig { 13 | 14 | /** 15 | * Constructs a new ExternalConfig instance. 16 | * 17 | * @param map Configuration map. 18 | */ 19 | @SuppressWarnings("rawtypes") 20 | public ExternalConfig(Map map) { 21 | super(map); 22 | } 23 | 24 | /** 25 | * Gets initial wait in seconds. 26 | * 27 | * @return Initial wait. 28 | */ 29 | public int getWait() { 30 | int wait = Math.toIntExact(getLongProperty("wait")); 31 | return Math.max(wait, 0); 32 | } 33 | 34 | /** 35 | * Gets retry delay in seconds. 36 | * 37 | * @return Retry delay. 38 | */ 39 | public int getDelay() { 40 | int delay = Math.toIntExact(getLongProperty("delay")); 41 | return Math.max(delay, 0); 42 | } 43 | 44 | /** 45 | * Gets retry count. 46 | * 47 | * @return Retry count. 48 | */ 49 | public int getRetry() { 50 | int retry = Math.toIntExact(getLongProperty("retry", 1L)); 51 | return retry > 0 ? retry : 1; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/StaticDigestCache.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import java.util.LinkedHashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Digest-MD5 authentication mechanism database. 8 | * 9 | *

It would be wise to implement limitations in both size and time. 10 | *

While the size should be based on hardware TTL should be at most an hour. 11 | */ 12 | public class StaticDigestCache extends DigestCache { 13 | 14 | /** 15 | * Static Deque cache. 16 | */ 17 | private static final LinkedHashMap> map = new LinkedHashMap>() { 18 | @Override 19 | protected boolean removeEldestEntry(Map.Entry> eldest) { 20 | return this.size() > 100; // Limit, 21 | } 22 | }; 23 | 24 | /** 25 | * Adds a DigestData instance. 26 | * 27 | * @param token Lookup token string. 28 | * @param data DigestData instance. 29 | */ 30 | @Override 31 | void add(String token, DigestData data) { 32 | map.put(token, data.getMap()); 33 | } 34 | 35 | /** 36 | * Lookup DigestData in cache by token. 37 | * 38 | * @param token Token string. 39 | * @return DigestData instance. 40 | */ 41 | @Override 42 | Map lookup(String token) { 43 | return map.get(token); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/auth/LoginTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.Connection; 5 | import com.mimecast.robin.smtp.session.Session; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | class LoginTest { 14 | 15 | private static Login authPlain; 16 | private static Login authPlainNoSession; 17 | 18 | @BeforeAll 19 | static void before() throws ConfigurationException { 20 | Foundation.init("src/test/resources/"); 21 | 22 | Session session = new Session(); 23 | session.setUsername("tony@example.com"); 24 | session.setPassword("giveHerTheRing"); 25 | 26 | authPlain = new Login(new Connection(session)); 27 | authPlainNoSession = new Login(new Connection(new Session())); 28 | } 29 | 30 | @Test 31 | void getUsername() { 32 | assertEquals("dG9ueUBleGFtcGxlLmNvbQ==", authPlain.getUsername()); 33 | assertEquals("", authPlainNoSession.getUsername()); 34 | } 35 | 36 | @Test 37 | void getPassword() { 38 | assertEquals("Z2l2ZUhlclRoZVJpbmc=", authPlain.getPassword()); 39 | assertEquals("", authPlainNoSession.getPassword()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/transaction/SessionTransactionList.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.transaction; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Session transaction list. 8 | * 9 | *

This provides the implementation for session transactions. 10 | * 11 | * @see TransactionList 12 | */ 13 | public class SessionTransactionList extends TransactionList { 14 | 15 | /** 16 | * Gets last SMTP transaction of defined verb. 17 | * 18 | * @param verb Verb string. 19 | * @return Transaction instance. 20 | */ 21 | public Transaction getLast(String verb) { 22 | return !getTransactions(verb).isEmpty() ? getTransactions(verb).get((getTransactions(verb).size() - 1)) : null; 23 | } 24 | 25 | /** 26 | * Session envelopes. 27 | */ 28 | private final List envelopes = new ArrayList<>(); 29 | 30 | /** 31 | * Adds envelope to list. 32 | * 33 | * @param envelopeTransactionList EnvelopeTransactionList instance. 34 | */ 35 | public void addEnvelope(EnvelopeTransactionList envelopeTransactionList) { 36 | envelopes.add(envelopeTransactionList); 37 | } 38 | 39 | /** 40 | * Gets envelopes. 41 | * 42 | * @return List of EnvelopeTransactionList. 43 | */ 44 | public List getEnvelopes() { 45 | return envelopes; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/main/Foundation.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.main; 2 | 3 | import com.mimecast.robin.annotation.AnnotationLoader; 4 | import com.mimecast.robin.config.ConfigLoader; 5 | import org.apache.logging.log4j.LogManager; 6 | import org.apache.logging.log4j.Logger; 7 | 8 | import javax.naming.ConfigurationException; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | 11 | /** 12 | * Run-once initializer for server and client. 13 | * 14 | *

Provides the runonce initializer for global configuration. 15 | *

Both server and client extend this. 16 | *

This loads the config files and annotations. 17 | * 18 | * @see Server 19 | * @see Client 20 | * @see ConfigLoader 21 | * @see AnnotationLoader 22 | */ 23 | public abstract class Foundation { 24 | protected static final Logger log = LogManager.getLogger(Foundation.class); 25 | 26 | /** 27 | * Run once boolean. 28 | */ 29 | private static final AtomicBoolean runOnce = new AtomicBoolean(); 30 | 31 | /** 32 | * Run once initializer. 33 | * 34 | * @param path Path to configuration file. 35 | * @throws ConfigurationException Unable to read/parse config file. 36 | */ 37 | public static synchronized void init(String path) throws ConfigurationException { 38 | if (runOnce.get()) return; 39 | if (runOnce.compareAndSet(false, true)) { 40 | ConfigLoader.load(path); 41 | AnnotationLoader.load(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/Login.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import org.apache.commons.codec.binary.Base64; 5 | 6 | /** 7 | * Login authentication mechanism. 8 | * 9 | * @see DRAFT SASL LOGIN 10 | */ 11 | public class Login { 12 | 13 | /** 14 | * Username. 15 | */ 16 | private final String username; 17 | 18 | /** 19 | * Password. 20 | */ 21 | private final String password; 22 | 23 | /** 24 | * Constructs a new Login instance. 25 | * 26 | * @param connection Connection instance. 27 | */ 28 | public Login(Connection connection) { 29 | if (connection.getSession() != null) { 30 | this.username = connection.getSession().getUsername(); 31 | this.password = connection.getSession().getPassword(); 32 | } else { 33 | this.username = ""; 34 | this.password = ""; 35 | } 36 | } 37 | 38 | /** 39 | * Gets username. 40 | * 41 | * @return Username string. 42 | */ 43 | public String getUsername() { 44 | return Base64.encodeBase64String(username.getBytes()); 45 | } 46 | 47 | /** 48 | * Gets password. 49 | * 50 | * @return Password string. 51 | */ 52 | public String getPassword() { 53 | return Base64.encodeBase64String(password.getBytes()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/annotation/plugin/XclientPlugin.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.annotation.plugin; 2 | 3 | import com.mimecast.robin.annotation.Plugin; 4 | import com.mimecast.robin.main.Extensions; 5 | import com.mimecast.robin.main.Factories; 6 | import com.mimecast.robin.smtp.extension.Extension; 7 | import com.mimecast.robin.smtp.extension.client.ClientXclient; 8 | import com.mimecast.robin.smtp.extension.client.XclientBehaviour; 9 | import com.mimecast.robin.smtp.extension.server.ServerXclient; 10 | import com.mimecast.robin.smtp.session.XclientSession; 11 | 12 | /** 13 | * XCLIENT plugin. 14 | * 15 | *

XCLIENT is a SMTP extension developed by Postfix to provide the means to forge a sender. 16 | *

The intended purpose is for testing but if exposed by a real MTA it can be used to exploit the system. 17 | *

This plugin implements this without any security, but if used in production ensure the strictest control. 18 | * 19 | * @see Postfix XCLIENT 20 | */ 21 | @SuppressWarnings("WeakerAccess") 22 | @Plugin(priority = 101) 23 | public class XclientPlugin { 24 | 25 | /** 26 | * Constructs a new XclientPlugin instance and sets XCLIENT extension and behaviour. 27 | */ 28 | public XclientPlugin() { 29 | Factories.setBehaviour(XclientBehaviour::new); 30 | Factories.setSession(XclientSession::new); 31 | 32 | Extensions.addExtension("xclient", new Extension(ServerXclient::new, ClientXclient::new)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/main/ServerCLI.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.main; 2 | 3 | import com.mimecast.robin.Main; 4 | 5 | import javax.naming.ConfigurationException; 6 | 7 | /** 8 | * Implementation of server CLI. 9 | * 10 | * @see Server 11 | */ 12 | public class ServerCLI { 13 | 14 | /** 15 | * Protected constructor. 16 | */ 17 | private ServerCLI() { 18 | throw new IllegalStateException("Static class"); 19 | } 20 | 21 | /** 22 | * Listener usage. 23 | */ 24 | private static final String USAGE = Main.USAGE + " --server"; 25 | 26 | /** 27 | * Listener description. 28 | */ 29 | private static final String DESCRIPTION = "Debug MTA server"; 30 | 31 | /** 32 | * Constructs a new ServerCLI instance. 33 | * 34 | * @param main Main instance. 35 | */ 36 | public static void main(Main main) { 37 | if (main.getArgs().length > 0) { 38 | try { 39 | Server.run(main.getArgs()[0]); 40 | } catch (ConfigurationException e) { 41 | main.log("Server error: " + e.getMessage()); 42 | } 43 | 44 | } else { 45 | main.log(USAGE); 46 | main.log(" " + DESCRIPTION); 47 | main.log(""); 48 | main.log("usage:"); 49 | main.log(" Path to configuration directory"); 50 | main.log(""); 51 | main.log("example:"); 52 | main.log(" " + USAGE + " config/"); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/client/ClientMailTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.MessageEnvelope; 5 | import com.mimecast.robin.smtp.connection.ConnectionMock; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | class ClientMailTest { 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | } 21 | 22 | @Test 23 | void process() throws IOException { 24 | StringBuilder stringBuilder = new StringBuilder(); 25 | stringBuilder.append("250 OK\r\n"); 26 | ConnectionMock connection = new ConnectionMock(stringBuilder); 27 | 28 | MessageEnvelope envelope = new MessageEnvelope(); 29 | envelope.setMail("tony@example.com"); 30 | envelope.setFile("src/test/resources/lipsum.eml"); 31 | connection.getSession().addEnvelope(envelope); 32 | 33 | ClientMail mail = new ClientMail(); 34 | boolean process = mail.process(connection); 35 | 36 | assertTrue(process); 37 | 38 | connection.parseLines(); 39 | assertEquals("MAIL FROM: SIZE=2744\r\n", connection.getLine(1)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/http/MockOkHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.http; 2 | 3 | import okhttp3.*; 4 | import okio.Timeout; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | public class MockOkHttpClient extends OkHttpClient { 8 | Response response; 9 | 10 | public MockOkHttpClient(Response response) { 11 | this.response = response; 12 | } 13 | 14 | @NotNull 15 | @Override 16 | public Call newCall(@NotNull Request request) { 17 | return new MockCall(response); 18 | } 19 | } 20 | 21 | class MockCall implements Call { 22 | 23 | Response response; 24 | 25 | public MockCall(Response response) { 26 | this.response = response; 27 | } 28 | 29 | @Override 30 | public void cancel() { 31 | 32 | } 33 | 34 | @NotNull 35 | @Override 36 | public Request request() { 37 | return response.request(); 38 | } 39 | 40 | @NotNull 41 | @Override 42 | public Response execute() { 43 | return response; 44 | } 45 | 46 | @Override 47 | public void enqueue(@NotNull Callback callback) { 48 | 49 | } 50 | 51 | @Override 52 | public boolean isExecuted() { 53 | return true; 54 | } 55 | 56 | @Override 57 | public boolean isCanceled() { 58 | return false; 59 | } 60 | 61 | @NotNull 62 | @Override 63 | public Timeout timeout() { 64 | return new Timeout(); 65 | } 66 | 67 | @NotNull 68 | @Override 69 | public Call clone() { 70 | return null; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/resources/server.json5: -------------------------------------------------------------------------------- 1 | { 2 | "bind": "::", 3 | "port": 25, 4 | "backlog": 20, 5 | "transactionsLimit": 200, 6 | "errorLimit": 3, 7 | 8 | "auth": true, 9 | "starttls": true, 10 | "chunking": true, 11 | 12 | "keystore": "src/test/resources/keystore.jks", 13 | "keystorepassword": "avengers", 14 | 15 | "storage": { 16 | "enabled": true, 17 | "path": "/tmp/store" 18 | }, 19 | 20 | "users": [ 21 | { 22 | "name": "tony@example.com", 23 | "pass": "giveHerTheRing" 24 | } 25 | ], 26 | 27 | "scenarios": { 28 | "*": { 29 | "rcpt": [ 30 | { 31 | "value": "friday\\-[0-9]+@example\\.com", 32 | "response": "252 I think I know this user" 33 | } 34 | ] 35 | }, 36 | "reject.com": { 37 | "ehlo": "501 Not talking to you", 38 | "starttls": { 39 | "protocols": ["TLSv1.0"], 40 | "ciphers": ["TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"], 41 | "response": "220 You will fail" 42 | }, 43 | "mail": "451 I'm not listening to you", 44 | "rcpt": [ 45 | { 46 | "value": "ultron@reject\\.com", 47 | "response": "501 Heart not found" 48 | } 49 | ], 50 | "data": "554 Your data is corrupted" 51 | }, 52 | "rejectmail.com": { 53 | "rcpt": [ 54 | { 55 | "value": "jane@example\\.com", 56 | "response": "501 Invalid address" 57 | } 58 | ] 59 | }, 60 | "helo.com": { 61 | "ehlo": "500 ESMTP Error (Try again using SMTP)" 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/benchmark/EmailParserBench.java: -------------------------------------------------------------------------------- 1 | package benchmark; 2 | 3 | import com.mimecast.robin.mime.EmailParser; 4 | import com.mimecast.robin.smtp.io.LineInputStream; 5 | import org.openjdk.jmh.annotations.*; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.IOException; 9 | 10 | @State(Scope.Benchmark) 11 | public class EmailParserBench { 12 | 13 | static final String mime = "MIME-Version: 1.0\r\n" + 14 | "From: Lady Robin \r\n" + 15 | "To: Sir Robin \r\n" + 16 | "Date: Thu, 28 Jan 2021 20:27:09 +0000\r\n" + 17 | "Message-ID: \r\n" + 18 | "Subject: Robin likes\r\n" + 19 | "Content-Type: text/plain; charset=\"ISO-8859-1\",\r\n\tname=robin.txt,\r\n\tlanguage='en_UK';\r\n" + 20 | "Content-Disposition: inline charset='ISO-8859-1'\r\n\tfilename=robin.txt;\r\n\tlanguage=en_UK,"; 21 | 22 | @State(Scope.Thread) 23 | public static class Input { 24 | public LineInputStream inputStream = new LineInputStream(new ByteArrayInputStream(mime.getBytes()), 1024); 25 | } 26 | 27 | @TearDown(Level.Iteration) 28 | public void tearDown(Input state) throws IOException { 29 | state.inputStream.close(); 30 | } 31 | 32 | @Benchmark 33 | @BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime}) 34 | public void headers(Input state) throws IOException { 35 | EmailParser parser = new EmailParser(state.inputStream) 36 | .parse(true); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/resources/cases/config/behaviour.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | // How many times to try and establish a connection to remote server. First counts. 5 | retry: 2, 6 | 7 | // Delay between tries. 8 | delay: 5, 9 | 10 | // Enable TLS. 11 | tls: true, 12 | 13 | // Will execute commands in this order. 14 | // RCPT will send one address, multiple required for multiple addresses. 15 | // QUIT is always executed last. 16 | // AUTH is also supported. 17 | behaviour: [ 18 | "EHLO", "MAIL", "RCPT", "STARTTLS", "RCPT", "DATA" 19 | ], 20 | 21 | // Email envelopes. 22 | envelopes: [ 23 | // Envelope one. 24 | { 25 | // Recipients list. 26 | rcpt: [ 27 | "robin@example.com", 28 | "lady@example.com" 29 | ], 30 | 31 | // Email eml file to transmit. 32 | file: "src/test/resources/cases/sources/lipsum.eml", 33 | 34 | // Assertions to run against the envelope. 35 | assertions: { 36 | 37 | // Protocol assertions. 38 | // Check SMTP responses match regular expressions. 39 | protocol: [ 40 | ["MAIL", "250 Sender OK"], 41 | ["RCPT", "250 Recipient OK"], 42 | ["DATA", "^250"], 43 | ["DATA", "Received OK"] 44 | ] 45 | } 46 | } 47 | ], 48 | 49 | // Assertions to run against the connection. 50 | assertions: { 51 | 52 | // Protocol assertions. 53 | // Check SMTP responses match regular expressions. 54 | protocol: [ 55 | [ "SMTP", "^220" ], 56 | [ "EHLO", "STARTTLS" ], 57 | [ "QUIT" ] 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/io/LineInputStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.io; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import javax.naming.ConfigurationException; 8 | import java.io.FileInputStream; 9 | import java.io.IOException; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | 15 | class LineInputStreamTest { 16 | 17 | private static LineInputStream stream; 18 | 19 | @BeforeAll 20 | static void before() throws ConfigurationException { 21 | Foundation.init("src/test/resources/"); 22 | } 23 | 24 | @Test 25 | void readLine() throws IOException { 26 | Map lines = new HashMap<>(); 27 | 28 | byte[] bytes; 29 | LineInputStream stream = new LineInputStream(new FileInputStream("src/test/resources/lipsum.mixed.eol.eml")); 30 | while ((bytes = stream.readLine()) != null) { 31 | lines.put(stream.getLineNumber(), new String(bytes)); 32 | } 33 | 34 | assertEquals(76, lines.size()); 35 | assertEquals("From: <{$MAILFROM}>", lines.get(2).trim()); 36 | assertEquals("To: <{$RCPTTO}>", lines.get(3).trim()); 37 | assertEquals("Subject: Lipsum", lines.get(6).trim()); 38 | assertEquals("Integer at finibus orci.", lines.get(27).trim()); 39 | assertEquals("Content-Transfer-Encoding: 8bit", lines.get(42).trim()); 40 | assertEquals("--MCBoundary11505141140170031--", lines.get(76).trim()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/Plain.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import org.apache.commons.codec.binary.Base64; 5 | 6 | import java.io.ByteArrayOutputStream; 7 | 8 | /** 9 | * Plain authentication mechanism. 10 | * 11 | * @see RFC 4616 12 | */ 13 | public class Plain { 14 | 15 | /** 16 | * Username. 17 | */ 18 | private final String username; 19 | 20 | /** 21 | * Password. 22 | */ 23 | private final String password; 24 | 25 | /** 26 | * Constructs a new Plain instance. 27 | * 28 | * @param connection Connection instance. 29 | */ 30 | public Plain(Connection connection) { 31 | if (connection.getSession() != null) { 32 | this.username = connection.getSession().getUsername(); 33 | this.password = connection.getSession().getPassword(); 34 | } else { 35 | this.username = ""; 36 | this.password = ""; 37 | } 38 | } 39 | 40 | /** 41 | * Gets response. 42 | * 43 | * @return Response string. 44 | */ 45 | public String getLogin() { 46 | ByteArrayOutputStream plain = new ByteArrayOutputStream(); 47 | plain.write(username.getBytes(), 0, username.length()); 48 | plain.write(0); 49 | plain.write(username.getBytes(), 0, username.length()); 50 | plain.write(0); 51 | plain.write(password.getBytes(), 0, password.length()); 52 | 53 | return Base64.encodeBase64String(plain.toByteArray()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/ClientXclient.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import com.mimecast.robin.smtp.session.XclientSession; 5 | 6 | import java.io.IOException; 7 | import java.util.Map; 8 | 9 | /** 10 | * XCLIENT extension processor. 11 | * 12 | * @see Postfix XCLIENT 13 | */ 14 | public class ClientXclient extends ClientProcessor { 15 | 16 | /** 17 | * XCLIENT processor. 18 | *

Parameters: NAME, ADDR, PORT, PROTO, HELO, LOGIN, DESTADDR, DESTPORT 19 | * 20 | * @param connection Connection instance. 21 | * @return Boolean. 22 | * @throws IOException Unable to communicate. 23 | */ 24 | @SuppressWarnings("rawtypes") 25 | @Override 26 | public boolean process(Connection connection) throws IOException { 27 | super.process(connection); 28 | 29 | final String client = "XCLIENT"; 30 | 31 | StringBuilder params = new StringBuilder(); 32 | for (Map.Entry pair : ((XclientSession) connection.getSession()).getXclient().entrySet()) { 33 | params.append(" ").append(pair.getKey()).append("=").append(pair.getValue()); 34 | } 35 | connection.write(client + params.toString()); 36 | 37 | String read = connection.read("220"); 38 | 39 | connection.getSession().getSessionTransactionList().addTransaction(client, client + params, read, !read.startsWith("220")); 40 | connection.getSession().setEhloLog("XHLO"); 41 | 42 | return read.startsWith("220"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/server/ServerXclientTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | class ServerXclientTest { 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | } 21 | 22 | @Test 23 | void process() throws IOException { 24 | StringBuilder stringBuilder = new StringBuilder(); 25 | ConnectionMock connection = new ConnectionMock(stringBuilder); 26 | connection.getSession().setRdns("example.com"); 27 | 28 | Verb verb = new Verb("XCLIENT helo=example.com name=example.com addr=127.0.0.1"); 29 | 30 | ServerXclient xclient = new ServerXclient(); 31 | boolean process = xclient.process(connection, verb); 32 | 33 | assertTrue(process); 34 | 35 | connection.parseLines(); 36 | assertTrue(connection.getLine(1).startsWith("220 example.com ESMTP")); 37 | assertEquals("example.com", connection.getSession().getEhlo()); 38 | assertEquals("example.com", connection.getSession().getFriendRdns()); 39 | assertEquals("127.0.0.1", connection.getSession().getFriendAddr()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/server/ServerHelpTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | class ServerHelpTest { 15 | 16 | @BeforeAll 17 | static void before() throws ConfigurationException { 18 | Foundation.init("src/test/resources/"); 19 | } 20 | 21 | @Test 22 | void process() throws IOException { 23 | StringBuilder stringBuilder = new StringBuilder(); 24 | ConnectionMock connection = new ConnectionMock(stringBuilder); 25 | 26 | Verb verb = new Verb("HELP"); 27 | 28 | ServerHelp help = new ServerHelp(); 29 | boolean process = help.process(connection, verb); 30 | 31 | assertTrue(process); 32 | 33 | String out = connection.getOutput(); 34 | assertTrue(out.contains("HELO")); 35 | assertTrue(out.contains("EHLO")); 36 | assertTrue(out.contains("STARTTLS")); 37 | assertTrue(out.contains("AUTH")); 38 | assertTrue(out.contains("MAIL")); 39 | assertTrue(out.contains("RCPT")); 40 | assertTrue(out.contains("DATA")); 41 | assertTrue(out.contains("BDAT")); 42 | assertTrue(out.contains("RSET")); 43 | assertTrue(out.contains("HELP")); 44 | assertTrue(out.contains("QUIT")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/auth/DigestCache.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.auth; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * Digest-MD5 authentication mechanism database. 9 | * 10 | *

It would be wise to implement limitations in both size and time. 11 | *

While the size should be based on hardware TTL should be at most an hour. 12 | */ 13 | @SuppressWarnings("squid:S1610") 14 | public abstract class DigestCache { 15 | 16 | /** 17 | * Saves a DigestData instance. 18 | * 19 | * @param token Lookup token string. 20 | * @param data DigestData instance. 21 | */ 22 | public void put(String token, DigestData data) { 23 | add(token, data); 24 | } 25 | 26 | /** 27 | * Finds a DigestData instance. 28 | * 29 | * @param token Lookup token string. 30 | * @return DigestData instance. 31 | */ 32 | public DigestData find(String token) { 33 | DigestData data = new DigestData(); 34 | 35 | if (StringUtils.isNotBlank(token)) { 36 | Map search = lookup(token); 37 | if (search != null) { 38 | data.setMap(search); 39 | } 40 | } 41 | 42 | return data; 43 | } 44 | 45 | /** 46 | * Adds a DigestData instance. 47 | * 48 | * @param token Lookup token string. 49 | * @param data DigestData instance. 50 | */ 51 | abstract void add(String token, DigestData data); 52 | 53 | /** 54 | * Lookup DigestData in cache by token. 55 | * 56 | * @param token Token string. 57 | * @return DigestData instance. 58 | */ 59 | abstract Map lookup(String token); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/util/StreamUtils.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import com.mimecast.robin.smtp.io.LineInputStream; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.Closeable; 8 | import java.io.IOException; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * Stream utils. 14 | */ 15 | public class StreamUtils { 16 | 17 | /** 18 | * Protected constructor. 19 | */ 20 | private StreamUtils() { 21 | throw new IllegalStateException("Static class"); 22 | } 23 | 24 | /** 25 | * Parses lines into map of numbered lines. 26 | * 27 | * @param outputStream Stream to parse. 28 | * @return Map of Integer and String. 29 | * @throws IOException Unable to read from stream. 30 | */ 31 | public static Map parseLines(ByteArrayOutputStream outputStream) throws IOException { 32 | final Map lines = new HashMap<>(); 33 | 34 | LineInputStream stream = new LineInputStream(new ByteArrayInputStream(outputStream.toByteArray())); 35 | 36 | byte[] bytes; 37 | while ((bytes = stream.readLine()) != null) { 38 | lines.put(stream.getLineNumber(), new String(bytes)); 39 | } 40 | 41 | return lines; 42 | } 43 | 44 | /** 45 | * Closes a Closeable unconditionally. 46 | * 47 | * @param closeable Object to close. 48 | */ 49 | public static void closeQuietly(final Closeable closeable) { 50 | try { 51 | if (closeable != null) { 52 | closeable.close(); 53 | } 54 | } catch (final IOException ioe) { 55 | // Ignore exception on purpose. 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/server/ServerEhloTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | class ServerEhloTest { 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | } 21 | 22 | @Test 23 | void process() throws IOException { 24 | StringBuilder stringBuilder = new StringBuilder(); 25 | ConnectionMock connection = new ConnectionMock(stringBuilder); 26 | connection.getSession().setFriendRdns("example.com"); 27 | connection.getSession().setFriendAddr("127.0.0.1"); 28 | 29 | Verb verb = new Verb("EHLO example.com"); 30 | 31 | ServerEhlo ehlo = new ServerEhlo(); 32 | boolean process = ehlo.process(connection, verb); 33 | 34 | assertTrue(process); 35 | assertEquals("example.com", connection.getSession().getEhlo()); 36 | 37 | String out = connection.getOutput(); 38 | assertTrue(out.contains("example.com")); 39 | assertTrue(out.contains("127.0.0.1")); 40 | assertTrue(out.contains("HELP")); 41 | assertTrue(out.contains("STARTTLS")); 42 | assertTrue(out.contains("AUTH")); 43 | assertTrue(out.contains("PLAIN")); 44 | assertTrue(out.contains("LOGIN")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/client/ClientXclientTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.session.XclientSession; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | class ClientXclientTest { 18 | 19 | @BeforeAll 20 | static void before() throws ConfigurationException { 21 | Foundation.init("src/test/resources/"); 22 | } 23 | 24 | @Test 25 | void process() throws IOException { 26 | StringBuilder stringBuilder = new StringBuilder(); 27 | stringBuilder.append("220 Go\r\n"); 28 | ConnectionMock connection = new ConnectionMock(stringBuilder); 29 | 30 | Map map = new HashMap<>(); 31 | map.put("name", "example.com"); 32 | map.put("helo", "example.com"); 33 | map.put("addr", "127.0.0.1"); 34 | ((XclientSession) connection.getSession()).setXclient(map); 35 | 36 | ClientXclient xclient = new ClientXclient(); 37 | boolean process = xclient.process(connection); 38 | 39 | assertTrue(process); 40 | assertEquals("220 Go", connection.getSession().getSessionTransactionList().getLast("XCLIENT").getResponse()); 41 | 42 | connection.parseLines(); 43 | assertEquals("XCLIENT helo=example.com name=example.com addr=127.0.0.1\r\n", connection.getLine(1)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/util/Random.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import java.security.SecureRandom; 4 | 5 | /** 6 | * Random number and string generator. 7 | */ 8 | public final class Random { 9 | 10 | /** 11 | * Protected constructor. 12 | */ 13 | private Random() { 14 | throw new IllegalStateException("Static class"); 15 | } 16 | 17 | /** 18 | * Random string seed. 19 | */ 20 | private static final String CH = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 21 | 22 | /** 23 | * Random generator. 24 | */ 25 | private static final java.util.Random randomGenerator = new SecureRandom(); 26 | 27 | /** 28 | * Random string generator with fixed length. 29 | * 30 | * @return Random string. 31 | */ 32 | public static String ch() { 33 | return ch(20); 34 | } 35 | 36 | /** 37 | * Random string generator with variable length. 38 | * 39 | * @param length Length. 40 | * @return Random string. 41 | */ 42 | public static String ch(int length) { 43 | StringBuilder str = new StringBuilder(); 44 | for (int i = 0; i < length; i++) { 45 | str.append(CH.charAt(no(CH.length() - 1))); 46 | } 47 | return str.toString(); 48 | } 49 | 50 | /** 51 | * Random number generator with fixed max. 52 | * 53 | * @return Random number. 54 | */ 55 | public static int no() { 56 | return randomGenerator.nextInt(10) + 1; 57 | } 58 | 59 | /** 60 | * Random number generator with variable max. 61 | * 62 | * @param length Length. 63 | * @return Random number. 64 | */ 65 | public static int no(int length) { 66 | return randomGenerator.nextInt(length) + 1; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/ClientRcpt.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.MessageEnvelope; 4 | import com.mimecast.robin.smtp.connection.Connection; 5 | import com.mimecast.robin.smtp.transaction.EnvelopeTransactionList; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * RCPT extension processor. 11 | */ 12 | public class ClientRcpt extends ClientProcessor { 13 | 14 | /** 15 | * RCPT processor. 16 | * 17 | * @param connection Connection instance. 18 | * @return Boolean. 19 | * @throws IOException Unable to communicate. 20 | */ 21 | @Override 22 | public boolean process(Connection connection) throws IOException { 23 | super.process(connection); 24 | 25 | // Select message and envelope to send. 26 | int messageID = connection.getSession().getSessionTransactionList().getEnvelopes().size() - 1; // Adjust as it's initially added in ClientMail. 27 | MessageEnvelope envelope = connection.getSession().getEnvelopes().get(messageID); 28 | 29 | // Get delivery envelope. 30 | EnvelopeTransactionList envelopeTransactions = connection.getSession().getSessionTransactionList().getEnvelopes().get(messageID); 31 | 32 | // Loop recipients. 33 | String write; 34 | String read; 35 | boolean accepting = false; 36 | for (String to : envelope.getRcpts()) { 37 | write = "RCPT TO:<" + to + ">" + envelope.getParams("rcpt"); 38 | connection.write(write); 39 | 40 | read = connection.read("250"); 41 | 42 | if (read.startsWith("250")) accepting = true; 43 | envelopeTransactions.addTransaction("RCPT", write, read, !accepting); 44 | } 45 | 46 | return accepting; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/transaction/EnvelopeTransactionList.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.transaction; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * EnvelopeTransactionList. 8 | * 9 | *

This provides the implementation for envelope transactions. 10 | * 11 | * @see TransactionList 12 | */ 13 | public class EnvelopeTransactionList extends TransactionList { 14 | 15 | /** 16 | * Gets MAIL transaction. 17 | * 18 | * @return MAIL transaction instance. 19 | */ 20 | public Transaction getMail() { 21 | return !getTransactions("MAIL").isEmpty() ? getTransactions("MAIL").get(0) : null; 22 | } 23 | 24 | /** 25 | * Gets RCPT transactions. 26 | * 27 | * @return RCPT transactions list. 28 | */ 29 | public List getRcpt() { 30 | return getTransactions("RCPT"); 31 | } 32 | 33 | /** 34 | * Gets RCPT errors logs. 35 | * 36 | * @return List of Transaction. 37 | */ 38 | public List getRcptErrors() { 39 | List found = new ArrayList<>(); 40 | for (Transaction transaction : getRcpt()) { 41 | if (transaction.isError()) { 42 | found.add(transaction); 43 | } 44 | } 45 | 46 | return found; 47 | } 48 | 49 | /** 50 | * Gets DATA transaction. 51 | * 52 | * @return DATA transaction instance. 53 | */ 54 | public Transaction getData() { 55 | return !getTransactions("DATA").isEmpty() ? getTransactions("DATA").get(0) : null; 56 | } 57 | 58 | /** 59 | * Gets BDAT transactions. 60 | * 61 | * @return BDAT transactions list. 62 | */ 63 | public List getBdat() { 64 | return getTransactions("BDAT"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/mime/parts/TextMimePart.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.mime.parts; 2 | 3 | import com.mimecast.robin.mime.headers.MimeHeader; 4 | import com.mimecast.robin.util.CharsetDetector; 5 | import org.apache.commons.io.IOUtils; 6 | 7 | import java.io.ByteArrayInputStream; 8 | import java.io.IOException; 9 | import java.io.OutputStream; 10 | 11 | /** 12 | * MIME part container from string. 13 | */ 14 | public class TextMimePart extends MimePart { 15 | 16 | /** 17 | * Constructs a new TextMimePart instance with given string. 18 | * 19 | * @param content Body content. 20 | */ 21 | public TextMimePart(byte[] content) { 22 | body = new ByteArrayInputStream(content); 23 | } 24 | 25 | /** 26 | * Writes email to given output stream. 27 | * 28 | * @param outputStream OutputStream instance. 29 | * @return Self. 30 | * @throws IOException Unable to write to output stream. 31 | */ 32 | @Override 33 | public MimePart writeTo(OutputStream outputStream) throws IOException { 34 | // Ensure we have a Content-Type header. 35 | MimeHeader contentType = getHeader("Content-Type"); 36 | if (contentType == null) { 37 | headers.put(new MimeHeader("Content-Type", "text/plain; charset=\"UTF-8\"")); 38 | } 39 | 40 | super.writeTo(outputStream); 41 | return this; 42 | } 43 | 44 | /** 45 | * Gets content. 46 | * 47 | * @return Content String. 48 | * @throws IOException Unable to read stream. 49 | */ 50 | public String getContent() throws IOException { 51 | byte[] bytes = getBytes(); 52 | 53 | if (content == null) { 54 | content = IOUtils.toString(bytes, CharsetDetector.getCharset(bytes)); 55 | } 56 | 57 | return content; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/security/TLSSocket.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.security; 2 | 3 | import javax.net.ssl.SSLSocket; 4 | import java.io.IOException; 5 | import java.net.Socket; 6 | import java.security.GeneralSecurityException; 7 | 8 | /** 9 | * TLS socket. 10 | */ 11 | public interface TLSSocket { 12 | 13 | /** 14 | * Sets socket. 15 | * 16 | * @param socket Socket instance. 17 | * @return Self. 18 | */ 19 | TLSSocket setSocket(Socket socket); 20 | 21 | /** 22 | * Sets TLS protocols supported. 23 | * 24 | * @param protocols Protocols list. 25 | * @return Self. 26 | */ 27 | TLSSocket setProtocols(String[] protocols); 28 | 29 | /** 30 | * Sets TLS ciphers supported. 31 | * 32 | * @param ciphers Cipher suites list. 33 | * @return Self. 34 | */ 35 | TLSSocket setCiphers(String[] ciphers); 36 | 37 | /** 38 | * Enable encryption for the given socket. 39 | * 40 | * @param client True if in client mode. 41 | * @return SSLSocket instance. 42 | * @throws IOException Unable to read. 43 | * @throws GeneralSecurityException Problems with TrustManager or KeyManager. 44 | */ 45 | SSLSocket startTLS(boolean client) throws IOException, GeneralSecurityException; 46 | 47 | /** 48 | * Gets default protocols or enabled ones from configured list. 49 | * 50 | * @param sslSocket SSLSocket instance. 51 | * @return Protocols list. 52 | */ 53 | String[] getEnabledProtocols(SSLSocket sslSocket); 54 | 55 | /** 56 | * Gets default cipher suites or enabled ones from configured list. 57 | * 58 | * @param sslSocket SSLSocket instance. 59 | * @return Cipher suites list. 60 | */ 61 | String[] getEnabledCipherSuites(SSLSocket sslSocket); 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/client/RouteConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.client; 2 | 3 | import com.mimecast.robin.main.Config; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class RouteConfigTest { 13 | 14 | private static RouteConfig routeConfig1; 15 | private static RouteConfig routeConfig2; 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | 21 | routeConfig1 = Config.getClient().getRoute("com"); 22 | routeConfig2 = Config.getClient().getRoute("net"); 23 | } 24 | 25 | @Test 26 | void getName() { 27 | assertEquals("com", routeConfig1.getName()); 28 | 29 | assertEquals("net", routeConfig2.getName()); 30 | } 31 | 32 | @Test 33 | void getMx() { 34 | assertEquals("example.com", routeConfig1.getMx().get(0)); 35 | 36 | assertEquals("example.net", routeConfig2.getMx().get(0)); 37 | } 38 | 39 | @Test 40 | void getPort() { 41 | assertEquals(25, routeConfig1.getPort()); 42 | 43 | assertEquals(465, routeConfig2.getPort()); 44 | } 45 | 46 | @Test 47 | void getAuth() { 48 | assertFalse(routeConfig1.isAuth()); 49 | 50 | assertTrue(routeConfig2.isAuth()); 51 | } 52 | 53 | @Test 54 | void getUser() { 55 | assertNull(routeConfig1.getUser()); 56 | 57 | assertEquals("tony@example.com", routeConfig2.getUser()); 58 | } 59 | 60 | @Test 61 | void getPass() { 62 | assertNull(routeConfig1.getPass()); 63 | 64 | assertEquals("giveHerTheRing", routeConfig2.getPass()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/server/ServerMailTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import com.mimecast.robin.smtp.connection.ConnectionMock; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | import java.util.Arrays; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | 16 | class ServerMailTest { 17 | 18 | @BeforeAll 19 | static void before() throws ConfigurationException { 20 | Foundation.init("src/test/resources/"); 21 | } 22 | 23 | @Test 24 | void process() throws IOException { 25 | StringBuilder stringBuilder = new StringBuilder(); 26 | ConnectionMock connection = new ConnectionMock(stringBuilder); 27 | 28 | Verb verb = new Verb("MAIL FROM: SIZE=42 BODY=BINARYMIME " + 29 | "NOTIFY=FAILURE,DELAY ORCPT=rfc822;pepper@example.com RET=HDRS ENVID=QQ314159"); 30 | 31 | ServerMail mail = new ServerMail(); 32 | boolean process = mail.process(connection, verb); 33 | 34 | assertTrue(process); 35 | assertEquals("tony@example.com", mail.getAddress().getAddress()); 36 | assertEquals("tony@example.com", connection.getSession().getMail().getAddress()); 37 | assertEquals(42, mail.getSize()); 38 | assertEquals("BINARYMIME", mail.getBody()); 39 | assertEquals("[FAILURE, DELAY]", Arrays.toString(mail.getNotify())); 40 | assertEquals("pepper@example.com", mail.getORcpt().getAddress()); 41 | assertEquals("HDRS", mail.getRet()); 42 | assertEquals("QQ314159", mail.getEnvId()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/util/PathUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import com.mimecast.robin.main.Foundation; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import javax.naming.ConfigurationException; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.nio.charset.Charset; 11 | 12 | import static org.junit.jupiter.api.Assertions.*; 13 | 14 | class PathUtilsTest { 15 | 16 | @BeforeAll 17 | static void before() throws ConfigurationException { 18 | Foundation.init("src/test/resources/"); 19 | } 20 | 21 | @Test 22 | void isFile() { 23 | assertTrue(PathUtils.isFile("src/test/resources/properties.json5")); 24 | } 25 | 26 | @Test 27 | void isNotFile() { 28 | assertFalse(PathUtils.isFile("src/test/resources/not.file")); 29 | } 30 | 31 | @Test 32 | void cleanFilePath() { 33 | String random = "¬!\"£$%^&*()_+Q{}:@~|<>?`-=[];'#\\,./"; 34 | assertEquals("¬!\"£$%^&*()_+Q{}:@~|<>?`-=[];'#,.", PathUtils.normalize(random)); 35 | } 36 | 37 | @Test 38 | void makePath() { 39 | String path = "/tmp/" + System.nanoTime(); 40 | assertTrue(PathUtils.makePath(path)); 41 | assertTrue(new File(path).delete()); 42 | } 43 | 44 | @Test 45 | void isDirectory() { 46 | assertTrue(PathUtils.isDirectory("src/test/resources/")); 47 | } 48 | 49 | @Test 50 | void isNotDirectory() { 51 | assertFalse(PathUtils.isFile("src/test/resources/not.dir/")); 52 | } 53 | 54 | @Test 55 | void readFile() throws IOException { 56 | String payload = PathUtils.readFile("src/test/resources/properties.json5", Charset.defaultCharset()); 57 | assertEquals(123, payload.charAt(0)); 58 | assertEquals(125, payload.charAt(payload.length() - 1)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/server/ScenarioConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.server; 2 | 3 | import com.mimecast.robin.main.Config; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | import static org.junit.jupiter.api.Assertions.assertFalse; 12 | 13 | class ScenarioConfigTest { 14 | 15 | private static ScenarioConfig scenarioConfig; 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | 21 | scenarioConfig = Config.getServer().getScenarios().get("reject.com"); 22 | } 23 | 24 | @Test 25 | void getEhlo() { 26 | assertEquals("501 Not talking to you", scenarioConfig.getEhlo()); 27 | } 28 | 29 | @Test 30 | void getMail() { 31 | assertEquals("451 I'm not listening to you", scenarioConfig.getMail()); 32 | } 33 | 34 | @Test 35 | void getStartTls() { 36 | assertEquals("TLSv1.0", scenarioConfig.getStarTls().getListProperty("protocols").get(0)); 37 | assertEquals("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", scenarioConfig.getStarTls().getListProperty("ciphers").get(0)); 38 | assertEquals("220 You will fail", scenarioConfig.getStarTls().getStringProperty("response")); 39 | } 40 | 41 | @Test 42 | void getRcpt() { 43 | assertFalse(scenarioConfig.getRcpt().isEmpty()); 44 | assertEquals("ultron@reject\\.com", scenarioConfig.getRcpt().get(0).get("value")); 45 | assertEquals("501 Heart not found", scenarioConfig.getRcpt().get(0).get("response")); 46 | } 47 | 48 | @Test 49 | void getData() { 50 | assertEquals("554 Your data is corrupted", scenarioConfig.getData()); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/assertion/AssertConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.assertion; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.reflect.TypeToken; 5 | import com.mimecast.robin.main.Foundation; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | 15 | class AssertConfigTest { 16 | 17 | @BeforeAll 18 | static void before() throws ConfigurationException { 19 | Foundation.init("src/test/resources/"); 20 | } 21 | 22 | @Test 23 | void smtp() { 24 | String json = 25 | "{\n" + 26 | " \"smtp\": [\n" + 27 | " [\"MAIL\", \"250 Sender OK\"],\n" + 28 | " [\"RCPT\", \"250 Recipient OK\"],\n" + 29 | " [\"DATA\", \"^250\"],\n" + 30 | " [\"DATA\", \"Received OK$\"]\n" + 31 | " ]\n" + 32 | "}"; 33 | 34 | Map map = new Gson().fromJson(json, new TypeToken>() {}.getType()); 35 | AssertConfig assertConfig = new AssertConfig(map); 36 | 37 | assertEquals("MAIL", assertConfig.getProtocol().get(0).get(0)); 38 | assertEquals("250 Sender OK", assertConfig.getProtocol().get(0).get(1)); 39 | assertEquals("RCPT", assertConfig.getProtocol().get(1).get(0)); 40 | assertEquals("250 Recipient OK", assertConfig.getProtocol().get(1).get(1)); 41 | assertEquals("DATA", assertConfig.getProtocol().get(2).get(0)); 42 | assertEquals("^250", assertConfig.getProtocol().get(2).get(1)); 43 | assertEquals("DATA", assertConfig.getProtocol().get(3).get(0)); 44 | assertEquals("Received OK$", assertConfig.getProtocol().get(3).get(1)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/connection/ConnectionTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.connection; 2 | 3 | import com.mimecast.robin.config.server.ScenarioConfig; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | import java.util.HashMap; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | class ConnectionTest { 15 | 16 | @BeforeAll 17 | static void before() throws ConfigurationException { 18 | Foundation.init("src/test/resources/"); 19 | } 20 | 21 | private ConnectionMock getConnection(StringBuilder stringBuilder) { 22 | ConnectionMock connection = new ConnectionMock(stringBuilder); 23 | connection.getSession().setRdns("example.com"); 24 | connection.getSession().setFriendRdns("example.net"); 25 | connection.getSession().setFriendAddr("127.0.0.1"); 26 | 27 | return connection; 28 | } 29 | 30 | @Test 31 | void scenarios() { 32 | StringBuilder stringBuilder = new StringBuilder(); 33 | stringBuilder.append("EHLO helo.com\r\n"); 34 | stringBuilder.append("HELO example.com\r\n"); 35 | stringBuilder.append("QUIT\r\n"); 36 | 37 | ConnectionMock connection = getConnection(stringBuilder); 38 | assertTrue(connection.getScenario().isPresent()); 39 | assertEquals("252 I think I know this user", connection.getScenario().get().getRcpt().get(0).get("response")); 40 | 41 | connection.setScenario(new ScenarioConfig(new HashMap() {{ 42 | put("ehlo", "501 Not talking to you"); 43 | }})); 44 | assertTrue(connection.getScenario().isPresent()); 45 | assertEquals("501 Not talking to you", connection.getScenario().get().getEhlo()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/io/SlowInputStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.io; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | @SuppressWarnings("StatementWithEmptyBody") 12 | class SlowInputStreamTest { 13 | 14 | @Test 15 | void fast() throws IOException { 16 | String stringBuilder = "MIME-Version: 1.0\r\n" + 17 | "From: \r\n" + 18 | "To: \r\n" + 19 | "Subject: Lost in space\r\n" + 20 | "Message-ID: <23szwa4xd5ec6rf7tgyh8j9um0kiol-tony@example.com>\r\n" + 21 | "\r\n" + 22 | "Rescue me!\r\n" + 23 | ".\r\n"; 24 | InputStream inputStream = new ByteArrayInputStream(stringBuilder.getBytes()); 25 | SlowInputStream slowInputStream = new SlowInputStream(inputStream, 64, 50); 26 | while (slowInputStream.read() != -1) {} 27 | 28 | assertEquals(0, slowInputStream.getTotalWait()); 29 | } 30 | 31 | @Test 32 | void slow() throws IOException { 33 | String stringBuilder = "MIME-Version: 1.0\r\n" + 34 | "From: \r\n" + 35 | "To: \r\n" + 36 | "Subject: Lost in space\r\n" + 37 | "Message-ID: <23szwa4xd5ec6rf7tgyh8j9um0kiol-tony@example.com>\r\n" + 38 | "\r\n" + 39 | "Rescue me!\r\n" + 40 | ".\r\n"; 41 | InputStream inputStream = new ByteArrayInputStream(stringBuilder.getBytes()); 42 | SlowInputStream slowInputStream = new SlowInputStream(inputStream, 128, 100); 43 | while (slowInputStream.read() != -1) {} 44 | 45 | assertEquals(100, slowInputStream.getTotalWait()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/Extension.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension; 2 | 3 | import com.mimecast.robin.smtp.extension.client.ClientProcessor; 4 | import com.mimecast.robin.smtp.extension.server.ServerProcessor; 5 | import org.apache.logging.log4j.LogManager; 6 | import org.apache.logging.log4j.Logger; 7 | 8 | import java.util.concurrent.Callable; 9 | 10 | /** 11 | * Extension container. 12 | * 13 | *

This holds pairs of client and server callable for the extension implementations. 14 | */ 15 | public class Extension { 16 | private static final Logger log = LogManager.getLogger(Extension.class); 17 | 18 | /** 19 | * Server callable. 20 | */ 21 | private final Callable server; 22 | 23 | /** 24 | * Client callable. 25 | */ 26 | private final Callable client; 27 | 28 | /** 29 | * Constructs a new Extension instance. 30 | * 31 | * @param server Server callable. 32 | * @param client Client callable. 33 | */ 34 | public Extension(Callable server, Callable client) { 35 | this.server = server; 36 | this.client = client; 37 | } 38 | 39 | /** 40 | * Gets server. 41 | * 42 | * @return Server instance. 43 | */ 44 | public ServerProcessor getServer() { 45 | try { 46 | return server.call(); 47 | } catch (Exception e) { 48 | log.error("Error calling server for extension: {}", e.getMessage()); 49 | } 50 | return null; 51 | } 52 | 53 | /** 54 | * Gets client. 55 | * 56 | * @return Client instance. 57 | */ 58 | public ClientProcessor getClient() { 59 | try { 60 | return client.call(); 61 | } catch (Exception e) { 62 | log.error("Error calling client for extension: {}", e.getMessage()); 63 | } 64 | return null; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/extension/client/ClientHelpTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.main.Extensions; 4 | import com.mimecast.robin.main.Foundation; 5 | import com.mimecast.robin.smtp.connection.ConnectionMock; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | 14 | class ClientHelpTest { 15 | 16 | @BeforeAll 17 | static void before() throws ConfigurationException { 18 | Foundation.init("src/test/resources/"); 19 | } 20 | 21 | @Test 22 | void process() throws IOException { 23 | StringBuilder stringBuilder = new StringBuilder(); 24 | stringBuilder.append("214 "); 25 | stringBuilder.append(Extensions.getHelp()); 26 | stringBuilder.append("\r\n"); 27 | ConnectionMock connection = new ConnectionMock(stringBuilder); 28 | 29 | ClientHelp help = new ClientHelp(); 30 | boolean process = help.process(connection); 31 | 32 | assertTrue(process); 33 | 34 | connection.parseLines(); 35 | assertTrue(connection.getLine(1).startsWith("HELP")); 36 | 37 | String response = connection.getSession().getSessionTransactionList().getLast("HELP").getResponse(); 38 | assertTrue(response.contains("HELO")); 39 | assertTrue(response.contains("EHLO")); 40 | assertTrue(response.contains("STARTTLS")); 41 | assertTrue(response.contains("AUTH")); 42 | assertTrue(response.contains("MAIL")); 43 | assertTrue(response.contains("RCPT")); 44 | assertTrue(response.contains("DATA")); 45 | assertTrue(response.contains("BDAT")); 46 | assertTrue(response.contains("RSET")); 47 | assertTrue(response.contains("HELP")); 48 | assertTrue(response.contains("QUIT")); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /cfg/client.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "../src/main/resources/schema/client.schema.json", 3 | 4 | // Default MX list and port to attempt to deliver the email to. 5 | mx: [ 6 | "127.0.0.1" 7 | ], 8 | port: 25, 9 | 10 | // Default TLS enablement. 11 | tls: true, 12 | 13 | // Default supported protocols and ciphers. 14 | protocols: [ 15 | "TLSv1.2", "TLSv1.3" 16 | ], 17 | ciphers: [ 18 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 19 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", 20 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 21 | "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", 22 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", 23 | "TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", 24 | "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", 25 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 26 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 27 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 28 | "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", 29 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 30 | "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256" 31 | ], 32 | 33 | // Default EHLO to use. 34 | ehlo: "mimecast.net", 35 | 36 | // Default sender and recipients. 37 | mail: "robin@mimecast.net", 38 | rcpt: [ 39 | "robin@example.com" 40 | ], 41 | 42 | // Default asserting configuration. 43 | assertions: { 44 | protocolFails: true, // If protocol assertion fails, fail test/exit gracefully. 45 | verifyFails: true // If external verify checks fail, fail test/exit gracefully. 46 | }, 47 | 48 | // Predefined delivery routes to use instead of MX and port. 49 | routes: [ 50 | { 51 | name: "local", 52 | mx: [ 53 | "127.0.0.1" 54 | ], 55 | port: 25 56 | }, 57 | 58 | { 59 | name: "com", 60 | mx: [ 61 | "example.com" 62 | ], 63 | port: 25 64 | }, 65 | 66 | { 67 | name: "net", 68 | mx: [ 69 | "example.net" 70 | ], 71 | port: 465, 72 | auth: true, 73 | user: "tony@example.com", 74 | pass: "giveHerTheRing" 75 | } 76 | ] 77 | } -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * About Robin! 3 | * 4 | *

Robin MTA Tester is a development, debug and testing tool for MTA architects. 5 | *
It is powered by a highly customizable SMTP client designed to emulate the behaviour of popular email clients. 6 | *
A rudimentary server is also provided that is mainly used for testing the client. 7 | * 8 | *

The primary usage is done via JSON files called test cases. 9 | *
Cases are client configuration files ran as Junit tests. 10 | * 11 | *

This project can be compiled into a runnable JAR. 12 | *
A CLI interface is implemented with support for both client and server execution. 13 | * 14 | *

CLI usage:

15 | *
16 |  *      $ java -jar robin.jar
17 |  *      MTA development, debug and testing tool
18 |  *
19 |  *      usage:
20 |  *      --client   Run as client
21 |  *      --server   Run as server
22 |  * 
23 | * 24 | *

CLI usage client:

25 | *
26 |  *      $ java -jar robin.jar --client
27 |  *      Email delivery client
28 |  *
29 |  *      usage:
30 |  *      -c,--conf <arg>    Path to configuration dir (Default: cfg/)
31 |  *      -f,--file <arg>    EML file to send
32 |  *      -h,--help         Show usage help
33 |  *      -j,--gson <arg>    Path to case file JSON
34 |  *      -m,--mail <arg>    MAIL FROM address
35 |  *      -p,--port <arg>    Port to connect to
36 |  *      -r,--rcpt <arg>    RCPT TO address
37 |  *      -x,--mx <arg>      Server to connect to
38 |  * 
39 | * 40 | *

CLI usage server:

41 | *
42 |  *      $ java -jar robin.jar --server
43 |  *      Debug MTA server
44 |  *
45 |  *      usage:
46 |  *      Path to configuration directory
47 |  *
48 |  *      example:
49 |  *      java -jar robin.jar --server config/
50 |  * 
51 | * 52 | *

Mimecast uses this to run smoke tests every time a new MTA snapshot is built. 53 | *
This helps identify bugs early before leaving the development environment. 54 | */ 55 | package com.mimecast.robin; 56 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/security/PermissiveTrustManager.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.security; 2 | 3 | import javax.net.ssl.X509TrustManager; 4 | import java.security.cert.CertificateException; 5 | import java.security.cert.X509Certificate; 6 | 7 | /** 8 | * All trusting manager. 9 | * 10 | *

Don't use in any production environments. 11 | */ 12 | @SuppressWarnings("all") 13 | public class PermissiveTrustManager implements X509TrustManager { 14 | 15 | /** 16 | * Is client trusted. 17 | * 18 | * @param chain Peer certificate chain. 19 | * @return Boolean. 20 | */ 21 | public boolean isClientTrusted(X509Certificate[] chain) { 22 | return true; 23 | } 24 | 25 | /** 26 | * Is host trusted. 27 | * 28 | * @param chain Peer certificate chain. 29 | * @return Boolean. 30 | */ 31 | public boolean isHostTrusted(X509Certificate[] chain) { 32 | return true; 33 | } 34 | 35 | /** 36 | * Check if client is trusted. 37 | * 38 | * @param chain Peer certificate chain. 39 | * @param authType Key exchange algorithm used. 40 | * @throws CertificateException If the certificate chain is not trusted. 41 | */ 42 | public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { 43 | // The purpose of this is to trust everything. 44 | } 45 | 46 | /** 47 | * Check if server is trusted. 48 | * 49 | * @param chain Peer certificate chain. 50 | * @param authType Key exchange algorithm used. 51 | * @throws CertificateException If the certificate chain is not trusted. 52 | */ 53 | public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { 54 | // The purpose of this is to trust everything. 55 | } 56 | 57 | /** 58 | * Gets accepted issuers. 59 | * 60 | * @return X509Certificate array. 61 | */ 62 | public X509Certificate[] getAcceptedIssuers() { 63 | return new X509Certificate[0]; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/util/MapUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import com.google.gson.Gson; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | class MapUtilsTest { 13 | 14 | @Test 15 | @SuppressWarnings("unchecked") 16 | void flattenMapValid() { 17 | List data = new ArrayList<>(); 18 | 19 | MapUtils.flattenMap( 20 | new Gson().fromJson("{\"meta\":{\"status\":200},\"data\":[{\"valid\":true}],\"errors\":[]}", Map.class), 21 | "", 22 | data 23 | ); 24 | 25 | assertEquals(3, data.size()); 26 | assertEquals("meta>status: 200.0", data.get(0)); 27 | assertEquals("data>0>valid: true", data.get(1)); 28 | assertEquals("errors: []", data.get(2)); 29 | } 30 | 31 | @Test 32 | @SuppressWarnings("unchecked") 33 | void flattenMapError() { 34 | List data = new ArrayList<>(); 35 | 36 | MapUtils.flattenMap( 37 | new Gson().fromJson("{\"meta\":{\"status\":200},\"data\":[{\"valid\":false}],\"errors\":[{\"key\":\"EndpointException\",\"errors\":[{\"code\":\"502\",\"message\":\"Unable to process request!\",\"retryable\":false}]}]}", Map.class), 38 | "", 39 | data 40 | );// {"key":"EndpointException","errors":[{"code":"502","message":"We did not get any response from the termite server after trying multiple times!","retryable":false}]} 41 | 42 | assertEquals(6, data.size()); 43 | assertEquals("meta>status: 200.0", data.get(0)); 44 | assertEquals("data>0>valid: false", data.get(1)); 45 | assertEquals("errors>0>key: EndpointException", data.get(2)); 46 | assertEquals("errors>0>errors>0>code: 502", data.get(3)); 47 | assertEquals("errors>0>errors>0>message: Unable to process request!", data.get(4)); 48 | assertEquals("errors>0>errors>0>retryable: false", data.get(5)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /doc/plugin.md: -------------------------------------------------------------------------------- 1 | Plugins 2 | ======= 3 | 4 | A number of components can be replaced/extended by the use of a plugin annotation. 5 | SMTP XCLIENT extension is provided in this fasion to demonstrate the use case. 6 | To allow for specific loading order a priority can be provided (default 100). 7 | 8 | 9 | Example 10 | ------- 11 | 12 | @Plugin(priority=101) 13 | public class XclientPlugin { 14 | public XclientPlugin() { 15 | Factories.setBehaviour(XclientBehaviour::new); 16 | Factories.setSession(XclientSession::new); 17 | Extensions.addExtension("xclient", new Extension(ServerXclient::new, ClientXclient::new)); 18 | } 19 | } 20 | 21 | Factories 22 | --------- 23 | The following components may be added/replaced: 24 | 25 | - **Behaviour** - Provides the behaviour logic for the SMTP client. *See: DefaultBehaviour.java* 26 | 27 | Factories.setBehaviour(Behaviour::new) 28 | 29 | - **Session** - SMTP Session data container. *See: Session.java* 30 | 31 | Factories.setSession(Session::new) 32 | 33 | - **TLSSocket** - TLS implementation. *See: DefaultTLSSocket.java* 34 | 35 | Factories.setTLSSocket(TLSSocket::new) 36 | 37 | - **X509TrustManager** - TrustManager implementation. *See: PermissiveTrustManager.java* 38 | 39 | Factories.setTrustManager(X509TrustManager::new) 40 | 41 | - **DigestDatabase** - Deque storage map. *See: StaticDigestDatabase.java* 42 | 43 | Factories.setDatabase(DigestDatabase::new) 44 | 45 | - **LogsClient** - MTA logs client. *See: LogsClient.java* 46 | 47 | Factories.setLogsClient(LogsClient::new) 48 | 49 | 50 | 51 | Extensions 52 | ---------- 53 | The following SMTP extensions exist by default. 54 | Adding one by the same name will replace an existing one. 55 | 56 | Both server and client callable should be provided but this is not enforced. 57 | However adding a null client or server implementation will result in a NullPointerException at runtime. 58 | 59 | - HELO 60 | - EHLO 61 | - STARTTLS 62 | - AUTH 63 | - MAIL 64 | - RCPT 65 | - DATA 66 | - BDAT 67 | - RSET 68 | - HELP 69 | - QUIT 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/client/ClientStartTls.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.client; 2 | 3 | import com.mimecast.robin.smtp.connection.Connection; 4 | import com.mimecast.robin.smtp.connection.SmtpException; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * STARTTLS extension processor. 10 | */ 11 | public class ClientStartTls extends ClientProcessor { 12 | 13 | /** 14 | * STARTTLS processor. 15 | * 16 | * @param connection Connection instance. 17 | * @return Boolean. 18 | * @throws IOException Unable to communicate. 19 | */ 20 | @Override 21 | public boolean process(Connection connection) throws IOException { 22 | super.process(connection); 23 | 24 | if (connection.getSession().isTls() && connection.getSession().isEhloTls() && !connection.getSession().isStartTls()) { 25 | String write = "STARTTLS"; 26 | connection.write(write); 27 | 28 | String read = connection.read("220"); 29 | 30 | connection.getSession().getSessionTransactionList().addTransaction(write, write, read, !read.startsWith("220")); 31 | if (!read.startsWith("220")) throw new SmtpException("STARTTLS"); 32 | 33 | connection.setProtocols(connection.getSession().getProtocols()); 34 | connection.setCiphers(connection.getSession().getCiphers()); 35 | 36 | try { 37 | connection.startTLS(true); 38 | connection.getSession().getSessionTransactionList().addTransaction("TLS", "", 39 | connection.getProtocol() + ":" + connection.getCipherSuite(), false); 40 | } catch (SmtpException e) { 41 | connection.getSession().getSessionTransactionList().addTransaction("TLS", "", 42 | e.getCause().getMessage(), true); 43 | throw e; 44 | } 45 | 46 | connection.getSession().setStartTls(true); 47 | connection.buildStreams(); 48 | connection.getSession().setEhloLog("SHLO"); 49 | } 50 | 51 | return true; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/assertion/client/humio/HumioClientMock.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.assertion.client.humio; 2 | 3 | import com.mimecast.robin.config.assertion.external.MatchExternalClientConfig; 4 | import com.mimecast.robin.smtp.connection.Connection; 5 | import org.json.JSONArray; 6 | 7 | class HumioClientMock extends HumioClient { 8 | 9 | /** 10 | * Constructs a new HumioClient instance. 11 | * 12 | * @param connection Connection instance. 13 | * @param config MatchExternalClientConfig instance. 14 | * @param transactionId Transaction ID. 15 | */ 16 | public HumioClientMock(Connection connection, MatchExternalClientConfig config, int transactionId) { 17 | super(connection, config, transactionId); 18 | } 19 | 20 | /** 21 | * Runs client. 22 | * 23 | * @return Server logs. 24 | */ 25 | @Override 26 | public JSONArray run() { 27 | JSONArray array = new JSONArray(); 28 | 29 | array.put("DEBUG|0810-110152743|SmtpThread-30307|smtp.Receipt|wiUcnEI38Tjdnqw984Gtjd|||||Closing Transmission Channel"); 30 | array.put("INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|dSuG02ERMxOO1cR2Eawg9A|||||Accepted connection from 8.8.8.8:7575"); 31 | array.put("INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|dSuG02ERMxOO1cR2Eawg9A|||||> EHLO example.com"); 32 | array.put("INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|dSuG02ERMxOO1cR2Eawg9A|||||> MAIL FROM: SIZE=294"); 33 | array.put("INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|dSuG02ERMxOO1cR2Eawg9A|||||> RCPT TO:"); 34 | array.put("INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|dSuG02ERMxOO1cR2Eawg9A|||||> DATA"); 35 | array.put("INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|dSuG02ERMxOO1cR2Eawg9A|||||> ."); 36 | array.put("INFO |0810-110152743|SmtpThread-30307|smtp.Receipt|dSuG02ERMxOO1cR2Eawg9A|||||> QUIT"); 37 | array.put("DEBUG|0810-110152743|SmtpThread-30307|smtp.Receipt|dSuG02ERMxOO1cR2Eawg9A|||||Closing Transmission Channel"); 38 | 39 | return array; 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/PropertiesTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config; 2 | 3 | import com.mimecast.robin.main.Config; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class PropertiesTest { 13 | 14 | @BeforeAll 15 | static void before() throws ConfigurationException { 16 | Foundation.init("src/test/resources/"); 17 | } 18 | 19 | @Test 20 | void getBooleanProperty() { 21 | assertTrue(Config.getProperties().hasProperty("boolean")); 22 | assertTrue(Config.getProperties().getBooleanProperty("boolean")); 23 | } 24 | 25 | @Test 26 | void getLongProperty() { 27 | assertTrue(Config.getProperties().hasProperty("long")); 28 | assertEquals((Long) 7L, Config.getProperties().getLongProperty("long")); 29 | } 30 | 31 | @Test 32 | void getStringProperty() { 33 | assertTrue(Config.getProperties().hasProperty("string")); 34 | assertEquals("string", Config.getProperties().getStringProperty("string")); 35 | } 36 | 37 | @Test 38 | void getStringSubProperty() { 39 | assertEquals("substring", Config.getProperties().getStringProperty("sub.string")); 40 | } 41 | 42 | @Test 43 | void getListProperty() { 44 | assertTrue(Config.getProperties().hasProperty("list")); 45 | assertEquals("[monkey, weasel, dragon]", Config.getProperties().getListProperty("list").toString()); 46 | } 47 | 48 | @Test 49 | void getMapProperty() { 50 | assertTrue(Config.getProperties().hasProperty("map")); 51 | assertEquals(1, Config.getProperties().getMapProperty("map").size()); 52 | assertEquals("map", Config.getProperties().getMapProperty("map").get("string")); 53 | } 54 | 55 | @Test 56 | void getPropertyWithDefault() { 57 | assertFalse(Config.getProperties().hasProperty("default")); 58 | assertEquals("value", Config.getProperties().getStringProperty("default", "value")); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/config/server/ServerConfigTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.server; 2 | 3 | import com.mimecast.robin.main.Config; 4 | import com.mimecast.robin.main.Foundation; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | class ServerConfigTest { 13 | 14 | @BeforeAll 15 | static void before() throws ConfigurationException { 16 | Foundation.init("src/test/resources/"); 17 | } 18 | 19 | @Test 20 | void getBind() { 21 | assertEquals("::", Config.getServer().getBind()); 22 | } 23 | 24 | @Test 25 | void getPort() { 26 | assertEquals(25, Config.getServer().getPort()); 27 | } 28 | 29 | @Test 30 | void getBacklog() { 31 | assertEquals(20, Config.getServer().getBacklog()); 32 | } 33 | 34 | @Test 35 | void getErrorLimit() { 36 | assertEquals(3, Config.getServer().getErrorLimit()); 37 | } 38 | 39 | @Test 40 | void isAuth() { 41 | assertTrue(Config.getServer().isAuth()); 42 | } 43 | 44 | @Test 45 | void isStartTls() { 46 | assertTrue(Config.getServer().isStartTls()); 47 | } 48 | 49 | @Test 50 | void isChunking() { 51 | assertTrue(Config.getServer().isChunking()); 52 | } 53 | 54 | @Test 55 | void getKeyStore() { 56 | assertEquals("src/test/resources/keystore.jks", Config.getServer().getKeyStore()); 57 | } 58 | 59 | @Test 60 | void getKeyStorePassword() { 61 | assertEquals("avengers", Config.getServer().getKeyStorePassword()); 62 | } 63 | 64 | @Test 65 | void getUsers() { 66 | assertEquals(1, Config.getServer().getUsers().size()); 67 | } 68 | 69 | @Test 70 | void getUser() { 71 | // Tested in UserConfigTest. 72 | assertTrue(Config.getServer().getUser("tony@example.com").isPresent()); 73 | } 74 | 75 | @Test 76 | void getScenarios() { 77 | // Tested in ScenarioConfigTest. 78 | assertFalse(Config.getServer().getScenarios().isEmpty()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/mime/headers/MimeHeaders.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.mime.headers; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import java.util.stream.Collectors; 7 | 8 | /** 9 | * Mime headers container. 10 | */ 11 | public class MimeHeaders { 12 | 13 | /** 14 | * Headers list. 15 | */ 16 | private final List headers = new ArrayList<>(); 17 | 18 | /** 19 | * Puts header. 20 | * 21 | * @param header MimeHeader instance. 22 | * @return Self. 23 | */ 24 | public MimeHeaders put(MimeHeader header) { 25 | headers.add(header); 26 | return this; 27 | } 28 | 29 | /** 30 | * Removed header. 31 | * 32 | * @param header MimeHeader instance. 33 | * @return Self. 34 | */ 35 | public MimeHeaders remove(MimeHeader header) { 36 | headers.remove(header); 37 | return this; 38 | } 39 | 40 | /** 41 | * Gets headers as a list. 42 | * 43 | * @return List of MimeHeader. 44 | */ 45 | public List get() { 46 | return headers; 47 | } 48 | 49 | /** 50 | * Gets header by name. 51 | * 52 | * @param name Header name. 53 | * @return Optional of MimeHeader. 54 | */ 55 | public Optional get(String name) { 56 | for (MimeHeader header : headers) { 57 | if (header.getName().equalsIgnoreCase(name)) { 58 | return Optional.of(header); 59 | } 60 | } 61 | return Optional.empty(); 62 | } 63 | 64 | /** 65 | * Gets header by starts with partial name. 66 | * 67 | * @param name Header name. 68 | * @return List of MimeHeader. 69 | */ 70 | public List startsWith(String name) { 71 | return headers.stream() 72 | .filter(h -> h.getName().toLowerCase().startsWith(name.toLowerCase())) 73 | .collect(Collectors.toList()); 74 | } 75 | 76 | /** 77 | * Gets headers list size. 78 | * 79 | * @return Integer. 80 | */ 81 | public int size() { 82 | return headers.size(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/http/HttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.http; 2 | 3 | import java.util.Map; 4 | import java.util.TreeMap; 5 | 6 | /** 7 | * HTTP/S response container. 8 | */ 9 | public class HttpResponse { 10 | 11 | /** 12 | * Success container. 13 | */ 14 | private boolean success = false; 15 | 16 | /** 17 | * Is successfull. 18 | * 19 | * @return Boolean. 20 | */ 21 | public boolean isSuccessfull() { 22 | return success; 23 | } 24 | 25 | /** 26 | * Sets success. 27 | * 28 | * @param success Boolean. 29 | * @return Self. 30 | */ 31 | HttpResponse setSuccess(boolean success) { 32 | this.success = success; 33 | return this; 34 | } 35 | 36 | /** 37 | * Headers container. 38 | */ 39 | private final Map headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 40 | 41 | /** 42 | * Gets HTTP/S response headers. 43 | * 44 | * @return Map of String, String. 45 | */ 46 | public Map getHeaders() { 47 | return headers; 48 | } 49 | 50 | /** 51 | * Gets HTTP/S response header by name. 52 | * 53 | * @param name Header name. 54 | * @return Self. 55 | */ 56 | String getHeader(String name) { 57 | return headers.get(name); 58 | } 59 | 60 | /** 61 | * Adds HTTP/S response header. 62 | * 63 | * @param name Header name. 64 | * @param value Header value. 65 | * @return Self. 66 | */ 67 | HttpResponse addHeader(String name, String value) { 68 | headers.put(name, value); 69 | return this; 70 | } 71 | 72 | /** 73 | * Body container. 74 | */ 75 | private String body; 76 | 77 | /** 78 | * Gets HTTP/S response body. 79 | * 80 | * @return String. 81 | */ 82 | public String getBody() { 83 | return body; 84 | } 85 | 86 | /** 87 | * Adds HTTP/S response header. 88 | * 89 | * @param body String. 90 | * @return Self. 91 | */ 92 | HttpResponse addBody(String body) { 93 | this.body = body; 94 | return this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/mime/parts/PdfMimePartTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.mime.parts; 2 | 3 | import com.mimecast.robin.config.assertion.MimeConfig; 4 | import com.mimecast.robin.smtp.MessageEnvelope; 5 | import com.mimecast.robin.util.StreamUtils; 6 | import org.apache.commons.codec.binary.Base64; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.util.AbstractMap; 12 | import java.util.Arrays; 13 | import java.util.Map; 14 | import java.util.stream.Collectors; 15 | import java.util.stream.Stream; 16 | 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.junit.jupiter.api.Assertions.assertTrue; 19 | 20 | class PdfMimePartTest { 21 | 22 | @Test 23 | void writeTo() throws IOException { 24 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 25 | 26 | Map config = Stream.of( 27 | new AbstractMap.SimpleEntry<>("headers", 28 | Arrays.asList( 29 | Arrays.asList("Content-Type", "application/pdf; name=\"article.pdf\""), 30 | Arrays.asList("Content-Disposition", "attachment; filename=\"article.pdf\""), 31 | Arrays.asList("Content-Transfer-Encoding", "base64") 32 | ) 33 | ), 34 | new AbstractMap.SimpleEntry<>("folder", "src/test/resources/mime/") 35 | ).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 36 | 37 | new PdfMimePart(new MimeConfig(config), new MessageEnvelope()).writeTo(outputStream); 38 | 39 | Map lines = StreamUtils.parseLines(outputStream); 40 | 41 | assertEquals("Content-Type: application/pdf; name=\"article.pdf\"\r\n", lines.get(1)); 42 | assertEquals("Content-Disposition: attachment; filename=\"article.pdf\"\r\n", lines.get(2)); 43 | assertEquals("Content-Transfer-Encoding: base64\r\n", lines.get(3)); 44 | assertEquals("\r\n", lines.get(4)); 45 | assertTrue(Base64.isBase64(lines.get(5))); 46 | assertTrue(Base64.isBase64(lines.get(6))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/io/SlowOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.io; 2 | 3 | import com.mimecast.robin.util.Sleep; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | 7 | import java.io.IOException; 8 | import java.io.OutputStream; 9 | 10 | /** 11 | * Output stream with slow data writing capability. 12 | * 13 | *

Slows down the writing for given miliseconds every given bytes. 14 | */ 15 | @SuppressWarnings("squid:S4349") 16 | public class SlowOutputStream extends OutputStream { 17 | private static final Logger log = LogManager.getLogger(SlowOutputStream.class); 18 | 19 | /** 20 | * Output stream instance. 21 | */ 22 | private final OutputStream out; 23 | 24 | /** 25 | * Size of bytes to wait after. 26 | */ 27 | private final int bytes; 28 | 29 | /** 30 | * Time in miliseconds to wait for. 31 | */ 32 | private final int wait; 33 | 34 | /** 35 | * Time in miliseconds waited for. 36 | */ 37 | private int totalWait = 0; 38 | 39 | /** 40 | * Bytes read counter. 41 | */ 42 | private int count = 0; 43 | 44 | /** 45 | * Constructs a new SlowOutputStream instance with given bytes and wait. 46 | * 47 | * @param out OutputStream instance. 48 | * @param bytes Size of bytes. 49 | * @param wait Time out miliseconds. 50 | */ 51 | public SlowOutputStream(OutputStream out, int bytes, int wait) { 52 | this.out = out; 53 | this.bytes = bytes; 54 | this.wait = wait; 55 | } 56 | 57 | @Override 58 | public void write(int b) throws IOException { 59 | if (bytes >= 128 && wait >= 100) { 60 | count++; 61 | if (count == bytes) { 62 | count = 0; 63 | log.info("Waiting after {} bytes wrote.", bytes); 64 | totalWait += wait; 65 | Sleep.nap(wait); 66 | } 67 | } 68 | 69 | out.write(b); 70 | } 71 | 72 | /** 73 | * Gets total wait time spent waiting in miliseconds. 74 | *

This is primarly here for unit testing. 75 | * 76 | * @return Integer. 77 | */ 78 | public int getTotalWait() { 79 | return totalWait; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/cases/ExampleSend.java: -------------------------------------------------------------------------------- 1 | package cases; 2 | 3 | import com.mimecast.robin.assertion.AssertException; 4 | import com.mimecast.robin.main.Client; 5 | import com.mimecast.robin.main.Foundation; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.naming.ConfigurationException; 10 | import java.io.IOException; 11 | 12 | public class ExampleSend { 13 | 14 | @BeforeAll 15 | static void before() throws ConfigurationException { 16 | Foundation.init("cfg/"); 17 | } 18 | 19 | /** 20 | * JSON example of a basic test that sends an eml file. 21 | */ 22 | @Test 23 | void plainTextEml() throws AssertException, IOException { 24 | new Client() 25 | .send("src/test/resources/cases/config/lipsum.json5"); 26 | } 27 | 28 | /** 29 | * JSON example of a basic test that sends an UTF-8 eml file. 30 | */ 31 | @Test 32 | void plainTextUtf8Eml() throws AssertException, IOException { 33 | new Client() 34 | .send("src/test/resources/cases/config/pangrams.json5"); 35 | } 36 | 37 | /** 38 | * JSON example of a test with built email from MIME. 39 | */ 40 | @Test 41 | void dynamicMime() throws AssertException, IOException { 42 | new Client() 43 | .send("src/test/resources/cases/config/dynamic/dynamic.json5"); 44 | } 45 | 46 | /** 47 | * JSON example of a test with built email from MIME with randomly generated PDF. 48 | */ 49 | @Test 50 | void dynamicPdf() throws AssertException, IOException { 51 | new Client() 52 | .send("src/test/resources/cases/config/dynamic/dynamic.pdf.json5"); 53 | } 54 | 55 | /** 56 | * JSON example of a basic test that uses XCLIENT extension. 57 | */ 58 | @Test 59 | void xclient() throws AssertException, IOException { 60 | new Client() 61 | .send("src/test/resources/cases/config/xclient.json5"); 62 | } 63 | 64 | /** 65 | * JSON example of a basic test that uses a custom defined SMTP behaviour. 66 | */ 67 | @Test 68 | void behaviour() throws AssertException, IOException { 69 | new Client() 70 | .send("src/test/resources/cases/config/behaviour.json5"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/client/ClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.client; 2 | 3 | import com.mimecast.robin.config.ConfigFoundation; 4 | import com.mimecast.robin.config.assertion.AssertConfig; 5 | 6 | import java.io.IOException; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | /** 12 | * Default client configuration container. 13 | * 14 | *

This class provides type safe access to default client configuration. 15 | *

Cases inherit defaults from here. 16 | *

This also houses routes that can be chosen in a case. 17 | * 18 | * @see ConfigFoundation 19 | */ 20 | @SuppressWarnings({"unchecked", "rawtypes"}) 21 | public class ClientConfig extends ConfigFoundation { 22 | private final List routes = new ArrayList<>(); 23 | 24 | /** 25 | * Constructs a new ClientConfig instance. 26 | */ 27 | public ClientConfig() { 28 | super(); 29 | } 30 | 31 | /** 32 | * Constructs a new ClientConfig instance with configuration path. 33 | * 34 | * @param path Path to configuration file. 35 | * @throws IOException Unable to read file. 36 | */ 37 | public ClientConfig(String path) throws IOException { 38 | super(path); 39 | getListProperty("routes").forEach(map -> routes.add(new RouteConfig((Map) map))); 40 | } 41 | 42 | /** 43 | * Gets MAIL property. 44 | * 45 | * @return MAIL string. 46 | */ 47 | public String getMail() { 48 | return getStringProperty("mail"); 49 | } 50 | 51 | /** 52 | * Gets RCPT property. 53 | * 54 | * @return RCPT string. 55 | */ 56 | public List getRcpt() { 57 | return getListProperty("rcpt"); 58 | } 59 | 60 | /** 61 | * Gets assertion configuration. 62 | * 63 | * @return AssertConfig instance. 64 | */ 65 | public AssertConfig getAssertions() { 66 | return new AssertConfig(getMapProperty("assertions")); 67 | } 68 | 69 | /** 70 | * Gets route if any. 71 | * 72 | * @param name Route name. 73 | * @return RouteConfig instance. 74 | */ 75 | public RouteConfig getRoute(String name) { 76 | return routes.stream().filter(route -> route.getName().equals(name)).findFirst().orElse(null); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/io/SlowInputStream.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.io; 2 | 3 | import com.mimecast.robin.util.Sleep; 4 | import org.apache.logging.log4j.LogManager; 5 | import org.apache.logging.log4j.Logger; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | 10 | /** 11 | * Input stream with slow data reading capability. 12 | * 13 | *

Slows down the reading for given miliseconds every given bytes. 14 | */ 15 | public class SlowInputStream extends InputStream { 16 | private static final Logger log = LogManager.getLogger(SlowInputStream.class); 17 | 18 | /** 19 | * Input stream instance. 20 | */ 21 | private final InputStream in; 22 | 23 | /** 24 | * Size of bytes to wait after. 25 | */ 26 | private final int bytes; 27 | 28 | /** 29 | * Time in miliseconds to wait for. 30 | */ 31 | private final int wait; 32 | 33 | /** 34 | * Time in miliseconds waited for. 35 | */ 36 | private int totalWait = 0; 37 | 38 | /** 39 | * Bytes read counter. 40 | */ 41 | private int count = 0; 42 | 43 | /** 44 | * Constructs a new SlowInputStream instance with given bytes and wait. 45 | * 46 | * @param in InputStream instance. 47 | * @param bytes Size of bytes. 48 | * @param wait Time in miliseconds. 49 | */ 50 | public SlowInputStream(InputStream in, int bytes, int wait) { 51 | this.in = in; 52 | this.bytes = bytes; 53 | this.wait = wait; 54 | } 55 | 56 | @Override 57 | public int read() throws IOException { 58 | if (bytes >= 128 && wait >= 100) { 59 | int read = in.read(); 60 | 61 | count++; 62 | if (count == bytes) { 63 | count = 0; 64 | log.info("Waiting after {} bytes read.", bytes); 65 | totalWait += wait; 66 | Sleep.nap(wait); 67 | } 68 | 69 | return read; 70 | } else { 71 | return in.read(); 72 | } 73 | } 74 | 75 | /** 76 | * Gets total wait time spent waiting in miliseconds. 77 | *

This is primarly here for unit testing. 78 | * 79 | * @return Integer. 80 | */ 81 | public int getTotalWait() { 82 | return totalWait; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/util/UIDExtractor.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import com.mimecast.robin.main.Config; 4 | import com.mimecast.robin.smtp.connection.Connection; 5 | import com.mimecast.robin.smtp.transaction.Transaction; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | /** 13 | * UID Extractor. 14 | */ 15 | public class UIDExtractor { 16 | 17 | /** 18 | * UID pattern. 19 | */ 20 | protected static final Pattern uidPattern; 21 | 22 | static { 23 | uidPattern = Pattern.compile( 24 | Config.getProperties().getStringProperty("uidPattern", Config.getProperties().getStringProperty("uid.pattern", "\\s\\[([a-z0-9\\-_]+)]")), 25 | Pattern.CASE_INSENSITIVE 26 | ); 27 | } 28 | 29 | /** 30 | * Protected constructor. 31 | */ 32 | private UIDExtractor() { 33 | throw new IllegalStateException("Static class"); 34 | } 35 | 36 | /** 37 | * Get UID from SMTP command response with given connection and transaction ID. 38 | * 39 | * @param connection Connection instance. 40 | * @param transactionId Transaction ID. 41 | * @return String. 42 | */ 43 | public static String getUID(Connection connection, int transactionId) { 44 | List transactions = new ArrayList<>(); 45 | 46 | // Select transactions. 47 | if (transactionId >= 0 && !connection.getSession().getSessionTransactionList().getEnvelopes().isEmpty()) { 48 | transactions.addAll(connection.getSession().getSessionTransactionList().getEnvelopes().get(transactionId).getTransactions()); 49 | } else { 50 | transactions.addAll(connection.getSession().getSessionTransactionList().getTransactions()); 51 | } 52 | 53 | // Match UID pattern to transaction response. 54 | for (Transaction transaction : transactions) { 55 | if (transaction.getResponse() != null && !transaction.getResponse().isEmpty()) { 56 | Matcher m = uidPattern.matcher(transaction.getResponse()); 57 | if (m.find()) { 58 | return m.group(1).replaceAll("^-+", ""); 59 | } 60 | } 61 | } 62 | 63 | return null; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/util/MapUtils.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | /** 7 | * Map utils. 8 | */ 9 | @SuppressWarnings("unchecked") 10 | public class MapUtils { 11 | 12 | /** 13 | * Flatten map. 14 | * 15 | * @param map Map of String, Object instance to flatten. 16 | * @param precedence Preceding precedence. 17 | * @param collector List of string to collect results. 18 | */ 19 | public static void flattenMap(Map map, String precedence, List collector) { 20 | for (Map.Entry entry : map.entrySet()) { 21 | flattenObject(entry.getValue(), precedence + entry.getKey() + ">", collector); 22 | } 23 | } 24 | 25 | /** 26 | * Flatten list. 27 | * 28 | * @param list List of String instance to flatten. 29 | * @param precedence Preceding precedence. 30 | * @param collector List of string to collect results. 31 | */ 32 | public static void flattenList(List list, String precedence, List collector) { 33 | for (int i = 0; i < list.size(); i++) { 34 | flattenObject(list.get(i), precedence + String.valueOf(i) + ">", collector); 35 | } 36 | } 37 | 38 | /** 39 | * Flatten object. 40 | * 41 | * @param object Object instance to flatten. 42 | * @param precedence Preceding precedence. 43 | * @param collector List of string to collect results. 44 | */ 45 | @SuppressWarnings("unchecked") 46 | protected static void flattenObject(Object object, String precedence, List collector) { 47 | if (object instanceof Map) { 48 | flattenMap((Map) object, precedence, collector); 49 | 50 | } else { 51 | String endPrecedence = precedence.replaceAll(">$", ": "); 52 | 53 | if (object instanceof List) { 54 | if (!((List) object).isEmpty()) { 55 | flattenList((List) object, precedence, collector); 56 | 57 | } else { 58 | collector.add(endPrecedence + String.valueOf(object)); 59 | } 60 | 61 | } else { 62 | collector.add(endPrecedence + String.valueOf(object)); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/session/ConfigMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.session; 2 | 3 | import com.mimecast.robin.config.ConfigMapper; 4 | import com.mimecast.robin.config.client.CaseConfig; 5 | import com.mimecast.robin.main.Foundation; 6 | import com.mimecast.robin.smtp.MessageEnvelope; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import javax.naming.ConfigurationException; 11 | import java.io.IOException; 12 | import java.util.List; 13 | 14 | import static org.junit.jupiter.api.Assertions.*; 15 | 16 | class ConfigMapperTest { 17 | 18 | private static CaseConfig caseConfig; 19 | 20 | @BeforeAll 21 | static void before() throws IOException, ConfigurationException { 22 | Foundation.init("src/test/resources/"); 23 | 24 | caseConfig = new CaseConfig("src/test/resources/mapper.json5"); 25 | } 26 | 27 | @Test 28 | void mapTo() { 29 | Session session = new Session(); 30 | new ConfigMapper(caseConfig).mapTo(session); 31 | 32 | assertEquals(60000, session.getTimeout()); 33 | assertEquals("example.net", session.getMx().get(0)); 34 | assertEquals(465, session.getPort()); 35 | assertTrue(session.isTls()); 36 | assertFalse(session.isAuthBeforeTls()); 37 | assertEquals("TLSv1.1, TLSv1.2", String.join(", ", session.getProtocols())); 38 | assertEquals("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, " + 39 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384, " + 40 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, " + 41 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", String.join(", ", session.getCiphers())); 42 | assertEquals("example.com", session.getEhlo()); 43 | assertTrue(session.isAuth()); 44 | assertEquals("tony@example.com", session.getUsername()); 45 | assertEquals("giveHerTheRing", session.getPassword()); 46 | 47 | List envelopes = session.getEnvelopes(); 48 | assertEquals("tony@example.com", envelopes.get(0).getMail()); 49 | assertEquals("pepper@example.com", envelopes.get(0).getRcpts().get(0)); 50 | assertEquals("happy@example.com", envelopes.get(0).getRcpts().get(1)); 51 | 52 | assertEquals("tony@example.com", envelopes.get(1).getMail()); 53 | assertEquals("pepper@example.com", envelopes.get(1).getRcpts().get(0)); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/SetupListener.java: -------------------------------------------------------------------------------- 1 | import com.mimecast.robin.main.Foundation; 2 | import com.mimecast.robin.util.PathUtils; 3 | import org.junit.platform.launcher.LauncherSession; 4 | import org.junit.platform.launcher.LauncherSessionListener; 5 | import org.junit.platform.launcher.TestExecutionListener; 6 | import org.junit.platform.launcher.TestPlan; 7 | 8 | import javax.naming.ConfigurationException; 9 | 10 | /** 11 | * Junit setup listener. 12 | */ 13 | public class SetupListener implements LauncherSessionListener { 14 | 15 | /** 16 | * Initializer instance. 17 | */ 18 | private Initializer initializer; 19 | 20 | /** 21 | * Launcher session opened. 22 | * 23 | * @param session LauncherSession instance. 24 | */ 25 | @Override 26 | public void launcherSessionOpened(LauncherSession session) { 27 | session.getLauncher().registerTestExecutionListeners(new TestExecutionListener() { 28 | @Override 29 | public void testPlanExecutionStarted(TestPlan testPlan) { 30 | if (initializer == null) { 31 | initializer = new Initializer(); 32 | initializer.setUp(); 33 | } 34 | } 35 | }); 36 | } 37 | 38 | /** 39 | * Launcher session closed. 40 | * 41 | * @param session LauncherSession instance. 42 | */ 43 | @Override 44 | public void launcherSessionClosed(LauncherSession session) { 45 | if (initializer != null) { 46 | initializer.tearDown(); 47 | initializer = null; 48 | } 49 | } 50 | 51 | /** 52 | * Initializer class. 53 | *

Will initialise only if init.path system property is defined. 54 | *

Example VM options: -Dinit.path=cfg/ 55 | */ 56 | static class Initializer { 57 | 58 | /** 59 | * Set up. 60 | */ 61 | void setUp() { 62 | try { 63 | String initPath = System.getProperty("init.path"); 64 | 65 | if (PathUtils.isDirectory(initPath)) { 66 | Foundation.init(initPath); 67 | } 68 | } catch (ConfigurationException e) { 69 | System.out.println(e.getMessage()); 70 | } 71 | } 72 | 73 | /** 74 | * Tear down. 75 | */ 76 | void tearDown() { 77 | // Do nothing. 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/server/ServerRcpt.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.config.server.ScenarioConfig; 4 | import com.mimecast.robin.smtp.connection.Connection; 5 | import com.mimecast.robin.smtp.verb.Verb; 6 | 7 | import javax.mail.internet.AddressException; 8 | import javax.mail.internet.InternetAddress; 9 | import java.io.IOException; 10 | import java.util.Map; 11 | import java.util.Optional; 12 | 13 | /** 14 | * RCPT extension processor. 15 | */ 16 | public class ServerRcpt extends ServerMail { 17 | 18 | /** 19 | * RCPT processor. 20 | * 21 | * @param connection Connection instance. 22 | * @param verb Verb instance. 23 | * @return Boolean. 24 | * @throws IOException Unable to communicate. 25 | */ 26 | @Override 27 | public boolean process(Connection connection, Verb verb) throws IOException { 28 | super.process(connection, verb); 29 | 30 | // Scenario response. 31 | Optional opt = connection.getScenario(); 32 | if (opt.isPresent() && opt.get().getRcpt() != null) { 33 | for (Map entry : opt.get().getRcpt()) { 34 | if (getAddress() != null && getAddress().getAddress().matches(entry.get("value"))) { 35 | String response = entry.get("response"); 36 | if (response.startsWith("2")) { 37 | connection.getSession().addRcpt(getAddress()); 38 | } 39 | connection.write(response); 40 | return response.startsWith("2"); 41 | } 42 | } 43 | } 44 | 45 | // Accept all. 46 | connection.getSession().addRcpt(getAddress()); 47 | connection.write("250 2.1.5 Recipient OK [" + connection.getSession().getUID() + "]"); 48 | 49 | return true; 50 | } 51 | 52 | /** 53 | * Gets RCPT TO address. 54 | * 55 | * @return Address instance. 56 | * @throws IOException RCPT address parsing problem. 57 | */ 58 | @Override 59 | public InternetAddress getAddress() throws IOException { 60 | if (address == null) { 61 | try { 62 | address = new InternetAddress(verb.getParam("to")); 63 | } catch (AddressException e) { 64 | throw new IOException(e); 65 | } 66 | } 67 | 68 | return address; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/smtp/io/SlowOutputStreamTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.io; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.ByteArrayOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | class SlowOutputStreamTest { 13 | 14 | @Test 15 | void fast() throws IOException { 16 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 17 | SlowOutputStream slowOutputStream = new SlowOutputStream(byteArrayOutputStream, 64, 50); 18 | 19 | String stringBuilder = "MIME-Version: 1.0\r\n" + 20 | "From: \r\n" + 21 | "To: \r\n" + 22 | "Subject: Lost in space\r\n" + 23 | "Message-ID: <23szwa4xd5ec6rf7tgyh8j9um0kiol-tony@example.com>\r\n" + 24 | "\r\n" + 25 | "Rescue me!\r\n" + 26 | ".\r\n"; 27 | InputStream inputStream = new ByteArrayInputStream(stringBuilder.getBytes()); 28 | int intByte; 29 | while ((intByte = inputStream.read()) != -1) { 30 | slowOutputStream.write(intByte); 31 | } 32 | 33 | assertEquals(0, slowOutputStream.getTotalWait()); 34 | assertEquals(175, byteArrayOutputStream.toString().length()); 35 | } 36 | 37 | @Test 38 | void slow() throws IOException { 39 | ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); 40 | SlowOutputStream slowOutputStream = new SlowOutputStream(byteArrayOutputStream, 128, 100); 41 | 42 | String stringBuilder = "MIME-Version: 1.0\r\n" + 43 | "From: \r\n" + 44 | "To: \r\n" + 45 | "Subject: Lost in space\r\n" + 46 | "Message-ID: <23szwa4xd5ec6rf7tgyh8j9um0kiol-tony@example.com>\r\n" + 47 | "\r\n" + 48 | "Rescue me!\r\n" + 49 | ".\r\n"; 50 | InputStream inputStream = new ByteArrayInputStream(stringBuilder.getBytes()); 51 | int intByte; 52 | while ((intByte = inputStream.read()) != -1) { 53 | slowOutputStream.write(intByte); 54 | } 55 | 56 | assertEquals(100, slowOutputStream.getTotalWait()); 57 | assertEquals(175, byteArrayOutputStream.toString().length()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/resources/cases/config/dynamic/dynamic.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "/schema/case.schema.json", 3 | 4 | // How many times to try and establish a connection to remote server. First counts. 5 | retry: 2, 6 | 7 | // Delay between tries. 8 | delay: 5, 9 | 10 | // Enable TLS. 11 | tls: true, 12 | 13 | // Email envelopes. 14 | envelopes: [ 15 | { 16 | // Recipients list. 17 | rcpt: [ 18 | "robin@example.com" 19 | ], 20 | 21 | // Email to transmit defined as JSON MIME. 22 | mime: { 23 | 24 | // Main headers. 25 | headers: [ 26 | ["Subject", "Robin wrote"], 27 | ["To", "Sir Robin "], 28 | ["From", "Lady Robin "], 29 | ["X-Robin-Filename", "the.robins.eml"] 30 | ], 31 | 32 | // List of parts (no multiparts required, multiparts will be crated based on parts defined). 33 | parts: [ 34 | 35 | // Plain text part. 36 | { 37 | headers: [ 38 | ["Content-Type", "text/plain; charset=\"UTF-8\""], 39 | ["Content-Transfer-Encoding", "quoted-printable"] 40 | ], 41 | message: "Mon chéri,\ 42 | \ 43 | Please review this lovely blog post I have written about myself.\ 44 | Huge ego, right?\ 45 | \ 46 | Kisses,\ 47 | Your Robin." 48 | }, 49 | 50 | // PDF attachment part. 51 | { 52 | headers: [ 53 | ["Content-Type", "application/pdf; name=\"article.pdf\""], 54 | ["Content-Disposition", "attachment; filename=\"article.pdf\""], 55 | ["Content-Transfer-Encoding", "base64"] 56 | ], 57 | file: "src/test/resources/mime/robin.article.pdf" 58 | } 59 | ] 60 | }, 61 | 62 | // Assertions to run against the envelope. 63 | assertions: { 64 | 65 | // Protocol assertions. 66 | // Check SMTP responses match regular expressions. 67 | protocol: [ 68 | ["MAIL", "250 Sender OK"], 69 | ["RCPT", "250 Recipient OK"], 70 | ["DATA", "^250"], 71 | ["DATA", "Received OK"] 72 | ] 73 | } 74 | } 75 | ], 76 | 77 | // Assertions to run against the connection. 78 | assertions: { 79 | 80 | // Protocol assertions. 81 | // Check SMTP responses match regular expressions. 82 | protocol: [ 83 | [ "SMTP", "^220" ], 84 | [ "EHLO", "STARTTLS" ], 85 | [ "SHLO", "250 HELP" ], 86 | [ "QUIT" ] 87 | ] 88 | } 89 | } -------------------------------------------------------------------------------- /src/test/java/com/mimecast/robin/util/MagicTest.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.util; 2 | 3 | import com.mimecast.robin.smtp.session.Session; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.TimeZone; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.junit.jupiter.api.Assertions.assertNotNull; 13 | 14 | class MagicTest { 15 | 16 | @BeforeAll 17 | static void before() { 18 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 19 | } 20 | 21 | @Test 22 | void magicReplace() { 23 | Session session = new Session(); 24 | 25 | session.putMagic("port", "25"); 26 | assertEquals("25", Magic.magicReplace("{$port}", session, false)); 27 | 28 | session.putMagic("hostnames", List.of("example.com")); 29 | assertEquals("example.com", Magic.magicReplace("{$hostnames[0]}", session, false)); 30 | 31 | session.saveResults("hostnames", List.of("example.com")); 32 | assertNotNull(Magic.magicReplace("{$hostnames[?]}", session, false)); 33 | 34 | session.saveResults("host", List.of(Map.of("com", "example.com"))); 35 | assertEquals("example.com", Magic.magicReplace("{$host[0][com]}", session, false)); 36 | 37 | int offset = TimeZone.getDefault().getRawOffset(); 38 | 39 | session.putMagic("date", "20240109000000000"); 40 | assertEquals(String.valueOf(1704758400000L - offset), Magic.magicReplace("{dateToMillis$date}", session, false)); 41 | 42 | session.putMagic("milis", String.valueOf(1704758400000L - offset)); 43 | assertEquals("20240109000000000", Magic.magicReplace("{millisToDate$milis}", session, false)); 44 | 45 | session.putMagic("upper", "ABC"); 46 | assertEquals("abc", Magic.magicReplace("{toLowerCase$upper}", session, false)); 47 | 48 | session.putMagic("lower", "def"); 49 | assertEquals("DEF", Magic.magicReplace("{toUpperCase$lower}", session, false)); 50 | 51 | session.putMagic("pattern", ".*"); 52 | assertEquals("\\Q.*\\E", Magic.magicReplace("{patternQuote$pattern}", session, false)); 53 | 54 | session.putMagic("host", "example.com:8080"); 55 | assertEquals("https://example.com", Magic.magicReplace("https://{strip(:8080)$host}", session, false)); 56 | 57 | session.putMagic("host", "example.com:8080"); 58 | assertEquals("https://example.com", Magic.magicReplace("https://{replace(:8080|)$host}", session, false)); 59 | } 60 | } -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Robin MTA Tester 2 | ================ 3 | By **Vlad Marian** ** 4 | 5 | 6 | Overview 7 | -------- 8 | 9 | Robin MTA Tester is a development, debug and testing tool for MTA architects. 10 | It is powered by a highly customizable SMTP client designed to emulate the behaviour of popular email clients. 11 | A rudimentary server is also provided that is mainly used for testing the client. 12 | 13 | The primary usage is done via JSON files called test cases. 14 | Cases are client configuration files ran as Junit tests. 15 | 16 | This project can be compiled into a runnable JAR. 17 | A CLI interface is implemented with support for both client and server execution. 18 | 19 | Mimecast uses this to run smoke tests every time a new MTA snapshot is built. 20 | This helps identify bugs early before leaving the development environment. 21 | 22 | 23 | Contributions 24 | ------------- 25 | Contributions of any kind (bug fixes, new features...) are welcome! 26 | This is a development tool and as such it may not be perfect and may be lacking in some areas. 27 | 28 | Certain future functionalities are marked with TODO comments throughout the code. 29 | This however does not mean they will be given priority or ever be done. 30 | 31 | Any merge request made should align to existing coding style and naming convention. 32 | Before submitting a merge request please run a comprehensive code quality analysis (IntelliJ, SonarQube). 33 | 34 | Read more [here](contributing.md). 35 | 36 | 37 | Disclosure 38 | ---------- 39 | This project makes use of sample password as needed for testing and demonstration purposes. 40 | 41 | - notMyPassword - It's not my password. It can't be as password length and complexity not met. 42 | - 1234 - Sample used in some unit tests. 43 | - giveHerTheRing - Another sample used in unit tests and documentation. (Tony Stark / Pepper Pots easter egg) 44 | - avengers - Test keystore password that contains a single entry issued to Tony Stark. (Another easter egg) 45 | 46 | **These passwords are not in use within Mimecast production environments.** 47 | 48 | 49 | More... 50 | ------- 51 | - [Introduction](doc/introduction.md) 52 | - [CLI usage](doc/cli.md) 53 | - [Client usage](doc/client.md) 54 | - [Server configuration](doc/server.md) 55 | 56 | 57 | - [E/SMTP Cases](doc/case.md) 58 | - [HTTP/S Cases](doc/case.md) 59 | - [Magic](doc/magic.md) 60 | - [MIME](doc/mime.md) 61 | - [Plugins](doc/plugin.md) 62 | 63 | 64 | - [Contributing](contributing.md) 65 | - [Code of conduct](code_of_conduct.md) 66 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/assertion/AssertExternalGroup.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.assertion; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.regex.Pattern; 6 | 7 | /** 8 | * Assert external group. 9 | * 10 | *

Container class for pattern groups used for external logs assertion. 11 | */ 12 | @SuppressWarnings("UnusedReturnValue") 13 | public class AssertExternalGroup { 14 | 15 | /** 16 | * Compiled regex patterns. 17 | */ 18 | private List patterns = new ArrayList<>(); 19 | 20 | /** 21 | * Matched regex patterns. 22 | */ 23 | private final List matched = new ArrayList<>(); 24 | 25 | /** 26 | * Gets unmatched. 27 | * 28 | * @return Patterns list. 29 | */ 30 | public List getUnmatched() { 31 | List unmatched = new ArrayList<>(); 32 | for (Pattern pattern : patterns) { 33 | if (!matched.contains(pattern)) { 34 | unmatched.add(pattern); 35 | } 36 | } 37 | return unmatched; 38 | } 39 | 40 | /** 41 | * Gets matched. 42 | * 43 | * @return Patterns list. 44 | */ 45 | public List getMatched() { 46 | return matched; 47 | } 48 | 49 | /** 50 | * Adds unmatched. 51 | * 52 | * @param pattern Pattern. 53 | * @return Self. 54 | */ 55 | public AssertExternalGroup addMatched(Pattern pattern) { 56 | if (!matched.contains(pattern)) { 57 | matched.add(pattern); 58 | } 59 | return this; 60 | } 61 | 62 | /** 63 | * Clears matched. 64 | * 65 | * @return Self. 66 | */ 67 | public AssertExternalGroup clearMatched() { 68 | matched.clear(); 69 | return this; 70 | } 71 | 72 | /** 73 | * Gets patterns. 74 | * 75 | * @return Patterns list. 76 | */ 77 | public List getPatterns() { 78 | return patterns; 79 | } 80 | 81 | /** 82 | * Sets patterns. 83 | * 84 | * @param list Patterns list. 85 | * @return Self. 86 | */ 87 | public AssertExternalGroup setPatterns(List list) { 88 | patterns = list; 89 | return this; 90 | } 91 | 92 | /** 93 | * Has matched. 94 | * 95 | * @return Boolean. 96 | */ 97 | public Boolean hasMatched() { 98 | return matched.size() == patterns.size(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/annotation/AnnotationLoader.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.annotation; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.reflections.Reflections; 6 | 7 | import java.lang.annotation.Annotation; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.util.*; 10 | 11 | /** 12 | * Plugin annotation loader. 13 | * 14 | *

This class scans the plugin package at runtime to identify classes annotated with the @Plugin interface. 15 | *

After ordering them by priority it will instantiate each one in that order. 16 | *

Priority collision is not handled thus their order will be random. 17 | */ 18 | public abstract class AnnotationLoader { 19 | private static final Logger log = LogManager.getLogger(AnnotationLoader.class); 20 | 21 | /** 22 | * Protected constructor. 23 | */ 24 | private AnnotationLoader() { 25 | throw new IllegalStateException("Static class"); 26 | } 27 | 28 | /** 29 | * Scans and instantiates plugins found. 30 | */ 31 | @SuppressWarnings({"unchecked", "rawtypes"}) 32 | public static synchronized void load() { 33 | Reflections reflections = new Reflections("com.mimecast.robin.annotation.plugin"); 34 | Set> annotated = reflections.getTypesAnnotatedWith(Plugin.class); 35 | 36 | Map> tree = new TreeMap<>(); 37 | 38 | for (Class clazz : annotated) { 39 | Annotation[] annotations = clazz.getAnnotations(); 40 | for (Annotation annotation : annotations) { 41 | if (annotation instanceof Plugin) { 42 | tree.putIfAbsent(((Plugin) annotation).priority(), new ArrayList<>()); 43 | // Because tree map put is broken and returns null. 44 | tree.get(((Plugin) annotation).priority()).add(clazz); 45 | } 46 | } 47 | } 48 | 49 | for (Map.Entry> entry : tree.entrySet()) { 50 | for (Class clazz : entry.getValue()) { 51 | try { 52 | log.debug("Plugin: {} priority={}", clazz.getName(), entry.getKey()); 53 | clazz.getDeclaredConstructor().newInstance(); 54 | } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { 55 | log.error("Error constructing instance for class: {}", e.getMessage()); 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/extension/server/ServerStartTls.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.extension.server; 2 | 3 | import com.mimecast.robin.config.BasicConfig; 4 | import com.mimecast.robin.config.server.ScenarioConfig; 5 | import com.mimecast.robin.main.Config; 6 | import com.mimecast.robin.smtp.connection.Connection; 7 | import com.mimecast.robin.smtp.verb.Verb; 8 | 9 | import java.io.IOException; 10 | import java.util.Optional; 11 | 12 | /** 13 | * STARTLS extension processor. 14 | */ 15 | public class ServerStartTls extends ServerProcessor { 16 | 17 | /** 18 | * STARTTLS advert. 19 | * 20 | * @return Advert string. 21 | */ 22 | @Override 23 | public String getAdvert() { 24 | return Config.getServer().isStartTls() ? "STARTTLS" : ""; 25 | } 26 | 27 | /** 28 | * STARTTLS processor. 29 | * 30 | * @param connection Connection instance. 31 | * @param verb Verb instance. 32 | * @return Boolean. 33 | * @throws IOException Unable to communicate. 34 | */ 35 | @Override 36 | @SuppressWarnings("unchecked") 37 | public boolean process(Connection connection, Verb verb) throws IOException { 38 | super.process(connection, verb); 39 | 40 | boolean shakeHand = true; 41 | String handShake = "220 Ready for handshake [" + connection.getSession().getUID() + "]"; 42 | 43 | // ScenarioConfig response. 44 | Optional opt = connection.getScenario(); 45 | if (opt.isPresent() && opt.get().getStarTls() != null && !opt.get().getStarTls().isEmpty()) { 46 | BasicConfig tls = opt.get().getStarTls(); 47 | if (!tls.isEmpty()) { 48 | if (tls.hasProperty("response")) { 49 | handShake = tls.getStringProperty("response"); 50 | shakeHand = handShake.startsWith("2"); 51 | } 52 | 53 | if (tls.hasProperty("protocols")) { 54 | connection.setProtocols((String[]) tls.getListProperty("protocols").toArray(new String[0])); 55 | } 56 | 57 | if (tls.hasProperty("ciphers")) { 58 | connection.setCiphers((String[]) tls.getListProperty("ciphers").toArray(new String[0])); 59 | } 60 | } 61 | } 62 | 63 | // Shake hand. 64 | connection.write(handShake); 65 | 66 | if (shakeHand) { 67 | connection.startTLS(false); 68 | connection.getSession().setStartTls(true); 69 | connection.buildStreams(); 70 | } 71 | 72 | return true; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/smtp/session/XclientSession.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.smtp.session; 2 | 3 | import com.mimecast.robin.config.ConfigMapper; 4 | import com.mimecast.robin.config.client.CaseConfig; 5 | import com.mimecast.robin.util.Magic; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * XCLIENT session. 12 | * 13 | *

This provides access to XCLIENT session data. 14 | * 15 | * @see Postfix XCLIENT 16 | */ 17 | @SuppressWarnings("UnusedReturnValue") 18 | public class XclientSession extends Session { 19 | 20 | private Map xclient = new HashMap<>(); 21 | 22 | /** 23 | * Constructs a new XclientSession instance. 24 | */ 25 | public XclientSession() { 26 | super(); 27 | } 28 | 29 | /** 30 | * Maps CaseConfig to this session. 31 | */ 32 | @Override 33 | public void map(CaseConfig caseConfig) { 34 | new XclientConfigMapper(caseConfig).mapTo(this); 35 | } 36 | 37 | /** 38 | * Gets XCLIENT. 39 | * 40 | * @return XCLIENT parameters map. 41 | */ 42 | public Map getXclient() { 43 | return xclient; 44 | } 45 | 46 | /** 47 | * Sets XCLIENT parameters. 48 | * 49 | * @param xclient XCLIENT parameters map. 50 | * @return Self. 51 | */ 52 | public XclientSession setXclient(Map xclient) { 53 | this.xclient = xclient; 54 | return this; 55 | } 56 | 57 | /** 58 | * Mapper for CaseConfig to Session with XCLIENT. 59 | */ 60 | public static class XclientConfigMapper extends ConfigMapper { 61 | 62 | /** 63 | * Mapper for CaseConfig to Session. 64 | * 65 | * @param config CaseConfig instance. 66 | */ 67 | XclientConfigMapper(CaseConfig config) { 68 | super(config); 69 | } 70 | 71 | /** 72 | * Map configuration to given Session. 73 | * 74 | * @param session Session instance. 75 | */ 76 | @SuppressWarnings("unchecked") 77 | @Override 78 | public void mapTo(Session session) { 79 | super.mapTo(session); 80 | 81 | Map client = config.getMapProperty("xclient"); 82 | if (client != null) { 83 | for (Map.Entry pair : client.entrySet()) { 84 | ((XclientSession) session).getXclient().put(pair.getKey(), Magic.magicReplace(pair.getValue(), session)); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/mimecast/robin/config/client/RouteConfig.java: -------------------------------------------------------------------------------- 1 | package com.mimecast.robin.config.client; 2 | 3 | import com.mimecast.robin.config.ConfigFoundation; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * Client route configuration container. 10 | * 11 | *

This is a container for routes defined in the client configuration. 12 | *

One instance will be made for every route defined. 13 | *

This can be used to configure MX, PORT and SMTP AUTH authentication in cases. 14 | * 15 | * @see ConfigFoundation 16 | */ 17 | @SuppressWarnings("unchecked") 18 | public class RouteConfig extends ConfigFoundation { 19 | 20 | /** 21 | * Constructs a new RouteConfig instance with given map. 22 | * 23 | * @param map Properties map. 24 | */ 25 | @SuppressWarnings("rawtypes") 26 | public RouteConfig(Map map) { 27 | super(map); 28 | } 29 | 30 | /** 31 | * Gets route name. 32 | * 33 | * @return Name string. 34 | */ 35 | public String getName() { 36 | return getStringProperty("name"); 37 | } 38 | 39 | /** 40 | * Gets MX. 41 | * 42 | * @return MX list of string. 43 | */ 44 | public List getMx() { 45 | return getListProperty("mx"); 46 | } 47 | 48 | /** 49 | * Gets port. 50 | * 51 | * @return Port number. 52 | */ 53 | public int getPort() { 54 | return Math.toIntExact(getLongProperty("port")); 55 | } 56 | 57 | /** 58 | * Is authentication enabled. 59 | * 60 | * @return Boolean. 61 | */ 62 | public boolean isAuth() { 63 | return getBooleanProperty("auth"); 64 | } 65 | 66 | /** 67 | * Is AUTH LOGIN combined username and password login enabled. 68 | * 69 | * @return Boolean. 70 | */ 71 | public boolean isAuthLoginCombined() { 72 | return getBooleanProperty("authLoginCombined"); 73 | } 74 | 75 | /** 76 | * Is AUTH LOGIN retry enabled. 77 | * 78 | * @return Boolean. 79 | */ 80 | public boolean isAuthLoginRetry() { 81 | return getBooleanProperty("authLoginRetry"); 82 | } 83 | 84 | /** 85 | * Gets the username. 86 | * 87 | * @return Username string. 88 | */ 89 | public String getUser() { 90 | return getStringProperty("user"); 91 | } 92 | 93 | /** 94 | * Gets the password. 95 | * 96 | * @return password string. 97 | */ 98 | public String getPass() { 99 | return getStringProperty("pass"); 100 | } 101 | } 102 | --------------------------------------------------------------------------------