Helping out ActionMailer

by Sean Cribbs

ActionMailer is the “red-headed step-child” of Rails. Is it a model? Is it a view? Is it a controller? It’s really none of the above, but sits in no-man’s land. In addition it’s usage pattern is kind of strange, you call the class with create_ or deliver_ in front of the method name that represents the message you want to send!

That craziness aside, I encountered a particular problem today that ActionMailer made rather opaque. The solution is a peek at how you can bend Rails to your will using the power of Ruby. GiftLasso has some frequently used helpers in the view that generate parts-of-speech (primarily pronouns) based on the gender of the person in question. I lumped them all in a helper called, conveniently, GenderHelper. Now I wanted to make use of these in my ActionMailer class so we can say things like “Joe added an iPod to his wishlist” or “Jane purchased a shirt for herself”.

Naturally, I thought, “The ActionMailer views should have access to methods on the class generating them. So I’ll include the helper.”

  class Mailer < ActionMailer::Base
    include GenderHelper
    #...
  end

No dice. Unit tests failed left and right. Whenever it doesn’t “just work”, I head to the source. How better to determine how it works? Digging through ActionMailer::Base, I found this snippet at the bottom:

      def render_message(method_name, body)
        render :file => method_name, :body => body
      end

      def render(opts)
        body = opts.delete(:body)
        initialize_template_class(body).render(opts)
      end

      def template_path
        "#{template_root}/#{mailer_name}"
      end

      def initialize_template_class(assigns)
        ActionView::Base.new(template_path, assigns, self)
      end

Aha! So it actually uses ActionView! That’s our attack point.

Now the first temptation would be to open ActionView::Base and muck around with it, but I don’t want to break anything that the controllers depend on. Luckily, Ruby has the ability to extend the class of an instantiated object (aka the singleton class). We also have at our disposal the beautiful alias_method_chain from ActiveSupport. So what we’ll do is intercept the creation of the ActionView object and extend it before it renders anything. Here’s what I came up with:

  class Mailer < ActionMailer::Base
    #...
    def initialize_template_class_with_includes(assigns)
      template = initialize_template_class_without_includes(assigns)
      class << template
        include GenderHelper
        # any other stuff I want to add
      end
      template
    end
    alias_method_chain :initialize_template_class, :includes
  end

You can use this technique to add any helpers you want to ActionMailer.

The technique is part of a larger pattern called prototype-based programming. Ruby lets you freely mix prototype-based and inheritance-based patterns to achieve your goal. Javascript also uses prototype-based programming, hence the name of the library Prototype.

© 2006-present Sean CribbsGithub PagesTufte CSS