Send email with Spring Boot
Configure spring-boot-starter-mail for the PostStack SMTP relay and send HTML email with JavaMailSender — or call the REST API with RestClient.
Spring Boot’s mail starter auto-configures a JavaMailSender from spring.mail.* properties, so sending through PostStack is configuration: point it at smtp.poststack.dev with your API key as the password and inject the sender where you need it. Everything that already uses JavaMailSender — including Spring Security password-reset flows you have written on top of it — sends through PostStack unchanged. When you need the em_… email id back to match delivery and bounce webhooks, call the REST API with Spring’s RestClient instead. There is no Java SDK; both paths below use only Spring.
Quickstart#
Add spring-boot-starter-mail
xml<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> // Gradle: implementation("org.springframework.boot:spring-boot-starter-mail")Configure application.properties
java# src/main/resources/application.properties spring.mail.host=smtp.poststack.dev spring.mail.port=587 spring.mail.username=poststack spring.mail.password=${POSTSTACK_API_KEY} 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=5000 spring.mail.properties.mail.smtp.timeout=10000 spring.mail.properties.mail.smtp.writetimeout=10000 # Port 465 instead: spring.mail.port=465 and # spring.mail.properties.mail.smtp.ssl.enable=true (drop the starttls lines)Send with JavaMailSender
javaimport jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; @Service public class MailService { private final JavaMailSender mailSender; public MailService(JavaMailSender mailSender) { this.mailSender = mailSender; } public void sendWelcome(String to, String name) throws MessagingException { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); helper.setFrom("hello@yourdomain.com"); // must be on a verified domain helper.setTo(to); helper.setSubject("Welcome to Acme"); helper.setText( "Welcome, " + name + "!", // plain-text part "<h1>Welcome, " + name + "!</h1>" // HTML part ); message.addHeader("X-PostStack-Tags", "welcome,onboarding"); mailSender.send(message); } }Handle errors
javaimport org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.mail.MailAuthenticationException; import org.springframework.mail.MailSendException; import org.springframework.retry.annotation.Backoff; import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; // Spring Boot 3: needs @EnableAsync and @EnableRetry on a @Configuration class, // plus org.springframework.retry:spring-retry and spring-boot-starter-aop. // (Spring Boot 4 has @Retryable built into Spring Framework 7 instead — // org.springframework.resilience.annotation, enabled with @EnableResilientMethods.) @Service public class WelcomeMailer { private static final Logger log = LoggerFactory.getLogger(WelcomeMailer.class); private final MailService mail; public WelcomeMailer(MailService mail) { this.mail = mail; } @Async @Retryable( retryFor = MailSendException.class, // connection failures and rejected sends noRetryFor = MailAuthenticationException.class, // 535: bad API key, retrying won't help maxAttempts = 4, backoff = @Backoff(delay = 30_000, multiplier = 2)) public void send(String to, String name) throws Exception { mail.sendWelcome(to, name); } @Recover public void giveUp(Exception e, String to, String name) { // 550 = permanent rejection, e.g. the From domain is not verified log.error("PostStack could not send the welcome email to {}: {}", to, e.getMessage()); } }
Attachments and inline images#
Use a multipart MimeMessageHelper. PostStack accepts up to 10 attachments per message, 10 MB per file and 25 MB in total. X-PostStack-Tags, X-PostStack-Idempotency-Key and X-PostStack-Schedule headers are read by the relay.
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom("billing@yourdomain.com");
helper.setTo(order.getEmail());
helper.setReplyTo("support@yourdomain.com");
helper.setSubject("Invoice #" + order.getNumber());
helper.setText(
"Your invoice is attached.",
"<p>Your invoice is attached.</p><img src='cid:logo'>");
helper.addInline("logo", new ClassPathResource("static/logo.png"));
helper.addAttachment("invoice-" + order.getNumber() + ".pdf",
new ByteArrayResource(order.getInvoicePdf()), "application/pdf");
message.addHeader("X-PostStack-Idempotency-Key", "invoice-" + order.getId());
mailSender.send(message);Send through the REST API with RestClient#
Spring Boot 3.2+ includes RestClient. A successful send returns 202 with { "id": "em_…" }; recipients are always an array.
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
@Service
public class PostStackApi {
record SendResponse(String id) {}
private final RestClient client;
public PostStackApi(@Value("${poststack.api-key}") String apiKey) {
this.client = RestClient.builder()
.baseUrl("https://api.poststack.dev")
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey)
.build();
}
public String sendWelcome(String to) {
return client.post()
.uri("/emails")
.contentType(MediaType.APPLICATION_JSON)
.header("Idempotency-Key", UUID.randomUUID().toString()) // reuse it on retry
.body(Map.of(
"from", "Acme <hello@yourdomain.com>",
"to", List.of(to),
"subject", "Welcome to Acme",
"html", "<h1>Welcome!</h1>",
"tags", List.of("welcome")))
.retrieve() // throws RestClientResponseException on 4xx/5xx
.body(SendResponse.class)
.id();
}
}
// application.properties: poststack.api-key=${POSTSTACK_API_KEY}Templates with Thymeleaf#
Render the HTML body with the TemplateEngine that spring-boot-starter-thymeleaf provides, then pass it to the helper.
Context ctx = new Context(Locale.ENGLISH);
ctx.setVariable("name", user.getName());
String html = templateEngine.process("mail/welcome", ctx); // templates/mail/welcome.html
helper.setText("Welcome, " + user.getName() + "!", html);Framework integrations#
Auto-configured JavaMailSender
Setting spring.mail.host is enough for Spring Boot to create a JavaMailSender bean. Inject it by constructor; one instance is safe to share across threads.
Profiles for development
Use application-dev.properties to point spring.mail.host at a local catcher such as Mailpit on port 1025, and keep the PostStack settings in the production profile.
@Async and @Retryable
Sending over SMTP takes a network round-trip. Annotate the sending method with @Async so requests do not wait, and @Retryable so temporary failures are retried with backoff.
Health indicator
The mail starter adds a mail health indicator that connects to the SMTP server. It needs the correct TLS settings to report UP; disable it with management.health.mail.enabled=false if you do not want an SMTP connection on every health check.
Common pitfalls#
MailAuthenticationException: 535
spring.mail.password must be a PostStack API key with sending permission. Check that POSTSTACK_API_KEY is set in the environment the app actually runs in.
Timeouts with the default settings
JavaMail waits forever by default. Always set mail.smtp.connectiontimeout, mail.smtp.timeout and mail.smtp.writetimeout as shown, so a blocked port fails fast instead of hanging a thread.
Rejected From address
The From address must be on a domain verified in PostStack. SimpleMailMessage without setFrom falls back to the JVM’s default user, which is rejected.
SSL on port 587 vs 465
Use starttls.enable=true on 587, or ssl.enable=true on 465. Enabling STARTTLS on 465 fails the handshake.
FAQ#
How do I send email in Spring Boot?
Add spring-boot-starter-mail, set spring.mail.host=smtp.poststack.dev, port 587, username poststack and your API key as the password with STARTTLS enabled, then inject JavaMailSender and send a MimeMessage built with MimeMessageHelper.
How do I send HTML email with JavaMailSender?
Create a MimeMessageHelper(message, true, "UTF-8") and call setText(plainText, html) — Spring builds a multipart message with both parts.
Is there a Java SDK?
No. Use SMTP through JavaMailSender, or the REST API with RestClient, WebClient or the JDK HttpClient. The API is a single JSON POST with a Bearer token.
Does this work with Spring Boot 2?
Yes. The spring.mail.* properties are the same; import javax.mail instead of jakarta.mail, and use RestTemplate or WebClient instead of RestClient.
Ready to send from Spring Boot?
Create a free account and an API key — no card needed.