Using Clojure to Connect to GMail via IMAP

I had to dig through quite a few docs to get a couple of raw emails from my IMAP account so I am posting the snippet for future reference. I have uploaded necessary jars to clojars under the group org.clojars.nakkaya.javax.mail , in case anyone wants to play with it.

 (ns gmail.core
   (:use clojure.contrib.java-utils)
   (:import (javax.mail Session Folder Flags)
            (javax.mail.search FlagTerm)
            (javax.mail Flags$Flag)))

 (defn store [protocol server user pass]
   (let [p (as-properties [["mail.store.protocol" protocol]])]
     (doto (.getStore (Session/getDefaultInstance p) protocol)
       (.connect server user pass))))

 (def gmail (store "imaps" "imap.gmail.com" 
                   "nurullah@nakkaya.com" "super_secret_pass"))

Opening a connection is quite simple, we ask for a session instance and connect to the store (IMAP server in JavaMail lingo).

 (defn folders 
   ([s] (folders s (.getDefaultFolder s)))
   ([s f]
      (let [sub? #(if (= 0 (bit-and (.getType %) 
                                    Folder/HOLDS_FOLDERS)) false true)]
        (map #(cons (.getName %) (if (sub? %) (folders s %))) (.list f)))))

Now that the connection to the store is ready, we need to get a list of folders (Labels in GMail), to get a list of folders, first get the users default folder, that will give us the top level folders we recursively check each folder for sub folders returning a list of all folders and subfolders.

 gmail.core=> (folders gmail)
 (("Fatura") ("INBOX") ("[Gmail]" ("Starred") ("Trash")) ("clojure") 
  ("compojure") ("firmata-devel") ("help-gnu-emacs") ("ikiteker") 
  ("incanter") ("java-dev") ("jna-users") ("leningen")
  ("metasploit") ("neurobotics") ("org-mode") ("pen-test") 
  ("ring-clojure"))

A folder name and a connection to the store is all that is needed to interact with messages.

 (defn messages [s fd & opt]
   (let [fd (doto (.getFolder s fd) (.open Folder/READ_ONLY))
         [flags set] opt
         msgs (if opt 
                (.search fd (FlagTerm. (Flags. flags) set)) 
                (.getMessages fd))]
     (map #(vector (.getUID fd %) %) msgs)))

Before interacting with a folder we need to open it, either in read only mode or read/write mode. Without the optional parameters, messages just returns a sequence of messages, optional parameters allows us to search for messages using flags such as seen, deleted etc.

 gmail.core=> (take 3 (messages gmail "INBOX"))

 ([8709 #<IMAPMessage com.sun.mail.imap.IMAPMessage@1320a41>] 
  [8712 #<IMAPMessage com.sun.mail.imap.IMAPMessage@3f4ebd>] 
  [8713 #<IMAPMessage com.sun.mail.imap.IMAPMessage@4a5c78>])

 gmail.core=> (messages gmail "clojure" Flags$Flag/SEEN false)

 ([11401 #<IMAPMessage com.sun.mail.imap.IMAPMessage@13b5a3a>] 
  [11402 #<IMAPMessage com.sun.mail.imap.IMAPMessage@1a0b53e>])

The whole reason, I came up with this snippet was to get a copy of raw messages, saved using their IMAP UID,

(defn dump [msgs]
  (doseq [[uid msg] msgs]
    (.writeTo msg (java.io.FileOutputStream. (str uid)))))

(dump (take 3 (messages gmail "INBOX")))