3 min read

Merb's Mailer is Awesome (if I do say so myself)

A few months ago, when I first took a look at Merb, I noticed a distinct lack of support for email. There was a simple wrapper around MailFactory (a Ruby library that is, imho, superior to TMail, and actually documented to boot), but nothing like Rails' ActionMailer.

That said, ActionMailer suffers from some very serious deficiencies, which makes most Rails users rue the day when the pointy-headed boss throws some "simple" mail requirement at them. In fact, while most Rails users expect writing mails to be roughly equivalent to writing regular controllers and actions, ActionMailer barely resembles the much more manicured ActionController.

In fact, the company I work at Procore, maintains what is essentially a fork of ActionMailer with some of these deficiencies resolved. The problem is that because we dig deep into Rails internals, a Rails upgrade also requires tweaking to our fork.

Some simple examples:

  • ActionMailer "controllers" go in the models directory, but their views are mixed in with regular views
  • ActionMailer instance variables are not assigned and passed into ActionMailer views
  • ActionMailer does not support layouts
  • While ActionMailer does support some simple magic with mime-types, if you do anything that is not exactly the default, you are required to specify the parts of your email using tortured syntax.
  • The ActionMailer syntax for attaching files is also a bit tortured and not particularly well documented:
attachment "application/octet-stream" do |a|
  a.body = attachment2.data
  a.filename = attachment2.name
end

So my agenda was to clean up all of these pain points, in the context of a brand-new Merb::MailController. At the time, there was no way to subclass the base Controller class without getting all the request gunk along with it, but Ezra was more than willing to create an AbstractController class, which contained the parts of the Controller mechanism that were not related to request-handling.

It turned out to be a good deal, because Ezra later used it to create Merb's "Parts," partials that also have the lightweight AbstractController implementation along for the ride (more on that in another post).

With the AbstractController up and running, I proceeded to hack together a MailController implementation. Merb::MailController:

  • controllers go into the app/mailers directory
  • views go into the app/mailers/views directory
  • instance variables are assigned to their views, just like any other Merb controller
  • supports layouts by default, just like any other Merb controller. Layouts go in the app/mailers/layouts directory.
  • supports HTML/Plaintext emails very well.
    • render_mail :text => :foo, :html => :bar will look for a foo.text.ext and a bar.html.ext. If either is not found, it'll fall back to just foo.ext and bar.ext
    • render_mail :foo is the equivalent of render_mail :text => :foo, :html => :foo
    • render_mail is the equivalent of render_mail :text => :foo, :html => :foo when you are inside the :foo action
    • If you pass a string instead of a symbol in, it will render the actual string, so you can use regular render methods (since Merb render methods just return a string).
      • Example: render :text => "FOO", :html => "<p>FOO</p>"
    • You can also mix and match: render :text => "FOO", :html => :bar
    • Finally, you can also do stuff like render_mail :template => {:html => "foo/bar", :text => "foo/baz"}
  • supports attachments trivially:
    • attach File.open("foo")
    • which is the same as attach "foo"
    • attach [File.open("foo"), File.open("bar")]
    • which is the same as attach ["foo", "bar"]
    • attach File.open("foo"), "name_i_want_to_call_it"
    • attach [[File.open("foo"), ["name", "image/png"]], [File.open("bar"), ["bar_name", "application/octet-stream"]]]
    • which is the same as attach "foo" => ["name", "image/png"], "bar" => ["bar_name", "application/octet-stream"]
    • attach StringIO.new("Some data I want to send"), "the_file_name"
    • attach StringIO.new("Some data I want to send") => ["the_file_name"], StringIO.new("More data") => ["another_file"]
    • Because Merb already includes Mime::Types, you do not have to include a mime-type unless you want to override the default. Merb will automagically determine the mime-type and pass it along.

This means that the most common case will be something like attach "foo", which will simply attach the file in the file-system called "foo", with the name "foo", and its mime-type based upon its file-type. And multiple-files is no more complex: attach ["foo", "bar"], which will do the same for both files.

The bottom line is that all the major pain-points in ActionMailer are closed up. And this is all done in 79 lines of code, thanks to Merb's excellent architecture. In fact, this is a testament to the general excellence of the Merb architecture. Merb is not "a lightweight Rails", as many often say.

It is true that it's lightweight, but it's not true that it's missing giant swaths of useful features (and this will become less and less true as people start to write plugins for merb, which will provide functionality like Rails-style form helpers that are currently missing). In fact, there are a number of cases, like mail support, where the Merb version is dramatically more functional.