SendGridV4Sender.java

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

import static fr.sii.ogham.core.util.LogUtils.logString;
import static fr.sii.ogham.email.sendgrid.SendGridConstants.DEFAULT_SENDGRID_IMPLEMENTATION_PRIORITY;
import static fr.sii.ogham.email.sendgrid.sender.EmailValidator.validate;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.regex.Pattern;

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

import fr.sii.ogham.core.builder.priority.Priority;
import fr.sii.ogham.core.exception.MessageException;
import fr.sii.ogham.core.exception.mimetype.MimeTypeDetectionException;
import fr.sii.ogham.core.mimetype.MimeTypeProvider;
import fr.sii.ogham.core.sender.AbstractSpecializedSender;
import fr.sii.ogham.core.util.Base64Utils;
import fr.sii.ogham.core.util.IOUtils;
import fr.sii.ogham.email.attachment.Attachment;
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.sendgrid.sender.SendGridSender;
import fr.sii.ogham.email.sendgrid.sender.exception.AttachmentReadException;
import fr.sii.ogham.email.sendgrid.sender.exception.SendGridException;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.client.SendGridClient;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.client.SendGridInterceptor;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.compat.AttachmentsCompat;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.compat.CompatFactory;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.compat.CompatUtil;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.compat.MailCompat;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.compat.PersonalizationCompat;
import fr.sii.ogham.email.sendgrid.v4.sender.impl.sendgrid.handler.SendGridContentHandler;

/**
 * SendGrid-backed implementation of the email sender.
 */
@Priority(properties = "${ogham.email.implementation-priority.sendgrid}", defaultValue = DEFAULT_SENDGRID_IMPLEMENTATION_PRIORITY)
public final class SendGridV4Sender extends AbstractSpecializedSender<Email> implements SendGridSender {
	private static final Logger LOG = LoggerFactory.getLogger(SendGridV4Sender.class);
	private static final Pattern CID = Pattern.compile("^<(.+)>$");

	private final SendGridClient delegate;
	private final SendGridContentHandler handler;
	private final MimeTypeProvider mimetypeProvider;
	private final CompatFactory objectsFactory;
	private final SendGridInterceptor interceptor;

	/**
	 * Uses the default {@link CompatFactory} ({@link CompatUtil#getDefaultCompatFactory()}).
	 * 
	 * @param service
	 *            the underlying SendGrid service
	 * @param handler
	 *            the content handler, in change of converting the email content
	 *            into something the {@link SendGridClient} can work with
	 * @param mimetypeProvider
	 *            determines mimetype for attachments
	 */
	public SendGridV4Sender(final SendGridClient service, final SendGridContentHandler handler, MimeTypeProvider mimetypeProvider) {
		this(service, handler, mimetypeProvider, CompatUtil.getDefaultCompatFactory(), null);
	}

	/**
	 * Constructor.
	 * 
	 * @param service
	 *            the underlying SendGrid service
	 * @param handler
	 *            the content handler, in change of converting the email content
	 *            into something the {@link SendGridClient} can work with
	 * @param mimetypeProvider
	 *            determines mimetype for attachments
	 * @param objectsFactory
	 *            factory that creates instances of {@code sendgrid-java}
	 *            objects. This is needed due to issue in package naming with
	 *            {@code sendgrid-java} 4.3.0
	 */
	public SendGridV4Sender(final SendGridClient service, final SendGridContentHandler handler, MimeTypeProvider mimetypeProvider, CompatFactory objectsFactory) {
		this(service, handler, mimetypeProvider, objectsFactory, null);
	}

	/**
	 * Constructor.
	 * 
	 * @param service
	 *            the underlying SendGrid service
	 * @param handler
	 *            the content handler, in change of converting the email content
	 *            into something the {@link SendGridClient} can work with
	 * @param mimetypeProvider
	 *            determines mimetype for attachments
	 * @param interceptor
	 *            an extension point for customizing the email to send
	 * @param objectsFactory
	 *            factory that creates instances of {@code sendgrid-java}
	 *            objects. This is needed due to issue in package naming with
	 *            {@code sendgrid-java} 4.3.0
	 */
	public SendGridV4Sender(final SendGridClient service, final SendGridContentHandler handler, MimeTypeProvider mimetypeProvider, CompatFactory objectsFactory, SendGridInterceptor interceptor) {
		if (service == null) {
			throw new IllegalArgumentException("[service] cannot be null");
		}
		if (handler == null) {
			throw new IllegalArgumentException("[handler] cannot be null");
		}
		if (mimetypeProvider == null) {
			throw new IllegalArgumentException("[mimetypeProvider] cannot be null");
		}

		this.delegate = service;
		this.handler = handler;
		this.mimetypeProvider = mimetypeProvider;
		this.objectsFactory = objectsFactory;
		this.interceptor = interceptor;
	}

	@Override
	public void send(final Email message) throws MessageException {
		if (message == null) {
			throw new IllegalArgumentException("[message] cannot be null");
		}
		validate(message);

		try {
			LOG.debug("Preparing to send email using SendGrid: {}", message);
			final MailCompat sgEmail = intercept(toSendGridEmail(message), message);

			LOG.debug("Sending email {}", logString(message));
			LOG.trace("SendGrid email: {}", sgEmail);
			delegate.send(sgEmail);
			LOG.debug("Email has been successfully sent");
		} catch (ContentHandlerException e) {
			throw new MessageException("A content-related error occurred when trying to build an email", message, e);
		} catch (AttachmentReadException e) {
			throw new MessageException("Attaching file to email failed when trying to send an email", message, e);
		} catch (SendGridException e) {
			throw new MessageException("A SendGrid-related error occurred when trying to send an email", message, e);
		}
	}

	private MailCompat intercept(MailCompat sendGridEmail, Email source) {
		if (interceptor == null) {
			return sendGridEmail;
		}
		return interceptor.intercept(sendGridEmail, source);
	}

	private MailCompat toSendGridEmail(final Email message) throws ContentHandlerException, AttachmentReadException {
		final MailCompat sendGridMail = objectsFactory.newMail();
		sendGridMail.setSubject(message.getSubject());

		sendGridMail.setFrom(message.getFrom().getAddress(), message.getFrom().getPersonal());

		sendGridMail.addPersonalization(toPersonalization(message));

		handler.setContent(message, sendGridMail, message.getContent());

		for (Attachment attachment : message.getAttachments()) {
			addAttachment(sendGridMail, attachment);
		}

		return sendGridMail;
	}

	private PersonalizationCompat toPersonalization(final Email message) {
		PersonalizationCompat personalization = objectsFactory.newPersonalization();
		for (Recipient recipient : message.getRecipients()) {
			addRecipient(personalization, recipient);
		}
		return personalization;
	}

	private static void addRecipient(PersonalizationCompat personalization, Recipient recipient) {
		final EmailAddress address = recipient.getAddress();
		switch (recipient.getType()) {
			case TO:
				personalization.addTo(address.getAddress(), address.getPersonal());
				break;
			case CC:
				personalization.addCc(address.getAddress(), address.getPersonal());
				break;
			case BCC:
				personalization.addBcc(address.getAddress(), address.getPersonal());
				break;
		}
	}

	private void addAttachment(final MailCompat sendGridMail, final Attachment attachment) throws AttachmentReadException {
		try {
			AttachmentsCompat sendGridAttachment = objectsFactory.newAttachments();
			byte[] bytes = IOUtils.toByteArray(attachment.getResource().getInputStream());
			sendGridAttachment.setContent(Base64Utils.encodeToString(bytes));
			sendGridAttachment.setContentId(toCid(attachment.getContentId()));
			sendGridAttachment.setDisposition(attachment.getDisposition());
			sendGridAttachment.setFilename(attachment.getResource().getName());
			sendGridAttachment.setType(getMimetype(attachment, bytes));
			sendGridMail.addAttachments(sendGridAttachment);
		} catch (IOException e) {
			throw new AttachmentReadException("Failed to attach email attachment named " + attachment.getResource().getName(), attachment, e);
		} catch (MimeTypeDetectionException e) {
			throw new AttachmentReadException("Failed to determine mimetype for email attachment named " + attachment.getResource().getName(), attachment, e);
		}
	}

	private String getMimetype(Attachment attachment, byte[] bytes) throws MimeTypeDetectionException {
		if (attachment.getContentType() != null) {
			return attachment.getContentType();
		}
		return mimetypeProvider.detect(new ByteArrayInputStream(bytes)).toString();
	}

	private static String toCid(final String contentId) {
		if (contentId == null) {
			return null;
		}
		return CID.matcher(contentId).replaceAll("$1");
	}

	public SendGridClient getDelegate() {
		return delegate;
	}
}