An Architect's View

CFML, Clojure, Software Design, Frameworks and more...

An Architect's View

Real World Clojure - HTML email generation

October 28, 2011 ·

Like many companies, we send HTML emails. This has always been extremely easy in CFML because it has a cfmail tag built-in which allows you to put HTML directly inline inside it - which in turn can include CFML code so the HTML can be dynamically generated on the fly. As we unravel parts of our application model and make them reusable from both CFML and Clojure code, we have to replace low-level CFML code with Clojure because, right now, CFML cannot be easily called from Java code, especially outside the Servlet in which CFML runs. That situation will change with Railo 4.0 but we can't wait for that release so we needed to bite the bullet and create an HTML email service in Clojure.

The first low-level piece we built was a simple send-email function:

(defn send-email [from to subject body]
  (let [session (javax.mail.Session/getDefaultInstance @mail-properties)
        msg (doto (javax.mail.internet.MimeMessage. session)
              (.setFrom (javax.mail.internet.InternetAddress. from))
              (.setRecipient javax.mail.Message$RecipientType/TO
                (javax.mail.internet.InternetAddress. to))
              (.setSubject subject)
              (.setText body "utf-8" "html"))]
    (javax.mail.Transport/send msg)))

Boy, Java is an ugly, verbose language, isn't it? At least I can hide it away... Our real code has exception handling and logging but I'm omitting those make the example easier to read. At some point I might create a generic Clojure wrapper for javax.mail which provides a function API for things like MimeMessage etc. mail-properties was discussed in the blog post about environment control - it's a delayed java.util.Properties object. With that out of the way, the next piece to build was a way to render HTML content with dynamically inserted content. Christophe Grand has created a powerful selector-based templating library called Enlive which seemed like a good fit. You can annotate plain HTML with CSS classes and identifiers and then use Enlive to parse, modify and render back to HTML. One benefit of this approach, for us, is that we will be able to store email templates in a database and create an admin / authoring tool for our business team so they can edit email content - and previous it with "live" data - without needing engineers to modify the (current) code-based email templates.

Side story: I'd looked at Enlive before. Back in 2009, I created a lightweight MVC framework for CFML, called FW/1 - Framework One. In June 2011, I started a port to Clojure. I quickly had the core convention-based routes and Model-Controller interactions working pretty well but still needed to solve the View rendering portion. I started to explore Enlive but since HTML templates are located dynamically and I wanted to delegate the process of attaching dynamic data to the controller, the top-level macros in Enlive got in the way. I started unraveling the macros to get at the underlying functions but just got bogged down in the complexity of what Enlive does under the hood and gave up on the port of FW/1 to Clojure.

At The Strange Loop conference in September 2011, I got chatting to Chas Emerick about my frustrations with Enlive and he talked a little about how he had used it without some of the macros that I was struggling with. That was enough to point me in the right direction so when I started round two with Enlive, I able to quickly focus on just the parts I needed to deal with dynamically selected HTML templates and how to compose transformations within my own code. The key pieces, which I'd failed to identify before, are:

  • The html-resource function which can be given a "reader", amongst other things, and returns a parsed representation of your HTML template as a Clojure data structure - "nodes".
  • The at macro which takes a collection of nodes and pairs of selectors and transformation functions, and produces a modified "DOM" data structure.
  • The emit* function which takes a collection of nodes and flattens it into a sequence of HTML fragments. You can just (apply str ...) to the result to get a single HTML string.

Armed with this new insight, it was fairly straightforward to write functions that would replace annotated parts of our HTML email templates with the necessary data. As an example, here's part of the function that applies our standard email wrapper:

(defn- email-wrapper [nodes subject body site locale ...]
  (h/emit*
    (h/at nodes
          ...
          [:img.logo-src]
          (h/do-> (h/set-attr :src (str (:site-root-url site) "/skins/" (:id site) "/images/logo.png"))
                  (h/remove-class "logo-src"))
          ...
          [:a.site-link]
          (h/do-> (h/set-attr :href (:site-root-url site))
                  (h/remove-class "site-link"))
          [:.subject]
          (h/do-> (h/content subject)
                  (h/remove-class "subject"))
          ...)))

email-wrapper is passed the "DOM" as nodes, the subject and body of the email, and various other pieces of data used in the template. net.cgrand.enlive-html is required as h so you can see we're using emit* to render the wrapped email to HTML fragment and the at macro to apply a list of transformations:

  • Where we find <img> tags with CSS class logo-src, we set the src attribute to the URL of the site's logo and remove the class.
  • Where we find <a> tags with CSS class site-link, we set the href attribute to the top-level URL of the site (and remove the class).
  • Where we find an element with CSS class subject, we replace the content of that element with the email subject (and remove the class).

The do-> macro allows us to apply multiple transformations to a node. You write your own transformations as functions that take nodes and use the at macro internally to modify those nodes. The other Enlive macro we rely on is clone-for which takes a binding form (an identifier and a sequence to iterate over) and pairs of selectors and transformations. This "clones" part of the "DOM" for each element of the sequence and applies the specified transformations:

{[:tr.h-line] [:tr.profile]} ; process these two tr classes "in parallel" 
(h/clone-for [profile (:matches data)]
             [:tr.h-line]
             (h/remove-class "h-line")
             [:tr.profile]
             (h/do->
               (h/remove-class "profile")
               (fn [nodes] ; transform the nodes inside tr.profile
                 (h/at nodes
                       ...))))

This matches a pair of <tr> tags in the HTML, annotated with CSS classes h-line and profile respectively, and clones them for each element of the :matches sequence, applying the specified transformations (data is a map of information we pass into the email rendering system containing all the things that we reference in the template). The result is a pair of transformed table rows for each matching profile passed in.

Finally we convert the result to a string and replace certain troublesome characters to get a nice, clean HTML string that can be sent as email:

(-> (apply str (email-wrapper email-wrapper-html
                              subject body site locale
                              ...))
  (.replaceAll "\u00A0" "&nbsp;")
  (.replaceAll "\u0009" "    ")
  (.replaceAll "&amp;copy;" "&copy;"))))

I'm not quite sure why &nbsp; and &copy; are converted the way they are inside Enlive. I tried the html-content transformation instead of plain content but that caused other issues so we decided to just live with these three fixups.

Overall, I'm quite pleased with the result - we have a solid basis for converting the rest of our email templates in the future and can refactor the transformations we've built for the first template to make the code more generic - but it's a lot more work that it would have been in CFML! Now that I've cracked this nut for World Singles, I'll be able to go back to my port of FW/1 to Clojure and finish that off at some point.

Tags: clojure

0 responses