Search results
Sending Emails with Spring Boot and Thymeleaf
1. OVERVIEW
Your team used Java and Spring Boot to implement Logistics functionality, or a Hotel reservation system, or an e-commerce Shopping Cart, you name it.
Now Business folks would like to receive emails with daily shipment reports, or weekly bookings, or abandoned shopping carts.
Business people would like to get emails with consolidated data to take decisions based on these reports.
Spring Framework, specifically Spring Boot includes seamless integration with template engines to send HTML and/or text emails.
This blog post covers how to send HTML and text emails using Spring Boot and Thymeleaf template engine.
Sending HTML and text emails with Spring Boot and Thymeleaf
2. MAVEN DEPENDENCIES
This blog post uses Java 11
and Spring Boot 2.7.15
. Most likely the code discussed here will also work with other versions of Java and Spring Boot.
Let’s continue with the Maven dependencies:
pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
spring-boot-starter-mail
provides JavaMailSender and related beans autoconfiguration; while spring-boot-starter-thymeleaf
provides Thymeleaf SpringTemplateEngine-related beans autoconfiguration.
These are the only dependencies you need to start sending emails using Spring Boot and Thymeleaf.
3. EXECUTION FLOW
Let’s start with a sequence diagram for the Send sales report email functionality:
Send sales report email Sequence Diagram
I won’t cover the red rectangle-annotated objects and messages from the sequence diagram.
But it’s worth mentioning that you might retrieve reports, interested parties to send emails to, etc. from relational databases, and/or a NoSQL data store like Cosmos DB in Spring Boot Enterprise Applications.
How to write dynamic SQL queries with Spring Data JPA and EntityManager.
Or
How to write dynamic Cosmos DB queries with Spring Data Cosmos and ReactiveCosmosTemplate.
3.1. The REST Controller
Let’s now implement this execution flow starting with a RESTful endpoint to send sales report emails:
SalesReportController.java
:
@RestController
@RequestMapping(value = "/api/sales-reports", produces = MediaType.APPLICATION_JSON_VALUE)
public class SalesReportController {
private final ReportingService reportingService;
@PostMapping(path = "", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> sendSalesReportEmails(
@RequestParam(value = "reportDate", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate reportDate) {
this.reportingService.sendSalesReport((reportDate != null) ? reportDate : LocalDate.now());
return ResponseEntity.noContent().build();
}
You could include request parameter validation, but let’s skip it to keep this blog post simple.
If the POST
request doesn’t include the reportDate parameter, the method will use the current local date.
The relevant piece here is the REST controller executing the ReportingService’s sendSalesReport() method to send the sales report email for a given date, discussed next.
3.2. The Reporting Service
DefaultReportingService.java
:
@Service
public class DefaultReportingService implements ReportingService {
private final MessagingService messagingService;
// ...
@Override
public void sendSalesReport(LocalDate reportDate) {
// salesReport, ... could be retrieved/calculated from Repository-based interactions, data aggregation, etc.
// Let's hard-code it for simplicity
Map<String, BigDecimal> salesByCountry = this.retrieveSalesByCountry();
SalesReport salesReport = SalesReport.builder()
.reportDate(reportDate)
.salesByCountry(salesByCountry)
.build();
// ...
// Sends sales report notification
this.messagingService.sendSalesReportNotification(salesReport, recipients);
}
// ...
and
SalesReport.java
:
public class SalesReport {
private LocalDate reportDate;
private Map<String, BigDecimal> salesByCountry;
}
A simple sendSalesReport() method implementation that:
- Builds a SalesReport domain entity, hard-coded for the purposes of this blog post.
- Delegates to the MessagingService class to send SalesReport notifications to the intended recipients.
The ReportingService retrieves, aggregates, and associates sales data.
It doesn’t know, and should’t care which notification type the MessagingService will send. It’s not its concern. It could be SMS and/or Email notifications.
Those details might be configured, or come from a profile stored in a RDBMS, LDAP, MongoDB, etc..
Because we didn’t retrieve SalesReport data from a relational database, we didn’t need to use @Spring’s @Transactional annotation. You might have to do it in your Spring Boot Enterprise Applications.
How would you handle database transactions while sending emails, or publishing JMS messages, or sending RESTful or SOAP requests to 3rd party systems?
Stay tuned, I might cover how to handle relational database transactions, and RESTful API requests in a future blog post.
3.2. The Messaging Service
DefaultMessagingService.java
:
@Service
public class DefaultMessagingService implements MessagingService {
private final EmailRepository emailRepository;
private final EmailSender emailService;
// ...
@Override
public void sendSalesReportNotification(SalesReport salesReport, List<String> recipients) {
Email salesReportEmail = this.emailRepository.create(salesReport, recipients);
this.emailService.send(salesReportEmail);
}
}
and
Email.java
:
public class Email {
private String from;
private List<String> recipients;
private String subject;
private String html;
private String text;
private Map<String, Resource> inlines;
private List<File> attachments;
}
Another simple sendSalesReportNotification() method implementation that:
- Instanciates an Email domain entity through the EmailRepository.
- Delegates sending an email to the Email Sender class.
You could also use the MessagingService class to send profiles activation emails, reset password emails, SMS, etc..
It’s independent of the application’s business logic.
It doesn’t and shouldn’t know about Thymeleaf to process the HTML and text templates, or JavaMailSender to send emails using Java.
That’s abstracted out to their corresponding classes. An example of the Single Responsibility from the SOLID principles. They are covered in the next two sections.
4. CREATE EMAILS USING Thymeleaf
Next, let’s see how to create HTML and text emails using Thymeleaf.
ThymeleafEmailRepository.java
:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
@Repository
public class ThymeleafEmailRepository implements EmailRepository {
private final ISpringTemplateEngine emailTemplateEngine;
@Value("classpath:/email-templates/Asimio-Tech-Logo.png")
private Resource companyLogo;
// ...
@Override
public Email create(SalesReport salesReport, List<String> recipients) {
String subject = String.format("Sales Report for %s", DATE_FORMMATER_yyyy_MM_dd.format(salesReport.getReportDate()));
BigDecimal totalSales = salesReport.getSalesByCountry().values().stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
Context ctx = new Context(Locale.US);
ctx.setVariable("pageTitle", subject);
ctx.setVariable("name", "Orlando");
ctx.setVariable("reportDate", salesReport.getReportDate());
ctx.setVariable("salesByCountry", salesReport.getSalesByCountry());
ctx.setVariable("totalSales", totalSales);
ctx.setVariable("imageCompanyLogo", "imageCompanyLogo");
String htmlContent = this.emailTemplateEngine.process("sales-report/template.html", ctx);
String textContent = this.emailTemplateEngine.process("sales-report/template.txt", ctx);
Map<String, Resource> embeddedResources = new HashMap<>();
embeddedResources.put("imageCompanyLogo", this.companyLogo);
Email result = Email.builder()
.from(this.from)
.recipients(recipients)
.subject(subject)
.html(htmlContent)
.text(textContent)
.inlines(embeddedResources)
.build();
return result;
}
}
This is a Repository-like class that creates Email objects using Thymeleaf.
The emailTemplateEngine
attribute is a Thymeleaf-specific SpringTemplateEngine that processes both, HTML and text Thymeleaf templates.
The relevant part is the create() method used by the MessagingService.
- Uses the method arguments to create a ThymeLeaf Context object for Thymeleaf to replace placeholders in the HTML and text templates.
- Builds the Email resulting object with all the information needed to send HTML and text emails to interested recipients.
4.1. Thymeleaf EMAIL TEMPLATES
This is a simple, table-based Thymeleaf HTML template.
src/main/resources/email-templates/sales-report/template.html
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="${pageTitle}">Template Sales Report Title</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<p>Hello <span th:text="${name}"></span>,</p>
<p>Here is the Sales Report for <span th:text="${#temporals.format(reportDate, 'yyyy-MM-dd')}">2023-09-20</span>:</p>
<table style="border:solid 3px; border-color:#e4701e; border-radius:10px;">
<caption style="font-size:14px; font-weight:bold;">SALES BY COUNTRY</caption>
<thead>
<tr>
<th>Country</th>
<th>Sales amount</th>
</tr>
</thead>
<tbody>
<tr th:each="sales, status : ${salesByCountry}" th:style="${status.odd} ? 'background: #f0f0f2;' : 'background: #ffffff;'">
<td th:text="${sales.key}" />
<td style="text-align:center;" th:text="${#numbers.formatCurrency(sales.value)}" />
</tr>
<tr style="background:#dddddd;">
<td colspan="2" style="text-align:right; font-weight:bold;">Total: <span th:text="${#numbers.formatCurrency(totalSales)}"></span></td>
</tr>
</tbody>
</table>
<p>
<img th:src="|cid:${imageCompanyLogo}|" />
</p>
<!-- ... -->
</body>
</html>
A couple of things to highlight from this Thymeleaf HTML template:
th:each="sales, status : ${salesByCountry}
loops through a Java Map to output rows with country and sales data, alternating background color like:
<img th:src="|cid:${imageCompanyLogo}|" />
references the imageCompanyLogo added to the Thymeleaf Context in EmailRepository line 22.
Then EmailSender embeds resources like images to the MimeMessage in lines 30 through 35.
You could also have Thymeleaf text templates in case recipients’ email clients don’t support HTML, or you also plan to send text-based notifications like SMS messages.
4.2. Thymeleaf EMAIL CONFIGURATION
Let’s now configure our Spring Boot application to use Thymeleaf.
EmailConfig.java
:
@Configuration
public class EmailConfig {
@Bean
public ISpringTemplateEngine emailTemplateEngine() {
final SpringTemplateEngine result = new SpringTemplateEngine();
result.addDialect(new Java8TimeDialect());
// Text email resolver
result.addTemplateResolver(this.textTemplateResolver());
// Html email resolver
result.addTemplateResolver(this.htmlTemplateResolver());
return result;
}
@Bean
public ITemplateResolver htmlTemplateResolver() {
ClassLoaderTemplateResolver result = new ClassLoaderTemplateResolver();
result.setPrefix("email-templates/");
result.setSuffix(".html");
result.setTemplateMode(TemplateMode.HTML);
result.setCharacterEncoding("UTF-8");
return result;
}
// ...
}
-
Java8TimeDialect provides Thymeleaf support for LocalDate, LocalDateTime, LocalTime, etc. through the
temporals
Thymeleaf variable.
See<span th:text="${#temporals.format(reportDate, 'yyyy-MM-dd')}">
in the previous HTML template.
Thymeleaf uses thetemporals
variable with the Context’s locale set in the EmailRepository line 16 to format numbers accordingly. -
ClassLoaderTemplateResolver locates the Thymeleaf templates in the classpath. You would keep them in
src/main/resources
.
Whenever you reference a template name, ClassLoaderTemplateResolver will append the prefix to locate it. It might also do the same with the suffix with some exceptions.
See for instance thesales-report/template.html
reference in EmailRepository line 24. It resolves to classpath:/email-templates/sales-report/template.html.
You would also implement a textTemplateResolver() method if you would like to send text notifications like SMS messages.
5. SEND EMAILS USING JavaMailSender
Before implementing the service class to sends emails, you would first need to set spring.mail
properties for proper JavaMailSender configuration.
5.1. JavaMailSender CONFIGURATION
Spring Boot autoconfigures the JavaMailSender bean to send emails using these properties:
application.yml
:
spring:
mail:
host: smtp.gmail.com
port: 587
username: "<your email>@gmail.com"
password: "<your password>"
properties:
mail:
smtp:
auth: true
starttls:
required: true
Allow less secure apps
option found at https://myaccount.google.com/lesssecureapps if you plan to send emails using Google Gmail.
If the app still doesn’t send emails after turning on the
Allow less secure apps
, you would need to enable 2FA
via https://myaccount.google.com/security, add an app password via https://myaccount.google.com/apppasswords, and update spring.mail.password
.
And finally, let’s implement the service class to send emails:
DefaultEmailSender.java
:
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Service
public class DefaultEmailSender implements EmailSender {
private final JavaMailSender mailSender;
@Override
public void sendEmail(Email email) {
MimeMessage message = this.mailSender.createMimeMessage();
try {
// MULTIPART_MODE_MIXED to support embedding/inlining resources
MimeMessageHelper messageHelper = new MimeMessageHelper(
message,
MimeMessageHelper.MULTIPART_MODE_MIXED,
"UTF-8"
);
// Set attributes
messageHelper.setFrom(email.getFrom());
messageHelper.setTo(email.getRecipients().toArray(new String[email.getRecipients().size()]));
messageHelper.setSubject(email.getSubject());
// Set plain and/or html content
if (email.getHtml() == null) {
messageHelper.setText(email.getText());
} else {
messageHelper.setText(email.getText(), email.getHtml());
}
// Embedded resources
if (!CollectionUtils.isEmpty(email.getInlines())) {
email.getInlines().forEach((contentId, source) -> {
try {
Path path = Path.of(source.getFilename());
String contentType = Files.probeContentType(path);
messageHelper.addInline(contentId, source, contentType);
} catch (MessagingException | IOException ex) {
log.warn("Logging and rethrowing messaging exception", ex);
throw new SendMessageException(ex.getMessage());
}
});
}
// Send message
this.mailSender.send(message);
} // End try
// ...
}
}
This implementation uses the Spring’s JavaMailSender bean spring-boot-starter-mail dependency autoconfigures with the spring.mail properties defined earlier.
It can send both, text only, or HTML and text emails, in case an Email client doesn’t support HTML.
The MimeMessageHelper instantiation uses MimeMessageHelper.MULTIPART_MODE_MIXED
for the MimeMessage to support embedded elements like images.
Lines 30 through 35 loops through the Email’s inlines list to embed each inline to the MimeMessageHelper.
MimeMessageHelper’s embedded resources are used with Thymeleaf’s cid:${contentId}
like <img th:src="|cid:${imageCompanyLogo}|" />
found in the Thymeleaf template, and Thymeleaf’s Context.setVariable("imageCompanyLogo", "imageCompanyLogo");
as found in EmailRepository line 22.
To test it out, let’s send a request to the endpoint we implemented:
curl -H "Content-Type: application/json" -H "Accept: application/json" -X POST http://localhost:8080/api/sales-reports
which results in an HTML and text email like:
HTML Email using Spring Boot and Thymeleaf
Text Email using Spring Boot and Thymeleaf
6. CONCLUSION
This blog post showed you how to send HTML and text email reports using Spring Boot and Thymeleaf Java template engine.
The tutorial also helped you with the JavaMailSender and SpringTemplateEngine beans configuration properties as well as with an HTML Thymeleaf template to include a company logo and output table rows.
Spring Boot and Thymeleaf could also be used to send text notifications like SMS messages.
Your organization could use the code base as a reference to implement its own standalone Spring Boot Notification service, or use some pieces to write its own Spring Boot starter.
Now that you are sending emails using Spring Boot and Thymeleaf, how do you make sure they are sent to the intended parties? How do you make sure the emails include the expected reports and/or file attachments?
Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.
7. SOURCE CODE
Your organization can now save time and costs by purchasing a working code base, clean implementation, with support for future revisions.