ActionMailer tips

My latest story card is all about getting our app to send email so I’ve had to delve into ActionMailer. It’s a cool way to send emails without tons of code but there are some things that are a little bit wacky. Let us say that you’ve got a cookie website and you want people to be able to send emails of their favorite recipes to each other. “My that’s a handsome cookie” they might say and “Oh look, an email button that lets me send this recipe to a friend. This site has met my needs exactly.” In order to get such a thing to happen you would create a RecipeMailer model that extended ActionMailer kinda like this:

class RecipeMailer < ActionMailer::Base

def recipe_email(email_params)
@from = email_params[:from]
@recipients = email_params[:recipients]
@subject = email_params[:subject]
@sent_on = email_params[:sent_on]

@body[:recipe] = email_params[:recipe]
end
end


In your controller you’d call:

RecipeMailer.deliver_recipe_email(email_params)

And some good old method_missing magic would match deliver_recipe_email with your recipe_email method which would then render a view.

Er, a view?

That’s right, the email templates are rhtml views. You write them as you would a view, but ActionMailer turns them into emails. So one might look like this:

Howdy neighbor,
Here’s a recipe you might like:
<%= @recipe.text %>

And Rails would of course evaluate the <%= stuff like it would in a view. But, although it’s a view, it doesn’t have access to any helper methods you have defined, even if you put them in ApplicationHelper (which are supposed to be available to all views). You’ll have to declare this in your RecipeMailer model if you want the view to see them.

ApplicationHelper:
helper :application

Another thing to watch out for is any links you include in your email must have the full path to your app. When using link_to you can pass in these options:

:only_path => false, :host => @host

Where host is the host you get off of request.host but you won’t have access to request in your view or your mailer, so you’ll have to pass it in from your controller (again, the email template is only sort of a view). But if you’ve added some fancy custom tags (like a hypothetical recipe_image_tag that takes in a recipe object and does all sorts of neato stuff) you might find that the combination of needing a full path and being rendered by an email client (ie not your rails app) will force you to, sigh, hack up your own links like so:

<img src="http://<%= @host -%>/images/logos/<%=@cookie_name-%>.png"
alt="<%=@cookie_name-%>." />

Not the prettiest code I’ve ever written but sometimes time is a factor.

Comments

xmlblog said…
Actually, the best way to set the host from the request would be like so:

default_url_options[:host] = request.host

Now you can use link_to rather than having to do all that string interpolation.
xmlblog said…
Oh, I forgot to mentions, set that at the class level

class WhateverMailer < ActionMailer::Base

default_url_options[:host] = request.host

def cookie_email(params)
#...
end
end
Jake Scruggs said…
So I tried that but I get this error:

undefined local variable or method `request' for CookieMailer:Class

I just don't seem to have access to the request in the ActionMailer class.
Mark said…
Jake, found your blog whilst having the same problem. It's dirty, but I've got around this issue by adding:

ActionMailer::Base.default_url_options[:host] = request.host_with_port

to my ApplicationController, in a filter method. That sets the defaults on each mailer.

I'm not sure why I have to set it on the mailers - I wanted to set it on the ApplicationController so it was accessible everywhere but didn't manage that. Never mind, I've only had this problem with mailers so that should sort it.
dto123 said…
Hi Jake, good post on ActionMailer. I had the same problem with designating a host in link_to's on the view template.

I was able to get around this by using named routes. eg:

routes.rb
map.index 'home', :controller => 'home', :action => 'index'

mail view template
<\%\= link_to 'go', index_url(:host => @host) \%\>"

where @host is the domain name passed in by the mailer model.

Unfortunately, a regular link_to doesn't seem to like this as much.
free ps3 said…
Thanks for the nice post!
Colin said…
Great; thanks for the help on including helper methods in ActionMailer!
Anonymous said…
Any luck on using SMTP via SSL? I know there is a gem (tlsmail) for connecting via TSL, but I cannot find a solution for SSL.

Anyone?
Brian said…
Thanks that was helpful!
Glenn James said…
If you'd like a more sane way of linking t your app in ActionMailer emails, try doing a google search for 'retardase_inhibitor'. The name might throw you askew, but the summary of it is as follows:

---BEGIN README BLURB
RetardaseInhibitor
==================

It is difficult to generate URLs with ActionMailer. Although you can generate
absolute paths from your routes, you usually need to generate full URLs for
the links in the email to be useful to your recipients.

This plugin sets the default_url_options for host, port and protocol by
providing an around_filter which will grab the values from the request object.
In the cases when the request object is not available, for example, when
running tests, default values are set instead.

See the following blog post for more details:
http://www.pivotalblabs.com/articles/2007/08/20/how-i-leaned-to-stop-hating-and-love-action-mailer
----END README BLURB
What a pretty picture in your blog header. Wow!
Anonymous said…
There is a useful Action Mailer cheatsheet here: http://dizzy.co.uk/ruby_on_rails/cheatsheets/action-mailer
The Ultimation said…
noice, i was trying for a while to find how to include helper methods in action mailer. just used helper :application

Popular posts from this blog

Point Inside a Polygon in Ruby

SICP Wasn’t Written for You

What's a Good Flog Score?