Using Rails Action Mailer
Tags: rails, beginners, tutorial, ruby
Published: 2022-05-29 14:48:13

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 :)