Production · Lesson 85 of 95
Sending Email
Sending Email in Spring Boot with JavaMailSender: send plain and HTML mail with attachments, set up SMTP safely and test with a local mail server.
When you book a room at a hotel, you expect an email within a minute: your booking number, dates and a thank-you line. Nobody at the hotel types it. The booking system sends it. Almost every real app needs this: welcome mail, password reset, invoice, report. Sending email in Spring Boot takes one starter and one class, and you can test it on your own laptop without touching a real mailbox.
In this guide you will send a plain text mail and an HTML mail with an attachment, connect to a mail server through application.properties, and handle failures properly.
What is Sending Email in Spring Boot?
Your app does not deliver the mail itself. It talks to a mail server using a simple protocol called SMTP, like a customer handing a parcel to a courier. The server can be Gmail, your company's mail service, a paid sending service, or a fake local server for testing.
Two message types cover most needs:
SimpleMailMessagefor plain text: a to address, a subject and a text.MimeMessagefor HTML, attachments and inline images, built easily withMimeMessageHelper.
Why is it used?
- Account flows. Welcome mails, verification links and password resets.
- Receipts and confirmations. Bookings, orders and payments.
- Alerts and reports. Daily summaries from scheduled jobs.
- A standard tool. The same code works with any SMTP provider. Only the settings change.
How it works
textYour service | | send(message) v +------------------------+ | JavaMailSender | | (from the mail starter)| +------------------------+ | | SMTP: host + port v +------------------------+ | Mail server | +------------------------+ | v Receiver's inbox
When you add spring-boot-starter-mail and set spring.mail.host, Spring Boot creates a JavaMailSender bean using your settings. Your service asks for it, builds a message and calls send. The sender opens a connection to the mail server, gives it the message and closes the connection. If the server cannot be reached or rejects the mail, a MailException is thrown.
Real-Life Example
A courier company delivers your parcel, but you never see the trucks. You give the parcel to the pickup counter with an address, and the counter takes care of the rest. The mail server is that counter. Your app only needs the counter's address (host and port) and, for a real one, an ID card (username and login). A test counter that accepts every parcel and throws it in a box lets you practise without sending anything to real people.
Code Example
Let's build SeaBreeze Resort. After a booking, the app sends a plain confirmation and an HTML mail with a small attachment. The app talks to a mail server on localhost:1025. That is where tools like Mailpit, MailHog or a small test SMTP server listen, so nothing reaches a real inbox.
File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.seabreeze</groupId> <artifactId>booking</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.application.name=seabreeze-booking spring.main.banner-mode=off logging.level.root=warn spring.mail.host=localhost spring.mail.port=1025 seabreeze.mail.from=bookings@seabreeze.example
File: BookingMailer.java in package com.seabreeze.booking
javapackage com.seabreeze.booking; import jakarta.mail.internet.MimeMessage; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; @Service public class BookingMailer { private final JavaMailSender mailSender; private final String from; public BookingMailer(JavaMailSender mailSender, @Value("${seabreeze.mail.from}") String from) { this.mailSender = mailSender; this.from = from; } public void sendPlain(String to, String bookingNo) { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom(from); message.setTo(to); message.setSubject("Booking " + bookingNo + " confirmed"); message.setText("Thank you for booking with SeaBreeze Resort.\nCheck-in is after 2 PM."); mailSender.send(message); } public void sendHtml(String to, String bookingNo) throws Exception { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); helper.setFrom(from); helper.setTo(to); helper.setSubject("Your stay details for " + bookingNo); helper.setText("<h2>Welcome to SeaBreeze</h2><p>Booking <b>" + bookingNo + "</b> is ready.</p>", true); helper.addAttachment("directions.txt", new ByteArrayResource("Take the coastal road, turn left at the lighthouse.".getBytes())); mailSender.send(message); } }
File: BookingApplication.java in package com.seabreeze.booking
javapackage com.seabreeze.booking; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.mail.MailException; @SpringBootApplication public class BookingApplication { public static void main(String[] args) { SpringApplication.run(BookingApplication.class, args); } @Bean CommandLineRunner demo(BookingMailer mailer) { return args -> { try { mailer.sendPlain("guest@example.com", "SB-1001"); System.out.println("Plain mail handed to the server"); mailer.sendHtml("guest@example.com", "SB-1001"); System.out.println("HTML mail handed to the server"); } catch (MailException e) { System.out.println("Mail could not be sent: " + e.getClass().getSimpleName()); } }; } }
Start a test mail server first, then run the app with ./mvnw spring-boot:run.
Output:
textPlain mail handed to the server HTML mail handed to the server
Here the test server logged two mails: the plain one with the subject "Booking SB-1001 confirmed", and the second one with the subject "Your stay details for SB-1001", an HTML part and the attachment directions.txt. If no server is listening, the same run prints the fallback line instead:
textMail could not be sent: MailSendException
That second output is one line on screen, wrapped here to fit small screens.
Code Explained
spring-boot-starter-mailbrings the Jakarta Mail library and creates theJavaMailSenderbean whenspring.mail.hostis set. Without the host property, the bean is not created.SimpleMailMessageis enough for plain text: from, to, subject and body.- For HTML and attachments we build a
MimeMessagethroughMimeMessageHelper. Thetruein the constructor turns on multipart mode, which is required for attachments. ThetrueinsetTextsays the text is HTML. - The sender address is kept in
application.properties, so each environment can use its own. - The runner catches
MailException, the parent of all Spring mail errors. If the server is down, the app prints a friendly line instead of crashing. - The test server received both mails. Its log showed the subject lines and, for the second one, a multipart body with an HTML part and the
directions.txtattachment.
Settings for a Real Mail Provider
Real providers need a secure connection and a login. Keep the login out of the source code, and pass it in from your hosting platform's protected settings.
propertiesspring.mail.host=smtp.provider.example spring.mail.port=587 spring.mail.username=${MAIL_USER} spring.mail.password=${MAIL_PASS} spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true
Port 587 with STARTTLS is the most common. Some providers use port 465 with SSL. Gmail needs an app-specific login rather than your normal one. Never commit the real values to Git.
Common Mistakes
- Sending mail on the request thread. Mail can take seconds. Send it with
@Asyncor from an event listener, so the user does not wait. - Not handling failures. Servers go down. Catch
MailExceptionand decide whether to retry, log or tell the user. - Forgetting multipart mode. Attachments need
new MimeMessageHelper(message, true, ...). Withouttrue, adding an attachment fails. - Testing against a real inbox. Use a local test server such as Mailpit or MailHog during development.
- Building HTML with string glue. For bigger mails, use a template engine so designers can edit the layout.
- Trusting user input in the subject. Line breaks in a subject can be used to inject extra headers. Clean values that come from users.
Interview Questions
Which starter do you need for email?
Ans:spring-boot-starter-mail. It adds Jakarta Mail and creates a JavaMailSender.
What is the difference between `SimpleMailMessage` and `MimeMessage`?
Ans:SimpleMailMessage is for plain text. MimeMessage supports HTML, attachments and inline images.
How do you configure the mail server?
Ans:Set spring.mail.host, spring.mail.port, and if needed spring.mail.username and spring.mail.password in application.properties.
How do you avoid making users wait for email?
Ans:Send it in the background with @Async, or trigger it from an event listener after the main work is done.
How do you test email without a real inbox?
Ans:Run a fake SMTP server such as Mailpit or MailHog and point spring.mail.host to it.
Key Points to Remember
- Add the mail starter and set
spring.mail.hostto get aJavaMailSender. - Use
SimpleMailMessagefor plain text andMimeMessageHelperfor HTML and attachments. - Your app talks to an SMTP server, which delivers the mail.
- Catch
MailException, and send mail off the request thread. - Keep mail logins out of source code.
Frequently Asked Questions
Can I use Gmail for sending email in Spring Boot?
Yes. Use smtp.gmail.com on port 587 with STARTTLS and an app password. For real products, a proper sending service is more reliable.
Why is my JavaMailSender bean missing?
The mail starter may be missing, or spring.mail.host is not set. Both are needed for the bean to be created.
How do I send an email with an attachment?
Create a MimeMessage, wrap it in MimeMessageHelper with multipart set to true, and call addAttachment.
How do I test email safely?
Use a local fake mail server like Mailpit or MailHog. It accepts all mail and shows it in a web page or log, so nothing reaches real people.
Related Topics
- Async Processing with @Async: send mail without making the user wait.
- Spring Events: trigger mail from an event listener.
- Scheduling with @Scheduled: send a daily report by mail.
- Externalized Configuration: keep mail logins out of your code.
Practice Problems
Try each problem on your own first. Both are command line programs that use the JavaMailSender from the mail starter.
Easy: Overdue Book Reminder
PageTurn Library wants a plain text reminder mail to member@example.com with the subject Book overdue: River Songs and the text Please return the book by Friday. Send it with a test mail server on port 1025, and print Reminder sent when done.
Show answerHide answer
SimpleMailMessage is enough for plain text.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.pageturn</groupId> <artifactId>reminder</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.main.banner-mode=off logging.level.root=warn spring.mail.host=localhost spring.mail.port=1025
File: ReminderApplication.java in package com.pageturn.reminder
javapackage com.pageturn.reminder; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; @SpringBootApplication public class ReminderApplication { public static void main(String[] args) { SpringApplication.run(ReminderApplication.class, args); } @Bean CommandLineRunner demo(JavaMailSender mailSender) { return args -> { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom("desk@pageturn.example"); message.setTo("member@example.com"); message.setSubject("Book overdue: River Songs"); message.setText("Please return the book by Friday."); mailSender.send(message); System.out.println("Reminder sent"); }; } }
Output:
textReminder sent
The test server logged one message with the subject line above.
Medium: Retry When the Mail Server Is Down
FreshCart sends order mails, but its mail server is sometimes down. Write a MailRetry service with sendWithRetry(to, subject, text, attempts). It tries up to the given number of times, prints Attempt N failed after each failure, and prints Gave up after N attempts at the end. Test it with no mail server running, using 2 attempts and a short connection timeout.
Show answerHide answer
MailSendException, so both attempts fail and the method gives up. A real app would wait between attempts and run this in the background.File: pom.xml
xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.1.1</version> <relativePath/> </parent> <groupId>com.freshcart</groupId> <artifactId>orders</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
File: application.properties in src/main/resources
propertiesspring.main.banner-mode=off logging.level.root=warn spring.mail.host=localhost spring.mail.port=1025 spring.mail.properties.mail.smtp.connectiontimeout=2000
File: MailRetry.java in package com.freshcart.orders
javapackage com.freshcart.orders; import org.springframework.mail.MailException; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; @Service public class MailRetry { private final JavaMailSender mailSender; public MailRetry(JavaMailSender mailSender) { this.mailSender = mailSender; } public boolean sendWithRetry(String to, String subject, String text, int attempts) { for (int attempt = 1; attempt <= attempts; attempt++) { try { SimpleMailMessage message = new SimpleMailMessage(); message.setFrom("orders@freshcart.example"); message.setTo(to); message.setSubject(subject); message.setText(text); mailSender.send(message); System.out.println("Sent on attempt " + attempt); return true; } catch (MailException e) { System.out.println("Attempt " + attempt + " failed"); } } System.out.println("Gave up after " + attempts + " attempts"); return false; } }
File: OrdersApplication.java in package com.freshcart.orders
javapackage com.freshcart.orders; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class OrdersApplication { public static void main(String[] args) { SpringApplication.run(OrdersApplication.class, args); } @Bean CommandLineRunner demo(MailRetry retry) { return args -> retry.sendWithRetry("buyer@example.com", "Order 55 packed", "Your order is on its way.", 2); } }
Output:
textAttempt 1 failed Attempt 2 failed Gave up after 2 attempts