Skip to content

Commit

Permalink
Add support for the TLS registry to the mailer extension
Browse files Browse the repository at this point in the history
It is now possible to use the TLS registry to configure the mailer extension.
Note that it's required to use a named configuration in this case, to avoid conflicts.
  • Loading branch information
cescoffier committed Jun 7, 2024
1 parent 75928a5 commit 61a1c78
Show file tree
Hide file tree
Showing 16 changed files with 308 additions and 72 deletions.
41 changes: 39 additions & 2 deletions docs/src/main/asciidoc/mailer-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ You need to switch on 2-Step Verification at https://myaccount.google.com/securi

When done, you can configure your Quarkus application by adding the following properties to your `application.properties`:

With TLS:
With TLS (recommended):

[source,properties]
----
Expand Down Expand Up @@ -591,13 +591,50 @@ quarkus.mailer.port=465
quarkus.mailer.username=${sendgrid.username}
quarkus.mailer.password=${sendgrid.key}
quarkus.mailer.start-tls=OPTIONAL
quarkus.mailer.ssl=true
quarkus.mailer.login=REQUIRED
quarkus.mailer.from=...
quarkus.mailer.tls-configuration-name=mailer
quarkus.tls.mailer.enable-tls=true
quarkus.mailer.mock=false # In dev mode, prevent from using the mock SMTP server
----

== Configuring TLS

To establish a TLS connection, you need to configure a _named_ configuration using the xref:./tls-registry-reference.adoc[TLS registry]:

[source, properties]
----
quarkus.tls.my-mailer.trust-store.p12.path=server-truststore.p12
quarkus.tls.my-mailer.trust-store.p12.password=secret
quarkus.mailer.tls-configuration-name=my-mailer # Reference the named configuration
----

If your SMTP server uses a valid (trusted) certificate, you need to enable TLS explicitly (as you do not have to configure a trust store):

[source, properties]
----
quarkus.tls.my-mailer.enable-tls=true
quarkus.mailer.tls-configuration-name=my-mailer # Reference the named configuration
----

WARNING: When using the mailer, using a _named_ configuration is required to avoid conflicts with other TLS configurations.
The mailer will not use the default TLS configuration.

[IMPORTANT]
====
When `quarkus.tls.trust-all` is set to `true`, the trust store configuration is ignored. This is not recommended for production.
Also, we recommend avoiding using `quarkus.tls.trust-all`, and use a named configuration instead if `trust-all` is required:
[source, properties]
----
quarkus.tls.my-mailer.trust-all=true
quarkus.mailer.tls-configuration-name=my-mailer # Reference the named configuration
----
====

[[configuration-reference]]
== Mailer Configuration Reference

Expand Down
5 changes: 5 additions & 0 deletions extensions/mailer/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>me.escoffier.certs</groupId>
<artifactId>certificate-generator-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,37 @@ public class MailerRuntimeConfig {
@ConfigItem
public Optional<String> password = Optional.empty();

/**
* The name of the TLS configuration to use.
* <p>
* If not set and the default TLS configuration is configured ({@code quarkus.tls.*}) then that will be used.
* If a name is configured, it uses the configuration from {@code quarkus.tls.<name>.*}
* If a name is configured, but no TLS configuration is found with that name then an error will be thrown.
* <p>
* If no TLS configuration is set, and {@code quarkus.tls.*} is not configured, then,
* `quarkus.mailer` SSL configuration will be used.
* <p>
*/
@ConfigItem
public Optional<String> tlsConfigurationName = Optional.empty();

/**
* Enables or disables the TLS/SSL.
*
* @deprecated Use the TLS registry instead.
*/
@Deprecated
@ConfigItem(defaultValue = "false")
public boolean ssl;

/**
* Set whether all server certificates should be trusted.
* This option is only used when {@link #ssl} is enabled.
*
* @deprecated Use the TLS registry instead.
*/
@ConfigItem
@Deprecated
public Optional<Boolean> trustAll = Optional.empty();

/**
Expand Down Expand Up @@ -151,7 +171,7 @@ public class MailerRuntimeConfig {
/**
* Set the trust store.
*
* @deprecated Use {{@link #truststore} instead.
* @deprecated Use the TLS registry instead.
*/
@Deprecated
@ConfigItem
Expand All @@ -160,16 +180,19 @@ public class MailerRuntimeConfig {
/**
* Sets the trust store password if any.
*
* @deprecated Use {{@link #truststore} instead.
* @deprecated Use the TLS registry instead.
*/
@ConfigItem
@Deprecated
public Optional<String> keyStorePassword = Optional.empty();

/**
* Configures the trust store.
*
* @deprecated Use the TLS registry instead.
*/
@ConfigItem
@Deprecated
public TrustStoreConfig truststore = new TrustStoreConfig();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,17 @@
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.net.JksOptions;
import io.vertx.core.net.PemTrustOptions;
import io.vertx.core.net.PfxOptions;
import io.vertx.core.net.SSLOptions;
import io.vertx.core.net.TrustOptions;
import io.vertx.ext.mail.CanonicalizationAlgorithm;
import io.vertx.ext.mail.DKIMSignOptions;
import io.vertx.ext.mail.LoginOption;
import io.vertx.ext.mail.MailClient;
import io.vertx.ext.mail.MailConfig;
import io.vertx.ext.mail.StartTLSOptions;

/**
Expand Down Expand Up @@ -57,10 +60,9 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti
Map<String, MockMailboxImpl> localMockMailboxes = new HashMap<>();
Map<String, MutinyMailerImpl> localMutinyMailers = new HashMap<>();

TlsConfiguration defaultTlsConfiguration = tlsRegistry.getDefault().orElse(null);

if (mailerSupport.hasDefaultMailer) {
MailClient mailClient = createMailClient(vertx, mailersRuntimeConfig.defaultMailer, defaultTlsConfiguration);
MailClient mailClient = createMailClient(vertx, DEFAULT_MAILER_NAME, mailersRuntimeConfig.defaultMailer,
tlsRegistry);
io.vertx.mutiny.ext.mail.MailClient mutinyMailClient = io.vertx.mutiny.ext.mail.MailClient.newInstance(mailClient);
MockMailboxImpl mockMailbox = new MockMailboxImpl();
localClients.put(DEFAULT_MAILER_NAME, mailClient);
Expand All @@ -80,7 +82,8 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti
MailerRuntimeConfig namedMailerRuntimeConfig = mailersRuntimeConfig.namedMailers
.getOrDefault(name, new MailerRuntimeConfig());

MailClient namedMailClient = createMailClient(vertx, namedMailerRuntimeConfig, defaultTlsConfiguration);
MailClient namedMailClient = createMailClient(vertx, name, namedMailerRuntimeConfig,
tlsRegistry);
io.vertx.mutiny.ext.mail.MailClient namedMutinyMailClient = io.vertx.mutiny.ext.mail.MailClient
.newInstance(namedMailClient);
MockMailboxImpl namedMockMailbox = new MockMailboxImpl();
Expand Down Expand Up @@ -130,8 +133,9 @@ public void stop() {
}
}

private MailClient createMailClient(Vertx vertx, MailerRuntimeConfig config, TlsConfiguration defaultTlsConfiguration) {
io.vertx.ext.mail.MailConfig cfg = toVertxMailConfig(config, defaultTlsConfiguration);
private MailClient createMailClient(Vertx vertx, String name, MailerRuntimeConfig config,
TlsConfigurationRegistry tlsRegistry) {
io.vertx.ext.mail.MailConfig cfg = toVertxMailConfig(name, config, tlsRegistry);
// Do not create a shared instance, as we want separated connection pool for each SMTP servers.
return MailClient.create(vertx, cfg);
}
Expand Down Expand Up @@ -198,9 +202,8 @@ private io.vertx.ext.mail.DKIMSignOptions toVertxDkimSignOptions(DkimSignOptions
return vertxDkimOptions;
}

private io.vertx.ext.mail.MailConfig toVertxMailConfig(MailerRuntimeConfig config,
TlsConfiguration defaultTlsConfiguration) {
boolean globalTrustAll = defaultTlsConfiguration != null && defaultTlsConfiguration.isTrustAll();
private io.vertx.ext.mail.MailConfig toVertxMailConfig(String name, MailerRuntimeConfig config,
TlsConfigurationRegistry tlsRegistry) {
io.vertx.ext.mail.MailConfig cfg = new io.vertx.ext.mail.MailConfig();
if (config.authMethods.isPresent()) {
cfg.setAuthMethods(config.authMethods.get());
Expand Down Expand Up @@ -231,7 +234,6 @@ private io.vertx.ext.mail.MailConfig toVertxMailConfig(MailerRuntimeConfig confi
cfg.addDKIMSignOption(toVertxDkimSignOptions(config.dkim));
}

cfg.setSsl(config.ssl);
cfg.setStarttls(StartTLSOptions.valueOf(config.startTLS.toUpperCase()));
cfg.setMultiPartOnly(config.multiPartOnly);

Expand All @@ -242,9 +244,7 @@ private io.vertx.ext.mail.MailConfig toVertxMailConfig(MailerRuntimeConfig confi
cfg.setKeepAliveTimeout((int) config.keepAliveTimeout.toMillis());
cfg.setKeepAliveTimeoutUnit(TimeUnit.MILLISECONDS);

boolean trustAll = config.trustAll.isPresent() ? config.trustAll.get() : globalTrustAll;
cfg.setTrustAll(trustAll);
applyTruststore(config, cfg);
configureTLS(name, config, tlsRegistry, cfg);

// Sets the metrics name so micrometer metrics will collect metrics for the client.
// Because the mail client is _unnamed_, we only pass a prefix.
Expand All @@ -255,6 +255,63 @@ private io.vertx.ext.mail.MailConfig toVertxMailConfig(MailerRuntimeConfig confi
return cfg;
}

private void configureTLS(String name, MailerRuntimeConfig config, TlsConfigurationRegistry tlsRegistry, MailConfig cfg) {
TlsConfiguration configuration = null;
boolean defaultTrustAll = false;
if (config.tlsConfigurationName.isPresent()) {
Optional<TlsConfiguration> maybeConfiguration = tlsRegistry.get(config.tlsConfigurationName.get());
if (!maybeConfiguration.isPresent()) {
throw new IllegalStateException("Unable to find the TLS configuration "
+ config.tlsConfigurationName.get() + " for the mailer " + name + ".");
}
configuration = maybeConfiguration.get();
} else if (tlsRegistry.getDefault().isPresent() && tlsRegistry.getDefault().get().isTrustAll()) {
defaultTrustAll = tlsRegistry.getDefault().get().isTrustAll();
if (defaultTrustAll) {
LOGGER.warn("The default TLS configuration is set to trust all certificates. This is a security risk."
+ "Please use a named TLS configuration for the mailer " + name + " to avoid this warning.");
}
}

if (configuration != null) {
cfg.setSsl(true);

if (configuration.getTrustStoreOptions() != null) {
cfg.setTrustOptions(configuration.getTrustStoreOptions());
}
if (configuration.getKeyStoreOptions() != null) {
cfg.setKeyCertOptions(configuration.getKeyStoreOptions());
}

if (configuration.isTrustAll()) {
cfg.setTrustAll(true);
}
if (configuration.getHostnameVerificationAlgorithm().isPresent()) {
cfg.setHostnameVerificationAlgorithm(configuration.getHostnameVerificationAlgorithm().get());
}

SSLOptions sslOptions = configuration.getSSLOptions();
if (sslOptions != null) {
cfg.setSslHandshakeTimeout(sslOptions.getSslHandshakeTimeout());
cfg.setSslHandshakeTimeoutUnit(sslOptions.getSslHandshakeTimeoutUnit());
for (String suite : sslOptions.getEnabledCipherSuites()) {
cfg.addEnabledCipherSuite(suite);
}
for (Buffer buffer : sslOptions.getCrlValues()) {
cfg.addCrlValue(buffer);
}
cfg.setEnabledSecureTransportProtocols(sslOptions.getEnabledSecureTransportProtocols());

}

} else {
boolean trustAll = config.trustAll.isPresent() ? config.trustAll.get() : defaultTrustAll;
cfg.setSsl(config.ssl || trustAll);
cfg.setTrustAll(trustAll);
applyTruststore(config, cfg);
}
}

private void applyTruststore(MailerRuntimeConfig config, io.vertx.ext.mail.MailConfig cfg) {
// Handle deprecated config
if (config.keyStore.isPresent()) {
Expand All @@ -272,7 +329,7 @@ private void applyTruststore(MailerRuntimeConfig config, io.vertx.ext.mail.MailC

TrustStoreConfig truststore = config.truststore;
if (truststore.isConfigured()) {
if (cfg.isTrustAll()) { // USe the value configured before.
if (cfg.isTrustAll()) { // Use the value configured before.
LOGGER.warn(
"SMTP is configured with a trust store and also with trust-all, disable trust-all to enforce the trust store usage");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ private void startServer(Vertx vertx) {
NetServerOptions nsOptions = new NetServerOptions();
int port = ssl ? 1465 : 1587;
nsOptions.setPort(port);
if (keystore == null) {
keystore = "src/test/resources/certs/server2.jks";
}
JksOptions jksOptions = new JksOptions().setPath(keystore).setPassword("password");
nsOptions.setKeyStoreOptions(jksOptions);
if (ssl) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;
import io.vertx.mutiny.core.Vertx;
import me.escoffier.certs.Format;
import me.escoffier.certs.junit5.Certificate;
import me.escoffier.certs.junit5.Certificates;

@Certificates(baseDir = "target/certs", certificates = {
@Certificate(name = "mailer-certs", formats = Format.PKCS12, password = "password") })
public class FakeSmtpTestBase {

protected static final int FAKE_SMTP_PORT = 1465;
protected static final String SERVER_JKS = "certs/server2.jks";
protected static final String CLIENT_JKS = "certs/client.jks";
protected static final String SERVER_JKS = "target/certs/mailer-certs-keystore.p12";
protected static final String CLIENT_TRUSTSTORE = "target/certs/mailer-certs-truststore.p12";

protected static final String FROM = "test@test.org";
protected static final String TO = "foo@quarkus.io";
Expand Down Expand Up @@ -78,6 +83,35 @@ public void register(String name, TlsConfiguration configuration) {
return mailers.reactiveMailerFromName(Mailers.DEFAULT_MAILER_NAME);
}

protected ReactiveMailer getMailer(MailersRuntimeConfig config, String confName, TlsConfiguration configuration) {
Mailers mailers = new Mailers(vertx.getDelegate(), vertx, config, LaunchMode.NORMAL,
new MailerSupport(true, Set.of()),
new TlsConfigurationRegistry() {

@Override
public Optional<TlsConfiguration> get(String name) {
if (confName != null && confName.equals(name)) {
return Optional.of(configuration);
}
return Optional.empty();
}

@Override
public Optional<TlsConfiguration> getDefault() {
if (confName == null) {
return Optional.of(configuration);
}
return Optional.empty();
}

@Override
public void register(String name, TlsConfiguration configuration) {
throw new UnsupportedOperationException();
}
});
return mailers.reactiveMailerFromName(Mailers.DEFAULT_MAILER_NAME);
}

protected MailersRuntimeConfig getDefaultConfig() {
MailersRuntimeConfig mailersConfig = new MailersRuntimeConfig();
mailersConfig.defaultMailer = new MailerRuntimeConfig();
Expand Down
Loading

0 comments on commit 61a1c78

Please sign in to comment.