Sending Email

Send plain and HTML email with JavaMailSender, configure SMTP safely, and keep slow mail servers out of your request path.

Syntaxspring.mail.host=smtp.example.com MimeMessageHelper helper = new MimeMessageHelper(mailSender.createMimeMessage(), true, "UTF-8"); helper.setText(plain, html); mailSender.send(helper.getMimeMessage());

Add spring-boot-starter-mail and set spring.mail.host; Spring Boot then creates a JavaMailSender bean for you. Without the host property there is no bean, and any class that injects one fails at startup, which is a useful early warning.

Plain text and HTML

SimpleMailMessage covers plain text: set from, to, subject and text, then call send. For HTML or attachments, create a MimeMessage from the sender and fill it with MimeMessageHelper. Pass true for multipart and use setText(plain, html) so mail clients that block HTML still show a readable message. Escape any user data you put into HTML, exactly as you would in a web page.

MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(customer.email());
helper.setSubject("Your receipt");
helper.setText(plainText, html);
mailSender.send(message);

Keep mail out of the request

An SMTP server can take seconds to answer, or not answer at all. JavaMail's socket timeouts are infinite by default, so one stuck connection can hold a request thread forever. Always set mail.smtp.connectiontimeout, mail.smtp.timeout and mail.smtp.writetimeout through spring.mail.properties.*.

Send mail after the business transaction commits, never inside it. A @TransactionalEventListener runs after commit, so a rolled-back signup does not leave the user with a welcome email for an account that does not exist. Mark the listener @Async so the HTTP response does not wait for SMTP. For mail that must not be lost, such as password resets, put a job on a queue or in an outbox table so a failed send can be retried.

Production notes

  • Use a transactional email provider's SMTP relay with credentials from environment variables, and set up SPF, DKIM and DMARC for your sending domain, or your mail lands in spam.
  • In development, point spring.mail.host at a local catcher such as Mailpit, so no test ever emails a real customer.
  • The Actuator mail health indicator connects to the SMTP server on every health check. Disable it with management.health.mail.enabled=false if a provider outage should not mark the whole application down.

Example

Example · java
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import java.math.BigDecimal;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.MailSendException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.web.util.HtmlUtils;

record Receipt(String email, String customerName, long orderId, BigDecimal total) {
}

@Service
public class ReceiptMailer {

    private final JavaMailSender mailSender;
    private final String from;

    ReceiptMailer(JavaMailSender mailSender, @Value("${app.mail.from}") String from) {
        this.mailSender = mailSender;
        this.from = from;
    }

    public void send(Receipt receipt) {
        MimeMessage message = mailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
            helper.setFrom(from);
            helper.setTo(receipt.email());
            helper.setSubject("Receipt for order " + receipt.orderId());
            String plain = "Hello " + receipt.customerName() + ",\n\nThank you for order "
                    + receipt.orderId() + ". Total: " + receipt.total() + " GBP.";
            String html = "<p>Hello " + HtmlUtils.htmlEscape(receipt.customerName()) + ",</p>"
                    + "<p>Thank you for order <strong>" + receipt.orderId() + "</strong>. Total: "
                    + receipt.total() + " GBP.</p>";
            helper.setText(plain, html);
        } catch (MessagingException e) {
            throw new MailSendException("Could not build receipt for order " + receipt.orderId(), e);
        }
        mailSender.send(message);
    }
}

When to use it

  • Email an HTML receipt with a plain-text alternative after an order commits, without slowing the checkout response.
  • Send password reset links through an SMTP relay with credentials injected from Kubernetes secrets.
  • Catch every email in Mailpit during development and end-to-end tests so no real customer is ever contacted.

More examples

SMTP settings with timeouts

The defaults point at port 1025 on localhost, where Mailpit listens in development; production supplies the real host and credentials through environment variables and sets <code>SMTP_AUTH</code> and <code>SMTP_STARTTLS_REQUIRED</code> to <code>true</code>, so the connection fails rather than falling back to plain text. Leaving <code>mail.smtp.auth=true</code> with no credentials makes every send fail locally. The <code>mail.smtp.*</code> entries are passed straight to Jakarta Mail, and the three timeouts are in milliseconds.

Example · properties
spring.mail.host=${SMTP_HOST:localhost}
spring.mail.port=${SMTP_PORT:1025}
spring.mail.username=${SMTP_USERNAME:}
spring.mail.password=${SMTP_PASSWORD:}
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.auth=${SMTP_AUTH:false}
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=${SMTP_STARTTLS_REQUIRED:false}
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.writetimeout=5000

[email protected]
management.health.mail.enabled=false

Sending after commit, off the request thread

The signup service publishes <code>AccountCreated</code> with <code>ApplicationEventPublisher</code> inside its transaction. The listener runs only if that transaction commits, and <code>@Async</code> (enabled with <code>@EnableAsync</code>) moves the SMTP call to another thread. A failure here is logged, not retried, so use this for mail that is nice to have.

Example · java
import java.math.BigDecimal;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

record AccountCreated(String email, String displayName) {
}

@Component
public class WelcomeEmailListener {

    private final JavaMailSender mailSender;

    WelcomeEmailListener(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void onAccountCreated(AccountCreated event) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom("[email protected]");
        message.setTo(event.email());
        message.setSubject("Welcome, " + event.displayName());
        message.setText("Your account is ready. You can sign in at https://app.example.com.");
        mailSender.send(message);
    }
}

Unit testing the mailer with Mockito

A real <code>MimeMessage</code> built on an empty <code>Session</code> needs no SMTP server, so the test can inspect exactly what would have been sent. Integration tests go one step further and point <code>spring.mail.host</code> at a Mailpit container, then read the caught message from its HTTP API.

Example · java
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import jakarta.mail.Message;
import jakarta.mail.Session;
import jakarta.mail.internet.MimeMessage;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;

class PasswordResetMailer {

    private final JavaMailSender mailSender;

    PasswordResetMailer(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }

    void send(String email, String link) throws Exception {
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8");
        helper.setFrom("[email protected]");
        helper.setTo(email);
        helper.setSubject("Reset your password");
        helper.setText("Use this link within 30 minutes: " + link);
        mailSender.send(message);
    }
}

class PasswordResetMailerTest {

    @Test
    void sendsTheResetLinkToTheUser() throws Exception {
        JavaMailSender mailSender = mock(JavaMailSender.class);
        MimeMessage message = new MimeMessage(Session.getInstance(new Properties()));
        when(mailSender.createMimeMessage()).thenReturn(message);

        new PasswordResetMailer(mailSender).send("[email protected]", "https://app.example.com/reset/abc");

        verify(mailSender).send(message);
        assertThat(message.getRecipients(Message.RecipientType.TO)[0].toString()).isEqualTo("[email protected]");
        assertThat(message.getSubject()).isEqualTo("Reset your password");
        assertThat((String) message.getContent()).contains("/reset/abc");
    }
}

Discussion

  • Be the first to comment on this lesson.