ActionMailer
Swivel loves email. Alerting people (when they want!) about activity on their data is a crucial part of what makes Swivel swivel. When you sign up for Swivel, or edit your settings, you can designate how often you want to receive email notifications.
For web developers, dealing with email can be intimidating. It can be an afterthought. It can be an intimidating afterthought. But any web project of any size will need to send emails from time to time. For Rails developers, the tool that comes to hand is ActionMailer.
ActionMailer is a funny-looking sort of animal that might lead you to believe that it was an afterthought, too, but with proper care and feeding it can meet all your emailing needs.
ActionMailer
First, we'll need a notification model. script/generate mailer Notifier will put
a new file in your models directory like all your models -- but right away we can see we're not in ActiveRecord anymore:
in RAILS_ROOT/models/notifier.rb:
class Notifier < ActionMailer::Base
For every different type of email you send, the notification model will need a method with the same name -- you might have feedback emails that your site's users are sending to you, and invitation or confirmation emails that your site sends to its users.
class Notifier < ActionMailer::Base
def feedback
from "Web User <webuser@example.com>"
recipients "Helpdesk <helpdesk@example.com>"
subject "An email inquiry"
body :foo => 'bar'
content_type 'text/html'
end
end
Calling Notifier.deliver_feedback in your code will send this message using a template file
expected to exist at views/notifier/feedback.html.erb (or .rhtml). Simple as that.
The body line is the interesting one. This is where you can set
variables for use in the email template. So, in this example, the template will
have the variable @foo available as you'd expect:
"Thank you for your comment about '<%= @foo %>'."
Gives:
"Thank you for your comment about 'bar'."
So far, so much documentation. But you may already be seeing some limitations and weirdnesses of ActionMailer. First of all, it's obvious that the essence of each email is pretty much the same: it has a name, a sender, recipients, a subject, and it assigns a bunch of values to some named variables. So, you might define a whole set of methods in the Notification model:
class Notifier < ActionMailer::Base
def feedback
from "Web User <webuser@example.com>"
recipients "Helpdesk <helpdesk@example.com>"
subject "An email inquiry"
body :foo => 'bar'
content_type 'text/html'
end
def sales
from "Prospective Client <sucker@example.com>"
recipients "Sales <sales@example.com>"
subject "Please sell me some stuff"
body :amount => '$2,500'
content_type 'text/html'
end
def hate
from "Disgruntled Person <webuser@example.com>"
recipients "Dev Null <dev_null@example.com>"
subject "You guys suck"
content_type 'text/html'
end
end
At some point during this process, you might think that you're doing the same thing over and over again, and you might think it doesn't seem right. Why not have just one method to send all the emails, and pass things like the recipient, the template, and the variables and their values as arguments?
class Notifier < ActionMailer::Base
def message
from args.delete(:from)
recipients args.delete(:recipients)
subject args.delete(:subject)
body args
content_type 'text/html'
end
end
Now we can call Notifier.deliver_message(:from => 'Web User <webuser@example.com>', :recipients =>
'Help Desk <helpdesk@example.com>', :subject => 'An email inquiry', :foo => 'bar'). The email gets sent
using the message template in the views/notifier directory, with the variable @foo
ready to use.
Of course, you don't want to use the same template for all of the different types of mail you're sending, but there's still no need to create different methods. Just alias each of the message types you'll be using:
['feedback', 'sales', 'hate'].each { |t| alias_method t.to_sym, :message }
Now our Notifier file can send any sort of email that you write a template for. It's that easy.
Templates
We've made some progress in taming ActionMailer, but it does still have a poisonous spur or two. The biggest problem for the casual user is that ActionMailer does not support layouts and other ActionView goodies that you might expect.
Your email templates cannot, by default, access your application's helper methods. In any event, emails are likely
to want their own helper methods. My preference is to create a helpers/notifier_helper.rb file for this, and
to include it in the Notifier model by adding the line helper :notifier at the top.
The lack of proper layout support in ActionMailer is a bigger problem, for which there are quite a few work-arounds but no
really great solution. Alex
Wolfe has tackled the issue, but I'd prefer not to switch back to .rhtml files just for this.
Sending template/css/header/footer information via an assigned variable in the Notifier arguments works,
but it's not quite right:
body args.merge(:header => EMAIL_HEADER, :footer => EMAIL_FOOTER)
A true flexible layout binding for ActionMailer is under development in Swivel labs.
UPDATE: Rails has this functionality out-of-the-box now. It works as you would expect.
Testing
Writing tests for your email senders is a cinch. By default, tests will not actually send the email, but
will keep a record of what would have been sent. The "deliveries" property
of the ActionMailer object is an Array of TMail::Mail objects that you can easily access:
assert_difference('ActionMailer::Base.deliveries.length', 1) do
Notifier.deliver_feedback( :from => 'Web User <webuser@example.com>',
:recipients => 'Help Desk <helpdesk@example.com>',
:subject => 'An email inquiry',
:foo => 'bar' )
end
msg = ActionMailer::Base.deliveries.last
assert_equal 'An email inquiry', msg.subject
Scheduling
Sure, you can just call Notifier.deliver_<message_name> in your application, but you'll probably
want to schedule certain email events, and it would be better to have the email queue run as a process
independent of the web app anyway. For one thing, if your app has a User or Person model
-- and most do -- you might want the ability to send a message to that person easily:
class Person < ActiveRecord::Base
...
def send_reminder
Notifier.deliver_reminder(:recipients => email, :subject => "Confirm your registration" ...)
end
Your email scheduler will probably live in the scripts directory and, at its simplest,
will look something like this:
#!/usr/bin/env ruby
require File.dirname(__FILE__) + '/../../config/boot'
require File.dirname(__FILE__) + '/../../config/environment'
class NotificationDaemon
def run
People.registered_but_not_confirmed.each { |p| p.send_reminder }
sleep(60)
end
end
We've made a few leaps here to show how this might all fit together in a real Rails app. registered_but_not_confirmed
is an imaginary named scope
that selects a subset of People. If you're not using named scopes, you're missing out -- but that's for another day.
Very educational and helpful... I got a little help from your post today... Thanks for your good work!
Posted by: Acai Berry | January 19, 2010 at 11:05 PM