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.
| Setting | Value |
|---|---|
| SMTP host | smtp.postwing.app |
| Port | 587 |
| Encryption | STARTTLS (the connection is upgraded to TLS before login) |
| Username | The login of an SMTP token for your domain |
| Password | The password of that token — shown once, when the token is created |
Without this dependency Spring Boot creates no JavaMailSender bean at all:
<!-- Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>// Gradle
implementation("org.springframework.boot:spring-boot-starter-mail")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:
# 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 Inject the auto-configured JavaMailSender and send a plain text message:
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);
}
}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:
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);
}Building HTML by string concatenation gets unmaintainable quickly. Process a Thymeleaf template instead and pass the result as the HTML part:
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.");
}addAttachment attaches a file; addInline embeds one that the HTML references with cid:. Both require the helper's multipart flag to be true:
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);
}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:
@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);
}@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));
}
}| Error | Cause and fix |
|---|---|
NoSuchBeanDefinitionException: JavaMailSender | spring-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. |
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.
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.
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.
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.
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.
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.