├── .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 | *
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 | *
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 | *
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 | *
The Log4j2 XML filename can be configured via properties.json or a system property called log4j2.
9 | * Example:
10 | *
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 |
--------------------------------------------------------------------------------