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:

  1. Allow us to sign up / log in
  2. Allow us to enter a friend’s email address
  3. 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

Additional Resources

Lesson Search Results

Showing top 10 results