Action Mailer - The Rails 4 Way (2014)

The Rails 4 Way (2014)

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 letting users control their accounts 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 by writing mailer models, 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 3rd-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 notices 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 classLateNotice < ActionMailer::Base

2 default from: "from@example.com"

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 classLateNotice < ActionMailer::Base

2 default from: "system@timeandexpenses.com"

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 above:

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, ie { "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: "user@example.com") 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:

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: system@timeandexpenses.com

To: aslak.hellesoy@gmail.com

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 method this would be in (or .) You can also override this template name in the block.

1 mail(to: "user@example.com") 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 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 classLateNotice < 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: "test@myapp.com",

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 attachments hash, 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 the must be rendered as absolute URLs.

1 classLateNotice < 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: "test@myapp.com",

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 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 classLateNotice < 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 a 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 post processing 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 classLateNotice < 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: "test@myapp.com",

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 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::Message82 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.83

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

16.2: The simple MessageArchiver mailer class with a receive method


1 classMessageArchiver < 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.84

1 classPhotoByEmail < 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}]") andreturnunless 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 to wrestle with the configuration of your mail-processor (outside of Rails) since they are notoriously difficult to configure.85 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.

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:

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_spec86 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.

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

Match for text in the body.

have_header

Check 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: "joe@example.com") }

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("joe@example.com")

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 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 classLateNoticePreview < 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.