- •Contents
- •Preface to the Second Edition
- •Introduction
- •Rails Is Agile
- •Finding Your Way Around
- •Acknowledgments
- •Getting Started
- •The Architecture of Rails Applications
- •Models, Views, and Controllers
- •Active Record: Rails Model Support
- •Action Pack: The View and Controller
- •Installing Rails
- •Your Shopping List
- •Installing on Windows
- •Installing on Mac OS X
- •Installing on Linux
- •Development Environments
- •Rails and Databases
- •Rails and ISPs
- •Creating a New Application
- •Hello, Rails!
- •Linking Pages Together
- •What We Just Did
- •Building an Application
- •The Depot Application
- •Incremental Development
- •What Depot Does
- •Task A: Product Maintenance
- •Iteration A1: Get Something Running
- •Iteration A2: Add a Missing Column
- •Iteration A3: Validate!
- •Iteration A4: Prettier Listings
- •Task B: Catalog Display
- •Iteration B1: Create the Catalog Listing
- •Iteration B4: Linking to the Cart
- •Task C: Cart Creation
- •Sessions
- •Iteration C1: Creating a Cart
- •Iteration C2: A Smarter Cart
- •Iteration C3: Handling Errors
- •Iteration C4: Finishing the Cart
- •Task D: Add a Dash of AJAX
- •Iteration D1: Moving the Cart
- •Iteration D3: Highlighting Changes
- •Iteration D4: Hide an Empty Cart
- •Iteration D5: Degrading If Javascript Is Disabled
- •What We Just Did
- •Task E: Check Out!
- •Iteration E1: Capturing an Order
- •Task F: Administration
- •Iteration F1: Adding Users
- •Iteration F2: Logging In
- •Iteration F3: Limiting Access
- •Iteration F4: A Sidebar, More Administration
- •Task G: One Last Wafer-Thin Change
- •Generating the XML Feed
- •Finishing Up
- •Task T: Testing
- •Tests Baked Right In
- •Unit Testing of Models
- •Functional Testing of Controllers
- •Integration Testing of Applications
- •Performance Testing
- •Using Mock Objects
- •The Rails Framework
- •Rails in Depth
- •Directory Structure
- •Naming Conventions
- •Logging in Rails
- •Debugging Hints
- •Active Support
- •Generally Available Extensions
- •Enumerations and Arrays
- •String Extensions
- •Extensions to Numbers
- •Time and Date Extensions
- •An Extension to Ruby Symbols
- •with_options
- •Unicode Support
- •Migrations
- •Creating and Running Migrations
- •Anatomy of a Migration
- •Managing Tables
- •Data Migrations
- •Advanced Migrations
- •When Migrations Go Bad
- •Schema Manipulation Outside Migrations
- •Managing Migrations
- •Tables and Classes
- •Columns and Attributes
- •Primary Keys and IDs
- •Connecting to the Database
- •Aggregation and Structured Data
- •Miscellany
- •Creating Foreign Keys
- •Specifying Relationships in Models
- •belongs_to and has_xxx Declarations
- •Joining to Multiple Tables
- •Acts As
- •When Things Get Saved
- •Preloading Child Rows
- •Counters
- •Validation
- •Callbacks
- •Advanced Attributes
- •Transactions
- •Action Controller: Routing and URLs
- •The Basics
- •Routing Requests
- •Action Controller and Rails
- •Action Methods
- •Cookies and Sessions
- •Caching, Part One
- •The Problem with GET Requests
- •Action View
- •Templates
- •Using Helpers
- •How Forms Work
- •Forms That Wrap Model Objects
- •Custom Form Builders
- •Working with Nonmodel Fields
- •Uploading Files to Rails Applications
- •Layouts and Components
- •Caching, Part Two
- •Adding New Templating Systems
- •Prototype
- •Script.aculo.us
- •RJS Templates
- •Conclusion
- •Action Mailer
- •Web Services on Rails
- •Dispatching Modes
- •Using Alternate Dispatching
- •Method Invocation Interception
- •Testing Web Services
- •Protocol Clients
- •Secure and Deploy Your Application
- •Securing Your Rails Application
- •SQL Injection
- •Creating Records Directly from Form Parameters
- •Avoid Session Fixation Attacks
- •File Uploads
- •Use SSL to Transmit Sensitive Information
- •Knowing That It Works
- •Deployment and Production
- •Starting Early
- •How a Production Server Works
- •Repeatable Deployments with Capistrano
- •Setting Up a Deployment Environment
- •Checking Up on a Deployed Application
- •Production Application Chores
- •Moving On to Launch and Beyond
- •Appendices
- •Introduction to Ruby
- •Classes
- •Source Code
- •Resources
- •Index
- •Symbols
Chapter 24
Action Mailer
Action Mailer is a simple Rails component that allows your applications to send and receive e-mail. Using Action Mailer, your online store could send out order confirmations, and your incident-tracking system could automatically log problems submitted to a particular e-mail address.
24.1Sending E-mail
Before you start sending e-mail, you’ll need to configure Action Mailer. Its default configuration works on some hosts, but you’ll want to create your own configuration anyway, just to make it an explicit part of your application.
E-mail Configuration
E-mail configuration is part of a Rails application’s environment. If you want to use the same configuration for development, testing, and production, add the configuration to environment.rb in the config directory; otherwise, add different configurations to the appropriate files in the config/environments directory.
You first have to decide how you want mail delivered.
config.action_mailer.delivery_method = :smtp | :sendmail | :test
The :smtp and :sendmail options are used when you want Action Mailer to attempt to deliver e-mail. You’ll clearly want to use one of these methods in production.
The :test setting is great for unit and functional testing. E-mail will not be delivered but instead will be appended to an array (accessible via the attribute ActionMailer::Base.deliveries). This is the default delivery method in the test environment. Interestingly, though, the default in development mode is :smtp. If you want your development code to deliver e-mail, this is good. If you’d rather disable e-mail delivery in development mode, edit the file development.rb in the directory config/environments, and add the line
SENDING E-MAIL 568
config.action_mailer.delivery_method = :test
The :sendmail setting delegates mail delivery to your local system’s sendmail program, which is assumed to be in /usr/sbin. This delivery mechanism is not particularly portable, because sendmail is not always installed in this directory on different operating systems. It also relies on your local sendmail supporting the -i and -t command options.
You achieve more portability by leaving this option at its default value of :smtp. If you do so, though, you’ll need also to specify some additional configuration to tell Action Mailer where to find an SMTP server to handle your outgoing e-mail. This may be the machine running your web application, or it may be a separate box (perhaps at your ISP if you’re running Rails in a noncorporate environment). Your system administrator will be able to give you the settings for these parameters. You may also be able to determine them from your own mail client’s configuration.
config.action_mailer.server_settings = {
:address |
=> "domain.of.smtp.host.net", |
|
:port |
=> |
25, |
:domain |
=> |
"domain.of.sender.net" , |
:authentication => :login, :user_name => "dave", :password => "secret"
}
:address => and :port =>
Determines the address and port of the SMTP server you’ll be using. These default to localhost and 25, respectively.
:domain =>
The domain that the mailer should use when identifying itself to the server. This is called the HELO domain (because HELO is the command the client sends to the server to initiate a connection). You should normally use the top-level domain name of the machine sending the e-mail, but this depends on the settings of your SMTP server (some don’t check, and some check to try to reduce spam and so-called open-relay issues).
:authentication =>
One of :plain, :login, or :cram_md5. Your server administrator will help choose the right option. There is currently no way of using TLS (SSL) to connect to a mail server from Rails. This parameter should be omitted if your server does not require authentication. If you do omit this parameter, also omit (or comment out) the :user_name and :password options.
:user_name => and :password =>
Required if :authentication is set.
Report erratum
SENDING E-MAIL 569
Other configuration options apply to all delivery mechanisms.
config.action_mailer.perform_deliveries = true | false
If perform_deliveries is true (the default), mail will be delivered normally. If false, requests to deliver mail will be silently ignored. This might be useful to disable e-mail while testing.
config.action_mailer.raise_delivery_errors = true | false
If raise_delivery_errors is true (the default), any errors that occur when initially sending the e-mail will raise an exception back to your application. If false, errors will be ignored. Remember that not all e-mail errors are immediate—an e-mail might bounce three days after you send it, and your application will (you hope) have moved on by then.
Set the character set used for new e-mail with
config.action_mailer.default_charset = "utf-8"
As with all configuration changes, you’ll need to restart your application if you make changes to any of the environment files.
Sending E-mail
Now that we’ve got everything configured, let’s write some code to send e-mails.
By now you shouldn’t be surprised that Rails has a generator script to create mailers. What might be surprising is where it creates them. In Rails, a mailer is a class that’s stored in the app/models directory. It contains one or more methods, each method corresponding to an e-mail template. To create the body of the e-mail, these methods in turn use views (in just the same way that controller actions use views to create HTML and XML). So, let’s create a mailer for our store application. We’ll use it to send two different types of e-mail: one when an order is placed and a second when the order ships. The generate mailer script takes the name of the mailer class, along with the names of the e-mail action methods.
depot> ruby script/generate mailer OrderMailer confirm sent
exists |
app/models/ |
exists |
app/views/order_mailer |
exists |
test/unit/ |
create |
test/fixtures/order_mailer |
create |
app/models/order_mailer.rb |
create test/unit/order_mailer_test.rb |
|
create |
app/views/order_mailer/confirm.rhtml |
create |
test/fixtures/order_mailer/confirm |
create |
app/views/order_mailer/sent.rhtml |
create |
test/fixtures/order_mailer/sent |
Notice that we’ve created an OrderMailer class in app/models and two template files, one for each e-mail type, in app/views/order_mailer. (We also created a
Report erratum
SENDING E-MAIL 570
bunch of test-related files—we’ll look into these later in Section 24.3, Testing E-mail, on page 579.)
Each method in the mailer class is responsible for setting up the environment for sending a particular e-mail. It does this by setting up instance variables containing data for the e-mail’s header and body. Let’s look at an example before going into the details. Here’s the code that was generated for our OrderMailer class.
class OrderMailer < ActionMailer::Base
def confirm(sent_at = Time.now)
@subject |
= 'OrderMailer#confirm' |
@body |
= {} |
@recipients |
= '' |
@from |
= '' |
@sent_on |
= sent_at |
@headers |
= {} |
end |
|
def sent(sent_at = Time.now) @subject = 'OrderMailer#sent'
# ... same as above ...
end end
Apart from @body, which we’ll discuss in a second, the instance variables all set up the envelope and header of the e-mail that’s to be created:
@bcc = array or string
Blind-copy recipients, using the same format as @recipients.
@cc = array or string
Carbon-copy recipients, using the same format as @recipients.
@charset = string
The character set used in the e-mail’s Content-Type header. Defaults to the default_charset attribute in server_settings, or "utf-8".
@from = array or string
One or more e-mail addresses to appear on the From: line, using the same format as @recipients. You’ll probably want to use the same domain name in these addresses as the domain you configured in server_settings.
@headers = hash
A hash of header name/value pairs, used to add arbitrary header lines to the e-mail.
@headers["Organization" ] = "Pragmatic Programmers, LLC"
Report erratum
SENDING E-MAIL 571
@recipients = array or string
One or more recipient e-mail addresses. These may be simple addresses, such as dave@pragprog.com, or some identifying phrase followed by the e-mail address in angle brackets.
@recipients = [ "andy@pragprog.com", "Dave Thomas <dave@pragprog.com>" ]
@sent_on = time
A Time object that sets the e-mail’s Date: header. If not specified, the current date and time will be used.
@subject = string
The subject line for the e-mail.
The @body is a hash, used to pass values to the template that contains the e-mail. We’ll see how that works shortly.
E-mail Templates
The generate script created two e-mail templates in app/views/order_mailer, one for each action in the OrderMailer class. These are regular ERb rhtml files. We’ll use them to create plain-text e-mails (we’ll see later how to create HTML e- mail). As with the templates we use to create our application’s web pages, the files contain a combination of static text and dynamic content. We can customize the template in confirm.rhtml; this is the e-mail that is sent to confirm an order.
Download e1/mailer/app/views/order_mailer/confirm.rhtml
Dear <%= @order.name %>
Thank you for your recent
You ordered the following
order from The Pragmatic Store.
items:
<%= render(:partial => "./line_item", :collection => @order.line_items) %>
We'll send you a separate e-mail when your order ships.
There’s one small wrinkle in this template. We have to give render the explicit path to the template (the leading ./) because we’re not invoking the view from a real controller and Rails can’t guess the default location.
The partial template that renders a line item formats a single line with the item quantity and the title. Because we’re in a template, all the regular helper methods, such as truncate, are available.
Download e1/mailer/app/views/order_mailer/_line_item.rhtml
<%= sprintf("%2d x %s", line_item.quantity,
truncate(line_item.product.title, 50)) %>
Report erratum
SENDING E-MAIL 572
We now have to go back and fill in the confirm method in the OrderMailer class.
Download e1/mailer/app/models/order_mailer.rb
class OrderMailer < ActionMailer::Base def confirm(order)
@subject |
= "Pragmatic Store Order Confirmation" |
|
@recipients |
= order.email |
|
@from |
= |
'orders@pragprog.com' |
@sent_on |
= |
Time.now |
@body["order"] |
= order |
end end
Now we get to see what the @body hash does: values set into it are available as instance variables in the template. In this case, the order object will be stored into @order.
Generating E-mails
Now that we have our template set up and our mailer method defined, we can use them in our regular controllers to create and/or send e-mails. However, we don’t call the method directly. That’s because there are two different ways you can create e-mail from within Rails: you can create an e-mail as an object, or you can deliver an e-mail to its recipients. To access these functions, we call class methods called create_xxx and deliver_xxx, where xxx is the name of the instance method we wrote in OrderMailer. We pass to these class methods the parameter(s) that we’d like our instance methods to receive. To send an order confirmation e-mail, for example, we could call
OrderMailer.deliver_confirm(order)
To experiment with this without actually sending any e-mails, we can write a simple action that creates an e-mail and displays its contents in a browser window.
Download e1/mailer/app/controllers/test_controller.rb
class TestController < ApplicationController def create_order
order = Order.find_by_name("Dave Thomas") email = OrderMailer.create_confirm(order)
render(:text => "<pre>" + email.encoded + "</pre>") end
end
The create_confirm call invokes our confirm instance method to set up the details of an e-mail. Our template is used to generate the body text. The body, along with the header information, gets added to a new e-mail object, which create_confirm returns. The object is an instance of class TMail::Mail.1 The
1. TMail is Minero Aoki’s excellent e-mail library; a version ships with Rails.
Report erratum
SENDING E-MAIL 573
email.encoded call returns the text of the e-mail we just created: our browser will show something like
Date: Thu, 12 Oct 2006 12:17:36 -0500
From: orders@pragprog.com
To: dave@pragprog.com
Subject: Pragmatic Store Order Confirmation
Mime-Version: 1.0
Content-Type: text/plain; charset=utf-8
Dear Dave Thomas
Thank you for your recent order from The Pragmatic Store.
You ordered the following items:
1 x Programming Ruby, 2nd Edition
1 x Pragmatic Project Automation
We'll send you a separate e-mail when your order ships.
If we’d wanted to send the e-mail, rather than just create an e-mail object, we could have called OrderMailer.deliver_confirm(order).
Delivering HTML-Format E-mail
One way of creating HTML e-mail is to create a template that generates HTML for the e-mail body and then set the content type on the TMail::Mail object to text/html before delivering the message.
We’ll start by implementing the so much commonality between that we’d probably refactor both
sent method in OrderMailer. (In reality, there’s this method and the original confirm method to use a shared helper.)
Download e1/mailer/app/models/order_mailer.rb
class OrderMailer |
< ActionMailer::Base |
def sent(order) |
|
@subject |
= "Pragmatic Order Shipped" |
@recipients |
= order.email |
@from |
= 'orders@pragprog.com' |
@sent_on |
= Time.now |
@body["order"] = order end
end
Next, we’ll write the sent.rhtml template.
Download e1/mailer/app/views/order_mailer/sent.rhtml
<h3>Pragmatic Order Shipped</h3> <p>
This is just to let you know that we've shipped your recent order:
</p>
Report erratum
SENDING E-MAIL 574
<table>
<tr><th colspan="2">Qty</th><th>Description</th></tr>
<%= render(:partial => "./html_line_item", :collection => @order.line_items) %>
</table>
We’ll need a new partial template that generates table rows. This goes in the file _html_line_item.rhtml.
Download e1/mailer/app/views/order_mailer/_html_line_item.rhtml
<tr>
<td><%= html_line_item.quantity %></td> <td>×</td>
<td><%= html_line_item.product.title %></td> </tr>
And finally we’ll test this using an action method that renders the e-mail, sets the content type to text/html, and calls the mailer to deliver it.
Download e1/mailer/app/controllers/test_controller.rb
class TestController < ApplicationController def ship_order
order = Order.find_by_name("Dave Thomas") email = OrderMailer.create_sent(order) email.set_content_type("text/html") OrderMailer.deliver(email)
render(:text => "Thank you...") end
end
The resulting e-mail will look something like Figure 24.1, on the next page.
Delivering Multiple Content Types
Some people prefer receiving e-mail in plain-text format, while others like the look of an HTML e-mail. Rails makes it easy to send e-mail messages that contain alternative content formats, allowing the user (or their e-mail client) to decide what they’d prefer to view.
In the preceding section, we created an HTML e-mail by generating HTML content and then setting the content type to text/html. It turns out that Rails has a convention that will do all this, and more, automatically.
The view file for our sent action was called sent.rhtml. This is the standard Rails naming convention. But, for e-mail templates, there’s a little bit more naming magic. If you name a template file
name.content.type.rhtml
Rails will automatically set the content type of the e-mail to the content type in the filename. For our previous example, we could have set the view filename to sent.text.html.rhtml, and Rails would have sent it as an HTML e-mail automatically. But there’s more. If you create multiple templates with the
Report erratum
SENDING E-MAIL 575
Figure 24.1: An HTML-Format E-mail
same name but with different content types embedded in their filenames, Rails will send all of them in one e-mail, arranging the content so that the e-mail client will be able to distinguish each. Thus by creating sent.text.plain.rhtml and sent.text.html.rhtml templates, we could give the user the option of viewing our e-mail as either text or HTML.
Let’s try this. We’ll set up a new action.
Download e1/mailer/app/controllers/test_controller.rb
def survey
order = Order.find_by_name("Dave Thomas") email = OrderMailer.deliver_survey(order) render(:text => "E-Mail sent")
end
We’ll add support for the survey to order_mailer.rb in the app/models directory.
Download e1/mailer/app/models/order_mailer.rb
def survey(order)
@subject |
= "Pragmatic Order: Give us your thoughts" |
|
@recipients |
= order.email |
|
@from |
= |
'orders@pragprog.com' |
@sent_on |
= |
Time.now |
@body["order"] |
= order |
end
And we’ll create two templates. Here’s the plain-text version, in the file survey.text.plain.rhtml.
Report erratum
SENDING E-MAIL 576
Download e1/mailer/app/views/order_mailer/survey.text.plain.rhtml
Dear <%= @order.name %>
You recently placed an order with our store.
We were wondering if you'd mind taking the time to visit http://some.survey.site and rate your experience.
Many thanks
And here’s survey.text.html.rhtml, the template that generates the HTML e-mail.
Download e1/mailer/app/views/order_mailer/survey.text.html.rhtml
<h3>A Pragmatic Survey</h3>
<p>
Dear <%= @order.name %>
</p>
<p>
You recently placed an order with our store.
</p>
<p>
We were wondering if you'd mind taking the time to
visit <a href="http://some.survey.site" >our survey site</a> and rate your experience.
<p>
<p>
Many thanks.
</p>
You can also use the part method within an Action Mailer method to create multiple content types explicitly. See the Rails API documentation for ActionMailer::Base for details.
Sending Attachments
When you send e-mail with multiple content types, Rails actually creates a separate e-mail attachment for each. This all happens behind the scenes. However, you can also manually add your own attachments to e-mails.
Let’s create a different version of our confirmation e-mail that sends cover images as attachments. The action is called ship_with_images.
Download e1/mailer/app/controllers/test_controller.rb
def ship_with_images
order = Order.find_by_name("Dave Thomas")
email = OrderMailer.deliver_ship_with_images(order) render(:text => "E-Mail sent")
end
Report erratum
SENDING E-MAIL 577
The template is the same as the original sent.rhtml file.
Download e1/mailer/app/views/order_mailer/sent.rhtml
<h3>Pragmatic Order Shipped</h3> <p>
This is just to let you know that we've shipped your recent order:
</p>
<table>
<tr><th colspan="2">Qty</th><th>Description</th></tr>
<%= render(:partial => "./html_line_item", :collection => @order.line_items) %>
</table>
All the interesting work takes place in the ship_with_images method in the mailer class.
Download e1/mailer/app/models/order_mailer.rb
def ship_with_images(order)
@subject |
= "Pragmatic Order Shipped" |
|
@recipients |
= order.email |
|
@from |
= |
'orders@pragprog.com' |
@sent_on |
= |
Time.now |
@body["order"] |
= order |
part :content_type => "text/html",
:body => render_message("sent" , :order => order)
order.line_items.each do |li| image = li.product.image_location
content_type = case File.extname(image)
when ".jpg", ".jpeg"; |
"image/jpeg" |
when ".png"; |
"image/png" |
when ".gif"; |
"image/gif" |
else; |
"application/octet-stream" |
end |
|
attachment :content_type => content_type, |
|
:body |
=> File.read(File.join("public" , image)), |
:filename |
=> File.basename(image) |
end end
Notice that this time we explicitly render the message using a part directive, forcing its type to be text/html and its body to be the result of rendering the template.2 We then loop over the line items in the order. For each, we determine the name of the image file, construct the mime type based on the file’s extension, and add the file as an inline attachment.
2. At the time of writing, there’s a minor bug in Rails. If a message has attachments, Rails will not render the default template for the message if you name it using the xxx.text.html.rhtml convention. Adding the content explicitly using part works fine.
Report erratum
RECEIVING E-MAIL 578
24.2Receiving E-mail
Action Mailer makes it easy to write Rails applications that handle incoming e-mail. Unfortunately, you also need to find a way of getting appropriate e- mails from your server environment and injecting them into the application; this requires a bit more work.
The easy part is handling an e-mail within your application. In your Action Mailer class, write an instance method called receive that takes a single parameter. This parameter will be a TMail::Mail object corresponding to the incoming e-mail. You can extract fields, the body text, and/or attachments and use them in your application.
For example, a bug-tracking system might accept trouble tickets by e-mail. From each e-mail, it constructs a Ticket model object containing the basic ticket information. If the e-mail contains attachments, each will be copied into a new TicketCollateral object, which is associated with the new ticket.
Download e1/mailer/app/models/incoming_ticket_handler.rb
class IncomingTicketHandler < ActionMailer::Base
def receive(email) ticket = Ticket.new
ticket.from_email = email.from[0] ticket.initial_report = email.body if email.has_attachments?
email.attachments.each |
do |attachment| |
|
collateral = TicketCollateral.new( |
||
:name |
=> |
attachment.original_filename, |
:body |
=> |
attachment.read) |
ticket.ticket_collaterals << collateral end
end ticket.save
end end
So now we have the problem of feeding an e-mail received by our server computer into the receive instance method of our IncomingTicketHandler. This problem is actually two problems in one: first we have to arrange to intercept the reception of e-mails that meet some kind of criteria, and then we have to feed those e-mails into our application.
If you have control over the configuration of your mail server (such as a Postfix or sendmail installation on Unix-based systems), you might be able to arrange to run a script when an e-mail addressed to a particular mailbox or virtual host is received. Mail systems are complex, though, and we don’t have room to go into all the possible configuration permutations here. There’s a good
Report erratum
TESTING E-MAIL 579
introduction to this on the Ruby development wiki.3
If you don’t have this kind of system-level access but you are on a Unix system, you could intercept e-mail at the user level by adding a rule to your .procmailrc file. We’ll see an example of this shortly.
The objective of intercepting incoming e-mail is to pass it to our application. To do this, we use the Rails runner facility. This allows us to invoke code within our application’s codebase without going through the Web. Instead, the runner loads up the application in a separate process and invokes code that we specify in the application.
All of the normal techniques for intercepting incoming e-mail end up running a command, passing that command the content of the e-mail as standard input. If we make the Rails runner script the command that’s invoked whenever an e-mail arrives, we can arrange to pass that e-mail into our application’s e- mail handling code. For example, using procmail-based interception, we could write a rule that looks something like the example that follows. Using the arcane syntax of procmail, this rule copies any incoming e-mail whose subject line contains Bug Report through our runner script.
RUBY=/Users/dave/ruby1.8/bin/ruby
TICKET_APP_DIR=/Users/dave/Work/BS2/titles/RAILS/Book/code/e1/mailer
HANDLER='IncomingTicketHandler.receive(STDIN.read)'
:0 c
* ^Subject:.*Bug Report.*
| cd $TICKET_APP_DIR && $RUBY script/runner $HANDLER
The receive class method is available to all Action Mailer classes. It takes the e-mail text passed as a parameter, parses it into a TMail object, creates a new instance of the receiver’s class, and passes the TMail object to the receive instance method in that class. This is the method we wrote on the preceding page. The upshot is that an e-mail received from the outside world ends up creating a Rails model object, which in turn stores a new trouble ticket in the database.
24.3Testing E-mail
There are two levels of e-mail testing. At the unit test level you can verify that your Action Mailer classes correctly generate e-mails. At the functional level, you can test that your application sends these e-mails when you expect it to send them.
3. http://wiki.rubyonrails.com/rails/show/HowToReceiveEmailsWithActionMailer
Report erratum
TESTING E-MAIL 580
Unit Testing E-mail
When we used the generate script to create our order mailer, it automatically constructed a corresponding order_mailer_test.rb file in the application’s test/unit directory. If you were to look at this file, you’d see that it is fairly complex. That’s because it lets you read the expected content of e-mails from fixture files and compare this content to the e-mail produced by your mailer class. However, this is fairly fragile testing. Anytime you change the template used to generate an e-mail, you’ll need to change the corresponding fixture.
If exact testing of the e-mail content is important to you, then use the pregenerated test class. Create the expected content in a subdirectory of the test/fixtures directory named for the test (so our OrderMailer fixtures would be in test/fixtures/order_mailer). Use the read_fixture method included in the generated code to read in a particular fixture file and compare it with the e-mail generated by your model.
However, I prefer something simpler. In the same way that I don’t test every byte of the web pages produced by templates, I won’t normally bother to test the entire content of a generated e-mail. Instead, I test the part that’s likely to break: the dynamic content. This simplifies the unit test code and makes it more resilient to small changes in the template. Here’s a typical e-mail unit test.
Download e1/mailer/test/unit/order_mailer_test.rb
require File.dirname(__FILE__) + '/../test_helper' require 'order_mailer'
class OrderMailerTest < Test::Unit::TestCase
def setup
@order = Order.new(:name =>"Dave Thomas", :email => "dave@pragprog.com") end
def test_confirm
response = OrderMailer.create_confirm(@order) assert_equal("Pragmatic Store Order Confirmation" , response.subject) assert_equal("dave@pragprog.com", response.to[0])
assert_match(/Dear Dave Thomas/, response.body) end
end
The setup method creates an order object for the mail sender to use. In the test method we get the mail class to create (but not to send) an e-mail, and we use assertions to verify that the dynamic content is what we expect. Note the use of assert_match to validate just part of the body content.
Report erratum
TESTING E-MAIL 581
Functional Testing of E-mail
Now that we know that e-mails can be created for orders, we’d like to make sure that our application sends the correct e-mail at the right time. This is a job for functional testing.
Let’s start by generating a new controller for our application.
depot> ruby script/generate controller Order confirm
We’ll implement the single action, confirm, which sends the confirmation e-mail for a new order.
Download e1/mailer/app/controllers/order_controller.rb
class OrderController < ApplicationController def confirm
order = Order.find(params[:id]) OrderMailer.deliver_confirm(order) redirect_to(:action => :index)
end end
We saw how Rails constructs a stub functional test for generated controllers back in Section 13.3, Functional Testing of Controllers, on page 197. We’ll add our mail testing to this generated test.
Action Mailer does not deliver e-mail in the test environment. Instead, it adds each e-mail it generates to an array, ActionMailer::base.deliveries. We’ll use this to get at the e-mail generated by our controller. We’ll add a couple of lines to the generated test’s setup method. One line aliases this array to the more manageable name @emails. The second clears the array at the start of each test.
Download e1/mailer/test/functional/order_controller_test.rb
@emails |
= ActionMailer::Base.deliveries |
@emails.clear
We’ll also need a fixture holding a sample order. We’ll create a file called orders.yml in the test/fixtures directory.
Download e1/mailer/test/fixtures/orders.yml
daves_order: id: 1
name: Dave Thomas address: 123 Main St email: dave@pragprog.com
Now we can write a test for our action. Here’s the full source for the test class.
Download e1/mailer/test/functional/order_controller_test.rb
require File.dirname(__FILE__) + '/../test_helper' require 'order_controller'
Report erratum
TESTING E-MAIL 582
# Re-raise errors caught by the controller.
class OrderController; def rescue_action(e) raise e end; end
# continued...
class OrderControllerTest < Test::Unit::TestCase
fixtures :orders
def setup |
|
|
@controller |
= OrderController.new |
|
@request |
= ActionController::TestRequest.new |
|
@response |
= |
ActionController::TestResponse.new |
@emails |
= |
ActionMailer::Base.deliveries |
@emails.clear end
def test_confirm
get(:confirm, :id => orders(:daves_order).id) assert_redirected_to(:action => :index) assert_equal(1, @emails.size)
email = @emails.first
assert_equal("Pragmatic Store Order Confirmation" , email.subject) assert_equal("dave@pragprog.com", email.to[0])
assert_match(/Dear Dave Thomas/, email.body) end
end
It uses the @emails alias to access the array of e-mails generated by Action Mailer since the test started running. Having checked that exactly one e-mail is in the list, it then validates the contents are what we expect.
We can run this test either by using the test_functional target of rake or by executing the script directly.
depot> ruby test/functional/order_controller_test.rb
Report erratum