Fork me on GitHub
Simple Java Mail
Simple API, Complex Emails

Migrating to Simple Java Mail 9.0.0

§

Dedicated recipient builders

Impact: The old repeated to(...), cc(...) and bcc(...) recipient method matrix has been replaced by dedicated recipient builders plus the canonical withRecipients(...) methods on the email builder.

Build one recipient with RecipientBuilder or a group with RecipientsBuilder, then add the result to the email using withRecipients(Recipient...) or withRecipients(Collection<Recipient>). The string-based generic withRecipients(name, fixedName, type, addresses...) method remains for CLI and property-driven integration code.

For grouped recipients, RecipientsBuilder is also the place to express local governance. Default names and certificates fill missing values, fixed names and certificates intentionally replace source values for that group, and clearingSmimeCertificates() strips reused recipient certificate state before the flat recipient list is added to the email. After that, recipient certificates take precedence over email-level and mailer-level S/MIME encryption fallbacks.

Recipient recipient = new RecipientBuilder()
    .withAddressAndNameOrDefault("Alice <alice@example.com>", null)
    .withType(Message.RecipientType.TO)
    .build();

Email email = EmailBuilder.startingBlank()
    .from("me@example.com")
    .withRecipients(recipient)
    .withPlainText("Hello")
    .buildEmail();
§

Per-recipient S/MIME certificates

Impact: Existing email-level and mailer-level S/MIME defaults still work. Use recipient certificates when recipients need different certificates.

Recipients can now carry their own S/MIME encryption certificate. Recipient-level certificates, including certificates materialized by RecipientsBuilder group defaults or fixed group values, take precedence over governance-resolved email and mailer encryption defaults. This makes multi-recipient encrypted email practical without one shared certificate source.

When reusing recipients, RecipientsBuilder.clearingSmimeCertificates() removes existing recipient certificate state so the current email can fall back to its email-level or mailer-level S/MIME encryption configuration.

Recipient alice = new RecipientBuilder()
    .withAddress("alice@example.com")
    .withType(Message.RecipientType.TO)
    .withSmimeCertificate(aliceCertificate)
    .build();

Email email = EmailBuilder.startingBlank()
    .from("me@example.com")
    .withRecipients(alice)
    .withPlainText("Encrypted for Alice")
    .buildEmail();
§

Outlook conversion results expose source data

Impact: Code using the old direct Outlook-to-email conversion methods can keep doing so. Use the new result API when source .msg metadata is needed.

Outlook conversion now has an explicit result API that separates the converted email builder from Outlook-specific source data. Structural source headers and MAPI metadata no longer need to be copied into the converted email just to remain inspectable.

OutlookEmailConversionResult result =
    EmailConverter.outlookMsgToEmailBuilderWithOutlookData(msgFile);

Email email = result.buildEmail();
OutlookMessageData outlookData = result.getOutlookMessageData();
List<String> receivedHeaders = outlookData.getHeaderValues("Received");

The old InputStream overloads returning EmailFromOutlookMessage are deprecated in favor of outlookMsgToEmailBuilderWithOutlookData(...).

§

Delivery Status Notification API

Impact: New API only. Projects can now request SMTP DSN behavior through Java, property files and Spring configuration.

Delivery Status Notification settings can be attached to an email using DeliveryStatusNotification and the new email builder methods.

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();

Property-file defaults are available as:

simplejavamail.defaults.delivery.status.notification.notify=FAILURE,DELAY
simplejavamail.defaults.delivery.status.notification.return.option=HEADERS_ONLY
§

Per-body Content-Transfer-Encoding defaults

Impact: Existing global content-transfer encoding behavior remains the fallback. Use the new per-body settings when text, HTML and calendar parts need different encodings.

The existing withContentTransferEncoding(...) setting can now be refined per body part.

Email email = EmailBuilder.startingBlank()
    .from("sender@example.com")
    .withRecipients(new RecipientBuilder()
        .withAddress("receiver@example.com")
        .withType(Message.RecipientType.TO)
        .build())
    .withPlainText("Plain")
    .withHTMLText("<b>HTML</b>")
    .withContentTransferEncoding(ContentTransferEncoding.QUOTED_PRINTABLE)
    .withPlainTextContentTransferEncoding(ContentTransferEncoding.BIT7)
    .withHTMLTextContentTransferEncoding(ContentTransferEncoding.BASE_64)
    .buildEmail();

Property-file defaults are available as:

simplejavamail.defaults.content.transfer.encoding=quoted-printable
simplejavamail.defaults.body.text.content.transfer.encoding=7bit
simplejavamail.defaults.body.html.content.transfer.encoding=base64
simplejavamail.defaults.body.calendar.content.transfer.encoding=quoted-printable
§

Pre-encoded attachments and embedded images

Impact: New API only. Existing attachment APIs still represent raw data that Simple Java Mail may encode for MIME transport.

When attachment or embedded-image data is already encoded, use the new pre-encoded APIs so Simple Java Mail preserves the payload instead of encoding it again.

Email email = EmailBuilder.startingBlank()
    .from("sender@example.com")
    .withRecipients(new RecipientBuilder()
        .withAddress("receiver@example.com")
        .withType(Message.RecipientType.TO)
        .build())
    .withPlainText("See attachment")
    .withPreEncodedAttachment(
        "report.pdf",
        alreadyBase64EncodedBytes,
        "application/pdf",
        ContentTransferEncoding.BASE_64)
    .buildEmail();

The same concept exists for embedded images using withPreEncodedEmbeddedImage(...).

§

Explicit resource Content-ID values

Impact: New API only. Generated fallback Content-ID values are now opaque and safe, while caller-supplied values are preserved.

Attachment and embedded-image APIs now support separating the display filename/resource name from the MIME Content-ID. This matters for round-tripping and for HTML that references a stable cid:... value.

Email email = EmailBuilder.startingBlank()
    .from("sender@example.com")
    .withRecipients(new RecipientBuilder()
        .withAddress("receiver@example.com")
        .withType(Message.RecipientType.TO)
        .build())
    .withHTMLText("<img src='cid:logo-content-id'>")
    .withEmbeddedImage("logo.png", logoDataSource, "logo-content-id")
    .buildEmail();
§

Jakarta Mail debug output targets

Impact: New API only. Existing withDebugLogging(true) behavior remains available.

Jakarta Mail debug output can now be redirected to one of the built-in targets, or to a custom PrintStream from Java code.

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

Property-file configuration uses:

simplejavamail.javaxmail.debug=true
simplejavamail.javaxmail.debug.out=SLF4J
§

Local bind address configuration

Impact: New API only. This hides the transport-specific Jakarta Mail property names behind Simple Java Mail configuration.

On multi-IP machines, the outgoing SMTP socket can now be bound to a specific local/source address. The local port is supported as an advanced option, but should normally be left empty.

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

Property-file configuration uses:

simplejavamail.smtp.localaddress=203.0.113.10
simplejavamail.smtp.localport=25252

See the feature docs and configuration reference for the full examples.

§

Mailer-level default DKIM signing

Impact: New API only. Existing per-email DKIM signing still takes precedence.

DKIM signing can now be configured once on the mailer. This is a convenience wrapper around the mailer defaults mechanism and preserves other configured email defaults.

Mailer mailer = MailerBuilder
    .withSMTPServer("smtp.example.com", 587, "user", "password")
    .withDefaultDkimSigning(defaultDkimConfig)
    .buildMailer();

Property-driven DKIM defaults continue to use simplejavamail.dkim.signing.*. See DKIM signing for usage.

§

Simple sequential batch and open-connection sending

Impact: New API only. This does not replace the batch-module.

Mailer.sendMailsInSimpleBatch(...) sends multiple emails sequentially over one SMTP connection. It is meant for caller-managed loops where reconnecting for each email is the only problem to solve.

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

This API is not concurrent, pooled, retry-oriented or clustered. For those use cases, keep using the batch-module. See batch and clustering support for the full distinction.

For caller-owned queues that need custom work between successful sends, use Mailer.withOpenConnection(...). It opens one SMTP connection for the callback and provides a scoped sender; each send is still sequential and processed like a normal blocking send.

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

Batch-module cluster configuration

Impact: Cluster pool defaults are now scoped per cluster key instead of effectively behaving like one global JVM-level pool default.

With the Java API, the first Mailer registered for a cluster key defines that cluster's pool defaults, claim timeout, expiry and load-balancing strategy. Different cluster keys can now have different defaults.

Property-file and Spring users can also define multiple cluster configs using simplejavamail.defaults.connectionpool.clusters.*.

Mailer ordersMailer = MailerBuilder
    .withSMTPServer("orders-smtp.example.com", 587, "user", "password")
    .withClusterKey(ordersCluster)
    .withConnectionPoolMaxSize(3)
    .withConnectionPoolLoadBalancingStrategy(LoadBalancingStrategy.RANDOM_ACCESS)
    .buildMailer();
simplejavamail.defaults.connectionpool.clusters.orders.clusterkey.uuid=00000000-0000-0000-0000-000000000301
simplejavamail.defaults.connectionpool.clusters.orders.maxsize=3
simplejavamail.defaults.connectionpool.clusters.orders.loadbalancing.strategy=RANDOM_ACCESS

See clustering configuration for alias-based, UUID-keyed and Spring examples.

§

Async mailer defaults apply to testConnection

Impact: If a mailer is configured with async(), no-arg testConnection() now follows that configured default.

This aligns testConnection() with no-arg sendMail(...): both use the mailer's configured async default. Use testConnection(false) when a blocking connection test is required.

§

Resource filenames moved out of Content-Type filename

Impact: Only code or tests that inspect generated raw MIME headers should need attention.

Simple Java Mail no longer adds the non-standard filename parameter to attachment and embedded-image Content-Type headers. The filename remains available from the standard Content-Disposition filename parameter, while Content-Type keeps the legacy-compatible name parameter.

If you compare generated EML literally, update expectations from this:

Content-Type: application/pdf; filename=report.pdf; name=report.pdf
Content-Disposition: attachment; filename=report.pdf

to this:

Content-Type: application/pdf; name=report.pdf
Content-Disposition: attachment; filename=report.pdf
§

Java module descriptors

Impact: New metadata only. Classpath users do not need to change anything.

The facade and core jars now include Java module descriptors, completing the multi-release jar support for modular applications. Applications using the Java Platform Module System can require org.simplejavamail directly.

module com.example.mailer {
    requires org.simplejavamail;
}

See modules for the module layout.