![JAR search and dependency download from the Maven repository](/logo.png)
com.day.cq.commons.mail.MailTemplate Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*
* Copyright 1997-2010 Day Management AG
* Barfuesserplatz 6, 4001 Basel, Switzerland
* All Rights Reserved.
*
* This software is the confidential and proprietary information of
* Day Management AG, ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Day.
*/
package com.day.cq.commons.mail;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.commons.mail.impl.HtmlParserAccessor;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang.text.StrLookup;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.commons.mail.Email;
import org.apache.commons.mail.EmailException;
import org.apache.commons.mail.HtmlEmail;
import org.apache.sling.commons.html.HtmlParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.mail.Header;
import javax.mail.MessagingException;
import javax.mail.internet.InternetHeaders;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The MessageTemplate
class provides email text templating functionality. Templates are
* nt:file
nodes in the repository representing a text file.
*
* The text file contains the complete email. Email headers are defined as the first lines of the file in the format
* Header-Name: Header-Value
, one header per line. Headers supporting multiple values can thus have several
* header lines. The supported headers are the standard email headers.
*
* After the last header line put an empty line and start the email body afterwards.
*
* Within all of the text file, replacement variables can be used in the form of ${variable}
, e.g.
* ${payload.title}
. The available variables are defined by the variable resolver given in the {@link
* #getEmail(org.apache.commons.lang.text.StrLookup, Class)} method.
*
* The getEmail
method returns the chosen (type
argument) email implementation, as long as the
* type extends {@link org.apache.commons.mail.Email} and has a publically accessible default constructor. Out of the
* box the three email implementations provided by the Apache Commons Email library can be used: {@link
* org.apache.commons.mail.SimpleEmail}, {@link org.apache.commons.mail.HtmlEmail} and {@link
* org.apache.commons.mail.MultiPartEmail}.
*
*/
public class MailTemplate {
private static final String HEADER_TO = "To";
private static final String HEADER_CC = "CC";
private static final String HEADER_BCC = "BCC";
private static final String HEADER_REPLYTO = "Reply-To";
private static final String HEADER_FROM = "From";
private static final String HEADER_SUBJECT = "Subject";
private static final String HEADER_BOUNCETO = "Bounce-To";
private static final String[] PRIMARY_HEADERS = new String[]{
HEADER_TO,
HEADER_CC,
HEADER_BCC,
HEADER_REPLYTO,
HEADER_FROM,
HEADER_SUBJECT,
HEADER_BOUNCETO
};
private static final String DEFAULT_CHARSET = "utf-8";
private static final Logger log = LoggerFactory.getLogger(MailTemplate.class);
private String message;
private String charset;
/**
* Constructs a new MailTemplate
with the template text given as the inputStream
and the
* encoding (may be null).
*
* @param inputStream The template text.
* @param encoding The encoding of the input stream (may be null). If empty, UTF-8 will be used.
*
* @throws IOException If an error occurs handling the input stream.
*/
public MailTemplate(final InputStream inputStream, final String encoding) throws IOException {
if (null == inputStream) {
throw new IllegalArgumentException("input stream may not be null");
}
charset = StringUtils.defaultIfEmpty(encoding, DEFAULT_CHARSET);
final InputStreamReader reader = new InputStreamReader(inputStream, charset);
final StringWriter writer = new StringWriter();
IOUtils.copy(reader, writer);
message = writer.toString();
}
/**
* Create an {@link org.apache.commons.mail.Email} based on the template text and replacing variables in the
* template text. Emails are constructed and returned with the
* given type
. If the type
is {@link org.apache.commons.mail.HtmlEmail} (or a subclass),
* and the template content appears to be HTML, it is used as the HTML part of the email message. The text part of
* the email message is constructed from doing a basic HTML to plain text conversion.
*
* @param variables The variables to use for variable substitution or null
if no variables are needed.
* @param type The class defining the email type.
* @param The email type.
*
* @return An email based on the template text and the variables.
*
* @throws IOException If an error occurs handling the text template.
* @throws MessagingException If an error occurs during building the email message.
* @throws EmailException If an error occurs during building the email.
* @since 5.8
*/
public T getEmail(final Map variables, final Class type)
throws IOException, MessagingException, EmailException {
return this.getEmailInternal(StrLookup.mapLookup(variables), type);
}
/**
* Create an {@link org.apache.commons.mail.Email} based on the template text and replacing variables in the
* template text using the given lookup
implementation. Emails are constructed and returned with the
* given type
. If the type
is {@link org.apache.commons.mail.HtmlEmail} (or a subclass),
* and the template content appears to be HTML, it is used as the HTML part of the email message. The text part of
* the email message is constructed from doing a basic HTML to plain text conversion.
*
* @param lookup The {@link org.apache.commons.lang.text.StrLookup} implementation to use for variable lookup.
* @param type The class defining the email type.
* @param The email type.
*
* @return An email based on the template text and the variable resolver.
*
* @throws IOException If an error occurs handling the text template.
* @throws MessagingException If an error occurs during building the email message.
* @throws EmailException If an error occurs during building the email.
* @throws IllegalArgumentException If lookup
is null
.
* @deprecated Use {@link #getEmail(java.util.Map, Class)} instead.
*/
@Deprecated
public T getEmail(final StrLookup lookup, final Class type)
throws IOException, MessagingException, EmailException {
log.warn("Depcrecated method com.day.cq.commons.mail.MailTemplate#getEmail(StrLookup, Class) used. Please use getEmail(Map, Class) instead.");
if (null == lookup) {
throw new IllegalArgumentException("lookup may not be null");
}
return this.getEmailInternal(lookup, type);
}
@SuppressWarnings("rawtypes")
private T getEmailInternal(final StrLookup lookup, final Class type)
throws IOException, MessagingException, EmailException {
final StrSubstitutor substitutor = new StrSubstitutor(lookup);
final String source = substitutor.replace(message);
final ByteArrayInputStream in = new ByteArrayInputStream(
source.getBytes(charset));
final InternetHeaders headers = new InternetHeaders(in);
T email = null;
try {
final Constructor constructor = type.getConstructor();
email = constructor.newInstance();
email.setCharset(charset);
// read the remainder of the original message and use as the
// email's body. any header's at the beginning of the
// stream will have already been read, so anything remaining
// will be the email message content.
final StringWriter writer = new StringWriter();
IOUtils.copy(in, writer, charset);
final String msg = writer.toString();
HtmlParser parser = HtmlParserAccessor.HTML_PARSER_INSTANCE;
if (email instanceof HtmlEmail && parser != null && isHtmlMessage(msg)) {
final HtmlEmail htmlEmail = (HtmlEmail) email;
try {
final PlainTextExtractor plainTextExtractor = new PlainTextExtractor();
parser.parse(new ByteArrayInputStream(msg.getBytes(charset)), charset, plainTextExtractor);
htmlEmail.setTextMsg(plainTextExtractor.toString());
htmlEmail.setHtmlMsg(msg);
} catch (SAXException e) {
email.setMsg(msg);
}
} else {
email.setMsg(msg);
}
// add primary headers (sending relevant)
final Enumeration primaryHeaders = headers.getMatchingHeaders(PRIMARY_HEADERS);
while (primaryHeaders.hasMoreElements()) {
final Header header = (Header) primaryHeaders.nextElement();
final String name = header.getName();
final String value = header.getValue();
if (null != value) {
if (HEADER_TO.equalsIgnoreCase(name)) {
email.addTo(value);
} else if (HEADER_CC.equalsIgnoreCase(name)) {
email.addCc(value);
} else if (HEADER_BCC.equalsIgnoreCase(name)) {
email.addBcc(value);
} else if (HEADER_REPLYTO.equalsIgnoreCase(name)) {
email.addReplyTo(value);
} else if (HEADER_FROM.equalsIgnoreCase(name)) {
email.setFrom(value);
} else if (HEADER_SUBJECT.equalsIgnoreCase(name)) {
email.setSubject(value);
} else if (HEADER_BOUNCETO.equalsIgnoreCase(name)) {
email.setBounceAddress(value);
}
} else {
log.warn("got empty primary header [{}].", name);
}
}
// add secondary headers
final Enumeration secondaryHeaders = headers.getNonMatchingHeaders(PRIMARY_HEADERS);
while (secondaryHeaders.hasMoreElements()) {
final Header header = (Header) secondaryHeaders.nextElement();
final String name = header.getName();
final String value = header.getValue();
if (null != value) {
email.addHeader(name, value);
} else {
log.warn("got empty secondary header [{}].", name);
}
}
} catch (NoSuchMethodException e) {
// ignore
} catch (InvocationTargetException e) {
// ignore
} catch (InstantiationException e) {
// ignore
} catch (IllegalAccessException e) {
// ignore
}
return email;
}
/**
* Convenience method to create a new {@link com.day.cq.commons.mail.MailTemplate} based on the path
* identifying the location of the email template text in the repository.
*
* @param path The location of the email template text in the repository. Must point to an nt:file node.
* @param session The session used for accessing the repository.
*
* @return A mail template or null
if there was an error creating the template.
*/
public static MailTemplate create(final String path, final Session session) {
if (StringUtils.isBlank(path)) {
throw new IllegalArgumentException("path may not be null or empty");
}
if (null == session) {
throw new IllegalArgumentException("session may not be null");
}
InputStream is = null;
try {
if (session.itemExists(path)) {
final Node node = session.getNode(path);
if (JcrConstants.NT_FILE.equals(node.getPrimaryNodeType().getName())) {
final Node content = node.getNode(JcrConstants.JCR_CONTENT);
final String encoding = (content.hasProperty(JcrConstants.JCR_ENCODING))
? content.getProperty(JcrConstants.JCR_ENCODING).getString()
: "utf-8";
is = content.getProperty(JcrConstants.JCR_DATA).getBinary().getStream();
log.debug("loaded template [{}].", path);
return new MailTemplate(is, encoding);
} else {
throw new IllegalArgumentException("provided path does not point to a nt:file node");
}
}
} catch (RepositoryException e) {
log.error("error creating message template: ", e);
} catch (IOException e) {
log.error("error creating message template: ", e);
} finally {
IOUtils.closeQuietly(is);
}
return null;
}
/*
* This is perhaps an inelegant way to determine this, but Sling's Commons HTML parser is too lenient and will
* create false positives.
*/
private static boolean isHtmlMessage(final String msg) {
Pattern p = Pattern.compile("<\\s*html[^>]*>");
Matcher m = p.matcher(msg.toLowerCase());
return m.find();
}
private static class PlainTextExtractor implements ContentHandler {
private boolean append = false;
private StringBuilder buffer = new StringBuilder();
private String linkText;
@Override
public void setDocumentLocator(Locator locator) {
}
@Override
public void startDocument() throws SAXException {
}
@Override
public void endDocument() throws SAXException {
}
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
}
@Override
public void endPrefixMapping(String prefix) throws SAXException {
}
@Override
public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
if ("body".equals(localName)) {
append = true;
} else if ("li".equals(localName)) {
buffer.append("\n * ");
} else if ("dt".equals(localName)) {
buffer.append(" ");
} else if ("p".equals(localName) || "tr".equals(localName) || localName.matches("h[1-5]")) {
buffer.append("\n");
} else if ("a".equals(localName)) {
final String href = atts.getValue("href");
if (href != null) {
linkText = String.format(" <%s>", href);
}
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if ("body".equals(localName)) {
append = false;
} else if ("br".equals(localName) || "p".equals(localName) || "tr".equals(localName) || localName.matches("h[1-5]")) {
buffer.append("\n");
} else if ("a".equals(localName) && linkText != null) {
buffer.append(linkText);
linkText = null;
}
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (append) {
buffer.append(ch, start, length);
}
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
}
@Override
public void processingInstruction(String target, String data) throws SAXException {
}
@Override
public void skippedEntity(String name) throws SAXException {
}
@Override
public String toString() {
return buffer.toString().trim();
}
}
}