How to interact with Apache James in integration tests
At times, I have the need for my development testing to interact with an email server (e.g. testing cgi-bin scripts, JIRA mail plug-ins), to verify that emails are sent and received by either my components or external systems. I found that Apache James was quick to get up and running, with very little configuration.
Apache James is a open source Java implementation of a fully-featured mail server. It is component based, so you can attach or detach what ever parts you like (e.g. I am only interested in the SMTP and IMAP connectors), and for my purposes requires very little setup (although I wouldn't recommend using default settings in a permanent production environment).
Note that to use the default ports (SMTP: 25, IMAP: 143), you'll have to run as root on unix, or administrator on Windows.
James can automatically create a user account for the recipients(s) when receiving an email, if they don't already exist. So we are free to send emails to and check the accounts of which ever user we want, without a setup overhead!
Mail Client Utility
When the James server is running, a utility class will
help us to send emails and clear user's inboxes (this class based on Claude
Duguay's IBM
article).
The parent class javax.mail.Authenticator
helps us with the
javax.mail.Session
connection.
public class JamesMailClient extends Authenticator {
public static final int SHOW_MESSAGES = 1;
public static final int CLEAR_MESSAGES = 2;
public static final int SHOW_AND_CLEAR = SHOW_MESSAGES + CLEAR_MESSAGES;
private final Session _session;
private final PasswordAuthentication _passwordAuthentication;
private final String _userAddress;
private final String _user;
private final String _pass;
private final String _host;
public JamesMailClient(final String user, final String pass,
final String host, final boolean debug) {
_user = user;
_host = host;
_userAddress = _user + '@' + _host;
_pass = pass;
_passwordAuthentication = new PasswordAuthentication(user, _pass);
Properties props = new Properties();
props.put("mail.user", user);
props.put("mail.host", host);
props.put("mail.debug", debug ? "true" : "false");
props.put("mail.store.protocol", "imap");
props.put("mail.transport.protocol", "smtp");
_session = Session.getInstance(props, this);
}
public void sendMessage(final String recipientTo,
final String messageSubject, final String messageBody)
throws MessagingException {
Properties properties = new Properties();
properties.put("mail.smtp.host", _host);
properties.put("mail.smtp.port", "25");
properties.put("mail.smtp.username", _user);
properties.put("mail.smtp.password", _pass);
Session session = Session.getDefaultInstance(properties, null);
Message msg = new MimeMessage(session);
msg.addFrom(new Address[]{new InternetAddress(_userAddress)});
msg.setRecipients(Message.RecipientType.TO,
parse(recipientTo));
msg.setSubject(messageSubject);
msg.setText(messageBody);
Transport.send(msg);
}
public boolean waitForIncomingEmail(final long timeout,
final int emailCount)
throws MessagingException, InterruptedException {
Folder inbox = connect(Folder.READ_ONLY);
Message[] msgs = inbox.getMessages();
long t0 = System.currentTimeMillis();
while (msgs.length < emailCount) {
Thread.sleep(timeout / 10);
if ((System.currentTimeMillis() - t0) > timeout) {
return false;
}
msgs = inbox.getMessages();
}
disconnect(inbox, false);
return true;
}
public void checkInbox(final int mode)
throws MessagingException, IOException {
if (mode == 0) {
return;
}
boolean show = (mode & SHOW_MESSAGES) > 0;
boolean clear = (mode & CLEAR_MESSAGES) > 0;
String action = (show ? "Show" : "")
+ (show && clear ? " and " : "") + (clear ? "Clear" : "");
System.out.println(action + " INBOX for " + _userAddress);
Folder inbox = connect(Folder.READ_WRITE);
Message[] msgs = inbox.getMessages();
if (msgs.length == 0 && show) {
System.out.println("No messages in inbox");
} else {
System.out.println(msgs.length + " messages in inbox");
}
for (Message msg1 : msgs) {
MimeMessage msg = (MimeMessage) msg1;
if (show) {
System.out.println(" From: " + msg.getFrom()[0]);
System.out.println(" Subject: " + msg.getSubject());
System.out.println(" Content: " + msg.getContent());
}
if (clear) {
msg.setFlag(Flags.Flag.DELETED, true);
}
}
disconnect(inbox, true);
}
public Message getMessage(final int index) throws MessagingException {
Folder inbox = connect(Folder.READ_WRITE);
Message[] msgs = inbox.getMessages();
disconnect(inbox, false);
return msgs[index];
}
public int getMessageCount() throws MessagingException {
Folder inbox = connect(Folder.READ_ONLY);
Message[] msgs = inbox.getMessages();
disconnect(inbox, false);
return msgs.length;
}
public PasswordAuthentication getPasswordAuthentication() {
return _passwordAuthentication;
}
private Folder connect(final int accessType) throws MessagingException {
Store store = _session.getStore();
store.connect();
Folder root = store.getDefaultFolder();
Folder inbox = root.getFolder("inbox");
inbox.open(accessType);
return inbox;
}
private void disconnect(final Folder inbox, final boolean expunge)
throws MessagingException {
final Store store = inbox.getStore();
inbox.close(expunge);
store.close();
}
}
This utility class gives us the ability to send and receive messages, as well
as providing a method of pausing in a mailbox until a new message arrives.
You'll notice the constructor includes a username and password, although the
default behaviour of James when it receives an email for an account that
doesn't exist is to create a new account with a password that is the same as
the username. The host parameter will most likely be localhost
unless you've
deployed James to another server.
The waitForIncomingEmail
method allows us to pause execution while waiting
for an email to be received, useful if the next test stage requires an email
to be present in the inbox.
We can also clear out an inbox using checkInbox
with the constant
CLEAR_MESSAGES
.
Test class setup
In a JUnit test class, we can set up references to user
accounts using instances of JamesMailClient
and clear out the inboxes when
we've finished.
If the account doesn't exists (i.e. before you've sent an email to the account
or created it manually), a NoSuchProviderException
would be thrown when the
account is accessed in tearDown()
.
@Before
protected void setUp() {
_mailFooAccount = new JamesMailClient("foo", "foo", "localhost",
false);
_mailBarAccount = new JamesMailClient("bar", "bar", "localhost",
false);
}
@After
public void tearDown() {
try {
_mailFooAccount.checkInbox(JamesMailClient.CLEAR_MESSAGES);
_mailBarAccount.checkInbox(JamesMailClient.CLEAR_MESSAGES);
} catch (MessagingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
Our test harness is then ready for test cases to send and receive emails!
Test Case Usage
Using the functions in JamesMailClient
, this code shows
foo@localhost
sending an email to bar@localhost
, and bar
waiting to
receive it!
final String subject = "Neque porro quisquam dolor sit amet: "
+ System.currentTimeMillis();
final String body = "Lorem ipsum dolor sit amet, consectetur "
+ "adipiscing elit. Nullam a elit purus, eget "
+ "eleifend turpis. Suspendisse condimentum dictum.";
_mailFooAccount.sendMessage("bar@localhost", subject, body);
// Delay to allow for message delivery
_mailBarAccount.waitForIncomingEmail(1000, 1);
assertEquals(subject, _mailBarAccount.getMessage(0).getSubject);
Apache James & Maven
So far I've been simply running Apache James from the command line, but it does exist in the maven repository (http://repo1.maven.org/maven2/org/apache/james/). In the future I could be tempted to write a maven plugin or similar for it to launch for integration testing targets.. Stay tuned!