How we like to deliver emails | NetEngine

How we like to deliver emails

Dan Monday, 5 August 2013

tldr;

Steal ideas from our recent open-source TEDx app

You send email. A lot of it. It would be nice if…

Knowing that as a bonus…

All nice things. I’m sure there’s more loveliness that I’ve forgotten. Let’s start at delivery, and work backwards.

I want an object that performs the delivery, by POSTing JSON to the API of whichever email delivery service you’ve selected. I like Mandrill, but choices are great, and you have many. There’s probably a gem for each service, and they’re probably great. You don’t need it.

It’s time for an HTTParty!!!.

class EmailDeliverer
  include HTTParty

  attr_reader :email

  def self.deliver(id)
    new(id).deliver
  end

  def initialize(id)
    @email = Email.find(id)
  end

  def deliver
    self.class.post(url, options)
  end

  private

  def url
    "https://mandrillapp.com/api/1.0/messages/send.json"
  end

  def options
    {
      body: email.to_json,
      headers: {
        'Accept' => 'application/json',
        'Content-type' => 'application/json'
      }
    }
  end
end

I dare that class to look more re-usable. :-) It’s quickly beaten by its background worker.

class EmailDeliveryWorker
  include Sidekiq::Worker

  def perform(id)
    EmailDeliverer.deliver(id)
  end
end

Single responsibility principle, anyone?

Ok, so there’s an Email class somewhere, and it knows how to render itself to the JSON expected by the Mandrill API. I’ve suggested here that it probably belongs to a user - let’s not go duplicating the delivery address, name etc. We’ll come back to the token - it’s for the browser link.

class Email < ActiveRecord::Base
  belongs_to :user

  validates :event, :user, presence: true
  validates :token, uniqueness: true

  before_create :build_token

  def to_name
    user.full_name
  end

  def to_address
    user.email_address
  end

  def deliver
    EmailDeliveryWorker.perform_async(id)
  end

  def html
    EmailContent.for(self)
  end

  def to_json
    EmailSerializer.new(self).to_json
  end

  private
  def build_token
    self.token = BCrypt::Password.create("#{Time.now.to_f}_#{self.to_address}")
  end
end

The serializer should come as no surprise, given my preference for tiny, reusable objects.

class EmailSerializer < ActiveModel::Serializer
  attributes :key, :message
  self.root = false

  def key
    MANDRILL.key
  end

  def message
    {
      html: object.html,
      auto_text: true,
      subject: "Message from TEDx",
      from_email: "noreply@tedxbrisbane.com",
      from_name: "TEDx Brisbane",
      to: message_recipients
    }
  end

  private
  def message_recipients
    [
      {
          email: object.to_address,
          name: object.to_name
      }
    ]
  end
end

Let’s take a look at the source of the HTML. That’s a bit more interesting.

class EmailContent
  attr_reader :email

  def self.for(email)
    self.new(email).content
  end

  def initialize(email)
    @email = email
  end

  def content
    if recognised_events.include?(email.event)
      render_html
    else
      raise Exceptions::EmailEventNotRecognised
    end
  end

  private

  def user
    email.user
  end

  def recognised_events
    %w(register invite)
  end

  def render_html
    EmailContentController.new.render_to_string 'emails/content',
      layout: 'email', locals: { email: email, user: user }
  end
end
class EmailContentController < ActionController::Base
  include ApplicationHelper
end

The ‘magic’ here is in :render_to_string. Now we’re developing emails using HAML, helpers, decorators, your usual front-end weapons of choice.. Handy. Very handy.

Providing a 'view in browser’ link is now as simple as adding an EmailsController - and you’ll do it anyway, because of all the time it’ll save your designer. It removes any need to send an email to test the designs as they’re being developed - no more mailcatcher, no more fiddling in the console.

class EmailsController < ApplicationController
  layout "email"

  def content
    if email
      render('emails/content', locals: { email: email, attendee: email.attendee })
    else
      redirect_to '/', notice: message
    end
  end

  private
  def email
    Email.where(token: decoded_token).first
  end

  def decoded_token
    Base64.urlsafe_decode64(params[:token]) rescue "null_token"
  end

  def message
    I18n.t("controllers.emails.invalid")
  end
end

Finally, it would be nice to have a quick way of getting a link for each email.

class EmailLink
  attr_reader :resource

  def self.for(resource)
    new(resource).for
  end

  def initialize(resource)
    @resource = resource
  end

  def for
    Addressable::URI.escape("#{host_name}/#{route}/#{token}")
  end

  private
  def host_name
    HOSTNAME.public_send(Rails.env)
  end

  def route
    resource.class.to_s.downcase.pluralize
  end

  def token
    Base64.urlsafe_encode64(resource.token)
  end
end

And that’s about it. I’ve modified the code slightly from its original form to suit the example, but you can see the full source, and importantly, the test suite, over on a recent application that NetEngine built for TEDx.

How do you send email? I’d love to compare notes.

comments powered by Disqus