This happens because ActionMailer instance doesn't have any context about the incoming request so you'll need to provide the :host, :controller, and :action:. If you use a named route, ActionPack provides controller and action names for you. Otherwise, with the url_for helper you need to pass all the parameters.

<%=message_url%><%=url_for:controller=>"messages",:action=>"index"%>

Regardless your choice, you always need to provide the host option to generate an URL in ActionMailer. As shown by the ActionMailer guide, you basically have two ways to pass the host value to ActionMailer:

set a global value

pass the option each time you generate an URL

This works for almost the most part of basic Rails applications but never really worked for me.

Scenario

You have a medium complex Rails application and you need to send emails in different environments including development, staging and production. Each environment is usually hosted on a specific domain:

development environment runs on localhost

staging environment runs on staging.example.com

production environment runs on example.com

A single application instance serves different languages. Each language is hosted on a specific subdomain. So, for example

And here's the development environment. In this case, the locale is passed via querystring instead of using a subdomain.

localhost?locale=en (English)

localhost?locale=it (Italian)

localhost?locale=fr (French)

The locale detection system is quite complex but I'm not going to show it here. It doesn't play a key role in this article.

Problem

As you can guess, none of the solutions mentioned in the guide work for this scenario. The problem here is that I can't provide a default option because the host vary depending on external variables. Also, I don't want to manually pass the host option each time I generate an URL because it would require to pass the request object as email argument each time.

I tried at least 5 different solutions in the past but, unfortunately, each of them has some problem. The "almost perfect one" was to store the request object as an ApplicationController class variable each time a visitor requested a page, unfortunately this solution didn't work in a multithreaded environment. The same issue affect an other similar solution based on Ruby global variables.

Solution

The final solution I worked on is now available as a plugin. I haven't written any test yet because I just extracted it from a real world application. The plugin provides the following features:

It's thread-safe

Makes the request context available to action mailer

Automatically extracts request host and port and pass them as defaulturloptions

If you want to know something more about how it works, continue to read.

The idea behind the plugin is to store the request instance somewhere and then access it from ActionMailer.

The first part of the problem is quite simple. The only thread-safe place where the instance can be saved is the current thread itself. This is almost straightforward, you just need to append a new before_filter at the lowest-level of your application, that is ApplicationController.

ActionMailer::Base.default_url_options is expected to be a Hash and it's automatically initialized to an empty Hash

ActionMailer::Base.default_url_options is a class variable and is shared across the entire application.

You need a way to convert Hash value into a runtime-evaluated expression. Of course a lambda would be perfect, but ActionMailer::Base.default_url_options can't be a lambda!

For this reason I created an options proxy taking advantage of Ruby duck typing. default_url_options doesn't necessary need to be a Hash, in order to work it just need to acts like a Hash.

The OptionsProxy class is basically a proxy for a Hash instance. The only different between the Hash class and OptionsProxy is that the latter merges some default values to the base Hash each time on each method call. Where do these default values come from? But from the OptionsProxy.defaults labmda, of course!

Now let's go back to our Rails application. Each time a method is called on the OptionsProxy instance, OptionsProxy automatically merges the result of OptionsProxy.defaults and finally executes the method on the resulting Hash. Because OptionsProxy.defaults is evaluated at runtime, you can access the current thread and read extract the default url options from the request context.

The last step is as easy as drinking a glass of water. You need to replace the ActionMailer::Base.default_url_optionsHash with an OptionsProxy instance. This can be done in any Rails environment file, but because I wanted the plugin to be atomic I decided to let the plugin inject itself into ActionMailer.