Simple API to fake an SMTP server for Unit testing (and more).
This API is inspired from dumbster with the following improvements:
- Dynamic port lookup
- Support of MIME messages with attachments
- Access to SMTP exchanges
- Improved multi-threading support
- Up-to-date dependencies
- Extended tests
- Numerous bugfixes
Here is the compatibility map of this API:
Version | JDK | Package |
---|---|---|
<= 1.2.2 | JDK 8 and upwards | javax |
>= 2.0.0 | JDK 11 and upwards | jakarta |
Use the following dependency in your pom.xml
:
<dependency>
<groupId>ch.astorm</groupId>
<artifactId>smtp4j</artifactId>
<version>3.1.3</version>
</dependency>
Here is a quick example of the usage of this API that shows an oversight on how it can be used:
/* SMTP server is started on port 1025 */
SmtpServerBuilder builder = new SmtpServerBuilder();
try(SmtpServer server = builder.withPort(1025).start()) {
/* create and send an SMTP message to smtp4j */
MimeMessageBuilder messageBuilder = new MimeMessageBuilder(server);
messageBuilder.from("source@smtp4j.local").
to("target1@smtp4j.local", "John Doe <john@smtp4j.local>").
cc("target3@smtp4j.local").
subject("Hello, world !").
body("Hello\r\nGreetings from smtp4j !\r\n\r\nBye.").
attachment("data.txt", new File("someAttachment.txt"));
messageBuilder.send(); //uses Transport.send(...)
/* retrieve the sent message in smtp4j */
List<SmtpMessage> messages = server.readReceivedMessages();
assertEquals(1, messages.size());
/* analyze the content of the message */
SmtpMessage receivedMessage = messages.get(0);
String from = receivedMessage.getFrom();
String subject = receivedMessage.getSubject();
String body = receivedMessage.getBody();
Date sentDate = receivedMessage.getSentDate();
List<String> recipientsTo = receivedMessage.getRecipients(RecipientType.TO);
List<SmtpAttachment> attachments = receivedMessage.getAttachments();
}
Here are some usages about specific parts of the API. For more examples, look in the tests.
Basically, it is recommended to always use the SmtpServerBuilder
class to instanciate a new SmtpServer
instance.
The SmtpServer
can be configured either to listen to a specific port or to find
dynamically a free port to listen to.
A static port can simply be specified like this:
SmtpServerBuilder builder = new SmtpServerBuilder();
try(SmtpServer server = builder.withPort(1025).start()) {
//server is listening on port 1025
}
On the other hand, if no port is defined, the SmtpServer
will find a free port
to listen to when it is started:
SmtpServerBuilder builder = new SmtpServerBuilder();
try(SmtpServer server = builder.start()) {
int port = server.getPort(); //port listen by the server
}
When the port is not specified, the SmtpServer
will try to open a server socket on
the default SMTP port (25). If the latter fails (most probably), it will look up for
a free port starting from 1024.
Note that generally ports under 1024 can only be used with root privileges.
The SmtpServer
provides some utilities that let you create a new Session
for message creation and sending. The latter will be automatically connected
to the running server (on localhost):
SmtpServerBuilder builder = new SmtpServerBuilder();
try(SmtpServer server = builder.start()) {
Session session = server.createSession();
//use the session to create a MimeMessage
}
The received messages can be accessed directly through the SmtpServer
:
List<SmtpMessage> receivedMessages = smtpServer.readReceivedMessages();
This method will clear the server's storage cache, hence another invocation of the same method will yield an empty list until a new message has been received.
WARNING: Do not use this method concurrently SmtpServer.receivedMessageReader()
because of race conditions.
Since smtp4j is multithreaded, it may happen that there is not enough time to process the message before reading it. This can be easily circumvented by defining a delay to wait when there is no message yet received.
List<SmtpMessage> receivedMessages = smtpServer.readReceivedMessages(2, TimeUnit.SECONDS);
A simple API is provided to wait and loop over the received messages:
try(SmtpMessageReader reader = smtpServer.receivedMessageReader()) {
SmtpMessage smtpMessage = reader.readMessage(); //blocks until the first message is available
while(smtpMessage!=null) {
/* ... */
//blocks until the next message is available
smtpMessage = reader.readMessage();
}
}
When the SmtpServer
is closed, the reader will yield null
.
WARNING: Creating multiple instances of SmtpMessageReader
will cause a race condition between
them and hence, a message will be received only by one of the readers. For the same reasons, do not use
SmtpServer.readReceivedMessages()
when using a reader.
The API of SmtpMessage
provides an easy access to all the basic fields:
String from = smtpMessage.getFrom();
String subject = smtpMessage.getSubject();
String body = smtpMessage.getBody();
Date sentDate = smtpMessage.getSentDate();
List<String> recipientsTo = smtpMessage.getRecipients(RecipientType.TO);
List<SmtpAttachment> attachments = smtpMessage.getAttachments();
It is also possible to retrieve some data directly issued from the underlying SMTP exchanges
between the server and the client. Those data might differ (even be missing) from the resulting
MimeMessage
:
String sourceFrom = smtpMessage.getSourceFrom();
List<String> sourceRecipients = smtpMessage.getSourceRecipients();
Typically, the BCC
recipients will be absent from the MimeMessage
but will
be available through the getSourceRecipients()
method.
If more specific data has to be accessed, it is possible to retrieve the raw data with the following methods:
MimeMessage mimeMessage = smtpMessage.getMimeMessage();
String mimeMessageStr = smtpMessage.getRawMimeContent();
One can access direclty the exchanges between the sender and smtp4j.
List<SmtpExchange> exchanges = smtpMessage.getSmtpExchanges();
List<String> receivedData = exchanges.get(0).getReceivedData();
String repliedData = exchanges.get(0).getRepliedData();
Multipart messages might contain many attachments that are accessibles with the getAttachments()
method of the SmtpMessage
. Here is an insight of the SmtpAttachment
API:
String filename = attachment.getFilename(); // myFile.pdf
String contentType = attachment.getContentType(); // application/pdf; charset=us-ascii; name=myFile.pdf
The content of an attachment can be read with the following piece of code:
try(InputStream is = attachment.openStream()) {
//...
}
The API includes a utility class to build SMTP messages from the client side that can easily be sent to smtp4j. The MimeMessageBuilder class provides easy-to-use methods to create a Multipart MIME message:
/* SMTP server is started on port 1025 */
SmtpServerBuilder builder = new SmtpServerBuilder();
try(SmtpServer server = builder.withPort(1025).start()) {
/* create and send an SMTP message */
MimeMessageBuilder messageBuilder = new MimeMessageBuilder(server).
from("source@smtp4j.local").
//use either multiple arguments
to("to1@smtp4j.local", "Igôr <to2@smtp4.local>").
//or a comma-separated list
to("to3@smtp4j.local, My Friend <to4@smtp4j.local>").
//or call the method multiple times
cc("cc1@smtp4j.local").
cc("cc2@smtp4j.local").
bcc("bcc@smtp4j.local").
at("31.12.2020 23:59:59").
subject("Hello, world !").
body("Hello\r\nGreetings from smtp4j !\r\n\r\nBye.").
attachment(new File("file.pdf"));
//build the message and send it to smtp4j
messageBuilder.send();
//process the received message
//...
}
It is also possible to use this builder in a production application by using the
dedicated Session
constructor:
MimeMessageBuilder messageBuilder = new MimeMessageBuilder(session);
It is possible to listen to SmtpServer
events by implementing a
SmtpServerListener.
SmtpServerListener myListener = new SmtpServerListener() {
public void notifyStart(SmtpServer server) { System.out.println("Server has been started"); }
public void notifyClose(SmtpServer server) { System.out.println("Server has been closed"); }
public void notifyMessage(SmtpServer server, SmtpMessage message) { System.out.println("Message has been received"); }
}
mySmtpServer.addListener(myListener);
It is possible to trigger message refusal through the API. The exception message will be received on the SMTP client side.
SmtpServerBuilder builder = new SmtpServerBuilder();
try(SmtpServer server = builder.start()) {
server.addListener((srv, msg) -> {
throw new IllegalStateException("Message refused");
});
try {
new MimeMessageBuilder(server).
to("test@astorm.ch").
subject("Test").
body("Hello!").
send();
} catch(MessagingException e) {
String message = e.getMessage(); //554 Message refused
}
}
By default, once a SmtpMessage
has been received, it will be stored in a default
DefaultSmtpMessageHandler instance,
which can be directly accessed like this:
SmtpMessageHandler messageHandler = smtpServer.getMessageHandler();
DefaultSmtpMessageHandler defaultMessageHandler = (DefaultSmtpMessageHandler)messageHandler;
It is possible to override this default behavior with your custom handler with the following piece of code:
SmtpMessageHandler myCustomHandler = new CustomSmtpMessageHandler();
SmtpServerBuilder builder = new SmtpServerBuilder();
try(SmtpServer server = builder.withMessageHandler(myCustomHandler).start()) {
//...
}
For now, it is not possible to communicate securely (SMTPS or SSL/TLS) through this API. If the client tries to initiate a secure channel, the connection will be closed.
This project is completely developed during my spare time.
Since I'm a big fan of cryptocurrencies and especially Cardano (ADA), you can send me some coins at the address below (check it here):
addr1q9sgms4vc038nq7hu4499yeszy0rsq3hjeu2k9wraksle8arg0n953hlsrtdzpfnxxw996l4t6qu5xsx8cmmakjcqhksaqpj66