Docs / Spring Boot

Send email in Spring Boot over SMTP

Spring Boot's mail starter auto-configures a JavaMailSender from a handful of properties, so sending email from a Spring application needs no client code of its own. This guide points that sender at Postwing and covers HTML mail with Thymeleaf, attachments and asynchronous delivery.

You can get them on the token management page. For security reasons, a token is shown only once — at the moment it is created.

SMTP connection settings

SettingValue
SMTP hostsmtp.postwing.app
Port587
EncryptionSTARTTLS (the connection is upgraded to TLS before login)
UsernameThe login of an SMTP token for your domain
PasswordThe password of that token — shown once, when the token is created
Every mode is also available on a high port: 8465 (SSL/TLS), 8587 (STARTTLS) and 8025 (plain). Many hosting providers and clouds block outbound 25, 465 and 587 — if the connection times out, switch to the matching high port.

Add the mail starter

Without this dependency Spring Boot creates no JavaMailSender bean at all:

pom.xml
<!-- Maven -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
build.gradle.kts
// Gradle
implementation("org.springframework.boot:spring-boot-starter-mail")

Configure application.properties

application.properties
spring.mail.host=smtp.postwing.app
spring.mail.port=587
spring.mail.username=${SMTP_USER}
spring.mail.password=${SMTP_PASS}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.connectiontimeout=10000
spring.mail.properties.mail.smtp.timeout=10000
spring.mail.properties.mail.smtp.writetimeout=10000

app.mail.from=Acme <noreply@your-domain.com>

For implicit TLS on port 465 instead of STARTTLS:

properties
# Implicit TLS on port 465 instead of STARTTLS
spring.mail.port=465
spring.mail.properties.mail.smtp.ssl.enable=true
spring.mail.properties.mail.smtp.starttls.enable=false
JavaMail ships with no default timeout. If the port is blocked, the sending thread blocks forever rather than failing — under load that exhausts the thread pool and takes the application down with it.

Send your first email

Inject the auto-configured JavaMailSender and send a plain text message:

EmailService.java
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 text) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom(from);
        message.setTo(to);
        message.setSubject(subject);
        message.setText(text);
        mailSender.send(message);
    }
}

Send an HTML email

SimpleMailMessage is text-only. For HTML build a MimeMessage through MimeMessageHelper — the two-argument setText takes the plain-text and HTML parts and assembles a multipart/alternative message:

java
import jakarta.mail.internet.MimeMessage;
import org.springframework.mail.javamail.MimeMessageHelper;

public void sendHtml(String to, String subject, String html, String text) throws Exception {
    MimeMessage message = mailSender.createMimeMessage();

    // true = multipart, so both the text and the HTML part fit in one message
    MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
    helper.setFrom(from);
    helper.setTo(to);
    helper.setSubject(subject);
    helper.setText(text, html);   // (plainText, html) — always send both

    mailSender.send(message);
}

Render the email from a Thymeleaf template

Building HTML by string concatenation gets unmaintainable quickly. Process a Thymeleaf template instead and pass the result as the HTML part:

java
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;

private final SpringTemplateEngine templateEngine;

public void sendOrderConfirmation(Order order) throws Exception {
    Context ctx = new Context();
    ctx.setVariable("order", order);

    String html = templateEngine.process("email/order-confirmed", ctx);

    sendHtml(order.getCustomerEmail(),
             "Your order #" + order.getId() + " is confirmed",
             html,
             "Your order ships tomorrow.");
}

Send an email with attachments

addAttachment attaches a file; addInline embeds one that the HTML references with cid:. Both require the helper's multipart flag to be true:

java
public void sendInvoice(String to, File pdf) throws Exception {
    MimeMessage message = mailSender.createMimeMessage();
    MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

    helper.setFrom(from);
    helper.setTo(to);
    helper.setSubject("Your invoice");
    helper.setText("The invoice for March is attached.");

    helper.addAttachment("invoice.pdf", pdf);
    helper.addInline("logo", new ClassPathResource("static/logo.png"));  // <img src="cid:logo">

    mailSender.send(message);
}

Send email asynchronously

mailSender.send() blocks for the whole SMTP round trip. In a web application that is a request thread held open for hundreds of milliseconds — move it to a dedicated executor:

java
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "mailExecutor")
    public Executor mailExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("mail-");
        executor.initialize();
        return executor;
    }
}

// Then, in the service — the caller returns without waiting for SMTP.
@Async("mailExecutor")
public void sendOrderConfirmationAsync(Order order) throws Exception {
    sendOrderConfirmation(order);
}

Test without sending real email

java
@SpringBootTest
@TestPropertySource(properties = "spring.mail.host=localhost")
class EmailServiceTest {

    @MockBean
    private JavaMailSender mailSender;

    @Autowired
    private EmailService emailService;

    @Test
    void sendsConfirmation() {
        emailService.sendPlainText("customer@example.com", "Hi", "Body");
        verify(mailSender).send(any(SimpleMailMessage.class));
    }
}

Troubleshooting

ErrorCause and fix
NoSuchBeanDefinitionException: JavaMailSenderspring-boot-starter-mail is missing, or spring.mail.host is not set.
AuthenticationFailedException (535) Wrong credentials, or mail.smtp.auth is not true so none were sent.
Send hangs indefinitely No timeouts configured and the port is blocked. Set the three timeouts and switch to 8587.
MailSendException: Could not convert socket to TLS STARTTLS required on a port that does not offer it. Use 587 for STARTTLS or ssl.enable on 465.
Accented characters arrive mangled Pass "UTF-8" as the MimeMessageHelper encoding argument.
SMTPSendFailedException (550) The from domain is not verified, or the token does not cover it.

Frequently asked questions

Why is JavaMailSender not being injected in Spring Boot?

The auto-configuration only activates when spring-boot-starter-mail is on the classpath and spring.mail.host is set. If either is missing, no JavaMailSender bean is created and the application fails to start with a NoSuchBeanDefinitionException.

Do I need mail.smtp.starttls.enable in Spring Boot?

Yes, on port 587. Spring Boot does not enable STARTTLS for you, so without that property the session stays in plain text and the server refuses to authenticate. Also set starttls.required=true so a failed upgrade is an error rather than a silent downgrade.

Why does Spring Boot mail hang instead of failing?

JavaMail has no default timeouts — a blocked port means the thread waits indefinitely. Always set mail.smtp.connectiontimeout, mail.smtp.timeout and mail.smtp.writetimeout. If it then times out, your host is blocking outbound 587; use 8587 instead.

What is the difference between SimpleMailMessage and MimeMessage?

SimpleMailMessage sends plain text only. Anything with HTML, attachments, inline images or a custom character set needs a MimeMessage, usually built through MimeMessageHelper with multipart set to true.

How do I send email asynchronously in Spring Boot?

Annotate the sending method with @Async and back it with a dedicated Executor bean, so the SMTP round trip happens on a mail thread instead of the request thread. Remember that @Async only works when called from another bean — a self-invocation bypasses the proxy.

Why does authentication fail with 535 even though the password is right?

Check that spring.mail.properties.mail.smtp.auth=true is set — without it JavaMail never sends credentials at all. Also make sure the username is the full SMTP token login for your domain, not just the local part.

Next steps