Skip to content

Commit

Permalink
Merge pull request #33278 from gsmet/mailer-approve-list
Browse files Browse the repository at this point in the history
Mailer - Add the ability to configure a list of approved recipients
  • Loading branch information
gsmet authored May 15, 2023
2 parents 52060e6 + 8f43b43 commit 910b1cb
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.quarkus.mailer;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.logging.Level;

import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class ApproveListNoEmailTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset(
"quarkus.mailer.approved-recipients=.*@approved1.com\nquarkus.mailer.log-rejected-recipients=true"),
"application.properties"))
.setLogRecordPredicate(record -> record.getLevel().equals(Level.WARNING))
.assertLogRecords(lrs -> {
assertTrue(lrs.stream().anyMatch(lr -> lr.getMessage().equals(
"Email 'A subject' was not sent because all recipients were rejected by the configuration: [[email protected], [email protected], [email protected], [email protected], [email protected]]")));
});

@Inject
Mailer mailer;

@Inject
MockMailbox mockMailbox;

@Test
public void testApproveList() {
mailer.send(Mail.withText("[email protected]", "A subject", "")
.addCc("[email protected]", "[email protected]")
.addBcc("[email protected]", "[email protected]"));

assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.mailer;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.logging.Level;

import jakarta.inject.Inject;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class ApproveListTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(new StringAsset(
"quarkus.mailer.approved-recipients=.*@approved1.com,.*@approved2.com,.*@approved3.com\nquarkus.mailer.log-rejected-recipients=true"),
"application.properties"))
.setLogRecordPredicate(record -> record.getLevel().equals(Level.WARNING))
.assertLogRecords(lrs -> {
assertTrue(lrs.stream().anyMatch(lr -> lr.getMessage().equals(
"Email 'A subject' was not sent to the following recipients as they were rejected by the configuration: [[email protected], [email protected], [email protected], [email protected], [email protected]]")));
});

@Inject
Mailer mailer;

@Inject
MockMailbox mockMailbox;

@Test
public void testApproveList() {
mailer.send(Mail.withText("[email protected]", "A subject", "")
.addTo("[email protected]")
.addCc("[email protected]", "[email protected]", "[email protected]")
.addBcc("[email protected]", "[email protected]", "[email protected]"));

assertEquals(1, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(1, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(1, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
assertEquals(0, mockMailbox.getMailMessagesSentTo("[email protected]").size());
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package io.quarkus.mailer.runtime;

import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.regex.Pattern;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConvertWith;

@ConfigGroup
public class MailerRuntimeConfig {
Expand Down Expand Up @@ -210,4 +213,26 @@ public class MailerRuntimeConfig {
@ConfigItem
public NtlmConfig ntlm = new NtlmConfig();

/**
* Allows sending emails to these recipients only.
* <p>
* Approved recipients are compiled to a {@code Pattern} and must be a valid regular expression.
* The created {@code Pattern} is case-insensitive as emails are case insensitive.
* Provided patterns are trimmed before being compiled.
*
* @see {@link #logRejectedRecipients}
*/
@ConfigItem
@ConvertWith(TrimmedPatternConverter.class)
public Optional<List<Pattern>> approvedRecipients = Optional.empty();

/**
* Log rejected recipients as warnings.
* <p>
* If false, the rejected recipients will be logged at the DEBUG level.
*
* @see {@link #approvedRecipients}
*/
@ConfigItem(defaultValue = "false")
public boolean logRejectedRecipients = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import jakarta.annotation.PreDestroy;
import jakarta.inject.Singleton;
Expand Down Expand Up @@ -65,7 +66,10 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti
new MutinyMailerImpl(mutinyVertx, mutinyMailClient, mockMailbox,
mailersRuntimeConfig.defaultMailer.from.orElse(null),
mailersRuntimeConfig.defaultMailer.bounceAddress.orElse(null),
mailersRuntimeConfig.defaultMailer.mock.orElse(launchMode.isDevOrTest())));
mailersRuntimeConfig.defaultMailer.mock.orElse(launchMode.isDevOrTest()),
mailersRuntimeConfig.defaultMailer.approvedRecipients.orElse(List.of()).stream()
.filter(p -> p != null).collect(Collectors.toList()),
mailersRuntimeConfig.defaultMailer.logRejectedRecipients));
}

for (String name : mailerSupport.namedMailers) {
Expand All @@ -83,7 +87,10 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti
new MutinyMailerImpl(mutinyVertx, namedMutinyMailClient, namedMockMailbox,
namedMailerRuntimeConfig.from.orElse(null),
namedMailerRuntimeConfig.bounceAddress.orElse(null),
namedMailerRuntimeConfig.mock.orElse(false)));
namedMailerRuntimeConfig.mock.orElse(false),
namedMailerRuntimeConfig.approvedRecipients.orElse(List.of()).stream()
.filter(p -> p != null).collect(Collectors.toList()),
namedMailerRuntimeConfig.logRejectedRecipients));
}

this.clients = Collections.unmodifiableMap(localClients);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import static java.util.Arrays.stream;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Flow.Publisher;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.jboss.logging.Logger;
Expand Down Expand Up @@ -40,16 +43,23 @@ public class MutinyMailerImpl implements ReactiveMailer {

private final String bounceAddress;

private boolean mock;
private final boolean mock;

private final List<Pattern> approvedRecipients;

private boolean logRejectedRecipients;

MutinyMailerImpl(Vertx vertx, MailClient client, MockMailboxImpl mockMailbox,
String from, String bounceAddress, boolean mock) {
String from, String bounceAddress, boolean mock, List<Pattern> approvedRecipients,
boolean logRejectedRecipients) {
this.vertx = vertx;
this.client = client;
this.mockMailbox = mockMailbox;
this.from = from;
this.bounceAddress = bounceAddress;
this.mock = mock;
this.approvedRecipients = approvedRecipients;
this.logRejectedRecipients = logRejectedRecipients;
}

@Override
Expand Down Expand Up @@ -77,9 +87,41 @@ public Uni<? extends Void> apply(MailMessage mailMessage) {
}

private Uni<Void> send(Mail mail, MailMessage message) {
if (!approvedRecipients.isEmpty()) {
Recipients to = filterApprovedRecipients(message.getTo());
Recipients cc = filterApprovedRecipients(message.getCc());
Recipients bcc = filterApprovedRecipients(message.getBcc());

if (to.approved.isEmpty() && cc.approved.isEmpty() && bcc.approved.isEmpty()) {
logRejectedRecipients("Email '%s' was not sent because all recipients were rejected by the configuration: %s",
message.getSubject(), to.rejected, cc.rejected, bcc.rejected);
return Uni.createFrom().voidItem();
}

if (!to.rejected.isEmpty() || !cc.rejected.isEmpty() || !bcc.rejected.isEmpty()) {
logRejectedRecipients(
"Email '%s' was not sent to the following recipients as they were rejected by the configuration: %s",
message.getSubject(), to.rejected, cc.rejected, bcc.rejected);
}

if (!to.rejected.isEmpty()) {
mail.setTo(to.approved);
message.setTo(to.approved);
}
if (!cc.rejected.isEmpty()) {
mail.setCc(cc.approved);
message.setCc(cc.approved);
}
if (!bcc.rejected.isEmpty()) {
mail.setBcc(bcc.approved);
message.setBcc(bcc.approved);
}
}

if (mock) {
LOGGER.infof("Sending email %s from %s to %s, text body: \n%s\nhtml body: \n%s",
LOGGER.infof("Sending email %s from %s to %s (cc: %s, bcc: %s), text body: \n%s\nhtml body: \n%s",
message.getSubject(), message.getFrom(), message.getTo(),
message.getCc(), message.getBcc(),
message.getText() == null ? "<empty>" : message.getText(),
message.getHtml() == null ? "<empty>" : message.getHtml());
return mockMailbox.send(mail, message);
Expand All @@ -103,6 +145,7 @@ private Uni<MailMessage> toMailMessage(Mail mail) {
} else {
message.setFrom(from);
}

message.setTo(mail.getTo());
message.setCc(mail.getCc());
message.setBcc(mail.getBcc());
Expand Down Expand Up @@ -166,6 +209,47 @@ private Uni<MailAttachment> toMailAttachment(Attachment attachment) {
.onItem().transform(attach::setData);
}

private Recipients filterApprovedRecipients(List<String> emails) {
if (approvedRecipients.isEmpty()) {
return new Recipients(emails, List.of());
}

List<String> allowedRecipients = new ArrayList<>();
List<String> rejectedRecipients = new ArrayList<>();

emailLoop: for (String email : emails) {
for (Pattern approvedRecipient : approvedRecipients) {
if (approvedRecipient.matcher(email).matches()) {
allowedRecipients.add(email);
continue emailLoop;
}
}

rejectedRecipients.add(email);
}

return new Recipients(allowedRecipients, rejectedRecipients);
}

@SafeVarargs
private void logRejectedRecipients(String logMessage, String subject, List<String>... rejectedRecipientLists) {
if (logRejectedRecipients) {
Set<String> allRejectedRecipients = new LinkedHashSet<>();
for (List<String> rejectedRecipients : rejectedRecipientLists) {
allRejectedRecipients.addAll(rejectedRecipients);
}

LOGGER.warn(String.format(logMessage, subject, allRejectedRecipients));
} else if (LOGGER.isDebugEnabled()) {
List<String> allRejectedRecipients = new ArrayList<>();
for (List<String> rejectedRecipients : rejectedRecipientLists) {
allRejectedRecipients.addAll(rejectedRecipients);
}

LOGGER.warn(String.format(logMessage, subject, allRejectedRecipients));
}
}

public static Uni<Buffer> getAttachmentStream(Vertx vertx, Attachment attachment) {
if (attachment.getFile() != null) {
Uni<AsyncFile> open = vertx.fileSystem().open(attachment.getFile().getAbsolutePath(),
Expand All @@ -183,4 +267,15 @@ public static Uni<Buffer> getAttachmentStream(Vertx vertx, Attachment attachment
return Uni.createFrom().failure(new IllegalArgumentException("Attachment has no data"));
}
}

private static class Recipients {

private final List<String> approved;
private final List<String> rejected;

Recipients(List<String> approved, List<String> rejected) {
this.approved = approved;
this.rejected = rejected;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.mailer.runtime;

import java.util.regex.Pattern;

import org.eclipse.microprofile.config.spi.Converter;

public class TrimmedPatternConverter implements Converter<Pattern> {

public TrimmedPatternConverter() {
}

@Override
public Pattern convert(String s) {
if (s == null) {
return null;
}

String trimmedString = s.trim().toLowerCase();

if (trimmedString.isEmpty()) {
return null;
}

return Pattern.compile(trimmedString, Pattern.CASE_INSENSITIVE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.quarkus.mailer.runtime.TrimmedPatternConverter
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ void init() {
mailer = new MutinyMailerImpl(vertx,
MailClient.createShared(vertx,
new MailConfig().setPort(wiser.getServer().getPort())),
null, FROM, null, false);
null, FROM, null, false, List.of(), false);

wiser.getMessages().clear();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ static void stopWiser() {
void init() {
mailer = new MutinyMailerImpl(vertx, MailClient.createShared(vertx,
new MailConfig().setPort(wiser.getServer().getPort()).setMultiPartOnly(true)), null,
FROM, null, false);
FROM, null, false, List.of(), false);

wiser.getMessages().clear();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ static void stop() {
@BeforeEach
void init() {
mockMailbox = new MockMailboxImpl();
mailer = new MutinyMailerImpl(vertx, null, mockMailbox, FROM, null, true);
mailer = new MutinyMailerImpl(vertx, null, mockMailbox, FROM, null, true, List.of(), false);
}

@Test
Expand Down

0 comments on commit 910b1cb

Please sign in to comment.