Rails 3: Action Mailer

As a result of designing this blog, I’ve come to appreciate the refinement of Rails 3, and the drudgery of a few “less than perfect” spots.

ActionMailer (now powered by the Mail gem instead of TMail) is an incredible refactoring of the traditional Rails 2.x email system; frankly, before Rails 3, the email system was anything but elegant — besides the clunky API, none of the traditional view helpers were available, and multipart emails with attachments were hackish.

The new system takes a huge step in the right direction by incorporating a sleek API interface and merge of ActionMailer with the traditional ActionController/ActionView. In essence, whereas before the mailer and controller/views were entirely separate entities, ActionMailer now inherits from these classes, and consequently acts much more like a traditional view. This approach hugely simplifies the design process by recognizing the similarity between HTML webpages and HTML email.

However, I still have a few “nits to pick.” There are still a number of kinks in the ActionMailer, which I will address here.

Named to/from addresses

Admittedly, this isn’t a biggy, but it’d be a nice addition. Currently, if you want to include a name in the from/to address, you have to manually form the string.

mail :to => "\"#{@name}\" <#{@email}>",
     :subject => "#{@name} left a comment"

For me, this is just difficult to read, and I easily crashed my application with misplaced quote/hash marks. I’d prefer something like:

mail :to => { :email => @email, :name => @name },
     :subject => "#{@name} left a comment"

Perhaps it’s a little more text, but it is infinitely more readable for those of us that don’t enjoy mental text escaping.

simple_format

To my knowledge, this is simply a 3.0.5 bug, but if you call simple_format from the email template (to render out line breaks), the behavior you get is terribly unusual, and it turns out this helper is overwritten with a private method.

Until the private method is renamed in a later version, I had to make a custom helper method (basically a copy paste from the original simple_format source) and called it simpler_format.

def simpler_format(text, html_options={}, options={})
  text = ''.html_safe if text.nil?
  start_tag = tag('p', html_options, true)
  text = sanitize(text) unless options[:sanitize] == false
  text.gsub!(/\r\n?/, "\n")                    # \r\n and \r -> \n
  text.gsub!(/\n\n+/, "</p>\n\n#{start_tag}")  # 2+ newline  -> paragraph
  text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline   -> br
  text.insert 0, start_tag
  text.html_safe.safe_concat("</p>")
end

But wait, how you do use custom helpers in an email template? Well, that’s next.

Custom mailer helpers

It turns out that helpers are not included by default for the ActionMailer views — while it may make sense not to do so simply because you don’t usually need the webpage helpers in emails, I found myself needing several of my helpers, simply because the emails I send out are meant to vaguely resemble the website. At any rate, it’s not a deal breaker, but I think Rails 3 could stand to have auto-generated helper files that are auto-included for each mailer. Finally, just for kicks the “helper” method ought to include the :all option that is available in ActionView.

I made a module called MailHelper in the helpers directory, then included it with the classic “helper :name” command from the mailer model.

class CommentMailer < ActionMailer::Base
  helper :mail, :layout
  ...
end

As you can see, I also included my layout helper, which has all of my convenient layout “yields” — these come in handy for webpages and HTML email.

Absolute path: default_host and asset_host

I had a less than enjoyable time trying to get link_to, image_tag, and post_url to output absolute urls instead of the traditional relative path. From what I had read, you simply have to add the following configuration to your environment.rb:

ActionController::Base.default_url_options[:host] = "nybblr.com"

That should do the trick, right? Well…no. That changed in Rails 3 with the move to ActionMailer, so now the config option is:

ActionMailer::Base.default_url_options[:host] = "nybblr.com"

This seemed to work great for link_to and path_url, but when I tried image_tag, I again was back to relative paths. Turns out there is yet another option for both ActionController and ActionMailer necessary for “asset” files (in case they are hosted on another server).

# Notice that the host must be prefixed with "http://" since it assumes the host is a full web URI.
ActionController::Base.asset_host = 'http://nybblr.com'
ActionMailer::Base.asset_host = 'http://nybblr.com'

So this seems to work, but frankly I hate hardcoding in host URLs — I wanted this to work in development and production regardless of the host domain, so to do that I would have to call request.host_with_port. However, since the request object is only available in the controllers/views, we need to set those configuration options before the actions. I accomplished this with a before filter:

class ApplicationController < ActionController::Base
  before_filter :configure_default_host

  ...

  private

  def configure_default_host
    ActionController::Base.default_url_options[:host] = request.host_with_port
    ActionController::Base.asset_host = 'http://'+request.host_with_port
    ActionMailer::Base.default_url_options[:host] = request.host_with_port
    ActionMailer::Base.asset_host = 'http://'+request.host_with_port
  end
  ...
end

This worked perfectly; granted, I didn’t like having to clutter up my ApplicationController, but that is far better than manually rendering links/overwriting asset helpers in the email templates, hardwiring hosts, and cluttering up the view.

So now that I’m done bashing ActionMailer…

ActionMailer really is a huge relief compared to 2.x; I’d say it’s a pretty remarkable Rails achievement. It’s just shy of perfection.