Sending Email with Spring MVC Using Template

This tutorial walks through a small Spring MVC application that sends a templated e-mail, using Thymeleaf to render the message body and Jakarta Mail to deliver it.

It assumes you are comfortable with Java EE development and with building Spring MVC applications.

Note: This post has been updated for Spring Framework 7 and Thymeleaf 3.1. The original version rendered the message with Apache Velocity, but Spring deprecated its Velocity support in 4.3 and removed it entirely in 5.0; templating is now handled by Thymeleaf, and the mail dependency has moved from javax.mail to Jakarta Mail (jakarta.mail).

Spring Framework’s Support for E-mail

Built on top of Jakarta Mail, the Spring Framework provides a high-level abstraction that hides most of the boilerplate of sending e-mail. The key types in the diagram above are worth a quick look:

  • MailSender is the root interface, with a single send() contract for plain-text messages carried by SimpleMailMessage.
  • JavaMailSender extends it to support MIME messages (HTML bodies, attachments, inline images).
  • JavaMailSenderImpl is the implementation you configure as a bean; it wraps a Jakarta Mail Session.
  • MimeMessageHelper and MimeMessagePreparator make populating a MimeMessage (recipients, subject, HTML body) far less verbose than the raw Jakarta Mail API.

We will configure JavaMailSenderImpl as a bean and let Thymeleaf produce the HTML body.

Dependencies (pom.xml)

<!-- Spring -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>7.0.8</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>7.0.8</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>7.0.8</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>7.0.8</version>
</dependency>
<!-- Provides the org.springframework.mail package -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>7.0.8</version>
</dependency>

<!-- Jakarta Mail (API + Eclipse Angus implementation) -->
<dependency>
    <groupId>jakarta.mail</groupId>
    <artifactId>jakarta.mail-api</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>org.eclipse.angus</groupId>
    <artifactId>angus-mail</artifactId>
    <version>2.0.4</version>
</dependency>

<!-- Thymeleaf (template engine + Spring 7 integration) -->
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf</artifactId>
    <version>3.1.5.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring7</artifactId>
    <version>3.1.5.RELEASE</version>
</dependency>

The first group is the usual Spring MVC stack; spring-context-support is the module that actually provides the org.springframework.mail package. jakarta.mail-api together with the Eclipse Angus implementation replaces the old com.sun.mail/javax.mail pair, and the two Thymeleaf artifacts replace Velocity (thymeleaf-spring7 is the integration module for Spring Framework 7).

Configuring the Mail Sender and Template Engine (dispatcher-servlet.xml)

<context:annotation-config />
<context:component-scan base-package="org.verwandlung.voj.web" />

<!-- SMTP connection -->
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
    <property name="host" value="smtp.mailgun.org" />
    <property name="username" value="postmaster@verwandlung.org" />
    <property name="password" value="" />
    <property name="javaMailProperties">
        <props>
            <prop key="mail.smtp.auth">true</prop>
            <prop key="mail.smtp.starttls.enable">true</prop>
        </props>
    </property>
</bean>

<!-- Thymeleaf engine dedicated to e-mail templates -->
<bean id="emailTemplateResolver" class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">
    <property name="prefix" value="mails/" />
    <property name="suffix" value=".html" />
    <property name="templateMode" value="HTML" />
    <property name="characterEncoding" value="UTF-8" />
    <property name="cacheable" value="false" />
</bean>
<bean id="emailTemplateEngine" class="org.thymeleaf.spring7.SpringTemplateEngine">
    <property name="templateResolver" ref="emailTemplateResolver" />
</bean>

JavaMailSenderImpl holds the SMTP connection details (here, Mailgun’s relay). The ClassLoaderTemplateResolver tells Thymeleaf to load templates from mails/ on the classpath with an .html suffix, and SpringTemplateEngine is the engine our code calls to render them. Because the MailSender helper below is annotated with @Component, component scanning picks it up automatically, so it needs no explicit bean of its own.

The Mail Template (verifyEmail.html)

File location: classpath:/mails/verifyEmail.html

<div style="padding:0;margin:0;background:#e9e9e9;font-family:'Segoe UI', 'Microsoft YaHei'">
<table width="625" style="background:#e9e9e9" cellspacing="0" cellpadding="0" border="0">
    <tbody>
        <tr>
            <td>
                <div style="color:#1b1b1b;background:#fff;border:1px solid #aaa;border-radius:5px;margin:15px;">
                    <table cellspacing="0" cellpadding="0" border="0">
                        <tbody>
                            <tr>
                                <td style="padding:15px 15px 0 15px;background:#fff;border-radius:4px 4px 0 0;">
                                    <div>
                                        <img src="https://verwandlung.org/assets/img/logo.webp" alt="Testzilla" height="60" width="250">
                                    </div>
                                </td>
                            </tr>
                            <tr>
                                <td style="padding:15px; background:#fff; border-radius:0 0 4px 4px;font-size:12px;">
                                    Hi <span th:text="${username}">User</span>, <br />
                                    There's just one step left to create your Verwandlung Online Judge account. <br /><br />
                                    To access your account and be eligible to receive testing projects, we need you to confirm your email address. <br />
                                    <a th:href="@{https://www.verwandlung.org/account/verifyEmail(email=${email},code=${code})}" href="#">Click here to confirm your email address</a>. <br /><br />
                                    Thank you, <br />
                                    The Verwandlung Online Judge Team. <br /><br />
                                    <div style="border-top:3px solid #eee;color:#999;font-size:11px;line-height:1.2">
                                        <br>Powered by <a href="https://verwandlung.org" target="_blank" style="color: #005399;text-decoration: none;">Testzilla</a>. All rights reserved.<br>
                                    </div>
                                </td>
                            </tr>
                        </tbody>
                    </table>
                </div>
            </td>
        </tr>
    </tbody>
</table>
</div>

Thymeleaf replaces Velocity’s ${...} placeholders with th:* attributes: th:text sets element content (escaping it safely), and th:href with the @{...(param=value)} syntax builds the confirmation URL and URL-encodes each query parameter for you.

The Mail Sender Class

package org.verwandlung.voj.web.util;

import java.util.Map;

import jakarta.mail.internet.MimeMessage;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring7.SpringTemplateEngine;

@Component
public class MailSender {
    private final SpringTemplateEngine templateEngine;
    private final JavaMailSender mailSender;
    private final Logger logger = LogManager.getLogger(MailSender.class);

    @Autowired
    public MailSender(SpringTemplateEngine templateEngine, JavaMailSender mailSender) {
        this.templateEngine = templateEngine;
        this.mailSender = mailSender;
    }

    public String getMailContent(String templateName, Map<String, Object> model) {
        Context context = new Context();
        context.setVariables(model);
        return templateEngine.process(templateName, context);
    }

    public void sendMail(String recipient, String subject, String body) {
        MimeMessagePreparator preparator = mimeMessage -> {
            MimeMessageHelper message = new MimeMessageHelper(mimeMessage, "UTF-8");
            message.setTo(recipient);
            message.setFrom("noreply@verwandlung.org");
            message.setSubject(subject);
            message.setText(body, true);
        };
        mailSender.send(preparator);
        logger.info("An email {Recipient: {}, Subject: {}} has been sent.", recipient, subject);
    }
}

getMailContent() copies the model into a Thymeleaf Context and renders the named template to a String. sendMail() then hands that HTML to a MimeMessagePreparator (a lambda here) and asks JavaMailSender to deliver it; passing true to setText() marks the body as HTML.

How to Use It

package org.verwandlung.voj.web.service;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import org.verwandlung.voj.web.util.MailSender;

@Service
@Transactional
public class UserService {
    @Autowired
    private MailSender mailSender;

    public void sendActivationMail(String username, String email, String code) {
        Map<String, Object> model = new HashMap<>();
        model.put("username", username);
        model.put("email", email);
        model.put("code", code);

        String subject = "Activate Your Account";
        String body = mailSender.getMailContent("verifyEmail", model);
        mailSender.sendMail(email, subject, body);
    }
}

getMailContent("verifyEmail", model) resolves to mails/verifyEmail.html through the prefix and suffix configured on the resolver, so the service only needs the bare template name.

Conclusion

With Velocity gone, Thymeleaf is the natural template engine for e-mail on modern Spring: the resolver and engine beans take the place of Velocity’s VelocityEngineFactoryBean, while the JavaMail-based sending code is unchanged apart from the move to the jakarta.mail package. The same MailSender helper can render any template under mails/, so adding a password-reset or welcome e-mail is just another .html file.

References