In this post, I will be setting up a mailer in Rails that will send out an email to the website owner when a customer submits an order. (I was using Rails 6 but this should also work in Rails 5).
The steps:
- Set up a mailer with
rails generate mailer
- Create email templates (views)
- Tell the appropriate controller action to send the email
- Set up an email previewer
- Configure the mail settings for Gmail.
- Set up tests
Let's assume we already have a basic Order Model and Orders Controller setup, which simply shows a confirmation to the customer via a flash message when the order is successful:
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def new
@order = Order.new
end
def create
@order = Order.new(order_params)
if @order.save
flash[:success] = t('flash.order.success')
redirect_to root_path
else
flash.now[:error] = t('flash.order.error_html')
render :new
end
end
private
def order_params
params.require(:order).permit(:name, :email, :address, :phone, :message)
end
end
We also want to send the email to the website owner when the order is received, otherwise, how would the know?
To get started, we will first need to create a mailer which is easy to do with Rails' generate mailer
command. In this case, the mailer will be used in the OrdersController
so we'll name it OrderMailer
:
$ rails generate mailer OrderMailer
This will create the following files and output:
create app/mailers/order_mailer.rb
invoke erb
create app/views/order_mailer
invoke test_unit
create test/mailers/order_mailer_test.rb
create test/mailers/previews/order_mailer_preview.rb
Let's out the app/mailers
folder. We'll find the application_mailer.rb
and the newly created order_mailer.rb
files.
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com' # Replace this email address with your own
layout 'mailer'
end
# app/mailers/order_mailer.rb
class OrderMailer < ApplicationMailer
end
The new OrderMailer
works very similarly to a regular controller. A controller prepares content like HTML and shows it to the user through views. A mailer prepares content in the form of an email and delivers it. We can create an email by adding a method to the mailer, as you would add an action to a controller.
Let's add a method for the order email to the OrderMailer
:
# app/mailers/order_mailer.rb
class OrderMailer < ApplicationMailer
def new_order_email
@order = params[:order]
mail(to: <ADMIN_EMAIL>, subject: "You got a new order!")
end
end
*Replace <ADMIN_EMAIL>
with the email you want to use, preferably hidden away as an environment variable.
Any instance variables in new_order_email
can be used in the mailer views. The params[:order]
will be provided when we tell the OrderController
to send the email (which I'll go over below).
Let's create an email view file now, making sure to name the file the same as the method. Make a new_order_email.html.erb
in the app/views/order_mailer/
folder:
# app/views/order_mailer/new_order_email.html.erb
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body>
<p>You got a new order from <%= @order.name %>!</p>
<p>
Order details<br>
--------------------------
</p>
<p>Name: <%= @order.name %></p>
<p>Email: <%= @order.email %></p>
<p>Address: <%= @order.address %></p>
<p>Phone: <%= @order.phone %></p>
<p>
Message:<br>
----------
</p>
<p><%= @order.message %></p>
</body>
</html>
And as a best practice, let's also create a text version of the email in case the receiver doesn't use HTML email. This goes in the same folder and has the same file name but uses the text.erb
extension instead of html.erb
.
# app/views/order_mailer/new_order_email.text.erb
You got a new order from <%= @order.name %>!
===============================================
Order Details:
--------------------------
Name: <%= @order.name %>
Email: <%= @order.email %>
Address: <%= @order.address %>
Phone: <%= @order.phone %>
Message:
<%= @order.message %>
Now that we have the emails set up, next we'll tell the OrdersController
to send an email when an order is made, that is, after an order is saved in the create
action.
class OrdersController < ApplicationController
def create
@order = Order.new(order_params)
if @order.save
OrderMailer.with(order: @order).new_order_email.deliver_later
flash[:success] = "Thank you for your order! We'll get contact you soon!"
redirect_to root_path
else
flash.now[:error] = "Your order form had some errors. Please check the form and resubmit."
render :new
end
end
...
end
Here, we added the following line of code after the order was saved:
OrderMailer.with(order: @order).new_order_email.deliver_later
Notice the with(order: @order)
code. This is what gives the OrderMailer
access to the order info as a param. Remember setting the instance variable with @order = params[:order]
in the OrderMailer
? That's where the param is coming from!
So, how can we preview this email before sending it? Well, when we generated our mailer, a preview file was created in the test/mailers/previews/
folder. It contains a file called order_mailer_preview.rb
that has an empty OrderMailerPreview
class in it:
# test/mailers/previews/order_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/order_mailer
class OrderMailerPreview < ActionMailer::Preview
end
If we try to go to the http://localhost:3000/rails/mailers/order_mailer
URL, it just shows us a white page with the text "Order Mailer".
To set up a preview for our new order email, simply add a method with the same name as the mailer method you want to preview (in this case new_order_email
and set up the mailer:
# Preview all emails at http://localhost:3000/rails/mailers/order_mailer
class OrderMailerPreview < ActionMailer::Preview
def new_order_email
# Set up a temporary order for the preview
order = Order.new(name: "Joe Smith", email: "joe@gmail.com", address: "1-2-3 Chuo, Tokyo, 333-0000", phone: "090-7777-8888", message: "I want to place an order!")
OrderMailer.with(order: order).new_order_email
end
end
Restart the Rails server and navigate to http://localhost:3000/rails/mailers/order_mailer/new_order_email
to see the email. You can even see both the HTML and the text versions. Awesome! Tweak the look of the email until you like it. (Additionally, http://localhost:3000/rails/mailers/order_mailer
now shows a list of available previews.)
Lastly, we need to configure our Rails app to send emails via Gmail. To do so, we'll add the following settings to our config/environments/production.rb
:
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
host = 'example.com' #replace with your own url
config.action_mailer.default_url_options = { host: host }
# SMTP settings for gmail
config.action_mailer.smtp_settings = {
:address => "smtp.gmail.com",
:port => 587,
:user_name => <gmail_username>,
:password => <gmail_password>,
:authentication => "plain",
:enable_starttls_auto => true
}
Replace <gmail_username>
and <gmail_password>
with your own username and password, which would preferably be hidden away as environment variables. Note on the password: I highly recommend enabling 2-Step Verification and registering an "app password" to use in the app or you're likely to run into problems with Gmail blocking the emails. See below in the troubleshooting section.
For local use only or development use, use
host = 'localhost:3000'
config.action_mailer.default_url_options = { :host => 'localhost:3000', protocol: 'http' }
instead of the above settings for host
and config.action_mailer.default_url_options
.
To test if the mail really gets sent in development, too, add the same settings to config/environments/development.rb
. You can later change the line config.action_mailer.delivery_method = :smtp
to config.action_mailer.delivery_method = :test
to prevent sending real emails during development.
Troubleshooting Email Sending Errors
The above configuration actually didn't work for me at first, due to Google's security features, and I was getting Net::SMTPAuthenticationError
errors. Here's how I go everything to work.
Account with 2-step Verification
If your Gmail account uses 2-step verification, you will need to get an app password and use that instead of your regular password. If you don't use 2-step verification, I recommend turning it on to avoid getting the emails blocked by Google.
To create an app password, go to your Google account settings and navigate to the "Security" tab. Under "Signing in to Google", click on the "App passwords" menu item (this will only be available if you have 2-step verification turned on). Next, select Other (Custom name)
under the "Select app" dropdown and enter the name of your app or something else useful. Click "Generate" and your new app password will appear on the screen. Make sure you copy it before closing the window, or you won't be able to see the password again.
Now, in your Rails app mailer settings, replace the <gmail_password>
with the new app password instead.
Account without 2-step verification
If you don't use 2-step verification, you will have to allow your account to be accessed by "less secure apps". In your Google settings under the "Security" tab, look for the "Less secure app access" section and click "Turn on access".
After pushing to production, I still had problems sending mail because Google was blocking access from unknown locations, in this case, the app in production. I was able to solve the problem by going through the "Display Unlock Captcha" process. If you still have problems after doing the above, this will grant access to the account for a few minutes, allowing you to register the new app.
Activate this option by going to http://www.google.com/accounts/DisplayUnlockCaptcha. After that, have the app send an email again. This should register the app so that you will be allowed to send emails from then on!
UPDATE: After a few months, for some reason, emails sent through the app were getting blocked again and security warnings would get sent to the email account. To avoid headaches, I would go with the 2-step verification + app password method.
Rails credentials issue with Heroku
One last problem I had in production with Heroku was forgetting to register my Rails app's master key in the Heroku app settings and configuring Rails to require the master key. This will be a problem if you are using Rails' credential file (credentials.yml.enc
) to keep track of your secret keys (in this case, your email and password).
In development, Rails can access the secret credentials because the master key is directly available on the system, but in production with Heroku, the app cannot access secret keys without first registering your master key with Heroku.
To fix this problem, in the config/environments/production.rb
file, uncomment or add the following line:
config.require_master_key = true
Then, in Heroku app settings, register a key called RAILS_MASTER_KEY
. Enter the value found inside the config/master.key
file. This allows Heroku to access secret keys registered inside the credentials.yml.enc
file.
Testing
Lastly, let's make sure we have some tests set up!
If there isn't already an orders.yml
file under test_fixtures
, create one and add:
# test/fixtures/orders.yml
one:
name: "Joe Smith"
email: "joe@gmail.com"
address: "1-2-3 Chuo, Tokyo, 333-0000"
phone: "090-7777-8888"
message: "I want to place an order!"
This is an easy way to pull in a sample order for use in the mailer tester.
Next, in test/mailers/order_mailer_test.rb
(create this file if it is not already there), we can add a simple test that asserts that the email is getting sent and that the content is correct.
require 'test_helper'
class OrderMailerTest < ActionMailer::TestCase
test "new order email" do
# Set up an order based on the fixture
order = orders(:one)
# Set up an email using the order contents
email = OrderMailer.with(order: order).new_order_email
# Check if the email is sent
assert_emails 1 do
email.deliver_now
end
# Check the contents are correct
assert_equal [<ADMIN_EMAIL>], email.from
assert_equal [<ADMIN_EMAIL>], email.to
assert_equal "You got a new order!", email.subject
assert_match order.name, email.html_part.body.encoded
assert_match order.name, email.text_part.body.encoded
assert_match order.email, email.html_part.body.encoded
assert_match order.email, email.text_part.body.encoded
assert_match order.address, email.html_part.body.encoded
assert_match order.address, email.text_part.body.encoded
assert_match order.phone, email.html_part.body.encoded
assert_match order.phone, email.text_part.body.encoded
assert_match order.message, email.html_part.body.encoded
assert_match order.message, email.text_part.body.encoded
end
end
The <ADMIN_EMAIL>
should be replaced with the email you are using.
email.html_part.body.encoded
checks the content in the HTML email while email.text_part.body.encoded
checks the text email.
Conclusion
We should now have a functional mailer that notifies us (or the website owner) of newly incoming orders!
Once the Rails mailer and Gmail settings are properly configured, we can easily send other emails from other controller actions by generating and setting up new mailers in much the same way. :)
Top comments (26)
Just so helpful and clear - thanks very much for putting this together and sharing it
Thanks! This article is a little old but hopefully it still works in newer Rails versions :)
thank you for writing this detailed tutorial.
I just wanted to thank you for writing out this detailed tutorial. I'm going through the Rails Tutorial (which uses SendGrid) but was able to get everything working with Gmail thanks to your instructions. It was kind of hard finding recent, still-accurate information!
If this is not done, google will block you and mail won't be delivered. You may get
Net::SMTPAuthenticationError: 535-5.7.8 Username and Password not accepted
errorAlso google may block you.
Honestly, I ended up running into problems even after allowing "Less secure app access" (it just stopped sending emails) so I definitely recommend using the two-factor authentication!
Felice -
This is such a nice post. Thanks for taking the time to break things down like you did. I'd never built a mailer in Rails, but following your post I was able to with no problems at all.
Keep up the great work!
Rather than "allowing less secure apps" in gmail you can create a specific app password: support.google.com/accounts/answer...
Thanks man! That helped me to make it work!
Thanks a lot, the 2-step method really helped!
*just need to restart the server for it to actually work (rails s)
Hi there,
Thank you very much for the great tutorial.
On Docker in dev mode I had this error:
OpenSSL::SSL::SSLError (SSL_connect returned=1 errno=0 peeraddr=172.253.122.108:587 state=error: certificate verify failed (self signed certificate in certificate chain))
To fix it I added to
config.action_mailer.smtp_settings
::enable_starttls_auto => true
Maybe it will help out someone
Cheers
Hi, I'm following this article to setup mailer, and get the following error ==>
Template is missing
Missing template event_mailer/new_event_request_email with "mailer". Searched in: * "event_mailer"
would you be able taking a look on it ? Do u have any idea why I'm getting this error ?
This was very helpful @morinoko! Thank you so much for writing this article.
Thanks for your comment, Michael! I'm glad the article helped :D