com.databasesandlife.util.EmailTransaction Maven / Gradle / Ivy
Show all versions of java-common Show documentation
package com.databasesandlife.util;
import com.databasesandlife.util.gwtsafe.ConfigurationException;
import jakarta.mail.Message;
import jakarta.mail.*;
import jakarta.mail.Message.RecipientType;
import jakarta.mail.internet.MimeMessage;
import org.xbill.DNS.*;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* An transaction allowing emails to be sent over SMTP
*
* This object allows emails to take part in transactional operations.
* Any part of the code may {@link #send(Message)} emails using this transaction.
* They get stored in memory, and only really get sent when {@link #commit()} is called.
* This way, if an exception occurs after the email gets sent, and the operation is successfully
* retried, the recipient only gets one email based on the successful operation and no emails
* based on the unsuccessful operation.
*
* There is no need to rollback this object if an operation has been not successful.
* Simply throw the object away.
*
* @author This source is copyright Adrian Smith and licensed under the LGPL 3.
* @see Project on GitHub
*/
public class EmailTransaction {
// ------------------------------------------------------------------------
// SmtpServerConfiguration
// ------------------------------------------------------------------------
public static abstract class SmtpServerConfiguration { }
public static class MxSmtpConfiguration extends SmtpServerConfiguration {
public String mxAddress;
}
public static class SmtpServerAddress extends SmtpServerConfiguration {
public String host;
public int port = 25;
}
public static class TlsSmtpServerAddress extends SmtpServerAddress {
public String username, password;
public TlsSmtpServerAddress() { port = 587; }
}
// ------------------------------------------------------------------------
// EmailSendingConfiguration
// ------------------------------------------------------------------------
public static class EmailSendingConfiguration {
public @Nonnull SmtpServerConfiguration server;
public @Nonnull Map extraHeaders = new HashMap<>();
public EmailSendingConfiguration(SmtpServerConfiguration server) { this.server = server; }
}
// ------------------------------------------------------------------------
// State
// ------------------------------------------------------------------------
protected final @Nonnull EmailSendingConfiguration config;
protected final List messages = new ArrayList<>();
// ------------------------------------------------------------------------
// Internal methods
// ------------------------------------------------------------------------
protected static String getHostForMxRecord(String mxAddress) throws ConfigurationException {
try {
Record[] r = new Lookup(mxAddress, Type.MX).run(); // null, or array with at least one element
if (r == null) throw new ConfigurationException("No results found for MX lookup '"+mxAddress+"'");
MXRecord[] records = Arrays.copyOf(r, r.length, MXRecord[].class);
Arrays.sort(records, (x,y) -> (Integer.compare(y.getPriority(), x.getPriority())));
return records[0].getTarget().toString(true); // true is omit final dot
}
catch (TextParseException e) { throw new ConfigurationException(e); }
}
protected Properties newSessionProperties() throws ConfigurationException {
var props = new Properties();
props.put("mail.transport.protocol", "smtp");
if (config.server instanceof MxSmtpConfiguration) {
props.put("mail.smtp.host", getHostForMxRecord(((MxSmtpConfiguration)config.server).mxAddress));
}
if (config.server instanceof SmtpServerAddress) {
props.put("mail.smtp.host", ((SmtpServerAddress)config.server).host);
props.put("mail.smtp.port", ((SmtpServerAddress)config.server).port);
}
if (config.server instanceof TlsSmtpServerAddress) {
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
}
return props;
}
protected Session newSession() throws ConfigurationException {
if (config.server instanceof TlsSmtpServerAddress) {
var auth = new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
var c = (TlsSmtpServerAddress) config.server;
return new PasswordAuthentication(c.username, c.password);
}
};
return Session.getInstance(newSessionProperties(), auth);
}
else {
return Session.getDefaultInstance(newSessionProperties());
}
}
// ------------------------------------------------------------------------
// Public methods
// ------------------------------------------------------------------------
public EmailTransaction(@Nonnull EmailSendingConfiguration config) throws ConfigurationException {
this.config = config;
newSessionProperties(); // do MX lookup to check it works
}
/** Can be "foo" or "foo:123" or "foo:123|adrian|password" or "MX:foo.com" */
public static @Nonnull SmtpServerConfiguration parseAddress(@Nonnull String str) throws ConfigurationException {
Matcher m;
m = Pattern.compile("^MX:(.+)$").matcher(str);
if (m.matches()) {
var result = new MxSmtpConfiguration();
result.mxAddress = m.group(1);
return result;
}
m = Pattern.compile("^([^|:]+)(:(\\d+))?(\\|(.+)\\|(.+))?$").matcher(str);
if (m.matches()) {
SmtpServerAddress result;
if (m.group(4) != null) {
result = new TlsSmtpServerAddress();
((TlsSmtpServerAddress)result).username = m.group(5);
((TlsSmtpServerAddress)result).password = m.group(6);
} else {
result = new SmtpServerAddress();
}
result.host = m.group(1);
if (m.group(2) != null) result.port = Integer.parseInt(m.group(3));
return result;
}
throw new ConfigurationException("SMTP config '"+str+"' not understood");
}
public @Nonnull MimeMessage newMimeMessage() {
try {
var result = new MimeMessage(newSession());
for (var header : config.extraHeaders.entrySet())
result.setHeader(header.getKey(), header.getValue());
return result;
}
catch (MessagingException | ConfigurationException e) { throw new RuntimeException(e); }
}
public void send(Message msg) {
this.messages.add(msg);
}
public void commit() {
try (var ignored = new Timer(getClass().getSimpleName()+".commit")) {
var threads = new ThreadPool();
threads.setThreadNamePrefix(getClass().getSimpleName() + ".commit");
threads.setThreadCount(3); // Have some parallelism but do not overload the remote SMTP server
for (var msg : messages) {
threads.addTask(() -> {
try (var ignored2 = new Timer("Send email to '" + msg.getRecipients(RecipientType.TO)[0]+"'")) {
Transport.send(msg);
}
catch (MessagingException e) { throw new RuntimeException(e); }
});
}
threads.execute();
}
}
public int getEmailCountForTesting() { return messages.size(); }
public String getEmailBodyForTesting(int idx) {
try { return (String) messages.get(idx).getContent(); }
catch (IOException | MessagingException e) { throw new RuntimeException(e); }
}
}