Action Mailer In Rails: A Practical Guide
Learn how Action Mailer in Rails works: build mailers, configure SMTP, use deliver_later, preview and test emails in Rails 8.1.
By Jean Emmanuel Cadet • 27 min read
Almost every web application needs to talk to its users outside the browser. A new user expects a welcome message, a forgotten password needs a reset link, and a customer who just paid wants a receipt. Email is still the most reliable channel for these moments, and if you are searching for how to send emails with Rails, Action Mailer in Rails is the framework component you need.
Action Mailer lets you build emails the same way you build web pages: with classes, methods, and view templates. You write a mailer, render a template, and hand the finished message to a delivery method. The framework handles the rest, including message formatting, multipart bodies, and integration with Active Job for background delivery.
In this guide, you will learn how Action Mailer works, how to generate and write your first mailer, how to build HTML and plain-text templates, and how to pass data safely. You will also see how to configure Rails SMTP settings securely, choose between deliver_now and deliver_later, avoid sending emails before a database commit, and preview and test your emails. Common use cases like password resets and order confirmations are covered too, along with the mistakes that cause unreliable email in production.
The examples target Rails 8.1 and use Minitest, the default Rails test framework. Where a detail depends on your Rails version, the article says so.
What Is Action Mailer in Rails?
Action Mailer is the part of Rails responsible for creating and sending emails. It follows the same conventions as the rest of the framework. A mailer is a Ruby class that inherits from ActionMailer::Base (usually through ApplicationMailer). Its public methods are called mailer actions, and each action prepares one kind of email. Its views are ordinary Rails templates stored in app/views.
If you have written a controller, the structure will feel familiar:
Controller world | Mailer world |
|---|---|
Controller class | Mailer class |
Controller action | Mailer action |
|
|
View template | Email template |
Layout | Mailer layout |
HTTP response | Email message |
The Building Blocks
- Mailer: a class such as
UserMailerthat groups related emails. - Mailer action: a method such as
welcome_emailthat builds one email. - Views: templates such as
welcome_email.html.erbandwelcome_email.text.erbthat hold the email body. - Layouts: shared wrappers (
mailer.html.erbandmailer.text.erb) that give every email a consistent header, footer, and style. - Mail object: the final message, represented by an instance of
Mail::Message.
Action Mailer and the Mail Gem
Action Mailer does not implement the email format itself. Under the hood, it builds on the Mail gem, which parses, constructs, and delivers messages that follow the email standards. Action Mailer adds the Rails layer on top: mailer classes, template rendering, configuration, previews, test helpers, and Active Job integration.
In practice, this means the object returned by a mailer action behaves like a Mail::Message. You can inspect to, from, subject, and the message parts using the Mail gem's API, which is especially useful in tests.
Action Mailer Is Not Your Email Provider
One distinction trips up many beginners. Action Mailer builds the message and hands it to a delivery method. It is not an email service. Something else has to actually move the message across the internet: an SMTP server, a transactional email provider, or another delivery mechanism. Think of Action Mailer as the part that writes and addresses the letter, and your provider as the postal service.
It is also different from Active Job, which runs work in the background, and Action Mailbox, which handles incoming email. You will see how they connect later in this guide.
How Action Mailer Works
Understanding the lifecycle of an email makes everything else easier, especially the difference between building a message and delivering it.
The Email Lifecycle
When you write UserMailer.welcome_email(user).deliver_later, several steps happen:
Your code calls UserMailer.welcome_email(user)
|
v
Rails returns a lazy ActionMailer::MessageDelivery object
|
v
deliver_now or deliver_later is called
|
+--> deliver_now: the mailer action runs immediately
|
+--> deliver_later: a job is enqueued with Active Job,
and a worker runs the mailer action later
|
v
The mailer action runs and calls mail(...)
|
v
Rails renders the templates and builds a Mail::Message
|
v
The configured delivery method (SMTP, file, test) delivers it
Calling a Mailer Action
A mailer action is called on the class, not on an instance, even though it is defined as an instance method. Rails uses method_missing to turn UserMailer.welcome_email(user) into a call to the instance method. The return value is an ActionMailer::MessageDelivery object, which is a lightweight wrapper that delays the real work.
Nothing has been rendered and nothing has been sent at this point. That is deliberate. It lets you choose how to deliver the message by calling deliver_now or deliver_later.
Building the Message
Inside the mailer action, you set up instance variables for your templates and then call mail. This method accepts headers such as to, from, subject, cc, bcc, and reply_to. It then looks for templates that match the mailer and action name, renders each one, and assembles a multipart message when more than one format exists.
Generating vs. Delivering
Keep this distinction clear:
- Generating a message means running the mailer action and rendering templates. You can do this in a test or a preview without sending anything.
- Delivering a message means passing the finished
Mail::Messageto the delivery method configured for the current environment.
This separation is why previews and tests are so convenient. You can inspect exactly what would be sent without touching a real mail server.
Creating Your First Mailer
Rails includes a generator that creates the mailer class, its view folder, a preview, and a test. Run:
bin/rails generate mailer UserMailer
If you already know the first email you want, you can include the action name and Rails will create the templates too:
bin/rails generate mailer UserMailer welcome_email
What the Generator Creates
app/mailers/application_mailer.rb: the base class for your mailers (created if it does not already exist).app/mailers/user_mailer.rb: your new mailer class.app/views/user_mailer/: the folder for this mailer's templates. With an action name, it contains both an HTML and a plain-text template.app/views/layouts/mailer.html.erbandmailer.text.erb: the shared layouts.test/mailers/user_mailer_test.rb: a starter test.test/mailers/previews/user_mailer_preview.rb: a preview class for browsing the email in development.
Setting Application-Wide Defaults
Open ApplicationMailer and set the sender address once, so every mailer inherits it:
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "CodeCurious <[email protected]>"
layout "mailer"
end
Replace the address with one on a domain you control. Many providers require the sender address or domain to be verified, so check your provider's documentation.
A Welcome Email Mailer
Now write a practical mailer action:
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome_email(user)
@user = user
@login_url = new_session_url
mail(
to: @user.email,
subject: "Welcome to CodeCurious, #{@user.name}"
)
end
end
This action receives a User, stores it in an instance variable so the templates can use it, builds an absolute login URL, and calls mail with the recipient and subject. The sender comes from ApplicationMailer.
Two details deserve attention. First, mailers need absolute URLs (_url helpers), not relative paths (_path helpers), because an email is read outside your site. Second, URL helpers require default_url_options to be configured per environment, which is covered in the delivery section.
Invoking the Mailer
Call the mailer wherever the event happens, for example after a user signs up:
# app/controllers/users_controller.rb
def create
@user = User.new(user_params)
if @user.save
UserMailer.welcome_email(@user).deliver_later
redirect_to root_path, notice: "Welcome! Check your inbox."
else
render :new, status: :unprocessable_entity
end
end
The deliver_later call places the email in a background job so the user does not wait for the mail server. The difference between deliver_later and deliver_now is explained below.
Writing Email Templates
Action Mailer uses the same view system as the rest of Rails. The template name must match the mailer action, and the file extension tells Rails which format it produces. For the welcome_email action, Rails looks for app/views/user_mailer/welcome_email.html.erb and welcome_email.text.erb.
The HTML Template
<%# app/views/user_mailer/welcome_email.html.erb %>
<h1>Welcome, <%= @user.name %>!</h1>
<p>Thanks for joining CodeCurious. Your account is ready.</p>
<p>
<a href="<%= @login_url %>" style="display:inline-block;padding:10px 16px;background:#1d4ed8;color:#ffffff;text-decoration:none;border-radius:4px;">
Sign in to your account
</a>
</p>
The Plain-Text Template
<%# app/views/user_mailer/welcome_email.text.erb %>
Welcome, <%= @user.name %>!
Thanks for joining CodeCurious. Your account is ready.
Sign in to your account: <%= @login_url %>
Why Provide Both Formats?
When both templates exist, Rails sends a multipart/alternative message. The email client picks the version it prefers or can display. Providing both is a good practice because some people read email in text-only clients, some accessibility tools work better with plain text, and some mail systems score messages with no text alternative as less trustworthy. A plain-text version also gives you a readable fallback when HTML rendering goes wrong.
Passing Data with Instance Variables
Just as in controllers, instance variables set in the mailer action are available in the templates. In the example, @user and @login_url are defined in UserMailer#welcome_email and read in both templates. Keep the logic in the templates light. If you find yourself writing conditionals and loops everywhere, move the logic into a helper or prepare the data in the mailer action.
Using Layouts for Consistent Emails
The generated app/views/layouts/mailer.html.erb wraps every HTML email, and mailer.text.erb wraps every text email. This is the right place for your logo, a consistent footer, and shared styles:
<%# app/views/layouts/mailer.html.erb %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body style="font-family: Arial, Helvetica, sans-serif; color: #111827; line-height: 1.5;">
<div style="max-width: 600px; margin: 0 auto; padding: 24px;">
<%= yield %>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 24px 0;" />
<p style="font-size: 12px; color: #6b7280;">
You are receiving this email because you created an account on CodeCurious.
</p>
</div>
</body>
</html>
You can also use a different layout per mailer or per action with the layout method or the layout: option of mail.
Email HTML Is Not Web HTML
Email clients render HTML far less consistently than browsers. A few practical guidelines will save you trouble:
- Use inline styles for anything that matters. Many clients ignore or strip
<style>blocks and external stylesheets. - Keep layouts simple. Single-column designs with a fixed maximum width are the safest. Complex CSS layouts are unreliable across clients.
- Use absolute URLs for links and images. Remember that remote images are often blocked until the reader allows them.
- Always include meaningful link text and an
altattribute on images. - Test in the clients your users actually use, since behavior differs between them.
If you need to embed an image in the message itself, Action Mailer supports inline attachments through attachments.inline. For most transactional emails, a text-based design with a clear button is more dependable.
Passing Data and Using Parameters
Mailer actions can receive data in two ways: as ordinary method arguments or through parameterized mailers.
Method Arguments
You have already seen this style:
UserMailer.welcome_email(user).deliver_later
The welcome_email(user) action receives the record as a normal argument. This works well when an action needs one or two values.
Parameterized Mailers
When a mailer needs several values, or when you want several actions to share the same setup, use with:
# app/mailers/order_mailer.rb
class OrderMailer < ApplicationMailer
before_action :set_order
def confirmation
mail(
to: @order.user.email,
subject: "Order ##{@order.number} confirmed"
)
end
private
def set_order
@order = params[:order]
end
end
OrderMailer.with(order: order).confirmation.deliver_later
The with method stores a hash that the mailer reads through params. A before_action callback then prepares shared data, so each action stays short. This pattern keeps your code tidy as a mailer grows.
Parameters vs. Method Arguments
The difference is mostly about structure, not capability:
- Method arguments are positional values passed directly to a specific action. They are explicit and good for simple cases.
- Mailer parameters are a named hash available as
paramsin every action and callback of that mailer. They are better when several actions need the same data or when you use callbacks.
You can combine them, for example OrderMailer.with(order: order).shipment_update(tracking_code). Pick one style per mailer when possible, so the code stays consistent.
Pass Records, Not Giant Objects
With deliver_later, arguments and parameters are serialized by Active Job. Active Record objects are passed using GlobalID, which stores a reference to the record, then loads a fresh copy when the job runs. This is usually what you want, because the email reflects the current database state. Avoid passing large hashes or objects that cannot be serialized. If you need to pass a simple value, such as a token or a plain string, those work fine.
One consequence is worth remembering: if the record is deleted before the job runs, the job cannot load it. Design for this by sending emails only for records that persist, or by passing the plain values the email needs.
Handling User-Provided Data Safely
Emails often include text that users typed, such as a name or a contact form message. Treat it as untrusted:
- Rely on ERB escaping in HTML templates.
<%= @user.name %>escapes HTML automatically. Avoidrawandhtml_safeon user input. - Do not assume plain-text templates are escaped. They are plain text, so they are safe from HTML injection, but untrusted content can still be misleading. Show it clearly as quoted content.
- Never build recipients or headers directly from raw input. Validate email addresses before using them as
to,reply_to, or similar headers, and keep user-controlled text out of headers when you can. - Do not let users control which links appear. In the sections below, links with tokens should always be generated by your own code.
A practical example is a contact form notification where the visitor's address goes in reply_to:
# app/mailers/contact_mailer.rb
class ContactMailer < ApplicationMailer
def new_message(contact_message)
@contact_message = contact_message
mail(
to: "[email protected]",
reply_to: contact_message.email,
subject: "New contact form message from #{contact_message.name}"
)
end
end
The ContactMessage model should validate the email format and limit the length of the name and message. That keeps unexpected data out of the email before the mailer ever sees it.
Configuring Email Delivery
A mailer builds the message, but a delivery method sends it. Configure delivery per environment in config/environments/.
Common Delivery Methods
:smtp: sends the message through an SMTP server. This is the standard choice for production, and it works with most email providers.:test: stores messages inActionMailer::Base.deliveriesinstead of sending them. Rails uses this in the test environment.:file: writes messages to files on disk, which is helpful in development.:sendmail: hands the message to a localsendmailbinary. It is less common today.
Your email provider might also offer its own Ruby gem and delivery method. If so, follow that provider's documentation.
Development and Test Configuration
In development, writing emails to files avoids accidentally sending real messages:
# config/environments/development.rb
config.action_mailer.delivery_method = :file
config.action_mailer.file_settings = { location: Rails.root.join("tmp/mails") }
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
In the test environment, Rails already uses the :test method. Make sure URL options are set so _url helpers work:
# config/environments/test.rb
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: "www.example.com" }
Production SMTP Configuration
For production, point Action Mailer at your provider's SMTP server and read the sensitive values from the environment:
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default_url_options = {
host: ENV.fetch("APP_HOST"),
protocol: "https"
}
config.action_mailer.smtp_settings = {
address: ENV.fetch("SMTP_ADDRESS"),
port: ENV.fetch("SMTP_PORT", 587).to_i,
domain: ENV.fetch("SMTP_DOMAIN"),
user_name: ENV.fetch("SMTP_USERNAME"),
password: ENV.fetch("SMTP_PASSWORD"),
authentication: :plain,
enable_starttls_auto: true
}
Using ENV.fetch without a default for required values makes the application fail fast at boot if a setting is missing, which is better than silently failing to send emails later.
What Each SMTP Setting Means
address: the hostname of the SMTP server, as provided by your email service.port: the port to connect to. Port 587 with STARTTLS is common, but your provider's documentation decides.domain: the domain name your application identifies itself with during the SMTP conversation (the HELO or EHLO command). It is usually your application's domain.user_nameandpassword: the credentials for authenticating with the server. Some providers use an API key as the password.authentication: the authentication mechanism, such as:plain,:login, or:cram_md5. Use the one your provider requires.enable_starttls_auto: upgrades the connection to an encrypted one when the server supports STARTTLS.
Provider-specific values vary. Always take the exact address, port, and authentication details from your provider's documentation rather than copying them from a blog post, including this one.
Keep Credentials Out of Git
SMTP passwords and API keys must never be hardcoded in a Ruby file or committed to a repository. Anyone with access to the repository, its history, or a leaked backup could send email as you. Use one of two safe approaches:
- Environment variables, set on your hosting platform or in a secrets manager, as shown above.
- Rails encrypted credentials, which keep secrets in an encrypted file that can be committed safely, while the key stays out of version control.
With credentials, the SMTP settings can look like this:
config.action_mailer.smtp_settings = {
address: ENV.fetch("SMTP_ADDRESS"),
port: 587,
domain: ENV.fetch("SMTP_DOMAIN"),
user_name: Rails.application.credentials.dig(:smtp, :user_name),
password: Rails.application.credentials.dig(:smtp, :password),
authentication: :plain,
enable_starttls_auto: true
}
For a deeper walkthrough of editing and managing secrets, see how to use Rails credentials securely.
Delivering Emails: deliver_now vs. deliver_later
Once the message is ready, you choose how to deliver it.
deliver_now
UserMailer.welcome_email(user).deliver_now
deliver_now runs the mailer action and delivers the email immediately, in the current process. If you call it in a controller, the web request waits for the SMTP conversation to finish. That is synchronous delivery. It is simple, and it is useful in scripts, console sessions, and tests where you want to see the result at once.
deliver_later
UserMailer.welcome_email(user).deliver_later
deliver_later puts a job on a queue and returns right away. A background worker picks up the job, runs the mailer action, and delivers the message. That is asynchronous delivery. You can also schedule it:
UserMailer.welcome_email(user).deliver_later(wait: 10.minutes)
How It Works with Active Job
Under the hood, deliver_later uses Active Job. Action Mailer provides a job class, ActionMailer::MailDeliveryJob, and Active Job enqueues it using whichever queue adapter your application is configured with. New Rails 8 applications use Solid Queue as the default production adapter, but you might use Sidekiq, GoodJob, or another backend. If you want a primer on jobs, queues, and adapters, read Active Job in Rails: A Beginner's Guide.
This is also where the boundaries between components become clear:
- Action Mailer builds and delivers the email.
- Active Job runs work in the background and decides when and where the job runs.
- The email provider (or SMTP server) accepts the message and sends it to the recipient's mail system.
Why Asynchronous Delivery Is Usually Better
Contacting a mail server over the network can be slow, and sometimes it fails. If you deliver synchronously during a web request, the user waits for that network call, and a provider timeout can turn a successful signup into an error page. With deliver_later, the request finishes quickly, and email problems are handled in the background where they can be retried and monitored. For user-facing requests, deliver_later is generally the better default.
What Happens When a Job Fails?
Failure behavior depends on your job backend and application settings. A few points to keep in mind:
- Active Job lets you declare retry rules in a job class with
retry_on. The default mail delivery job does not define special retry rules for SMTP errors, so what happens next depends on your adapter and configuration. - Some adapters, such as Sidekiq, have their own retry systems. Others rely on Active Job's configuration.
- If you want explicit control, you can configure a custom delivery job.
# app/jobs/application_mail_delivery_job.rb
class ApplicationMailDeliveryJob < ActionMailer::MailDeliveryJob
retry_on Net::OpenTimeout, Net::ReadTimeout, wait: :polynomially_longer, attempts: 5
end
# config/application.rb
config.action_mailer.delivery_job = "ApplicationMailDeliveryJob"
Be careful with retries. If a failure happens after the provider accepted the message, a retry could send the email twice. Retry only the errors you understand, and prefer failures that clearly happen before the message is accepted, such as connection timeouts.
A Working Job Backend Is Required
deliver_later does nothing useful if no worker is running. In production, you need a configured queue adapter and a process that executes jobs. If you use Solid Queue, make sure its workers are running, either in a dedicated process or through the supervisor setup your deployment uses. A common mistake is to ship deliver_later calls to production without any running worker, so emails pile up in the queue and never leave. Treat the job process as part of your email infrastructure and monitor it.
Sending Emails After Database Transactions
Timing matters when an email depends on data that has to be saved first.
The Problem
Suppose you create an order inside a database transaction and enqueue the confirmation email before the transaction finishes. A background worker might pick up the job quickly and try to load the order before the commit happens. In that case, the record might not be visible yet, and the job can fail. Worse, if the transaction rolls back after the job was enqueued, the customer receives a confirmation for an order that never existed.
Remember a basic rule: do not deliver the email inside the transaction. Network calls do not belong inside a database transaction, because they hold the transaction open and cannot be rolled back. What you want is for the email job to be enqueued only after the commit succeeds. If transactions are new to you, Rails transactions: a practical guide explains how they work.
Pattern 1: Send After the Transaction Block
The simplest and clearest approach is to call the mailer after the transaction block has finished:
# app/services/place_order.rb
class PlaceOrder
def call(user:, cart:)
order = nil
ActiveRecord::Base.transaction do
order = user.orders.create!(total_cents: cart.total_cents)
cart.items.each { |item| order.line_items.create!(item.attributes_for_order) }
cart.clear!
end
OrderMailer.with(order: order).confirmation.deliver_later
order
end
end
If anything inside the block raises an exception, the transaction rolls back and the mailer line is never reached. The email is only enqueued when the data is safely committed.
Pattern 2: after_commit Callbacks
When the email belongs to a single model's lifecycle, an after_commit style callback runs only once the surrounding transaction has committed:
class Order < ApplicationRecord
after_create_commit :send_confirmation_email
private
def send_confirmation_email
OrderMailer.with(order: self).confirmation.deliver_later
end
end
This is convenient, but it hides side effects inside the model, which can surprise you in tests, seeds, and data imports. For more guidance on when callbacks help and when they hurt, see Rails callbacks: what you should know.
Pattern 3: Let Active Job Defer the Enqueue
Active Job can defer enqueueing until after the surrounding transaction commits. Support for this was introduced in Rails 7.2, and the way you configure it has changed across versions. In Rails 8.1, the old global setting and the symbol values (:default, :always, :never) are no longer supported, so opt in per job class using a boolean. Since mailers use ActionMailer::MailDeliveryJob, you can do this through a custom delivery job:
# app/jobs/application_mail_delivery_job.rb
class ApplicationMailDeliveryJob < ActionMailer::MailDeliveryJob
self.enqueue_after_transaction_commit = true
end
# config/application.rb
config.action_mailer.delivery_job = "ApplicationMailDeliveryJob"
With this in place, calling deliver_later inside a transaction waits for the commit before the job is enqueued, and discards the enqueue if the transaction rolls back. Because this area has changed between Rails releases, check the Active Job documentation and release notes for the exact version you run before relying on it. Even then, Pattern 1 stays the most explicit and the easiest to reason about.
Previewing and Testing Emails
Email bugs are annoying because the feedback loop is slow. Previews and tests shorten it.
Action Mailer Previews
A preview is a class that builds an email without sending it, so you can look at it in your browser. Previews live in test/mailers/previews by default (or spec/mailers/previews if you use RSpec). The generator creates the file for you. Here is one for the welcome email:
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome_email
UserMailer.welcome_email(User.first)
end
end
Start your server and visit http://localhost:3000/rails/mailers. You will see a list of previews, and you can open each one to view the HTML and plain-text versions, headers, and recipients. Because previews run your real mailer code and templates, they are the fastest way to adjust copy and layout.
Use stable data in previews. Calling User.first works only if the development database has a user. For more reliable previews, build unsaved records with User.new(name: "Ada", email: "[email protected]") where the template does not need persisted data.
Testing the Mailer
Rails generates mailer tests based on ActionMailer::TestCase. Use them to check recipients, subjects, and content:
# test/mailers/user_mailer_test.rb
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
test "welcome_email builds the expected message" do
user = users(:ada)
email = UserMailer.welcome_email(user)
assert_emails 1 do
email.deliver_now
end
assert_equal ["[email protected]"], email.from
assert_equal [user.email], email.to
assert_equal "Welcome to CodeCurious, #{user.name}", email.subject
assert_match user.name, email.html_part.body.to_s
assert_match user.name, email.text_part.body.to_s
end
end
This test assumes a users(:ada) fixture and that the default sender is [email protected]. It checks the sender, recipient, subject, and both body parts. The assert_emails helper confirms that exactly one email was delivered, using the :test delivery method, so nothing leaves your machine.
Testing That Email Is Queued
When your code uses deliver_later, you want to verify that the right email was enqueued. Rails provides assert_enqueued_email_with for this:
# test/models/order_test.rb
require "test_helper"
class OrderTest < ActiveSupport::TestCase
include ActionMailer::TestHelper
test "marking an order as paid enqueues the confirmation email" do
order = orders(:pending)
assert_enqueued_email_with OrderMailer, :confirmation, params: { order: order } do
order.mark_as_paid!
end
end
end
For a mailer that takes positional arguments instead of parameters, pass them with args::
assert_enqueued_email_with UserMailer, :welcome_email, args: [user] do
UserRegistration.new(user).complete
end
You can also use assert_enqueued_emails to check how many emails were enqueued, and assert_no_enqueued_emails to confirm none were.
Running the Queued Jobs in a Test
Sometimes you want to confirm the whole path, from the user action to the delivered message. Wrap the code in perform_enqueued_jobs:
test "signup delivers a welcome email" do
perform_enqueued_jobs do
post users_url, params: { user: { name: "Ada", email: "[email protected]", password: "secret-password" } }
end
email = ActionMailer::Base.deliveries.last
assert_equal ["[email protected]"], email.to
end
This runs the jobs inline using the test adapter and delivers through the :test delivery method, so no real email is sent. Use the enqueued assertions for fast, focused tests, and use a few full-path tests for important flows.
Common Action Mailer Use Cases
Once you understand the pattern, most transactional emails follow the same recipe: a mailer action, two templates, and a deliver_later call at the right moment.
Welcome Emails
Covered above. Send after the user record is saved, and keep the content short: a greeting, one clear next step, and a way to get help.
Password Reset Emails
Never put the password itself in an email. Send a link containing a signed, expiring token. Rails provides generates_token_for for this:
class User < ApplicationRecord
has_secure_password
generates_token_for :password_reset, expires_in: 15.minutes do
password_salt&.last(10)
end
end
class PasswordsMailer < ApplicationMailer
def reset(user)
@user = user
@token = user.generate_token_for(:password_reset)
mail(to: user.email, subject: "Reset your password")
end
end
The token expires, and including part of the password salt in the block makes the token invalid once the password changes. You can find the user later with User.find_by_token_for(:password_reset, token). Rails 8's built-in authentication generator follows a similar approach with its own passwords mailer.
Account Confirmation Emails
Confirmation follows the same pattern with a different token purpose:
generates_token_for :email_confirmation, expires_in: 24.hours do
end
Using the email in the token block means the link stops working if the address changes. Send the link, and when the user clicks it, look up the user with find_by_token_for and mark the account as confirmed.
Order Confirmation Emails
Use the parameterized OrderMailer shown earlier. Send it after the order is committed, include the order number and a summary, and avoid putting full payment details in the message.
Contact Form Notifications
Use ContactMailer#new_message from the earlier example. Validate the submission, use reply_to for the visitor's address, and consider adding rate limiting or spam protection to the form itself, so the form cannot be used to flood your inbox.
Application Notifications
Comment replies, project invitations, and status changes are all notifications. Group them in a NotificationMailer with one action per event, rather than one generic action that tries to handle everything with conditionals.
Choosing Actions and Avoiding Duplication
A few habits keep your mailers maintainable:
- One action per email type.
confirmationandshipment_updateshould be separate actions with their own templates. - Share setup with callbacks and helpers. Use
before_actionand view helpers instead of copying lines between actions. - Share layout, not content. Put branding in the layout, and let each template focus on its message.
- Group by domain.
OrderMailer,UserMailer, andNotificationMailercommunicate intent better than a single huge mailer.
Security for Links and Tokens
Links in email are sensitive because anyone who has the email can use them. Use signed, expiring tokens for resets and confirmations, make single-purpose tokens, and keep expiry times short. Generate links with HTTPS URLs, and do not log or display tokens unnecessarily. Never include passwords, full card numbers, or similar secrets in an email body.
Common Mistakes and Best Practices
Most email problems in production come from a short list of mistakes. Here is how to avoid them.
Sending emails synchronously during slow requests. Use deliver_later for user-facing flows, so a slow mail server does not slow your application down.
Hardcoding SMTP credentials. Keep them in environment variables or Rails credentials, as described earlier, and rotate them if they are ever exposed.
Forgetting production delivery settings. Development and test settings do not carry over. Check delivery_method, smtp_settings, and default_url_options in production.rb. Missing URL options cause errors or broken links.
Providing only HTML templates. Always add a plain-text version for accessibility, compatibility, and fallback.
Not testing content and delivery behavior. Write mailer tests for recipients, subjects, and key content, and write enqueue tests for the code that triggers the email.
Sending before the transaction commits. Call the mailer after the transaction block, use an after_commit callback, or configure the job to enqueue after commit.
Putting sensitive information in content or logs. Do not email secrets, and be careful about what you log. Mail bodies and recipient addresses can end up in log files, so review your logging setup. Rails logging: how to debug production apps covers how to log useful information without exposing private data.
Assuming deliver_later works without a worker. A queued job needs a running worker. Verify this in staging by triggering a real email.
Not monitoring failures. Watch your job queue for failed mail jobs, track errors from your provider, and use your provider's dashboard to see bounces and complaints. Application-level error reporting helps too. For ideas on structuring failures so they are easy to track, see how to handle errors in Rails APIs, as the same principles of rescuing, reporting, and surfacing errors apply to background jobs.
Practical Recommendations
- Start with a clear list of the emails your application sends, and give each a mailer action.
- Use previews while designing and tests while changing.
- Send a real test email from staging using your actual provider settings before launch.
- Keep emails short, with one primary action.
- Make sure the job process, queue, and delivery settings are monitored like any other production dependency.
Action Mailer vs. Action Mailbox
The names are similar, but the two components solve opposite problems.
Action Mailer sends email. Your application builds a message and delivers it through SMTP or another delivery method. Use it for welcome emails, password resets, receipts, and notifications.
Action Mailbox receives email. It routes incoming messages to mailbox classes inside your application, so you can process them like requests. Messages arrive through an ingress, such as a provider webhook or a mail server relay, and are stored as inbound email records before a mailbox processes them. Use it for features such as replying to a comment by email, creating support tickets from messages sent to a support address, or importing forwarded emails.
If your application only needs to send transactional emails, you need Action Mailer and nothing else. If users should be able to send email to your application, look at Action Mailbox. Many applications eventually use both, with Action Mailer sending notifications and Action Mailbox handling the replies.
Conclusion: Building Reliable Email with Action Mailer in Rails
Action Mailer in Rails gives you a clean, conventional way to build and send email. Mailers define what to send, templates control how it looks, and the delivery method decides how it reaches the outside world. Because the framework builds on the Mail gem, integrates with Active Job, and ships with previews and test helpers, you can treat email like any other part of your application instead of an afterthought.
The details that make email reliable are the ones that are easy to skip. Provide both HTML and plain-text templates. Keep credentials out of source control. Use deliver_later with a configured and running job backend, and send emails only after the database changes they depend on are committed. Preview your messages in the browser, and test recipients, subjects, content, and enqueue behavior.
Your next step is simple: pick one email in your application, such as a welcome or password reset message. Build the mailer, write both templates, preview it, add tests, configure SMTP for staging, and send a real message end to end. Once one complete email workflow works well, the rest of your Rails mailer code will follow the same pattern.