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

Configuring Simple Java Mail

Simple Java Mail provides full configuration through programmatic API as well as system variables, environment variables and properties files (including Spring).

The Java API and config files complement each other. If you provide the overlapping configuration, the programmatic API takes priority, overriding system and environment values, overriding property values.

Central to configuring Simple Java Mail is the MailerBuilder. As the library grew maintaining all the constructors and setters proved unwieldy and so it moved to a completely builder based fluent API which produces a largely immutable Mailer instance.

Second, there is the ConfigLoader, which contains all the preconfigured defaults read initially from .properties files. It contains programmatic API to clear, add or replace default values.

§

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("smtp.host.com")
  .withProxyPort(25);

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

// anonymous smtp + anonymous proxy + default SMTP protocol strategy
currentMailerBuilder
        .withSMTPServer("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);
        .withProxyHost("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)
        .withProxyHost("socksproxy.host.com", 1080);

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

Programmatic API - other settings

Aside from transport strategy, SMTP and Proxy server details, there are a few other more generic settings.

// ignore invalid email addresses (if email validator was provided)
// and ignore CRLF injection suspicions
// useful in case you want to delegate all responsibility to the server
currentMailerBuilder.disablingAllClientValidation(true);
// make the underlying javax.mail produce more logging
currentMailerBuilder.withDebugLogging(true);
// 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"))
)

// reset to default RFC compliant checks:
currentMailerBuilder.clearEmailValidator();
// deactivate email validation completely:
currentMailerBuilder.resetEmailValidator();

// change SOCKS5 bridge port in case of authenticated proxy
currentMailerBuilder.withProxyBridgePort(1081); // always localhost

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

/* Regarding the following config on trusting hosts,
    the Javadoc has more detailed info (in the mailer builder api). */
// trust all hosts for SSL connections
currentMailerBuilder.trustingAllHosts(true);
// or white list hosts for SSL connections (identity key validation notwithstanding)
currentMailerBuilder.trustingSSLHosts("a", "b", "c", ...);

// or clearing these options
currentMailerBuilder.clearTrustedSSLHosts();
currentMailerBuilder.resetTrustingAllHosts();

/* Regarding the following config on identifying hosts,
    the Javadoc has more detailed info (in the mailer builder api). */
// don't validate keys thus not verifying server hosts
currentMailerBuilder.verifyingServerIdentity(false);
currentMailerBuilder.resetVerifyingServerIdentity();
// change the pool size (default 10) for concurrent threads, each sending an email
currentMailerBuilder.withThreadPoolSize(3);
// change keepAliveTime for other behavior.
// 0: threads don't die, !0: threads die after delay (default 1)
currentMailerBuilder.withKeepAliveTime(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
// change the default sending logic to your own approach
currentMailerBuilder.withCustomMailer(yourOwnCustomMailerImpl); // send emails, test connections
§

Properties files

With properties files you can define defaults and overrides. You can also provide overriding value by defining system variables.

Simple Java Mail will automatically load properties from simplejavamail.properties, if available on the classpath. Alternatively, you can manually load additional properties files in a number of ways.

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

  1. Programmatic values
  2. System variables
  3. Environment variables
  4. Properties from config files
ConfigLoader.loadProperties("overrides-on-classpath.properties", /* addProperties = */ true);
ConfigLoader.loadProperties(new File("d:/replace-from-environment.properties"), /* addProperties = */ false);
ConfigLoader.loadProperties(usingMyOwnInputStream, addProperties);
ConfigLoader.loadProperties(usingMyOwnPropertiesObject, addProperties);
This clears everything:
ConfigLoader.loadProperties(new Properties(), /* addProperties = */ false);
§

Available properties

Almost everything can be set as a default property. This way you can easily configure environments without changing the code.

simplejavamail.javaxmail.debug=true
simplejavamail.transportstrategy=SMTPS
simplejavamail.smtp.host=smtp.default.com
simplejavamail.smtp.port=25
simplejavamail.smtp.username=username
simplejavamail.smtp.password=password
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.content.transfer.encoding=BINARY
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
simplejavamail.defaults.poolsize=10
simplejavamail.defaults.poolsize.keepalivetime=2000
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
simplejavamail.defaults.sessiontimeoutmillis=60000
simplejavamail.defaults.trustallhosts=false
# following property is ignored when trustallhosts is true:
simplejavamail.defaults.trustedhosts=192.168.1.122;mymailserver.com;ix55432y
simplejavamail.defaults.verifyserveridentity=true
simplejavamail.transport.mode.logging.only=true
simplejavamail.opportunistic.tls=false
# following properties are used as defaults on Mailer level
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 # default SHA256withRSA
simplejavamail.smime.encryption.certificate=x509inStandardPEM.crt
simplejavamail.smime.encryption.key_encapsulation_algorithm=RSA_OAEP_SHA384 # default RSA
simplejavamail.smime.encryption.cipher=DES_EDE3_WRAP # default DES_EDE3_CBC
simplejavamail.dkim.signing.private_key_file_or_data=my_dkim_key.der # or key as base64
simplejavamail.dkim.signing.selector=dkim1
simplejavamail.dkim.signing.signing_domain=your-domain.com
simplejavamail.dkim.signing.excluded_headers_from_default_signing_list=From
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

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

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 variables 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 = mailerBuiler.
	(..)
	.withEmailDefaults(yourServerLevelDefaults)
	.withEmailOverrides(yourServerLevelOverrides)
	.build();

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

You can exclude specific emails completely from having defaults or overrides applied by using ignoreDefaults or ignoreOverrides.

emailBuilder.ignoringDefaults(true)
emailBuilder.ignoringOverrides(true)

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

emailBuilder
	.dontApplyDefaultValueFor(EmailProperty.FROM, EmailProperty.REPLY_TO)
	.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

// simplejavamail.properties is automatically loaded

// assume that every environment provides its own property file
ConfigLoader.loadProperties(new File(".../config/candystore/simplejavamail.properties"));

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

// or maybe we want to ditch all defaults and trust someone else's config completely
if (ditchOwnAndTrustOtherSource) {
  ConfigLoader.loadProperties(someFileOrInputSource, false);
}

// maybe the config service has something?
ConfigLoader.loadProperties(socket.getInpuStream(), true);
// or you have your own Properties source?
ConfigLoader.loadProperties(myOwnProperties, true);

Maybe we want to connect slightly different for some reason:

// 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

Everything can be configured through Spring properties, allowing for robust profile-based configuration.

To enable this, include the spring-module on your classpath.

By importing the Spring support bean from Simple Java Mail, whatever properties are provided through Spring are then transferred to Simple Java Mail using the ConfigLoader. It will add or overwrite whatever properties have been loaded before that (including the regular simplejavamail.properties).

Here's a sample configuration using Java style configuration.

Loading Spring support and obtaining default Mailer instance:
@Component
@Import(SimpleJavaMailSpringSupport.class)
public class YourEmailService {

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

}
Or obtaining the intermediate builder and customize:
@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();
        }
}
Then when you have profile based configuration (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=443
simplejavamail.transportstrategy=SMTPS
simplejavamail.smtp.username=<username>
simplejavamail.smtp.password=<password>
simplejavamail.proxy.username=<proxy_username>
simplejavamail.proxy.password=<proxy_password>
§

Batch and clustering support

Simple Java Mail provides three ways of optimizing performance for high-volume email sending:

  1. asynchronous sending using a user-provided ExecutorService
  2. connection pooling with the batch module reusing server connections
  3. server clustering with multiple connection pools dedicated to different SMTP servers
§

How connections work without batch-module

Without the batch module, Simple Java Mail supports asynchronous email sending, where each message is handled in its own thread using a single Transport connection. This process involves opening and closing the connection for each email, managed independently of a connection pool. Additionally, Simple Java Mail allows the configuration of a custom ExecutorService to enhance performance through a custom thread pool. Adjusting these settings to match your server’s specifications can optimize performance. However, without the batch module, each thread maintains a dedicated Transport connection that is repeatedly opened and closed for each email, which is less efficient in high-volume scenarios. Additionally, you may find you will hit your maximum concurrent connections for your server more easily without the batch module.

Mailer regularMailer = mailerBuiler.(..).build();
regularMailer.send(email); // blocks

Mailer defaultAsyncMailer = mailerBuiler.(..).async().build();
defaultAsyncMailer.send(email); // doesn't block

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

Defining your own thread pool using a ExecutorService (default is Executors.newSingleThreadExecutor):
currentMailerBuilder.withExecutorService(new MyAwesomeCustomThreadPoolExecutor())
§

Reusing connections with batch-module's connection pool

The batch module significantly boosts performance by introducing connection pooling. Simply include the module in your classpath (add the Maven dependency) to enable up to four pooled connections. 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.

While the default setup without the batch module allows for the creation of a custom thread pool to manage email sending, the batch module introduces a connection pool. This addition enables threads to reuse Transport connections more efficiently, significantly improving 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 forever)
	   .withConnectionPoolExpireAfterMillis(TimeUnit.MINUTES.toMillis(30)) // keep connections spinning for half an hour (default 5 seconds)
	   .build();
	

Or using properties:

simplejavamail.defaults.connectionpool.coresize=2 (defaults to 0)
simplejavamail.defaults.connectionpool.maxsize=10 (defaults to 4)
simplejavamail.defaults.connectionpool.claimtimeout.millis=60000 (defaults to forever)
simplejavamail.defaults.connectionpool.expireafter.millis=1800000 (defaults to 5000)
	

Note that with the batch-module enabled, the JVM won't shut down by itself anymore, as the connection pool stays alive until shutdown manually. To do this, just call mailer.shutdownConnectionPool() (repeat with each mailer you might have in a cluster).

§

Advanced performance tuning: Clustering with multiple connection pools

To enable high-availability / fail-over or to really take performance to out-of-this world levels and handle truly enormous email batch loads, Simple Java Mail enables you to easily configure cluster(s) of SMTP servers.

For example, for a simple fail-over setup with three SMTP servers, You can define a cluster with low/default connection pool settings and have three Mailer instances use the same cluster key. Then sending an email with any of these Mailer instances will result in a send-action resolved using the cluster.

Global cluster config:
Mailer clusteredMailer = mailerBuilder
	   .(..) // normal settings
	   .(..) // connection pool settings
	   .withConnectionPoolLoadBalancingStrategy(LoadBalancingStrategy.ROUND_ROBIN)
	   .build();
or using properties:
simplejavamail.defaults.connectionpool.loadbalancing.strategy=ROUND_ROBIN
# valid values: ROUND_ROBIN, RANDOM

Mailer behavior in a cluster setup:

Mailer mailer1InFailoverCluster = mailerBuilderServer1.(..).withClusterKey(myClusterKey).build();
Mailer mailer2InFailoverCluster = mailerBuilderServer2.(..).withClusterKey(myClusterKey).build();
Mailer mailer3InFailoverCluster = mailerBuilderServer3.(..).withClusterKey(myClusterKey).build();

// or default cluster using property:
// simplejavamail.defaults.connectionpool.clusterkey.uuid=38400000-8cf0-11bd-b23e-10b96e4ef00d

mailer1InFailoverCluster.send(email); // 1 of 3 servers is selected (default Round Robin)
mailer2InFailoverCluster.send(email); // 1 of 3 servers is selected (default Round Robin)
mailer3InFailoverCluster.send(email); // 1 of 3 servers is selected (default Round Robin)

// now server 2 breaks down and becomes unreachable or produces errors
// server 2 is removed from the cluster, but all mailers still work:
mailer1InFailoverCluster.send(email); // 1 of 2 servers is selected (default Round Robin)
mailer2InFailoverCluster.send(email); // 1 of 2 servers is selected (default Round Robin)
mailer3InFailoverCluster.send(email); // 1 of 2 servers is selected (default Round Robin)

Note 1: The send-actions don't automatically recover from errors and are not retried automatically. This is because Simple Java Mail cannot determine how a send-action was botched and what the next course of action should be. You can monitor individual emails using the CompletableFuture async return value obtained from mailer.send() and determine followup-actions for errored-out results.

Note 2: The Connection Pool defaults (core size, max size etc.) are set and fixed by the first Mailer instance in the cluster. Subsequent Mailer instances cannot change these global settings. If they provide different global defaults, a warning will be logged.

Note 3: Using the Java API, you can define any number of clusters. Using the default cluster uuid property, you can define only one default cluster.

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!