Skip to content

Commit 744bc75

Browse files
authored
Merge pull request #43955 from cescoffier/mailer-no-log-invalid-recipient
Add option to skip logging invalid recipients when sending emails
2 parents 6e8ab9a + fa28a69 commit 744bc75

File tree

10 files changed

+236
-8
lines changed

10 files changed

+236
-8
lines changed

extensions/mailer/deployment/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
<artifactId>quarkus-junit5-internal</artifactId>
4242
<scope>test</scope>
4343
</dependency>
44+
<dependency>
45+
<groupId>org.assertj</groupId>
46+
<artifactId>assertj-core</artifactId>
47+
<scope>test</scope>
48+
</dependency>
4449

4550
</dependencies>
4651

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package io.quarkus.mailer;
2+
3+
import java.time.Duration;
4+
import java.util.List;
5+
6+
import jakarta.inject.Inject;
7+
import jakarta.inject.Singleton;
8+
9+
import org.assertj.core.api.Assertions;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.mailer.reactive.ReactiveMailer;
14+
import io.quarkus.test.QuarkusUnitTest;
15+
import io.smallrye.mutiny.Uni;
16+
17+
public class InvalidEmailTest {
18+
19+
@RegisterExtension
20+
static final QuarkusUnitTest config = new QuarkusUnitTest()
21+
.withApplicationRoot(root -> root
22+
.addClasses(Sender.class));
23+
24+
@Inject
25+
MockMailbox mockMailbox;
26+
27+
@Inject
28+
Sender sender;
29+
30+
@Test
31+
public void testInvalidTo() {
32+
List<String> to = List.of("[email protected]", "inv [email protected]", "[email protected]");
33+
List<String> cc = List.of();
34+
List<String> bcc = List.of();
35+
Assertions.assertThatThrownBy(() -> sender.send(to, cc, bcc).await().atMost(Duration.ofSeconds(5)))
36+
.isInstanceOf(IllegalArgumentException.class)
37+
.hasMessageContaining("Unable to send an email, an email address is invalid")
38+
.hasMessageNotContaining("@");
39+
Assertions.assertThat(mockMailbox.getTotalMessagesSent()).isEqualTo(0);
40+
}
41+
42+
@Test
43+
public void testInvalidCC() {
44+
List<String> cc = List.of("[email protected]", "inv [email protected]", "[email protected]");
45+
List<String> to = List.of();
46+
List<String> bcc = List.of();
47+
Assertions.assertThatThrownBy(() -> sender.send(to, cc, bcc).await().atMost(Duration.ofSeconds(5)))
48+
.isInstanceOf(IllegalArgumentException.class)
49+
.hasMessageContaining("Unable to send an email, an email address is invalid")
50+
.hasMessageNotContaining("@");
51+
Assertions.assertThat(mockMailbox.getTotalMessagesSent()).isEqualTo(0);
52+
}
53+
54+
@Test
55+
public void testInvalidBCC() {
56+
List<String> bcc = List.of("[email protected]", "inv [email protected]", "[email protected]");
57+
List<String> to = List.of();
58+
List<String> cc = List.of();
59+
Assertions.assertThatThrownBy(() -> sender.send(to, cc, bcc).await().atMost(Duration.ofSeconds(5)))
60+
.isInstanceOf(IllegalArgumentException.class)
61+
.hasMessageContaining("Unable to send an email, an email address is invalid")
62+
.hasMessageNotContaining("@");
63+
Assertions.assertThat(mockMailbox.getTotalMessagesSent()).isEqualTo(0);
64+
}
65+
66+
@Singleton
67+
static class Sender {
68+
69+
@Inject
70+
ReactiveMailer mailer;
71+
72+
Uni<Void> send(List<String> to, List<String> cc, List<String> bcc) {
73+
Mail mail = new Mail()
74+
.setTo(to)
75+
.setCc(cc)
76+
.setBcc(bcc)
77+
.setSubject("Test")
78+
.setText("Hello!");
79+
return mailer.send(mail);
80+
}
81+
}
82+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.quarkus.mailer;
2+
3+
import java.time.Duration;
4+
import java.util.List;
5+
6+
import jakarta.inject.Inject;
7+
import jakarta.inject.Singleton;
8+
9+
import org.assertj.core.api.Assertions;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.mailer.reactive.ReactiveMailer;
14+
import io.quarkus.test.QuarkusUnitTest;
15+
import io.smallrye.mutiny.Uni;
16+
17+
public class LoggedInvalidEmailTest {
18+
19+
@RegisterExtension
20+
static final QuarkusUnitTest config = new QuarkusUnitTest()
21+
.withApplicationRoot(root -> root
22+
.addClasses(Sender.class))
23+
.overrideRuntimeConfigKey("quarkus.mailer.log-invalid-recipients", "true");
24+
25+
@Inject
26+
MockMailbox mockMailbox;
27+
28+
@Inject
29+
Sender sender;
30+
31+
@Test
32+
public void testInvalidTo() {
33+
List<String> to = List.of("[email protected]", "inv [email protected]", "[email protected]");
34+
List<String> cc = List.of();
35+
List<String> bcc = List.of();
36+
Assertions.assertThatThrownBy(() -> sender.send(to, cc, bcc).await().atMost(Duration.ofSeconds(5)))
37+
.isInstanceOf(IllegalArgumentException.class)
38+
.hasMessageContaining("Unable to send an email")
39+
.hasStackTraceContaining("inv [email protected]")
40+
.hasMessageNotContaining("@text.io");
41+
Assertions.assertThat(mockMailbox.getTotalMessagesSent()).isEqualTo(0);
42+
}
43+
44+
@Test
45+
public void testInvalidCC() {
46+
List<String> cc = List.of("[email protected]", "inv [email protected]", "[email protected]");
47+
List<String> to = List.of();
48+
List<String> bcc = List.of();
49+
Assertions.assertThatThrownBy(() -> sender.send(to, cc, bcc).await().atMost(Duration.ofSeconds(5)))
50+
.isInstanceOf(IllegalArgumentException.class)
51+
.hasMessageContaining("Unable to send an email")
52+
.hasStackTraceContaining("inv [email protected]")
53+
.hasMessageNotContaining("@text.io");
54+
Assertions.assertThat(mockMailbox.getTotalMessagesSent()).isEqualTo(0);
55+
}
56+
57+
@Test
58+
public void testInvalidBCC() {
59+
List<String> bcc = List.of("[email protected]", "inv [email protected]", "[email protected]");
60+
List<String> to = List.of();
61+
List<String> cc = List.of();
62+
Assertions.assertThatThrownBy(() -> sender.send(to, cc, bcc).await().atMost(Duration.ofSeconds(5)))
63+
.isInstanceOf(IllegalArgumentException.class)
64+
.hasMessageContaining("Unable to send an email")
65+
.hasStackTraceContaining("inv [email protected]")
66+
.hasMessageNotContaining("@text.io");
67+
Assertions.assertThat(mockMailbox.getTotalMessagesSent()).isEqualTo(0);
68+
}
69+
70+
@Singleton
71+
static class Sender {
72+
73+
@Inject
74+
ReactiveMailer mailer;
75+
76+
Uni<Void> send(List<String> to, List<String> cc, List<String> bcc) {
77+
Mail mail = new Mail()
78+
.setTo(to)
79+
.setCc(cc)
80+
.setBcc(bcc)
81+
.setSubject("Test")
82+
.setText("Hello!");
83+
return mailer.send(mail);
84+
}
85+
}
86+
}

extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MailerRuntimeConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,4 +272,13 @@ public class MailerRuntimeConfig {
272272
*/
273273
@ConfigItem(defaultValue = "false")
274274
public boolean logRejectedRecipients = false;
275+
276+
/**
277+
* Log invalid recipients as warnings.
278+
* <p>
279+
* If false, the invalid recipients will not be logged and the thrown exception will not contain the invalid email address.
280+
*
281+
*/
282+
@ConfigItem(defaultValue = "false")
283+
public boolean logInvalidRecipients = false;
275284
}

extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/Mailers.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti
7575
mailersRuntimeConfig.defaultMailer.mock.orElse(launchMode.isDevOrTest()),
7676
mailersRuntimeConfig.defaultMailer.approvedRecipients.orElse(List.of()).stream()
7777
.filter(Objects::nonNull).collect(Collectors.toList()),
78-
mailersRuntimeConfig.defaultMailer.logRejectedRecipients));
78+
mailersRuntimeConfig.defaultMailer.logRejectedRecipients,
79+
mailersRuntimeConfig.defaultMailer.logInvalidRecipients));
7980
}
8081

8182
for (String name : mailerSupport.namedMailers) {
@@ -97,7 +98,8 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti
9798
namedMailerRuntimeConfig.mock.orElse(false),
9899
namedMailerRuntimeConfig.approvedRecipients.orElse(List.of()).stream()
99100
.filter(p -> p != null).collect(Collectors.toList()),
100-
namedMailerRuntimeConfig.logRejectedRecipients));
101+
namedMailerRuntimeConfig.logRejectedRecipients,
102+
namedMailerRuntimeConfig.logInvalidRecipients));
101103
}
102104

103105
this.clients = Collections.unmodifiableMap(localClients);

extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MockMailboxImpl.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import io.quarkus.mailer.MockMailbox;
1010
import io.smallrye.mutiny.Uni;
1111
import io.vertx.ext.mail.MailMessage;
12+
import io.vertx.ext.mail.mailencoder.EmailAddress;
1213

1314
/**
1415
* Mock mailbox bean, will be populated if mocking emails.
@@ -22,22 +23,30 @@ public class MockMailboxImpl implements MockMailbox {
2223
Uni<Void> send(Mail email, MailMessage mailMessage) {
2324
if (email.getTo() != null) {
2425
for (String to : email.getTo()) {
26+
validateEmailAddress(to);
2527
send(email, mailMessage, to);
2628
}
2729
}
2830
if (email.getCc() != null) {
2931
for (String to : email.getCc()) {
32+
validateEmailAddress(to);
3033
send(email, mailMessage, to);
3134
}
3235
}
3336
if (email.getBcc() != null) {
3437
for (String to : email.getBcc()) {
38+
validateEmailAddress(to);
3539
send(email, mailMessage, to);
3640
}
3741
}
3842
return Uni.createFrom().item(() -> null);
3943
}
4044

45+
private void validateEmailAddress(String to) {
46+
// Just here to validate the email address.
47+
new EmailAddress(to);
48+
}
49+
4150
private void send(Mail sentMail, MailMessage sentMailMessage, String to) {
4251
sentMails.computeIfAbsent(to, k -> new ArrayList<>()).add(sentMail);
4352
sentMailMessages.computeIfAbsent(to, k -> new ArrayList<>()).add(sentMailMessage);

extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MutinyMailerImpl.java

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.vertx.core.file.OpenOptions;
2626
import io.vertx.ext.mail.MailAttachment;
2727
import io.vertx.ext.mail.MailMessage;
28+
import io.vertx.ext.mail.mailencoder.EmailAddress;
2829
import io.vertx.mutiny.core.Vertx;
2930
import io.vertx.mutiny.core.file.AsyncFile;
3031
import io.vertx.mutiny.ext.mail.MailClient;
@@ -47,11 +48,13 @@ public class MutinyMailerImpl implements ReactiveMailer {
4748

4849
private final List<Pattern> approvedRecipients;
4950

50-
private boolean logRejectedRecipients;
51+
private final boolean logRejectedRecipients;
52+
53+
private final boolean logInvalidRecipients;
5154

5255
MutinyMailerImpl(Vertx vertx, MailClient client, MockMailboxImpl mockMailbox,
5356
String from, String bounceAddress, boolean mock, List<Pattern> approvedRecipients,
54-
boolean logRejectedRecipients) {
57+
boolean logRejectedRecipients, boolean logInvalidRecipients) {
5558
this.vertx = vertx;
5659
this.client = client;
5760
this.mockMailbox = mockMailbox;
@@ -60,6 +63,7 @@ public class MutinyMailerImpl implements ReactiveMailer {
6063
this.mock = mock;
6164
this.approvedRecipients = approvedRecipients;
6265
this.logRejectedRecipients = logRejectedRecipients;
66+
this.logInvalidRecipients = logInvalidRecipients;
6367
}
6468

6569
@Override
@@ -149,6 +153,11 @@ private Uni<MailMessage> toMailMessage(Mail mail) {
149153
message.setTo(mail.getTo());
150154
message.setCc(mail.getCc());
151155
message.setBcc(mail.getBcc());
156+
157+
// Validate that the email addresses are valid
158+
// We do that early to avoid having to read attachments if an email is invalid
159+
validate(mail.getTo(), mail.getCc(), mail.getBcc());
160+
152161
message.setSubject(mail.getSubject());
153162
message.setText(mail.getText());
154163
message.setHtml(mail.getHtml());
@@ -177,13 +186,39 @@ private Uni<MailMessage> toMailMessage(Mail mail) {
177186
return Uni.createFrom().item(message);
178187
}
179188

180-
return Uni.combine().all().unis(stages).combinedWith(res -> {
189+
return Uni.combine().all().unis(stages).with(res -> {
181190
message.setAttachment(attachments);
182191
message.setInlineAttachment(inline);
183192
return message;
184193
});
185194
}
186195

196+
private void validate(List<String> to, List<String> cc, List<String> bcc) {
197+
try {
198+
for (String email : to) {
199+
new EmailAddress(email);
200+
}
201+
for (String email : cc) {
202+
new EmailAddress(email);
203+
}
204+
for (String email : bcc) {
205+
new EmailAddress(email);
206+
}
207+
} catch (IllegalArgumentException e) {
208+
// One of the email addresses is invalid
209+
if (logInvalidRecipients) {
210+
// We are allowed to log the invalid email address
211+
// The exception message contains the invalid email address.
212+
LOGGER.warn("Unable to send an email", e);
213+
throw new IllegalArgumentException("Unable to send an email", e);
214+
} else {
215+
// Do not print the invalid email address.
216+
LOGGER.warn("Unable to send an email, an email address is invalid");
217+
throw new IllegalArgumentException("Unable to send an email, an email address is invalid");
218+
}
219+
}
220+
}
221+
187222
private MultiMap toMultimap(Map<String, List<String>> headers) {
188223
MultiMap mm = MultiMap.caseInsensitiveMultiMap();
189224
headers.forEach(mm::add);

extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ void init() {
5959
mailer = new MutinyMailerImpl(vertx,
6060
MailClient.createShared(vertx,
6161
new MailConfig().setPort(wiser.getServer().getPort())),
62-
null, FROM, null, false, List.of(), false);
62+
null, FROM, null, false, List.of(), false, false);
6363

6464
wiser.getMessages().clear();
6565
}

extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerWithMultipartImplTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ static void stopWiser() {
6262
void init() {
6363
mailer = new MutinyMailerImpl(vertx, MailClient.createShared(vertx,
6464
new MailConfig().setPort(wiser.getServer().getPort()).setMultiPartOnly(true)), null,
65-
FROM, null, false, List.of(), false);
65+
FROM, null, false, List.of(), false, false);
6666

6767
wiser.getMessages().clear();
6868
}

extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ static void stop() {
3535
@BeforeEach
3636
void init() {
3737
mockMailbox = new MockMailboxImpl();
38-
mailer = new MutinyMailerImpl(vertx, null, mockMailbox, FROM, null, true, List.of(), false);
38+
mailer = new MutinyMailerImpl(vertx, null, mockMailbox, FROM, null, true, List.of(), false, false);
3939
}
4040

4141
@Test

0 commit comments

Comments
 (0)