14 June 2008

This took quite a bit of Googling to get right, so here it is summarized to help prospective journey makers. Spring's integration is very good for the common cases, but lack of insight can prevent you from achieving more complex integrations. For example, I wanted to be able to authenticate against Gmail (this is a program I'm writing to scratch a personal itch, but nonetheless knowing how to authenticate against SSL is a very useful thing in the enterprise.). Then I wanted to be able to send email messages that rendered as HTML for the sophisticated email clients that supported it (I suspect that's the overwhelming majority at this point - it's been a while since RFC 822 was relevant!)  and have the content fall back to plain text content otherwise. Finally, I wanted to integrate with Velocity so that I could parameterize my emails readily. This begat the following method, a few stanzas of Spring configuration, and a properties file. NB: Replace the values in the properties file with your own information! 

See the public static void main(String[] args) method to see an example invocation of this code.

# utils.properties
mail.smtp.host=smtp.gmail.com
mail.username=USERNAME@gmail.com
mail.password=PASSWORD
mail.smtp.port=465

The Spring configuration file is not as overwhelming as it looks - most of it is scaffolding.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang-2.0.xsd
">


<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host" value="${mail.smtp.host}"/>
<property name="port" value="${mail.smtp.port}"/>
<property name="username" value="${mail.username}"/>
<property name="password" value="${mail.password}"/>
<property name="javaMailProperties">
<props>
<prop key="mail.smtp.host">${mail.smtp.host}</prop>
<prop key="mail.smtp.port">${mail.smtp.port}</prop>
<prop key="mail.smtp.auth">true</prop>
<prop key="mail.smtp.starttls.enable">true</prop>
<prop key="mail.debug">false</prop>

<prop key="mail.smtp.socketFactory.port">${mail.smtp.port}</prop>
<prop key="mail.smtp.socketFactory.class">javax.net.ssl.SSLSocketFactory</prop>
<prop key="mail.smtp.socketFactory.fallback">false</prop>
</props>
</property>
</bean>

<bean id="emailUtils" class="com.joshlong.example.util.EmailUtils">
<property name="mailSender" ref="mailSender"/>
</bean>

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location">
<value>utils.properties</value>
</property>
<property name="ignoreUnresolvablePlaceholders">
<value>true</value>
</property>
</bean>

</beans>

And finally, the utility code itself. I have a Maven POM that's bringing in most of this stuff. You could probably get there by using Spring-All and making sure to add velocity, Apache Commons Lang, and the JavaMail/Activation jars.


package com.joshlong.example.util;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.tools.generic.DateTool;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import javax.mail.Message;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
*
* @author Josh Long
*
* This class is designed to support _easily_ sending out e-mails and parameterizing them.
* In approach, this is <i>almost</i> as simple as PHP's send() function! Naturally, it's
* quite a bit more powerful for it's simplicity.
*
*/
public class EmailUtils {

private Logger log = Logger.getLogger(EmailUtils.class);
private JavaMailSenderImpl mailSender;

public JavaMailSenderImpl getMailSender() {
return mailSender;
}

public void setMailSender(JavaMailSenderImpl mailSender) {
this.mailSender = mailSender;
}

public static void main(String a[]) throws Throwable {
ApplicationContext context = new ClassPathXmlApplicationContext("utils.xml");
EmailUtils utils = (EmailUtils) context.getBean("emailUtils");
Map<String, Object> params = new HashMap<String, Object>();
params.put("user", "John Doe");
utils.sendEmailMessage("from@foobar.com", new String[]{"towhoever@gmail.com"},
"A Subject", "Hello ${user} from plain text",
 "<h1>Hello ${user} from High Fidelity HTML</h1>", params);
}

public String mergeTemplate(String template, Map<String, Object> macros) throws Throwable {

if (StringUtils.isEmpty(template))
return StringUtils.EMPTY;

String answer = null;

VelocityContext context = new VelocityContext();

context.put("dateTool", new DateTool());

for (String key : macros.keySet())
context.put(key, macros.get(key));

StringWriter writer = new StringWriter();

Velocity.init();

if (Velocity.evaluate(context, writer, "LOG", template)) {
IOUtils.closeQuietly(writer);
answer = writer.toString();
}
return answer;
}

public void sendEmailMessage(String from, String[] to, String subject, String textBody, String htmlBody, Map<String, Object> params) throws Throwable {

MimeMessage msg = mailSender.createMimeMessage();

msg.setFrom(new InternetAddress(from));
msg.setSubject(subject);
msg.setRecipients(Message.RecipientType.TO, getInternetAddresses(to));

MimeMultipart content = new MimeMultipart("alternative");

if (!StringUtils.isEmpty(textBody)) {
MimeBodyPart text = new MimeBodyPart();
text.setText(mergeTemplate(textBody, params));
content.addBodyPart(text);
}

if (!StringUtils.isEmpty(htmlBody)) {
MimeBodyPart html = new MimeBodyPart();
html.setContent(mergeTemplate(htmlBody, params), "text/html");
content.addBodyPart(html);
}

msg.setContent(content);
msg.saveChanges();

try {
mailSender.send(msg);
} catch (MailException ex) {
log.info("Issue with sending out mail having body " + StringUtils.defaultString(textBody) + "; params are:" + params, ex);
}
}

private InternetAddress[] getInternetAddresses(String... emails) throws Throwable {
List<InternetAddress> addys = new ArrayList<InternetAddress>();
for (String e : emails)
addys.add(new InternetAddress(e));
return addys.toArray(new InternetAddress[0]);
}

}