loading...

Sending Emails in Rails with Action Mailer and Gmail

morinoko profile image Felice Forby Updated on ・8 min read

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:

  1. Set up a mailer with rails generate mailer
  2. Create email templates (views)
  3. Tell the appropriate controller action to send the email
  4. Set up an email previewer
  5. Configure the mail settings for Gmail.
  6. 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. :)

Discussion

pic
Editor guide
Collapse
matthewchao profile image
matthewchao

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!

Collapse
amit_savani profile image
Amit Patel

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".

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 error

Also google may block you.

Gmail Notification

Collapse
morinoko profile image
Felice Forby Author

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!

Collapse
michael profile image
Michael Lee 🍕

This was very helpful @morinoko ! Thank you so much for writing this article.

Collapse
morinoko profile image
Felice Forby Author

Thanks for your comment, Michael! I'm glad the article helped :D

Collapse
mahanmashoof profile image
Mahan Mashoof

Thanks a lot, the 2-step method really helped!
*just need to restart the server for it to actually work (rails s)

Collapse
godsloveady profile image
Derrick Amenuve

thank you for writing this detailed tutorial.