Using Rails Action Mailer
Learn how to use Rails ActionMailer to send emails from your Rails app in a development and production enviornment.

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
```ruby
# 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
```ruby
# 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
```ruby
# 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
```ruby
# 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.
```ruby
# 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
```ruby
# 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:
``` ruby
# 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:
```ruby
# 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
```html
<!-- 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
```html
<!-- 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.
```ruby
# 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
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.
```yaml
# test/fixtures/users.yml
one:
email: user@example.com
name: Example
id: 1
```
```yaml
# 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.
```ruby
# 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`
```ruby
# 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.
```ruby
# 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 <span style="color:green"><strong>green</strong></span>. 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](https://myaccount.google.com/apppasswords), 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:
```ruby
# 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](https://github.com/heartcombo/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](https://devcenter.heroku.com/articles/getting-started-with-rails7). How to use [Heroku Config Vars](https://devcenter.heroku.com/articles/config-vars). If you don't know how to use git/github, [start here](https://product.hubspot.com/blog/git-and-github-tutorial-for-beginners). Git docs are also a [good place for information](https://git-scm.com/book/en/v2/Getting-Started-What-is-Git%3F). The Rails documentation for the mailer [here](https://guides.rubyonrails.org/action_mailer_basics.html) and pretty much everything else you could need for Rails is on Rails Guides. Hope you all learned something :)