Build and configure

Configuration

Configure mailers in Java, property files, environment variables, or Spring, including defaults, overrides, pools, and clusters.

Configuration at a glance

Choose where your mailer gets its settings.

Start with the route that matches your application. The same settings can be layered when you need more than one.

Configure Simple Java Mail through its Java API, system properties, environment variables, or property files. The Spring module reads the same settings from Spring's environment.

The Java API and configuration files work together. When the same setting appears in more than one place, Java values win, followed by system properties, environment variables, and property files.

MailerBuilder is the main configuration API. Its fluent builder produces a largely immutable Mailer instance.

ConfigLoader keeps these defaults. Load another property source to either add its values to the current set or start again with that source. Each load checks system properties and environment variables again.

§

Programmatic API - common settings

Everything can be configured through the java API. Specifically the builders are the entry point to creating Mailers and Emails and everything can be configured through them.

// start with a builder
MailerBuilder.withSMTPServer("smtp.host.com", 25, "username", "password");
// or
MailerBuilder
  .withSMTPServerHost("smtp.host.com")
  .withSMTPServerPort(25)
  .withSMTPServerUsername("username")
  .withSMTPServerPassword("password");

// you can even leave out some details for an anonymous SMTP server
MailerBuilder.withSMTPServer("smtp.host.com", 25);
// or
MailerBuilder
  .withSMTPServerHost("smtp.host.com")
  .withSMTPServerPort(25);

// adding the transport strategy...
currentMailerBuilder.withTransportStrategy(TransportStrategy.SMTP_TLS)

// or instead adding anonymous proxy configuration
currentMailerBuilder.withProxy("proxy.host.com", 1080);
// or
currentMailerBuilder
  .withProxyHost("proxy.host.com")
  .withProxyPort(1080);

// or authenticated proxy
currentMailerBuilder
  .withProxy("proxy.host.com", 1080, "proxy username", "proxy password");
// or
currentMailerBuilder
  .withProxyHost("proxy.host.com")
  .withProxyPort(1080)
  .withProxyUsername("proxy username")
  .withProxyPassword("proxy password");

// anonymous smtp + anonymous proxy + default SMTP protocol strategy
currentMailerBuilder
        .withSMTPServerHost("smtp.host.com").withSMTPServerPort(25)
        .withProxyHost("proxy.host.com").withProxyPort(1080);

// configure everything!
MailerBuilder
        .withSMTPServer("smtp.host.com", 587, "user@host.com", "password")
        .withTransportStrategy(TransportStrategy.SMTP_TLS)
        .withProxy("socksproxy.host.com", 1080, "proxy user", "proxy password")
        .buildMailer()
        .sendMail(email);

// preconfigured Session?
MailerBuilder.usingSession(session);

// preconfigured but you need anonymous proxy?
MailerBuilder
        .usingSession(session)
        .withProxy("socksproxy.host.com", 1080);

// preconfigured but you need authenticated proxy?
MailerBuilder
        .usingSession(session)
        .withProxy("socksproxy.host.com", 1080, "proxy user", "proxy password");
§

Programmatic API - other settings

The mailer builder also covers validation, diagnostics, timeouts, local binding, shared signing defaults, and extension points.

// report missing fields, invalid addresses and CRLF suspicions as warnings,
// then continue with SMTP handling
currentMailerBuilder.disablingAllClientValidation(true);
// make the underlying Jakarta Mail Session produce debug output
currentMailerBuilder
    .withDebugLogging(true)
    .withDebugOutput(SessionDebugOutput.SLF4J); // STDOUT, STDERR or SLF4J
// skip actually sending email, just log it
currentMailerBuilder.withTransportModeLoggingOnly(true);
// custom SSL connection factory (note: breaks setups with authenticated proxy!)
currentMailerBuilder.withCustomSSLFactoryClass("org.mypackage.MySSLFactory");
currentMailerBuilder.withCustomSSLFactoryInstance(mySSLFactoryInstance);

// change email validation strategy
currentMailerBuilder.withEmailValidator(
	JMail.strictValidator()
		.requireOnlyTopLevelDomains(TopLevelDomain.DOT_COM)
		.withRule(email -> email.localPart().startsWith("allowed"))
)

// restore the default strict JMail address validator:
currentMailerBuilder.resetEmailValidator();
// clear the JMail address policy; keep completeness, encoded-word and CRLF checks:
currentMailerBuilder.clearEmailValidator();

// set custom properties
currentMailerBuilder.withProperties(new Properties());
currentMailerBuilder.withProperties(new HashMap());
currentMailerBuilder.withProperty("mail.smtp.sendpartial", true);

// or directly modify the internal Session instance:
mailer.getSession().getProperties().setProperty("mail.smtp.sendpartial", true);

/* Secure TLS defaults since 9.2.0. These calls are optional. */
currentMailerBuilder.trustingAllHosts(false);
currentMailerBuilder.verifyingServerIdentity(true);

// preferred for private PKI: add the issuing CA to the JVM trust store
// narrow compatibility exception if that is not possible:
currentMailerBuilder.trustingSSLHosts("smtp.internal.example");
// broad compatibility escape hatch; avoid in production:
currentMailerBuilder.trustingAllHosts(true);

// clear or reset trust exceptions
currentMailerBuilder.clearTrustedSSLHosts();
currentMailerBuilder.resetTrustingAllHosts();

// compatibility escape hatch; disables hostname verification
currentMailerBuilder.verifyingServerIdentity(false);
// restore the secure default
currentMailerBuilder.resetVerifyingServerIdentity();
// pooled-delivery executor defaults
currentMailerBuilder.withThreadPoolSize(3);
// 0: core threads stay alive, !0: threads die after delay (default 1)
currentMailerBuilder.withThreadPoolKeepAliveTime(5000);

// completely replace the thread pool executor with your own
// this negates all related properties such as pool size and keepAliveTime
currentMailerBuilder.withExecutorService(new MyAwesomeCustomThreadPoolExecutor())
// change the SMTP session timeout (affects socket connect-, read- and write timeouts)
currentMailerBuilder.withSessionTimeout(10 * 1000); // 10 seconds for quick disconnect
// bind the outgoing SMTP socket to a local/source address
// the local port is advanced and usually should be omitted
currentMailerBuilder.withLocalBindAddress("203.0.113.10");
currentMailerBuilder.withLocalBindAddress("203.0.113.10", 25252);
// identify the SMTP client hostname sent in EHLO/HELO
// useful for corporate relay policy, logging, tracing and diagnostics
currentMailerBuilder.withSmtpClientHostname("orders-service.prod.example.com");
currentMailerBuilder.clearSmtpClientHostname();
// configure DKIM once for all emails sent through this mailer
// individual emails can still provide their own DKIM config
currentMailerBuilder.withDefaultDkimSigning(defaultDkimConfig);
// change the default sending logic to your own approach
currentMailerBuilder.withCustomMailer(yourOwnCustomMailerImpl); // send emails, test connections
§

Authenticated proxy bridge ports authenticated-socks-module

Jakarta Mail cannot authenticate to a SOCKS proxy by itself. For authenticated proxies, Simple Java Mail starts a small local bridge that handles the proxy login and relays the connection. The bridge listens only on the JVM's loopback address and stops when its Mailer has no active proxy operations.

Concurrent work through one Mailer shares that Mailer's bridge. Separate Mailers each own a bridge, so two authenticated-proxy Mailers that can be active at the same time must use different bridge ports. Otherwise they both try to claim the default port, 1081. Anonymous SOCKS proxies do not use the bridge.

The property simplejavamail.proxy.socks5bridge.port sets the shared default. When an application creates more than one authenticated-proxy Mailer, set a distinct port on each builder instead.

Mailer transactionalMailer = MailerBuilder
    .withSMTPServer("smtp.example.com", 587, "smtp-user", "smtp-password")
    .withTransportStrategy(TransportStrategy.SMTP_TLS)
    .withProxy("proxy.example.com", 1080, "proxy-user", "proxy-password")
    .withProxyBridgePort(1081)
    .buildMailer();

Mailer batchMailer = MailerBuilder
    .withSMTPServer("smtp.example.com", 587, "smtp-user", "smtp-password")
    .withTransportStrategy(TransportStrategy.SMTP_TLS)
    .withProxy("proxy.example.com", 1080, "proxy-user", "proxy-password")
    .withProxyBridgePort(1082)
    .buildMailer();
§

Properties files

Property files can define defaults and overrides. System properties and environment variables can replace those values at deployment time.

Simple Java Mail will automatically load properties from simplejavamail.properties, if available on the classpath. Alternatively, you can load a classpath resource, a file from the filesystem, an existing stream, or a Properties object. The String overload resolves a classpath resource. For a filesystem path, open an InputStream as shown here. ConfigLoader reads and closes every supplied stream before returning, including when loading fails.

Properties are loaded in order of priority from high to low:

  1. Programmatic values
  2. System properties
  3. Environment variables
  4. Properties from config files

Use addProperties=true to add the new source to what is already loaded. Any setting the new source does not contain stays in place. Use addProperties=false to throw away the loaded defaults before reading the new source. Either way, current system properties and environment variables are read again and take priority over the supplied source.

From the classpath:
ConfigLoader.loadProperties(
    "overrides-on-classpath.properties",
    /* addProperties = */ true
);
From the filesystem:
ConfigLoader.loadProperties(
    Files.newInputStream(Path.of("config", "simplejavamail.properties")),
    /* addProperties = */ false
);
From application-provided sources:
ConfigLoader.loadProperties(usingMyOwnInputStream, addProperties);
ConfigLoader.loadProperties(usingMyOwnPropertiesObject, addProperties);
Discard the loaded defaults without loading another property source:
ConfigLoader.loadProperties(new Properties(), /* addProperties = */ false);
Current system properties and environment variables still apply.
§

When changes take effect

ConfigLoader holds one set of defaults for the entire process. Loading another source does not reconfigure an existing Email, Mailer, or mail session.

Load every source for an environment before starting its builders. Then create a fresh builder and build the replacement object. Do not keep an EmailBuilder or MailerBuilder around while reloading ConfigLoader: settings are read while builders and their final objects are created.

When replacing a Mailer, let any work already using the old instance finish and then close it.

ConfigLoader.loadProperties(productionProperties, false);
Mailer productionMailer = MailerBuilder
    .withSMTPServerPort(587)
    .buildMailer();

ConfigLoader.loadProperties(failoverProperties, false);
Mailer failoverMailer = MailerBuilder
    .withSMTPServerPort(587)
    .buildMailer();

// productionMailer still uses its original configuration
productionMailer.close(); // once no work is using it
§

Environment variable names

For the fixed properties listed below, uppercase the property name and replace each dot with an underscore. There is no additional relaxed binding: the name comes directly from the documented property key.

The two wildcard namespaces are different: simplejavamail.extraproperties.* and simplejavamail.defaults.connectionpool.clusters.* are scanned as literal names. An environment variable such as SIMPLEJAVAMAIL_EXTRAPROPERTIES_MAIL_SMTP_TIMEOUT is therefore not recognized.

Some process launchers can supply literal lowercase names containing dots, but support varies between shells, containers, and operating systems. For wildcard settings, prefer a property file, an exact dotted JVM -D system property, or the Java API.

SIMPLEJAVAMAIL_SMTP_HOST=smtp.example.com
SIMPLEJAVAMAIL_SMTP_PORT=587
SIMPLEJAVAMAIL_TRANSPORTSTRATEGY=SMTP_TLS
SIMPLEJAVAMAIL_DEFAULTS_FROM_ADDRESS=sender@example.com
§

Available properties

Most mailer settings have a property equivalent, so each environment can use different values without changing application code.

For simplejavamail.dkim.signing.private_key_file_or_data, prefix a path with file: or inline Base64-encoded key bytes with base64:. Unprefixed values retain their legacy path-or-UTF-8-data behavior.

# Debugging and transport
simplejavamail.javaxmail.debug=true
simplejavamail.javaxmail.debug.out=SLF4J
simplejavamail.transportstrategy=SMTP_TLS
simplejavamail.transport.mode.logging.only=false
simplejavamail.opportunistic.tls=false

# SMTP destination, protocol identity and local/source binding
simplejavamail.smtp.host=smtp.default.com
simplejavamail.smtp.port=587
simplejavamail.smtp.username=username
simplejavamail.smtp.password=password
simplejavamail.smtp.clienthostname=orders-service.prod.example.com
simplejavamail.smtp.localaddress=203.0.113.10
simplejavamail.smtp.localport=25252

# Validation, SSL and proxying
simplejavamail.disable.all.clientvalidation=false
simplejavamail.custom.sslfactory.class=org.mypackage.ssl.MySSLSocketFactoryClass
simplejavamail.proxy.host=proxy.default.com
simplejavamail.proxy.port=1080
simplejavamail.proxy.username=username proxy
simplejavamail.proxy.password=password proxy
simplejavamail.proxy.socks5bridge.port=1081
simplejavamail.defaults.sessiontimeoutmillis=60000
simplejavamail.defaults.trustallhosts=false
simplejavamail.defaults.trustedhosts=192.168.1.122;mymailserver.com;ix55432y
simplejavamail.defaults.verifyserveridentity=true

# Email defaults
simplejavamail.defaults.subject=Sweet News
simplejavamail.defaults.from.name=From Default
simplejavamail.defaults.from.address=from@default.com
simplejavamail.defaults.replyto.name=Reply-To Default
simplejavamail.defaults.replyto.address=reply-to@default.com
simplejavamail.defaults.bounceto.name=Bounce-To Default
simplejavamail.defaults.bounceto.address=bounce-to@default.com
simplejavamail.defaults.to.name=To Default
simplejavamail.defaults.to.address=to@default.com
simplejavamail.defaults.cc.name=CC Default
simplejavamail.defaults.cc.address=cc@default.com
simplejavamail.defaults.bcc.name=
simplejavamail.defaults.bcc.address=bcc1@default.com;bcc2@default.com

# Delivery Status Notification defaults
simplejavamail.defaults.delivery.status.notification.notify=FAILURE,DELAY
simplejavamail.defaults.delivery.status.notification.return.option=HEADERS_ONLY

# Content-Transfer-Encoding defaults
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

# Pooled-delivery executor defaults
simplejavamail.defaults.poolsize=10
simplejavamail.defaults.poolsize.keepalivetime=2000

# Connection-pool defaults
simplejavamail.defaults.connectionpool.clusterkey.uuid=38400000-8cf0-11bd-b23e-10b96e4ef00d
simplejavamail.defaults.connectionpool.coresize=0
simplejavamail.defaults.connectionpool.maxsize=4
simplejavamail.defaults.connectionpool.claimtimeout.millis=10000
simplejavamail.defaults.connectionpool.expireafter.millis=5000
simplejavamail.defaults.connectionpool.loadbalancing.strategy=ROUND_ROBIN

# Per-cluster defaults
simplejavamail.defaults.connectionpool.clusters.orders.clusterkey.uuid=00000000-0000-0000-0000-000000000301
simplejavamail.defaults.connectionpool.clusters.orders.coresize=0
simplejavamail.defaults.connectionpool.clusters.orders.maxsize=3
simplejavamail.defaults.connectionpool.clusters.orders.claimtimeout.millis=30000
simplejavamail.defaults.connectionpool.clusters.orders.expireafter.millis=600000
simplejavamail.defaults.connectionpool.clusters.orders.loadbalancing.strategy=RANDOM_ACCESS
simplejavamail.defaults.connectionpool.clusters.00000000-0000-0000-0000-000000000302.maxsize=8

# S/MIME defaults
simplejavamail.smime.signing.keystore=my_keystore.pkcs12
simplejavamail.smime.signing.keystore_password=keystore_password
simplejavamail.smime.signing.key_alias=key_alias
simplejavamail.smime.signing.key_password=key_password
simplejavamail.smime.signing.algorithm=SHA3-256withECDSA
simplejavamail.smime.encryption.certificate=x509inStandardPEM.crt
simplejavamail.smime.encryption.key_encapsulation_algorithm=RSA_OAEP_SHA384
# AES is recommended; DES_EDE3_CBC remains available for legacy recipient compatibility.
simplejavamail.smime.encryption.cipher=AES256_CBC

# DKIM defaults
simplejavamail.dkim.signing.private_key_file_or_data=file:my_dkim_key.der
simplejavamail.dkim.signing.selector=dkim1
simplejavamail.dkim.signing.signing_domain=your-domain.com
simplejavamail.dkim.signing.use_length_param=false
# Omit header exclusions to keep the default signing list, including From.
simplejavamail.dkim.signing.header_canonicalization=RELAXED
simplejavamail.dkim.signing.body_canonicalization=RELAXED
simplejavamail.dkim.signing.algorithm=SHA256_WITH_RSA

# Embedded image dynamic resolution
simplejavamail.embeddedimages.dynamicresolution.enable.dir=true
simplejavamail.embeddedimages.dynamicresolution.enable.url=false
simplejavamail.embeddedimages.dynamicresolution.enable.classpath=true
simplejavamail.embeddedimages.dynamicresolution.base.dir=/var/opt/static
simplejavamail.embeddedimages.dynamicresolution.base.url=
simplejavamail.embeddedimages.dynamicresolution.base.classpath=/static
simplejavamail.embeddedimages.dynamicresolution.outside.base.dir=true
simplejavamail.embeddedimages.dynamicresolution.outside.base.classpath=false
simplejavamail.embeddedimages.dynamicresolution.outside.base.url=false
simplejavamail.embeddedimages.dynamicresolution.mustbesuccesful=true

Spring Boot users can use the same canonical property names. A few generated metadata aliases exist for IDE compatibility where Spring's relaxed binding cannot represent the original name cleanly.

simplejavamail.javaxmail.debug-out=SLF4J
simplejavamail.custom.sslfactory.clazz=org.mypackage.ssl.MySSLSocketFactoryClass
simplejavamail.defaults.poolsize-more.keepalivetime=2000
simplejavamail.smime.signing.keystore-password=keystore_password
simplejavamail.smime.signing.key-alias=key_alias
simplejavamail.smime.signing.key-password=key_password
simplejavamail.dkim.signing.private-key-file-or-data=file:my_dkim_key.der
simplejavamail.dkim.signing.signing-domain=your-domain.com
# Leave header exclusions unset unless a known relay rewrites a header; From must remain signed.

Then there are extra properties which will directly go on the internal Session object when building a Mailer instance.

They use the same precedence as the other settings: system properties win over environment variables, and environment variables win over property-file values.

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

Mailer level email defaults and overrides

With property files and system properties you can define global defaults. But sometimes that is not enough.

With Simple Java Mail, you can set both defaults and overrides on the Mailer level using Java code. This will override global defaults loaded from a property file for example. However, before using it as defaults Email reference, you can still have your defaults initialized with global defaults, by using emailBuilder.buildEmailCompletedWithDefaultsAndOverrides().

Email yourServerLevelDefaults = EmailBuilder.startingBlank()
	/* set your defaults here */
	.buildEmailCompletedWithDefaultsAndOverrides(); // complete the instance with global defaults

Mailer adminServer = mailerBuilder
	(..)
	.withEmailDefaults(yourServerLevelDefaults)
	.withEmailOverrides(yourServerLevelOverrides)
	.buildMailer();

One use case for this is when you have multiple mailer instances, each for a separate SMTP server clustered together (see clustering), you may want to define defaults or overrides for a specific server. You can do that by defining a reference Email instance and set it as defaults or overrides parameter.

// for example: force FROM to be always the same for the
// specific SMTP adminServer from the previous example:
Email yourServerLevelOverrides = EmailBuilder.startingBlank()
    .from("Admin", "admin@yourcompany.com")
    .buildEmail();

To keep defaults or overrides away from one email, configure that after whichever builder starter you used.

Email email = EmailBuilder.startingBlank()
    .ignoringDefaults()
    .ignoringOverrides()
    // add the email content
    .buildEmail();

You can even exclude specific fields from the defaults or overrides if you want to make very specific exceptions.

emailBuilder
	.dontApplyDefaultValueFor(EmailProperty.FROM_RECIPIENT, EmailProperty.REPLYTO_RECIPIENT)
	.dontApplyOverrideValueFor(EmailProperty.DKIM_SIGNING_CONFIG)
§

Combining everything for multiple environments

Let's set up configuration for a test, acceptance and production environment.

Properties for the environments

#global default properties (simplejavamail.properties on classpath)

    # anonoymous SMTP inside 'safe' DMZ
    simplejavamail.smtp.host=dmz.smtp.candyshop.com
    simplejavamail.smtp.port=25

    # default sender and reply-to address
    simplejavamail.defaults.from.name=The Candy App
    simplejavamail.defaults.from.address=candyapp@candystore.com
    simplejavamail.defaults.replyto.name=Candystore Helpdesk
    simplejavamail.defaults.replyto.address=helpdesk@candystore.com
#overrides from TEST and UAT .../config/candystore/simplejavamail.properties

    # always send a copy to test archive
    simplejavamail.defaults.bcc.name=Archive TST UAT
    simplejavamail.defaults.bcc.address=test-archive@candyshop.com
#overrides from PRODUCTION .../config/candystore/simplejavamail.properties

    # always send a copy to production archive
    simplejavamail.defaults.bcc.name=Archive PRODUCTION
    simplejavamail.defaults.bcc.address=prod-archive@candyshop.com

    # smtp server in production is protected
    simplejavamail.smtp.username=creamcake
    simplejavamail.smtp.password=crusty_l0llyp0p

    # sending mails in production must go through proxy
    simplejavamail.proxy.host=proxy.candyshop.com
    simplejavamail.proxy.port=1080
    simplejavamail.proxy.username=candyman
    simplejavamail.proxy.password=I has the sugarcanes!!1!

Now for the programmatic part

Load the complete configuration for the selected environment first. Only then create its builder and Mailer.

// simplejavamail.properties is automatically loaded

// replace previously loaded defaults with this environment's property file;
// system properties and environment variables still take priority
Path environmentConfig = Path.of("config", "candystore", "simplejavamail.properties");
ConfigLoader.loadProperties(Files.newInputStream(environmentConfig), false);

// see if we need to do some specific override for some reason
if (someSpecialCondition) {
  ConfigLoader.loadProperties("special-override.properties", true);
}

// add values assembled by the application
Properties runtimeOverrides = new Properties();
runtimeOverrides.setProperty("simplejavamail.smtp.port", "2525");
ConfigLoader.loadProperties(runtimeOverrides, true);

Maybe we want to connect slightly different for some reason:

// start a fresh builder after every source for this environment has been loaded
// override only the port and connection type, leave everything else to config files
Mailer mailer = MailerBuilder
                  .withSMTPServerPort(587)
                  .withTransportStrategy(TransportStrategy.SMTP_TLS)
                  .buildMailer();
§

Spring support spring-module

Spring can supply the same Simple Java Mail settings, so each profile can use its own SMTP server, credentials, transport strategy, and defaults.

Import SimpleJavaMailSpringSupport and the module passes Spring's simplejavamail.* values to ConfigLoader. Those values replace matching settings loaded earlier, including values from simplejavamail.properties.

Here is the Java configuration:

Load Spring support and inject the default mailer:
@Component
@Import(SimpleJavaMailSpringSupport.class)
public class YourEmailService {

    @Autowired // or roll your own, as long as SimpleJavaMailSpringSupport is processed first
    private Mailer mailer;

}
Or inject the builder and customize the mailer:
@Configuration
@Import(SimpleJavaMailSpringSupport.class)
public class YourEmailService {

        @Autowired
        private MailerGenericBuilder mailerGenericBuilder;

        @Bean
        public Mailer customMailer() {
            return mailerGenericBuilder
                            .resetThreadPoolSize()
                            .withThreadPoolKeepAliveTime(5000)
                            .withProxyBridgePort(7777)
                            .withExecutorService(new MyAwesomeCustomThreadPoolExecutor())
                            .buildMailer();
        }
}
When the default Mailer uses SMTP_OAUTH2, a single OAuth2 token-provider bean is picked up automatically:
@Bean
OAuth2AccessTokenProvider smtpAccessTokens(TokenService tokenService) {
    return tokenService::currentAccessToken;
}
If the application has more than one OAuth2AccessTokenProvider bean, mark the one for SMTP with @Primary or use normal Spring bean disambiguation.

Applications using Spring Security OAuth2 Client can adapt its OAuth2AuthorizedClientManager. The manager obtains or refreshes the authorized client when needed; Simple Java Mail receives only the current token value.

@Bean
OAuth2AccessTokenProvider smtpAccessTokens(
        OAuth2AuthorizedClientManager authorizedClients) {
    return () -> {
        OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest
                .withClientRegistrationId("smtp")
                .principal("simple-java-mail")
                .build();

        OAuth2AuthorizedClient client = authorizedClients.authorize(request);
        if (client == null) {
            throw new IllegalStateException("Unable to authorize the SMTP OAuth2 client");
        }
        return client.getAccessToken().getTokenValue();
    };
}

For scheduled jobs and other code without an HTTP request, Spring Security recommends an AuthorizedClientServiceOAuth2AuthorizedClientManager. This adapter stays in your application; the Simple Java Mail Spring module adds no Spring Security dependency.

Spring profiles can then provide different values, for example default and production:
#application.properties
simplejavamail.javaxmail.debug=true
simplejavamail.smtp.host=smtp.host
simplejavamail.smtp.port=25
simplejavamail.transportstrategy=SMTP
#application-production.properties
simplejavamail.javaxmail.debug=false
simplejavamail.smtp.host=smtp.production.host
simplejavamail.smtp.port=465
simplejavamail.transportstrategy=SMTPS
simplejavamail.smtp.username=<username>
simplejavamail.smtp.password=<password>
simplejavamail.proxy.host=proxy.production.host
simplejavamail.proxy.port=1080
simplejavamail.proxy.username=<proxy_username>
simplejavamail.proxy.password=<proxy_password>
For per-cluster pool settings, Spring properties use the same dynamic namespace as regular property files:
#application.properties
simplejavamail.defaults.connectionpool.clusters.orders.clusterkey.uuid=00000000-0000-0000-0000-000000000301
simplejavamail.defaults.connectionpool.clusters.orders.coresize=0
simplejavamail.defaults.connectionpool.clusters.orders.maxsize=3
simplejavamail.defaults.connectionpool.clusters.orders.claimtimeout.millis=30000
simplejavamail.defaults.connectionpool.clusters.orders.expireafter.millis=600000
simplejavamail.defaults.connectionpool.clusters.orders.loadbalancing.strategy=RANDOM_ACCESS

# or key the config directly by UUID
simplejavamail.defaults.connectionpool.clusters.00000000-0000-0000-0000-000000000302.maxsize=8
simplejavamail.defaults.connectionpool.clusters.00000000-0000-0000-0000-000000000302.loadbalancing.strategy=ROUND_ROBIN
§

Mailer lifecycle and resource ownership batch-module spring-module

A Mailer is designed to be built once and reused. When your application is finished with it, call mailer.close() directly or let a try-with-resources block call it. Which resources need attention depends on how the Mailer is used and whether its executor came from Simple Java Mail or from your application.

Closing a Mailer releases the resources it owns: it stops an executor created by Simple Java Mail and, when pooled delivery is active, closes that Mailer's connection-pool registration. It does not wait for outstanding send futures or stop an executor supplied by your application.

How the Mailer is used What it owns What to do at shutdown
Synchronous direct sending Each SMTP connection is closed after the operation. The internal executor starts no worker thread unless an asynchronous operation is scheduled. Call mailer.close() when its application lifetime ends. There are no send futures to await if it was only used synchronously.
Asynchronous sending with the default executor The Mailer owns its executor. Direct sending uses a single-thread executor by default. Wait for every returned future, then call mailer.close().
Sending with a custom ExecutorService The Mailer owns its SMTP resources; your application owns the executor it supplied. Wait for the futures and call mailer.close(). Shut down the executor separately when its owner is finished with it.
Pooled sending The Mailer registers its SMTP connection pool and owns its default worker executor. Wait for the futures, then call close() on every Mailer. In a cluster, each Mailer has its own pool registration.
A Spring-managed Mailer The same resources as its underlying configuration. Spring calls close() on the managed Mailer when the application context stops. A Mailer created outside Spring bean management remains your responsibility.

Use close() for normal application shutdown. It shuts down an executor created by Simple Java Mail and, when pooled delivery is active, closes the connection pool registered for that Mailer. It never shuts down an executor supplied through withExecutorService(...).

The older shutdownConnectionPool() method starts the same cleanup and returns the connection-pool shutdown future. Despite its name, it also initiates shutdown of a Mailer-created executor when no connection pool is present. Neither method waits for your asynchronous send futures, so finish those first.

Synchronous work

A try-with-resources block can close the Mailer as soon as the final synchronous call returns.

try (Mailer mailer = mailerBuilder
        .buildMailer()) {
    mailer.sendMail(email);
}

Asynchronous work

Keep the Mailer open until all returned futures have completed. Try-with-resources is safe when the wait happens inside its scope.

try (Mailer mailer = mailerBuilder
        .async()
        .buildMailer()) {
    CompletableFuture<Void> first =
            mailer.sendMail(firstEmail);
    CompletableFuture<Void> second =
            mailer.sendMail(secondEmail);

    CompletableFuture.allOf(first, second)
            .join();
}
§

Batch and clustering support batch-module

Simple Java Mail provides four ways of sending more than one email, each meant for a different level of control and throughput:

  1. regular synchronous sending, one email at a time
  2. asynchronous sending using the mailer's configured ExecutorService
  3. simple sequential batch sending over one SMTP connection
  4. pooled and clustered sending for sustained throughput

If your application creates its own Jakarta Mail messages or a framework already owns Transport, first use the SMTP connection pooling and batch orchestration guide to choose the delivery path and lifecycle boundary that match your code.

§

How direct connections work core

Direct asynchronous sending schedules send tasks on the mailer's configured ExecutorService. Each send still opens and closes its own Transport connection, managed independently of a connection pool. By default this direct executor is a single-thread executor; provide a custom ExecutorService if you want different concurrency. Every concurrent send still needs its own SMTP connection, which is less efficient in high-volume scenarios and can reach the SMTP server's connection limit more quickly.

Mailer regularMailer = mailerBuilder.(..).buildMailer();
regularMailer.sendMail(email); // blocks

Mailer defaultAsyncMailer = mailerBuilder.(..).async().buildMailer();
defaultAsyncMailer.sendMail(email); // doesn't block

/* or be explicit about it: */
mailer.sendMail(email, /*async = */ true);

Defining your own thread pool using an ExecutorService (the direct-sending default is Executors.newSingleThreadExecutor):
currentMailerBuilder.withExecutorService(new MyAwesomeCustomThreadPoolExecutor())

See Mailer lifecycle and resource ownership for who shuts down the default and custom executors.

When reconnecting for every message is the only problem you need to solve, use simple sequential batch sending. It sends all provided emails over one SMTP connection. It sends one at a time, stops at the first failure and reuses only that 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

For sustained high-volume sending, prefer pooled connections.

When the caller owns the queue and needs custom work between successful sends, use withOpenConnection(...). Simple Java Mail owns and closes the SMTP connection for the callback, while the callback decides what to send next. This is useful for database-backed queues where a message should be marked as sent before the next one is read.

This form uses the Mailer's built-in SMTP connection. It cannot be combined with a CustomMailer, which owns its own connection lifecycle. Use sendMail(...) normally and keep any connection reuse inside that implementation.

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

The scoped sender is sequential, not asynchronous, not pooled and not clustered. For asynchronous queueing, higher throughput, pooled connections or clustered load balancing, use pooled connections.

Use sender.sendMailAndGetReceipt(next) instead when the checkpoint needs the SMTP submission response that was returned by the server.

§

Reusing connections with a pool batch-module

Pooled delivery reuses SMTP connections across worker threads. The default setup provides up to four pooled connections, which automatically close if inactive for five seconds following their last use. This mechanism is optimal for managing bursts of email traffic, keeping up to four connections active as long as needed.

The connection pool configuration is flexible, allowing adjustments to core connection pool size, maximum pool size, and connection expiry policy.

This section configures the Simple Java Mail Mailer path. Compare it with the standalone batch facade, direct leases, and provider-owned pooling in the pool orchestration guide.

Direct sending can use a custom thread pool, while pooled sending also lets those threads reuse Transport connections. This can improve performance even with fewer threads, as it eliminates the need to repeatedly open and close connections for each email.

Mailer pooledMailer = mailerBuilder
	   .(..)
	   .withConnectionPoolCoreSize(2) // keep 2 connections up at all times, automatically refreshed after expiry policy closes it (default 0)
	   .withConnectionPoolMaxSize(10) // scale up to max 10 connections until expiry policy kicks in and cleans up (default 4)
	   .withConnectionPoolClaimTimeoutMillis(TimeUnit.MINUTES.toMillis(1)) // wait max 1 minute for available connection (default about 24.9 days)
	   .withConnectionPoolExpireAfterMillis(TimeUnit.MINUTES.toMillis(30)) // keep connections spinning for half an hour (default 5 seconds)
	   .buildMailer();
	

Or using properties:

simplejavamail.defaults.connectionpool.coresize=2
simplejavamail.defaults.connectionpool.maxsize=10
simplejavamail.defaults.connectionpool.claimtimeout.millis=60000
simplejavamail.defaults.connectionpool.expireafter.millis=1800000
	

A live connection pool keeps the JVM running. See Mailer lifecycle and resource ownership for orderly application shutdown, including clustered Mailers.

§

Clustered SMTP load balancing batch-module

Use a cluster when several SMTP servers handle the same workload, or when different workloads need their own connection pools. Mailers that share a cluster key each register a pool for their configured SMTP server.

For each send, the cluster selects one registered pool using its round-robin or random-access strategy. This spreads work across the SMTP servers registered for that cluster.

For cluster-selected versus exact-Session callbacks outside the full Mailer API, see the standalone batch-module facade.

Cluster-level Java config:
UUID ordersCluster = UUID.fromString("00000000-0000-0000-0000-000000000301");

Mailer clusteredMailer = mailerBuilder
    .(..) // normal SMTP settings
    .withClusterKey(ordersCluster)
    .withConnectionPoolCoreSize(2)
    .withConnectionPoolMaxSize(10)
    .withConnectionPoolClaimTimeoutMillis(60000)
    .withConnectionPoolExpireAfterMillis(1800000)
    .withConnectionPoolLoadBalancingStrategy(LoadBalancingStrategy.ROUND_ROBIN)
    .buildMailer();
or using properties:
simplejavamail.defaults.connectionpool.clusterkey.uuid=00000000-0000-0000-0000-000000000301
simplejavamail.defaults.connectionpool.loadbalancing.strategy=ROUND_ROBIN
# valid values: ROUND_ROBIN, RANDOM_ACCESS

Failure behavior in a cluster:

Mailer primary = MailerBuilder
    .withSMTPServer("smtp-primary.example.com", 587, "user", "password")
    .withClusterKey(myClusterKey)
    .buildMailer();
Mailer secondary = MailerBuilder
    .withSMTPServer("smtp-secondary.example.com", 587, "user", "password")
    .withClusterKey(myClusterKey)
    .buildMailer();
Mailer tertiary = MailerBuilder
    .withSMTPServer("smtp-tertiary.example.com", 587, "user", "password")
    .withClusterKey(myClusterKey)
    .buildMailer();

try {
    primary.sendMail(email, false); // one registered pool is selected for this send
} catch (MailException failure) {
    // The selected SMTP submission failed.
    // Its connection is discarded, but its server pool stays in the cluster.
    // Apply your retry, provider-routing, or circuit-breaking policy here.
}

Handling a failed send: Simple Java Mail invalidates the failed transport and reports the failure to your application. The server pool remains registered and participates in normal selection for later sends. With synchronous sending, the call throws MailException; with asynchronous sending, the returned CompletableFuture completes exceptionally. Use that failure signal with your application's retry, health-check, circuit-breaking, or provider-routing policy, including its safeguards for ambiguous SMTP outcomes.

Note 2: Connection pool defaults are scoped by cluster key. The first Mailer instance in a cluster sets that cluster's pool defaults; later Mailer instances in the same cluster cannot change them. Different cluster keys can use different pool defaults.

Note 3: Using Java API, you can define any number of clusters. Using simplejavamail.defaults.connectionpool.clusterkey.uuid, you define one default cluster key. Using simplejavamail.defaults.connectionpool.clusters.*, property files and Spring can also define separate pool defaults for multiple cluster keys.

Property-defined cluster configs:

# Alias-based config. The alias is only a property-file name; clusterkey.uuid is the actual cluster key.
simplejavamail.defaults.connectionpool.clusters.orders.clusterkey.uuid=00000000-0000-0000-0000-000000000301
simplejavamail.defaults.connectionpool.clusters.orders.coresize=0
simplejavamail.defaults.connectionpool.clusters.orders.maxsize=3
simplejavamail.defaults.connectionpool.clusters.orders.claimtimeout.millis=30000
simplejavamail.defaults.connectionpool.clusters.orders.expireafter.millis=600000
simplejavamail.defaults.connectionpool.clusters.orders.loadbalancing.strategy=RANDOM_ACCESS

# Direct UUID-keyed config. If clusterkey.uuid is omitted, the alias itself must be a UUID.
simplejavamail.defaults.connectionpool.clusters.00000000-0000-0000-0000-000000000302.maxsize=8
simplejavamail.defaults.connectionpool.clusters.00000000-0000-0000-0000-000000000302.loadbalancing.strategy=ROUND_ROBIN

You set the limit
So really, there's no limit to the email performance you are looking for except maybe in the client which generates the emails. You can add as many servers as you like to a cluster, use multiple clusters for different purposes and have as many pooled connections as you want dormant or spinned up at all time!