All Downloads are FREE. Search and download functionalities are using the official Maven repository.

fi.evolver.basics.spring.messaging.sender.SmtpSender Maven / Gradle / Ivy

There is a newer version: 6.5.1
Show newest version
package fi.evolver.basics.spring.messaging.sender;

import static fi.evolver.basics.spring.messaging.util.SendUtils.createDataStream;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import fi.evolver.basics.spring.common.ConfigurationRepository;
import fi.evolver.basics.spring.log.MessageLogService;
import fi.evolver.basics.spring.log.entity.MessageLog.Direction;
import fi.evolver.basics.spring.messaging.SendResult;
import fi.evolver.basics.spring.messaging.entity.Message;
import fi.evolver.basics.spring.messaging.util.SendUtils;
import fi.evolver.utils.OptionalUtils;
import fi.evolver.utils.UriUtils;
import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.mail.Authenticator;
import jakarta.mail.Message.RecipientType;
import jakarta.mail.MessagingException;
import jakarta.mail.PasswordAuthentication;
import jakarta.mail.Session;
import jakarta.mail.Transport;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.util.ByteArrayDataSource;


@Component
public class SmtpSender implements Sender {
	private static final Logger LOG = LoggerFactory.getLogger(SmtpSender.class);

	private static final String MAIL_SERVER_URI = "MailServerUri";

	private static final String PROPERTY_ATTACHMENT_NAME = "AttachmentName";
	private static final String PROPERTY_BODY = "Body";
	private static final String PROPERTY_FROM = "From";
	private static final String PROPERTY_REPLY_TO = "ReplyTo";
	private static final String PROPERTY_SUBJECT = "Subject";

	private static final InternetAddress DEFAULT_FROM;

	static {
		InternetAddress address = null;
		try {
			address = new InternetAddress("[email protected]");
		}
		catch (AddressException e) {
			LOG.error("Could not initialize default from address");
			throw new IllegalStateException(e);
		}
		DEFAULT_FROM = address;
	}


	private final ConfigurationRepository configurationRepository;
	private final MessageLogService messageLogService;


	@Autowired
	public SmtpSender(ConfigurationRepository configurationRepository, MessageLogService messageLogService) {
		this.configurationRepository = configurationRepository;
		this.messageLogService = messageLogService;
	}


	@Override
	public SendResult send(Message message, URI uri) {
		try {
			String subject = getSubject(message, uri);
			List recipients = listRecipients(uri);

			List attachments = createAttachments(message);
			String mailBody = createMailBody(message, uri);
			String from = message.getMessageTargetConfig().getProperty(PROPERTY_FROM).orElse(null);
			List replyTo = message.getMessageTargetConfig().getProperty(PROPERTY_REPLY_TO).map(Collections::singletonList).orElse(null);

			sendMail(message.getMessageType(), subject, mailBody, recipients, from, replyTo, attachments);
			return SendResult.success();
		}
		catch (IOException e) {
			LOG.warn("Failed sending mail", e);
			return SendResult.error("Failed sending mail: %s", e.getMessage());
		}
	}


	private static String createMailBody(Message message, URI uri) throws IOException {
		String body = message.getMessageTargetConfig().getProperty(PROPERTY_BODY).orElse(getQueryParameter(uri, "body"));
		if (body != null)
			body = SendUtils.replaceTags(body, message);

		if (body == null)
			body = IOUtils.toString(new InputStreamReader(message.getDataStream(), StandardCharsets.UTF_8));
		return body;
	}


	private static List listRecipients(URI uri) {
		String schemeSpecificPart = uri.toString().replaceAll("^mailto:", "").replaceAll("\\?.*", "").replaceAll("#.*", "");
		return Arrays.asList(schemeSpecificPart.split(","));
	}


	private static String getSubject(Message message, URI uri) throws IllegalArgumentException {
		String subject = message.getMessageTargetConfig().getProperty(PROPERTY_SUBJECT)
				.orElse(getQueryParameter(uri, "subject"));

		if (subject == null)
			throw new IllegalArgumentException("Required query parameter 'subject' not set");
		return SendUtils.replaceTags(subject, message);
	}


	private static List createAttachments(Message message) throws IOException {
		Optional name = message.getMessageTargetConfig().getProperty(PROPERTY_ATTACHMENT_NAME);
		if (!name.isPresent())
			return Collections.emptyList();

		String attachmentName = SendUtils.replaceTags(name.get(), message);
		ByteArrayOutputStream bout = new ByteArrayOutputStream();
		try (InputStream in = createDataStream(message)) {
			IOUtils.copy(in, bout);
		}
		return Collections.singletonList(new EmailAttachment(bout.toByteArray(), attachmentName));
	}


	private static String getQueryParameter(URI uri, String attributeName)  {
		String query = uri.toString().replaceAll("[^?]*(?:[?]([^#]*))?.*", "$1");

		Pattern pattern = Pattern.compile("(?:^|&)" + attributeName + "=([^&]*)");
		Matcher matcher = pattern.matcher(query);
		if (matcher.find()) {
			try {
				return URLDecoder.decode(matcher.group(1), "UTF-8");
			}
			catch (UnsupportedEncodingException e) {
				throw new IllegalStateException("BUG! UTF-8 not supported?", e);
			}
		}

		return null;
	}



	public void sendMail(String messageType, String subject, String message, List recipients, String from, List replyTo, List emailAttachments) throws IOException {
		LocalDateTime startTime = LocalDateTime.now();
		String statusCode = "OK";
		String statusMessage = "OK";
		URI uri = null;
		try {
			uri = configurationRepository.getUri(MAIL_SERVER_URI)
					.orElseThrow(() -> new IllegalStateException(MAIL_SERVER_URI + " not configured, "));

			InternetAddress[] recipientAddresses = convert(recipients);
			if (recipientAddresses.length == 0) {
				LOG.warn("No recipients for mail, not sending");
				statusMessage = "No recipients defined";
				statusCode = "NO RECIPIENTS";
				return;
			}

			InternetAddress[] replyToAddresses = convert(replyTo);

			String user = UriUtils.getUserName(uri);
			String password = UriUtils.getPassword(uri);
			String host = uri.getHost();
			int port = uri.getPort() == -1 ? 25 : uri.getPort();
			InternetAddress fromAddress = inferFromAddress(from, uri);
			boolean sslEnabled = UriUtils.getFragmentParameter(uri, "ssl").map(Boolean::parseBoolean).orElse(false);
			boolean startTlsEnabled = UriUtils.getFragmentParameter(uri, "tls").map(Boolean::parseBoolean).orElse(false);

			sendMail(host, port, user, password, sslEnabled, startTlsEnabled, subject, message, fromAddress, replyToAddresses, emailAttachments, recipientAddresses);
		}
		catch (MessagingException e) {
			statusCode = "FAIL";
			statusMessage = e.toString();
			throw new IOException("Failed sending mail", e);
		}
		catch (RuntimeException e) {
			statusCode = "FAIL";
			statusMessage = e.toString();
			throw e;
		}
		finally {
			messageLogService.logMessage(
					startTime,
					messageType,
					"smtp",
					uri == null ? "?" : uri.toString(),
					messageLogService.getApplicationName(),
					uri == null ? "?" : uri.getHost(),
					Direction.OUTBOUND,
					message,
					null,
					null,
					null,
					statusCode,
					statusMessage);
		}
	}


	private static InternetAddress inferFromAddress(String from, URI uri) {
		return OptionalUtils.stream(Optional.ofNullable(from), UriUtils.getFragmentParameter(uri, "from"))
				.map(SmtpSender::convert)
				.findFirst()
				.orElse(DEFAULT_FROM);
	}


	private static InternetAddress convert(String address) {
		if (address == null)
			return null;

		InternetAddress[] converted = convert(Collections.singletonList(address));
		return converted.length >= 1 ? converted[0] : null;
	}


	private static InternetAddress[] convert(List addresses) {
		if (addresses == null || addresses.isEmpty())
			return new InternetAddress[0];

		List results = new ArrayList<>(addresses.size());
		for (String address: addresses) {
			try {
				results.add(new InternetAddress(address));
			}
			catch (AddressException e) {
				LOG.warn("Invalid address");
			}
		}
		return results.toArray(new InternetAddress[results.size()]);
	}



	private static void sendMail(
			String host,
			int port,
			String user,
			String password,
			boolean sslEnabled,
			boolean startTlsEnabled,
			String subject,
			String message,
			InternetAddress from,
			InternetAddress[] replyTo,
			List emailAttachments,
			InternetAddress... recipients) throws MessagingException {

		Properties properties = System.getProperties();
		properties.setProperty("mail.smtp.host", host);
		properties.setProperty("mail.smtp.starttls.enable", Boolean.toString(startTlsEnabled));
		if (user != null && user.length() > 0)
			properties.setProperty("mail.smtp.user", user);
		properties.setProperty("mail.smtp.port", String.valueOf(port));
		if (password != null && password.length() > 0)
			properties.setProperty("mail.smtp.auth", "true");
		if (sslEnabled) {
			properties.setProperty("mail.smtp.socketFactory.port", String.valueOf(port));
			properties.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
			properties.setProperty("mail.smtp.socketFactory.fallback", "false");
		}

		Session session = (password != null && password.length() > 0) ?
				Session.getInstance(properties, new SimpleAuthenticator(user, password)) :
				Session.getInstance(properties);

		MimeMessage mail = new MimeMessage(session);
		if (replyTo != null && replyTo.length > 0)
			mail.setReplyTo(replyTo);
		if (from != null)
			mail.setFrom(from);
		mail.setRecipients(RecipientType.TO, recipients);
		mail.setSubject(subject);

		MimeMultipart multipart = new MimeMultipart();
		MimeBodyPart part = new MimeBodyPart();
		multipart.addBodyPart(part);

		part.setText(message, "utf-8");
		part.setHeader("Content-Type","text/plain; charset=\"utf-8\"");
		part.setHeader("Content-Transfer-Encoding", "quoted-printable");

		for (EmailAttachment attachment : emailAttachments)
			addAttachmentToEmail(attachment, multipart);

		mail.setContent(multipart);
		mail.setSentDate(new Date());

		Transport.send(mail);
	}


	private static void addAttachmentToEmail(EmailAttachment emailAttachment, MimeMultipart multipart) {
		try {
			MimeBodyPart attachement = new MimeBodyPart();
			DataSource source = new ByteArrayDataSource(emailAttachment.getData(), "APPLICATION/OCTET-STREAM");
			attachement.setDataHandler(new DataHandler(source));
			attachement.setFileName(emailAttachment.getFilename());
			multipart.addBodyPart(attachement);
		}
		catch (Exception e) {
			throw new IllegalArgumentException("FAILED to add attachment to email", e);
		}
	}


	private static class SimpleAuthenticator extends Authenticator {
		private final String user;
		private final String password;

		public SimpleAuthenticator(String user, String password) {
			this.user = user;
			this.password = password;
		}

		@Override
		protected PasswordAuthentication getPasswordAuthentication() {
			return new PasswordAuthentication(user, password);
		}
	}


	private static class EmailAttachment {
		private final byte[] data;
		private final String filename;

		public EmailAttachment(byte[] data, String filename) {
			this.data = data;
			this.filename = filename;
		}

		public byte[] getData() {
	        return data;
	    }

		public String getFilename() {
	        return filename;
	    }
	}


	@Override
	public Set getSupportedProtocols() {
		return Set.of("mailto");
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy