Yet Another Disposable E-Mail Web Application in Clojure

This post walks you through the process of creating a disposable e-mail web application in Clojure. I've moved my site to a new provider in Germany which gave me a box with 32 GB of memory, while thinking what to do with all that memory I thought why not a disposable email service, I can keep everything in memory since accounts will be active for a short amount of time and be done in an afternoon this was before I actually made a google search and found out that there a lot of them in production. I scrapped the idea, following is the meat of the code if anyone is interested.

Fractal Fern Explained

(def inboxes (ref {}))

(defn rand-addr []
  (apply str (shuffle 
              (concat (take 2 (repeatedly #(rand-int 10)))
                      (take 2 (repeatedly #(char (+ 97 (rand-int 26)))))
                      (take 2 (repeatedly #(char (+ 65 (rand-int 26)))))))))

(defn new-inbox []
  (let [addr (str (rand-addr) "@nakkaya.com")]
    (dosync (alter inboxes assoc addr {:created (System/currentTimeMillis)
                                       :messages []}))
    addr))

All mailboxes are kept in a reference, we create a new mailbox by taking 6 random alphanumeric characters shuffling them and adding them to list of mailboxes along with its creation time.

(defn inbox-expired? [addr]
  (let [created ((@inboxes addr) :created)
        now (System/currentTimeMillis)]
    (if (> (- now created)  (* 15 60 1000))
      true
      false)))

(defn delete-inbox [mailbox]
  (dosync (alter inboxes dissoc mailbox)))

(defn watch-inboxes []
  (future
    (while true
      (doseq [mailbox (keys @inboxes)]
        (when (inbox-expired? mailbox)
          (delete-inbox mailbox)))
      (Thread/sleep 100))))

Periodically using a future, we go over the list of active mailboxes check if any of them expired. A mailbox expires 15 minutes after its creation.

(defn inbox-active? [addr]
  (contains? @inboxes addr))

(defn process-message [from to message]
  (let [message-list ((@inboxes to) :messages)]
    (dosync (alter inboxes assoc-in
                   [to :messages]
                   (conj message-list {:from from
                                       :to to
                                       :time (System/currentTimeMillis)
                                       :subject (.getSubject message)
                                       :content (.getContent message)})))))

(defn message-listener []
  (proxy [SimpleMessageListener] []
    (accept [from to]
      (inbox-active? to))

    (deliver [from to data]
      (process-message from to
                       (javax.mail.internet.MimeMessage.
                        (javax.mail.Session/getDefaultInstance
                         (java.util.Properties.)) data)))))

(def smtp-server (org.subethamail.smtp.server.SMTPServer.
                  (org.subethamail.smtp.helper.SimpleMessageListenerAdapter.
                   (message-listener))))

For receiving mail we rely on SubEtha SMTP which is a Java library that allows your application to receive SMTP mail. SimpleMessageListener will call accept when a mail arrives if the mail destined to an active inbox it will return true which causes deliver to be called giving us a InputStream to the message, we then convert it to a MimeMessage extract the parts we are interested in and add it to the list of messages for the mailbox.

(defroutes app-routes
  (GET "/" {session :session}
       (if (and (contains? session :mailbox)
                (inbox-active? (:mailbox session)))
         (template (show-inbox (:mailbox session)))
         (assoc (redirect "/") :session {:mailbox (new-inbox)})))

  (GET "/view/:mailbox/:idx" [mailbox idx]
       (template (view-message mailbox idx)))

  (POST "/reply" [from to subject reply]
        (reply-message from to subject reply)
        (redirect "/"))

  (route/not-found "<h1>Page not found</h1>"))

(def app (-> app-routes
             wrap-params
             (wrap-session {:cookie-name "mail-session"
                            :store (cookie-store)})))

When the user navigates to the top domain, we check if the user's session has a mailbox associated with it or the associated mailbox is still active. If it is we show the content of the mailbox else we create a new mailbox set the users cookie and redirect the user back to top domain.

(defn template [content]
  (html [:html {:lang "en"}
         [:head
          [:link
           {:href "http://twitter.github.com/bootstrap/assets/css/bootstrap.css"
            :rel "stylesheet"}]]
         [:body
          [:div {:class "container"}
           content]]]))

(defn show-inbox [mailbox]
  (html [:br]
        [:table {:class "table table-striped table-bordered table-condensed"}
         [:thead
          [:tr
           [:th {:span "3"} (str "Inbox for " mailbox)]]
          [:tr
           [:th "Subject"]
           [:th "From"]
           [:th "Time"]]]

         (map-indexed (fn [idx {from :from time :time subject :subject}]
                        [:tr
                         [:td [:a {:href (str "/view/" mailbox "/" idx)}
                               subject]]
                         [:td from]
                         [:td (let [time (java.util.Date. time)]
                                (str (.getHours time) ":" (.getMinutes time)))]])
                      (:messages (@inboxes mailbox)))]))

(defn view-message [mailbox idx]
  (if-let [mailbox (:messages (@inboxes mailbox))]
    (try
      (let [message (mailbox (read-string idx))]
        (html
         [:h3 [:a {:href "/"} "Back to Inbox"]]
         [:h3 "From: " (:from message)]
         [:h3 "Subject: " (:subject message)]
         [:p (:content message)]

         [:form {:action "/reply" :method "post" :class "xxx"}
          [:textarea {:name "reply" :rows "10" :cols "100"}]
          [:br]
          [:input {:type "hidden" :name "subject" :value (:subject message)}]
          [:input {:type "hidden" :name "from" :value (:from message)}]
          [:input {:type "hidden" :name "to" :value (:to message)}]
          [:input {:type "submit" :value "Reply" :class "btn"}]]))
      (catch Exception e
        "<h1>Message does not exist!<h1>"))
    "<h1>Mailbox does not exist!<h1>"))

(defn reply-message [from to subject reply]
  (send-message {:from to
                 :to from
                 :subject subject
                 :body reply}))

Above snippet generates the HTML for viewing the list of messages in a users mailbox, view the message and reply to it.

(defn -main [& args]
  (watch-inboxes)
  (.setPort smtp-server 2525)
  (.start smtp-server)
  (run-jetty #'app {:join? false :port 8080}))

Finally, start it all up. In order to export sources from this document get the original from my github repository and run org-babel-tangle or manually copy/paste snippets in the correct order.