Protect and operate

Security

Configure TLS, OAuth2, server verification, DKIM, S/MIME, and protection against header injection.

Security at a glance

Choose your protection.

These are the security features most applications come here for. The table of contents below covers every transport, certificate, and connection setting.

§

Authentication methods with 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 four strategies:

TransportStrategy strategy = TransportStrategy.SMTP;
// or: TransportStrategy.SMTPS, TransportStrategy.SMTP_TLS, TransportStrategy.SMTP_OAUTH2

MailerBuilder
  .withSMTPServer("host", port, "username", passwordOrOAUTH2Token)
  .withTransportStrategy(strategy);

Or with property default:

simplejavamail.transportstrategy=SMTP
# or: SMTPS, SMTP_TLS, SMTP_OAUTH2

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

§

TransportStrategy.SMTP

MailerBuilder.withTransportStrategy(TransportStrategy.SMTP);

The SMTP legacy strategy is the oldest and simplest strategy, which works with simple username and password. This transport strategy attempts a TLS upgrade, but falls back to plaintext when a mail server does not indicate support for STARTTLS.

Whenever TLS is negotiated, Simple Java Mail verifies that the certificate belongs to the SMTP host. From 9.2.0 onward, it also uses the JVM trust store instead of accepting every certificate by default. Both checks can be overridden, but that should be reserved for controlled compatibility or test environments.

This strategy can still fall back to plaintext when the server does not advertise STARTTLS. An active attacker can therefore suppress the STARTTLS offer before certificate validation begins. Use TransportStrategy.SMTPS or TransportStrategy.SMTP_TLS when encryption must be mandatory.

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
	  .verifyingServerIdentity(false)
	  .withTransportStrategy(TransportStrategy.SMTP);
Or with properties:
simplejavamail.defaults.verifyserveridentity=false
simplejavamail.opportunistic.tls=false
§

TransportStrategy.SMTPS

SMTP entirely encapsulated by TLS. Commonly known as SMTPS.

MailerBuilder.withTransportStrategy(TransportStrategy.SMTPS);

Strict certificate validation is the default from 9.2.0 onward. Unless you configure an explicit exception, server certificates must be issued

  1. by a certificate authority in the JVM trust store; and
  2. to a subject matching the identity of the remote SMTP server.
§

TransportStrategy.SMTP_TLS

Plaintext SMTP with a mandatory, authenticated STARTTLS upgrade.

MailerBuilder.withTransportStrategy(TransportStrategy.SMTP_TLS);

Strict certificate validation is the default from 9.2.0 onward. Unless you configure an explicit exception, server certificates must be issued

  1. by a certificate authority in the JVM trust store; and
  2. to a subject matching the identity of the remote SMTP server.

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

§

TransportStrategy.SMTP_OAUTH2

SMTP_OAUTH2 uses normal SMTP submission but authenticates with the XOAUTH2 mechanism instead of a mailbox password. It uses port 587 by default and requires STARTTLS, so the connection is encrypted before authentication.

The SMTP username identifies the mailbox and the credential is an OAuth2 access token, not a refresh token, client secret, or authorization code. If your application already has a token whose lifetime covers the Mailer, pass it in the usual password position.

Mailer mailer = MailerBuilder
    .withSMTPServer("smtp.example.com", 587, "user@example.com", accessToken)
    .withTransportStrategy(TransportStrategy.SMTP_OAUTH2)
    .buildMailer();

Simple Java Mail configures SMTP and XOAUTH2, but it does not run the OAuth authorization flow itself. Your application remains responsible for obtaining the access token from its provider.

For a long-lived Mailer, use an OAuth2AccessTokenProvider so an expired token can be replaced without rebuilding the Mailer. The provider can use whichever OAuth client, token endpoint, cache, or refresh flow your application already uses.

OAuth2AccessTokenProvider accessTokens =
    () -> tokenService.currentAccessToken();

Mailer mailer = MailerBuilder
    .withSMTPServer("smtp.example.com", 587, "user@example.com")
    .withTransportStrategy(TransportStrategy.SMTP_OAUTH2)
    .withOAuth2AccessTokenProvider(accessTokens)
    .buildMailer();

Simple Java Mail asks the provider immediately before opening a physical SMTP connection and again when a disconnected connection must be reopened. An already-connected pooled transport is reused without another provider call. The provider should be thread-safe and cache a valid token rather than contacting the authorization server on every call.

Choose either a fixed token or a provider for one Mailer; configuring both is rejected. Provider errors and null or blank results stop the connection without exposing the token, while SMTP authentication rejection is returned normally without a hidden retry.

A supplied Jakarta Mail Session can use the provider too; configure that Session for mail.smtp.auth.mechanisms=XOAUTH2 and then call MailerBuilder.usingSession(session).withOAuth2AccessTokenProvider(...).

Spring applications can register the provider as a bean. See Spring support for the direct bean form and an adapter for Spring Security's OAuth2 client manager.

§

Configure your own SSL connection factory

Furthermore, you can take complete control of SSL connections by providing your own SSL connection factory:

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

Certificate trust

From 9.2.0 onward, Simple Java Mail leaves mail.*.ssl.trust unset by default. Angus Mail then validates the certificate chain against the JVM trust store.

Certificate trust and server identity are two separate checks. A normal production connection needs both: a certificate from a trusted issuer and a certificate that names the SMTP host you connected to.

For a private certificate authority, the best fix is to add that CA to the JVM trust store. If that is not practical, trustingSSLHosts(...) creates a narrow exception for named hosts. trustingAllHosts(true) accepts certificates from every host and should be a last-resort compatibility setting.

MailerBuilder
	.withSMTPServer("smtp.example.com", 587)
	.withTransportStrategy(TransportStrategy.SMTP_TLS)
	// secure default since 9.2.0; shown explicitly here
	.trustingAllHosts(false)
	.verifyingServerIdentity(true);

// narrow exception when the issuing CA cannot be added to the JVM trust store
MailerBuilder.withSMTPServer("smtp.internal.example", 587)
	.trustingSSLHosts("smtp.internal.example");

// broad compatibility escape hatch; avoid in production
MailerBuilder.withSMTPServer("smtp.internal.example", 587)
	.trustingAllHosts(true);
Or with property default:
simplejavamail.defaults.trustallhosts=false
# optional narrow exception; ignored when trustallhosts is true:
simplejavamail.defaults.trustedhosts=smtp.internal.example
§

Verifying server identity

Simple Java Mail enables server identity verification for TLS connections by default (also see RFC 2595, 2.4. Server Identity Check). Angus Mail checks that the certificate names the host used to start the connection.

This does not replace certificate-chain validation. Keep both checks enabled for normal production use, including when a private CA has been added to the JVM trust store. Disabling hostname verification is intended only for controlled compatibility or testing scenarios.

// enabled by default
MailerBuilder.withSMTPServer("smtp.example.com", 587)
	.verifyingServerIdentity(true);

// compatibility escape hatch; avoid in production
MailerBuilder.withSMTPServer("smtp.internal.example", 587)
	.verifyingServerIdentity(false);
Or with property default:
simplejavamail.defaults.verifyserveridentity=true
§

Scanning for suspicious content

Simple Java Mail by default tests most fields and headers for suspicious content, which could indicate a CRLF injection attack. This is a unique feature of Simple Java Mail.

The values being scanned are:

  • subject
  • every header name and value
  • every attachment name, content ID, nested datasource name and description
  • every embedded image name, content ID, nested datasource name and description
  • from recipient name and address
  • every replyTo recipient name and address, if provided
  • bounceTo recipient name and address, if provided
  • every TO/CC/BCC recipient name and address
  • disposition-notification-to recipient name and address, if provided
  • return-receipt-to recipient name and address, if provided

Here's some more info on this topic:

This behaviour can only be turned off by turning off all client validations, which also includes checking for email completeness and email-address validations. The scans will still be performed, but issues found will only be logged as warnings.

MailerBuilder
	.disablingAllClientValidation(true);
§

Signing emails with DKIM dkim-module

Simple Java Mail supports signing with DKIM domain keys at email or Mailer level.

Use dkimPrivateKeyPath(String) or dkimPrivateKeyPath(File) for a key file; it is read immediately. For key material already in memory, dkimPrivateKeyData(byte[]) copies the bytes. The String overload converts the supplied key data to UTF-8 bytes without Base64-decoding it. The InputStream overload consumes the stream immediately but leaves closing it to the caller.

currentEmailBuilder.signWithDomainKey(
	DkimConfig.builder()
		.dkimPrivateKeyPath("secrets/dkim-private-key.der")
		.dkimSigningDomain("your-domain.org")
		.dkimSelector("dkim1")
		.useLengthParam(false) // default
		.build()
);
Or configure DKIM once for every email sent through a mailer:
currentMailerBuilder
    .withDefaultDkimSigning(
        DkimConfig.builder()
            .dkimPrivateKeyData(privateKeyBytes)
            .dkimSigningDomain("your_domain.org")
            .dkimSelector("your_selector")
            .build())
    .buildMailer();
Or with properties:
# defaults on Mailer level:
				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 # default
				# Omit header exclusions to sign the default header set, including From.
				simplejavamail.dkim.signing.header_canonicalization=RELAXED
				simplejavamail.dkim.signing.body_canonicalization=RELAXED
				simplejavamail.dkim.signing.algorithm=SHA256_WITH_RSA
				

Use file: for a path or base64: followed by Base64-encoded key bytes for inline data. A missing explicit file or malformed Base64 value fails during configuration without including the key data in the error. Unprefixed values keep their pre-9.2 interpretation for compatibility.

Use the exceptions deliberately

The normal configuration leaves the DKIM body-length tag off and signs the default header list. Turn on the l= body-length tag only when a known downstream service appends a footer: content appended after the signed length is not covered by the signature.

Likewise, exclude a header only when that specific relay rewrites it. From is mandatory in a DKIM signature, so Simple Java Mail rejects it as an exclusion.

DkimConfig.builder()
    .dkimPrivateKeyPath("secrets/dkim-private-key.der")
    .dkimSigningDomain("your-domain.org")
    .dkimSelector("dkim1")
    // Only if a known service appends a footer:
    .useLengthParam(true)
    // Only for headers that this relay rewrites:
    .excludedHeadersFromDkimDefaultSigningList("Message-ID", "Date")
    .build();

You can also use the helper method to sign a message yourself, but beware that the signing is only triggered when the MimeMessage streamed to a transport (or file):

MailerHelper.signMessageWithDKIM(mimeMessageToSign, emailContainingSigningDetails);

Excluding headers

Header exclusions are a compatibility setting, not a normal tuning option. Leave the default list alone unless a specific relay is known to rewrite a header. From must remain signed and cannot be excluded.

DkimConfig.builder()
    // ... your signing key, domain and selector
    .excludedHeadersFromDkimDefaultSigningList("Message-ID", "Date")
    .build()
§

Signing / encrypting emails with S/MIME smime-module

Simple Java Mail supports signing and encrypting with S/MIME.


You can sign, encrypt or both sign and encrypt an email. In the latter case the email will first be signed and then encrypted, as per advice of the underlying library. All signing/encrypting is performed when the email is being sent.


You can sign individual emails or sign all emails by configuring S/MIME defaults on the Mailer or through properties.

For encryption, Simple Java Mail can use one email-level encryption certificate, or different certificates per recipient. When any TO/CC/BCC recipient carries a certificate, that recipient certificate is used first. Recipients without a certificate fall back to the email-level or mailer-level SmimeEncryptionConfig, if one is configured.

For a mailing list, department, or any group that should share certificate rules, use RecipientsBuilder before adding the flat recipient list to the email. withDefaultSmimeCertificate(...) fills only missing certificates, withFixedSmimeCertificate(...) replaces every recipient certificate in that group, and clearingSmimeCertificates() removes certificate state from reused recipients so the email-level or mailer-level fallback certificate can apply.


For maximum flexibility, you can configure all algorithms and certificates specific to S/MIME signing and encryption. This includes choosing the key encapsulation algorithm and cipher algorithm for encryption, and the signature algorithm for signing. For a list of available algorithms, see the SmimeEncryptionConfig and SmimeSigningConfig classes.

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

Email emailToBeSigned = currentEmailBuilder
    .(..)
    .signWithSmime(SmimeSigningConfig.builder()
		.pkcs12Config(myKeyInfo)
		.signatureAlgorithm("SHA256withRSA") // optional
		.build())
    .buildEmail();

mailer.sendMail(emailToBeSigned);
Encrypting an email:
Email emailToBeEncrypted = currentEmailBuilder
    .(..)
    .encryptWithSmime(SmimeEncryptionConfig.builder()
		.x509Certificate("x509CertificateInStandardPEM.crt")
		.keyEncapsulationAlgorithm("RSA_OAEP_SHA384") // optional
		.cipherAlgorithm("AES256_CBC") // optional
		.build())
    .buildEmail();

mailer.sendMail(emailToBeEncrypted);
Encrypting with different recipient certificates:
SmimeEncryptionConfig algorithmsAndFallback = SmimeEncryptionConfig.builder()
    .x509Certificate(fallbackCertificate)
    .keyEncapsulationAlgorithm("RSA_OAEP_SHA384")
    .cipherAlgorithm("AES256_CBC")
    .build();

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

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

Email emailToBeEncrypted = currentEmailBuilder
    .withRecipients(alice, bob)
    .encryptWithSmime(algorithmsAndFallback)
    .buildEmail();
Encrypting a group with exactly one certificate:
Collection<Recipient> finance = new RecipientsBuilder()
    .withFixedSmimeCertificate(financeCertificate)
    .withRecipientsWithDefaultName("Finance", Message.RecipientType.TO,
            "alice@example.com", "bob@example.com")
    .buildRecipients();

Email emailToBeEncrypted = currentEmailBuilder
    .withRecipients(finance)
    .buildEmail();
Defaulting only missing certificates in a group:
Collection<Recipient> finance = new RecipientsBuilder()
    .withDefaultSmimeCertificate(financeFallbackCertificate)
    .withRecipients(previousFinanceRecipients)
    .buildRecipients();
// recipients that already had their own certificates keep them;
// the others receive financeFallbackCertificate
Clearing reused recipient certificates so the email-level certificate applies:
Collection<Recipient> reusableRecipients = new RecipientsBuilder()
    .clearingSmimeCertificates()
    .withRecipients(previousRecipients)
    .buildRecipients();

Email emailToBeEncrypted = currentEmailBuilder
    .withRecipients(reusableRecipients)
    .encryptWithSmime(emailLevelSmimeEncryptConfig)
    .buildEmail();
Sign all emails by default from Java:
SmimeSigningConfig signingDefaults = SmimeSigningConfig.builder()
    .pkcs12Config(myKeyInfo)
    .build();

currentMailerBuilder
    (...)
    .withEmailDefaults(EmailBuilder.startingBlank()
		.signWithSmime(signingDefaults)
		.buildEmailCompletedWithDefaultsAndOverrides()) // retain property defaults too
    .buildMailer();
Property-only S/MIME defaults:
# Sign every email sent through a property-configured mailer.
simplejavamail.smime.signing.keystore=my_smime_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=SHA256withRSA

# Encrypt with one fallback certificate and the selected algorithms.
# Recipient-level certificates configured in Java still take precedence.
simplejavamail.smime.encryption.certificate=shared-recipient-certificate.pem
simplejavamail.smime.encryption.key_encapsulation_algorithm=RSA
# AES is recommended; DES_EDE3_CBC remains available for legacy recipient compatibility.
simplejavamail.smime.encryption.cipher=AES256_CBC

# Optional when the application is fully property-driven.
simplejavamail.defaults.to.name=Finance
simplejavamail.defaults.to.address=alice@example.com,bob@example.com

Properties can configure the signing identity and a single encryption fallback. They cannot express different S/MIME certificates per recipient; use RecipientBuilder or RecipientsBuilder for per-recipient or group certificate policy.

§

Reading S/MIME signed / encrypted attachments smime-module

Simple Java Mail can automatically handle S/MIME signed messages or attachments and has some useful extras such as providing you with metadata.

Email mergedEmail = EmailConverter.outlookMsgToEmail("yourSMIMESignedMessage.msg"); // or
Email mergedEmail = EmailConverter.emlToEmail("yourSMIMESignedMessage.eml");

// all attachments as-is:
mergedEmail.getAttachments(); // smime.p7m, my-doc.docx
// all attachments with the encrypted ones replaced:
mergedEmail.getDecryptedAttachments(); // signed-email.eml, my-doc.docx

// if the message itself was signed (rather than an independently signed attachment):
OriginalSmimeDetails details = mergedEmail.getOriginalSmimeDetails();
details.getSmimeMode(); // SIGNED
details.getSmimeMime(); // application/pkcs7-mime or multipart/signed
details.getSmimeType(); // signed-data, enveloped-data
details.getSmimeName(); // smime.p7m or smime.p7s
details.getSmimeMicalg(); // ie. sha-512
details.getSmimeSignedBy(); // common name from the first signer certificate
details.getSmimeSignatureValid(); // true, false, or null when no check was performed
§

What the signature status means smime-module

getSmimeSignatureValid() reports cryptographic signature integrity. A value of true means every signature represented by these details was checked against the signer certificate included with the S/MIME message. A value of false means at least one signature failed or could not be verified. A null value means no signature check applied or no check was performed.

OriginalSmimeDetails details = mergedEmail.getOriginalSmimeDetails();

if (!Boolean.TRUE.equals(details.getSmimeSignatureValid())) {
    // The signature is invalid, could not be checked, or was not checked.
    // Reject or quarantine the message when integrity is required.
}

// Passing this check does not authenticate the From address.

The signer certificate is supplied by the message itself. Simple Java Mail does not validate its certificate path, validity period, revocation status or key usage, and it does not compare the certificate identity with the message's From address. Likewise, getSmimeSignedBy() is the common name printed on the first signer certificate, not a trusted sender identity. Applications that authenticate senders must validate the original S/MIME certificate and identity under their own trust policy before accepting that identity.

When cryptographic verification fails, Simple Java Mail still keeps parsed signed content available and records false. This preserves the lenient conversion introduced for relays that alter signed MIME formatting. Unsupported encrypted S/MIME payloads in Outlook messages are also treated leniently so the surrounding message can still be converted when possible.

§

S/MIME signed messages are merged by default smime-module

As an S/MIME signed message is actually nested as an attachment, the default behavior is to merge the S/MIME signed content into the root message. This only happens if there was exactly one S/MIME signed attachment and the decrypted version is of type "message/rfc822".

This default behavior can be deactivated. For your convenience, the decrypted message is available as a separate Email instance:

Email nonMergedEmail = EmailBuilder
                .copying(mergedEmail)
                .clearSMIMESignedAttachmentMergingBehavior()
                .buildEmail();

// or by configuring the intermediary builder:
emailBuilder = EmailConverter.outlookMsgToEmailBuilder(msgFile); // or
emailBuilder = EmailConverter.emlToEmailBuilder(emlFile);
Email nonMergedEmail = emailBuilder
                .notMergingSingleSMIMESignedAttachment()
                .buildEmail();
You always have access to the nested decrypted message:
mergedEmail.getSmimeSignedEmail();
nonMergedEmail.getSmimeSignedEmail();

If a message is both signed and encrypted, getSmimeSignedEmail() will itself have a nested getOriginalSmimeDetails().


        signedAndEncrypted.getOriginalSmimeDetails().getSmimeMode(); // SIGNED_ENCRYPTED
        Email signedOrEncrypted = signedAndEncrypted.getSmimeSignedEmail();

        signedOrEncrypted.getOriginalSmimeDetails().getSmimeMode(); // SIGNED or ENCRYPTED

        // whether it is SIGNED or ENCRYPTED depends on the order in which the original
        // email client handled this S/MIME scenario
§

Decrypting S/MIME attachments using certificate smime-module

Every conversion method optionally accepts a Pkcs12Config instance, which contains details about your key store and certificate. With that, you can decrypt an S/MIME encrypted mail.

Pkcs12Config yourPkcs12Config = Pkcs12Config.builder()
      .pkcs12Store("smime_keystore.pkcs12") // path, File or InputStream
      .storePassword("letmein")
      .keyAlias("smime_test_user_alias")
      .keyPassword("letmein")
      .build();

EmailConverter.outlookMsgToEmail("yourSMIMEEncryptedMessage.msg", yourPkcs12Config); // or
EmailConverter.emlToEmail("yourSMIMEEncryptedMessage.eml", yourPkcs12Config);

mergedEmail.getOriginalSmimeDetails().getSmimeMode(); // ENCRYPTED or SIGNED_ENCRYPTED