package org.simplejavamail.converter.internal.mimemessage;
import net.markenwerk.utils.mail.dkim.Canonicalization;
import net.markenwerk.utils.mail.dkim.DkimMessage;
import net.markenwerk.utils.mail.dkim.DkimSigner;
import net.markenwerk.utils.mail.dkim.SigningAlgorithm;
import org.simplejavamail.email.AttachmentResource;
import org.simplejavamail.email.Email;
import org.simplejavamail.email.Recipient;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.mail.*;
import javax.mail.internet.*;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import static java.lang.String.format;
import static org.simplejavamail.internal.util.MiscUtil.valueNullOrEmpty;
/**
* Helper class that deals with javax.mail RFC MimeMessage stuff, as well as DKIM signing.
*/
public final class MimeMessageHelper {
/**
* Encoding used for setting body text, email address, headers, reply-to fields etc. ({@link StandardCharsets#UTF_8}).
*/
private static final String CHARACTER_ENCODING = StandardCharsets.UTF_8.name();
private MimeMessageHelper() {
}
/**
* Creates a new {@link MimeMessage} instance coupled to a specific {@link Session} instance and prepares it in the email structure, so that it
* can be filled and send.
* <p/>
* Fills subject, from,reply-to, content, sent-date, recipients, texts, embedded images, attachments, content and adds all headers.
*
* @param email The email message from which the subject and From-address are extracted.
* @param session The Session to attach the MimeMessage to
* @return A fully preparated {@link Message} instance, ready to be sent.
* @throws MessagingException May be thrown when the message couldn't be processed by JavaMail.
* @throws UnsupportedEncodingException Zie {@link InternetAddress#InternetAddress(String, String)}.
* @see #setRecipients(Email, Message)
* @see #setTexts(Email, MimeMultipart)
* @see #setEmbeddedImages(Email, MimeMultipart)
* @see #setAttachments(Email, MimeMultipart)
*/
public static MimeMessage produceMimeMessage(final Email email, final Session session)
throws MessagingException, UnsupportedEncodingException {
if (email == null) {
throw new IllegalStateException("email is missing");
}
if (session == null) {
throw new IllegalStateException("session is needed, it cannot be attached later");
}
// create new wrapper for each mail being sent (enable sending multiple emails with one mailer)
final MimeEmailMessageWrapper messageRoot = new MimeEmailMessageWrapper();
final MimeMessage message = new MimeMessage(session);
// set basic email properties
message.setSubject(email.getSubject(), CHARACTER_ENCODING);
message.setFrom(new InternetAddress(email.getFromRecipient().getAddress(), email.getFromRecipient().getName(), CHARACTER_ENCODING));
setReplyTo(email, message);
setRecipients(email, message);
// fill multipart structure
setTexts(email, messageRoot.multipartAlternativeMessages);
setEmbeddedImages(email, messageRoot.multipartRelated);
setAttachments(email, messageRoot.multipartRoot);
message.setContent(messageRoot.multipartRoot);
setHeaders(email, message);
message.setSentDate(new Date());
if (email.isApplyDKIMSignature()) {
return signMessageWithDKIM(message, email);
}
return message;
}
/**
* Fills the {@link Message} instance with recipients from the {@link Email}.
*
* @param email The message in which the recipients are defined.
* @param message The javax message that needs to be filled with recipients.
* @throws UnsupportedEncodingException See {@link InternetAddress#InternetAddress(String, String)}.
* @throws MessagingException See {@link Message#addRecipient(Message.RecipientType, Address)}
*/
private static void setRecipients(final Email email, final Message message)
throws UnsupportedEncodingException, MessagingException {
for (final Recipient recipient : email.getRecipients()) {
final Address address = new InternetAddress(recipient.getAddress(), recipient.getName(), CHARACTER_ENCODING);
message.addRecipient(recipient.getType(), address);
}
}
/**
* Fills the {@link Message} instance with reply-to address.
*
* @param email The message in which the recipients are defined.
* @param message The javax message that needs to be filled with reply-to address.
* @throws UnsupportedEncodingException See {@link InternetAddress#InternetAddress(String, String)}.
* @throws MessagingException See {@link Message#setReplyTo(Address[])}
*/
private static void setReplyTo(final Email email, final Message message)
throws UnsupportedEncodingException, MessagingException {
final Recipient replyToRecipient = email.getReplyToRecipient();
if (replyToRecipient != null) {
final InternetAddress replyToAddress = new InternetAddress(replyToRecipient.getAddress(), replyToRecipient.getName(),
CHARACTER_ENCODING);
message.setReplyTo(new Address[] { replyToAddress });
}
}
/**
* Fills the {@link Message} instance with the content bodies (text and html).
*
* @param email The message in which the content is defined.
* @param multipartAlternativeMessages See {@link MimeMultipart#addBodyPart(BodyPart)}
* @throws MessagingException See {@link BodyPart#setText(String)}, {@link BodyPart#setContent(Object, String)} and {@link
* MimeMultipart#addBodyPart(BodyPart)}.
*/
private static void setTexts(final Email email, final MimeMultipart multipartAlternativeMessages)
throws MessagingException {
if (email.getText() != null) {
final MimeBodyPart messagePart = new MimeBodyPart();
messagePart.setText(email.getText(), CHARACTER_ENCODING);
multipartAlternativeMessages.addBodyPart(messagePart);
}
if (email.getTextHTML() != null) {
final MimeBodyPart messagePartHTML = new MimeBodyPart();
messagePartHTML.setContent(email.getTextHTML(), "text/html; charset=\"" + CHARACTER_ENCODING + "\"");
multipartAlternativeMessages.addBodyPart(messagePartHTML);
}
}
/**
* Fills the {@link Message} instance with the embedded images from the {@link Email}.
*
* @param email The message in which the embedded images are defined.
* @param multipartRelated The branch in the email structure in which we'll stuff the embedded images.
* @throws MessagingException See {@link MimeMultipart#addBodyPart(BodyPart)} and {@link #getBodyPartFromDatasource(AttachmentResource, String)}
*/
private static void setEmbeddedImages(final Email email, final MimeMultipart multipartRelated)
throws MessagingException {
for (final AttachmentResource embeddedImage : email.getEmbeddedImages()) {
multipartRelated.addBodyPart(getBodyPartFromDatasource(embeddedImage, Part.INLINE));
}
}
/**
* Fills the {@link Message} instance with the attachments from the {@link Email}.
*
* @param email The message in which the attachments are defined.
* @param multipartRoot The branch in the email structure in which we'll stuff the attachments.
* @throws MessagingException See {@link MimeMultipart#addBodyPart(BodyPart)} and {@link #getBodyPartFromDatasource(AttachmentResource, String)}
*/
private static void setAttachments(final Email email, final MimeMultipart multipartRoot)
throws MessagingException {
for (final AttachmentResource resource : email.getAttachments()) {
multipartRoot.addBodyPart(getBodyPartFromDatasource(resource, Part.ATTACHMENT));
}
}
/**
* Sets all headers on the {@link Message} instance. Since we're not using a high-level JavaMail method, the JavaMail library says we need to do
* some encoding and 'folding' manually, to get the value right for the headers (see {@link MimeUtility}.
*
* @param email The message in which the headers are defined.
* @param message The {@link Message} on which to set the raw, encoded and folded headers.
* @throws UnsupportedEncodingException See {@link MimeUtility#encodeText(String, String, String)}
* @throws MessagingException See {@link Message#addHeader(String, String)}
* @see MimeUtility#encodeText(String, String, String)
* @see MimeUtility#fold(int, String)
*/
private static void setHeaders(final Email email, final Message message)
throws UnsupportedEncodingException, MessagingException {
// add headers (for raw message headers we need to 'fold' them using MimeUtility
for (final Map.Entry<String, String> header : email.getHeaders().entrySet()) {
final String headerName = header.getKey();
final String headerValue = MimeUtility.encodeText(header.getValue(), CHARACTER_ENCODING, null);
final String foldedHeaderValue = MimeUtility.fold(headerName.length() + 2, headerValue);
message.addHeader(header.getKey(), foldedHeaderValue);
}
}
/**
* Helper method which generates a {@link BodyPart} from an {@link AttachmentResource} (from its {@link DataSource}) and a disposition type
* ({@link Part#INLINE} or {@link Part#ATTACHMENT}). With this the attachment data can be converted into objects that fit in the email structure.
* <br> <br> For every attachment and embedded image a header needs to be set.
*
* @param attachmentResource An object that describes the attachment and contains the actual content data.
* @param dispositionType The type of attachment, {@link Part#INLINE} or {@link Part#ATTACHMENT} .
* @return An object with the attachment data read for placement in the email structure.
* @throws MessagingException All BodyPart setters.
*/
private static BodyPart getBodyPartFromDatasource(final AttachmentResource attachmentResource, final String dispositionType)
throws MessagingException {
final BodyPart attachmentPart = new MimeBodyPart();
// setting headers isn't working nicely using the javax mail API, so let's do that manually
final String resourceName = determineResourceName(attachmentResource, false);
final String fileName = determineResourceName(attachmentResource, true);
attachmentPart.setDataHandler(new DataHandler(new NamedDataSource(fileName, attachmentResource.getDataSource())));
attachmentPart.setFileName(fileName);
final String contentType = attachmentResource.getDataSource().getContentType();
attachmentPart.setHeader("Content-Type", contentType + "; filename=" + fileName + "; name=" + resourceName);
attachmentPart.setHeader("Content-ID", format("<%s>", resourceName));
attachmentPart.setDisposition(dispositionType + "; size=0");
return attachmentPart;
}
/**
* Determines the right resource name and optionally attaches the correct extension to the name.
*/
static String determineResourceName(final AttachmentResource attachmentResource, final boolean includeExtension) {
final String datasourceName = attachmentResource.getDataSource().getName();
String resourceName;
if (!valueNullOrEmpty(attachmentResource.getName())) {
resourceName = attachmentResource.getName();
} else if (!valueNullOrEmpty(datasourceName)) {
resourceName = datasourceName;
} else {
resourceName = "resource" + UUID.randomUUID();
}
if (includeExtension && !valueNullOrEmpty(datasourceName)) {
@SuppressWarnings("UnnecessaryLocalVariable") final
String possibleFilename = datasourceName;
if (possibleFilename.contains(".")) {
final String extension = possibleFilename.substring(possibleFilename.lastIndexOf("."), possibleFilename.length());
if (!resourceName.endsWith(extension)) {
resourceName += extension;
}
}
} else if (!includeExtension && resourceName.contains(".") && resourceName.equals(datasourceName)) {
final String extension = resourceName.substring(resourceName.lastIndexOf("."), resourceName.length());
resourceName = resourceName.replace(extension, "");
}
return resourceName;
}
/**
* Primes the {@link MimeMessage} instance for signing with DKIM. The signing itself is performed by {@link DkimMessage} and {@link DkimSigner}
* during the physical sending of the message.
*
* @param message The message to be signed when sent.
* @param email The {@link Email} that contains the relevant signing information
* @return The original mime message wrapped in a new one that performs signing when sent.
*/
public static MimeMessage signMessageWithDKIM(final MimeMessage message, final Email email) {
try {
final DkimSigner dkimSigner;
if (email.getDkimPrivateKeyFile() != null) {
// InputStream is managed by Dkim library
dkimSigner = new DkimSigner(email.getSigningDomain(), email.getSelector(),
email.getDkimPrivateKeyFile());
} else {
// InputStream is managed by SimpleJavaMail user
dkimSigner = new DkimSigner(email.getSigningDomain(), email.getSelector(),
email.getDkimPrivateKeyInputStream());
}
dkimSigner.setIdentity(email.getFromRecipient().getAddress());
dkimSigner.setHeaderCanonicalization(Canonicalization.SIMPLE);
dkimSigner.setBodyCanonicalization(Canonicalization.RELAXED);
dkimSigner.setSigningAlgorithm(SigningAlgorithm.SHA256_WITH_RSA);
dkimSigner.setLengthParam(true);
dkimSigner.setZParam(false);
return new DkimMessage(message, dkimSigner);
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException | MessagingException e) {
throw new MimeMessageException(MimeMessageException.INVALID_DOMAINKEY, e);
}
}
/**
* This class conveniently wraps all necessary mimemessage parts that need to be filled with content, attachments etc. The root is ultimately sent
* using JavaMail.<br> <br> The constructor creates a new email message constructed from {@link MimeMultipart} as follows:
* <p/>
* <pre>
* - root
* - related
* - alternative
* - mail tekst
* - mail html tekst
* - embedded images
* - attachments
* </pre>
*
* @author Benny Bottema
*/
private static class MimeEmailMessageWrapper {
private final MimeMultipart multipartRoot;
private final MimeMultipart multipartRelated;
private final MimeMultipart multipartAlternativeMessages;
/**
* Creates an email skeleton structure, so that embedded images, attachments and (html) texts are being processed properly.
*/
MimeEmailMessageWrapper() {
multipartRoot = new MimeMultipart("mixed");
final MimeBodyPart contentRelated = new MimeBodyPart();
multipartRelated = new MimeMultipart("related");
final MimeBodyPart contentAlternativeMessages = new MimeBodyPart();
multipartAlternativeMessages = new MimeMultipart("alternative");
try {
// construct mail structure
multipartRoot.addBodyPart(contentRelated);
contentRelated.setContent(multipartRelated);
multipartRelated.addBodyPart(contentAlternativeMessages);
contentAlternativeMessages.setContent(multipartAlternativeMessages);
} catch (final MessagingException e) {
throw new MimeMessageException(e.getMessage(), e);
}
}
}
}