Developer

HTML Email

We recently added HTML versions of many of our email notifications. These new emails are much nicer to look at and give us flexibility to provide more information and context without overwhelming the message by using CSS to emphasize the most important information. Response from our members has been very positive, and we’ll be continuing the rollout to the rest of our notifications. However, the road to get from plain-text emails to HTML emails was fraught with peril, so I want to share some of the lessons that we learned.

It’s HTML, I Know This

At first glance, adding HTML to your email seems pretty simple. Your website is already HTML, you’re really good at working with it, so how hard could it be? As it happens, it’s harder than you expect. For one thing, you have to forget all of the lessons you’ve learned over the past decade about semantic markup and new whizz-bang HTML5 and CSS3 features.

The simple fact is that HTML rendering across email clients is confusing and inconsistent. The current best practices remind me quite a bit of web design in the last 90’s and early 2000’s: tables nested inside tables. Don’t believe me? See what MailChimp has to say on the matter:

If there’s only one thing you to know about coding email, it’s that tables rule the day.

MailChimp helpfully provides some battle-tested templates to start with. Dan Cederholm started from there, designed a Dribbble-branded base template, and designed several different notifications using that template. He built them as ERB templates with static markup and handed them off to me to plug in the necessary Ruby to populate them.

The Tests Are Failing

Before even adding tests for the HTML portion of the emails, our tests on the plain-text versions began to fail. Our tests for the emails look something like this:

mail = Notifier.comment_notification(comment)
assert_match user_url(comment.user), mail.body.to_s

Up until this point, mail.body.to_s would return the text of the email. Once you add a second ActionMailer template, that stops being true. Hopping into a console, I discovered that mail.body.to_s was now an empty string. What? Some more poking around led me to mail.body.parts which was an array of the plain-text email and the HTML email template, and mail.text_part and mail.html_part which would give me exactly the portion of the email I wanted to test.

Emboldened by my newfound knowledge, I built a new test helper:

def assert_text(mail, value)
  assert_match value, mail.text_part.body.to_s
end

# now the test looks like this
mail = Notifier.comment_notification(comment)
assert_text mail, user_url(comment.user)

Strangely, this test helper, which worked for emails with both plain-text and HTML templates, failed for our emails with just plain text. For those emails, mail.text_part was nil. Again, what? This necessitated building a more-complex method to fetch the text that I wanted to test:

def assert_text(mail, value)
  assert_match value, extract_content_of_type(mail, :text)
end

def extract_content_of_type(mail, type)
  if mail.parts.any?
    # with multiple parts, grab the one of the given type
    matching_part = mail.public_send("#{type}_part")
    assert matching_part, "Must have a part with type '#{type}'"
    matching_part.body.to_s
  else
    # if just one part of the email, assume it's what we want
    mail.body.to_s
  end
end

With this done, tests on our plain-text emails were passing again.

Testing the Markup

Now I wanted to add similar tests to the HTML emails. Since they’re HTML, I also wanted to use the same Rails-provided assertions we had used for our views, such as assert_select. Rails doesn’t provide that out of the box, but some searching lead me to this blog post, which gave me exactly what I wanted. I tweaked their approach slightly and ended up with this:

def assert_html(mail, &block)
  root = HTML::Document.new(extract_content_of_type(mail, :html)).root
  assert_select root, ":root", &block
end

# and the test looks like this
mail = Notifier.comment_notification(comment)
assert_html(mail) do
  assert_select "a[href=?]", user_url(comment.user)
end

Now I can get started with some tests and filling in the static HTML mockup with Ruby to make them dynamic.

And Now For Tests of a Different Color

Of course, that’s just half of the testing story. Remember what I said before about inconsistent rendering between email clients? We want these shiny new emails to look good for everybody, but testing against so many clients is hard. Thankfully, tools like Litmus make things easier.

Send Litmus your HTML (or forward them an email) and they’ll run it through a battery of clients and provide you screenshots of how your emails render. Lather, rinse, and repeat until you’re happy with the results. I can’t stress enough how important this was, as we found a number of bugs in our email template and strange rendering in common clients that we were able to correct before any of our members saw them.

You Didn’t Actually Want CSS in There, Did You?

As we were about to ship, we ran into a showstopper: Gmail ignores any linked stylesheets or style elements in your email. The best practice for styling HTML in an email is to use inline style attributes (for example <p style="color: font-size: 16px; font-weight: bold">…</p>).

Unwilling to perform this madness, we went out in search for a tool which would automatically convert our external stylesheets to inline attributes, and found actionmailer_inline_css. It hooks into the ActionMailer lifecycle and runs the emails through premailer to parse the CSS and add inline styles when delivering. This saved us from having to muck up our code with inline styles and allowed us to still share CSS between our emails.

There You Have It

It took a bit more work than we anticipated, but we’ve been happy with the result and the reaction from our members has been positive. Now that we’ve laid the groundwork, rolling out HTML versions for the rest of our notifications will hopefully be easier.

Throttle Record Creation in ActiveRecord

Throttling the creation of records is a component of our spam protection at Dribbble. There’s no sane reason for a user to create more than 10 comments in two minutes, or more than 100 comments in one day. We’ve had a method for setting these types of limits for a while, but we’ve extracted it into an open-source library, allowed.

Example Usage

class Comment < ActiveRecord::Base
  belongs_to :screenshot
  belongs_to :user

  # Custom scopes beyond default created_at attribute.
  allow 10, per: 1.day, scope: :user_id
  allow 5,  per: 1.day, scope: [:screenshot_id, :user_id]

  # Custom error message.
  allow 100, per: 7.days, message: "Too many comments this week."

  # Custom conditions.
  allow 100, per: 7.days, unless: :whitelisted_user?
  allow 100, per: 7.days, unless: -> (comment) { comment.user.admin? }

  # Callbacks when limit is reached.
  allow 10, per: 2.minutes, callback: -> (comment) { comment.user.suspend! }
  allow 25, per: 5.minutes do |comment|
    comment.user.suspend!
  end

  def whitelisted_user?
    user.whitelisted? || screenshot.user == user
  end
end

Future Development

Now that we’ve extracted it to make it a bit easier to work with, we’d like to support different methods of checking throttles. For example, a week long check would be better served using Redis over ActiveRecord. It helps to avoid extra queries on save for a rare condition that may never be met and it’s less important to be perfectly accurate, so it doesn’t matter if we lose the count.

Development Behind the Design

Did you know there is a ton of development behind Dribbble? Probably so, but you haven’t heard much about it before. We’ll be sharing what we’re working on behind the scenes, interviewing developers using the API, showing how we use libraries, extracting libraries from our code, and more.

Behind the Scenes

We have never been very public about what we’re actively developing or what we have planned for the future. While we aren’t guaranteeing we’ll be completely open, we would love to occasionally share the successes and failures we have while working on new features.

API Developers

Similar to Timeout interviews with designers we would like to talk with the developers who use our API. Beyond featuring products using the API it will hopefully help us learn more about how the API is used and help us plan for changes to it in the future. If you are interested, send me an e-mail with a brief description of how you are using it and any relevant links.

Open Source

Considering Dribbble is built on Rails and uses over 60 open source libraries, we’d like to start extracting and sharing some of our code. We have a couple of ideas on libraries we’d love to share in addition to possible patches and example uses for existing libraries.