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.
(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.