I’ve been playing around with Apache James, a complete email server written in Java, in the last few months and I’d like to share some of my thoughts about it. (I’ll focus on version 2.3.2 as the 3.0 version is still in beta.)
In short
Apache James is a flexible, highly configurable and extendable email server (SMTP, POP3, IMAP4 [since James 3.0]). It can store emails and users either in the file system or in a database (e.g. MySQL, PostgreSQL, and others) and handles emails for multiple domains via mappings.
Some insights
There is a comprehensive documentation at the James website including a wiki, so I will not go too much into detail here. Some of the server’s main concepts are storage, processor, matcher and mailet.
Storage
Both emails and users (and other configuration data) is persisted in the storage. James comes with a filesystem-based and a JDBC-based storage implementation, but other types of storage are possible too (e.g. LDAP, JPA and others, some of them will be provided with version 3.0).
Processor
When an incoming email is processed by the server it is injected into a so-called processor, which is a kind of queue consisting of actions to be performed on the email. Those actions are performed by the mailets.
Mailets
A mailet is similar to a servlet in a web container (like Tomcat), but it handles an incoming email instead of an HTTP request. James comes with many ready-to-go mailets, but it is very easy to implement own mailets to perform special tasks.
Matcher
A matcher decides if a a mailet will be executed for an incoming email. It checks if a condition is met and tells the email engine to execute the associated mailet.
Making it work all together
Sounds all a little bit confusing? Actually it isn’t. This is how everything works together:
- The email engine receives an incoming email, wrapping it internally with an container (which provides some helpful functions)
- The email engine forwards the the wrapped email to the processor named “root”
- The “root” processor consists of a sequence of mailets, each having a matcher associated.
- The mailet’s matcher is executed, returning whether or not the mailet should be executed.
- The matcher is then executed (or maybe not). It will “do something” with the email, e.g. a virus check, some validation, changing data, forwarding or rejecting or discarding the email, putting it into another processor, or doing some other action.
- If the email was not discarded, then the next mailet is processed, then the next, and so on.
Actually, there is a lot more going on internally, and in many points I oversimplified things here, but I just to give you a first impression about how James works.
Why is this so great (or useful)?
For me the most interesting point is that you can completely configure and extend the way how an incoming email is processed. You can directly interact with an incoming email: you can convert its content or forward it to a custom script or handle it any other way that you can imagine.
One example? Sure!
The following mailet adds support for an address mapping to receive emails with multiple address suffixes into one account: an email address like jcoder+projectone@example.com
will become jcoder@example.com
(and will be delivered to the account “jcoder” then.)
package me.jcoder.blog.examples.james.mailet;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import javax.mail.MessagingException;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
/**
* Mailet to support the "plus sign alias" syntax in addresses, so that
* name+something@domain becomes name@domain
* @author jCoder
*/
public class PlusAliasSupport extends GenericMailet {
private static final String MAILET_NAME = "PlusAliasSupport";
@Override
public void service(Mail mail) throws MessagingException {
if (mail == null) {
return;
}
try {
Collection recipients = mail.getRecipients();
Collection recipientsToRemove = new LinkedList();
Collection recipientsToAdd = new LinkedList();
for (Iterator itr = recipients.iterator(); itr.hasNext(); ) {
MailAddress address = (MailAddress)itr.next();
if (address == null) {
continue;
}
String localPartStr = address.getUser();
int plusIndex = -1;
if ((localPartStr != null) && ((plusIndex = localPartStr.indexOf('+')) > -1)) {
String newLocalPart = localPartStr.substring(0, plusIndex);
MailAddress replaceAddress = new MailAddress(newLocalPart, address.getHost());
recipientsToRemove.add(address);
recipientsToAdd.add(replaceAddress);
}
}
recipients.removeAll(recipientsToRemove);
recipients.addAll(recipientsToAdd);
} catch (Exception ex) {
}
}
@Override
public String getMailetName() {
return MAILET_NAME;
}
}
It’s pretty helpful to get the source code of James to take a lokk at how the provided mailets and matchers are implemented.
Some notes and tipps
When writing matchers and mailets or running James as your (test) email server, there might be some pitfalls, so I’ll share some of the points that I came across.
- When writing matchers and mailets you might want to download the Mailet SDK from the James website. This is generally a good idea, but make sure that you get the right version! James 2.3.2 normally uses the Mailet 2.3 API and the website provides the 2.4 API, so things will not work. If in doubt, add the mailet API-related JARs from a James installation.
- When placing the JAR into the
${JAMES_DIR}/apps/james/SAR_INF/lib
folder, don’t forget to add your mailet/matcher package name to the mailetpackages
/ matcherpackages
section of the config.xml
file so it can be found by the server.
- If you are going to use the JDBC-based storage, you need to put the JDBC driver for your database in a directory where the server can find it. It might not be best-practise, but I suggest to put the JDBC driver JAR into
${JAMES_DIR}/lib
where it is accessable by the main server.
- The same goes for using TLS/SSL. If you want to provide secure POP3 access, then you not only need a certificate (which is a whole other story to maybe being covered in an other posting), but you might run into some SSL-socket-related errors. One solution is to copy the
sunjce_provider.jar
from your Java runtime into ${JAMES_DIR}/lib
as well.
I hope that this posting gives you a first impression about the flexibility of the email server James. If you have any questions or ideas, feel free to leave a comment.