Fork me on GitHub

Simple Java Mail

Simple API, Complex Emails

Configuration

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.

§

Transport strategies

Although Simple Java Mail started out as a library to help produce RFC-anatomically correct emails, one of its primary drivers now is to simplify configuration, using transport strategies.

There are three strategies: TransportStrategy.SMTP, TransportStrategy.SMTPS and TransportStrategy.SMTP_TLS.

Let's quickly review them one-by-one.

§

TransportStrategy.SMTP

This transport strategy falls back to plaintext when a mail server does not indicate support for STARTTLS. Additionally, even if a TLS session is negotiated, server certificates are not validated in any way.


MailerBuilder.withTransportStrategy(TransportStrategy.SMTP);

// or through property defaults, skipping builder API:
simplejavamail.transportstrategy=SMTP
Mailer mailer = MailerBuilder.buildMailer();

Furthermore, this transport strategy only offers protection against passive network eavesdroppers when the mail server indicates support for STARTTLS. Active network attackers can trivially bypass the encryption 1) by tampering with the STARTTLS indicator, 2) by presenting a self-signed certificate, 3) by presenting a certificate issued by an untrusted certificate authority; or 4) by presenting a certificate that was issued by a valid certificate authority to a domain other than the mail server's.

For proper mail transport encryption, use TransportStrategy.SMTPS or TransportStrategy.SMTP_TLS.

To disable opportunistic TLS and revert back to the legacy SMTP_PLAIN behavior prior to 5.0.0 (not recommended), you can turn it off programmatically or by setting the property simplejavamail.opportunistic.tls.


TransportStrategy.SMTP.setOpportunisticTLS(false);
MailerBuilder.withTransportStrategy(TransportStrategy.SMTP);
// or as property:
simplejavamail.opportunistic.tls=false
§

TransportStrategy.SMTPS

SMTP entirely encapsulated by TLS. Commonly known as SMTPS.

Strict validation of server certificates is enabled. Server certificates must be issued
1) by a certificate authority in the system trust store; and
2) to a subject matching the identity of the remote SMTP server.


MailerBuilder.withTransportStrategy(TransportStrategy.SMTPS);

// or through property defaults, skipping builder API:
simplejavamail.transportstrategy=SMTPS
Mailer mailer = MailerBuilder.buildMailer();
§

TransportStrategy.SMTP_TLS

Plaintext SMTP with a mandatory, authenticated STARTTLS upgrade.

Strict validation of server certificates is enabled. Server certificates must be issued
1) by a certificate authority in the system trust store; and
2) to a subject matching the identity of the remote SMTP server.


MailerBuilder.withTransportStrategy(TransportStrategy.SMTP_TLS);

// or through property defaults, skipping builder API:
simplejavamail.transportstrategy=SMTP_TLS
Mailer mailer = MailerBuilder.buildMailer();

To quote FastMail on the differences between SSL and TLS:

SSL and TLS both provide a way to encrypt a communication channel between two computers (e.g. your computer and our server). TLS is the successor to SSL and the terms SSL and TLS are used interchangeably unless you're referring to a specific version of the protocol.

The ordering of protocols in terms of oldest to newest is: SSL v2, SSL v3, TLS v1.0, TLS v1.1, TLS v1.2, TLS v1.3 (currently proposed).

§

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)
  .withSMTPUsername("username")
  .withSMTPPassword("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.

// make the underlying javax.mail produce more logging
MailerBuilder.withDebugLogging(true);
// skip actually sending email, just log it
currentMailerBuilder.withTransportModeLoggingOnly(true);

// change email validation strategy
currentMailerBuilder.withEmailAddressCriteria(EmailAddressCriteria.DEFAULT);
currentMailerBuilder.withEmailAddressCriteria(EmailAddressCriteria.RFC_COMPLIANT);
currentMailerBuilder.withEmailAddressCriteria(EnumSet
       .of(ALLOW_QUOTED_IDENTIFIERS, ALLOW_PARENS_IN_LOCALPART));

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

// 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(yourOwnMailSenderImpl); // 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.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.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.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.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
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.encryption.certificate=x509inStandardPEM.crt
§

Combining both 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.

By importing the Spring support bean from Simple Java Mail, whatever properties are provided through Spring are then transfered 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

Without using the batch-module, you can send emails asynchronously, in which case a single Transport connection is used without pooling it. This means for each email the connection is opened and closed. Depending on your network and SMTP server's capacity, this in itself already provides a huge performance boost if you configure the thread pool accordingly.

/* regular mailer: */
Mailer mailer = mailerBuiler.(..).build();
mailer.send(email); // blocks

/* mailer that defaults to async sending: */
Mailer mailer = mailerBuiler.(..).async().build();
mailer.send(email); // doesn't block

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

Reusing connections with a connection pool

You can really take performance to the next level by using the connection pooling introduced by the batch-module. To do this you simply include the module on the class path (add the maven dependency) which gives you a default of maximum four pooled connections, which auto-close 5 seconds after the last time they were utilized. What this means is if you have bursts of emails batches, the connections spin up to max four and stay alive until activity dies down.

You can modify this behavior by defining a core connection pool size, max pool size and a custom expiry policy.

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).

§

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 batches, 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 AsyncResponse 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!