Introduction to Apache JAMES
By Nathan Tippy, OCI Senior Software Engineer
December 2004
Introduction
JAMES is short for the Java Apache Mail Enterprise Server. It is an easy to use Email based application platform implemented in 100% pure Java. With very little setup and configuration it can be used to meet any basic POP3, SMTP and NNTP needs one might have but it is capable of doing much more. Using customized components called Mailets and Matchers JAMES can host complex Email based applications.
As of this writing the latest release from apache is version 2.2.0. This article will demonstrate what JAMES can do "out of the box" and explore some of the possibilities that this framework provides the open-source community at large. There are many other features that will not be covered in order to remain brief. If you are interested in more details please visit the provided links.
Installation
Getting started with the environment is straight forward. The server will require JRE 1.3 or better installed and the James-2.2.0.zip
file expanded where it will be run. In order to run the example at the bottom of this article JRE 1.4 will be required. The Apache JAMES application is built upon Apache Avalon which among other benefits helps encourage inversion of control and separation of concerns in the implementation but adds some complexities that must be understood before we continue.
Upon startup Avalon will expand and deploy any SAR files found in the apps folder. SAR(Service Archive) files are analogous to WAR files used by web server platforms. They contain all the configuration and JAR files required for an application such as JAMES. If you look in the apps folder you will find the james.sar
file, do not modify this file, any needed modifications can be easily done after deployment. Use /bin/run.bat
to startup the server. If the server does not start up then there is probably another SMTP server running on socket 25 that will need to be stopped first.
Now that the server is running lets look at the directory structures imposed by the Avalon framework.
/James-2.2.0
/apps
Any sar files that will be deployed by Avalon eg. james.sar
/james
Generated when the james.sar file is deployed
/conf
The configuration of the built in features such as
fetch mail, list manager, and SQL database vendor
differences.
/logs
Dedicated log files for each of the core JAMES
components such as spoolmanager, mailstore, mailets,
remotemanager and many others. Often very helpful for
debugging. when they are enabled ;-)
/SAR-INF
Configuration files used by JAMES to load components
and enable logging levels. The config.xml file is the
most interesting because it defines everything needed
by the spool manager including the mailets. The
environment.xml file defines the logging configuration.
/lib
Custom mailets or matchers are stored here in jars.
They are loaded by a special class loader in JAMES
rather than the Avalon framework.
/classes
Same as the lib folder but classes are used instead
of jars.
/var
Folders for storing data in files if a database is not
being used.
/mail
message storage in folders defined in config.xml
/nntp
article storage in subfolders
/users
user storage
/bin
startup scripts run.bat, run.sh, and
wrapper.exe(for running as an NT service)
/conf
avalon phoenix startup configuration, also for NT service.
/docs
duplicate of the documentation found at james.apache.org.
/ext
avalon extension jars
/lib
apache avalon jars
/logs
phoenix kernel logs. May need to look here when debugging
if the specific logs do not indicate the problem.
Once the server is started the setup can easily be tested by making some fake accounts and sending a few messages between them. James supports remote maintenance which is enabled by default on port 4555. Use your favorite telnet client such as PuTTY for this. To connect with PuTTY enter the IP-address or localhost into the hostname field and enter 4555 into the port field. Ensure that the protocol is set to telnet and press open. A terminal window will open with the following:
JAMES Remote Administration Tool 2.2.0
Please enter your login and password
Login id:
The default id and password is root. These can be changed later by modifying the config.xml
file. Use the HELP command to list out all the commands available and use the ADDUSER command to add a pair of test users.
Add the test user accounts into a local install of Thunderbird or your favorite mail client. To add your test accounts into Thunderbird select Tools, Account Settings, and press the Add Account button. Select Email account and enter the test users name and then enter the Email address including @localhost
. Press Next and select POP3 at the top of the dialog and enter localhost as the incoming server. Continue to press next until the finished button appears. Press the finished button and repeat the same steps for any other test users. If the installation is set up properly Email messages will work between the test user accounts.
Testing is much easier if everything is running on the same machine. Because everything is local it is not necessary to setup a DNS server or MX entries. The mail client uses localhost as the mail server and the Email Addresses all look like USERNAME@localhost
. This only works because the senders, recipients, and the mail server are all local to the same machine. Normally, there would be one or more DNS servers available for validation and routing of outgoing remote Email. In order to receive email from remote machines there must be at least 1 MX record in the DNS entry for your domain. The MX record tells the other mail servers on the net where to route your mail based on the Email address domain.
Using the Provided Mailets and Matchers
JAMES comes with a significant number of Mailets and Matchers to be used in the spoolmanager. The spoolmanager represents the heart of the Email processing platform. It is defined in the config.xml
file and provides a mechanism for defining business rules using Mailets, Matchers and processes. All new Email will be spooled up and processed by the next available spoolmanager thread. The number of threads is configurable as well. Take a few minutes to open the config.xml
file and familiarize yourself with the xml structure. The file is well documented with CHECKME put in places that will most likely need modification after a fresh install.
Mailets are used to modify or act upon Email messages. A Mailet is very similar to a Servlet. It has familiar methods such as init, service and destroy which function in a similar manner. The spool manager is multi threaded so it's important to keep this in mind when implementing the service method. The Mailets will be loaded and initialized when the server starts. When mail is passed to the Mailet it can modify the Email or do other unrelated operations before passing it on. For example there are Mailets included with JAMES which add headers/footers to messages, map address aliases, or simply return the system time.
Matchers are used to filter or select specific Email messages. A Matcher will be used to direct Emails to a specific Mailet. Matchers are passed the entire Email message but should never modify it. The message is made available to the matcher for read only purposes so it can filter on any contrived criteria. The Matcher returns a collection of recipients that pass the criteria. If the criteria are not based upon any attributes of the recipients then the original collection of recipients or an empty collection should be returned. There are Matchers provided that can filter on message size, subject, recipient and many other criteria.
All processors are found inside the spoolmanager element of the config.xml
file. Each processor is made up of a list of Matchers each with its own Mailet. Processing will start with the first Matcher and continue until the end of the processor is reached or one of the Mailets modifies the Email state. The Email state holds the name of the processor by which the message is to be processed. There are 3 required processors that are used by the system, they are root, error and ghost. All new Email will start out in the root processor. If there are any errors processing a specific Email it will be sent to the error processor. The ghost processor is used to stop any further processing of the Email in question.
Note the following example:
<processor name="root">
<mailet match="RecipientIs=badboy@badhost"
class="ToProcessor">
<processor> spam </processor>
</mailet>
<!-- If the recipient is local, deliver it locally -->
<mailet match="RecipientIsLocal"
class="LocalDelivery"/>
</processor>
<processor name="spam">
<!-- To destroy all messages -->
<mailet match="All" class="Null"/>
</processor>
In the example above the ToProcessor
Mailet is given Email when the recipient is badboy@badhost. The ToProcessor
Mailet has one parameter defined in the xml. The processor element defines the new state for the Email. After this step any Email from the recipient in question will continue at the top of the spam processor. It is important to read the provided documentation because many of the Matchers by default will not change the Email state to ghost when you might expect it. This can be overridden by using the right parameter in most cases.
Using the included Matchers and Mailets one can easily setup mail forwarding and auto responders. On the JAMES site there are instructions for setting up mail lists. With a little imagination JAMES can be set up to do many other tasks by simply modifying the config.xml
file.
Configuration of Custom Features
There are many ideas for future Mailets listed on the JAMES wiki. Here are some more ideas.
Wrap a Mailet around Classifier4J or Naiban to do Bayesian spam filtering. These filters have become very popular over the last 3 years because they are relatively easy to setup and can give outstandingly accurate results.
Wrap a Mailet around Lucene to do keyword searching for Email or other files stored on your system. The results can be sent back as attachments for retrieval via Email. This might be a convenient way to access some of your personal files when you are not at home.
JAMES can be used as an application platform for agents who interact with users via Email. Using JAMES to process your SOAP messages more quickly is a great idea but this is only the beginning. Agents could be used to start all sorts of jobs like Ant tasks especially if they lend them selves to using Email as an interface.
Using Email as a secondary interface or even the only interface for an application may seem like a strange idea but there are many reasons for doing this. The first Killer-App on the internet was Email so today it is the most ubiquitous form of communication on the web. It should therefore come as no surprise that applications on the market today take advantage of this to inform users of various events. JAMES lets developers easily turn this into two way communication; when an event notification is delivered to the users in box they can immediately reply with new instructions for the agent. The convenience of this form of push notification, especially if the user has an Email enabled cell phone, makes it much more desirable than web based interfaces when it comes to timeliness.
To demonstrate how easily agents can be written an example has been provided. This agent manages a simple to-do list and satisfies the following requirements.
- Creation of new task list with optional administrator notification
- Target address will be todo@localhost
- Subject will be: new [task list name]
- Confirmation of new list is returned to the sending address.
- Confirmation will optionally be cc'd to the admin.
- New items are added by: open [priority] [task description]
- Update of task list with optional administrator notification.
- Change confirmation is returned to the sending address.
- Confirmation will optionally be cc'd to the admin.
- New items are added by: open [priority] [task description]
- Old items are removed by: close [task id]
- Query of task list.
- Target address will be todo@localhost
- Subject will be: query [task list name]
- Results will be sorted by priority
- Each task item will show up on a separate line
- Each line will look like: [task id] [priority] [task description]
This example has been simplified as much as possible to make the parts that directly relate to JAMES clear. For a full production quality application more work would need to be done; this has been left to the reader. One recommended addition would be the use of a database for storage.
The following 2 classes represent the business logic that was pulled out of the Matcher and Mailets to make them easier to understand. The TaskItem
class is a simple java bean which represents the to-do item with its priority. It implements the Comparable interface so we can generate a sorted list of items when a query is requested.
package com.ociweb.james.mailets;
public class TaskItem implements Comparable {
private String id;
private int priority;
private String description;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
public int compareTo(Object o) {
int oPriority = ((TaskItem)o).getPriority();
return (priority < oPriority ? -1 :
(priority==oPriority ? 0 : 1));
}
}
The TaskItemManager
class has a series of simple static methods that are mainly used for persistence and formatting. The most unusual of these methods is loadProperClassLoader
which must be called before using the XMLEncoder
or XMLDecoder
. The XML encoding/decoding classes are found in the 1.4 java runtime and are used for mapping java beans to xml. They were selected to help simplify the example but also to help us understand what is going on inside of JAMES.
When we deploy our application we will put these classes into a jar which will be in \james-2.2.0\apps\james\SAR-INF\lib
. JAMES uses its own class loader for these files because JAMES its self is loaded by Avalon which only knows how to deploy SAR files. If you have any code that will do dynamic loading of classes, you must be sure the right class loader is used. The loadProperClassLoader
method demonstrates an easy way to get the right one.
package com.ociweb.james.mailets;
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;
public class TaskItemManager {
private static int lastKey = 0;
/**
* Because James uses its own internal class loader on the
* mailets and matchers I must use the same one to allow
* XMLDecoder or XMLEncoder to find my classes
*/
private static void loadProperClassLoader() {
Thread cur = Thread.currentThread();
ClassLoader ccl = cur.getContextClassLoader();
ClassLoader classLoader = QueryTasks.class.getClassLoader();
cur.setContextClassLoader(classLoader);
}
public static List loadTasks(String filename)
throws FileNotFoundException {
File dataFile = new File(filename);
FileInputStream fist = new FileInputStream(filename);
BufferedInputStream bist = new BufferedInputStream(fist);
loadProperClassLoader();
XMLDecoder decoder = new XMLDecoder(bist);
Integer lastKey = (Integer) decoder.readObject();
TaskItemManager.lastKey = lastKey.intValue();
List tasks = (List) decoder.readObject();
decoder.close();
return tasks;
}
public static void saveTasks(List tasks, String filename)
throws FileNotFoundException {
FileOutputStream fost = new FileOutputStream(filename);
BufferedOutputStream bost = new BufferedOutputStream(fost);
loadProperClassLoader();
XMLEncoder encoder = new XMLEncoder(bost);
encoder.writeObject(new Integer(lastKey));
encoder.writeObject(tasks);
encoder.close();
return;
}
public static String getNextKey() {
String value = Integer.toHexString(++lastKey);
return "000000".substring(value.length()) + value;
}
/**
* file name and full path to be found in the james folder
* @param name
* @return
*/
public static String generateFilename(String name) {
return "../"+name+"_tasks.xml";
}
public static String readableTaskList(List tasks) {
StringBuffer textBuffer = new StringBuffer();
Collections.sort(tasks);
Iterator taskIdIterator = tasks.iterator();
while (taskIdIterator.hasNext()) {
TaskItem task=(TaskItem)taskIdIterator.next();
textBuffer.append(task.getId()).append(' ');
textBuffer.append(task.getPriority()).append(' ');
textBuffer.append(task.getDescription()).append("\n");
}
return textBuffer.toString();
}
}
Now we are ready to look at the Matcher and Mailets. To make development easier JAMES comes with 2 recommended base classes GenericMatcher
and GenericMailet
which greatly simplify the development of any new agents. In addition to the expected API functionality logging methods are provided by these base classes which output to the log folder as one might expect.
The CommandMatches
class will be used to detect incoming Email that looks like a new, update, or query command following our requirements above. For the update and query commands there is also an additional check to ensure that the task list of that name really does exist. The todo@localhost
filter will be done by one of the canned matchers so it is not implemented here.
package com.ociweb.james.matcher;
import java.io.File;
import java.util.Collection;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.apache.mailet.GenericMatcher;
import org.apache.mailet.Mail;
import com.ociweb.james.mailets.TaskItemManager;
public class CommandMatches extends GenericMatcher {
public Collection match(Mail aMail) throws MessagingException {
//get the message we will run our match logic against
MimeMessage message=aMail.getMessage();
//get the condition string which will be the command
String command=this.getCondition().toLowerCase();
try {
//get the subject line, one of many data points avalable.
String subject=message.getSubject().toLowerCase().trim();
//the recipient of this mail must be returned if it matches
Collection recipients = aMail.getRecipients();
//Check our criteria
// 1.it must start with the command given
// 2.have some extra characters to use as the key
// 3.there must only be 1 recipient
if ((subject.startsWith(command))&&
(subject.length()>command.length())&&
(recipients.size()==1)){
if(!command.equals("new")) {
//check to ensure this is a valid task list!
String filename =TaskItemManager.generateFilename(
subject.substring(command.length()).trim());
File dataFile=new File(filename);
return dataFile.isFile() ? recipients : null;
}
return recipients;
}
return null;
} catch (MessagingException e) {
//Unable to get any content from message
//Log the failure and return no matches
this.log("Unable to check for \""+command+
"\" command due to MessagingException. No matches were returned.",e);
return null;
}
}
}
The QueryTasks
class is used for generating an Email response with a list of tasks left to do sorted by priority. Generating a simple response message is done with the reply method on the mail message. By passing in false we are generating a reply to only the sender, true will generate a reply to all. This method will use some mapping rules (like using the respond-to field when available) and may not always populate the from and to addresses because there is no guarantee that the incoming Email will be properly formatted. Before returning the message it is necessary to check to ensure that these are not null and assign them more desirable values.
Once we have set the subject, text, from, and recipient fields the saveChanges
method must be called. This will ensure that header fields that may depend on the body length and encoding are properly set before transmission. The getMailContext
method is another benefit of using the provided generic base classes. It provides many helper methods for writing to log files, storing messages, and sending new messages. The sendMail
method will place the new message at the top of the root processor like it would for any other message coming in on socket 25. This must be kept in mind because it is very easy to cause an infinite loop if the outgoing message passes all the matchers that were used to select the original incoming message.
package com.ociweb.james.mailets;
import java.io.IOException;
import java.util.List;
import java.util.SortedMap;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
public class QueryTasks extends GenericMailet {
String subject;
public void init() throws MessagingException {
subject = "Task list for ";
}
public void service(Mail mail) throws MessagingException {
//generate reply message
MimeMessage response = (MimeMessage)mail.getMessage().reply(false);
String listName=mail.getMessage().getSubject().substring(5).trim();
response.setSubject(subject+listName);
try {
String filename=TaskItemManager.generateFilename(listName);
List tasks= TaskItemManager.loadTasks(filename);
response.setText(TaskItemManager.readableTaskList(tasks));
} catch (IOException e) {
String message="Unable to load task list because of an IO error";
response.setText(message+", please see the error logs.");
this.log(message ,e);
}
//because we may be using an email address that does not represent a
//real user the From address may be null when we generated the response.
//in this case we will populate it with the recipients address.
if (response.getFrom() == null) {
response.setFrom(((MailAddress)mail.getRecipients().
iterator().next()).toInternetAddress());
}
//if the to was not set because there was no response in the header then..
if (response.getRecipients(MimeMessage.RecipientType.TO).length==0) {
response.addRecipient(MimeMessage.RecipientType.TO,mail.getSender().
toInternetAddress());
}
//saving the response will commit all the changes and updates
//the approprate headder fields.
response.saveChanges();
//sendMail will insert this message into the incomming SMTP queue
getMailetContext().sendMail(response);
}
}
The UpdateTasks
class is used to modify the task list and return a confirmation response. Each line must be parsed to check for the open and close commands. Because many email clients will add >
or similar chars in front of lines when replying special logic was added to strip this off. This allows the user to get the response from a query tasklist Email and modify it with a simple reply by adding close
in front of the finished tasks and modifying the subject line to read update tasklist.
Although it's not strictly necessary the example code checks to ensure the mime type of the message body is text/plain. This is a good idea whenever you are processing mail because it helps reduce unexpected errors that may be difficult to debug.
The getInitParameter is used to retrieve any of the values found in child elements of the Mailet tag. For the UpdateTasks
class we are using to pass in the Email address of an administrator that should be notified every time the task list is updated. First we check to ensure it is not null and looks like an Email address then we add it to the recipients as a CC.
package com.ociweb.james.mailets;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.StringTokenizer;
import java.util.TreeMap;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
public class UpdateTasks extends GenericMailet {
String subject;
/**
* the init function is called once when the mailet is loaded.
*/
public void init() throws MessagingException {
subject = "Confirmation of Update ";
}
/**
* the service function is called as messages are passed in.
* this must be thread safe.
*/
public void service(Mail mail) throws MessagingException {
//generate reply message
MimeMessage response = (MimeMessage)mail.getMessage().reply(false);
String listname=null;
try {
String filename;
List tasks;
String subject=mail.getMessage().getSubject();
//start empty task list if this is new
if (subject.toLowerCase().startsWith("new")){
listname=subject.substring(3).trim();
filename=TaskItemManager.generateFilename(listname);
tasks=new ArrayList();
}
else {
listname=subject.substring(6).trim();
filename=TaskItemManager.generateFilename(listname);
tasks= TaskItemManager.loadTasks(filename);
}
//make sure this really a plain text message
if (mail.getMessage().isMimeType("text/plain")){
String content=(String)mail.getMessage().getContent();
//If anyone needs to be copied upon the list change then add them
String notification = getInitParameter("notificationAddress");
if ((notification!=null)&&
(notification.indexOf('@')>-1)) {
response.addRecipients(MimeMessage.RecipientType.CC, notification);
}
//Parse each line for updates
StringTokenizer body= new StringTokenizer(content,"\n\r");
while (body.hasMoreTokens()){
String line= body.nextToken().trim();
//trim off any < > or indent added by mail clients
while ((line.length()>0)&&(line.charAt(0)<'@'))
line=line.substring(1);
log("Update command: "+line);
if (line.toLowerCase().startsWith("open")) {
String temp = line.substring(4).trim();
int taskStart = temp.indexOf(' ');
String priority = temp.substring(0,taskStart);
String task = temp.substring(taskStart).trim();
TaskItem t = new TaskItem();
try {
t.setPriority(Integer.parseInt(priority));
t.setDescription(task);
t.setId(TaskItemManager.getNextKey());
tasks.add(t);
log("open task:"+t.getId());
} catch (NumberFormatException e) {
log("Invalid priority for task.",e);
}
}
else if (line.toLowerCase().startsWith("close")) {
String temp = line.substring(5).trim();
//get the key even if it is the only thing left on the line
String id=temp;
if (temp.length()>6)
id = temp.substring(0,6);
//remove this key from the task list
log("close task:"+id);
int index=tasks.size();
while (--index>=0) {
if ( ((TaskItem)tasks.get(index)).getId().equals(id) ) {
tasks.remove(index);
index=-1;
}
}
}
}
TaskItemManager.saveTasks(tasks, filename);
}
response.setText(TaskItemManager.readableTaskList(tasks));
} catch (IOException e) {
String message="Unable to load task list because of an IO error";
response.setText(message+", please see the error logs.");
this.log(message ,e);
}
response.setSubject(subject+listname);
//because we may be using an email address that does not represent a
//real user the From address may be null when we generated the response.
//in this case we will populate it with the recipients address.
if (response.getFrom() == null) {
response.setFrom(((MailAddress)mail.getRecipients().iterator().next()).
toInternetAddress());
}
//if the to was not set because there was not response in the header then..
if (response.getRecipients(MimeMessage.RecipientType.TO).length==0) {
response.addRecipient(MimeMessage.RecipientType.TO,mail.getSender().
toInternetAddress());
}
//saving the response will commit all the changes and updates
//the approprate headder fields.
response.saveChanges();
//sendMail will insert this message into the incomming SMTP queue
getMailetContext().sendMail(response);
}
}
To deploy our example the 4 classes above should be put into a jar in \james-2.2.0\apps\james\SAR-INF\lib
. It has already been mentioned that the todo@localhost
Email address was not created as a user via the remote administration interface. This would only be necessary if it was important to archive the incoming messages.
To assist in debugging any problems running the example the environment.xml file should be edited. Set the spoolmanager loglevel to DEBUG. The spoolmanager*.log
files in the log folder will contain all the init, usage, and shutdown details. The mailet*.log
files in the log folder will contain any of the logging done within the custom mailets.
The config.xml
file must be modified to reflect our new business rules. Add the name spaces of your custom mailets and matchers in to the mailetpackages
and matcherpackages
elements. This allows the processor xml to be much cleaner without cluttering it up with the much longer explicit class names.
Find the mailetpackages
tag and insert the following element.
<mailetpackage>com.ociweb.james.mailets</mailetpackage>
Find the matcherpackages
tag and insert the following element
<matcherpackage>com.ociweb.james.matcher</matcherpackage>
At the bottom of the root processor find the mailet sending all Email to the transport processor and, in front of it, add a filter to send any mail for todo@localhost
to the new taskAgent
.
<mailet match="RecipientIs=todo@localhost"
class="ToProcessor">
<processor> taskAgent </processor>
</mailet>
<mailet match="All" class="ToProcessor">
<processor> transport </processor>
</mailet>
After the close of the root processor add the following new taskAgent
processor. Be sure to replace the ADMIN EMAIL
string with a valid address that will get CC'd on any of the task list changes.
<processor name="taskAgent">
<mailet match="CommandMatches=new"
class="UpdateTasks">
<notificationAddress>ADMIN EMAIL</notificationAddress>
</mailet>
<mailet match="CommandMatches=query"
class="QueryTasks">
</mailet>
<mailet match="CommandMatches=update"
class="UpdateTasks">
</mailet>
</processor>
Stop and restart JAMES. The task agent is now ready for use. With your local email client you can test it by creating a new task list and modifying it. Try the following: (this assumes testuser is a valid local user)
To: todo@localhost
From: testuser@localhost
Subject: new mylist
Body: open 10 read a good book
Conclusion
JAMES has a lot of potential for future development. As you can see from the example it is very easy to leverage the framework to build custom agents. The project continues under active development so there are underpinnings that may change. However, my experience has been that each release remains backwards compatible, so upgrading should not present a problem.
Production installations of JAMES have been running since 1999 and most users consider it very stable and well tested. The next time you need to process incoming Emails in a project consider leveraging the JAMES platform. This will reduce development time while improving the reliability and dependability of your application.
References
- [1] JAMES
http://james.apache.org/ - [2] Avalon
http://avalon.apache.org/ - [3] Working with James
http://www-106.ibm.com/developerworks/java/library/j-james1.html - [4] Apache James 2.2 Released
http://www.theserverside.com/news/thread.tss?thread_id=27212
Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.