One feature that seems to eventually creep into every web application is the ability to send email. Generally, it's a very specific kind of email, like a password reminder, welcome message, order confirmation, or receipt. Despite the fact that the content of these emails differs from application to application, the process of sending email rarely changes. You construct a message, give it to the mail server, and it gets delivered.
When you program in Java, the JavaMail API is most commonly used to do the dirty work of connecting to the mail server and sending the message. Unfortunately, it's kind of ponderous to use (mainly due to the flexibility of email), so if you're going to use it more than once, it generally makes sense to write a wrapper for it. Depending on how you use it, the wrapper may be written to send one specific kind of email, such as a password reminder, or to work generically, accepting the subject, recipients, and body of the email as arguments.
Once you have a wrapper for actually sending mail, you need a system for constructing the messages themselves. Let's look at password reminders. Nearly all email messages have a subject, body, and recipients. When a password reminder is sent, the user's email address and password are usually fetched from some sort of repository where login information is stored. The subject and the body have to be merged with the data in the database, and have to be stored somewhere, as well. One of the great problems of application design in general is where to keep these sorts of strings. In many cases, such strings are stored in property files, which keeps them from littering your source code and makes localization easier. I've used this approach for storing templates for outgoing emails in many web applications, but unfortunately, it's quite flawed.
Here's a quick list of reasons why property files are suboptimal for storing strings to be used in email messages:
To: field and three in the Cc: field.
You can't easily do that using property files.\n.One alternative approach is to use XML for the email templates, and that's the approach I'm going to discuss in this article. XML provides great flexibility in how you structure your templates and does not have the same strict formatting rules as property files do, so it's easier to maintain large strings. The main downside is that XML files can be trickier to deal with than property files. With a property file, it's easy to load the files and it's easy to access the properties once they've been loaded. On the other hand, it takes more work to load the XML file and process it using one of the many XML processing libraries provided for use with Java.
|
Related Articles: Using the Jakarta Commons, Part 2 -- Ever find yourself thinking, "Someone's surely solved this problem before?" The Jakarta Commons is a good place to start looking when that thought tickles your mind. In this second of three articles, Vikram Goyal explores the Packages and XML groups of the Commons. Learning and Using Jakarta Digester -- Turning an XML doc into Java bean objects is a common task, but the SAX and DOM APIs are too low-level. Jakarta Digester uses a series of rules to simplify this important task. |
This article and the accompanying code attempt to take as much pain as possible out of the process for you by providing a generic framework that enables you to create email templates using XML files and send them. In this framework, I'll be using the Commons Digester package from the Jakarta project for XML processing, and the JavaMail API do send the actual email.
Let's look at the format for the email template itself. The templates are
XML files that contain only a root element and children of that element. The
root element is <email>. The required children are
<subject>, <body>, and
<from>. The optional elements are <to>,
<cc>, and <bcc>. If you've used an email
client at all, you can deduce what each of these elements should contain. There
can be multiple instances of the optional elements, so that you can include
multiple addresses for each type of recipient. I'll explain how this works when
I describe how the message is processed. Here's an example template file:
<email>
<from>rafe@rafe.us</from>
<to>someone@example.com</to>
<cc>someoneelse@example.com</cc>
<bcc>rafe@rafe.us</bcc>
<subject>This is the subject</subject>
<body>This is the body of an email message.</body>
</email>
One useful feature provided by property files is that you can use the
MessageFormat class to substitute placeholders in your properties
with values that are passed in at runtime. For example, if you are putting
errors in a property file and one of the errors is file not found,
you can write the property like this:
file.not.found.error=Error, could not find file {0}.
Then, at runtime, you can use MessageFormat as follows:
ResourceBundle bundle = ResourceBundle.getBundle(
"MyProperties", currentLocale);
Object[] arguments = { "some_file.txt" };
String newString = MessageFormat.format(
bundle.getString("file.not.found.error"), arguments);
At the end, newString will contain Error, could not find
file some_file.txt. I've built similar flexibility into this system. The
MessageFormat can format any string, so you can embed the same
tokens that can be used in a property file into the subject and
body elements of your email template.
In some cases, you'll want to insert personalized information into your
templates when the email is sent. For example, you might want to include the
recipient's first name in the email, or perhaps the details of their order.
This system handles these situations by processing the body and subject of the
email template using MessageFormat. The catch is that it accepts
only one array of arguments that are applied to both the subject and the body.
So the subject could contain tokens {0}, {2}, and
{3}, and the body could contain tokens {0},
{1}, and {4}. I took this approach because in many
cases, the same arguments are used in both the body and the subject and it
simplifies the parameter list that's passed in to the
EmailSender.
Once you've created your template, the next step is to process it. As you probably know, there are many XML processing libraries to choose from. Commons Digester, part of the Jakarta Commons project, was originally created as part of the Struts project in order to provide a quick and easy way to parse the Struts configuration file. It provides an easy way to map the elements in the XML file to a data structure using syntax similar to XPath. The advantage is that it enables you to pluck needed elements out of an XML document without parsing it node-by-node using SAX or dealing with the treelike data structure provided by the DOM.
Here's the method that reads the data from the XML file and copies it into
an EmailTemplate object:
public static EmailTemplate getEmailTemplate(InputStream aStream)
{
Digester digester = new Digester();
digester.setValidating(false);
digester.addObjectCreate("email", EmailTemplate.class);
digester.addBeanPropertySetter("email/subject", "subject");
digester.addBeanPropertySetter("email/body", "body");
digester.addBeanPropertySetter("email/from", "from");
digester.addCallMethod("email/to", "addTo", 0);
digester.addCallMethod("email/cc", "addCc", 0);
digester.addCallMethod("email/bcc", "addBcc", 0);
try
{
return (EmailTemplate)digester.parse(aStream);
}
catch (IOException e)
{
logger.error("Error: ", e);
return null;
}
catch (SAXException e)
{
logger.error("Error: ", e);
return null;
}
}
Let's look at the example line by line. The way Commons Digester works is
that you set up some rules to apply to the file to be parsed. Before I set up
my processing rules, I set the validating flag on the digester to false,
because there's no DTD to validate the email template against. To begin
processing the file, I instantiate the Digester object and then
call methods to set up the data mapping rules. First, I call the
addObjectCreate() method, which sets up a rule to create an
EmailTemplate object whenever an email element is encountered.
email is the root element in the XML template file, so each file
maps to one EmailTemplate object.
For elements that only appear once in the template file, I use the
addBeanPropertySetter() method. It accepts two arguments, the path
to an element to match and the setter to call. In the first call, I indicate
that the values in elements that match the pattern email/subject
should be passed in to the subject setter of the
EmailTemplate class. The patterns describe the nesting order of
elements to search for, separated by slashes. In this case, the pattern matches
subject elements that are children of email elements.
Wildcards can also be used in patterns to provide more flexibility. For a full
description of how patterns are constructed, see the JavaDoc
for Commons Digester.
For elements that can occur multiple times in the template file, calling the
setter method for the property won't work. Instead, I use the
addCallMethod() rule, which takes the value in the element and
calls the specified method. I use the version of this method that takes three
arguments: the pattern to match, the method to call, and the number of
arguments the method being called takes. In all three cases, I pass in
0 as the third argument, which indicates that the body of the
matched element is the only argument that should be passed to the method being
called. In the EmailTemplate class, I've written three methods,
addTo(), addCc(), and addBcc(), which
add the recipients listed in the template file to collections in the template
class.
Once the rules have been set up for the six types of children of the email
element, I'm ready to parse the file. In this case, I pass in the
InputStream that the getEmailTemplate method accepts
as an argument. The parse method can accept a File, a
SAX InputSource, an InputStream, a
Reader, or a URI as the target for the file to parse. I chose an
InputStream in this case. It's up to the calling code to fetch the
XML file and turn it into an InputStream. To write this in a more
generic way, I could have set up this method to accept an Object
and used the instanceof operator inside to verify that the
argument is one of the types of arguments accepted by the parse
method and then call it appropriately.
The parse method can throw an IOException or a
SAXException. Those are handled by passing the exception off to a
Log4J logger, and then returning null. If no exceptions occur, then the new
EmailTemplate object generated by the Digester is
returned.
EmailTemplate ClassThe meat of the EmailTemplate class is the
getEmailTemplate() method. The rest is just properties and
convenience methods. It has three String properties: the body,
subject, and from address, and three ArrayList properties: the to,
CC, and BCC lists, all of which will contain strings as members. There are
getters and setters for those properties, as well as "add" methods for the
collections. There are three additional convenience methods:
getToAddresses(), getCcAddresses(), and
getBccAddresses(). The JavaMail API expects that you'll pass in
addresses as old-fashioned arrays of InternetAddress objects.
These methods convert the ArrayLists of String
objects into the arrays required by JavaMail.
EmailSenderOnce the template file is parsed and is an EmailTemplate
object, the next step is to send the email message. The
EmailSender class includes one static, overloaded method —
sendEmail(). It can be called in a number of different ways, all
of which are shortcuts to the full method signature:
public static void sendEmail(
String aTo,
EmailTemplate aTemplate,
String[] aArgs)
The arguments don't require much explanation. The first is a To
address for the email. You can include recipient addresses in your email
templates, but in many cases, the system will need to specify a recipient at
runtime. For example, if you're sending a password reminder, it needs to go to
the email address of the user requesting the password. The recipient addresses
in the email template are for cases where the system needs to send an email to
a hardcoded address or needs to include certain hardcoded recipients for
testing. For example, suppose a system needs to generate an email that triggers
a workflow every time an order is submitted — the hardcoded address in
the template is useful in those situations.
The second argument is the EmailTemplate itself. The third is
the list of arguments that will be passed to MessageFormat when it
parses the subject and body of the email. It's up to the calling code to create
the array of information used to personalize the email template. There are also
several other method declarations that simplify calling this method (so that
you can call it without specifying a recipient, or without arguments).
The body of the sendEmail() method mostly consists of calls to
set up sending the message using JavaMail. I talked about the fact that there
was a lot of overhead when it comes to using JavaMail, and now you see what I
meant. First, I check to make sure that the EmailTemplate isn't
null. If it is, there's nothing I can do. The first step in
setting up is creating a Properties object (a glorified
Hashtable) with the SMTP server set. In my configuration, I have
the SMTP server setting in a properties file, so I read it from the properties
file and put it in the properties object I created.
I then create a JavaMail Session object and pass in the
Properties object. The Session object is needed to
create the MimeMessage object, which I do next. Then I set the
From: address to the address specified in the
EmailTemplate object that I passed in as an argument. The next
step is to set the To: field for the message I'm constructing.
There are some gymnastics here because the user can pass in a To:
address and the template can also contain any number of To: addresses. The
problem here is that JavaMail likes to use arrays for lists of addresses, so
it's up to me to determine how large the list of recipients is and construct
the array to pass in. Once I've created an array of recipients that includes
the argument to the method (if it's not null) and the recipients
in the template, I set it on the MimeMessage object.
Since all CC: and BCC: addresses must be specified
within the template, handling them is more straightforward. I simply use the
convenience methods in the EmailTemplate class and add the
additional recipients to the message. As I mentioned earlier, I use
MessageFormat to apply any arguments supplied with the method call
to the subject and body. Once I've done so, I copy the new subject into the
message object. I do the same thing with the message body. All that's left at
that point is to call Transport.send() and pass in the
MimeMessage object.
Now that I've explained how the system works, I'll explain how to use it from a servlet, though it will work similarly in any real program. Here's the code:
// Grab the email template.
InputStream template =
getServlet()
.getServletConfig()
.getServletContext()
.getResourceAsStream(
"/WEB-INF/email/registrationNotification.xml");
EmailTemplate notification = EmailTemplate.getEmailTemplate(template);
// Create the section of the email containing the actual user data.
String[] args = { "Rafe" };
EmailSender.sendEmail("rafe@rafe.us", notification, args);
The first step in using this system is to turn your XML template file into
an InputStream. Because I'm using a servlet, I fetch the file from
the ServletContext. There are other ways to get the file, but in a
servlet environment, this one works well. Then all I have to do is pass the
InputStream off to the
EmailTemplate.getEmailTemplate() method that I described earlier.
Next, I just set up my array of arguments for customizing the email and call
the EmailSender.sendEmail() method.
There are a number of enhancements that can be made to this system. Two
obvious improvements are the ability to support both HTML and plaintext email,
and the ability to include attachments with the email messages. To create these
kinds of messages, the javax.mail.MimeMultipart message type is
used. There's also the issue of where to store and how to specify attachments.
In my system, I handle adding the attachments outside of the context of the
template file, because my attachments are generated at the time the email is
sent.
Rafe Colburn is a Java developer and computer book author, and has written on Perl, CGI programming, HTML, and Java.
Return to ONJava.com.
Copyright © 2007 O'Reilly Media, Inc.