/**
* =========================================================================
* __ ____ ____ __ ____ ___ __ __ ____ ____ ____
* || || \\ || (( \ || \\ // \\ ||\ || || \\ || || \\
* || ||_// ||== \\ ||_// (( )) ||\\|| || )) ||== ||_//
* |__|| || \\ ||___ \_)) || \\_// || \|| ||_// ||___ || \\
* =========================================================================
*
* Copyright 2012 Brad Peabody
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* =========================================================================
*/
package org.jresponder.message;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.mail.BodyPart;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import org.joda.time.format.ISOPeriodFormat;
import org.joda.time.format.PeriodFormatter;
import org.jresponder.domain.Subscriber;
import org.jresponder.domain.Subscription;
import org.jresponder.engine.SendConfig;
import org.jresponder.util.TextRenderUtil;
import org.jresponder.util.TextUtil;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mail.javamail.MimeMessageHelper;
/**
* Default MessageRef implementation - reads messages from a file on disk,
* renders contents using Velocity
*
* @author bradpeabody
*
*/
public class MessageRefImpl implements MessageRef {
/* ====================================================================== */
/* Logger boiler plate */
/* ====================================================================== */
private static Logger l = null;
private Logger logger() { if (l == null) l = LoggerFactory.getLogger(this.getClass()); return l; }
/* ====================================================================== */
private String name;
private File file;
private String fileContents;
private long fileContentsTimestamp = 0;
private Document document;
private Map<String,String> propMap;
/**
* Default constructor - make sure to call setFile() and then refresh()
*/
public MessageRefImpl() {
}
/**
* Constructor which calls setFile() and refresh() for you
* @param aFile
* @throws InvalidMessageException
*/
public MessageRefImpl(File aFile) throws InvalidMessageException {
setFile(aFile);
refresh();
}
@Override
public String getName() {
return name;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
// id is set to file name without .html extension
name = file.getName().replaceAll("[.]html$", "");
}
public String getFileContents() {
return fileContents;
}
@Override
public synchronized void refresh() throws InvalidMessageException {
try {
logger().debug("MessageRef - Starting refresh for: {}", file.getCanonicalPath());
// set timestamp
fileContentsTimestamp = file.lastModified();
StringBuilder myStringBuilder = new StringBuilder();
char[] buf = new char[4096];
BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
int len;
while ((len = r.read(buf)) > 0) {
myStringBuilder.append(buf, 0, len);
}
r.close();
fileContents = myStringBuilder.toString();
document = Jsoup.parse(fileContents, "UTF-8");
propMap = new HashMap<String,String>();
Elements myMetaTagElements = document.select("meta");
if (myMetaTagElements == null || myMetaTagElements.isEmpty()) {
throw new InvalidMessageException("No meta tags found in file: "+file.getCanonicalPath());
}
for (Element myPropElement: myMetaTagElements) {
String myName = myPropElement.attr("name");
String myValue = myPropElement.attr("content");
propMap.put(myName, myValue);
}
// bodies are not read at all until message generation time
} catch (IOException e) {
throw new InvalidMessageException(e);
}
// debug dump
if (logger().isDebugEnabled()) {
for (String myKey: propMap.keySet()) {
logger().debug(" property -- {}: {}", myKey, (propMap.get(myKey)));
}
}
}
/**
* Get a property by string name
*/
@Override
public String getProp(String aName) {
return propMap.get(aName);
}
/**
* Type-safe version of getProp
*/
@Override
public String getProp(MessageRefProp aName) {
return getProp(aName.toString());
}
/**
* Get all property names
*/
@Override
public List<String> getPropNames() {
return new ArrayList<String>(propMap.keySet());
}
/**
* Gets the JR_WAIT_AFTER_LAST_MESSAGE property and parses it as an
* ISO8601 duration and returns the number of milliseconds it represents.
* Returns null if not set.
* @throws IllegalArgumentException if the value is in an invalid format
* @return
*/
@Override
public Long getWaitAfterLastMessage() {
// try to parse time from message
String myWaitAfterLastMessageString =
this.getProp(MessageRefProp.JR_WAIT_AFTER_LAST_MESSAGE);
if (myWaitAfterLastMessageString == null) {
logger().debug("No JR_WAIT_AFTER_LAST_MESSAGE property found on message (message={})", getName());
return null;
}
PeriodFormatter myPeriodFormatter = ISOPeriodFormat.standard();
try {
long myDuration = myPeriodFormatter.parsePeriod(myWaitAfterLastMessageString).toStandardDuration().getMillis();
return myDuration;
}
catch (IllegalArgumentException e) {
logger().error("Unable to parse JR_WAIT_AFTER_LAST_MESSAGE value for (message={}), value was: \"{}\" (this is a problem you need to fix!!! e.g. for one day, use \"P1D\")", new Object[] { getName(), myWaitAfterLastMessageString });
throw new IllegalArgumentException("Error parsing JR_WAIT_AFTER_LAST_MESSAGE value: " + myWaitAfterLastMessageString, e);
}
}
/**
* Render a message in the context of a particular subscriber
* and subscription.
*/
@Override
public boolean populateMessage(MimeMessage aMimeMessage, SendConfig aSendConfig, Subscriber aSubscriber, Subscription aSubscription) {
try {
// prepare context
Map<String,Object> myRenderContext = new HashMap<String,Object>();
myRenderContext.put("subscriber", aSubscriber);
myRenderContext.put("subscription", aSubscription);
myRenderContext.put("config", aSendConfig);
myRenderContext.put("message", this);
// render the whole file
String myRenderedFileContents = TextRenderUtil.getInstance().render(fileContents, myRenderContext);
// now parse again with Jsoup
Document myDocument = Jsoup.parse(myRenderedFileContents);
String myHtmlBody = "";
String myTextBody = "";
// html body
Elements myBodyElements = myDocument.select("#htmlbody");
if (!myBodyElements.isEmpty()) {
myHtmlBody = myBodyElements.html();
}
// text body
Elements myJrTextBodyElements = myDocument.select("#textbody");
if (!myJrTextBodyElements.isEmpty()) {
myTextBody = TextUtil.getInstance().getWholeText(myJrTextBodyElements.first());
}
// now build the actual message
MimeMessage myMimeMessage = aMimeMessage;
// wrap it in a MimeMessageHelper - since some things are easier with that
MimeMessageHelper myMimeMessageHelper = new MimeMessageHelper(myMimeMessage);
// set headers
// subject
myMimeMessageHelper.setSubject
(
TextRenderUtil.getInstance().render
(
(String)propMap.get(MessageRefProp.JR_SUBJECT.toString()),
myRenderContext
)
);
// TODO: implement DKIM, figure out subetha
String mySenderEmailPattern = aSendConfig.getSenderEmailPattern();
String mySenderEmail = TextRenderUtil.getInstance().render(mySenderEmailPattern, myRenderContext);
myMimeMessage.setSender(new InternetAddress(mySenderEmail));
myMimeMessageHelper.setTo(aSubscriber.getEmail());
// from
myMimeMessageHelper.setFrom
(
TextRenderUtil.getInstance().render
(
(String)propMap.get(MessageRefProp.JR_FROM_EMAIL.toString()),
myRenderContext
),
TextRenderUtil.getInstance().render
(
(String)propMap.get(MessageRefProp.JR_FROM_NAME.toString()),
myRenderContext
)
);
// see how to set body
// if we have both text and html, then do multipart
if (myTextBody.trim().length() > 0 && myHtmlBody.trim().length() > 0) {
// create wrapper multipart/alternative part
MimeMultipart ma = new MimeMultipart("alternative");
myMimeMessage.setContent(ma);
// create the plain text
BodyPart plainText = new MimeBodyPart();
plainText.setText(myTextBody);
ma.addBodyPart(plainText);
// create the html part
BodyPart html = new MimeBodyPart();
html.setContent(myHtmlBody, "text/html");
ma.addBodyPart(html);
}
// if only HTML, then just use that
else if (myHtmlBody.trim().length() > 0) {
myMimeMessageHelper.setText(myHtmlBody, true);
}
// if only text, then just use that
else if (myTextBody.trim().length() > 0) {
myMimeMessageHelper.setText(myTextBody, false);
}
// if neither text nor HTML, then the message is being skipped,
// so we just return null
else {
return false;
}
return true;
}
catch (MessagingException e) {
throw new RuntimeException(e);
}
catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
@Override
public synchronized boolean conditionalRefresh() throws InvalidMessageException {
if (file.lastModified() != fileContentsTimestamp) {
refresh();
return true;
}
return false;
}
}