io.quarkus.mailer.runtime.MutinyMailerImpl Maven / Gradle / Ivy
The newest version!
package io.quarkus.mailer.runtime;
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;
import io.quarkus.mailer.Attachment;
import io.quarkus.mailer.Mail;
import io.quarkus.mailer.reactive.ReactiveMailer;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.file.OpenOptions;
import io.vertx.ext.mail.MailAttachment;
import io.vertx.ext.mail.MailMessage;
import io.vertx.ext.mail.mailencoder.EmailAddress;
import io.vertx.mutiny.core.Vertx;
import io.vertx.mutiny.core.file.AsyncFile;
import io.vertx.mutiny.ext.mail.MailClient;
public class MutinyMailerImpl implements ReactiveMailer {
private static final Logger LOGGER = Logger.getLogger("quarkus-mailer");
private final Vertx vertx;
private final MailClient client;
private final MockMailboxImpl mockMailbox;
private final String from;
private final String bounceAddress;
private final boolean mock;
private final List approvedRecipients;
private final boolean logRejectedRecipients;
private final boolean logInvalidRecipients;
MutinyMailerImpl(Vertx vertx, MailClient client, MockMailboxImpl mockMailbox,
String from, String bounceAddress, boolean mock, List approvedRecipients,
boolean logRejectedRecipients, boolean logInvalidRecipients) {
this.vertx = vertx;
this.client = client;
this.mockMailbox = mockMailbox;
this.from = from;
this.bounceAddress = bounceAddress;
this.mock = mock;
this.approvedRecipients = approvedRecipients;
this.logRejectedRecipients = logRejectedRecipients;
this.logInvalidRecipients = logInvalidRecipients;
}
@Override
public Uni send(Mail... mails) {
if (mails == null) {
throw new IllegalArgumentException("The `mails` parameter must not be `null`");
}
List> unis = stream(mails)
.map(new Function>() {
@Override
public Uni apply(Mail mail) {
return MutinyMailerImpl.this.toMailMessage(mail)
.chain(new Function>() {
@Override
public Uni extends Void> apply(MailMessage mailMessage) {
return send(mail, mailMessage);
}
});
}
})
.collect(Collectors.toList());
return Uni.combine().all().unis(unis).discardItems();
}
private Uni 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 (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 ? "" : message.getText(),
message.getHtml() == null ? "" : message.getHtml());
return mockMailbox.send(mail, message);
} else {
return client.sendMail(message)
.replaceWithVoid();
}
}
private Uni toMailMessage(Mail mail) {
MailMessage message = new MailMessage();
if (mail.getBounceAddress() != null) {
message.setBounceAddress(mail.getBounceAddress());
} else {
message.setBounceAddress(bounceAddress);
}
if (mail.getFrom() != null) {
message.setFrom(mail.getFrom());
} else {
message.setFrom(from);
}
message.setTo(mail.getTo());
message.setCc(mail.getCc());
message.setBcc(mail.getBcc());
// Validate that the email addresses are valid
// We do that early to avoid having to read attachments if an email is invalid
validate(mail.getTo(), mail.getCc(), mail.getBcc());
message.setSubject(mail.getSubject());
message.setText(mail.getText());
message.setHtml(mail.getHtml());
message.setHeaders(toMultimap(mail.getHeaders()));
if (mail.getReplyTo() != null) {
// getReplyTo produces the comma-separated list.
message.addHeader("Reply-To", mail.getReplyTo());
}
List> stages = new ArrayList<>();
List attachments = new CopyOnWriteArrayList<>();
List inline = new CopyOnWriteArrayList<>();
for (Attachment attachment : mail.getAttachments()) {
if (attachment.isInlineAttachment()) {
stages.add(
toMailAttachment(attachment).onItem().invoke(inline::add));
} else {
stages.add(
toMailAttachment(attachment).onItem().invoke(attachments::add));
}
}
if (stages.isEmpty()) {
message.setAttachment(attachments);
message.setInlineAttachment(inline);
return Uni.createFrom().item(message);
}
return Uni.combine().all().unis(stages).with(res -> {
message.setAttachment(attachments);
message.setInlineAttachment(inline);
return message;
});
}
private void validate(List to, List cc, List bcc) {
try {
for (String email : to) {
new EmailAddress(email);
}
for (String email : cc) {
new EmailAddress(email);
}
for (String email : bcc) {
new EmailAddress(email);
}
} catch (IllegalArgumentException e) {
// One of the email addresses is invalid
if (logInvalidRecipients) {
// We are allowed to log the invalid email address
// The exception message contains the invalid email address.
LOGGER.warn("Unable to send an email", e);
throw new IllegalArgumentException("Unable to send an email", e);
} else {
// Do not print the invalid email address.
LOGGER.warn("Unable to send an email, an email address is invalid");
throw new IllegalArgumentException("Unable to send an email, an email address is invalid");
}
}
}
private MultiMap toMultimap(Map> headers) {
MultiMap mm = MultiMap.caseInsensitiveMultiMap();
headers.forEach(mm::add);
return mm;
}
private Uni toMailAttachment(Attachment attachment) {
MailAttachment attach = MailAttachment.create();
attach.setName(attachment.getName());
attach.setContentId(attachment.getContentId());
attach.setDescription(attachment.getDescription());
attach.setDisposition(attachment.getDisposition());
attach.setContentType(attachment.getContentType());
if ((attachment.getFile() == null && attachment.getData() == null) // No content
|| (attachment.getFile() != null && attachment.getData() != null)) // Too much content
{
throw new IllegalArgumentException("An attachment must contain either a file or a raw data");
}
return getAttachmentStream(vertx, attachment)
.onItem().transform(attach::setData);
}
private Recipients filterApprovedRecipients(List emails) {
if (approvedRecipients.isEmpty()) {
return new Recipients(emails, List.of());
}
List allowedRecipients = new ArrayList<>();
List 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... rejectedRecipientLists) {
if (logRejectedRecipients) {
Set allRejectedRecipients = new LinkedHashSet<>();
for (List rejectedRecipients : rejectedRecipientLists) {
allRejectedRecipients.addAll(rejectedRecipients);
}
LOGGER.warn(String.format(logMessage, subject, allRejectedRecipients));
} else if (LOGGER.isDebugEnabled()) {
List allRejectedRecipients = new ArrayList<>();
for (List rejectedRecipients : rejectedRecipientLists) {
allRejectedRecipients.addAll(rejectedRecipients);
}
LOGGER.warn(String.format(logMessage, subject, allRejectedRecipients));
}
}
public static Uni getAttachmentStream(Vertx vertx, Attachment attachment) {
if (attachment.getFile() != null) {
Uni open = vertx.fileSystem().open(attachment.getFile().getAbsolutePath(),
new OpenOptions().setRead(true).setCreate(false));
return open
.flatMap(af -> af.toMulti()
.map(io.vertx.mutiny.core.buffer.Buffer::getDelegate)
.onTermination().call((r, f) -> af.close())
.collect().in(Buffer::buffer, Buffer::appendBuffer));
} else if (attachment.getData() != null) {
Publisher data = attachment.getData();
return Multi.createFrom().publisher(data)
.collect().in(Buffer::buffer, Buffer::appendByte);
} else {
return Uni.createFrom().failure(new IllegalArgumentException("Attachment has no data"));
}
}
private static class Recipients {
private final List approved;
private final List rejected;
Recipients(List approved, List rejected) {
this.approved = approved;
this.rejected = rejected;
}
}
}