JavaMailSender.java

package fr.sii.ogham.email.sender.impl;

import static fr.sii.ogham.core.util.LogUtils.logString;
import static fr.sii.ogham.email.JavaMailConstants.DEFAULT_JAVAMAIL_IMPLEMENTATION_PRIORITY;
import static fr.sii.ogham.email.attachment.ContentDisposition.ATTACHMENT;
import static fr.sii.ogham.email.attachment.ContentDisposition.INLINE;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Properties;

import javax.mail.Authenticator;
import javax.mail.BodyPart;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import fr.sii.ogham.core.builder.priority.Priority;
import fr.sii.ogham.core.env.PropertiesBridge;
import fr.sii.ogham.core.env.PropertyResolver;
import fr.sii.ogham.core.exception.InvalidMessageException;
import fr.sii.ogham.core.exception.MessageException;
import fr.sii.ogham.core.sender.AbstractSpecializedSender;
import fr.sii.ogham.email.attachment.Attachment;
import fr.sii.ogham.email.exception.handler.AttachmentResourceHandlerException;
import fr.sii.ogham.email.exception.handler.ContentHandlerException;
import fr.sii.ogham.email.message.Email;
import fr.sii.ogham.email.message.EmailAddress;
import fr.sii.ogham.email.message.Recipient;
import fr.sii.ogham.email.sender.impl.javamail.JavaMailAttachmentHandler;
import fr.sii.ogham.email.sender.impl.javamail.JavaMailContentHandler;
import fr.sii.ogham.email.sender.impl.javamail.JavaMailInterceptor;

/**
 * Java mail API implementation.
 * 
 * @author Aurélien Baudet
 * @see JavaMailContentHandler
 */
@Priority(properties = "${ogham.email.implementation-priority.javamail}", defaultValue = DEFAULT_JAVAMAIL_IMPLEMENTATION_PRIORITY)
public class JavaMailSender extends AbstractSpecializedSender<Email> {
	private static final Logger LOG = LoggerFactory.getLogger(JavaMailSender.class);

	/**
	 * Properties that is used to initialize the session
	 */
	private final Properties properties;

	/**
	 * The content handler used to add message content
	 */
	private final JavaMailContentHandler contentHandler;

	/**
	 * The handler used to attach files to the email
	 */
	private final JavaMailAttachmentHandler attachmentHandler;

	/**
	 * Extra operations to apply on the message
	 */
	private final JavaMailInterceptor interceptor;

	/**
	 * Authentication mechanism
	 */
	private final Authenticator authenticator;

	public JavaMailSender(PropertyResolver propertyResolver, JavaMailContentHandler contentHandler, JavaMailAttachmentHandler attachmentHandler, Authenticator authenticator) {
		this(new PropertiesBridge(propertyResolver), contentHandler, attachmentHandler, authenticator);
	}

	public JavaMailSender(PropertyResolver propertyResolver, JavaMailContentHandler contentHandler, JavaMailAttachmentHandler attachmentHandler, Authenticator authenticator,
			JavaMailInterceptor interceptor) {
		this(new PropertiesBridge(propertyResolver), contentHandler, attachmentHandler, authenticator, interceptor);
	}

	public JavaMailSender(Properties properties, JavaMailContentHandler contentHandler, JavaMailAttachmentHandler attachmentHandler, Authenticator authenticator) {
		this(properties, contentHandler, attachmentHandler, authenticator, null);
	}

	public JavaMailSender(Properties properties, JavaMailContentHandler contentHandler, JavaMailAttachmentHandler attachmentHandler, Authenticator authenticator, JavaMailInterceptor interceptor) {
		super();
		this.properties = properties;
		this.contentHandler = contentHandler;
		this.attachmentHandler = attachmentHandler;
		this.authenticator = authenticator;
		this.interceptor = interceptor;
	}

	@Override
	public void send(Email email) throws MessageException {
		try {
			LOG.debug("Initialize Java mail session with authenticator {} and properties {}", authenticator, properties);
			LOG.debug("Create the mime message for email {}", logString(email));
			MimeMessage mimeMsg = createMimeMessage();
			// set the sender address
			setFrom(email, mimeMsg);
			// set recipients (to, cc, bcc)
			setRecipients(email, mimeMsg);
			// set subject and content
			mimeMsg.setSubject(email.getSubject());
			setMimeContent(email, mimeMsg);
			// default behavior is done => message is ready but let possibility
			// to add extra operations to do on the message
			if (interceptor != null) {
				LOG.debug("Executing extra operations for email {}", logString(email));
				interceptor.intercept(mimeMsg, email);
			}
			// message is ready => send it
			LOG.info("Sending email using Java Mail API through server {}:{}...", properties.getProperty("mail.smtp.host", properties.getProperty("mail.host")),
					properties.getProperty("mail.smtp.port", properties.getProperty("mail.port")));
			Transport.send(mimeMsg);
		} catch (MessagingException | ContentHandlerException | AttachmentResourceHandlerException | IOException e) {
			throw new MessageException("failed to send message using Java Mail API", email, e);
		}
	}

	/**
	 * Initialize the session and create the mime message.
	 * 
	 * @return the mime message
	 */
	private MimeMessage createMimeMessage() {
		// prepare the message
		Session session = Session.getInstance(properties, authenticator);
		return new MimeMessage(session);
	}

	/**
	 * Set the sender address on the mime message.
	 * 
	 * @param email
	 *            the source email
	 * @param mimeMsg
	 *            the mime message to fill
	 * @throws MessagingException
	 *             when the email address is not valid
	 * @throws AddressException
	 *             when the email address is not valid
	 * @throws UnsupportedEncodingException
	 *             when the email address is not valid
	 * @throws InvalidMessageException
	 *             when the email address is not valid
	 */
	private static void setFrom(Email email, MimeMessage mimeMsg) throws MessagingException, UnsupportedEncodingException, InvalidMessageException {
		if (email.getFrom() == null) {
			throw new InvalidMessageException("The sender address has not been set", email, "Missing sender email address");
		}
		mimeMsg.setFrom(toInternetAddress(email.getFrom()));
	}

	/**
	 * Set the recipients addresses on the mime message.
	 * 
	 * @param email
	 *            the source email
	 * @param mimeMsg
	 *            the mime message to fill
	 * @throws MessagingException
	 *             when the email address is not valid
	 * @throws AddressException
	 *             when the email address is not valid
	 * @throws UnsupportedEncodingException
	 *             when the email address is not valid
	 */
	private static void setRecipients(Email email, MimeMessage mimeMsg) throws MessagingException, UnsupportedEncodingException {
		for (Recipient recipient : email.getRecipients()) {
			mimeMsg.addRecipient(convert(recipient.getType()), toInternetAddress(recipient.getAddress()));
		}
	}

	/**
	 * Set the content on the mime message.
	 * 
	 * <ul>
	 * <li>If the source email has only one textual content (text/html for
	 * example), the structure is:
	 * 
	 * <pre>
	 * [text/html] (root/body)
	 * </pre>
	 * 
	 * </li>
	 * <li>If the source email has HTML content with embedded attachments
	 * (images for example), the structure is:
	 * 
	 * <pre>
	 * [multipart/related] (root/body)
	 *   [text/html]       
	 *   [image/png]       (embedded image 1)
	 *   [image/gif]       (embedded image 2)
	 * </pre>
	 * 
	 * </li>
	 * <li>If the source email has HTML content with attachments, the structure
	 * is:
	 * 
	 * <pre>
	 * [multipart/mixed]              (root)
	 *   [text/html]                  (body)
	 *   [application/pdf]            (attached file 1)
	 *   [application/octet-stream]   (attached file 2)
	 * </pre>
	 * 
	 * </li>
	 * <li>If the source email has HTML content with embedded attachments
	 * (images for example) and additional attachments, the structure is:
	 * 
	 * <pre>
	 * [multipart/mixed]              (root)
	 *   [multipart/related]          (body)
	 *     [text/html]                
	 *     [image/png]                (embedded image 1)
	 *     [image/gif]                (embedded image 2)
	 *   [application/pdf]            (attached file 1)
	 *   [application/octet-stream]   (attached file 2)
	 * </pre>
	 * 
	 * </li>
	 * <li>If the source email has several textual contents (text/html and
	 * text/plain for example), the structure is:
	 * 
	 * <pre>
	 * [multipart/alternative]  (root/body)
	 *   [text/plain]           (alternative body)
	 *   [text/html]            (main body)
	 * </pre>
	 * 
	 * </li>
	 * <li>If the source email has several textual contents (text/html and
	 * text/plain for example) and embedded attachments (images for example),
	 * the structure is:
	 * 
	 * <pre>
	 * [multipart/related]          (root/body)
	 *   [multipart/alternative]    
	 *     [text/plain]             (alternative body)
	 *     [text/html]              (main body)
	 *   [image/png]                (embedded image 1)
	 *   [image/gif]                (embedded image 2)
	 * </pre>
	 * 
	 * </li>
	 * <li>If the source email has several textual contents (text/html and
	 * text/plain for example) and attachments, the structure is:
	 * 
	 * <pre>
	 * [multipart/mixed]              (root)
	 *   [multipart/alternative]      (body)
	 *     [text/plain]               (alternative body)
	 *     [text/html]                (main body)
	 *   [application/pdf]            (attached file 1)
	 *   [application/octet-stream]   (attached file 2)
	 * </pre>
	 * 
	 * </li>
	 * <li>If the source email has several textual contents (text/html and
	 * text/plain for example), embedded attachment (images for example) and
	 * attachments, the structure is:
	 * 
	 * <pre>
	 * [multipart/mixed]              (root)
	 *   [multipart/related]          (body)
	 *     [multipart/alternative]      
	 *       [text/plain]             (alternative body)
	 *       [text/html]              (main body)
	 *     [image/png]                (embedded image 1)
	 *     [image/gif]                (embedded image 2)
	 *   [application/pdf]            (attached file 1)
	 *   [application/octet-stream]   (attached file 2)
	 * </pre>
	 * 
	 * </li>
	 * </ul>
	 * 
	 * @param email
	 *            the source email
	 * @param mimeMsg
	 *            the mime message to fill
	 * @throws MessagingException
	 *             when the email address is not valid
	 * @throws ContentHandlerException
	 *             when the email address is not valid
	 * @throws AttachmentResourceHandlerException
	 *             when the email address is not valid
	 * @throws IOException
	 *             when the content can't be constructed
	 */
	private void setMimeContent(Email email, MimeMessage mimeMsg) throws MessagingException, ContentHandlerException, AttachmentResourceHandlerException, IOException {
		LOG.debug("Add message content for email {}", logString(email));

		Multipart mixedContainer = new MimeMultipart("mixed");

		// prepare the body
		contentHandler.setContent(mimeMsg, mixedContainer, email, email.getContent());

		// add the attachments (either embedded or attached)
		Multipart relatedContainer = getOrAddRelatedContainer(mixedContainer, email);
		for (Attachment attachment : email.getAttachments()) {
			Multipart attachmentContainer = isEmbeddableAttachment(attachment) ? relatedContainer : mixedContainer;
			attachmentHandler.addAttachment(attachmentContainer, attachment);
		}

		// set the content of the email
		if (hasDownloadableAttachments(email) || hasEmbeddableAttachments(email)) {
			mimeMsg.setContent(hasDownloadableAttachments(email) ? mixedContainer : relatedContainer);
		} else {
			// extract the body from the container (as it is not necessary)
			// and place the body at the root of the message
			BodyPart body = mixedContainer.getBodyPart(0);
			mimeMsg.setContent(body.getContent(), body.getContentType());
		}
	}

	private static Multipart getOrAddRelatedContainer(Multipart root, Email email) throws MessagingException, IOException {
		// no embeddable attachments means that there is no need of the related
		// container
		if (!hasEmbeddableAttachments(email)) {
			return null;
		}
		Multipart related = findRelatedContainer(root);
		if (related == null) {
			related = new MimeMultipart("related");
			moveBodyToRelatedContainer(root, related);
			addRelatedContainer(root, related);
		}
		return related;
	}

	private static void moveBodyToRelatedContainer(Multipart root, Multipart related) throws MessagingException {
		while (root.getCount() > 0) {
			related.addBodyPart(root.getBodyPart(0));
			root.removeBodyPart(0);
		}
	}

	private static void addRelatedContainer(Multipart root, Multipart related) throws MessagingException {
		MimeBodyPart part = new MimeBodyPart();
		part.setContent(related);
		root.addBodyPart(part);
	}

	private static Multipart findRelatedContainer(Multipart container) throws MessagingException, IOException {
		if (isRelated(container)) {
			return container;
		}
		for (int i = 0; i < container.getCount(); i++) {
			Object content = container.getBodyPart(i).getContent();
			if (content instanceof Multipart) {
				return findRelatedContainer((Multipart) content);
			}
		}
		return null;
	}

	private static boolean isRelated(Multipart mp) {
		return mp.getContentType().startsWith("multipart/related");
	}

	private static boolean isEmbeddableAttachment(Attachment attachment) {
		return INLINE.equals(attachment.getDisposition());
	}

	private static boolean isDownloadableAttachment(Attachment attachment) {
		return ATTACHMENT.equals(attachment.getDisposition());
	}

	private static boolean hasEmbeddableAttachments(Email email) {
		return email.getAttachments().stream().anyMatch(JavaMailSender::isEmbeddableAttachment);
	}

	private static boolean hasDownloadableAttachments(Email email) {
		return email.getAttachments().stream().anyMatch(JavaMailSender::isDownloadableAttachment);
	}

	private static RecipientType convert(fr.sii.ogham.email.message.RecipientType type) {
		switch (type) {
			case BCC:
				return RecipientType.BCC;
			case CC:
				return RecipientType.CC;
			case TO:
				return RecipientType.TO;
			default:
				throw new IllegalArgumentException("Invalid recipient type " + type);
		}
	}

	private static InternetAddress toInternetAddress(EmailAddress address) throws AddressException, UnsupportedEncodingException {
		return address.getPersonal() == null ? new InternetAddress(address.getAddress()) : new InternetAddress(address.getAddress(), address.getPersonal());
	}

	@Override
	public String toString() {
		return "JavaMailSender";
	}
}