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
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
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
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
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
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
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
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
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>
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 %>
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
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
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
# test/fixtures/orders.yml
one:
item: Item
price: 20
description: Description
user_id: 1
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
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
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
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
}
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)
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...
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?
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