Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

How I Learned to Stop Hating and Love Action Mailer

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

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
      MyMailer.deliver_foo(...,, ...)

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

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,


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)

    module InstanceMethods
      def inhibit_retardase
          request = self.request
          ::ActionController::UrlWriter.module_eval do
            @old_default_url_options = default_url_options.clone
            default_url_options[: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
          ::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]

  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'

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 wherever you feel like? It’s called Global Variables, man. Embrace it. I call this liberation theology.

  1. Nathan Sobo says:

    What rhetorical flair. It fills me with delight.

  2. Cody Caughlan says:

    If you are calling your mailer from a controller and using something like a hash to pass into your deliver method then you can just assign a hash key/value pair of “controller” / self and you then have transparent access to url_for and friends. E.g.

    class Foo < ApplicationController
        def foo
            params[:recipients] = ""
            params[:subject] = "Yay!"
            params[:controller] = self

    And then url_for works in your ActionMailer views.

  3. nick kallen says:

    Cody thanks for the suggestion. The disadvantage of this approach is the need to pass around stuff. I want to hide from the controller the fact that a model has a triggered action. In the worst case a model creates another as an after create, that creates another, etc. Finally the last model in the chain sends an email. Crap — now you have to pass around the controller n frames down the call stack. Not only that, but to unit test the last model you have to create a mock controller.

  4. Kamal Fariz says:

    You probably would want to edit

    around_filter :retardase_inhibitor


    around_filter : inhibit_retardase

    to follow your method definition.

  5. Dan Manges says:

    ActionMailer “already includes the UrlWriter”:, it just needs the host and port to generate the URLs. I would rather use defaulturloptions (already built-in):
    class Notifier < ActionMailer::Base
    defaulturloptions[:host] = DEFAULT_HOST
    defaulturloptions[:port] = DEFAULT_PORT

    And then define the host and port in the environment. The benefit of taking the info from the request in the controller (like you’re doing) is that you don’t have to do this configuration, you can have multiple domains and still generate the corresponding URLs, etc. But if you’re sending e-mail from outside the context of a request (such as some async process), you still need to specify a host, and this is the easiest way to do that.

  6. Matt says:

    I found replacing the line:

    default_url_options[:port] = request.port unless request.port == 80


    default_url_options[:port] = request.port
    default_url_options.delete(:port) if request.port == 80

    To work much better. I was having problems with the default port getting into my production url string and I was occasionally having problems with the port getting set to nil. This fixed both issues.

  7. Alex C says:

    Brilliant! I’m perfectly happy using a hack to fix a broken framework, and the name (“retardase inhibitor”) makes it crystal clear there are no heroes in this story.

    One way to make this palatable as a patch to core would be to put the settings-saving and -restoring methods on UrlWriter, not ActionController. Call them “push_settings” and “pop_settings” perhaps. Then your filter would be

      def inhibit_retardase
          ::ActionController::UrlWriter.push_settings(:host =>, :port => request.port, :protocol => request.protocol)

    Reads quite a bit clearer to my eye. Want to pair on that?

  8. Alex Chaffee says:

    Dan –

    You gotta escape underscores in markdown (with a backslash, like this: _). I think your comment meant to say…

    ActionMailer already includes the UrlWriter,
    it just needs the host and port to generate the URLs. I would rather use default_url_options (already built-in):

    class Notifier < ActionMailer::Base
        default_url_options[:host] = DEFAULT_HOST
        default_url_options[:port] = DEFAULT_PORT

    And then define the host and port in the environment.

  9. Alex Chaffee says:

    I mean, with a backslash, like this: _ (Did I finally escape the escaping?)

  10. Nick Kallen says:

    Dan — Wonderful suggestion. My first implementation of the retardase inhibitor used exactly this approach. A sage developer here vetoed it for precisely the reason you specified:

    you can have multiple domains and still generate the corresponding URLs, etc.

    I’m surprised ActionMailer already has Urlwriter, my mistake.

    Matt, thanks for the fix: This is what I ended up with in my app:

    default_url_options[:port] = request.port == 80 ? nil : request.port
  11. Nick Kallen says:

    Dan — also note that my plugin makes it easy to specify a default in absence of a request object. These should probably come from the environment or a yaml file…

  12. Dan Manges says:

    @Alex – thanks for formatting my comment.

    @Nick – That’s cool that the plugin also allows you to specify a default – I missed that when I looked at it.

  13. Jan-Willem van der Meer says:

    Hi Guys,

    I made a little change to the plugin code, the plugin kept adding port 3000 in production mode.

    this is the changed part in retardase_inhibitor.rb:

    `::ActionController::UrlWriter.module_eval do
    case ENV[‘RAILS_ENV’]
    when ‘test’
    default_url_options[:host] = ‘’
    default_url_options[:protocol] = ‘http’
    when ‘development’
    default_url_options[:host] = ‘localhost’
    default_url_options[:port] = 3000
    default_url_options[:protocol] = ‘http’

  14. Chris Wilson says:

    Hi Nick,

    I too had problems with port 3000 appearing in production (when deployed on port 80) and only then, and it didn’t become clear until after the site had gone live (oops!).

    I see that you changed to this:

    default_url_options[:port] = request.port == 80 ? nil : request.port

    I had a slightly different fix, the same as Matt’s, but I believe both should work equally well.

    Please could you update the public version of the plugin to save others from the same headache?

    Cheers, Chris.

  15. Darryl Hamilton says:


    Just installed the plugin, and everything seemed to be working fine. However, I’m getting an error when starting up a server (mongrel)…

    vendor/plugins/retardase_inhibitor/lib/retardase_inhibitor.rb:39:in `included': undefined local variable or method `request’ for ActionController::UrlWriter:Module (NameError)

    Also, with the plugin installed, and the around_filter applied, my unit tests are taking orders of magnitude more time to complete.

  16. Darryl Hamilton says:

    Just a little addendum to this – it seems that access to ‘request’ isn’t available at that point. I’m not sure if it’s because I’m using Rails 2.0.2 or not.

  17. Dylan says:

    This is awesome. +1 for getting this or something like it in core.

  18. Nate says:

    You guys really need to fix your blog. Auto-focusing on the first form field is lame when it’s at the bottom of the page.


  19. Dav says:

    Nate’s right. It annoys the heck out of me every time.

  20. Thomas says:

    That’s the way, I do it:

    class UsersController < ApplicationController def reset_password ... if MyMailer.reset_password_mail(@user, edit_password_reset_url(@user.token)) end end end

    You should do it all in the controller, because you cannot expect the MyMailer-model to always run in a context, where the routes are available. Like ActiveRecord-models, the ActionMailer-models could also be used from a shell script or a cron job.

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *