Using Mailpit with Spring Boot
- June 6th, 2024
- 8 minute read
- Spring Spring boot SMTP Testcontainers Mailpit
What is Mailpit?
If you’re developing an application that needs to send emails, you probably don’t want to send them to your users while still in development. However, completely disabling email sending can make it difficult to test your application. Mailpit is a free and open source SMTP testing tool, which allows you to test email sending without actually sending emails. In this tutorial, we will learn how to use Mailpit with Spring Boot with Docker Compose and Testcontainers.
Setting up a Spring Boot application
The first thing we need to do is to set up a Spring Boot application.
You can create a new Spring Boot application using the Spring Initializr.
For this tutorial, we will create a simple Spring Boot application using the spring-boot-starter-mail
dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
To send an email, you can use the JavaMailSender
interface:
public void send() {
var mimeMessage = mailSender.createMimeMessage();
var message = new MimeMessageHelper(mimeMessage);
var content = """
<html>
<h1>This is a test email</h1>
<p>Please do not respond to this email.</p>
</html>
""";
message.setFrom("noreply@example.org");
message.setTo("me@example.org");
message.setSubject("Test email");
message.setText(content, true);
mailSender.send(message.getMimeMessage());
}
Option 1: Setting up Mailpit with Docker Compose
In this tutorial I’ll cover a few options to set up Mailpit.
The first option is to use Docker Compose.
To set up Mailpit with Docker Compose, create a docker-compose.yml
file with the following content:
services:
mailpit:
image: axllent/mailpit:v1.15
ports:
- 1025:1025
- 8025:8025
Mailpit exposes two ports: 1025
for SMTP and 8025
for the web interface.
To start Mailpit, run the following command:
docker-compose up
Alternatively, you can add the following dependency to your pom.xml
file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
</dependency>
This dependency will allow you to start Mailpit automatically when you run your Spring Boot application.
All we need to do now is to configure the following properties in the application.properties
file:
spring.mail.host=localhost
spring.mail.port=1025
Option 2: Setting up Mailpit with Testcontainers in development
Since Spring Boot 3.1, you can use Testcontainers during development.
To do this, add the following dependency to your pom.xml
file:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
After that, create another main class in your test sources (src/test/java
):
@TestConfiguration(proxyBeanMethods = false)
public class TestSpringBootMailpitApplication {
public static void main(String[] args) {
SpringApplication
.from(SpringBootMailpitApplication::main) // Your main class
.with(TestSpringBootMailpitApplication.class)
.run(args);
}
}
Now you have two options to launch your application: you can run the SpringBootMailpitApplication
to regularly run your application, or you can run the TestSpringBootMailpitApplication
to run your application with Testcontainers.
Within TestSpringBootMailpitApplication
, you can now define the Mailpit testcontainer:
@Bean
GenericContainer<?> mailpitContainer() {
return new GenericContainer<>("axllent/mailpit:v1.15")
.withExposedPorts(1025, 8025)
.waitingFor(Wait.forLogMessage(".*accessible via.*", 1));
}
This code will start a Mailpit container with the axllent/mailpit:v1.15
image.
The withExposedPorts
method exposes the SMTP and web interface ports, and the waitingFor
method waits for the log message “accessible via” to appear in the logs.
The benefit of using Testcontainers is that Spring can automatically derive connection details from the container, so oftene you don’t need to configure anything in your application.properties
file.
For example, if you create a Postgres testcontainer, Spring will automatically configure the spring.datasource.*
properties.
This is thanks to the @ServiceConnection
annotation.
Sadly, for Mailpit, you still need to configure the spring.mail.*
properties.
However, since Testcontainers runs the container on a dynamic port, you need to create a DynamicPropertyRegistrar
bean as described within the documentation.
To do this, add the following bean:
@Bean
public DynamicPropertyRegistrar mailpitRegistrar(GenericContainer<?> mailpitContainer) {
return registry -> {
registry.add("spring.mail.host", mailpitContainer::getHost);
registry.add("spring.mail.port", mailpitContainer::getFirstMappedPort);
};
}
Important: The
DynamicPropertyRegistrar
is new since Spring Boot 3.4. Before that, you had to inject theDynamicPropertyRegistry
and add the properties to it. For example:@Bean GenericContainer<?> mailpitContainer(DynamicPropertyRegistry properties) { var container = new GenericContainer<>("axllent/mailpit:v1.15") .withExposedPorts(1025, 8025) .waitingFor(Wait.forLogMessage(".*accessible via.*", 1)); properties.add("spring.mail.host", container::getHost); properties.add("spring.mail.port", container::getFirstMappedPort); return container; }
Another thing I noticed is that the container-related properties are not immediately available.
Due to this, Spring’s MailSenderAutoConfiguration
isn’t bootstrapped since it requires the spring.mail.host
property to be present.
To work around this, you can add a dummy property to the application.properties
file within src/test/resources
:
spring.mail.host=dummy
Due to the fact that Testcontainers binds the container ports to a random port on the host, it can be a bit tricky to find out how to open the Mailpit web interface. To solve that problem, you can map another dynamic property containing the mapped port for port 8025:
@Bean
public DynamicPropertyRegistrar mailpitRegistrar(GenericContainer<?> mailpitContainer) {
return registry -> {
registry.add("spring.mail.host", mailpitContainer::getHost);
registry.add("spring.mail.port", mailpitContainer::getFirstMappedPort);
registry.add("mailpit.web.port", () -> mailpitContainer.getMappedPort(8025));
};
}
After that, you can write an ApplicationRunner
to log the URL:
@Bean
public ApplicationRunner logMailpitWebPort(@Value("${spring.mail.host}") String host, @Value("${mailpit.web.port}") int port) {
Logger log = LoggerFactory.getLogger(getClass());
return args -> log.info("Mailpit accessible through http://{}:{}", host, port);
}
As soon as you run your Spring boot application now through the TestSpringBootMailpitApplication
class, you should see a log message like this:
Demo
Now that you configured Mailpit, you can open the web interface either at http://localhost:8025 if you’re using Docker Compose or at the URL that was logged if you’re using Testcontainers. The result should be something like this:
As soon as you open one of the emails, you get to see a screen like this:
Mailpit contains some nice features, such as viewing the email on different devices, and check whether the HTML markup you used is properly supported by various email clients.
Extra: Using Mailpit for integration testing
In addition to helping you test your email sending functionality, Mailpit can also be used for integration testing. For example, you can use Mailpit to verify that your application sends the correct emails under certain conditions. To verify this, we can use Mailpit’s REST API.
Before we can interact with the Mailpit REST API, we need to add the following dependencies:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>test</scope>
</dependency>
The junit-jupiter
dependency is required to use Testcontainers with JUnit 5.
The spring-web
dependency is required to use Spring’s RestClient
.
If your application is already a web application, you don’t need to include this spring-web
dependency.
After that, we can create our integration test:
@Testcontainers
@SpringBootTest(classes = {
MailService.class,
MailSenderAutoConfiguration.class,
RestClientAutoConfiguration.class,
MailServiceTest.Configuration.class
})
class MailServiceTest {
// TODO: implement
@TestConfiguration
static class Configuration {
// TODO: implement
}
}
In this test, we bootstrap a Spring Boot application with our business logic (MailService
) and include the necessary autoconfigurations to set up the JavaMailSender
and RestClient.Builder
beans.
We also define an inner Configuration
class to set up the Mailpit REST client in the future.
The first step we need to take is to set up the testcontainer within the integration test:
@Container
static GenericContainer<?> mailpitContainer = new GenericContainer<>("axllent/mailpit:v1.15")
.withExposedPorts(1025, 8025)
.waitingFor(Wait.forLogMessage(".*accessible via.*", 1));
@DynamicPropertySource
static void configureMail(DynamicPropertyRegistry registry) {
registry.add("spring.mail.host", mailpitContainer::getHost);
registry.add("spring.mail.port", mailpitContainer::getFirstMappedPort);
registry.add("mailpit.web.port", () -> mailpitContainer.getMappedPort(8025));
}
Like before, we set up a GenericContainer
for the axllent/mailpit:v1.15
image.
We also set up the necessary properties using the DynamicPropertyRegistry
.
The main difference is that we now use the @DynamicPropertySource
annotation to set up the properties.
Now that we have set up the Mailpit testcontainer, we can create a MailpitClient
class to interact with the Mailpit REST API:
public class MailpitClient {
private final RestClient restClient;
public MailpitClient(RestClient restClient) {
this.restClient = restClient;
}
public ObjectNode findFirstMessage() {
ObjectNode listNode = listAllMessages();
assertThat(listNode).isNotNull();
var id = listNode.get("messages").get(0).get("ID").asText();
ObjectNode messageNode = findMessage(id);
assertThat(messageNode).isNotNull();
return messageNode;
}
public ObjectNode listAllMessages() {
return restClient
.get()
.uri(builder -> builder.pathSegment("messages").build())
.retrieve()
.body(ObjectNode.class);
}
public ObjectNode findMessage(String messageId) {
return restClient
.get()
.uri(builder -> builder
.pathSegment("message", messageId)
.build())
.retrieve()
.body(ObjectNode.class);
}
public void deleteAllMessages() {
restClient
.delete()
.uri(builder -> builder.pathSegment("messages").build())
.retrieve()
.toBodilessEntity();
}
}
In this class, we use Spring’s RestClient
to interact with Mailpit’s REST API.
We’ll provide methods for listing all messages, finding a specific message, and deleting all messages.
To bootstrap the MailpitClient
, we need to set up the RestClient
bean within the Configuration
class:
@TestConfiguration
static class Configuration {
@Bean
RestClient mailpitRestClient(RestClient.Builder builder, @Value("${spring.mail.host}") String host, @Value("${mailpit.web.port}") int port) {
return builder
.baseUrl("http://" + host + ":" + port + "/api/v1")
.build();
}
@Bean
MailpitClient mailpitClient(RestClient mailpitRestClient) {
return new MailpitClient(mailpitRestClient);
}
}
In this class, we set up the RestClient
bean using the RestClient.Builder
and the properties we set up earlier.
Now we can set up our integration test by injecting both the MailService
and MailpitClient
:
@Autowired
private MailService service;
@Autowired
private MailpitClient mailpitClient;
And finally, we can write our integration test:
@Test
void send() throws MessagingException {
service.send();
ObjectNode result = mailpitClient.findFirstMessage();
assertSoftly(softly -> {
softly.assertThat(result.get("From").get("Address").asText()).isEqualTo("noreply@example.org");
softly.assertThat(result.get("Subject").asText()).isEqualTo("Test email");
softly.assertThat(result.get("To").get(0).get("Address").asText()).isEqualTo("me@example.org");
softly.assertThat(result.get("Text").asText()).isEqualTo("""
********************
This is a test email
********************
Please do not respond to this email.""");
});
}
In this test, we send an email using the MailService
and then use the MailpitClient
to find the first message.
We then use AssertJ’s assertSoftly
to verify that the email contains the proper content.
We can do this by using the Text
field from the Mailpit API, which contains the email content mapped as a text.
If you want to write more integration tests, don’t forget to delete all messages after each test:
@AfterEach
void tearDown() {
mailpitClient.deleteAllMessages();
}
Conclusion
In this tutorial, we learned how to use Mailpit with Spring Boot to send emails. We set up Mailpit using Docker Compose and Testcontainers, and we used Mailpit for integration testing. Mailpit is a great tool for testing email sending functionality without actually sending emails.
For integration testing, Mailpit might be a bit overkill.
This is because you will be testing Spring’s JavaMailSender
implementation, which is already well-tested.
However, if you want to verify that your application sends the correct emails under certain conditions, Mailpit can be a great tool.
As always, the code for this tutorial is available on GitHub.