Chapter 16. Action Mailer

It’s a cool way to send emails without tons of code.

—Jake Scruggs

Integration with email is a crucial part of most modern web application projects. Whether it’s sign-up confirmations, password recovery, or account control via email, you’ll be happy to hear that Rails offers great support for both sending and receiving email, thanks to its Action Mailer framework.

In this chapter, we’ll cover what’s needed to set up your deployment to be able to send and receive mail with the Action Mailer framework and what’s needed to write mailer models, which are the entities in Rails that encapsulate code having to do with email handling.

16.1 Setup

By default, Rails will try to send email via SMTP (port 25) of localhost. If you are running Rails on a host that has an SMTP daemon running and it accepts SMTP email locally, you don’t have to do anything else in order to send mail. If you don’t have SMTP available on localhost, you have to decide how your system will send email.

When not using SMTP directly, the main options are to use sendmail or to give Rails information on how to connect to an external mail server. Most organizations have SMTP servers available for this type of use, although it’s worth noting that, due to abuse, many hosting providers have stopped offering shared SMTP service.

Most serious production deployments use third-party SMTP services that specialize in delivering automated email, avoiding user spam filters and blacklists.

16.2 Mailer Models

Assuming the mail system is configured, let’s go ahead and create a mailer model that will contain code pertaining to sending and receiving a class of email. Rails provides a generator to get us started rapidly. Our mailer will send out a notice to any user of our sample application who is late entering their time.

$ rails generate mailer LateNotice
  create  app/mailers/late_notice.rb
  invoke  haml
  create    app/views/late_notice
  invoke  rspec
  create    spec/mailers/late_notice_spec.rb

A view folder for the mailer is created at app/views/late_notice, and the mailer itself is stubbed out at app/mailers/late_notice.rb:

1 class LateNotice < ActionMailer::Base
2   default from: "[email protected]"
3 end

Kind of like a default Active Record subclass, there’s not much there at the start.

16.2.1 Preparing Outbound Email Messages

You work with Action Mailer classes by defining public mailer methods that correspond to types of emails that you want to send. Inside the public method, you assign any variables that will be needed by the email message template and then call the mail method, which is conceptually similar to the render method used in controllers.

Continuing with our example, let’s write a late_timesheet mailer method that takes user and week_of parameters. Notice that it sets the basic information needed to send our notice email (see Listing 16.1).

Listing 16.1 Adding a Mailer Method

 1 class LateNotice < ActionMailer::Base
 2     default from: "[email protected]"
 3
 4     def late_timesheet(user, week_of)
 5       @recipient = user.name
 6       @week = week_of
 7       attachments["image.png"] = File.read("/images/image.png")
 8       mail(
 9         to: user.email,
10         subject: "[Time and Expenses] Timesheet notice"
11       )
12     end
13   end

Inside the method we’ve created, we have access to a few methods to set up the message for delivery, including the mail method shown earlier:

attachments Allows you to add normal and inline file attachments to your message.

1 attachments["myfile.zip"] = File.read("/myfile.zip")
2 attachments.inline["logo.png"] = File.read("/logo.png")

headers Allows you to supply a hash of custom email headers.

1 headers("X-Author" => "Obie Fernandez")

mail Sets up the email that will get sent. It accepts a hash of headers that a Mail::Message will accept and allows an optional block. If no block is specified, views will be used to construct the email with the same name as the method in the mailer. If a block is specified, these can be customized.

Note also the change of the default from address to one set up for our application. Here is a sample list of the headers that you can include in the hash passed to the mail method or in the default macro. In addition to these, you may pass any email header that is needed when sending—that is, { "X-Spam" => value }.

subject The subject line for the message.

to The recipient addresses for the message, either as a string (for a single address) or an array (for multiple addresses). Remember that this method expects actual address strings, not your application’s user objects.

users.map(&:email)

from Specifies the from address for the message as a string (required).

cc Specifies carbon copy recipient (cc:) addresses for the message, either as a string (for a single address) or an array for multiple addresses.

bcc Specifies blind recipient (bcc:) addresses for the message, either as a string (for a single address) or an array for multiple addresses.

reply_to Sets the email for the reply-to header.

date An optional explicit sent on date for the message, usually passed Time.now. Will be automatically set by the delivery mechanism if you don’t supply a value and cannot be set using the default macro.

The mail method can either take a block or not if you want to do custom formats similar to Rails routes.

1 mail(to: "[email protected]") do |format|
2   format.text
3   format.html
4 end

The body of the email is created by using an Action View template (regular Haml or ERb) that has the instance variables in the mailer available as instance variables in the template. So the corresponding body template for the mailer method in Listing 16.1 could look like the following:

1 Dear #{@recipient},
2
3 Your timesheet for the week of #{@week} is late.

And if the recipient was Aslak, the email generated would look like this:

Date: Sun, 12 Dec 2004 00:00:00 +0100
From: [email protected]
To: [email protected]
Subject: [Time and Expenses] Late timesheet notice

Dear Aslak Hellesoy,

Your timesheet for the week of Aug 15th is late.

16.2.2 HTML Email Messages

To send mail as HTML, make sure your view template generates HTML and that the corresponding template name corresponds to the email method name. For our late_timesheet method, this would be in app/views/late_notice/late_timesheet.html.haml (or .erb). You can also override this template name in the mail block.

1 mail(to: "[email protected]") do |format|
2   format.text
3   format.html { render "another_template" }
4 end

16.2.3 Multipart Messages

If a plain text and HTML template are present for a specific mailer action, the text template and the HTML template will both get sent by default as a multipart message. The HTML part will be flagged as alternative content for those email clients that support it.

16.2.3.1 Implicit Multipart Messages

As mentioned earlier in the chapter, multipart messages can also be used implicitly without invoking the part method, because Action Mailer will automatically detect and use multipart templates, where each template is named after the name of the action followed by the content type. Each such detected template will be added as a separate part to the message.

For example, if the following templates existed, each would be rendered and added as a separate part to the message with the corresponding content type. The same body hash is passed to each template.

signup_notification.text.haml

signup_notification.text.html.haml

signup_notification.text.xml.builder

signup_notification.text.yaml.erb

16.2.4 Attachments

Including attachments in emails is relatively simple; just use the method in your class.

 1 class LateNotice < ActionMailer::Base
 2   def late_timesheet(user, week_of)
 3     @recipient = user.name
 4     attachments["image.png"] = File.read("/images/image.png")
 5     mail(
 6       to: user.email,
 7       from: "[email protected]",
 8       subject: "[Time and Expenses] Timesheet notice"
 9     )
10   end
11 end

If you wanted to attach the image inline, use attachments.inline.

attachments.inline["image.png"] = File.read("/images/image.png")

You can access this attachment in the template if need be via the attachments hash and then calling url on that object for the image’s relative content id (cid:) path.

1 Dear #{@recipient},
2
3 Your timesheet is late. Here's a photo depicting our sadness:
4
5 = image_tag attachments['image.png'].url, alt: "Invoicing"

16.2.5 Generating URLs

Generating application URLs is handled through named routes or using the url_for helper. Since mail does not have request context like controllers do, the host configuration option needs to be set. The best practice for this is to define them in the corresponding environment configuration, although it can be defined on a per mailer basis.

# config/environments/production.rb
config.action_mailer.default_url_options = { host: 'accounting.com' }

In your mailer, you can now generate your URL. It is important to note that you cannot use the _path variation for your named routes since they must be rendered as absolute URLs.

 1 class LateNotice < ActionMailer::Base
 2   def late_timesheet(user, week_of)
 3     @recipient = user.name
 4     @link = user_url(user)
 5     mail(
 6       to: user.email,
 7       from: "[email protected]",
 8       subject: "[Time and Expenses] Timesheet notice"
 9     )
10   end
11 end

When generating URLs through url_for, the controller and action also need to be specified. If you have provided a default host, then the :only_path option must be provided to tell the helper to generate an absolute path.

= url_for(controller: "users", action: "update", only_path: false)

16.2.6 Mailer Layouts

Mailer layouts behave just like controller layouts. To be automatically recognized, they need to have the same name as the mailer itself. In our previous case, they would automatically be used for our HTML emails. You can also add custom layouts if your heart desires, either at the class level or as a render option.

1 class LateNotice < ActionMailer::Base
2   layout "alternative"
3
4   def late_timesheet(user, week_of)
5     mail(to: user.email) do |format|
6       format.html { render layout: "another" }
7     end
8   end
9 end

We’ve now talked extensively about preparing email messages for sending, but what about actually sending them to the recipients?

16.2.7 Sending an Email

Sending emails only involves getting an object from your mailer and delivering it.

1 aslak = User.find_by(name: "Aslak Hellesoy")
2 message = LateNotice.late_timesheet(aslak, 1.week.ago)
3 message.deliver

16.2.8 Callbacks

As of Rails 4, the ability to define action callbacks for a mailer was added. Like their Action Controller counterparts, one could specify before_action, after_action, and around_action callbacks to run shared pre- and postprocessing code within a mailer.

Callbacks can accept one or more symbols, representing a matching method in the mailer class:

before_action :set_headers

Or you can pass the callback a block to execute, like this:

before_action { logger.info "Sending out an email!" }

A common example of why you would use a callback in a mailer is to set inline attachments, such as images that are used within the email template.

 1 class LateNotice < ActionMailer::Base
 2   before_action :set_inline_attachments
 3
 4   def late_timesheet(user, week_of)
 5     @recipient = user.name
 6     mail(
 7       to: user.email,
 8       from: "[email protected]",
 9       subject: "[Time and Expenses] Timesheet notice"
10     )
11   end
12
13   protected
14
15   def set_inline_attachments
16     attachments["logo.png"] = File.read("/images/logo.png")
17   end
18 end

Action callbacks are covered in detail in Chapter 4, “Working with Controllers,” in the “Action Callbacks” section.

16.3 Receiving Emails

To receive emails, you need to write a public method named receive on one of your application’s ActionMailer::Base subclasses. It will take a Mail::Message1 object instance as its single parameter. When there is incoming email to handle, you call an instance method named receive on your Mailer class. The raw email string is converted into a Mail::Message object automatically, and your receive method is invoked for further processing. You don’t have to implement the receive class method yourself; it is inherited from ActionMailer::Base.2

1. http://github.com/mikel/mail

2. If you are using a third-party email service, such as SendGrid, be sure to check out the Griddler gem by Thoughtbot. It’s a Rails engine that hands off preprocessed email objects to a class solely responsible for processing the incoming email: http://github.com/thoughtbot/griddler

That’s all pretty confusing to explain but simple in practice. Listing 16.2 shows an example.

Listing 16.2 The Simple MessageArchiver Mailer Class with a Receive Method

 1 class MessageArchiver < ActionMailer::Base
 2
 3     def receive(email)
 4       person = Person.where(email: email.to.first).first!
 5       person.emails.create(
 6         subject: email.subject,
 7         body: email.body
 8       )
 9     end
10   end

The receive class method can be the target for a Postfix recipe or any other mail-handler process that can pipe the contents of the email to another process. The rails runner command makes it easy to handle incoming mail:

$ rails runner 'MessageArchiver.receive(STDIN.read)'

That way, when a message is received, the receive class method would be fed the raw string content of the incoming email via STDIN.

16.3.1 Handling Incoming Attachments

Processing files attached to incoming email messages is just a matter of using the attachments attribute of Mail::Message, as in Listing 16.3. This example assumes that you have a Person class with a has_many association photos that contains a Carrierwave attachment.3

3. Carrierwave, created by Jonas Nicklas, can be found at http://github.com/jnicklas/carrierwave

 1 class PhotoByEmail < ActionMailer::Base
 2
 3   def receive(email)
 4     from = email.from.first
 5     person = Person.where(email: from).first
 6     logger.warn("Person not found [#{from}]") and return unless person
 7
 8     if email.has_attachments?
 9       email.attachments.each do |file|
10         person.photos.create(asset: file)
11       end
12     end
13   end
14 end

There’s not much more to it than that, except of course wrestling with the configuration of your mail processor (outside of Rails), since they are notoriously difficult to configure.4 After you have your mail processor calling the rails runner command correctly, add a crontab so that incoming mail is handled about every five minutes or so, depending on the needs of your application.

4. Rob Orsini, author of O’Reilly’s Rails Cookbook recommends getmail, which you can get from http://pyropus.ca/software/getmail

16.4 Server Configuration

Most of the time, you don’t have to configure anything specifically to get mail sending to work, because your production server will have sendmail installed and Action Mailer will happily use it to send emails.

If you don’t have sendmail installed on your server, you can try setting up Rails to send email directly via SMTP. The ActionMailer::Base class has a hash named smtp_settings that holds configuration information. The settings here will vary depending on the SMTP server that you use.

The sample (as shown in Listing 16.3) demonstrates the SMTP server settings that are available (and their default values). You’ll want to add similar code to your config/environment.rb file:

Listing 16.3 SMTP Settings for ActionMailer

1 ActionMailer::Base.smtp_settings = {
2     address: 'smtp.yourserver.com', # default: localhost
3     port: 25,  # default: 25
4     domain: 'yourserver.com', # default: localhost.localdomain
5     user_name: 'user', # no default
6     password: 'password', # no default
7     authentication: :plain  # :plain, :login or :cram_md5
8   }

16.5 Testing Email Content

Ben Mabey’s email_spec5 gem provides a nice way to test your mailers using RSpec. Add it to your Gemfile and first make the following additions to your spec/spec_helper.rb.

5. http://github.com/bmabey/email-spec

1 RSpec.configure do |config|
2   config.include(EmailSpec::Helpers)
3   config.include(EmailSpec::Matchers)
4 end

Mailer specs reside in spec/mailers, and email_spec provides convenience matchers for asserting that the mailer contains the right attributes.

reply_to Checks the reply-to value.

deliver_to Verifies the recipient.

deliver_from Assertion for the sender.

bcc_to Verifies the Bcc.

cc_to Verifies the Cc.

have_subject Performs matching of the subject text.

include_email_with_subject Performs matching of the subject text in multiple emails.

have_body_text Matches for text in the body.

have_header Checks for a matching email header.

These matchers can then be used to assert that the generated email has the correct content included in it.

 1 require "spec_helper"
 2
 3 describe InvoiceMailer do
 4   let(:invoice) { Invoice.new(name: "Acme", email: "[email protected]") }
 5
 6   describe "#create_late" do
 7     subject(:email) { InvoiceMailer.create_late(invoice) }
 8
 9     it "delivers to the invoice email" do
10       expect(email).to deliver_to("[email protected]")
11     end
12
13     it "contains the invoice name" do
14       expect(email).to have_body_text(/Acme/)
15     end
16
17     it "has a late invoice subject" do
18       expect(email).to have_subject(/Late Invoice/)
19     end
20   end
21 end

If you’re attempting to test whether or not the mailer gets called and sends the email, it is recommended to simply check via a mock that the deliver method got executed.

16.6 Previews

By default in Rails, all email messages sent in development via Action Mailer are delivered in test mode. This means that if you send an email from your application, the output of that message would display in your development log. While this can show you if the output is correct, it does not indicate if the email message is rendered correctly. A way around this would be to connect your development environment to an actual SMTP server. Even though this would allow you to view the email in your mail client of choice, it’s also a very bad idea, as you could potentially email real people.

New to Action Mailer as of Rails 4.1 is previews, which provides a means of rendering plain text and HTML mail templates in your browser without having to deliver them.

To create a preview, define a class that inherits from ActionMailer::Preview. Methods defined within the class must return a Mail::Message object, which can be created by calling a mailer method.

1 class LateNoticePreview < ActionMailer::Preview
2   def late_timesheet
3     user = FactoryGirl.create(:user)
4     LateNotice.late_timesheet(user, 1.week.ago)
5   end
6 end

By default, all previews are located in the test/mailers/previews directory; however, this directory path can be overridden using the preview_path configuration option.

1 # For those using RSpec
2 config.action_mailer.preview_path = "#{Rails.root}/spec/mailers/previews"

To obtain a listing of all Action Mailer previews available within your application, navigate to http://localhost:3000/rails/mailers/ while running a local development server instance.

16.7 Conclusion

In this chapter, we learned how Rails makes sending and receiving email easy. With relatively little code, you can set up your application to send out email, even HTML email with inline graphics attachments. Receiving email is even easier, except perhaps for setting up mail-processing scripts and cron jobs. We also briefly covered the configuration settings that go in your application’s environment-specific configuration related to mail.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.171.125