diff --git a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java index 5073b2022a8e2..4fe49df2ccb2c 100644 --- a/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java +++ b/extensions/mailer/deployment/src/test/java/io/quarkus/mailer/InjectionTest.java @@ -1,10 +1,12 @@ package io.quarkus.mailer; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.concurrent.CompletionStage; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; import jakarta.inject.Inject; import jakarta.inject.Singleton; @@ -28,7 +30,7 @@ public class InjectionTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar .addClasses(BeanUsingBareMailClient.class, BeanUsingBlockingMailer.class, - BeanUsingReactiveMailer.class, MailTemplates.class) + BeanUsingReactiveMailer.class, MailTemplates.class, MailListener.class) .addAsResource("mock-config.properties", "application.properties") .addAsResource(new StringAsset("" + "{name}"), "templates/test1.html") @@ -56,15 +58,33 @@ public class InjectionTest { @Inject MailTemplates templates; + @Inject + MailListener listener; + @Test public void testInjection() { beanUsingMutiny.verify(); beanUsingBare.verify(); beanUsingBlockingMailer.verify(); + + await().until(() -> listener.getLast() != null); + listener.reset(); + beanUsingReactiveMailer.verify().toCompletableFuture().join(); - templates.send1(); - templates.send2().await(); - templates.sendNative().await(); + await().until(() -> listener.getLast() != null); + listener.reset(); + + templates.send1().await().indefinitely(); + await().until(() -> listener.getLast() != null); + listener.reset(); + + templates.send2().await().indefinitely(); + await().until(() -> listener.getLast() != null); + listener.reset(); + + templates.sendNative().await().indefinitely(); + await().until(() -> listener.getLast() != null); + listener.reset(); assertEquals("Me", MailTemplates.Templates.testNative("Me").templateInstance().render()); } @@ -139,4 +159,22 @@ Uni sendNative() { return Templates.testNative("John").to("quarkus@quarkus.io").subject("Test").send(); } } + + @ApplicationScoped + public static class MailListener { + + volatile SentMail last; + + public void onMailSent(@Observes SentMail mail) { + last = mail; + } + + public SentMail getLast() { + return last; + } + + public void reset() { + last = null; + } + } } diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/SentMail.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/SentMail.java new file mode 100644 index 0000000000000..c01c9d49097de --- /dev/null +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/SentMail.java @@ -0,0 +1,44 @@ +package io.quarkus.mailer; + +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Flow; + +/** + * Represents a sent mail. + * Instances of this class are sent using CDI events. + * + * @param from the sender address + * @param to the list of recipients + * @param cc the list of CC recipients + * @param bcc the list of BCC recipients + * @param replyTo the list of reply-to addresses + * @param bounceAddress the bounce address + * @param subject the subject + * @param textBody the text body + * @param htmlBody the HTML body + * @param headers the headers + * @param attachments the attachments + */ +public record SentMail(String from, + List to, List cc, List bcc, + String replyTo, String bounceAddress, + String subject, String textBody, String htmlBody, + Map> headers, List attachments) { + + /** + * An immutable representation of an attachment that has been sent. + * + * @param name the name + * @param file the file + * @param description the description + * @param disposition the disposition + * @param data the data + * @param contentType the content type + * @param contentId the content ID + */ + public record SentAttachment(String name, File file, String description, String disposition, + Flow.Publisher data, String contentType, String contentId) { + } +} diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/Mailers.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/Mailers.java index c9a6628b0083d..2151d33d0396b 100644 --- a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/Mailers.java +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/Mailers.java @@ -10,12 +10,14 @@ import java.util.stream.Collectors; import jakarta.annotation.PreDestroy; +import jakarta.enterprise.event.Event; import jakarta.inject.Singleton; import org.jboss.logging.Logger; import io.quarkus.mailer.Mailer; import io.quarkus.mailer.MockMailbox; +import io.quarkus.mailer.SentMail; import io.quarkus.mailer.reactive.ReactiveMailer; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigurationException; @@ -54,7 +56,8 @@ public class Mailers { private final Map mutinyMailers; public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRuntimeConfig mailersRuntimeConfig, - LaunchMode launchMode, MailerSupport mailerSupport, TlsConfigurationRegistry tlsRegistry) { + LaunchMode launchMode, MailerSupport mailerSupport, TlsConfigurationRegistry tlsRegistry, + Event sentMailEvent) { Map localClients = new HashMap<>(); Map localMutinyClients = new HashMap<>(); Map localMockMailboxes = new HashMap<>(); @@ -76,7 +79,7 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti mailersRuntimeConfig.defaultMailer.approvedRecipients.orElse(List.of()).stream() .filter(Objects::nonNull).collect(Collectors.toList()), mailersRuntimeConfig.defaultMailer.logRejectedRecipients, - mailersRuntimeConfig.defaultMailer.logInvalidRecipients)); + mailersRuntimeConfig.defaultMailer.logInvalidRecipients, sentMailEvent)); } for (String name : mailerSupport.namedMailers) { @@ -99,7 +102,8 @@ public Mailers(Vertx vertx, io.vertx.mutiny.core.Vertx mutinyVertx, MailersRunti namedMailerRuntimeConfig.approvedRecipients.orElse(List.of()).stream() .filter(p -> p != null).collect(Collectors.toList()), namedMailerRuntimeConfig.logRejectedRecipients, - namedMailerRuntimeConfig.logInvalidRecipients)); + namedMailerRuntimeConfig.logInvalidRecipients, + sentMailEvent)); } this.clients = Collections.unmodifiableMap(localClients); diff --git a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MutinyMailerImpl.java b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MutinyMailerImpl.java index d09082cec1358..1b86b189cfb3c 100644 --- a/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MutinyMailerImpl.java +++ b/extensions/mailer/runtime/src/main/java/io/quarkus/mailer/runtime/MutinyMailerImpl.java @@ -3,6 +3,7 @@ import static java.util.Arrays.stream; import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -13,10 +14,13 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import jakarta.enterprise.event.Event; + import org.jboss.logging.Logger; import io.quarkus.mailer.Attachment; import io.quarkus.mailer.Mail; +import io.quarkus.mailer.SentMail; import io.quarkus.mailer.reactive.ReactiveMailer; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; @@ -51,10 +55,11 @@ public class MutinyMailerImpl implements ReactiveMailer { private final boolean logRejectedRecipients; private final boolean logInvalidRecipients; + private final Event sentEmailEvent; MutinyMailerImpl(Vertx vertx, MailClient client, MockMailboxImpl mockMailbox, String from, String bounceAddress, boolean mock, List approvedRecipients, - boolean logRejectedRecipients, boolean logInvalidRecipients) { + boolean logRejectedRecipients, boolean logInvalidRecipients, Event sentEmailEvent) { this.vertx = vertx; this.client = client; this.mockMailbox = mockMailbox; @@ -64,6 +69,7 @@ public class MutinyMailerImpl implements ReactiveMailer { this.approvedRecipients = approvedRecipients; this.logRejectedRecipients = logRejectedRecipients; this.logInvalidRecipients = logInvalidRecipients; + this.sentEmailEvent = sentEmailEvent; } @Override @@ -128,13 +134,41 @@ private Uni send(Mail mail, MailMessage message) { message.getCc(), message.getBcc(), message.getText() == null ? "" : message.getText(), message.getHtml() == null ? "" : message.getHtml()); - return mockMailbox.send(mail, message); + return mockMailbox.send(mail, message) + .invoke(() -> fire(mail, message)); } else { return client.sendMail(message) + .invoke(() -> fire(mail, message)) .replaceWithVoid(); } } + private Map> copy(MultiMap headers) { + return headers.entries().stream() + .collect( + Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList()))); + } + + private List copy(List attachments) { + return attachments.stream() + .map(attachment -> new SentMail.SentAttachment(attachment.getName(), attachment.getFile(), + attachment.getDescription(), attachment.getDisposition(), attachment.getData(), + attachment.getContentType(), attachment.getContentId())) + .collect(Collectors.toList()); + } + + private void fire(Mail mail, MailMessage message) { + if (sentEmailEvent != null) { + SentMail sentMail = new SentMail(message.getFrom(), + Collections.unmodifiableList(message.getTo()), Collections.unmodifiableList(message.getCc()), + Collections.unmodifiableList(message.getBcc()), + mail.getReplyTo(), message.getBounceAddress(), + message.getSubject(), message.getText(), message.getHtml(), + copy(message.getHeaders()), copy(mail.getAttachments())); + sentEmailEvent.fire(sentMail); + } + } + private Uni toMailMessage(Mail mail) { MailMessage message = new MailMessage(); diff --git a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/FakeSmtpTestBase.java b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/FakeSmtpTestBase.java index dc661c79e813a..fc8f0011b2a2a 100644 --- a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/FakeSmtpTestBase.java +++ b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/FakeSmtpTestBase.java @@ -79,7 +79,7 @@ public boolean isTrustAll() { public void register(String name, TlsConfiguration configuration) { throw new UnsupportedOperationException(); } - }); + }, null); return mailers.reactiveMailerFromName(Mailers.DEFAULT_MAILER_NAME); } @@ -108,7 +108,7 @@ public Optional getDefault() { public void register(String name, TlsConfiguration configuration) { throw new UnsupportedOperationException(); } - }); + }, null); return mailers.reactiveMailerFromName(Mailers.DEFAULT_MAILER_NAME); } diff --git a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java index a312f705a026e..99c8957464f17 100644 --- a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java +++ b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerImplTest.java @@ -59,7 +59,7 @@ void init() { mailer = new MutinyMailerImpl(vertx, MailClient.createShared(vertx, new MailConfig().setPort(wiser.getServer().getPort())), - null, FROM, null, false, List.of(), false, false); + null, FROM, null, false, List.of(), false, false, null); wiser.getMessages().clear(); } @@ -271,7 +271,7 @@ private String getInlineAttachment(String cid, MimeMultipart multipart) throws I private String read(BodyPart part) throws IOException, MessagingException { try (InputStream is = part.getInputStream()) { - Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); + Scanner s = new Scanner(is, StandardCharsets.UTF_8).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } } diff --git a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerWithMultipartImplTest.java b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerWithMultipartImplTest.java index 655a34c20cc24..3854ee0a9b631 100644 --- a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerWithMultipartImplTest.java +++ b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MailerWithMultipartImplTest.java @@ -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, List.of(), false, false); + FROM, null, false, List.of(), false, false, null); wiser.getMessages().clear(); } @@ -261,7 +261,7 @@ private String getInlineAttachment(String cid, MimeMultipart multipart) throws I private String read(BodyPart part) throws IOException, MessagingException { try (InputStream is = part.getInputStream()) { - Scanner s = new Scanner(is, "UTF-8").useDelimiter("\\A"); + Scanner s = new Scanner(is, StandardCharsets.UTF_8).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } } diff --git a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java index ac0ae1a3c714c..590747223a286 100644 --- a/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java +++ b/extensions/mailer/runtime/src/test/java/io/quarkus/mailer/runtime/MockMailerImplTest.java @@ -35,7 +35,7 @@ static void stop() { @BeforeEach void init() { mockMailbox = new MockMailboxImpl(); - mailer = new MutinyMailerImpl(vertx, null, mockMailbox, FROM, null, true, List.of(), false, false); + mailer = new MutinyMailerImpl(vertx, null, mockMailbox, FROM, null, true, List.of(), false, false, null); } @Test