Sending Email
Almost every application eventually needs to send email — password resets, order confirmations, alerts. Spring Boot ships a thin, reliable abstraction over JavaMail through the spring-boot-starter-mail dependency. Adding it auto-configures a JavaMailSender from your spring.mail.* properties, so you inject one bean and send messages without dealing with Session, Transport, or protocol details directly.
Adding the starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
The starter pulls in jakarta.mail and configures a JavaMailSenderImpl bean as soon as spring.mail.host is set.
SMTP configuration
Point Spring at an SMTP server. For development, a fake SMTP server like MailHog or Mailpit catches everything locally so you never email real users by accident.
spring:
mail:
host: smtp.gmail.com
port: 587
username: ${MAIL_USER}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
connectiontimeout: 5000
timeout: 5000
writetimeout: 5000
app:
mail:
from: [email protected]
Warning: Never hard-code SMTP credentials. Resolve them from environment variables or a secret manager — see Secrets & Vault and Externalized Configuration.
Sending a plain text email
The simplest case uses a SimpleMailMessage.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailService {
private final JavaMailSender mailSender;
@Value("${app.mail.from}")
private String from;
public EmailService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
public void sendPlainText(String to, String subject, String body) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(body);
mailSender.send(message);
}
}
Output (Mailpit captures the message):
From: [email protected]
To: [email protected]
Subject: Welcome to DevCraftly
Body: Thanks for signing up!
HTML email with MimeMessageHelper
SimpleMailMessage can only send plain text. For HTML, inline images, or attachments you need a MimeMessage, wrapped by the convenient MimeMessageHelper.
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import org.springframework.mail.javamail.MimeMessageHelper;
public void sendHtml(String to, String subject, String html) throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
// true = multipart so we can add attachments; UTF-8 for non-ASCII subjects
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(html, true); // second arg true => HTML body
mailSender.send(message);
}
The true flag on setText tells the mail client to render the body as HTML rather than escaping it.
Adding attachments
With a multipart helper you can attach files or embed inline content.
import org.springframework.core.io.FileSystemResource;
import java.io.File;
public void sendWithAttachment(String to, String subject, String html, File invoice)
throws MessagingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(html, true);
helper.addAttachment("invoice.pdf", new FileSystemResource(invoice));
// Inline image referenced in HTML via <img src="cid:logo">
helper.addInline("logo", new FileSystemResource(new File("static/logo.png")));
mailSender.send(message);
}
| Method | Purpose |
|---|---|
setText(body, false) | Plain text body |
setText(body, true) | HTML body |
setText(plain, html) | Multipart alternative (both versions) |
addAttachment(name, res) | File attachment |
addInline(cid, res) | Inline content referenced by cid: |
Templated emails with Thymeleaf
Building HTML with string concatenation is painful and error-prone. The Thymeleaf template engine renders a real template into a String you hand to the helper.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Create src/main/resources/templates/email/welcome.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h2>Welcome, <span th:text="${name}">User</span>!</h2>
<p>Your account <strong th:text="${email}">[email protected]</strong> is active.</p>
<a th:href="${verifyUrl}">Verify your email</a>
</body>
</html>
Render it with the TemplateEngine and send the result:
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
private final TemplateEngine templateEngine; // injected via constructor
public void sendWelcome(String to, String name, String verifyUrl) throws MessagingException {
Context context = new Context();
context.setVariable("name", name);
context.setVariable("email", to);
context.setVariable("verifyUrl", verifyUrl);
String html = templateEngine.process("email/welcome", context);
sendHtml(to, "Welcome to DevCraftly", html);
}
Tip: Keep email templates in a dedicated
templates/email/folder so they don’t collide with MVC view templates, and test rendering separately from sending.
Sending asynchronously
SMTP round-trips take hundreds of milliseconds — far too long to block an HTTP request thread. Mark the send method @Async so the caller returns immediately while a background thread does the work.
import org.springframework.scheduling.annotation.Async;
@Async
public void sendWelcomeAsync(String to, String name, String verifyUrl) throws MessagingException {
sendWelcome(to, name, verifyUrl);
}
Enable async support once on a configuration class with @EnableAsync (and ideally configure a dedicated executor and an error handler so swallowed failures are logged). Because async runs through an AOP proxy, the call must come from another bean — self-invocation bypasses it. See Async Methods for the full setup, including thread pools and exception handling.
@Configuration
@EnableAsync
public class AsyncMailConfig {
}
Note: A failed async send returns nothing to the caller. Log failures, and for critical mail (password resets) consider persisting an outbox row and retrying, rather than firing and forgetting.
Best Practices
- Use a fake SMTP server in development; never point tests at a real provider.
- Send via
@Asyncso mail latency never blocks request threads. - Build HTML with Thymeleaf templates, not string concatenation.
- Resolve credentials from the environment or a secret manager, never source.
- For high-volume or critical mail, use a transactional outbox with retries instead of fire-and-forget.