Use a MIME message when an email needs a file attachment or an image inside its HTML body. Spring Boot configures a JavaMailSender from SMTP properties, and MimeMessageHelper builds the message parts. A plain SimpleMailMessage can send text, but it cannot carry an attachment.
This tutorial uses an order confirmation email. The customer receives an HTML message with a small inline store mark and a CSV receipt attachment. The complete example runs on Spring Boot 4.1.2 and has a local SMTP test, so its MIME structure can be checked without a real mail account.
1. Configure Spring Boot Mail
Add spring-boot-starter-mail. When spring.mail.host is set, Spring Boot creates a JavaMailSender bean unless the application already defines one. The service in section 2 receives that bean through constructor injection.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
The example defaults to a local SMTP server on port 3025. Set the host and port for the SMTP service used by your application. The from address must be one your provider permits; orders@example.test is only a local test address.
spring.mail.host=localhost
spring.mail.port=3025
app.mail.from=orders@example.test
spring.mail.properties[mail.smtp.connectiontimeout]=5000
spring.mail.properties[mail.smtp.timeout]=3000
spring.mail.properties[mail.smtp.writetimeout]=5000
The three timeouts prevent a mail operation from waiting indefinitely for a connection, response or write. The Spring Boot email reference calls out these defaults because an unresponsive SMTP server can otherwise hold an application thread. For an authenticated SMTP server, set SPRING_MAIL_USERNAME and SPRING_MAIL_PASSWORD in the deployment environment. Enable SMTP authentication and TLS according to the provider’s settings; do not assume every server uses the same port or protocol.
spring.mail.properties[mail.smtp.auth]=true
spring.mail.properties[mail.smtp.starttls.enable]=true
These two lines are provider dependent and are not enabled in the local GreenMail test. The test server accepts messages without credentials.
2. Build the Order Confirmation
An order confirmation needs three pieces: HTML text that refers to the image, the inline PNG, and the downloadable receipt. MimeMessageHelper creates the nested MIME parts and attaches them to a MimeMessage. The service accepts receipt bytes, so a controller or order workflow can produce the CSV without writing a temporary file.
2.1. Add the HTML Body and Inline Image
The helper uses MULTIPART_MODE_MIXED_RELATED to keep body content and related resources together while supporting a separate attachment. setText(…, true) marks the body as HTML. The image tag uses cid:brand-mark, which matches the content ID passed to addInline().
The service receives the auto-configured sender and the configured from address through its constructor. In a real deployment, that address must be allowed by the SMTP provider.
private final JavaMailSender sender;
private final String from;
public OrderMailService(JavaMailSender sender, @Value("${app.mail.from}") String from) {
this.sender = sender;
this.from = from;
}
MimeMessage message = sender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(
message, MimeMessageHelper.MULTIPART_MODE_MIXED_RELATED,
StandardCharsets.UTF_8.name());
helper.setFrom(from);
helper.setTo(to);
helper.setSubject("Order " + orderId + " confirmation");
helper.setText("<p>Thanks for your order " + orderId + ".</p>"
+ "<img src='cid:brand-mark' alt='Store mark'>", true);
helper.addInline("brand-mark", new ClassPathResource("mail/brand-mark.png"), "image/png");
Call setText() before addInline(). Some mail readers need that ordering to resolve the content ID reliably, as the Spring Framework mail reference explains. The image is packaged in src/main/resources/mail, so ClassPathResource can read it from both the development classpath and the built JAR. Mail clients can still choose how to display images; the test proves the MIME part and content ID, not the appearance in every client.
2.2. Attach the Receipt and Send
ByteArrayResource supplies the generated CSV as the attachment source. Its filename is what the recipient sees. The service restricts the order ID to letters, digits and hyphens before using it in the subject and filename.
helper.addAttachment("receipt-" + orderId + ".csv",
new ByteArrayResource(receiptCsv), "text/csv");
sender.send(message);
send() hands the message to the configured SMTP server. A successful return means the server accepted the message; it does not prove that the recipient opened it or that a downstream server delivered it. The full OrderMailService in the project contains the validation and both the inline and attachment calls.
For example, the order workflow can supply a receipt for order A-17:
byte[] receipt = "order,total\nA-17,24.50\n".getBytes(StandardCharsets.UTF_8);
orders.sendConfirmation("buyer@example.test", "A-17", receipt);
3. Test What the SMTP Server Receives
The example test starts GreenMail on a random local port and supplies that port to Spring Boot with @DynamicPropertySource. This exercises the configured JavaMailSender, the service, and a real local SMTP exchange. It sends only to a local test mailbox.
static final GreenMail mailServer = new GreenMail(
new ServerSetup(0, "127.0.0.1", "smtp"));
static { mailServer.start(); }
@DynamicPropertySource
static void smtpProperties(DynamicPropertyRegistry properties) {
properties.add("spring.mail.host", () -> "127.0.0.1");
properties.add("spring.mail.port", () -> mailServer.getSmtp().getPort());
}
The test waits for two messages, one plain text and one multipart. It then checks the multipart message’s subject, HTML cid: reference, attachment filename and contents, and inline disposition. The received CSV may use CRLF line endings because MIME transport canonicalizes text lines, so the assertion normalizes line endings before comparing content.
assertThat(mailServer.waitForIncomingEmail(5000, 2)).isTrue();
MimeMessage[] received = mailServer.getReceivedMessages();
assertThat(received).hasSize(2);
List<Part> leaves = new ArrayList<>();
collectLeaves(received[1], leaves);
Part attachment = leaves.stream()
.filter(part -> "receipt-A-17.csv".equals(safeFileName(part)))
.findFirst().orElseThrow();
assertThat(attachment.getDisposition()).isEqualTo(Part.ATTACHMENT);
String receivedCsv = new String(attachment.getInputStream().readAllBytes(),
StandardCharsets.UTF_8).replace("\r\n", "\n");
assertThat(receivedCsv).isEqualTo("order,total\nA-17,24.50\n");
The same test walks the nested MIME parts to find an inline part with content ID <brand-mark>. The complete test class in the source folder includes that traversal. Run it with:
mvn test
The test passed on Spring Boot 4.1.2 with GreenMail 2.1.15. No external SMTP account or Docker service is needed. A local test establishes message composition and SMTP acceptance by GreenMail; provider authentication and real delivery need separate integration checks in the deployment environment.
4. Use a Plain Message When No Attachment Is Needed
If the order email contains only text, SimpleMailMessage is smaller and needs no MIME multipart setup. It uses the same JavaMailSender configuration. The example service includes a plain confirmation method so the test can compare the two message forms.
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject("Order " + orderId + " confirmation");
message.setText("Thanks for your order " + orderId + ".");
sender.send(message);
| Message content | Use | Why |
|---|---|---|
| Plain text only | SimpleMailMessage | No multipart structure is needed |
| HTML, attachment or inline image | MimeMessageHelper | It creates the MIME parts and headers for richer content |
Do not use SimpleMailMessage for the receipt case; there is no attachment method on that class. The Spring Framework email guide linked in section 2.1 shows both APIs and their roles.
5. Frequently Asked Questions
5.1. Why Is JavaMailSender Missing at Startup?
Check that the mail starter is present and spring.mail.host has a value. Spring Boot configures the default sender when both conditions hold and no custom sender bean overrides it. The local project provides the property in application.properties.
5.2. Why Does the Inline Image Appear as an Attachment?
First, verify that the cid: value in the HTML matches the ID passed to addInline(). Call setText() before addInline() and use the correct media type, such as image/png. Then inspect the received MIME parts. Some mail clients display inline resources differently, so a correct MIME structure does not guarantee the same visual result in every client.
5.3. Should the SMTP Password Go in application.properties?
No. Provide SPRING_MAIL_PASSWORD through your deployment platform or secret store, and set SPRING_MAIL_USERNAME if the server requires it. The repository uses a local unauthenticated server configuration and contains no provider credential.
6. Conclusion
Use SimpleMailMessage for text only. For an order receipt or inline image, build a MIME message with MimeMessageHelper, add the HTML before the inline resource, and attach the generated file before calling send(). Test the received MIME parts locally, then verify your provider’s authentication and delivery behavior in its environment.
Happy Learning!