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 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 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.

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:

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..

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:

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:

Thymeleaf loops through Map

  • <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 the temporals 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 the sales-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

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 HTML Email using Spring Boot and Thymeleaf


Text 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.

8. REFERENCES