Build and configure

Capabilities

Explore well-formed MIME, reusable message rules, security, diagnostics, conversion, authenticated SOCKS, batches, pools, and SMTP clusters.

Capabilities at a glance

Email essentials.

Pick a common task below, or use the full table of contents for the complete API reference.

Simple Java Mail starts with well-formed MIME messages. The same mailer model also covers reusable defaults and overrides, TLS and OAuth2, DKIM, S/MIME, diagnostics, conversion, authenticated SOCKS, batching, connection pools, and SMTP clusters. Some are absent from Jakarta Mail itself; the rest otherwise require direct work with its Session, MimeMessage, or Transport APIs.

This page is the complete feature reference. If you have a specific job in mind, start with the documentation index. To understand why the library sits above Jakarta Mail and where you can still drop down to it, read Why Simple Java Mail.

Entry classes

The primary entry classes are EmailBuilder and MailerBuilder. Other entry classes are EmailConverter and JMail (the latter as alternative to using the validation methods on the Mailer instance). Finally, MailerHelper exposes some utilities in case you don't actually need to connect to a server.

Default features

Simple Java Mail will do some basic validation checks so that your email is always populated with enough data. It also checks for CRLF injection attacks. It even verifies email addresses against RFC-2822 and others using JMail. Simple Java Mail also takes care of all the connection and security properties for you.

Builders all the way down

Email and Mailer have many optional settings, so Simple Java Mail uses fluent builders instead of long constructors and chains of setters.

  1. The builder API can offer useful combinations while keeping invalid ones out.
  2. Changes happen in the builders, so the objects they produce can remain mostly immutable.
  3. The builder methods also give the Javadocs one clear place to explain every option.

How does Simple Java Mail compare to other mail APIs?

Use the comparison page to see how much Jakarta Mail, Spring Mail, Apache Commons Email and Simple Java Mail handle for you.

Migrating from an older version?

Check the migration notes to see exactly what changed.

§

Basic usage core

Simply build an Email, populate it with your data, build a Mailer and send the Email instance. The mailer can be created with your own Session instance as well.

A Mailer instance is reusable.

Multiple Mailer instances can also be combined into a cluster.

Email email = EmailBuilder.startingBlank()
    .from("Michel Baker", "m.baker@mbakery.com")
    .withRecipients(new RecipientBuilder()
        .withName("mom")
        .withAddress("jean.baker@hotmail.com")
        .withType(Message.RecipientType.TO)
        .build())
    .withRecipients(new RecipientBuilder()
        .withName("dad")
        .withAddress("StevenOakly1963@hotmail.com")
        .withType(Message.RecipientType.TO)
        .build())
    .withSubject("My Bakery is finally open!")
    .withPlainText("Mom, Dad. We did the opening ceremony of our bakery!!!")
	.withHTMLText("<p>Mom, Dad. We did the opening ceremony of <strong>our bakery</strong>!!!</p>")
    .buildEmail();

MailerBuilder
  .withSMTPServer("server", 25, "username", "password")
  .buildMailer()
  .sendMail(email);
§

Use content rendered by your template system core

Render the body with the template system your application already uses, then pass the resulting plain-text and HTML strings to the email builder.

String plainText = templateEngine.render("order-confirmation.txt", model);
String html = templateEngine.render("order-confirmation.html", model);

Email email = currentEmailBuilder
    .withPlainText(plainText)
    .withHTMLText(html)
    .buildEmail();

The %s template accepted by reply starters only controls how the original HTML is quoted. See replying to and forwarding emails.

§

About the fluent API with the Builder pattern core

The entry classes for the builders are EmailBuilder and MailerBuilder.

For EmailBuilder, first say what you are creating. startingBlank() gives you an empty builder; copying() and the reply methods begin with another email; and forwarding() keeps the original message for forwarding. Every starter returns the same populating builder, so content and governance options come afterward.

Email email = EmailBuilder.copying(original)
    .ignoringDefaults()
    .ignoringOverrides()
    .withSubject("A separate copy")
    .buildEmail();

buildEmail() contains the values set through that builder. Defaults and overrides are applied later when a Mailer prepares it for sending. If you need that completed form without sending, use buildEmailCompletedWithDefaultsAndOverrides(), or its overload that accepts an EmailGovernance instance.

For MailerBuilder, the first method determines if you get a full builder API or a reduced API because you provided your own custom Session instance.
If you provide your own session, a lot of properties are presumed to be preconfigured, such as SMTP server details.

§

Configure once, reuse many times core

You can preconfigure a Mailer and use it many times. It is thread-safe.

Mailer inhouseMailer = MailerBuilder
    .withSMTPServer("server", 25, "username", "password")
    .buildMailer();

inhouseMailer.sendMail(email);
inhouseMailer.sendMail(anotherEmail);
Or as preconfigured Spring bean:
@Bean
public Mailer inhouseMailer() {
    return MailerBuilder
        .withSMTPServer(...)
        .buildMailer();
}
Or the default one from the Spring support module:

@Import(SimpleJavaMailSpringSupport.class)

@Autowired Mailer mailer; // configured completely using default properties
§

Alternative API for almost everything core

Most values can be supplied in more than one useful form.

For example, when building an email, add Recipient objects directly, or use RecipientsBuilder when a list needs RFC822 parsing, default names, fixed names, mixed recipient types or a group-level S/MIME certificate. The email builder itself keeps a small set of withRecipients(...) entry points.

Use RecipientsBuilder when a group of recipients should share the same rules. Defaults fill missing values; fixed values replace the source data. For S/MIME, you can keep existing certificates, fill only the missing ones, replace them all, or clear them so the email or mailer fallback certificate is used.

// Add your own Recipient instances
currentEmailBuilder.withRecipients(yourRecipient1, yourRecipient2);
// Or build comma / semicolon separated recipient lists first
String list = "twister@sweets.com,blue.tongue@sweets.com;honey@sweets.com";
Collection<Recipient> archive = new RecipientsBuilder()
    .withDefaultSmimeCertificate(archiveCertificate)
    .withRecipientsWithDefaultName("maintenance group", Message.RecipientType.BCC, list)
    .buildRecipients();
currentEmailBuilder.withRecipients(archive);
// what about a group with one deviating name?
String list = "bob@sweets.com, gene@sweets.com; Security Group <security@sweets.com>";
Collection<Recipient> stakeholders = new RecipientsBuilder()
    .withRecipientsWithDefaultName("stakeholders", Message.RecipientType.TO, list)
    .buildRecipients();
currentEmailBuilder.withRecipients(stakeholders);
// bob and gene are named "stakeholders", "Security Group" keeps its own name
// or force both display name and certificate for a controlled group
Collection<Recipient> legal = new RecipientsBuilder()
    .withFixedSmimeCertificate(legalDepartmentCertificate)
    .withRecipientsWithFixedName("Legal Department", Message.RecipientType.CC,
            "reviewer@example.com", "Counsel <counsel@example.com>")
    .buildRecipients();
currentEmailBuilder.withRecipients(legal);
// all recipients are shown as "Legal Department" and use the same group certificate
// or strip certificate state from reused recipients
Collection<Recipient> reusedWithoutCertificates = new RecipientsBuilder()
    .clearingSmimeCertificates()
    .withRecipients(reusedRecipients)
    .buildRecipients();
currentEmailBuilder
    .withRecipients(reusedWithoutCertificates)
    .encryptWithSmime(emailLevelFallbackCertificate);

Through properties:

simplejavamail.defaults.bcc.name=
simplejavamail.defaults.bcc.address=twister@sweets.com,blue.tongue@sweets.com;honey@sweets.com

The email builder only needs a small set of recipient methods:

// EmailPopulatingBuilder
.withRecipients(Recipient... recipients)
.withRecipients(Collection<Recipient> recipients)
.withRecipients(String name, boolean fixedName, RecipientType recipientType, String... oneOrMoreAddressesEach)

// RecipientsBuilder
.withRecipientsWithDefaultName(String name, RecipientType recipientType, String... oneOrMoreAddressesEach)
.withRecipientsWithFixedName(String name, RecipientType recipientType, String... oneOrMoreAddressesEach)
.withRecipientsFromAddressesWithDefaultName(String name, Collection<InternetAddress> addresses, RecipientType recipientType)
.withRecipientsFromAddressesWithFixedName(String name, Collection<InternetAddress> addresses, RecipientType recipientType)
.withDefaultSmimeCertificate(X509Certificate smimeCertificate)
.withFixedSmimeCertificate(X509Certificate smimeCertificate)
.clearingSmimeCertificates()
.withRecipients(Collection<Recipient> recipients)
.withRecipients(Recipient... recipients)
.buildRecipients()
§

Dedicated recipient builders smime-module

If a recipient needs to be assembled outside an email builder call, use the dedicated recipient builders. This keeps address parsing, recipient type and optional recipient metadata in one small object before it is added to an email. When several recipients belong together, RecipientsBuilder lets you apply their shared names and certificates before adding the group to an email.

RecipientBuilder always creates exactly one recipient: pass one RFC 2822 address to withAddress(...), optionally with a display name. For multiple recipients, use RecipientsBuilder. Its String inputs may be separate addresses or comma- and semicolon-delimited lists, and every parsed address becomes its own recipient. Set the recipient type explicitly to TO, CC or BCC in either builder.

Default names and default certificates only fill missing values. Fixed names and fixed certificates override source data for the whole group. clearingSmimeCertificates() removes certificate state from reused recipients when the current email should use its own S/MIME fallback instead.

Recipient-level S/MIME certificates are described in the S/MIME security section.

// Exactly one address
Recipient alice = new RecipientBuilder()
    .withName("Alice")
    .withAddress("alice@example.com")
    .withType(Message.RecipientType.TO)
    .build();

// Separate inputs and delimited lists can be combined
Collection<Recipient> reviewers = new RecipientsBuilder()
    .withRecipientsWithDefaultName(null, Message.RecipientType.CC,
            "Bob <bob@example.com>",
            "carol@example.com;dave@example.com")
    .buildRecipients();

Email email = EmailBuilder.startingBlank()
    .from("sender@example.com")
    .withRecipients(alice)
    .withRecipients(reviewers)
    .withPlainText("Hello")
    .buildEmail();
§

Authentication and OAuth2 support core

You use one of the builder methods on MailerBuilder for defining the server properties like host, port, username and password.

Simple Java Mail supports plain SMTP (default, but not recommended), SMTPS (legacy SSL) or TLS (recommended). The last authentication option to join the family is OAuth2 (by means of XOAUTH2 which comes built-in in Jakarta Mail, the underlying SMTP framework).

For a long-lived Mailer, an OAuth2AccessTokenProvider supplies a current token whenever Simple Java Mail opens or reconnects a physical SMTP connection. Your provider handles acquisition, expiry, caching and refresh. An already-connected pooled transport is reused without another provider call.

MailerBuilder
  .withSMTPServer("server host", 587, "username", yourPassword)
  .withTransportStrategy(TransportStrategy.SMTP_TLS)
  .buildMailer()
  .sendMail(email);
OAuth2AccessTokenProvider accessTokens =
    () -> tokenService.currentAccessToken();

MailerBuilder
  .withSMTPServer("server host", 587, "username")
  .withTransportStrategy(TransportStrategy.SMTP_OAUTH2)
  .withOAuth2AccessTokenProvider(accessTokens)
  .buildMailer()
  .sendMail(email);

The existing fixed-token form remains supported: pass the token as the SMTP password when it will stay valid for the Mailer's lifetime.

See the security page for a more in-depth explanation of transport strategies.

§

Asynchronous sending, simple batches and clustering batch-module

The default mode is to send emails synchronously, blocking execution until the email was processed completely and the SMTP server sent a successful result.

You can also send asynchronously in parallel or batches, or simply send in a fire-and-forget way. If an authenticated proxy is used, the proxy bridging server is kept alive until the last email has been sent.

Depending on the SMTP server (and proxy server if used) this can greatly influence how fast emails are sent.

mailer.sendMail(email, /* async = */ true);
Or configure it when building the mailer:

Mailer mailer = mailerBuilder
  .(..)
  .async()
  .buildMailer();

mailer.sendMail(email);

Refer to the configuration section on how to set thread-pool defaults or configure the executor service, and to Mailer lifecycle and resource ownership for orderly shutdown.

Simple sequential batch sending
If reconnecting for each email is the only overhead you need to avoid, use sendMailsInSimpleBatch(...). It sends multiple emails sequentially over one SMTP connection.

mailer.sendMailsInSimpleBatch(emails);        // uses the mailer's async default
mailer.sendMailsInSimpleBatch(emails, false); // blocks until the batch is done
mailer.sendMailsInSimpleBatch(emails, true);  // schedules the whole batch asynchronously

If the caller owns the queue and needs custom work between sends, use withOpenConnection(...). The scoped sender keeps one SMTP connection open for the callback, but it is still caller-managed, sequential and not pooled. This form uses the Mailer's built-in SMTP connection. A CustomMailer owns its own connection, so call sendMail(...) normally and manage any connection reuse inside that implementation.

mailer.withOpenConnection(sender -> {
    while (database.hasPendingMail()) {
        Email next = database.nextPendingMail();
        sender.sendMail(next);
        database.markSent(next);
    }
});

Advanced pooled batch processing
For sustained high-volume sending, use pooled delivery. It reuses SMTP connections over multiple sends and can cluster multiple pools for different SMTP servers.

First use the pool orchestration guide to choose the delivery path and lifecycle boundary that match your code. Then refer to the configuration section for Mailer settings. Its lifecycle section explains how to finish queued work and close each Mailer.

§

Handling asynchronous results core

Passing true to sendMail(...) or testConnection(...) runs the operation asynchronously. The returned CompletableFuture covers the whole operation: applying defaults and overrides, validation, scheduling, conversion, connection and transport. A failure at any of those stages completes the future exceptionally.

Preparation and validation still happen on the calling thread before the transport work is scheduled. In asynchronous mode, their failures are placed on the future instead of escaping from the method call. Clear call errors, such as a null email argument, can still be thrown immediately.

With false, the work runs on the calling thread. A successful call returns an already-completed future; a failure is thrown directly.

CompletableFuture<Void> future = mailer.sendMail(email, true);

future.whenComplete((unused, failure) -> {
    if (failure != null) {
        log.error("Unable to send email", failure);
    }
});
§

Sending with your own Session instance core

If you prefer to use your own preconfigured Session instance and still benefit from Simple Java Mail, you can!

Email email = ...
...

MailerBuilder
    .usingSession(yourSession)
    .buildMailer()
    .sendMail(email);
§

Changing the content encoding core

By default content is encoded in the target MimeMessage or EML using quoted-printable, so the EML is nicely and safely readable. You can also use other encoders. Base64, for example, turns the content into ASCII text for reliable MIME transport. It is encoding, not encryption; the original content remains directly decodable.

The email header that governs this feature is Content-Transfer-Encoding and Simple Java Mail takes care of inserting this header in the right places, which varies depending on the necessary email structure.

quoted-printable (the default) results in the following EML subsection:
------=_Part_2_1226020905.1657715639009
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

We should meet up!

Base64, on the other hand, produces the following:

currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.BASE_64);
------=_Part_2_1226020905.1657715871203
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64

V2Ugc2hvdWxkIG1lZXQgdXAh

Encoding attachments

You can control encoding separately for attachments, so your text files for examples can be encoded differently too.

.withAttachment("invitation.pdf", yourDataSource,
                "Invitation flyer", ContentTransferEncoding.BASE_64)

// or fix encoding by creating attachment objects directly
new AttachmentResource(..., ContentTransferEncoding.BINARY)

Available encoders

currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.QUOTED_PRINTABLE);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.BASE_64);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.BINARY);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.B);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.Q);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.BIT7);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.BIT8);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.UU);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.X_UU);
// or:
currentEmailBuilder.withContentTransferEncoding(ContentTransferEncoding.X_UUE);

// Or restore the quoted-printable default:
currentEmailBuilder.clearContentTransferEncoding();

Encoding body parts separately

The global content-transfer encoding is still the fallback, but plain text, HTML and calendar body parts can override it individually. The same defaults can be supplied through properties.

currentEmailBuilder
    .withContentTransferEncoding(ContentTransferEncoding.QUOTED_PRINTABLE)
    .withPlainTextContentTransferEncoding(ContentTransferEncoding.BIT7)
    .withHTMLTextContentTransferEncoding(ContentTransferEncoding.BASE_64)
    .withCalendarTextContentTransferEncoding(ContentTransferEncoding.QUOTED_PRINTABLE);
simplejavamail.defaults.content.transfer.encoding=QUOTED_PRINTABLE
simplejavamail.defaults.body.text.content.transfer.encoding=BIT7
simplejavamail.defaults.body.html.content.transfer.encoding=BASE_64
simplejavamail.defaults.body.calendar.content.transfer.encoding=QUOTED_PRINTABLE
§

Setting a custom message ID on sent email core

Message id's are normally generated by the underlying Jakarta Mail framework, but you can provide your own if required.

Just make sure your own id's conform to the rfc5322 msg-id format standard

currentEmailBuilder.fixingMessageId("<123@456>");
§

Setting a custom sent date on sent email core

Message sent date is normally filled with the current date, but you can provide your own date if required.

currentEmailBuilder.fixingSentDate(new GregorianCalendar(2011, APRIL, 1, 3, 51).getTime());
§

Getting the generated email id after sending core

Sometimes you need the actual ID used in the MimeMessage that went out to the SMTP server. Luckily, it's very easy to retrieve it.


mailer.sendMail(email); // id updated during sending!
email.getId(); // <1420232606.6.1509560747190@Cypher>
§

Reading SMTP submission receipts core

When a log, trace, or background job needs the SMTP server's response for a submitted message, use sendMailAndGetReceipt(...). It follows the same send path as sendMail(...), but returns a MailSubmissionReceipt.

With the built-in Angus SMTP transport, the receipt contains the return code and final SMTP response, such as a 250 ... queued as ... response. Logging-only transport mode and custom mailers still return a receipt after successful processing, but without an SMTP response.

This confirms submission acceptance by the SMTP server, not final recipient mailbox delivery. Use Delivery Status Notification, bounces, read receipts or provider-specific tracking when you need final delivery signals.

The open-connection example uses the Mailer's built-in SMTP transport. It cannot be combined with a CustomMailer, which owns its own connection lifecycle.

MailSubmissionReceipt receipt = mailer
    .sendMailAndGetReceipt(email, false)
    .get();

receipt.getSmtpResponse().ifPresent(response ->
    System.out.printf("SMTP accepted %s: %d %s%n",
        receipt.getEmailId(),
        response.getReturnCode(),
        response.getResponse()));
mailer.withOpenConnection(sender -> {
    MailSubmissionReceipt receipt = sender.sendMailAndGetReceipt(email);
    database.markSubmitted(email, receipt.getSmtpResponse().orElse(null));
});
§

Sending with SSL and TLS core

Activating SSL or TLS is super easy. Just use the appropriate TransportStrategy enum.

Email email = ...;

MailerBuilder.withTransportStrategy(TransportStrategy.SMTP); // default
// or:
MailerBuilder.withTransportStrategy(TransportStrategy.SMTPS);
// or:
MailerBuilder.withTransportStrategy(TransportStrategy.SMTP_TLS);
// or:
MailerBuilder.withTransportStrategy(TransportStrategy.SMTP_OAUTH2);
Or with property default:
simplejavamail.transportstrategy=SMTP
# or: SMTPS, SMTP_TLS, SMTP_OAUTH2

Customizing SSL connections further
For maximum control, you can provide your own SSLSocketFactory too:

mailerBuilder
	.withCustomSSLFactoryClass(theClassName)
	.withCustomSSLFactoryInstance(theInstance) // takes precedence
	.buildMailer();
Or with property default:
simplejavamail.custom.sslfactory.class=you.project.YourSSLSocketFactory
§

SSL and TLS with Google mail core

Here's an example of SSL and TLS using gMail.

If you have two-factor login turned on, you need to generate an application specific password from your Google account.

MailerBuilder
  .withSMTPServer("smtp.gmail.com", 25, "your user", "your password")
  .withTransportStrategy(TransportStrategy.SMTP_TLS);
// or:
MailerBuilder
  .withSMTPServer("smtp.gmail.com", 587, "your user", "your password")
  .withTransportStrategy(TransportStrategy.SMTP_TLS);
// or:
MailerBuilder
  .withSMTPServer("smtp.gmail.com", 465, "your user", "your password")
  .withTransportStrategy(TransportStrategy.SMTPS);
§

Adding attachments core

You can add attachments very easily, but you'll have to provide the data yourself. Simple Java Mail accepts byte[] and DataSource objects.

currentEmailBuilder
    .withAttachment("dresscode.txt", new ByteArrayDataSource("Black Tie Optional", "text/plain"))
    .withAttachment("location.txt", "On the moon!".getBytes(Charset.defaultCharset()), "text/plain")
    // of course it can be anything: a pdf, doc, image, csv or anything else
    .withAttachment("invitation.pdf", new FileDataSource("invitation_v8.3.pdf"))
	// you can provide your own list of attachments as well
    .withAttachments(yourAttachmentResourceCollection);

If for some reason you need the Content-Description header set as well, you can provide a content description on any attachment.

currentEmailBuilder.withAttachment(
	        "dresscode.txt",
	        new ByteArrayDataSource("Black Tie Optional", "text/plain"),
	        "The dresscode for the party")
Which results in something like this; the UUID is only an example:
Content-Type: text/plain; name="dresscode.txt"
Content-Disposition: attachment; filename="dresscode.txt"
Content-ID: <sjm-550e8400-e29b-41d4-a716-446655440000@simplejavamail.generated>
Content-Description: The dresscode for the party

Black Tie Optional

Because no Content-ID was supplied, Simple Java Mail generates a fresh opaque value when it produces the MimeMessage. Use the explicit Content-ID overload shown below when another system needs a stable value.

You can further control how the attachment is encoded in the MimeMessage / EML until the data read back from the MimeMessage, by providing a content transfer encoding (resulting in a Content-Transfer-Encoding header for the attachment).

If attachment data is already encoded, use a pre-encoded attachment method. This tells Simple Java Mail to preserve the payload and use the provided ContentTransferEncoding value instead of encoding the data again.

Attachment resource names and MIME Content-ID values can also be set separately.

currentEmailBuilder
    .withPreEncodedAttachment(
        "report.pdf",
        alreadyBase64EncodedData,
        "application/pdf",
        ContentTransferEncoding.BASE_64)
    .withAttachment(
        "report-display-name.pdf",
        reportDataSource,
        "Monthly report",
        ContentTransferEncoding.BASE_64,
        "stable-report-content-id")
§

Embedding images core

Embedding images dead simple with two options:

  1. manual embedding: add cid: placeholders in the HTML yourself
  2. auto resolution: enable auto-resolving image sources to files, class path resources or URL's

Manual embedding:

currentEmailBuilder.withEmbeddedImage("smiley", new FileDataSource("smiley.jpg"));
currentEmailBuilder.withEmbeddedImage("thumbsup", parseBase64Binary(base64String), "image/png");
currentEmailBuilder.withEmbeddedImage("brand-logo.png", logoDataSource, "brand-logo-cid");
// above example is included in the demo package in MailTestApp.java

// the corresponding HTML should contain the placeholders
<p>Let's go!</p><img src='cid:thumbsup'><br/>
<p>Smile!</p><img src='cid:smiley'>
<p>Logo</p><img src='cid:brand-logo-cid'>

Pre-encoded embedded images work the same way as pre-encoded attachments: provide the already-encoded data and the encoding that is already present.

currentEmailBuilder
    .withPreEncodedEmbeddedImage(
        "logo.png",
        alreadyBase64EncodedLogo,
        "image/png",
        ContentTransferEncoding.BASE_64)
    .withPreEncodedEmbeddedImage(
        "logo.png",
        logoDataSource,
        ContentTransferEncoding.BASE_64,
        "stable-logo-content-id")

Auto resolution is opt-in. It can read image sources from files, classpath resources and URLs, then embed what it finds. If someone else can edit the HTML, enable only the resolver you need, configure its base, and leave outside-base access disabled. Since 9.2, each configured base is an actual boundary rather than just a lookup hint.

Without a base, resolution is unrestricted. Turning on an allowingEmbeddedImageOutsideBase... option deliberately removes that boundary. Use embeddedImageAutoResolutionMustBeSuccesful(true) when a missing or blocked image should fail the build instead of remaining in the HTML.

Auto resolution:

<p>Let's go!</p><img src='smiley.jpg'><br/>
<p>Smile!</p><img src='https://www.myplace.com/smiley.png'>

// results in the following HTML when building the email:
<p>Let's go!</p><img src='cid:etweffxdeu'><br/>
<p>Smile!</p><img src='cid:sienfddiew'>

email.getEmbeddedImages(); // now contains two data sources!

To enable this:

emailBuilder
	// enable auto resolution
	.withEmbeddedImageAutoResolutionForFiles(true) // default false
	.withEmbeddedImageAutoResolutionForClassPathResources(true) // default false
	.withEmbeddedImageAutoResolutionForURLs(true) // default false

	// contain resolution to these locations (recommended for editable HTML)
	.withEmbeddedImageBaseDir(RESOURCES_PATH + "/images")
	.withEmbeddedImageBaseUrl("https://www.simplejavamail.org/static/")
	.withEmbeddedImageBaseClassPath("/images")

	// optional escape hatches; do not enable these for editable HTML
	// .allowingEmbeddedImageOutsideBaseDir(true) // default false
	// .allowingEmbeddedImageOutsideBaseClassPath(true) // default false
	// .allowingEmbeddedImageOutsideBaseUrl(true) // default false

	// fail if a resource couldn't be resolved
	.embeddedImageAutoResolutionMustBeSuccesful(true) // default false (lenient mode)
Also works with properties:
simplejavamail.embeddedimages.dynamicresolution.enable.dir=true
simplejavamail.embeddedimages.dynamicresolution.enable.url=true
simplejavamail.embeddedimages.dynamicresolution.enable.classpath=true
simplejavamail.embeddedimages.dynamicresolution.base.dir=...
simplejavamail.embeddedimages.dynamicresolution.base.url=https://www.simplejavamail.org/static/
simplejavamail.embeddedimages.dynamicresolution.base.classpath=/images

# Explicit escape hatches; leave these false for editable HTML:
simplejavamail.embeddedimages.dynamicresolution.outside.base.dir=false
simplejavamail.embeddedimages.dynamicresolution.outside.base.classpath=false
simplejavamail.embeddedimages.dynamicresolution.outside.base.url=false
simplejavamail.embeddedimages.dynamicresolution.mustbesuccesful=true

Upgrading from an earlier version? Read the 9.2 containment notes.

§

Setting custom headers core

Sometimes you need extra headers in your email because your email server, recipient server or your email client needs it. Or perhaps you have a proxy or monitoring setup in between mail servers. Whatever the case, adding headers is easy.

currentEmailBuilder
    .withHeader("X-Priority", 2);
    .withHeader("X-MC-GoogleAnalyticsCampaign", "halloween_sale");
    .withHeader("X-MEETUP-RECIP-ID", "71415272");
    .withHeader("X-my-custom-header", "foo");
    // or
    .withHeaders(yourHeadersMap);
§

Setting custom properties on the internal Session core

In case you need to modify the internal Session object itself, because you need a tailored configuration that is supported by the underlying Jakarta Mail, that too is very easy.

currentMailerBuilder
    .withProperty("mail.smtp.timeout", 30 * 1000)
    .withProperty("mail.smtp.connectiontimeout", 10 * 1000)
    // or
    .withProperties(yourPropertiesObject)
    .withProperties(yourPropertiesMap)

You can also set some default properties to automatically be added.

Every property prepended with simplejavamail.extraproperties will be loaded directly on the internal Session object.

simplejavamail.extraproperties.my.extra.property=value
simplejavamail.extraproperties.mail.smtp.ssl.socketFactory.class=org.mypackage.MySSLSocketFactory
simplejavamail.extraproperties.mail.smtp.timeout=30000
§

Routing Jakarta Mail debug output core

Jakarta Mail can produce low-level protocol debug output. Simple Java Mail can keep the old console behavior, route it to stderr, or send it to the org.simplejavamail.javamail.debug SLF4J logger.

Mailer mailer = MailerBuilder
    .withSMTPServer("smtp.example.com", 587, "user", "password")
    .withDebugLogging(true)
    .withDebugOutput(SessionDebugOutput.SLF4J)
    .buildMailer();
simplejavamail.javaxmail.debug=true
simplejavamail.javaxmail.debug.out=SLF4J
§

Binding the local/source SMTP address core

On machines with multiple local IP addresses, the outgoing SMTP socket can be bound to a specific local/source address. Simple Java Mail applies this to the right Jakarta Mail property for the configured transport strategy.

Leave the local port empty unless you specifically need it. Binding a fixed local port can conflict with another connection.

Mailer mailer = MailerBuilder
    .withSMTPServer("smtp.example.com", 587, "user", "password")
    .withTransportStrategy(TransportStrategy.SMTP_TLS)
    .withLocalBindAddress("203.0.113.10")
    .buildMailer();
simplejavamail.smtp.localaddress=203.0.113.10
simplejavamail.smtp.localport=25252
§

Setting the SMTP EHLO/HELO client hostname core

Corporate SMTP relays sometimes expect a stable, recognizable client hostname in the SMTP EHLO or HELO command. Even when the relay does not enforce it, this makes application traffic recognizable in mail-server logs, tracing, audit trails and SMTP diagnostics.

This is the SMTP protocol identity and maps to mail.smtp.localhost or mail.smtps.localhost depending on the transport strategy. It is not the same as binding the local/source socket address, and it does not change the SMTP envelope sender.

Mailer mailer = MailerBuilder
    .withSMTPServer("smtp.example.com", 587, "user", "password")
    .withTransportStrategy(TransportStrategy.SMTP_TLS)
    .withSmtpClientHostname("orders-service.prod.example.com")
    .buildMailer();
simplejavamail.smtp.clienthostname=orders-service.prod.example.com
§

Sending a Calendar event (iCalendar vEvent) core

You want to send a nice Calendar event (.ics) that a client such as Outlook processes nicely?

Easy!

Produce a Calendar event String (manually or by using a library such as ical4j) and pass it to the EmailBuilder.

See the test demo app included in the Simple Java Mail source for a working example.

// Create a Calendar with something like ical4j
Calendar icsCalendar = new Calendar();
icsCalendar.getProperties().add(new ProdId("-//Events Calendar//iCal4j 1.0//EN"));
icsCalendar.getProperties().add(Version.VERSION_2_0);

(..) // add attendees, organizer, end/start date and whatever else you need

// Produce calendar string
ByteArrayOutputStream bOutStream = new ByteArrayOutputStream();
new CalendarOutputter().output(icsCalendar, bOutStream);
String yourICalEventString = bOutStream.toString("UTF-8");

// Let Simple Java Mail handle the rest
currentEmailBuilder
    .withCalendarText(CalendarMethod.REQUEST, yourICalEventString);
§

Direct access to the internal Session core

For emergencies, you can also get a hold of the internal Session instance itself. You should never need this however and if you do it means Simple Java Mail failed to simplify the configuration process for you. Please let us know how we can help alleviate this need.

Mailer mailer = ...;

Session session = mailer.getSession();
// do your thing with session
§

Configure delivery / read receipt core

Simple Java Mail can add two receipt-request headers. Disposition-Notification-To asks supporting mail clients for a read receipt. Return-Receipt-To asks supporting servers or clients for a delivery or read notification, depending on their implementation.

Simple Java Mail writes the requested headers; the receiving server, mail client or user controls whether a receipt is returned. Delivery Status Notification, covered in the next section, is the separate SMTP-level mechanism for delivery status.

You can provide the receipt address explicitly. The no-argument methods use the first Reply-To recipient when present, or the From recipient otherwise.

Email email = EmailBuilder.startingBlank()
    .from("sender@example.com")
    .withRecipients(new RecipientBuilder()
        .withAddress("recipient@example.com")
        .withType(Message.RecipientType.TO)
        .build())
    .withPlainText("Hello")
    .withDispositionNotificationTo("Mail receipts", "receipts@example.com")
    .withReturnReceiptTo("Mail receipts", "receipts@example.com")
    .buildEmail();
§

Configure Delivery Status Notification core

Delivery Status Notification (DSN) asks the SMTP server for delivery-status reports such as failure, delay or success notifications. This is separate from read receipts: DSN is about delivery status at SMTP/server level.

DSN can be configured per email, through mailer defaults or through properties.

Email email = EmailBuilder.startingBlank()
    .from("sender@example.com")
    .withRecipients(new RecipientBuilder()
        .withAddress("receiver@example.com")
        .withType(Message.RecipientType.TO)
        .build())
    .withPlainText("Hello")
    .withDeliveryStatusNotification(
        DeliveryStatusNotification.ReturnOption.HEADERS_ONLY,
        DeliveryStatusNotification.NotifyOption.FAILURE,
        DeliveryStatusNotification.NotifyOption.DELAY)
    .buildEmail();
simplejavamail.defaults.delivery.status.notification.notify=FAILURE,DELAY
simplejavamail.defaults.delivery.status.notification.return.option=HEADERS_ONLY
§

Validating Email Addresses core

Simple Java Mail validates email addresses with JMail, which performs standards-aware checks rather than relying on a single regular expression. See RFC 2822 for one of the relevant specifications.

When sending, the mailer applies its defaults and overrides before checking the resulting email. Calling mailer.validate(email) directly runs the required sender and recipient checks, configured address validation, encoded-word protection and CRLF-injection scan against the supplied Email as it stands. MIME conversion and the final encoded-size check happen during sending.

See JMail for more examples and configurations.

currentMailerBuilder
    .withEmailValidator(
		JMail.strictValidator()
    		.requireOnlyTopLevelDomains(TopLevelDomain.DOT_COM)
    		.withRule(email -> email.localPart().startsWith("allowed"))
	)
    // or
    .clearEmailValidator() // retain completeness, encoded-word and CRLF checks
    .resetEmailValidator() // restore the default strict JMail address validator
// validate the supplied Email as it stands:
mailer.validate(email); // sender/recipients, addresses and injection-sensitive fields

// or just do the address validation
JMail.isValid("your_address@domain.com");

// or, fine-tuned to be stricter
JMail.strictValidator()
	.isValid("your_address@domain.com");

clearEmailValidator() clears only the configurable JMail address policy. Required sender and recipient checks, encoded-word protection and CRLF injection scanning remain active.

disablingAllClientValidation(true) changes client-side validation findings into warnings so SMTP handling can continue. Use this mode when the application deliberately assigns the acceptance decision to the SMTP server.

currentMailerBuilder
    .disablingAllClientValidation(true) // report validation findings as warnings
    // or
    .resetDisableAllClientValidations() // restore blocking validation
§

Converting between Email, MimeMessage, EML and Outlook .msg outlook-module smime-module

With Simple Java Mail you can easily convert between email types. This includes reading S/MIME protected emails from file.

For example, if you need a MimeMessage, you can convert Email objects, EML data and even Outlook .msg files.

If you already have a MimeMessage, you can convert it into an Email instance with its addresses, content, embedded images, attachments, and non-structural custom headers preserved where possible. You can also parse just the metadata without fetching attachment data.

Conversion does not copy the original header block verbatim. Addressing, date, subject, reply, message ID, and MIME content headers are represented by Email fields or rebuilt when another MimeMessage is produced. Transport-history headers such as Received and Resent-*, along with obsolete structural metadata, are omitted. See the complete filtered-header list.

You can even build a mass Outlook .msg to EML converter if you like!

For Outlook .msg input, the String overload takes a file path. Use the File or InputStream overload for those source types.

/*
 * Most conversion methods support an optional Pkcs12Config config for handling S/MIME
 */

// from Email
String eml =              EmailConverter.emailToEML(yourEmail);
MimeMessage mimeMessage = EmailConverter.emailToMimeMessage(yourEmail);
MimeMessage mimeMessage = EmailConverter.emailToMimeMessage(yourEmail, yourSession);

// from MimeMessage
Email email =             EmailConverter.mimeMessageToEmail(yourMimeMessage);
String eml =              EmailConverter.mimeMessageToEML(yourMimeMessage);

// from EML
Email email =             EmailConverter.emlToEmail(emlDataString);
MimeMessage mimeMessage = EmailConverter.emlToMimeMessage(emlDataString);
MimeMessage mimeMessage = EmailConverter.emlToMimeMessage(emlDataString, yourSession);

// from Outlook .msg: path String
Email emailFromPath =             EmailConverter.outlookMsgToEmail("yourMessage.msg");
String emlFromPath =              EmailConverter.outlookMsgToEML("yourMessage.msg");
MimeMessage mimeMessageFromPath = EmailConverter.outlookMsgToMimeMessage("yourMessage.msg");

// from Outlook .msg: File
File msgFile = new File("yourMessage.msg");
Email emailFromFile =             EmailConverter.outlookMsgToEmail(msgFile);
String emlFromFile =              EmailConverter.outlookMsgToEML(msgFile);
MimeMessage mimeMessageFromFile = EmailConverter.outlookMsgToMimeMessage(msgFile);

// from Outlook .msg: fresh binary InputStream for each conversion
try (InputStream msgInput = Files.newInputStream(Path.of("yourMessage.msg"))) {
    Email emailFromStream = EmailConverter.outlookMsgToEmail(msgInput);
}
try (InputStream msgInput = Files.newInputStream(Path.of("yourMessage.msg"))) {
    String emlFromStream = EmailConverter.outlookMsgToEML(msgInput);
}
try (InputStream msgInput = Files.newInputStream(Path.of("yourMessage.msg"))) {
    MimeMessage mimeMessageFromStream = EmailConverter.outlookMsgToMimeMessage(msgInput);
}

Pkcs12Config myKeyInfo = Pkcs12Config.builder()
    .pkcs12Store("my_smime_keystore.pkcs12")
    .storePassword("my_store_password")
    .keyAlias("my_key_alias")
    .keyPassword("my_key_password")
    .build();

Email decryptedEmail =    EmailConverter.emlToEmail(emlDataString, myKeyInfo);
Email decryptedEmail =    EmailConverter.mimeMessageToEmail(yourMimeMessage, myKeyInfo);
Email decryptedEmail =    EmailConverter.mimeMessageToEmail(yourMimeMessage, myKeyInfo, /*fetchAttachments*/ false);
emailBuilder   	     =    EmailConverter.mimeMessageToEmailBuilder(yourMimeMessage, /*Pkcs12Config*/ null, /*fetchAttachments*/ true);

When converting Outlook .msg files, use the result API if you need to inspect source-specific Outlook data. The converted email keeps only meaningful email headers; structural Outlook source headers stay available through OutlookMessageData.

OutlookEmailConversionResult result =
    EmailConverter.outlookMsgToEmailBuilderWithOutlookData(msgFile);

Email email = result.buildEmail();
OutlookMessageData outlookData = result.getOutlookMessageData();

List<String> receivedHeaders = outlookData.getHeaderValues("Received");
String messageClass = outlookData.getMessageClass();
String rawHeaders = outlookData.getRawHeaders();

Outlook conversion is lenient about unsupported encrypted S/MIME payloads. For signed S/MIME content, parsed email content is preserved even when cryptographic signature verification fails. Check email.getOriginalSmimeDetails().getSmimeSignatureValid() when integrity matters; a true result does not establish certificate trust or sender identity. See What the signature status means for the exact contract.

§

Setting a bounce address core

withBounceTo(...) sets the SMTP envelope sender (MAIL FROM) for that email. Delivery failures are normally returned to this address. The setting is applied per message, so emails sent through the same Mailer can use different bounce addresses without changing its shared Session.

This is different from Reply-To, which is a message header used by mail clients when a recipient replies. The bounce address is used by mail servers during delivery and is not written into the outgoing message headers. A receiving mail system may record it in a Return-Path header. Only the address is used; a display name has no role in the SMTP envelope.

currentEmailBuilder
    .withBounceTo("bounces@candyshop.com");
§

Replying to and forwarding emails core

EmailBuilder can start a reply, reply-all, or forward from an existing Email or MimeMessage. Each starter returns the same EmailPopulatingBuilder used for a new message.

Start with What it prepares Recipients
replyingTo(...) A reply subject, reply headers, quoted plain and HTML bodies, and the original embedded images The original message's reply address
replyingToAll(...) The same quoted reply, prepared through Jakarta Mail's reply-all behavior The reply address and the other recipients calculated from the original message
forwarding(...) A forward subject and the original message as a message/rfc822 body part None; add the forward's recipients yourself

Keep the quoted message when adding your reply core

Both reply starters quote the original message in plain text and HTML. Add your response to both alternatives because the receiving mail client normally displays only one of them.

A custom quote template changes the HTML alternative and must contain %s, which is replaced with the original HTML. The plain-text alternative keeps its usual > quoting.

Use prependText(...) and prependTextHTML(...) to keep the quoted content. Calling withPlainText(...) or withHTMLText(...) here would replace it.

See the reply starter overloads for the complete Email and MimeMessage API.

String quoteMarkup =
    "<blockquote class=\"quoted\">%s</blockquote>";

Email reply = EmailBuilder
    .replyingToAll(receivedEmail, quoteMarkup)
    .from("sender@example.com")
    .prependText("Thanks, I'll take care of it.\n\n")
    .prependTextHTML("<p>Thanks, I'll take care of it.</p>")
    .buildEmail();

Replace, extend, or remove body alternatives core

The same body-editing methods work after startingBlank(), copying(...), or any reply starter. Keep the plain and HTML operations paired when the message contains both alternatives.

Change Paired methods Effect
Replace withPlainText(String | File)
withHTMLText(String | File)
Replaces the current alternative
Prepend prependText(String | File)
prependTextHTML(String | File)
Adds content before the current alternative
Append appendText(String | File)
appendTextHTML(String | File)
Adds content after the current alternative
Remove clearPlainText()
clearHTMLText()
Removes that alternative from the email
Email revised = EmailBuilder.copying(draft)
    .withPlainText(new File("mail/body.txt"))
    .withHTMLText(new File("mail/body.html"))
    .prependText("Reference: 42\n\n")
    .prependTextHTML("<p>Reference: 42</p>")
    .appendText("\n\nContact support@example.com")
    .appendTextHTML("<p>Contact support@example.com</p>")
    .buildEmail();

See EmailPopulatingBuilder for every overload and the other fields that can be cleared when copying or rebuilding an email.

Reply-To is not an outgoing recipient core

Reply-To tells a recipient's mail client where a future reply should go. It does not add anyone to the current message's To, CC, or BCC recipients.

Call withReplyTo(...) more than once, or pass a List<Recipient>, when replies may go to more than one address.

Email email = EmailBuilder.startingBlank()
    .from("notices@example.com")
    .withRecipients(customer)
    .withReplyTo("Support", "support@example.com")
    .withReplyTo("Escalations", "escalations@example.com")
    .withSubject("Service notice")
    .withPlainText("Planned maintenance starts at 22:00.")
    .buildEmail();
§

Send using a proxy authenticated-socks-module

Simple Java Mail supports sending email through a SOCKS proxy, including proxy username/password negotiation. This is an uncommon built-in capability in Java mail libraries. The underlying Jakarta Mail framework supports anonymous SOCKS5 proxies, but not authenticated proxies. Proxying works with SMTP, STARTTLS and implicit-TLS SMTPS connections.

To make this work with authentication, Simple Java Mail uses a trick: it sets up a temporary anonymous proxy server for Jakarta Mail to connect to and then the bridge relays the connection to the target proxy performing the authentication outside of Jakarta Mail.

The bridge listens only on the JVM's loopback address and shuts down after the last active mail operation. It is an implementation detail for Jakarta Mail, not a general-purpose network proxy.

This temporary server is referred to as the Proxy Bridging Server.

// anonymous proxy
currentMailerBuilder.withProxy("proxy.host.com", 1080)

// authenticated proxy
currentMailerBuilder.withProxy("proxy.host.com", 1080, "proxy username", "proxy password");

See Authenticated proxy bridge ports for bridge ownership, port collisions, and applications that use more than one authenticated-proxy Mailer.

§

Testing a server connection core

If you just want to do a connection test using your current configuration, including transport strategy and (authenticated) proxy, Simple Java Mail got you covered.

The connection test can also be done asynchronously and the result can be handled asynchronously as well. Take a look at Handling asynchronous mailing result.

If the mailer was configured with async(), no-arg testConnection() uses that async default. Use testConnection(false) when you need a blocking test.

// configure your mailer
Mailer mailer = ...;

// perform connection test
mailer.testConnection();      // uses the mailer's async default
mailer.testConnection(false); // explicitly blocking
mailer.testConnection(true);  // explicitly asynchronous
§

Serializing Email objects core

An Email can be written with Java's ObjectOutputStream and restored later. Since 9.2.0, that restored email still contains what it needs to be sent.

  • Attachments, embedded images and decrypted attachments are stored with their bytes and MIME metadata.
  • A forwarded message is stored in RFC 822 form and restored with a neutral mail session.
  • S/MIME signing configuration is kept, including its PKCS12 data and passwords.

A custom DataSource is read in full while the email is serialized. After deserialization it is a repeatable, read-only byte source; the original class and any lazy loading, network access, caching, write support or other custom behavior are not recreated. This also means that a lazy or remote source must be available at serialization time, and a read failure stops serialization.

Treat the serialized data as sensitive. It can contain the complete message, attachments, credentials, private keys and passwords.

Emails serialized before 9.2.0 can still be opened for their ordinary fields and attachment metadata. Those versions did not store the attachment bytes, forwarded MIME message or S/MIME signing configuration, so that data cannot be recovered. Reading or sending a missing legacy attachment now fails with a clear pre-9.2.0 error instead of an unrelated null failure. See the 9.2 migration notes for the exact boundary.

§

Plug your own sending logic core

You can keep Simple Java Mail's message building, validation, security, and async support while replacing the code that tests a server and sends the message. Define a CustomMailer and plug it in.

A CustomMailer owns its sending connection, so call mailer.sendMail(...) normally. withOpenConnection(...) is unavailable in this setup; keep any connection reuse inside the CustomMailer implementation.


The benefit of this is that Simple Java Mail acts as an accelerator, providing thread pool, applying email content-validation, address validations, configuring a Session instance, producing a MimeMessage, all with full S/MIME, DKIM support and everything else.


Send mail using MailGun REST API:

Mailer mailGunMailer = MailerBuilder
      .withCustomMailer(new MailGunMailer())
      .(..)
      .buildMailer();
public class MailGunMailer implements CustomMailer {

	@Override
	public void testConnection(OperationalConfig operationalConfig, Session session) {
		// call MailGun rest service to test the provided config
	}

	@Override
	public void sendMessage(OperationalConfig operationalConfig, Session session, Email email, MimeMessage message) {
		// call MailGun rest service to send the email!
	}
}
  
§

Limit the maximum email size core

Do you know your server's maximum allowed email size? Then it might be helpful to have Simple Java Mail reject emails that exceed this before trying to send them.


The following throws an EmailTooBig exception as the cause in a parent MailerException instance

Mailer mailer = MailerBuilder
      .(..)
      .withMaximumEmailSize(4) // 4 bytes, that's not much
      .buildMailer();

try {
	mailer.sendMail(emailBiggerThan4Bytes);
} catch(Exception e) {
	// cause: EmailTooBigException
	// msg: "Email size of 277 bytes exceeds maximum allowed size of 4 bytes"
	e.getCause().printStackTrace();
}