Skip to content

Commit

Permalink
Enhance mailer to support XOAuth2 authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
sberyozkin committed Aug 25, 2024
1 parent 2105d46 commit 91ad547
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 2 deletions.
8 changes: 8 additions & 0 deletions extensions/mailer/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-mail-client</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-web-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus.security</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>com.github.davidmoten</groupId>
<artifactId>subethasmtp</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -272,4 +273,16 @@ public class MailerRuntimeConfig {
*/
@ConfigItem(defaultValue = "false")
public boolean logRejectedRecipients = false;

/**
* Parameters that should be sent to the OAuth2 provider to acquire a token.
*/
@ConfigItem
public Map<String, String> oauth2Params = Map.of();

/**
* OAuth2 token endpoint
*/
@ConfigItem
public Optional<String> oauth2TokenEndpoint = Optional.empty();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.mailer.runtime;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -14,15 +16,20 @@

import org.jboss.logging.Logger;

import io.quarkus.arc.Arc;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.mailer.Mailer;
import io.quarkus.mailer.MockMailbox;
import io.quarkus.mailer.reactive.ReactiveMailer;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.configuration.ConfigurationException;
import io.quarkus.security.credential.TokenCredential;
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.JksOptions;
import io.vertx.core.net.PemTrustOptions;
import io.vertx.core.net.PfxOptions;
Expand All @@ -34,6 +41,10 @@
import io.vertx.ext.mail.MailClient;
import io.vertx.ext.mail.MailConfig;
import io.vertx.ext.mail.StartTLSOptions;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import io.vertx.ext.web.client.WebClientOptions;

/**
* This class is a sort of producer for mailer instances.
Expand Down Expand Up @@ -135,7 +146,7 @@ public void stop() {

private MailClient createMailClient(Vertx vertx, String name, MailerRuntimeConfig config,
TlsConfigurationRegistry tlsRegistry) {
io.vertx.ext.mail.MailConfig cfg = toVertxMailConfig(name, config, tlsRegistry);
io.vertx.ext.mail.MailConfig cfg = toVertxMailConfig(vertx, 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 @@ -202,11 +213,34 @@ private io.vertx.ext.mail.DKIMSignOptions toVertxDkimSignOptions(DkimSignOptions
return vertxDkimOptions;
}

private io.vertx.ext.mail.MailConfig toVertxMailConfig(String name, MailerRuntimeConfig config,
private io.vertx.ext.mail.MailConfig toVertxMailConfig(Vertx vertx, 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());
if ("XOAUTH2".equals(config.authMethods.get())) {
if (config.oauth2TokenEndpoint.isPresent()) {
WebClientOptions options = new WebClientOptions()
.setSsl(true).setTrustAll(true).setVerifyHost(false);
WebClient webClient = WebClient.create(vertx, options);
Buffer encodedParams = urlEncodeOAuth2Params(config.oauth2Params);
HttpRequest<Buffer> request = webClient.getAbs(config.oauth2TokenEndpoint.get());
request.putHeader("Content-Type", "application/x-www-form-urlencode");
request.putHeader("Accept", "application/json");
HttpResponse<Buffer> response = awaitHttpResponse(request.sendBuffer(encodedParams));
JsonObject json = response.bodyAsJsonObject();
cfg.setPassword(json.getString("access_token"));
} else if (!config.password.isPresent()) {
// It is not clear right now what it means if XOAuth2 is set and the password is set.
// For example, this password may be set in devmode to a token acquired out of band
// Therefore we avoid picking up an authentication token if the password is set.
InstanceHandle<TokenCredential> token = Arc.container().instance(TokenCredential.class);
if (token.isAvailable()) {
cfg.setPassword(token.get().getToken());
}
}
}
}
cfg.setDisableEsmtp(config.disableEsmtp);
cfg.setHostname(config.host);
Expand All @@ -221,6 +255,7 @@ private io.vertx.ext.mail.MailConfig toVertxMailConfig(String name, MailerRuntim
if (config.username.isPresent()) {
cfg.setUsername(config.username.get());
}

if (config.password.isPresent()) {
cfg.setPassword(config.password.get());
}
Expand Down Expand Up @@ -255,6 +290,35 @@ private io.vertx.ext.mail.MailConfig toVertxMailConfig(String name, MailerRuntim
return cfg;
}

private <T> T awaitHttpResponse(Future<T> future) {
try {
return future.toCompletionStage().toCompletableFuture().get(30, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
}
}

public static Buffer urlEncodeOAuth2Params(Map<String, String> form) {
Buffer buffer = Buffer.buffer();
for (Map.Entry<String, String> entry : form.entrySet()) {
if (buffer.length() != 0) {
buffer.appendByte((byte) '&');
}
buffer.appendString(entry.getKey());
buffer.appendByte((byte) '=');
buffer.appendString(urlEncode(entry.getValue()));
}
return buffer;
}

private static String urlEncode(String value) {
try {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}

private void configureTLS(String name, MailerRuntimeConfig config, TlsConfigurationRegistry tlsRegistry, MailConfig cfg) {
TlsConfiguration configuration = null;
boolean defaultTrustAll = false;
Expand Down

0 comments on commit 91ad547

Please sign in to comment.