Site icon Mailtrap

How to Set Up Emails for Password Resets with Devise

The development team at Planet Argon recently helped one of our clients launch a redesigned application for their 50,000 customers. Before the existing users could get into the new app, they would need to reset their password. We use Devise for user authentication and wanted to leverage its existing functionality for our custom mailer.

This post walks through the process we used to create a custom mailer to welcome the users to the new platform and test that mailer before delivery.

At a high level, this entails:

This tutorial assumes that you already have Devise installed and configured on your application for a model named User. If you need to do this, I suggest consulting the Getting Started section in the gem’s documentation. It also assumes that you already have User model objects in your database that have email attributes.

Create a virtual inbox for your application with Mailtrap Email Sandbox

So, as the first step in this process, we have to create a virtual inbox which we will use to test emails. To create one, we will be using Email Sandbox, the email testing side of the Mailtrap Email Delivery Platform.

Email Sandbox, for those unfamiliar with it, is a tool you can use to preview test emails and do any necessary inspecting and debugging. 

What’s great about tools like Email Sandbox is that they save you from having to do manual email testing with your personal inbox (a practice very harmful to your domain reputation) but also from accidentally sending test emails to real recipients. 

Email Sandbox also has a much quicker setup (approximately 5 minutes) compared to setting up your own SMTP server, which can take hours.

Once you trap an email from staging with Email Sandbox, you will be able to validate its HTML/CSS, check its content spam score, find out if the domain the email was sent from is present on any blacklists, and review other major tech info as well as email headers.

To set up a virtual inbox with Email Sandbox, you will need to first create a free Mailtrap account

Then, log into the Mailtrap account and go to Sandbox-> Inboxes. 

A default inbox named “My Inbox” will already be there, which you can customize or delete.

You can also create a new inbox by clicking on the Add Inbox button.

Next, click on your inbox of choice. 

That should take you to the SMTP Settings page, where you need to select Ruby on Rails to get the integration code. 

Copy the code and paste it into your config/environments/development.rb file.

Replace your existing settings for action_mailer.delivery_method or action_mailer.stmp_settings with what Email Sandbox provided you with.

It will look something like this:

# config/environments/development.rb
…
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    from: 'from@example.com'',
    user_name: 'XXXXXXXXXXXX',
    password: 'XXXXXXXXXXXX',
    address: 'smtp.mailtrap.io',
    domain: 'smtp.mailtrap.io',
    port: '2525',
    authentication: :cram_md5,
  }

Generate Devise mailer views and our custom mailer template

Devise has many view options, including pages for logging in and out, resetting passwords, and more. For our purposes, we’ll just generate the Devise default mailer templates.

$ rails generate devise:views users -v mailer

The devise:views generator can add them all to your application. By passing users, the generated views will be nested within the users view directory. The -v mailer command scopes the generated views to just those pertaining to the mailers. You should see a few new mailers in the app/views/users/mailer directory.

This creates some basic mailers for confirming your email, resetting your password, unlocking your account, and more. We’ll leave these templates as-is, and create a new mailer template to specifically welcome users to the new app and provide them with a link to reset their passwords.

Let’s add that file to this directory, too:

$ touch app/views/users/mailer/welcome_reset_password_instructions.html.erb

And add some boilerplate HTML for the mailer:

<!doctype html>
<html>
  <head>
    <meta name="viewport" content="width=device-width" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Welcome to the new site</title>
  </head>
  <body>
    <div>
      <p>Hi,</p>
      <p>The new site is now live!</p>
      <p>To access the new site, <%=link_to "please first click this link to reset your password", edit_user_password_url(reset_password_token: @token), html_options = { style: "font-weight: bold" } %>.</p>
      <p>You have two weeks to reset your password. If you run into any issues, please email them to <a href="mailto:support@example.com">support@example.com</a>. Our Support Team will be happy to help you.</p>
      <p>Thank you!</p>
    </div>
  </body>
</html>

The key line in this template that provides a link to reset the user’s password is <%=link_to "please first click this link to reset your password", edit_user_password_url(reset_password_token: @token), html_options = { style: "font-weight: bold" } %>

This link uses Devise’s built-in edit_user_password_url helper to reset the User’s password token, copied straight from their reset_password_instructions mailer template. This will do nothing right now, but later on, we’ll connect it to our UserMailer to generate a token.

Configure Devise to use a Custom Mailer

Devise configurations live in an initializer, config/initializers/devise.rb. If you’ve never set up mailers with Devise before, you may want to change a few settings:

# config/initializers/devise.rb
...
  # ==> Mailer Configuration
  # Configure the e-mail address which will be shown in Devise::Mailer,
  # note that it will be overwritten if you use your own mailer class
  # with default "from" parameter.
  config.mailer_sender = 'donotreply@example.com'
  # Configure the class responsible to send e-mails.
  config.mailer = 'UserMailer'
…
  # ==> Configuration for :recoverable
  #
  # Defines which key will be used when recovering the password for an account
  # config.reset_password_keys = [:email]
  # Time interval you can reset your password with a reset password key.
  # Don't put a too small interval or your users won't have the time to
  # change their passwords.
  config.reset_password_within = 2.weeks

This creates a views directory for the mailer, app/views/users/mailer, a mailer file, app/mailers/user_mailer.rb, and a preview file to view the mailer using your Rails server. The location of this preview is dependent on your app’s test suite. If you’re using RSpec, it should be in spec/mailers/previews/user_mailer.rb. If you’re using MiniTest, it will be nested within the test directory.

Let’s open app/mailers/user_mailer.rb and make a few changes to get it ready to send Devise emails, based on Devise’s Custom Mailer Wiki.

# app/mailers/user_mailer.rb
class UserMailer < Devise::Mailer
  include Devise::Controllers::UrlHelpers
  default template_path: 'users/mailer'
end

First, we change the UserMailer from extending ApplicationMailer to Devise::Mailer, to leverage Devise::Mailer‘s methods. To access Devise’s Url Helpers in our custom mailer, we include Devise::Controllers::UrlHelpers. Finally, we specify the default template_path to the location of our mailer templates within the views directory, users/mailer.

Create a new action within UserMailer

Now, we create a new action within the UserMailer for our welcome_reset_password_instructions mailer. Mailer actions are similar to controller actions. They default to looking for a view that has a name matching the mailer’s method name.

# app/mailers/user_mailer.rb
…
  def welcome_reset_password_instructions(user)
    mail(to: user.email, subject: 'Welcome to the New Site')
  end

In its most basic form, all it needs is a call to the #mail method, specifying who to send the email to using the to: argument, and the subject line using the subject: argument.

Since this mailer will be called through a rake task, we’ve added a user argument to the mailer action. This could also be passed via params when a mailer is called in other cases.

Set up the Mailer preview

Let’s visit the MailerPreview file that was generated alongside our UserMailer.

# spec/mailers/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
end

This doesn’t have much to it right now. Just with this, any action in the UserMailer could be added to the file, and an ActionMailer::Preview object would be generated for the mailer when called in the rails console.

For our welcome_reset_password_instructions email, to get a preview generated, all we need to do is add a call to that mailer action:

# spec/mailers/previews/user_mailer_preview.rb
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
  def welcome_reset_password_instructions
    UserMailer.welcome_reset_password_instructions(User.first)
  end
end

We name the method the same as the UserMailer’s corresponding action. Then we call the action on the UserMailer, and fulfill the required arguments by passing User.first. This will generate the mail preview with attributes from the first User in the database.

To preview the mailer, fire up a rails server and visit http://localhost:3000/rails/mailers/user_mailer/welcome_reset_password_instructions.

You should see the mailer template we created earlier in this tutorial. Click on the link to reset your password. You’ll be taken to the sign-in page for the user. Our goal is to have this link go to the reset password page. 

By default, Devise adds attributes for its Recoverable module to the model you set it up with, in our cases, User. The reset_password_token attribute stores a string token that gets matched with a user, and reset_password_sent_at, a datetime attribute that gets compared with Devise’s reset_password_within setting to note when the token should be invalidated. If you don’t have these attributes on the User model, you’ll need to add them before this will work.

The built-in reset password instructions email generates the token behind the scenes. The problem is that there is no token currently associated with the user that clicked on the email. We’ll need to create one and add it to this mailer in order for it to work.

To verify this theory, go back to your rails console and look at the reset_password_token attribute for your user:

> user.reset_password_token
# => nil

It’s nil because the mailer action does not currently generate a token to use in the mailer. Let’s fix that.

Update the welcome_reset_password_instructions email to use Devise::Controllers::UrlHelpers

This post from mokacoding helped connect the dots to generate a reset password link in a custom mailer.

The first thing we’ll do is create a new protected method, create_reset_password_token to generate the reset password token for the user. It accepts one argument, user, a User object to assign the specific token.

private
def create_reset_password_token(user)
    raw, hashed = Devise.token_generator.generate(User, :reset_password_token)
    @token = raw
    user.reset_password_token = hashed
    user.reset_password_sent_at = Time.now.utc
    user.save
 end

The Devise.token_generator.generate method takes two arguments, the class or model that the token is being generated for, and the column that the token will be used in. This token is for the User model’s reset_password_token.

This method returns two values, the raw token and a hashed copy.

Save the value of the raw token as an instance variable, @token, to use in the edit_user_password_url within our welcome_reset_password_instructions email.

Assign the hashed copy to the user’s reset_password_token attribute, set the reset_password_sent_at attribute to the current time, and save the user.

Next, we’ll call create_reset_password_token in our welcome_reset_password_instructions mailer action

def welcome_reset_password_instructions(user)
    create_reset_password_token(user)
    mail(to: user.email, subject: 'Welcome to the New Site')
  end

The entire UserMailer file should now look like this:

class UserMailer < Devise::Mailer
  helper :application # gives access to all helpers defined within `application_helper`.
  include Devise::Controllers::UrlHelpers # Optional. eg. `confirmation_url`
  default template_path: 'users/mailer' # to make sure that your mailer uses the devise views
  def welcome_reset_password_instructions(user)
    create_reset_password_token(user)
    mail(to: user.email, subject: 'Welcome to the New Site')
  end
  private
  def create_reset_password_token(user)
    raw, hashed = Devise.token_generator.generate(User, :reset_password_token)
    @token = raw
    user.reset_password_token = hashed
    user.reset_password_sent_at = Time.now.utc
    user.save
  end
end

Let’s re-rest our Mailer preview. Fire up a Rails server and visit: http://localhost:3000/rails/mailers/user_mailer/welcome_reset_password_instructions.

Click on the reset password link within your mailer. You should now be redirected to Devise’s Change Password page instead of the log-in page. 

Create a rake task to send the new mailer

Devise has a tutorial on creating a rake task to send a mass password reset notification. Let’s tweak it a bit to use our mailer.

First, create a rake file to house our devise task:

$ touch lib/tasks/devise.rake

Next, let’s create a task to send our mass password reset notification email:

# lib/tasks/devise.rake
namespace :devise do
  desc "Send welcome reset password instructions to all users.
  This will lockout all users until they reset their password."
  task send_welcome_reset_password_instructions_to_all_users: :environment do
    User.in_batches(of: 10).each_record do |user|
        # Send instructions so user can enter a new password:
        UserMailer.welcome_reset_password_instructions(user).deliver
        p user.id
      end
   end
end

Under the namespace :devise, we provide a description for the task to explain its intended use to future developers. Then we define the task, send_welcome_reset_password_instructions_to_all_users.

The task is taking the User object and operating on batches of 10 to send the UserMailer’s welcome_reset_password_instructions action to each passed_in user and deliver the mailer immediately. Finally, the user.id that was operated on is printed out to the console so the developers can track which users have received the email.

We decided to limit the batches to 10 emails at a time to better track where sent emails may have failed. Then, each_record is called to operate on the enumerator returned by in_batches

There may be better settings for your project. See the in_batches documentation for more configuration options and suggestions.

Word of Caution: Scope the task to only call valid users

One of the issues we ran into was that, initially, the rake task was run with _all_ users. However, some users were created in tests and didn’t have valid email addresses. Other users didn’t have any products, so they shouldn’t receive an alert to visit the new site.

Try filtering your users through a query before sending every one of them to your rake task. You’ll avoid bounced emails and make the process of sending many, many emails a little shorter. This will be unique to every app. This could be as simple as:

User.where.not(email: nil)

Test the task using Mailtrap Email Sandbox

Testing the task on a large batch of users, but not _every_ user, may also be helpful to get a sense of timing and the way your task behaves under a larger load. 

Do keep in mind that if you’re using Mailtrap’s free plan, you have 500 free test emails to send with Email Sandbox in a month. The free plan also comes with access to an email-sending solution called Mailtrap Email API and a range of features you can check out on the Mailtrap pricing page.

To test the task out, let’s tweak it to return only 100 users:

# lib/tasks/devise.rb
…
  task send_welcome_reset_password_instructions_to_all_users: :environment do
    User.take(100).in_batches(of: 10).each_record do |user|
        # Send instructions so user can enter a new password:
        UserMailer.welcome_reset_password_instructions(user).deliver
        p user.id
      end
   end

We’ve already configured our development environment to run Email Sandbox, so all we need to do to test out the rake task is run the following code:

$ bin/rails devise:send_welcome_reset_password_instructions_to_all_users

If everything went well, you should see your emails in your Email Sandbox virtual inbox, where you can further inspect and debug them.

And there you have it! You’ve successfully learned how to set up emails for forgotten password resets using Devise and test the same with Email Sandbox.

This is a common challenge in all types of applications and is a useful skill for any Rails developer to learn. Good job!

Exit mobile version