Skip to content

Commit fa28a69

Browse files
committed
Add option to skip logging invalid recipients when sending emails
This commits detects and prevents logging invalid recipients before delegating to the underlying SMTP client, avoiding the overhead of processing attachments for emails that won’t be sent as well as acquiring an unnecessary connection. When the option is enabled (default), the invalid recipients is not logged and not part of the thrown exception.
1 parent fe164e0 commit fa28a69

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)