Showing posts with label imap. Show all posts
Showing posts with label imap. Show all posts

Wednesday, June 27, 2012

Calendaring and Email on Android with GCalendar and GMail

Its been a while since my last post, was busy completing the NLP and ML online Coursera courses. For what its worth, I loved the courses, and I think they are worth taking if you can make the necessary time/energy commitment (and if you want/need to know about this stuff, of course). The nice thing about both courses is that it provides you a broad (non industry specific) overview of the tools and techniques that are popular, and you can decide which ones you want to drill down on for your own needs. So its a bit easier than figuring out stuff from scratch on your own. But there is a time commitment required, and this is the first weekend in almost 4 months that I don't have homework to finish :-).

During this time, our phone contracts ran out, so my wife and I decided to upgrade from our Blackberries to Androids. Normally, I prefer to be waay behind the technology curve when it comes to stuff like this, but I had been wanting to get an Android for some time, and I figured its been around long enough for the obvious bugs to have been fixed. While that is true to some extent, I think that Android phones have still some ways to go compared to the Blackberry when it comes to the out-of-the-box experience with Email and Calendaring.

Its not as if my email/calendaring requirements are super special or anything. I have a personal account with Comcast that is served off a POP mail server and a work account served off an MS Exchange (IMAP) server. Android has a Mail app, which, like the Blackberry, results in two different Inboxes. Unlike the Blackberry, however, the Android Mail app has no support for Calendar Event reminders, which I have grown quite accustomed to.

Reading through various forums on the Internet, I found a setup, based on Google Calendar (GCalendar) and Google Mail (GMail) that was being used by some people who had similar requirements as mine. The diagram below shows the setup.


Mail Setup

GMail provides a way to pull in email from POP accounts, so I used that to pull down the emails from my personal mailbox on my ISP. Since GMail would be set up as an IMAP server, there was no need to keep email on the POP server after downloading.

GMail does not provide a way to pull emails from IMAP accounts such as Microsoft Exchange server that hosts my work email account. Based on the advice on the various forums I set up fetchmail on my work computer to fetch the work email at regular intervals and forward them to GMail. However, that has a side effect of marking all the fetched mail as read. Since the read/unread marker is a major navigation cue for me, this was unacceptable.

Unfortunately fetchmail does not offer a way to control this behavior, so I decided to write my own little Python script to pull emails at regular intervals from my Exchange server and forward to GMail. Before each run, the script reads the last UID (unique ID) retrieved by the previous invocation and retrieves mail that has a UID higher than that one. At the end of the run, it writes back the highest UID fetched into the file. Here is the script.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#!/usr/local/bin/python
# Runs via cron every 10 minutes (crontab entry below)
# */10 * * * * /home/spal/bin/copymail.py
#
import email
import imaplib
import smtplib
import traceback

# the file below contains the latest UID from previous run
TS_FILE = "/home/spal/.android/copymail.ts"
# configuration file, contains values for
# LOCAL_IMAP_HOST = host name of Exchange IMAP server.
# LOCAL_IMAP_USER = user name on Exchange IMAP server.
# LOCAL_IMAP_PASS = password on Exchange IMAP server.
# LOCAL_IMAP_INBOX = name of main Inbox directory on IMAP
# LOCAL_IMAP_EMAIL = source email address of IMAP
# SMTP_FORWARD_HOST = localhost (I had sendmail running)
# TARGET_EMAIL_ADDRESS = target email to forward to
CF_FILE = "/home/spal/.android/copymail.cf"

def get_conf():
  f = open(CF_FILE, 'r')
  conf = {}
  for line in f:
    if len(line.strip()) == 0:
      continue
    [key, value] = line[:-1].split("=")
    conf[key] = value
  f.close()
  return conf

def read_uid():
  f = open(TS_FILE, 'r')
  ts = f.read()
  f.close()
  return ts

def write_uid(uid):
  f = open(TS_FILE, 'w')
  f.write(uid)
  f.close()

def main():
  conf = get_conf()
  mail = imaplib.IMAP4(conf["LOCAL_IMAP_HOST"])
  mail.login(conf["LOCAL_IMAP_USER"], conf["LOCAL_IMAP_PASS"]) 
  mail.select(conf["LOCAL_IMAP_INBOX"])
  result, data = mail.uid("search", None, "ALL")
  avail_uids = data[0].split()
  uid = read_uid()
  smtp = smtplib.SMTP(conf["SMTP_FORWARD_HOST"])
  for avail_uid in avail_uids:
    if int(avail_uid) > int(uid):
      result, data = mail.uid("fetch", avail_uid, "(RFC822)") 
      raw_email = data[0][1]  
      email_message = email.message_from_string(raw_email)
      from_name, from_addr = email.utils.parseaddr(email_message["From"])
      try:
        smtp.sendmail(from_addr, [conf["TARGET_EMAIL_ADDRESS"]], \
          raw_email.encode("ascii", "replace"))
      except smtplib.SMTPException:
        print "error: unable to send mail"
        print traceback.print_exc()
  write_uid(avail_uids[-1])

if __name__ == "__main__":
  main()

The script is run via cron every 10 minutes (crontab entry is available in the comments of the script above.

On the GMail side, we create a label "WORK" which is applied to the forwarded emails via a custom filter (which we also create). My custom filter sets the label to "WORK" when the To: address contains *@healthline.com. Not perfect, but I rarely get personal emails from work (colleagues who do email me on my personal account tend to use their personal accounts as well), so it works for me.

Since our GMail account uses IMAP, I can access my email from either my phone or notebook without worrying about email loss.

Calendar Setup

For the Calendaring, I initially tried using GCalDaemon, as advised on the forums, but I just could not get it to work. Apparently I had several unparseable (to GCalendar) Calendar events which caused GCalendar to hit its daily maximum limit. After some attempts, I just gave up and decided to write another script similar to the one above, that would periodically read my Evolution Calendar (.ics) file and push these events over to GCalendar via the GData API. Here is the script.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/usr/local/bin/python
# Called by cron every 6 hours. Crontab entry:
# 0 0,6,12,18 * * * /home/spal/bin/copycal.py
#
import atom
import atom.service
from datetime import date
from datetime import datetime
from datetime import time
import gdata.calendar
import gdata.calendar.service
import gdata.service
from icalendar import Calendar

# Contains the timestamp from previous run.
TS_FILE = "/home/spal/.android/copycal.ts"
# Configuration parameters.
# CALENDAR_FILE = /full/path/to/evolution/calendar.ics
# GOOGLE_CALENDAR_EMAIL = GMail email
# GOOGLE_CALENDAR_PASSWORD = Google Password
# GOOGLE_CALENDAR_CLIENT_STR = some random client string
# GOOGLE_CALENDAR_FEED_URL = /calendar/feeds/default/private/full
CF_FILE = "/home/spal/.android/copycal.cf"

def str_to_dttime(s):
  d = date(int(s[0:4]), int(s[4:6]), int(s[6:8]))
  t = time(int(s[9:11]), int(s[11:13]), int(s[13:15]))
  return datetime.combine(d, t)

def dttime_to_str(dt):
  return dt.strftime("%Y%m%dT%H%m%S")

def dttime_to_iso_zulu(dt):
  return dt.strftime("%Y-%m-%dT%H:%M:%S.000Z")

def read_timestamp():
  f = open(TS_FILE, 'r')
  ts = f.read()
  f.close()
  return ts

def write_timestamp(ts):
  f = open(TS_FILE, 'w')
  f.write(dttime_to_str(ts))
  f.close()

def get_conf():
  f = open(CF_FILE, 'r')
  conf = {}
  for line in f:
    if len(line.strip()) == 0:
      continue
    [key, value] = line[:-1].split("=")
    conf[key] = value
  f.close()
  return conf

def main():
  # read start timestamp from file and update with end timestamp
  # for the next run
  start_ds = str_to_dttime(read_timestamp())
  end_ds = datetime.now()
  write_timestamp(end_ds)
  end_dttime = dttime_to_str(end_ds)
  # read configuration parameters
  conf = get_conf()
  # login to google calendar
  gcalendar = gdata.calendar.service.CalendarService()
  gcalendar.email = conf["GOOGLE_CALENDAR_EMAIL"]
  gcalendar.password = conf["GOOGLE_CALENDAR_PASSWORD"]
  gcalendar.source = conf["GOOGLE_CALENDAR_CLIENT_STR"]
  gcalendar.ProgrammaticLogin()
  # read calendar.ics file from Evolution
  cal = Calendar.from_ical(open(conf["CALENDAR_FILE"], 'rb').read())
  for event in cal.walk(name="vevent"):
    created_ds = str_to_dttime(event.get("created").to_ical())
    if created_ds < start_ds or created_ds > end_ds:
      continue
    start = event.get("dtstart").to_ical()
    end = event.get("dtend").to_ical()
    summary = event.get("summary")
    if summary == None:
      summary = "None"
    description = event.get("description")
    if description == None:
      description = "None"
    location = event.get("location")
    if location == None:
      location = "None"
    # create gdata event and populate
    gevent = gdata.calendar.CalendarEventEntry()
    gevent.title = atom.Title(text=summary.encode("utf-8"))
    gevent.content = atom.Content(text=description.encode("utf-8"))
    gevent.where.append(gdata.calendar.Where(
      value_string=location.encode("utf-8")))
    rrule = event.get("rrule")
    if rrule != None:
      rrule = rrule.to_ical();
      recurrence_data = "DTSTART;VALUE=DATE:" + start + "\r\n" + \
        "DTEND;VALUE=DATE:" + end + "\r\n" + \
        "RRULE:" + rrule + "\r\n"
      gevent.recurrence = gdata.calendar.Recurrence(text=recurrence_data)
    else:
      start_z = dttime_to_iso_zulu(str_to_dttime(start))
      end_z = dttime_to_iso_zulu(str_to_dttime(end))
      gevent.when.append(gdata.calendar.When(
        start_time=start_z, end_time=end_z))
    gcalendar.InsertEvent(gevent, conf["GOOGLE_CALENDAR_FEED_URL"])
    print "Inserted event: %s (%s-%s)" % (summary, start, end)

if __name__ == "__main__":
  main()

On the GCalendar side, I set up notifications for every calendar event so that it sends me an SMS 5 minutes before the event.

And thats pretty much it. Took me couple of weeks of reading, fiddling and coding to get this done, but now I have Email and Calendaring on my Android that are as good as on the Blackberry.

Saturday, December 09, 2006

Mock objects for Javamail Unit tests

I recently got to use Javamail for the very first time. Javamail provides the Java application developer with a convenient abstraction to send and receive email. My application would use email as an asynchronous remote invocation mechanism. It would need to read some mail from a specified IMAP mailbox, parse and process it, then send a confirmation email that the requested operation succeeded (or failed). However, I noticed that applications of this nature pose certain problems during development.

  • You need to be always connected to the SMTP, IMAP or POP servers that you are developing your application to talk to. This is not a tall order most of the time, except when you are developing code at a location outside the company firewall, or not connected to the Internet at all.
  • You may be working with database dumps of real email addresses, so you may end up inadverdently sending mail to real people during development, something that usually looks bad for you and your company. You can usually get around this by post-processing the email addresses to fake ones in the dev server, or by restricting the servers to within a certain network.

Both problems can be addressed by a mock framework that would allow Javamail to "send" messages to an in-memory data structure which can be queried by other Javamail components as they "receive" messages from it. That way, you can develop and test the code in offline mode, and also write round-trip integration tests without actually connecting to any real SMTP or IMAP servers. I searched around for something like this for a while (see resources), but could not find one that would meet all my requirements, so I decided to build one, which is described here.

Extension point

Central to Javamail is the Session object. It is a factory that returns an implementation of Transport (for SMTP), or Store and Folder (for IMAP and POP). It is final, so subclassing is not a viable approach to mocking it. What we can do is override the implementations it returns by specifying property file overrides. The property file overrides should be located in a META-INF directory in your classpath. Since mocks are only used during testing, I decided to locate them under the src/test/resources/META-INF directory of my Maven app.

1
2
3
4
5
6
7
8
# src/test/resources/META-INF/javamail.default.providers
# Specifies the mock implementations that would be returned by Session
protocol=smtp; type=transport; class=com.mycompany.smail.mocks.MockTransport; vendor=Smail, Inc.;
protocol=imap; type=store; class=com.mycompany.smail.mocks.MockImapStore; vendor=Smail, Inc.;

# src/test/resources/META-INF/javamail.default.address.map
# RFC-822 docs need to use the SMTP protocol
rfc822=smtp

Since the src/test/resources directory is in the test classpath, the following calls will now return our MockTransport and MockImapStore instead of the default Javamail implementations when we invoke the following calls from JUnit tests.

1
2
3
4
5
6
  // SMTP
  Transport transport = Session.getTransport("smtp"); // returns MockTransport

  // IMAP
  Store store = Session.getStore("imap");     // returns MockImapStore
  Folder folder = store.getFolder("INBOX");   // returns the only MockFolder

The Mock Message Store

For the mock message store which the MockTransport would write to and the MockImapStore would read from, I envisioned a Map of email address to List of MimeMessages. The email address would be the owner of the mailbox when reading (or the recipient address when writing a MimeMessage object). It would be a singleton with static methods which would be called by the MockTransport and MockImapStore and would have some dump methods to allow the developer to dump the object within the JUnit test. It is shown below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// Messages.java
public class Messages {

  public static Map<String,List<MimeMessage>> messages = new HashMap<String,List<MimeMessage>>();

  public static void addMessage(String toEmail, MimeMessage message) {
    List<MimeMessage> messagesForUser = messages.get(toEmail);
    if (messagesForUser == null) {
      messagesForUser = new ArrayList<MimeMessage>();
    }
    messagesForUser.add(message);
    messages.put(toEmail, messagesForUser);
  }

  public static List<MimeMessage> getMessages(String toEmail) {
    List<MimeMessage> messagesForUser = messages.get(toEmail);
    if (messagesForUser == null) {
      return new ArrayList<MimeMessage>();
    } else {
      return messagesForUser;
    }
  }

  public static void reset() throws Exception {
    messages.clear();
  }

  /**
   * Dumps the contents of the Messages data structure for the current run.
   * @return the string representation of the Messages structure.
   * @throws Exception if one is thrown.
   */
  public static String dumpAllMailboxes() throws Exception {
    StringBuilder builder = new StringBuilder();
    builder.append("{\n");
    for (String email : messages.keySet()) {
      builder.append(dumpMailbox(email)).append(",\n");
    }
    builder.append("}\n");
    return builder.toString();
  }

  /**
   * Dumps the contents of a single Mailbox.
   * @param ownerEmail the owner of the mailbox.
   * @return the string representation of the Mailbox.
   * @throws Exception if one is thrown.
   */
  public static String dumpMailbox(String ownerEmail) throws Exception {
    StringBuilder mailboxBuilder = new StringBuilder();
    List<MimeMessage> messagesForThisUser = messages.get(ownerEmail);
    mailboxBuilder.append(ownerEmail).append(":[\n");
    for (MimeMessage message : messagesForThisUser) {
      mailboxBuilder.append(stringifyMimeMessage(message));
    }
    mailboxBuilder.append("],\n");
    return mailboxBuilder.toString();
  }

  /**
   * Custom stringification method for a given MimeMessage object. This is
   * incomplete, more details can be added, but this is all I needed.
   * @param message the MimeMessage to stringify.
   * @return the stringified MimeMessage.
   */
  public static String stringifyMimeMessage(MimeMessage message) throws Exception {
    StringBuilder messageBuilder = new StringBuilder();
    messageBuilder.append("From:").append(message.getFrom()[0].toString()).append("\n");
    messageBuilder.append("To:").append(message.getRecipients(RecipientType.TO)[0].toString()).append("\n");
    for (Enumeration<Header> e = message.getAllHeaders(); e.hasMoreElements();) {
      Header header = e.nextElement();
      messageBuilder.append("Header:").append(header.getName()).append("=").append(header.getValue()).append("\n");
    }
    messageBuilder.append("Subject:").append(message.getSubject()).append("\n");    messageBuilder.append(message.getContent() == null ? "No content" : message.getContent().toString());
    return messageBuilder.toString();
  }
}

Mocking SMTP

Only Transport needs to be mocked for SMTP. This implementation is almost totally copied from Bill Dudney's blog entry (referenced in resources), replacing System.out.println() calls with LOGGER.debug() calls.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// MockTransport.java
public class MockTransport extends Transport {

  private static final Logger LOGGER = Logger.getLogger(MockTransport.class);

  public MockTransport(Session session, URLName urlName) {
    super(session, urlName);
  }

  @Override
  public void connect() throws MessagingException {
    LOGGER.info("Connecting to MockTransport:connect()");
  }

  @Override
  public void connect(String host, int port, String username, String password) throws MessagingException {
    LOGGER.info("Connecting to MockTransport:connect(String " + host + ", int " + port + ", String " + username + ", String " + password + ")");
  }

  @Override
  public void connect(String host, String username, String password) throws MessagingException {
    LOGGER.info("Connecting to MockTransport:connect(String " + host + ", String " + username + ", String " + password + ")");
  }

  @Override
  public void sendMessage(Message message, Address[] addresses) throws MessagingException {
    System.err.println("Sending message '" + message.getSubject() + "'");
    for (Address address : addresses) {
      Messages.addMessage(address.toString(), (MimeMessage) message);
    }
  }

  @Override
  public void close() {
    LOGGER.info("Closing MockTransport:close()");
  }
}

Mocking IMAP

For IMAP, we need to provide mock implementations for both Store and Folder. Most of the methods in my case are unsupported, but those that are work against the Messages object. Here they are:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
// MockImapStore.java
public class MockImapStore extends Store {

  private static final Logger LOGGER = Logger.getLogger(MockImapStore.class);

  private String ownerEmail;
  private MockFolder folder;

  public MockImapStore(Session session, URLName urlName) {
    super(session, urlName);
  }

  public String getOwnerEmail() {
    return ownerEmail;
  }

  @Override
  public void connect(String host, int port, String username, String password) {    this.ownerEmail = buildOwnerEmail(host, username);
    this.folder = new MockFolder(this);
    LOGGER.debug("MockImapStore:connect(String " + host + ", int " + port + ", String " + username + ", String " + password + ")");
  }

  @Override
  public Folder getFolder(String folderName) throws MessagingException {
    return getDefaultFolder();
  }

  @Override
  public Folder getDefaultFolder() throws MessagingException {
    return folder;
  }

  @Override
  public Folder getFolder(URLName urlName) throws MessagingException {
    return getDefaultFolder();
  }

  @Override
  public void close() {
    LOGGER.info("MockImapStore.close()");
  }

  /**
   * Converts user at mail.host.com to user@host.com
   * @param host the hostname of the mail server.
   * @param username the username that is used to connect.
   * @return the email address.
   */
  private String buildOwnerEmail(String host, String username) {
    return StringUtils.join(new String[] {
      username,
      StringUtils.join(ArrayUtils.subarray(host.split("\\."), 1, 3), ".")}, "@");
  }

}

// MockFolder.java
public class MockFolder extends Folder {

  private static final Logger LOGGER = Logger.getLogger(MockFolder.class);

  private Store store;
  private List<MimeMessage> messagesInFolder;

  public MockFolder(Store store) {
    super(store);
    this.store = store;
  }

  @Override
  public void open(int mode) throws MessagingException {
    String owner = ((MockImapStore) store).getOwnerEmail();
    LOGGER.debug("MockFolder.open(int " + mode + "), owner=" + owner);
    this.messagesInFolder = Messages.getMessages(owner);
  }

  @Override
  public Message[] getMessages() throws MessagingException {
    return messagesInFolder.toArray(new Message[0]);
  }

  @Override
  public Message[] expunge() throws MessagingException {
    return new Message[0];
  }

  @Override
  public void close(boolean expunge) throws MessagingException {
    LOGGER.debug("MockFolder.close(boolean " + expunge + ")");
  }

  @Override
  public Message getMessage(int index) throws MessagingException {
    try {
      return messagesInFolder.get(index);
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new MessagingException(e.getMessage());
    }
  }

  @Override
  public int getMessageCount() throws MessagingException {
    return messagesInFolder.size();
  }

  @Override
  public int getType() throws MessagingException {
    return Folder.HOLDS_MESSAGES;
  }

  @Override
  public boolean hasNewMessages() throws MessagingException {
    return (messagesInFolder.size() > 0);
  }

  @Override
  public boolean isOpen() {
    return (((MockImapStore) getStore()).getOwnerEmail() != null);
  }

  @Override
  public Folder[] list(String arg0) throws MessagingException {
    return new Folder[] {this};
  }

  @Override
  public void appendMessages(Message[] messages) throws MessagingException {
    this.messagesInFolder.addAll(Arrays.asList((MimeMessage[]) messages));
  }

  @Override
  public boolean exists() throws MessagingException {
    return true;
  }

  @Override
  public Folder getFolder(String folderName) throws MessagingException {
    return this;
  }

  @Override
  public String getFullName() {
    return "INBOX";
  }

  @Override
  public String getName() {
    return "INBOX";
  }

  @Override
  public boolean create(int type) throws MessagingException {
    throw new UnsupportedOperationException("MockFolder.create(int) not supported");
  }

  @Override
  public boolean delete(boolean recurse) throws MessagingException {
    throw new UnsupportedOperationException("MockFolder.delete(boolean) not supported");
  }

  @Override
  public Folder getParent() throws MessagingException {
    throw new UnsupportedOperationException("MockFolder.getParent() not supported");
  }

  @Override
  public Flags getPermanentFlags() {
    throw new UnsupportedOperationException("MockFolder.getPermanentFlags() not supported");
  }

  @Override
  public char getSeparator() throws MessagingException {
    throw new UnsupportedOperationException("MockFolder.getSeparator() not supported");
  }

  @Override
  public boolean renameTo(Folder newFolder) throws MessagingException {
    throw new UnsupportedOperationException("MockFolder.renameTo() not supported");
  }

}

Calling code

Once the mock objects are in place, the calling code will run unchanged against the mock objects as long as the property override properties files are visible in our classpath. In our case, our JUnit test code (under src/test/java) will automatically use the mock objects, while our production code (under src/main/java) will use the Sun implementations to connect to the real servers.

Resources

  • jGuru's Java Mail Tutorial on the Sun Developer Network - A very quick but comprehensive overview of the Javamail API. Takes about 15 minutes or so to read and contains code snippets you can use to quickstart your Javamail based app.
  • Bill Dudney's "Mocking Javamail" blog entry - this actually got me started on my own mock objects for Javamail unit testing. However, my needs went beyond just sending mail, so I had a little more work to do. However, my Mock Transport implementation (described above) started out as a direct cut-and-paste from the code shown in this entry.
  • Dumbster is a fake SMTP server, which stores messages sent to it in an in-memory data structure similar to the mock implementation described here. I considered using this for a while, but I needed something that would work with Javamail and have support for a mock IMAP store. Dumbster, as far as I know, does not work with Javamail, and it definitely does not have support for POP or IMAP.