How I Learned to Stop Hating and Love Action Mailer

August 21, 2007 Pivotal Labs

My biggest gripe with ActionMailer is how difficult it is to generate URL’s. It’s common enough when sending an email that it includes a link. But ActionMailer, by default, gives you no access to url_for and named routes. Ugh!

Even if you’re clever enough to do something like:

class ActionMailer::Base
  include ActionController::UrlWriter
end

You’re still screwed as you need to know the host, port, protocol, etc. to generate links. These data can be set globally, but by far the easiest and most flexible way is to get them is from the request object.

Passing around the host, port, etc.

The request object, is (of course) only available to Controllers. So the data need to be passed from the Controller to the mailer, like:

class UsersController < ApplicationController
  def create
    ...
    if @user.save
      MyMailer.deliver_foo(..., request.host, ...)
    end
  end
end

Of course, if you’ve read my previous post you know I HATE polluting my Controllers with business logic like this. I strongly prefer pushing this “triggered action” into the model:

class User
   after_create :send_email
end

The cost of this is that I now need to past the host, etc. down into my model so it can pass it on to the mailer! The Java Programmers over here just laugh at me for not having a real Dependency Injection framework, which they say would solve this handily. Some stupid Java framework solving this problem better than Rails?! This makes me MAD AS HELL!

Enter the Global Variable

Screw Dependency Injection. I’m going to use a Global Variable like every other God Fearing Rails programmer. His Excellency DHH said let there be cattr_accessor and it was Good.

Step one, set the around-filter in your ApplicationController:

around_filter :retardase_inhibitor

Step two,

THERE IS NO STEP TWO

You don’t need to pass around host. You can generate URL’s in ActionMailer no problem. Let’s look at how this is done:

module UrlWriterRetardaseInhibitor
  module ActionController
    def self.included(ac)
      ac.send(:include, InstanceMethods)
    end

    module InstanceMethods
      def inhibit_retardase
        begin
          request = self.request
          ::ActionController::UrlWriter.module_eval do
            @old_default_url_options = default_url_options.clone
            default_url_options[:host] = request.host
            default_url_options[:port] = request.port unless request.port == 80
            protocol = /(.*):///.match(request.protocol)[1] if request.protocol.ends_with?("://")
            default_url_options[:protocol] = protocol
          end
          yield
        ensure
          ::ActionController::UrlWriter.module_eval do
            default_url_options[:host] = @old_default_url_options[:host]
            default_url_options[:port] = @old_default_url_options[:port]
            default_url_options[:protocol] = @old_default_url_options[:protocol]
          end
        end
      end
    end
  end

  module ActionMailer
    def self.included(am)
      am.send(:include, ::ActionController::UrlWriter)
      ::ActionController::UrlWriter.module_eval do
        default_url_options[:host] = 'localhost'
        default_url_options[:port] = 3000
        default_url_options[:protocol] = 'http'
      end
    end
  end
end

ActionController::Base.send(:include, UrlWriterRetardaseInhibitor::ActionController)
ActionMailer::Base.send(:include, UrlWriterRetardaseInhibitor::ActionMailer)
(from url_writer_retardase_inhibitor.rb)

“Oh no!,” you exclaim, “Class Variables!! Get behind me, Satan!”.

Well, get over it. How do you think with_scope works? How do you think you can call a Finder like User.find or User.new wherever you feel like? It’s called Global Variables, man. Embrace it. I call this liberation theology.

About the Author

Biography

More Content by Pivotal Labs
Previous
Treetop: Bringing the Elegance of Ruby to Syntactic Analysis
Treetop: Bringing the Elegance of Ruby to Syntactic Analysis

(This is my proposal for this year's RubyConf. Fingers crossed!) Treetop is a parsing framework that bring...

Next
Perplexing constant scope behavior
Perplexing constant scope behavior

Why can't I refer to constants nested inside a class within a module eval? irb(main):001:0> class A irb(ma...

Enter curious. Exit smarter.

Register Now