Create email-based apps with JAMES
The open source JAMES mail server offers the tools to develop email-based applications in Java
When it’s not ideal to send users to a Website or to launch a GUI-based application, email-based applications can offer an innovative way for users to communicate with server software. Developers have long recognized the value of sending generated emails to users, but until now they have been unable to listen in response. Traditionally, the developer’s best approach for a response from generated email was to have the user click a link in the email, sending him or her to a Website URL.
With an email-based application, a user can send an email to a server; the server can then process that email, making the communication two-way. Email-based interaction offers advantages over sending a user to a Website. First, interaction proceeds even if the user is offline. A user can read her email and, in turn, reply with a series of emails that create an instruction queue for a remote system. Second, under some circumstances a user can respond quicker via an email. If the needed response is simple, an email proves faster than Website interactions because the user stays within her email software.
The Java Apache Mail Enterprise Server (JAMES), an open source effort from the Jakarta Project, allows for custom email processing. With JAMES, developers can process messages from users via email. JAMES offers POP3 (Post Office Protocol) and NNTP (Network News Transport Protocol) support, and may support IMAP (Internet Message Access Protocol) in a future release.
In this article, I present an online scheduling system that uses JAMES to capture users’ availability. In the future, the scheduling system may use JAMES to track the validity of email addresses, track unsuccessfully delivered scheduling requests, and otherwise further integrate the Web and email sides of the system.
This article assumes that you know how to send an email using JavaMail. (For a JavaWorld article on JavaMail, read “JavaMail Quick Start.”) I focus on how to design the communication flow for an email-based application, how to set up JAMES, and how to process user messages.
Note: You can download this article’s source code from Resources.
Plan the interaction
In our scheduling system, buyers look for sellers available to do a small piece of work. In the existing system, buyers use a Website to enter a request that is sent to a group of sellers, generating an email to each seller about the request. Sellers receive the email, click an HTTP link to launch a browser, and view the requests on the Website. Sellers then use the Website to enter their availability status and optional notes about why they are or are not available.
In the new approach, the seller composes an email in reply to the email the scheduling system generates. For the seller, there are only two possible responses: he is either available or unavailable. To make his decision known, the seller will send his reply to one of two special email addresses; depending on the address to which the seller responds, his availability will be apparent.
These special email addresses contain the seller’s state and a unique identifier for the request in question. The address’s structure looks like this:
request-[availability]-[request ID]@mydomain.com
For instance, a seller reading about request 79834 who wants to indicate his availability would send a message to:
[email protected]
When the server receives the seller’s email, it can identify the request and availability status from the target address, and identify the seller by way of his email address. If this information is sensitive, you could include a hash parameter based on the seller’s account and the request ID instead of the plain request number. The encoded address would then look like this:
[email protected]
The hash parameter a87bc8e0d0ea8
confirms that the message was not from a seller indicating availability for someone else.
Note that the seller need not type the special address; the email inserts it for both available and unavailable responses in the header and message body. The email contains the special email address that indicates availability in the Reply-To header line. Usually, when the seller hits the Reply button on his email client, that client sends a message addressed to the special email address. The unavailability-indication address resides in the CC header line. When the seller hits the Reply to All button on his email client, the unavailability-indication address appears in the list of recipients. The seller can then delete the other address that indicates availability.
The email’s body contains the special email addresses in the instructions; most email software will make the address clickable. Also, if you send HTML email, you can use mailto:
links easily generate messages to the appropriate address.
The Mailet API
Now that you know what you are listening for, you need to write code that will do the listening. The Mailet API makes the new approach possible. The API builds on the JavaMail API and uses an approach similar to the servlet specification’s lifecycle methods and object hierarchy.
The Mailet API comprises two core classes: Matcher
and Mailet
. Both are interfaces with abstract implementations available as GenericMatcher
and GenericMailet
, respectively. A matcher determines whether a message should be processed, while a mailet processes the message. A mailet container, which is a mailet-compliant SMTP (Simple Mail Transfer Protocol) mail server, instantiates and manages both.
The matcher determines the routing inside a mailet container. The match(Mail mail)
method returns a Collection
of recipients that meet the matcher’s criteria. (The result is a Collection
because a message can contain multiple recipient addresses during SMTP transport.)
You’ll also find an abstract GenericRecipientMatcher
implementation that helps the matching based on the recipient’s address. It iterates through the recipient in the Collection
and generates the resulting Collection
object so the developer need only implement the matchRecipient(MailAddress recipient)
method.
The mailet defines methods to initialize a mailet, service a message, and remove a mailet from the server. The mailet container calls these life-cycle methods in the following sequence:
- The mailet is constructed, then initialized with the
init(MailetConfig config)
method - Any messages for the
service(Mail mail)
method are handled - The mailet is taken out of service, destroyed with the
destroy()
method, then garbage collected and finalized
MailetConfig
, analogous to the Servlet API’s ServletConfig
, allows the mailet container to pass initialization parameters to the mailet.
Other useful Mailet API classes and interfaces include Mail
, MailAddress
, and MailetContext
. The Mail
interface wraps a MimeMessage
object with SMTP delivery information, including the sender’s address, a recipient’s Collection
, the connecting machine’s remote IP address, and the message state in the mailet container. MailAddress
, a more rigorous implementation of JavaMail’s InternetAddress
, provides helper methods to find an email address’s domain and user part. The MailetContext
is analogous to the ServletContext
as it provides access to the mailet container.
For the scheduling system, you will build a simple matcher that checks for addresses matching the address specification discussed above, and a mailet to process the responses and determine the sellers’ availability status.
Install and configure JAMES
You can download the latest version of JAMES from the Jakarta Project Website. At press time, the most recent version was 2.0 alpha 1 — a stable release with significant speed and feature improvements over version 1.2.1. It comprises near-production quality code, but the project elected to release an alpha version to allow for the option to change the configuration file’s structure before the final release.
After downloading and extracting the distribution to your hard drive, start JAMES by running run.bat
or run.sh
in the bin
directory. JAMES will extract itself in the apps
directory, and the basic service will begin running.
To configure JAMES, you need to modify the configuration file (apps/james/conf/config.xml
). If you run JAMES on a machine without a running local DNS server, you need to identify one in the config file; at line 278, add another line with an available DNS server’s IP address. You need to specify the domain names JAMES should handle. At line 37, add any additional domain names your machine should handle, according to your host settings.
You also need to configure a database connection. In the database connection section at line 602, either uncomment the appropriate data source or create your own by specifying the appropriate <driver>
, <dburl>
(the JDBC [Java Database Connectivity] URL), <user>
, and <password>
. Keep the name of the data source as maildb
.
The custom matcher
The matcher, dubbed SchedulingRequestMatcher
, extends GenericRecipientMatcher
to match based on the recipient’s address. The matcher will not attempt to validate the address’s structure, only determine whether the address starts with request-available-
or request-unavailable-
. The single method looks like this:
public boolean matchRecipient(MailAddress recipient) {
if (recipient.getUser().startsWith("request-available-")) {
return true;
}
if (recipient.getUser().startsWith("request-unavailable-")) {
return true;
}
return false;
}
As mentioned above, you could validate the address in the matcher; instead, we’ll leave it to the mailet to determine the validity. While it is unlikely that this kind of address would have errors, it’s better to have it match and then let the mailet catch any errors and reply with error messages.
The custom mailet
The mailet’s service()
method comprises two major sections. First, the mailet must parse the recipient address, and determine the availability and request ID. Second, the mailet must make a JDBC call to update the seller’s availability for this request. I’ve wrapped both sections in a try
/catch
block, so if errors occur and the action cannot complete, you can notify the seller of the problem. Name the mailet SchedulingRequestMailet
and have it extend GenericMailet
.
Start by parsing the recipient address to determine the availability and request ID. Check that only a single recipient has been specified, because each message can indicate availability or unavailability for only a single request. The service method starts:
public void service(Mail mail) throws MessagingException {
Collection recipients = mail.getRecipients();
if (recipients.size() > 1) {
//In final implementation, send error message to sender
return;
}
MailAddress recipient = (MailAddress)recipients.iterator().next();
//Determine whether the seller is available
boolean sellerAvailable = recipient.getUser().
startsWith("request-available-");
//Determine the request ID in question
String requestID = recipient.getUser();
int pos = requestID.lastIndexOf("-");
requestID = requestID.substring(pos + 1);
//Get the seller's email address
String sellerAddress = mail.getSender().toString();
The second section, taking these settings, uses JDBC to store the result. However, before it can store anything via JDBC, you need a handle to a database connection pool. The mailet will get a reference to a pool during the init()
method. Because much of this code is specific to Avalon, the underlying application framework upon which JAMES is built, it is not necessary to understand the specifics. Nonetheless, note that it gives a reference to a database connection pool:
protected DataSourceComponent datasource;
public void init() throws MessagingException {
try {
//Get a reference to the Avalon component manager
ComponentManager componentManager = (ComponentManager)getMailetContext()
.getAttribute(Constants.AVALON_COMPONENT_MANAGER);
// Get the list of possible data sources
DataSourceSelector datasources = (DataSourceSelector)
componentManager.lookup(DataSourceSelector.ROLE);
// Get the data source we need
datasource = (DataSourceComponent)datasources.select("maildb");
} catch (Exception e) {
throw new MessagingException("Error getting datasource", e);
}
}
With your database connection pool, you can now write the service()
method’s second half:
Connection conn = datasource.getConnection();
String sql = "UPDATE SellerRequest SET available =
? WHERE seller_email = ? AND request_id = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setBoolean(1, sellerAvailable);
stmt.setString(2, sellerAddress);
stmt.setInt(3, Integer.parseInt(requestID));
stmt.execute();
stmt.close();
conn.close();
mail.setState(Mail.GHOST);
}
The second-to-last line above sets the message’s state to Mail.GHOST
and indicates to the mailet container that this message should not be processed further. Mailets that filter a message and want let it continue processing should leave the state as is.
Again, in the final code version, you’ll wrap everything in the service()
method inside a try
/catch
block so that if any errors occur parsing the address or the message, or if there is a problem with the database connection, you can bounce a message back to the sender. You may also want to notify the seller that his response was successfully received and processed.
Install the matcher and mailet
To compile the code, you will need mailet.jar
, cornerstone.jar
, and excalibur.jar
, all of which come bundled with JAMES. Once you have compiled both classes, create a jar file in the lib
directory with the other included jar files. You should also put a jar file for your JDBC driver here if you are not using the bundled MySQL driver.
Next, open the config.xml
file you edited earlier. You want to insert your matcher and mailet in the root
processor, which is the first set of matchers and mailets that the mailet container sends messages through. Around line 158, add the following setting:
<mailet match="SchedulingRequestMatcher" class="SchedulingRequestMailet">
</mailet>
The above configuration block can appear anywhere in the root
processor, but should be before the final matcher/mailet setting that uses ToProcessor
to send messages to the transport
processor.
With this in place, restart JAMES and the new matcher and mailet should be initialized and begin processing. The log files should report whether or not they successfully loaded and if they encountered any problems.
Other ideas for email-based applications
In this article, I provided a simple example showing how you can use matchers and mailets. I hope my example sparks others to support email-based interaction in their applications. Other examples employing JAMES include:
- Attaching footers to messages
- Spam detection
- Traffic logging
- Message-delivery problem tracking
- SMS (Short Message Service) notification
- Easy file uploading with attachments
- Mailing list management
Other more exotic ideas include integrating JAMES with a fax server for email-fax-email integration. Or perhaps an application could scan for repeatedly forwarded messages to break chain messages before they clog up a mail server. Or, for the truly pious mail server administrator, a matcher could check the percentage of skin tone in attached images to spot email containing explicit images.
Email is indeed the Internet’s killer app, and JAMES gives Java developers the tools they need to build email-based applications.