Add fluent MimeMessage builder as a modern alternative to MimeMessageHelper

Author: dominik-kovacsCreated Jul 7, 2026Updated Sep 22, 2026
Labelsstatus: waiting-for-triagein: core

What I'd like

A fluent way to build MIME messages without the ceremony that MimeMessageHelper requires:

java
var message = MimeMessageBuilder.builder()
    .from("[email protected]", "App Name")
    .to(List.of("[email protected]"))
    .cc(ccList)
    .subject("Hello")
    .text("Plain text fallback")
    .html("<h1>Hello</h1>")
    .attachment("report.pdf", bytes, "application/pdf")
    .header("X-Custom", "value")
    .build();

javaMailSender.send(message);

What we do today

Every project I've seen (including ours) ends up with something like this:

java
javaMailSender.send(mimeMessage -> {
    var multipart = hasText(req.getHtml()) || !req.getAttachments().isEmpty();
    var helper = new MimeMessageHelper(mimeMessage, multipart, StandardCharsets.UTF_8.name());

    helper.setTo(req.getTo().toArray(String[]::new));
    if (req.getCc() != null) {
        helper.setCc(req.getCc().toArray(String[]::new));
    }
    if (req.getBcc() != null) {
        helper.setBcc(req.getBcc().toArray(String[]::new));
    }

    if (hasText(req.getHtml()) && hasText(req.getText())) {
        helper.setText(req.getText(), req.getHtml());
    } else if (hasText(req.getHtml())) {
        helper.setText(req.getHtml(), true);
    } else {
        helper.setText(req.getText());
    }

    if (hasText(req.getFromName())) {
        helper.setFrom(req.getFrom(), req.getFromName());
    } else {
        helper.setFrom(req.getFrom());
    }

    if (hasText(req.getSubject())) helper.setSubject(req.getSubject());
    if (hasText(req.getReplyTo())) helper.setReplyTo(req.getReplyTo());
    // attachments...
});

It's not terrible, but it's a lot of defensive code for something that should be straightforward. The main annoyances:

  • You have to figure out the multipart flag yourself before you even start
  • setCc(null) throws, so every optional field needs a null check
  • Lists need toArray(String[]::new) every time
  • Text vs HTML vs both requires a three-way branch
  • None of this is really "our" logic — it's just wiring

Suggested behavior

  • Multipart mode figured out automatically from whether there's HTML/attachments
  • Null or empty optional fields just get ignored (no exception)
  • Takes collections directly
  • If both .text() and .html() are called, it does the right thing (multipart/alternative)
  • UTF-8 by default

I think of it as the same kind of improvement RestClient brought over RestTemplate — the imperative API is still there, but the common case gets much nicer.

Happy to take a stab at a PR if there's interest.

Source: spring-projects/spring-framework