Working with James, Part 2: Build e-mail based applications with matchers and mailets

Two new APIs add power to this e-mail server

This is the second of two articles focused on developing applications with the James e-mail server from the Apache group. In this article, go beyond the basic James infrastructure and implement a practical application for flagging users as available or unavailable, and for automatically sending custom messages to senders from users who chose to make themselves unavailable.

Share:

Claude Duguay (claude.duguay@verizon.net), Senior J2EE architect, Capital Stream Inc.

Claude DuguayClaude Duguay has developed software for more than 20 years. He is passionate about the Java platform, reads about it incessantly, and writes about it as often as possible. You can reach him at claude.duguay@verizon.net.



10 June 2003

Also available in Japanese

This article is the second in a series that explores the Java Apache Mail Enterprise Server, more commonly known as James. In the first part of the series, we looked at the basics of James infrastructure and capabilities, and walked through the process of installing James on a test system. In this article, we'll build on that overview and apply the ideas behind the James infrastructure directly by developing an application that supports flagging a user's account as unavailable. To use our application, the user sends a specific type of message to a specified mail server address; this message is used as an automatic response to any incoming mail, until the user sends a subsequent message indicating that he or she is available again. A similar client-side mechanism is often used to alert mail senders to the user's absence -- during a vacation, for example. But such functionality isn't very useful unless the user leaves the client software active. By using a server-side solution, you can log in using alternate client software, perhaps while you're actually on vacation, and change the message at any time.

Application design

Before designing our application, we should articulate the requirements. The following points will be the basis for our example:

  1. A user can send an e-mail to unavailable@emailserver to set his or her account to unavailable mode. The message that had been sent to unavailable@emailserver is stored for future use. If an unavailable message is already stored for the user, it is overwritten with the new one. The user should receive notification that all this has occurred.
  2. A user can send an e-mail to available@emailserver to cancel any unavailable messages. The message sent to available@emailserver is discarded and the stored unavailable message is removed. The user should receive notification that the status has been changed to available.
  3. Whenever a user for whom an unavailable message has been stored receives an e-mail, the sender should be sent a copy of the stored message, indicating that the user is unavailable. The original incoming e-mail that triggers this process should be handled normally.

Understanding the James infrastucture

Part 1 of this series introduces you to the James infrastructure and capabilities.

It's no coincidence that I've written the requirements in three statements. Each maps directly onto a set of actions taken under specific conditions. Each condition should be recognized by a matcher. (For more on matchers and mailets, see the first part of this series.) For example, an e-mail sent to a specific address can be matched with the built-in RecipientIs matcher. Unfortunately, when I studied the source code for RecipientIs, I noticed that any number of recipients could be used and that any match was deemed appropriate. This is probably fine in most cases, but our application needs to make sure one and only one specified recipient is involved, so we'll develop a simple matcher that does this. The MatchSingleRecipient class we'll develop will also serve as a good introduction showing how you can put the Matcher API to work.

Recognizing recipients that have stored unavailable messages ready for processing is slightly more complicated. For that, we'll develop a MatchUnavailableUser matcher. To make it more efficient, we'll check whether the user is local before testing for the presence of a file in the unavailable directory. Otherwise, the process should be fairly straightforward. We'll develop the two matcher classes first and then move on to the mailets.

We'll be making fairly heavy use of the unavailable directory. In fact, several operations are required, including detecting the presence of a message, saving that message, reading it, and deleting it. Because each of these functions needs to know the location of the directory in which the messages are stored and something about the user involved, we'll create a separate class that the MatchUnavailableUser matcher can use, and we'll make use of it in our mailet implementation design.

There's little commonality between the matchers we'll implement; but the mailets that handle our stated requirements (there will be one mailet for each requirement) all need similar configuration information and access to the unavailable directory, so we'll use a base class that each of the mailets can extend. The base class is also a good place to put utility methods that are used by more than one of the subclasses, along with basic initialization code. We'll call the base class UnavailableUserBase.

The three application functions will be handled by our mailet classes -- UnavailableMessageSave, UnavailableMessageDrop, and UnavailableMessageSend. The first two have very similar functions and operate on a single incoming recipient -- the unavailable or available addresses -- so they share some code, which will end up in the superclass. The UnavailableMessageSend mailet is the most complicated, primarily because it has to deal with multiple recipients and send messages to the sender for each unavailable recipient. Fortunately, the Matcher and Mailet APIs are easy to work with.


Writing the matchers

Writing a matcher in James is simplified by the base class GenericMatcher, which most implementations will extend. We'll do that for both our matchers. Primarily, we want to retrieve configuration information in the init() method and do our processing in the match() method. Technically, we should implement the getMatcherInfo() method to report the vendor, version, and so on, but I've left all that out to keep the code for this example more concise.

When configuring a matcher in James's config.xml file (we discussed this file in more detail in the first part of this series), you specify it as an XML attribute. The matcher class name is followed by an equals sign and additional text; this text can be retrieved with the getCondition() method. To take advantage of the MailAddress comparison code available in James, we create an instance with the value retrieved from the getCondition() method and store it in an instance variable. Needless to say, MatchSingleRecipient, shown in Listing 1, expects a single, valid local e-mail address as its only argument.

The match() method gets the list of recipients to which the e-mail being processed is addressed. The Mail object provides several interesting methods. Among the most commonly used is getRecipients(), which returns a Java Collection object containing the MailAddress instance. To see if we have a match, we test first to make sure that only one recipient is involved by checking the size of the Collection, and then to make sure the Collection (of length 1) contains the address we specified in the configuration file.

To compile the code for these classes, make sure that you've downloaded all the components outlined in the first part of this series. You should have the JavaMail (mail.jar) and JavaBeans Activation Framework (activation.jar) JAR files on your classpath, as well as the james.jar file. If you have trouble finding james.jar, you can extract it from the James.SAR file in the James-2.1.2/apps directory.

Listing 1. MatchSingleRecipient: Identifying an e-mail sent to a single specific address
package com.claudeduguay.mailets;

import java.util.*;
import javax.mail.*;
import org.apache.mailet.*;

public class MatchSingleRecipient
  extends GenericMatcher
{
  protected MailAddress addressToMatch;
  
  public void init(MatcherConfig config)
    throws MessagingException
  {
    super.init(config);
    addressToMatch = new MailAddress(getCondition());
  }
  
  public Collection match(Mail mail)
    throws MessagingException
  {
    Collection recipients = mail.getRecipients();
    if (recipients.size() == 1 &&
        recipients.contains(addressToMatch))
    {
      return recipients;
    }
    return null;
  }
}

The MatchUnavailableUser class, shown in Listing 2, uses the UnavailableStore class, which we'll cover momentarily. To make it work, we expect to retrieve the directory used to store unavailable messages through the getCondition() method.

Listing 2. MatchUnavailableUser: Identifying unavailable users
package com.claudeduguay.mailets;

import java.util.*;
import javax.mail.*;
import org.apache.mailet.*;

public class MatchUnavailableUser
  extends GenericMatcher
{
  protected UnavailableStore store;
  
  public void init(MatcherConfig config)
    throws MessagingException
  {
    super.init(config);
    String folder = getCondition();
    store = new UnavailableStore(folder);
  }
  
  protected boolean isLocalAddress(MailAddress address)
  {
    String host = address.getHost();
    MailetContext context = getMailetContext();
    return context.isLocalServer(host);
  }
  
  protected boolean isUserUnavailable(MailAddress address)
  {
    return store.userMessageExists(address);
  }
  
  public Collection match(Mail mail)
    throws MessagingException
  {
    Collection matches = new Vector();
    Collection recipients = mail.getRecipients();
    Iterator iterator = recipients.iterator();
    while (iterator.hasNext())
    {
      MailAddress address = (MailAddress)iterator.next();
      String user = address.getUser();
      if (isLocalAddress(address) &&
          isUserUnavailable(address))
      {
        matches.add(address);
      }
    }
    return matches;
  }
}

To make it easy to implement the match() method, I've implemented two utility methods: isLocalAddress(), which leverages the MailetContext's isLocalServer() method to determine whether the user is local, and isUserUnavailable(), which uses the UnavailableStore's userMessageExists() method to figure out whether the user has specified an unavailable message.

The match creates a Collection object (a Vector) to collect the recipients that should be processed by a mailet. We then walk through the recipients and check each one to see if it involves a local address and whether the unavailable message was specified for that user. If both conditions are true, we add the recipient to our list and return the list when we're done.

That's all there is to implementing our matcher. Let's take a quick look at the UnavailableStore code in Listing 3, which is used by MatchUnavailableUser as well as each of our mailet implementations. The idea is to collect all the methods that have to do with access to the unavailable message store in a single class. As such, more sophisticated implementations could be developed if necessary. This version is intentionally simplified and merely stores all user messages in a single directory. This might be a problem if you are managing several thousand users on the server, but is more than adequate for most applications.

Listing 3. UnavailableStore: Accessing the unavailable message store
package com.claudeduguay.mailets;

import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;

public class UnavailableStore
{
  protected File home;
  
  public UnavailableStore(String folder)
  {
    home = new File(folder);
    if (!home.exists())
    {
      home.mkdirs();
    }
  }
  
  public File getUserMessageFile(MailAddress address)
  {
    String user = address.getUser();
    File file = new File(home, user + ".msg");
    return file;
  }
  
  public boolean userMessageExists(MailAddress address)
  {
    return getUserMessageFile(address).exists();
  }
  
  public void deleteMessage(MailAddress address)
  {
    getUserMessageFile(address).delete();
  }
    
  public void storeMessage(MailAddress address, MimeMessage msg)
    throws MessagingException, IOException
  {
    File file = getUserMessageFile(address);
    FileOutputStream out = new FileOutputStream(file);
    msg.writeTo(out);
    out.close();
  }
  
  public MimeMessage getMessage(MailAddress address)
    throws MessagingException, IOException
  {
    File file = getUserMessageFile(address);
    Properties props = System.getProperties();
    Session session = Session.getDefaultInstance(props);
    FileInputStream in = new FileInputStream(file);
    MimeMessage msg = new MimeMessage(session, in);
    in.close();
    return msg;
  }
}

The key configuration argument in UnavailableStore is the directory in which to look for files. We pass that value in via the constructor and make sure that the directory is created if it does not exist. You'll have to make sure your configuration (in config.xml) points all matchers and mailets to the same directory for this application to work.

The rest of the methods are primarily for file access. The getUserMessageFile() method provides a common way of resolving the file name for a given user. We use James MailAddress objects as arguments in each of these methods because it's easier to work with those in our matcher and mailet code. The file names take the form [unavailabledirectory]/[username].msg.

The MimeMessage object is part of the JavaMail API. James leverages these objects in its own infrastructure, so we can use the same API. The storeMessage() and getMessage() methods take advantage of MimeMessage's ability to read and write the messages in the standard text format used by most mail servers. While it would have been just as easy (and possibly more efficient) to store the messages in serialized object form, it's easier to debug things when the message is readable as text.


Writing the mailets

Now that we have a mechanism for recognizing e-mails that require processing (the matchers), we need to put together the processing elements themselves in the form of mailet implementations. We'll leverage UnavailableStore to store and access our unavailable messages.

Before moving on to our mailet implementations, let's take a quick look at the base class we'll use to keep some common behavior accessible. The mailet configuration parameters are provided in the config.xml file in the form of XML tags. These tags can be retrieved using the getInitParameter() method from the GenericMailet class, which we extend. UnavailableMessageBase, outlined in Listing 4, is declared abstract because it doesn't directly implement the service method.

The init() method resembles the init() method in the MatchUnavailableUser class in that it stores a reference to an UnavailableStore object. Most of the code in UnavailableMessageBase takes the form of utility methods. I've developed methods to translate between a MailAddress and an array of Address objects and to get the first MailAddress from a Collection of addresses, along with a method for packaging a single MailAddress as a Collection and a couple of methods for creating e-mail messages.

Because messages may contain arbitrary multipart content, the last createMessage() method uses an Object and String argument to specify the content and MIME type, respectively. The simple case, which uses a String for content and a MIME type of "text/plain", is abstracted by another method with a simplified signature. We use the more general case to send unavailable messages, because the stored message may be of arbitrary complexity.

Listing 4. UnavailableMessageBase: Base class used to keep common behavior available
package com.claudeduguay.mailets;

import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;

public abstract class UnavailableMessageBase
  extends GenericMailet
{
  protected UnavailableStore store;
  
  public void init(MailetConfig config)
    throws MessagingException
  {
    super.init(config);
    MailetContext context = config.getMailetContext();
    String folder = getInitParameter("folder");
    store = new UnavailableStore(folder);
  }
  
  protected Address[] toAddressArray(MailAddress address)
    throws AddressException
  {
    InternetAddress[] array = new InternetAddress[1];
    array[0] = address.toInternetAddress();
    return array;
  }
  
  protected MailAddress getFirstAddress(Collection list)
  {
    Iterator iterator = list.iterator();
    return (MailAddress)iterator.next();
  }
  
  protected Collection addressAsCollection(MailAddress address)
  {
    Collection list = new Vector();
    list.add(address);
    return list;
  }
  
  protected MimeMessage createMessage(
    MailAddress from, MailAddress to,
    String subject, String text)
      throws MessagingException
  {
    return createMessage(from, to, subject, text, "text/plain");
  }
  
  protected MimeMessage createMessage(
    MailAddress from, MailAddress to,
    String subject, Object content, String type)
      throws MessagingException
  {
    Properties props = System.getProperties();
    Session session = Session.getDefaultInstance(props);
    MimeMessage msg = new MimeMessage(session);
    msg.addFrom(toAddressArray(from));
    msg.addRecipients(Message.RecipientType.TO, toAddressArray(to));
    msg.setSubject(subject);
    msg.setContent(content, type);
    return msg;
  }
}

Now we can work on the mailets directly. The first thing we need to do is save messages sent to the unavailable address. We can configure the MatchSingleRecipient matcher to dispatch messages that match the unavailable recipient to the UnavailableMessageSave mailet, shown in Listing 5. To maximize configurablilty, I've made it possible to specify the configuration message's subject and content string. The values are stored to local variables in the init() method.

The service method does all the work. Because we've extended our base class, UnavailableMessageBase, we have access to an instance of UnavailableStore. Our objective is to figure out who sent the message, get the MimeMessage to store and put it in the right directory, and send a confirmation e-mail to the sender. We can assume that the sender is valid; if it weren't, the matcher would not have allowed the message through.

Note the way we deal with exceptions. The log() method is used to ensure that problems are reported, but you'll have to check the logs if you don't see the behavior you expected. Also, notice that we set the state for the e-mail to GHOST. This stops processing on the sent e-mail, because we'll be storing it ourselves and no additional action is required by the e-mail server.

Listing 5. UnavailableMessageSave: Saving messages sent to unavailable@emailserver
package com.claudeduguay.mailets;

import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;

public class UnavailableMessageSave
  extends UnavailableMessageBase
{
  protected String subject;
  protected String content;
  
  public void init(MailetConfig config)
    throws MessagingException
  {
    super.init(config);
    subject = getInitParameter("subject");
    content = getInitParameter("content");
  }
  
  public void service(Mail mail)
    throws MessagingException
  {
    MailAddress sender = mail.getSender();
    Collection recipients = mail.getRecipients();
    MailAddress address = getFirstAddress(recipients);
    MailetContext context = getMailetContext();
    try
    {
      MimeMessage msg = (MimeMessage)mail.getMessage();
      store.storeMessage(sender, msg);
      mail.setState(Mail.GHOST);
    }
    catch (IOException e)
    {
      log("Unable to store user message", e);
    }
    try
    {
      MimeMessage msg = createMessage(address, sender, subject, content);
      Collection target = addressAsCollection(sender);
      context.sendMail(address, target, msg);
    }
    catch (MessagingException e)
    {
      log("Unable to send confirmation message", e);
    }
  }
}

The next class to look at is the UnavailableMessageDrop mailet, shown in Listing 6. This mailet is very similar to UnavailableMessageSave, but it deletes the message before sending a confirmation rather than saving it, so it requires slightly less code. In both cases, we leverage the UnavailableStore class to access our unavailable message store.

Listing 6. UnavailableMessageDrop: Deleting the unavailable message
package com.claudeduguay.mailets;

import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;

public class UnavailableMessageDrop
  extends UnavailableMessageBase
{
  protected String subject;
  protected String content;
  
  public void init(MailetConfig config)
    throws MessagingException
  {
    super.init(config);
    subject = getInitParameter("subject");
    content = getInitParameter("content");
  }
  
  public void service(Mail mail)
    throws MessagingException
  {
    MailAddress sender = mail.getSender();
    Collection recipients = mail.getRecipients();
    MailAddress address = getFirstAddress(recipients);
    MailetContext context = getMailetContext();
    store.deleteMessage(sender);
    mail.setState(Mail.GHOST);
    try
    {
      MimeMessage msg = createMessage(address, sender, subject, content);
      Collection target = addressAsCollection(sender);
      context.sendMail(address, target, msg);
    }
    catch (MessagingException e)
    {
      log("Unable to send confirmation message", e);
    }
  }
}

The UnavailableMessageSend mailet, shown in Listing 7, is the most complicated, but even here you'll see that the Mailet API makes this kind of work relatively simple. The init() method supports a list of addresses that should be ignored when dealing with incoming messages to a given user. This is necessary to avoid loops that might occur when confirmation messages are sent to the user. In our configuration, we set two addresses -- one for the available state and one for the unavailable state -- to be ignored. I've made these settings configurable in case an administrator chooses to use different addresses. The list of addresses is tokenized from a semicolon-delimited string.

I've wrapped the getMessage() method from the UnavailableStore to log any problem retrieving messages, and added an isIgnorable() method to check whether a MailAddress is included in the ignore list. The bulk of the work is done in the service method.

Listing 7. UnavailableMessageSend: Sending the unavailable message to senders
package com.claudeduguay.mailets;

import java.io.*;
import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;
import org.apache.mailet.*;

public class UnavailableMessageSend
  extends UnavailableMessageBase
{
  protected MailAddress[] ignore;
    
  protected MimeMessage getMessage(MailAddress address)
  {
    try
    {
      return store.getMessage(address);
    }
    catch (Exception e)
    {
      log("Unable to read stored e-mail message: ", e);
    }
    return null;
  }

  public void init(MailetConfig config)
    throws MessagingException
  {
    super.init(config);
    String skip = getInitParameter("skip");
    StringTokenizer tokenizer = new StringTokenizer(skip, ";", false);
    int count = tokenizer.countTokens();
    ignore = new MailAddress[count];
    for (int i = 0; i < count; i++)
    {
      String address = tokenizer.nextToken().trim();
      ignore[i] = new MailAddress(address);
    }
  }
  
  protected boolean isIgnorable(MailAddress address)
  {
    for (int i = 0; i < ignore.length; i++)
    {
      if (address.equals(ignore[i]))
      {
        return true;
      }
    }
    return false;
  }
  
  public void service(Mail mail)
    throws MessagingException
  {
    MailAddress sender = mail.getSender();
    MailetContext context = getMailetContext();
    Collection recipients = mail.getRecipients();
    Iterator iterator = recipients.iterator();
    while (iterator.hasNext())
    {
      MailAddress address = (MailAddress)iterator.next();
      // If the recipient is unavailable, send the message.
      if (!isIgnorable(sender) && 
          store.userMessageExists(address))
      {
        Collection target = addressAsCollection(sender);
        MimeMessage msg = getMessage(address);
        if (msg != null)
        {
          try
          {
            msg = createMessage(address, sender, msg.getSubject(), 
              msg.getContent(), msg.getContentType());
            // Send e-mail from unavailable recipient, to sender
            context.sendMail(address, target, msg);
          }
          catch (IOException e)
          {
            log("Unable to construct new message: ", e);
          }
        }
      }
    }
  }
}

The UnavailableMessageSend service method needs to check all recipients for a match with local users who have stored an unavailable message. We use two methods to test each recipient. The isIgnorable() method is used to make sure the address is not ignorable, and the userMessageExists() method from the UnavailableStore class is used to check if this is a user with a stored unavailable message. If these conditions are met, we retrieve the unavailable message for that recipient, create a new message using the stored subject and content, and send it to the user who sent the initial message.


Building and deployment

Building the classes in this project is fairly simple, but deploying them is more complicated. In effect, we can build these classes easily, so long as JavaMail (mail.jar), the JavaBeans Activation Framework (activation.jar -- required by the JavaMail API), and the James package (james.jar) are on the classpath. To use the James JAR file, you have to get it out of the james.sar file, which you'll find in the james/apps directory. (For links to download all of these packages, see Resources. You can find more on installation in the first part of this series.)

A SAR file (Server Application Resource) is a Phoenix concept. You'll recall that James runs in the Phoenix server infrastructure, which is part of the Avalon project. (See Resources for more on Avalon.) Phoenix provides a common server framework for Java platform projects. A SAR file is a JAR file (which is in turn a zip file) with a specified structure. The james.jar you need is in the SAR-INF/lib subdirectory in the SAR archive file. You can use any suitable utility, such as WinZIP or the UNIX unzip utility, to get at it.

Unfortunately, the only viable way to add mailets to the James package is to reproduce the SAR file. This is awkward and much more complicated than it should be, but the James development team is aware of this inconvenience and intends to make it better. For now, however, we'll have to unzip the entire james.sar file somewhere so we can work with it, add our classes by putting them in the SAR-INF/lib directory (in JAR form as well) and change the James configuration file so that our code is properly recognized and used. Then we need to repackage the james.sar file. To avoid conflicts, we'll move the original james.jar file to a safe place and call ours james-plus.jar.

To add our mailets, we need to edit the config.xml configuration file, which is in the SAR-INF directory. We'll add two types of tags to the configuration file. The first set merely tells James which packages to look in for both matcher and mailet classes. In both cases, we extend the tags that are already there to reference the org.apache classes. Listing 8 illustrates our additions:

Listing 8. Matcher and mailet package declarations
<mailetpackages>
  <mailetpackage>org.apache.james.transport.mailets</mailetpackage>
  <mailetpackage>com.claudeduguay.mailets</mailetpackage>
</mailetpackages>
<matcherpackages>
  <matcherpackage>org.apache.james.transport.matchers</matcherpackage>
  <matcherpackage>com.claudeduguay.mailets</matcherpackage>
</matcherpackages>

With the class packages defined, the matchers and mailets can be found and we can configure the application, as shown in Listing 9:

Listing 9. Matcher and mailet process declarations
<mailet
  match="MatchSingleRecipient=unavailable@localhost"
  class="UnavailableMessageSave">
  <folder>d:/james/james-2.1.2/unavailable</folder>
  <subject>You have been marked as UNAVAILABLE</subject>
  <content>Send a message to available@localhost to reset.</content>
</mailet>
<mailet
  match="MatchSingleRecipient=available@localhost"
  class="UnavailableMessageDrop">
  <folder>d:/james/james-2.1.2/unavailable</folder>
  <subject>You have been marked as AVAILABLE</subject>
  <content>Send a message to unavailable@localhost to send the
    message to users when you are unavailable.</content>
</mailet>
<mailet
  match="MatchUnavailableUser=d:/james/james-2.1.2/unavailable"
  class="UnavailableMessageSend">
  <folder>d:/james/james-2.1.2/unavailable</folder>
  <skip>available@localhost;unavailable@localhost</skip>
</mailet>

If you've never used Ant, you may find the build script in Listing 10 unfamiliar. Ant is a build tool, also from the Apache group, that is now widely used for automating builds. We'll be using this tool to automate the extraction of the original SAR file, and to compile, rebuild, and deploy our application. If you're unfamiliar with Ant, you owe it to yourself to find out more about it. It is not only incredibly powerful, but is fully portable and a huge leap forward in build automation. To customize this build file, you can change the directories specified in the property tags. (The Resources section contains more information on Ant.)

Listing 10. Ant build script
<project name="unavailable" default="packageSAR">

  <property name="project.dir" value="d:/james" />
  <property name="javamail.dir" value="javamail-1.3" />
  <property name="james.dir" value="james-2.1.2" />
  <property name="jaf.dir" value="jaf-1.0.2" />
  <property name="temp.dir" value="temp" />
  
  <target name="init" >
    <echo>Project dir: ${project.dir}</echo>
    <echo>James dir: ${james.dir}</echo>
    <echo>JavaMail dir: ${javamail.dir}</echo>
    <echo>JAF dir: ${jaf.dir}</echo>
  </target>

  <target name="moveOriginalSAR" depends="init" >
    <move todir="${project.dir}">
      <fileset dir="${project.dir}/${james.dir}/apps" >
        <include name="james.sar" />
      </fileset>
    </move>
    <delete includeEmptyDirs="true" failonerror="false" >
      <fileset dir="${project.dir}/${james.dir}/apps/james" />
    </delete>
  </target>

  <target name="unzipSAR" depends="moveOriginalSAR" >
    <delete includeEmptyDirs="true" failonerror="false" >
      <fileset dir="${project.dir}/${temp.dir}" />
    </delete>
    <unjar
      src="${project.dir}/james.sar"
      dest="${project.dir}/${temp.dir}" />
  </target>
  
  <target name="compile" depends="unzipSAR" >
    <echo>Compiling</echo>
    <javac srcdir="." destdir=".">
      <classpath>
        <pathelement path="${project.dir}/${temp.dir}/SAR-INF/lib" />
      </classpath>
    </javac>
    <echo>Compiled</echo>
  </target>
  
  <target name="packageJAR" depends="compile" >
    <echo>Packaging</echo>
    <copy file="config.xml" todir="${project.dir}/${temp.dir}/SAR-INF" />
    <delete file="${project.dir}/${temp.dir}/SAR-INF/lib/unavailable.jar" />
    <jar jarfile="${project.dir}/${temp.dir}/SAR-INF/lib/unavailable.jar"
      basedir="." includes="com/claudeduguay/**/*.class" />
    <echo>Packaged</echo>
  </target>

  <target name="packageSAR" depends="packageJAR" >
    <delete includeEmptyDirs="true" failonerror="false" >
      <fileset dir="${project.dir}/${james.dir}/apps/james-plus" />
    </delete>
    <jar jarfile="${project.dir}/${james.dir}/apps/james-plus.sar"
      basedir="${project.dir}/${temp.dir}" includes="**/*" />
  </target>
  
</project>

Once the build is complete, we can start the James server by using the run scripts in the james/bin directory. If everything goes well, you'll see the same output we saw in Part 1 of the series, indicating that James is up and running.

Before we can test our application, we need to set up the red, green, and blue users. You can follow the instructions from the first article in this series to do this if you haven't already done so. After the users are set up, we're ready to proceed.


Simulating users

Once we have all our code running on the server, we need to apply a set of tests that will confirm that our application is running properly. The JamesApplicationTest class, shown in Listing 11, resembles the JamesConfigTest class developed in the first article in this series. It uses the MailClient class we developed then to send and retrieve e-mail messages.

How do we test our matchers and mailets? We start by clearing all messages for the red, green, and blue users. We then send a couple of messages to the blue user from red and green, and then have blue send an unavailable message. The next time blue checks for messages, he should see the original messages sent by red and green, along with a confirmation message from the unavailable user indicating that his message was properly saved and that he is now in the unavailable state. The blue user can continue operating normally, sending and receiving messages, but any user sending blue a message while he is in this state will receive the unavailable message stored by blue.

We can test what happens when a user is unavailable by sending a new message to blue. We do this using the green user, and then check green's inbox. The green user will receive an unavailable message from blue that reflects the content of the stored message that blue sent to the unavailable address. Finally, blue sends an available message and checks his e-mail to make sure that the confirmation message was sent by the available address.

Listing 11. JamesApplicationTest: Putting our matchers and mailets through the paces
public class JamesApplicationTest
{
  public static void main(String[] args)
    throws Exception
  {
    // CREATE CLIENT INSTANCES
    MailClient redClient = new MailClient("red", "localhost");
    MailClient greenClient = new MailClient("green", "localhost");
    MailClient blueClient = new MailClient("blue", "localhost");
    
    // CLEAR EVERYBODY'S INBOX
    redClient.checkInbox(MailClient.CLEAR_MESSAGES);
    greenClient.checkInbox(MailClient.CLEAR_MESSAGES);
    blueClient.checkInbox(MailClient.CLEAR_MESSAGES);
    Thread.sleep(500); // Let the server catch up
    
    // SEND A COUPLE OF MESSAGES TO BLUE (FROM RED AND GREEN)
    redClient.sendMessage(
      "blue@localhost",
      "Testing blue from red",
      "This is a test message");
    greenClient.sendMessage(
      "blue@localhost",
      "Testing blue from green",
      "This is a test message");
    
    // BLUE SENDS UNAVAILABLE MESSAGE
    blueClient.sendMessage(
      "unavailable@localhost",
      "On Vacation",
      "I am on vacation at the moment. " +
      "I will answer your mail as soon as I get back.");
    Thread.sleep(500); // Let the server catch up
    
    // LIST BLUE MESSAGES (RED, GREEN & UNAVAILABLE CONFIRMATION)
    blueClient.checkInbox(MailClient.SHOW_AND_CLEAR);
    
    // GREEN SENDS A NORMAL MESSAGE TO BLUE
    greenClient.sendMessage(
      "blue@localhost",
      "Testing blue from green",
      "This is a test message");
    Thread.sleep(500); // Let the server catch up
    
    // GREEN CHECKS MESSAGES (SHOULD SEE BLUE UNAVAILABLE MESSAGES)
    greenClient.checkInbox(MailClient.SHOW_AND_CLEAR);
    
    // BLUE SENDS MESSAGES TO BECOME AVAILABLE
    blueClient.sendMessage(
    "available@localhost",
    "Ignored subject",
    "Ignored content");
    Thread.sleep(500); // Let the server catch up
    
    // BLUE CHECKS MAIL, SHOULD SEE MESSAGE FROM GREEN AND AVAILABLE MSG
    blueClient.checkInbox(MailClient.SHOW_AND_CLEAR);
  }
}

The output of JamesApplicationTest should look like this:

Listing 12. JamesApplicationTest output
Clear INBOX for red@localhost

Clear INBOX for green@localhost

Clear INBOX for blue@localhost

SENDING message from red@localhost to blue@localhost

SENDING message from green@localhost to blue@localhost

SENDING message from blue@localhost to unavailable@localhost

Show and Clear INBOX for blue@localhost
    From: green@localhost
 Subject: Testing blue from green
 Content: This is a test message

    From: red@localhost
 Subject: Testing blue from red
 Content: This is a test message


SENDING message from green@localhost to blue@localhost

Show and Clear INBOX for green@localhost
    From: blue@localhost
 Subject: On Vacation
 Content: I am on vacation at the moment. I will answer your mail 
               as soon as I get back.


SENDING message from blue@localhost to available@localhost

Show and Clear INBOX for blue@localhost
    From: available@localhost
 Subject: You have been marked as AVAILABLE
 Content: Send a message to unavailable@localhost to send the 
               message to users when you are unavailable.

    From: green@localhost
 Subject: Testing blue from green
 Content: This is a test message

    From: unavailable@localhost
 Subject: You have been marked as UNAVAILABLE
 Content: Send a message to available@localhost to reset.

Summary

As you can see from the code presented in this article, working with James is fairly straightforward. The only complication is currently the deployment process, which the James team is working on. Given that e-mail is effectively the most used application on the Internet -- and is certainly more prevalent than Web services -- it's interesting to contemplate the possibilities made accessible by the James infrastructure. This approach is bound to solve important problems and is limited only by your imagination.


Download

DescriptionNameSize
Code samplej-james2.zip2KB

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10822
ArticleTitle=Working with James, Part 2: Build e-mail based applications with matchers and mailets
publish-date=06102003