Sending Email in Rails
Updated to work with Ruby 2.5.3 and Rails 5.2.4.3
Learning Goals
We’ll explore sending email in Rails by building a project that requires this functionality. By the end you should understand:
- How to use ActionMailer
- Send email locally using Mailcatcher
- How to test that a mailer is working
Warm Up
- Why are emails important?
Friendly Advice Exercise
We are going to build an application that allows us to send our friend some friendly advice via email.
What we’d like our app to do:
- Allow us to sign up / log in
- Allow us to enter a friend’s email address
- Send that friend some advice.
Researching with the Docs
Sending Email locally with Mailcatcher
Getting Started
For this tutorial, we are going to setup our emails to “send” through Mailcatcher locally. And in production, you’ll want to set up your Rails environment to send all emails through a third-party service like SendGrid.
First things first, go ahead and clone down this repo
$ git clone https://github.com/turingschool-examples/friendly-advice.git friendly-advice
$ cd friendly-advice
$ bundle
$ rake db:{drop,create,migrate,seed}
Rails Setup
Run your server to see what we’ve already got in our repo
rails s
Inspect the Form/Input field, where does this route to?
Make sure the route is in the routes file:
post '/advice', to: 'advice#create'
Next we’ll open our Advice Controller. The form asks for a POST route so we’ll need to update the create action where we will call our mailer.
class AdviceController < ApplicationController
def show
redirect_to root_path unless logged_in?
end
def create
@advice = AdviceGenerator.new
# `recipient` is the email address that should be receiving the message
recipient = params[:friends_email]
# `email_info` is the information that we want to include in the email message.
email_info = {
user: current_user,
friend: params[:friends_name],
message: @advice.message
}
FriendNotifierMailer.inform(email_info, recipient).deliver_now
flash[:notice] = 'Thank you for sending some friendly advice.'
redirect_to advice_url
end
end
Creating the Mailer
First, we set up our ApplicationMailer to set an automatic “From” address for every outgoing email. There’s no easy way to change this in the default setup of Rails; you can do this with a third-party email service like SendGrid.
The ‘layout’ option tells Rails where in /app/views/layouts/
to look for layout files, similar to how /app/views/layouts/application.html.erb
affects all HTML view pages with a yield
command. In this case, we’re configuring our setup to look for /app/views/layouts/mailer.html.erb
and /app/views/layouts/mailer.text.erb
for HTML-based emails and plaintext-based emails, respectively.
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: 'friendly@advice.io'
layout 'mailer'
end
Our next step will be to create the FriendNotifer mailer to send our friends advice. We’ll use a generator for this, but the generator will ignore the -T
we used to create the Rails app and generate some scaffolding for tests under a /test/
folder, which we can delete afterward.
rails g mailer FriendNotifier
rm -rf test/
This creates our friend_notifer_mailer
inside app/mailers
and a friend_notifer_mailer
folder in app/views
. Let’s first open the friend_notifer_mailer
in mailers and add our inform method.
# mailers/friend_notifier_mailer.rb
class FriendNotifierMailer < ApplicationMailer
def inform(info, recipient)
@user = info[:user]
@message = info[:message]
@friend = info[:friend]
mail(
reply_to: @user.email,
to: recipient,
subject: "#{@user.name} is sending you some advice"
)
end
end
Notice that in our call to ActionMailer::Base#mail that we’re setting a reply-to email address of our user. This allows the recipient to hit ‘reply’ on an email and their response will go back to our user, not to our “default” email address of “friendly@advice.io”
Next we’ll make the views that will have the body of the email that is sent. Similar to controllers, any instance variables you create in your mailer method will be available in your mailer view.
In app/views/friend_notifier_mailer
create two files, inform.html.erb
and inform.text.erb
Depending on the person’s email client you’re sending the email to, it will render either the plain text or the HTML view. We don’t have control over that, so we want to accomodate both and make them have the same content.
# inform.html.erb and inform.text.erb
Hello <%=@friend%>!
<%= @user.name %> has sent you some advice: <%= @message %>
You can add VERY VERY simple HTML within the .html.erb
file, but you cannot use the typical CSS layouts nor rely on class/id attributes of CSS. If you really want to style your email, you must look into “inline” styling like this:
<p style="background:yellow;">This background is yellow</p>
Configuring Mailcatcher
Take a moment to see what you can figure out from the MailCatcher docs.
Mailcatcher allows you to test sending email. It is a simple SMTP server that intercepts (catches) outgoing emails, and it gives you a web interface that allows you to inspect them. Mailcatcher does not allow the emails to actually go out, and so you are able to test that emails send, without having to worry about that email being flooded with emails.
Let’s get it set up.
You want to install the Mailcatcher gem, but you do not want to add it to your gemfile in order to prevent conflicts with your applications gems.
$ gem install mailcatcher
Now let’s setup our development config to send the emails to the mailcatcher port. Add the following code to your config/environments/development.rb
file, within the Rails.application.configure
block of code where you see other config.
settings.
# config/environments/development.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 }
The mail is sent through port 1025, but in order to view the Mailcatcher interface we want to visit port 1080. To start up Mailcatcher:
$ mailcatcher
Now open up http://localhost:1080 and you should see the interface for where emails are sent. Now let’s test to see emails come through.
If you open up the app in a browser locally, and run through the steps to send an email, you should see the email come through on mailcatcher.
Testing
I know, I know, TDD all the things, and we didn’t do that here. Here are some helpful steps for testing that your mailer is working:
In your test files
expect(ActionMailer::Base.deliveries.count).to eq(1)
email = ActionMailer::Base.deliveries.last
This will catch an email object of the last thing sent to ActionMailer
Next, we can test that the subject line and Reply-To email address were set correctly
expect(email.subject).to eq('Nancy Drew is sending you some advice')
# note: the reply_to address will come through as an array, not a single string
expect(email.reply_to).to eq(['nancydrew@detective.com'])
Finally, since we sent a multi-part email message with both plaintext and HTML parts, can test that the content came through correctly:
# we can test that the plaintext portion of the email worked as intended
expect(email.text_part.body.to_s).to include('Hello Leroy Brown')
expect(email.text_part.body.to_s).to include('Nancy Drew has sent you some advice:')
# we can test that the HTML portion of the email worked as intended
expect(email.html_part.body.to_s).to include('Hello Leroy Brown')
expect(email.html_part.body.to_s).to include('Nancy Drew has sent you some advice:')
Should that really be in a controller feature test though?
Let’s do this as a mailer spec test, within spec/mailers/friendnotifier_mailer_spec.rb
This will feel more like a proper “unit test” of our mailer setup.
require 'rails_helper'
RSpec.describe FriendNotifierMailer, type: :mailer do
describe 'inform' do
sending_user = User.create(
first_name: 'Rey',
last_name: 'Palpatine',
email: 'rey@dropofgoldensun.com',
password: 'thebestjedi'
)
email_info = {
user: sending_user,
friend: 'Kylo Ren',
message: 'Work through your anger with exercise, and wear a mask'
}
let(:mail) { FriendNotifierMailer.inform(email_info, 'kyloren@besties.com') }
it 'renders the headers' do
expect(mail.subject).to eq('Rey Palpatine is sending you some advice')
expect(mail.to).to eq(['kyloren@besties.com'])
expect(mail.from).to eq(['friendly@advice.io'])
expect(mail.reply_to).to eq(['rey@dropofgoldensun.com'])
end
it 'renders the body' do
expect(mail.text_part.body.to_s).to include('Hello Kylo Ren')
expect(mail.text_part.body.to_s).to include('Rey Palpatine has sent you some advice: Work through your anger with exercise, and wear a mask')
expect(mail.html_part.body.to_s).to include('Hello Kylo Ren')
expect(mail.html_part.body.to_s).to include('Rey Palpatine has sent you some advice: Work through your anger with exercise, and wear a mask')
expect(mail.body.encoded).to include('Hello Kylo Ren')
expect(mail.body.encoded).to include('Rey Palpatine has sent you some advice: Work through your anger with exercise, and wear a mask')
end
end
end