Developer Tutorial
Covers Jakarta Mail for direct SMTP, Spring Boot's JavaMailSender for web apps, and a plain HTTP client call for production services that need delivery webhooks.
Java's email landscape has improved significantly since the javax.mail days. Jakarta Mail 2.0 is the modern standard, Spring Boot wraps it cleanly with JavaMailSender, and Java 11's HttpClient makes REST API calls straightforward. This guide covers all three approaches with real working code.
The approach you pick depends on how much of the Java ecosystem you're already using.
The successor to JavaMail / javax.mail. Handles SMTP, IMAP, and POP3. Works in any Java app without a framework dependency.
Spring's abstraction over Jakarta Mail. Configure in application.properties, inject JavaMailSender, done. Pairs with @Async for background delivery.
POST JSON to an email API using Java 11's built-in HttpClient. No extra dependencies, works anywhere, and gives you delivery webhooks.
Add the dependency first:
<!-- Maven pom.xml --> <dependency> <groupId>org.eclipse.angus</groupId> <artifactId>angus-mail</artifactId> <version>2.0.3</version> </dependency> // Gradle build.gradle implementation 'org.eclipse.angus:angus-mail:2.0.3'
import jakarta.mail.*;
import jakarta.mail.internet.*;
import java.util.Properties;
public class EmailService {
public static void sendEmail(String to, String subject, String htmlBody, String textBody)
throws MessagingException {
String host = System.getenv("SMTP_HOST");
String user = System.getenv("SMTP_USER");
String pass = System.getenv("SMTP_PASSWORD");
String from = System.getenv("EMAIL_FROM");
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", host);
props.put("mail.smtp.port", "587");
props.put("mail.smtp.ssl.trust", host);
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(user, pass);
}
});
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to));
message.setSubject(subject);
// Multipart: plain text + HTML
MimeBodyPart textPart = new MimeBodyPart();
textPart.setText(textBody, "utf-8");
MimeBodyPart htmlPart = new MimeBodyPart();
htmlPart.setContent(htmlBody, "text/html; charset=utf-8");
Multipart multipart = new MimeMultipart("alternative");
multipart.addBodyPart(textPart);
multipart.addBodyPart(htmlPart);
message.setContent(multipart);
Transport.send(message);
}
} The MimeMultipart("alternative") puts plain text first, HTML second. Mail clients show the last part they understand — which for modern clients is HTML. Always include both: HTML-only emails fail spam filters and can't be read by clients that disable images.
Add the starter to your project, then configure in application.properties:
<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
# application.properties
spring.mail.host=${SMTP_HOST}
spring.mail.port=587
spring.mail.username=${SMTP_USER}
spring.mail.password=${SMTP_PASSWORD}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=trueimport org.springframework.mail.javamail.*;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import jakarta.mail.internet.MimeMessage;
import org.springframework.scheduling.annotation.Async;
@Service
public class EmailService {
@Autowired
private JavaMailSender mailSender;
@Value("${EMAIL_FROM}")
private String emailFrom;
@Async // non-blocking — requires @EnableAsync on your config class
public void sendWelcomeEmail(String to, String firstName) throws Exception {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(emailFrom);
helper.setTo(to);
helper.setSubject("Welcome, " + firstName + "!");
helper.setText(
"Hi " + firstName + ", your account is ready. Sign in at https://app.example.com",
"<p>Hi " + firstName + ", <a href='https://app.example.com'>sign in</a></p>"
);
mailSender.send(message);
}
}Call it from your controller:
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
User user = userService.create(req);
emailService.sendWelcomeEmail(user.getEmail(), user.getFirstName()); // @Async — returns immediately
return ResponseEntity.ok(Map.of("id", user.getId()));
} Java 11's built-in HttpClient handles this cleanly — no external HTTP library needed:
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
public class EmailClient {
private static final HttpClient HTTP = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
private static final String API_URL = "https://api.tinysend.co/v1/emails";
public static String send(String to, String subject, String html, String text)
throws Exception {
String apiKey = System.getenv("TINYSEND_API_KEY");
String from = System.getenv("EMAIL_FROM");
String body = String.format(
"{\"from\":\"%s\",\"to\":\"%s\",\"subject\":\"%s\",\"html\":\"%s\",\"text\":\"%s\"}",
escape(from), escape(to), escape(subject), escape(html), escape(text)
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HTTP.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
throw new RuntimeException("Email API error " + response.statusCode() + ": " + response.body());
}
return response.body(); // JSON with email ID
}
// Simple JSON escaping — use Jackson/Gson for complex payloads
private static String escape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
}
} For production use, swap the manual JSON building for Jackson or Gson. Add an Idempotency-Key header for critical emails:
// With Jackson (add jackson-databind dependency)
import com.fasterxml.jackson.databind.ObjectMapper;
private static final ObjectMapper MAPPER = new ObjectMapper();
Map<String, String> payload = Map.of(
"from", System.getenv("EMAIL_FROM"),
"to", to,
"subject", subject,
"html", html,
"text", text
);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + System.getenv("TINYSEND_API_KEY"))
.header("Idempotency-Key", "password-reset-" + userId + "-" + token) // prevents duplicate sends
.POST(HttpRequest.BodyPublishers.ofString(MAPPER.writeValueAsString(payload)))
.build();Thymeleaf works well for email templates in Spring Boot — it's already on the classpath if you use the web starter:
<!-- src/main/resources/templates/emails/welcome.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h1>Welcome, <span th:text="${firstName}">User</span>!</h1>
<p>Your account is ready. <a th:href="${loginUrl}">Sign in</a></p>
</body>
</html>import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.springframework.beans.factory.annotation.Autowired;
@Service
public class EmailTemplateService {
@Autowired
private TemplateEngine templateEngine;
@Autowired
private JavaMailSender mailSender;
public void sendWelcomeEmail(String to, String firstName) throws Exception {
Context ctx = new Context();
ctx.setVariable("firstName", firstName);
ctx.setVariable("loginUrl", "https://app.example.com/login");
String htmlBody = templateEngine.process("emails/welcome", ctx);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(to);
helper.setSubject("Welcome, " + firstName + "!");
helper.setText("Sign in at https://app.example.com/login", htmlBody);
mailSender.send(message);
}
} Avoid new Context() inside hot paths — create it fresh per send but share the TemplateEngine as a singleton bean. Thymeleaf caches parsed templates by default in production mode.
In Spring Boot, use @Retryable from Spring Retry for automatic backoff:
<!-- pom.xml --> <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency>
import org.springframework.retry.annotation.*;
@Service
@EnableRetry
public class EmailService {
@Retryable(
retryFor = { MailException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 2000, multiplier = 2) // 2s, 4s
)
@Async
public void sendEmail(String to, String subject, String html, String text) throws Exception {
// ... send logic ...
}
@Recover
public void recoverFromMailFailure(MailException ex, String to, String subject, String html, String text) {
// Log the failure, alert, queue for manual review
log.error("Failed to send email to {} after 3 attempts: {}", to, ex.getMessage());
}
}For the REST API approach without Spring Retry, check the HTTP response status before deciding whether to retry: 4xx errors (invalid address, bad payload) should not be retried. 5xx errors and network timeouts should be retried with exponential backoff.
application.properties should reference ${ENV_VAR}, not literal values. Never commit credentials.MimeMessageHelper(message, true) and call setText(text, html) to set both bodies. HTML-only emails are flagged by spam filters.@Async (requires @EnableAsync) or publish to a queue and process asynchronously.JavaMailSender is idiomatic Spring and works well for standard CRUD apps where delivery confirmation is a nice-to-have. The tradeoff: SMTP gives you no signal on whether the email was actually delivered. A REST API with webhooks tells you about bounces, spam complaints, and opens in real time — which matters for transactional email where delivery failures affect your product. See the SMTP vs email API comparison for a detailed breakdown.
tinysend gives you a clean REST API, templates with variables, bounce handling, and BYOK — so you can use your existing SES or Postmark account as the backend. Works with Spring Boot, Quarkus, Micronaut, and plain Java.
Get your API key →