DEV Community

Tim Carey
Tim Carey

Posted on

Using Rails Action Mailer

In this quick tutorial I will go over how I use Action Mailer in my Rails apps. A bonus if you have some Rails basics, HTML/CSS knowledge, and how to deploy to Heroku(Not required unless you wanna try the mailer in a production environment). If you don't know how to do these things, that's ok! You can still follow along, and I will link documentation and a few other sources to get you started, at the end of this post. Let's start building our mailer :)

Project Setup

To get started, create a new Rails 7 app(name it whatever you want of course). I named mine mailer-example. rails new mailer-example. Next I created a git repo to track changes. Always a good habit. Next we will scaffold basic User and Order models. In your terminal do

rails g scaffold User email:string name:string
rails g scaffold Order item:string price:integer description:text

Note that you should use a gem like devise in a real app for your User model/authentication. This is just for a basic demo of how the mailer works.

Next add a migration to add a user_id to Orders.
rails g migration AddUserIdToOrders
Rails will automoatically snake case for us. You should see the migration file created in db/migrate. It will be something like 20220524005557_add_user_id_to_orders. The numbers are a timestamp, so yours will be different, but the rest should be the same.
Here's what the migration file should look like

# db/migrate/<timestamp>_add_user_id_to_orders.rb
class AddUserIdToOrders < ActiveRecord::Migration[7.0]
  def change
    add_column :orders, :user_id, :integer
    add_index :orders, :user_id
  end
end
Enter fullscreen mode Exit fullscreen mode

We also need to add a migration to index the users emails so we can be sure they are unique at the database level. Do rails g migration AddIndexToUsersEmail and in that migration file

# db/migrate/<timestamp>_add_index_to_users_email.rb

class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  def change
    add_index :users, :email, unique: true
  end
end
Enter fullscreen mode Exit fullscreen mode

Save the file, then run rails db:migrate to migrate the database.

Next in config/routes.rb make the orders page the root route. Your routes.rb should look like this

# config/routes

Rails.application.routes.draw do
  root to: "orders#index"
  resources :orders
  resources :users
end
Enter fullscreen mode Exit fullscreen mode

Next we will add a link in our User and Order model index views just to make it easy to navigate between the two. Our User index view is located at app/views/users/index.html.erb At the bottom of the file add
<%= link_to "Orders", orders_path %>
In app/views/orders/index.html.erb put at the bottom of the file
<%= link_to "Users", users_path %>

Setting up the Mailer

Now we are ready to set up a mailer. We want an email to be sent to the customer when they submit an order. To set up the mailer we will run in terminal
rails g mailer OrderMailer
This will generate all the files we need for a basic mailer. Including tests and a preview. We will talk about that coming up. For now in app/mailers you will see a file called order_mailer.rb. This is the file we want to work with. In this file we make a method called order_email that will have instance variables to find the User and the Order and the mail method to send to the customer. It should look like this

# app/mailers/order_mailer.rb

class OrderMailer < ApplicationMailer
  def order_email
    @order = Order.last
    @user = User.find_by(id: @order.user_id)
    mail(to: @user.email, subject: "Thanks for your order #{@user.name}!")
  end
end
Enter fullscreen mode Exit fullscreen mode

Here we grab the user by the user_id in our Order, so the right user is grabbed for the order. We grab the last Order created and then use the mail method to send the email to the user with a subject. We also use string interpolation to put the users name in the subject dynamically.

Setting up our models

Now we need to add a few lines in our User and Order models for some basic validations. in app/models/user.rb add the following lines and save the file. Not totally necessary for this demo but, it doesn't hurt either.

# app/models/user.rb

class User < ApplicationRecord
  before_save { email.downcase! }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, uniqueness: true, format: { with: 
                                                              VALID_EMAIL_REGEX }

  validates :name, presence: true
  has_many :orders
end
Enter fullscreen mode Exit fullscreen mode

Dont' worry about the regex, you don't need to understand it. It just makes sure a proper email format is submitted. We also we downcase the email before saving it with the before_save line. We also make sure the fields cannot be blank withe presence: true and emails have to be unique.

Now in app/models/order.rb we need to add a few validations, an association to our User model, but more importantly, we need to add a few key lines to make our mailer work. Check it

# app/models/order.rb

class Order < ApplicationRecord
  validates :item, presence: true
  validates :price, presence: true
  validates :description, presence: true
  belongs_to :user

  after_create_commit :send_order_email

  def send_order_email
    OrderMailer.with(order: self).order_email.deliver_later
  end
end
Enter fullscreen mode Exit fullscreen mode

Now in our Order model we have a callback after an Order is created to send our email. The method send_order_email calls our OrderMailer which calls our order_email method defined in OrderMailer which calls order_email method that we defined in OrderMailer. We pass the Order model with itself on the order: self line. Then we pass the send_order_email method to the after_create_commit method.

After this is done, we need a gem so when an email is fired off, we see the email sent opened in a new tab in development. This is a great way to see your emails that are being sent, without actually sending emails to a real address and cluttering up a mailbox.
Put the gem letter_open_web in your Gemfile like so:

# Gemfile 

group :development, :test do
#Existing gems
#
#

  gem "letter_opener_web", "~> 2.0"
end
Enter fullscreen mode Exit fullscreen mode

Then run in your terminal bundle install to install the gem
After you install the gem, you need to add one line to your development.rb file in config. In /config/environments/development.rb scroll down to the bottom of the file and add the line:

# config/environments/development.rb

config.action_mailer.delivery_method = :letter_opener
Enter fullscreen mode Exit fullscreen mode

Setting up the views for our Mailer

Next we need a view for our mailer. We are going to make two files one a plain text file with erb(embedded ruby) and the other an html file with erb. The views for the mailer will be in app/views/order_mailer. In your terminal run touch app/views/order_mailer/order_email.html.erb && touch app/views/order_mailer/order_email.text.erb. This will created both view files that we need. Make your html.erb file as follows

<!-- app/views/order_mailer_order_email.html.erb -->

<h1>Thank you for your order, <%= @user.name %>!</h1>

<p>Below are the details of your order</p>

<table>

  <tr>

    <th>Item</th>

    <th>Price</th>

    <th>Description</th>

  </tr>

  <tr>

    <td><%= @order.item %></td>

    <td><%= @order.price %></td>

    <td><%= @order.description %></td>

  </tr>

</table>

<style>

td, th {

border: 1px solid #dddddd;

text-align: left;

padding: 8px;

}



table {

font-family: arial, sans-serif;

border-collapse: collapse;

width: 100%;

}



th {

padding-top: 12px;

padding-bottom: 12px;

text-align: left;

background-color: #04AA6D;

color: white;

}

</style>
Enter fullscreen mode Exit fullscreen mode

We are using <style> tags because rails mailer views because they do not support external CSS. You could also do in-line styling. Next up the text.erb file

<!-- app/views/order_mailer_order_email.text.erb -->

Thank you for your order, <%= @user.name %>!

===============================================

Here are the details of your order

Item: <%= @order.item %>

Price: <%= @order.price %>

Description: <%= @order.description %>
Enter fullscreen mode Exit fullscreen mode

Adding previews for our mailer

At this point, our mailer should work. Before trying it out, we will make a preview for it first. The generator we ran earlier to make our mailer already generated this file for us. It should be in test/mailers/previews/order_mailer_preview.rb. In this file we will create a method called order_email. It will pull the first user out of the database and the first order just so it has the data to fill the preview. put this in your order_mailer_preview.rb file.

# test/mailers/previews/order_mailer_preview.rb

class OrderMailerPreview < ActionMailer::Preview
  def order_email
    OrderMailer.with(user: User.first).order_email
  end
end
Enter fullscreen mode Exit fullscreen mode

Everything should be good to go now! However, the preview won't work until we add some data. It can't render the templates with no User or Orders in the database, so lets add a User and an Order! We could spin up the server and do it through the site, but I will do it in console here. You can do it through the site if you'd like. If not, start up rails console by typing in rails c in terminal

irb(main):001:0>User.create(email: "johnny@example.com", name: "Johnny")
irb(main):001:0>Order.create(item: "NBA Jersey", price: 110, description: "NBA Jersey for Joel Embiid")
irb(main):001:0>exit
Enter fullscreen mode Exit fullscreen mode

Now with this data added, spin up the server with rails s in terminal. Next you can go to localhost:3000/rails/mailers and you will see our Order Mailer with our order_email method. Click on order_email and you should see the preview for our email. You can switch between HTML and plain text previews.

Adding tests to our mailer

Now we will add tests to make sure that 1. our mailer is enqueued when it should be(after an order is placed) and 2. that the email contains the content we are expecting. Since the preview works, we should be able to write a passing test. If you spin up the server and make a new order, you should get the email that opens up in a new tab. Everything should work, but we will write tests to back that up, and so we don't have to test the mailer by hand everytime we make a change to the mailer system. Testing the mailer by hand to see if it still works everytime you make a change to the mailer system, gets slow and tedious, fast. That's where tests come in. We could have written the tests first and developed our mailer until they pass(known as TDD, Test Driven Development), but I prefer to do tests after. Our first test is going to see if the email contains the content we expect it to. First, we need to add fixtures, aka dummy data, for the tests to use. Because we don't actually want to write to the database or make actual queries to the DB. Add this to the users.yml and orders.yml fixtures. These files were auto generated when we ran the scaffold generator for both Models.

# test/fixtures/users.yml

one:
  email: user@example.com
  name: Example
  id: 1
Enter fullscreen mode Exit fullscreen mode
# test/fixtures/orders.yml

one:
  item: Item
  price: 20
  description: Description
  user_id: 1
Enter fullscreen mode Exit fullscreen mode

Now with our fixtures setup, we can begin writing our tests. First test we will write we see if the email has the contents we expect it to have.

# test/mailers/order_mailer_test.rb

require "test_helper"

class OrderMailerTest < ActionMailer::TestCase

    setup do
      @order = orders(:one)
      @user = users(:one)
    end

    test "send order details email to customer" do
      email = OrderMailer.with(order: @order).order_email

      assert_emails 1 do
        email.deliver_now
      end

      assert_equal [@user.email], email.to
      assert_match(/Below are the details of your order/, email.body.encoded)
    end
end
Enter fullscreen mode Exit fullscreen mode

Lets break down this first test. So first we setup the test to use our fixtures created in the previous step. We make an instance variable that uses our Users and Orders fixtures. In the test block, we create an email with our OrderMailer with the data from our Orders fixture, then we call the order_email method from our OrderMailer . Next we make sure that only one email is sent with the line assert_emails 1 do and we send the email. The last two lines check to see that the email was sent to the right user, and that part of the body also matches. We are not concerned with if it matches the content of the entire body, it would make the test too brittle. Next we will write a test to make sure the email is enqueued when it's supposed to be. First, we need a helper for our test. You are going to need to make the file orders_helper.rb in test/helpers directory. Put this in orders_helper.rb

# test/helpers/orders_helper.rb

module OrdersHelper
  def order_attributes
    {

    user: users(:one),
    item: "Item",
    price: 30,
    description: "Description"

    }
  end
end
Enter fullscreen mode Exit fullscreen mode

Here we use a helper instead of our yaml file because when assigning attributes, you must pass a hash as an argument. If you try to pass our orders fixture, the test will fail with an error. With our helper, we can now write our test to see if an email is enqueued when there is a new Order.

# test/models/order_test.rb

require "test_helper"
require "helpers/orders_helper"

class OrderTest < ActiveSupport::TestCase
    include ActionMailer::TestHelper
    include OrdersHelper

    test "sends email when order is created" do
      order = Order.create!(order_attributes)
      assert_enqueued_email_with OrderMailer, :order_email, args: {order: order}
    end
end
Enter fullscreen mode Exit fullscreen mode

Let's go over the code. We have our test_helper as usual. Then we pull in our helper we just made in the last step. In our class we bring in ActionMailer::TestHelper to use the assert_enqueued_email_with method, and of course we include our OrdersHelper. Next is the actual test, we create an order with our order_attributes which was defined in our module OrdersHelper from the last step. Then we checked to see if an email was enqueued with our OrderMailer using the order_email method defined in our mailer. We then pass it the created order. Running rails test in terminal, all tests should pass and be green. Starting our local server rails s we can create an order and see that we get an email sent that opens in another tab, thanks to our letter_opener gem we added at the start of this tutorial. Our mailer is complete! Next we will get our mailer working on a production server. If you don't know how to deploy a Rails app, feel free to skip the next section.

Sending mail in production

If you don't know how to deploy to Heroku, you can use whatever you want. If you don't know how to deploy a Rails app into production, you can skip this section.

There are a ton of ways to send emails in production. Send Grid, MailGun, among many others. The easiest(and free way), is to use gmail. In order to send emails from our app with gmail, we need to make an app password, a setting in our Google account. Here we will create an app password. It is a special password that can be used for our app to send emails, without actually using our gmail account password. In order to do this, you need to set up 2FA on your account. So do that if you haven't done that yet. Under Signing in to Google you should see App passwords. Click that, then you will see a few drop downs. First one, Select app, pick Mail. Under Select device, pick Other. It will ask for a name, you can name it whatever you want. I used mailer-example. If you are on Heroku, put this password in your Config Vars. On the Heroku dashboard click settings and look for Config Vars. Click Reveal Config Vars and add your env variable(I used GMAIL_USER_PASSWORD) with the password generated by Google. I linked at the end of this post the docs from Heroku on how to use Config Vars if you get stuck.

Next step

We need to setup our production config to use gmail in production. We need to edit our production.rb and add the following:

# config/environments/production.rb

config.action_mailer.delivery_method = :smtp

config.action_mailer.smtp_settings =

  {
    address: "smtp.gmail.com",
    port: 587,
    domain: "example.com",
    user_name: "username@gmail.com",
    password: ENV["GMAIL_USER_PASSWORD"],
    authentication: "plain",
    enable_starttls_auto: true,
    open_timeout: 5,
    read_timeout: 5
  }
Enter fullscreen mode Exit fullscreen mode

Change your user_name to your gmail email. Our password is protected in an environment variable, which we saved on Heroku, which was the app password generated by Google in our last step. Change the domain to whatever your Heroku domain is for the site. After pushing everything to Heroku, everything should work. Your app should send emails in both production and development environements. Awesome!

Wrapping things up

So there you have it, a basic overview of the mailer and how it works. This is a very basic app, but it's just to demo the mailer in Rails. Feel free to add to it. Add devise gem for a real user authentication system. Add some styling cause the app currently is ugly LOL. Build on Orders and create an Items model where you can add items to order. Build it into a fully functional ecommerce site. The sky is the limit, go forth and code!

If you don't know how to deploy Rails to Heroku here is a great place to start. How to use Heroku Config Vars. If you don't know how to use git/github, start here. Git docs are also a good place for information. The Rails documentation for the mailer here and pretty much everything else you could need for Rails is on Rails Guides. Hope you all learned something :)

Top comments (3)

Collapse
 
asolab profile image
asolab

I'm having a issue with my action mailer functionalities, it works fine with open letter gem but with smtp config token only run on my terminal it won’t deliver to mail, 421 server too busy error.
stackoverflow.com/questions/737354...

Collapse
 
tccodez profile image
Tim Carey

Are you using ENV variables for your smtp_settings config? Are you using your email password or the app pass that was generated? Are you sending more than one email at once?

Collapse
 
asolab profile image
asolab

I used config settings from 'guides.rubyonrails.org/action_mail...', I generate password from the gmail Apps Password, I only send one request at once like forgot password mail, you can check the stackoverflow link to undertsand me more, Thanks