Friday, 9 January 2009

Rails Mailer

Rails includes a mailer facility. To use it, you need a model - a class that inherits from ActionMailer::Base - and the best way to do that is use Rails to generate it:
ruby script/generate mailer MyMailer send_book

This also creates some default views (in this case just one, send_book) and tests, but we will discuss that later. Let us suppose we want to send an e-mail with a record from a table called books. In a moment, we will set up a method called send_book in MyMailer, but first, let us look at how we will invoke the mailer. In books_controller, to send the mail, you need to invoke a class method that starts deliver_.
MyMailer.deliver_send_book(:user => current_user, :data => find_book)

Somewhere in ActionMailer::Base, there is a method_missing method that handles your request. It creates an instance of MyMailer, does some setting up that we do not have to worry about, then invokes the method send_book. This is where we assign values specific to this e-mail. It might look like this:
def send_book(options)
@recipients = "#{options[:user].email}"
@from = "book-database@mydomain.com"
@subject = "Record from book database"
@sent_on = Time.now
@content_type = 'text/html'
@body[:user] = options[:user]
@body[:book] = options[:data]
end

Note that this will send the message in HTML format; it will default to plain text if there is no @content_type.

Next the method_missing method creates the text of the e-mail. It does this in the normal Rails manner, i.e., from a file in the views folder. In this case it will use views/my_mail/send_book.html.erb. When you create views/my_mail/send_book.erb, you have access to any variable you assign to the @body hash, so in the example above, I could use @user and @book, just as in a normal view. Similarly, you can use partials from other models/controllers in the normal way.

Finally ActionMailer sends the e-mail. It needs to know your your mail settings, which it will collect from a file mail.rb in config/initializers. This might look something like this:
# Email settings
ActionMailer::Base.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
:address => "smtp.mydomain.com",
:port => 25,
:domain => "mydomain.com",
:authentication => :login,
:user_name => "rails",
:password => "secret"
}

You can use your helper files in your mail views just like your normal views, just remember to declare them at the top of MyMailer:
helper :application

The one slight difference is that you cannot do helper :all. I imagine an oversight in Rails.

Attachments can be added easily:
  attachment "application/rtf" do |a|
a.body = File.read 'some_file.rtf'
a.filename = 'samples.rtf'
end


Testing
You can test your mailer. Rails will have generated a default unit test (no functional testing as there is no controller for a mailer).
require 'test_helper'

class MyMailerTest < ActionMailer::TestCase
tests MyMailer
def test_send_book
@expected.subject = 'MyMailer#send_book'
@expected.body = read_fixture('send_book')
@expected.date = Time.now

assert_equal @expected.encoded,
MyMailer.create_send_book(@expected.date).encoded
end

end

NB: The generated unit test includes a statement tests MyMailer. This seems to just invoke write_inheritable_attribute, which appears to make a copy of the class variables in te superclass in the subclass.

Struggling with Ruby: Contents Page

2 comments:

Piedpyper said...

Well put together article. Just one thing that I normally do instead of using the initializer is put the information in the environment config files. It lets you set environment-specific settings which comes in handy at my work place. It looks a little different:

config.action_mailer.delivery_method = :smtp

Kuba Suder said...

"The one slight difference is that you cannot do helper :all. I imagine an oversight in Rails."

Actually, they've just decided that it's not necessary (https://rails.lighthouseapp.com/projects/8994/tickets/1119-helper-all-for-actionmailer)... strange.