Action Mailer in Rails — Production-Ready Email Without the Footguns
Email in Rails looks deceptively simple — generate a mailer, write a template, call deliver_later. The basics take ten minutes. The production footguns take longer to find: emails sent synchronously blocking requests, views rendering with the wrong host, previews missing in development, subject lines missing variables, and unhandled delivery failures. Understanding how Action Mailer works end to end saves you from discovering these in production.
Setting Up a Mailer
Setup:
bin/rails generate mailer UserMailer welcome_email password_reset
This creates app/mailers/user_mailer.rb, app/views/user_mailer/welcome_email.html.erb, app/views/user_mailer/welcome_email.text.erb, and a preview file.
Example:
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "[email protected]"
layout "mailer"
end
Example:
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def welcome_email(user)
@user = user
@app_url = root_url
mail(
to: @user.email,
subject: "Welcome to #{AppConfig.name}, #{@user.first_name}!"
)
end
def password_reset(user, token)
@user = user
@reset_url = edit_password_url(token: token)
mail(
to: @user.email,
subject: "Reset your password"
)
end
end
Instance variables set in the mailer method are available in the template, just like in a controller.
HTML and Text Templates
Always provide both HTML and plain-text templates. Email clients that don’t render HTML get the text version. Spam filters give higher scores to emails with both.
Example:
<!-- app/views/user_mailer/welcome_email.html.erb -->
<!DOCTYPE html>
<html>
<body>
<h1>Welcome, <%= @user.first_name %>!</h1>
<p>Thanks for joining. Click below to get started:</p>
<%= link_to "Go to your dashboard", @app_url, style: "color: #3b82f6;" %>
</body>
</html>
<!-- app/views/user_mailer/welcome_email.text.erb -->
Welcome, <%= @user.first_name %>!
Thanks for joining. Get started here:
<%= @app_url %>
Action Mailer automatically sends a multipart/alternative email with both parts when templates with the same name exist in both .html.erb and .text.erb.
Delivering Emails — Sync vs Background
Example:
# Deliver synchronously — blocks the request thread until delivery completes
UserMailer.welcome_email(@user).deliver_now # Don't do this in web requests
# Deliver asynchronously via Active Job — preferred for web requests
UserMailer.welcome_email(@user).deliver_later
# Deliver with a delay
UserMailer.welcome_email(@user).deliver_later(wait: 5.minutes)
# Deliver at a specific time
UserMailer.digest_email(@user).deliver_later(wait_until: Date.tomorrow.noon)
deliver_later enqueues an ActionMailer::MailDeliveryJob via Active Job. Any Active Job adapter (Solid Queue, Sidekiq, GoodJob) picks it up. deliver_now is acceptable in scripts, rake tasks, and background jobs where blocking doesn’t affect user-facing response time.
Configuration for Different Environments
Setup:
# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener # or :mailcatcher, :smtp
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
# Letter Opener opens emails in the browser during development (gem "letter_opener")
Setup:
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.default_url_options = { host: "yourapp.com", protocol: "https" }
config.action_mailer.raise_delivery_errors = true
config.action_mailer.smtp_settings = {
address: "smtp.sendgrid.net",
port: 587,
user_name: ENV["SENDGRID_USERNAME"],
password: ENV["SENDGRID_API_KEY"],
authentication: :plain,
enable_starttls_auto: true
}
default_url_options is required for URL helpers (root_url, edit_password_url) to generate correct links. Without it, URL helpers raise an error or generate example.com links.
Email Previews
Rails includes a built-in email preview system. Define previews in test/mailers/previews/ and visit http://localhost:3000/rails/mailers in development.
Example:
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome_email
user = User.first || User.new(
first_name: "Ada",
email: "[email protected]"
)
UserMailer.welcome_email(user)
end
def password_reset
user = User.first
token = user.signed_id(purpose: :password_reset, expires_in: 1.hour)
UserMailer.password_reset(user, token)
end
end
Previews let you visually check layout and content without sending real emails. They’re the fastest feedback loop for template changes.
Handling Delivery Failures
Example:
# Catch SMTP errors at the job level
class ApplicationMailer < ActionMailer::Base
rescue_from Net::SMTPFatalError, Errno::ECONNREFUSED do |error|
Rails.logger.error "Mail delivery failed: #{error.message}"
# Don't re-raise — prevents job from retrying indefinitely for hard bounces
end
end
# Or use Active Job retry policies on the delivery job
class ActionMailer::MailDeliveryJob < ApplicationJob
retry_on Net::SMTPTemporaryError, wait: :polynomially_longer, attempts: 5
discard_on Net::SMTPFatalError # Hard bounce — don't retry
end
Distinguish between temporary failures (retry) and permanent ones (discard): a temporary SMTP error is worth retrying, but a 550 User unknown response means the address doesn’t exist and retrying will just generate more bounces.
Sending to Multiple Recipients and Attachments
Example:
class ReportMailer < ApplicationMailer
def weekly_report(team)
@team = team
@report = WeeklyReport.generate_for(team)
# Multiple recipients
mail(
to: team.managers.pluck(:email),
cc: team.lead.email,
bcc: "[email protected]",
subject: "Weekly Report — #{Date.today.strftime('%B %d, %Y')}"
)
end
def export_attachment(user, data)
@user = user
attachments["report.csv"] = {
mime_type: "text/csv",
content: data
}
# Inline attachment (embedded in HTML template)
attachments.inline["logo.png"] = File.read(
Rails.root.join("app/assets/images/logo.png")
)
mail(to: @user.email, subject: "Your export is ready")
end
end
Pro-Tip: Test your emails against real email clients before shipping — especially Outlook. CSS that looks perfect in Gmail renders completely differently in Outlook, which uses Microsoft Word’s rendering engine for HTML. Use a tool like Litmus or Email on Acid to check rendering across clients, or use an email framework like MJML or Foundation for Emails that handles the compatibility quirks for you. A transactional email template that looks broken in 40% of recipients’ clients is a support ticket generator.
Conclusion
Action Mailer handles the full email lifecycle — generation, templates, multipart content, delivery, and previews — with an API that fits the Rails pattern. The production requirements are straightforward: deliver_later to keep email off the request thread, correct default_url_options per environment, proper SMTP configuration, and previews to verify templates without sending. Adding delivery failure handling and logging gives you the observability to know when something goes wrong without discovering it from user reports.
FAQs
Q1: How do I test Action Mailer in RSpec?
Use ActionMailer::Base.deliveries to inspect enqueued emails in tests. expect { action }.to change(ActionMailer::Base.deliveries, :count).by(1) asserts an email was enqueued. Rails test helpers also include assert_emails(1) and assert_enqueued_email_with for more specific assertions.
Q2: What’s the difference between deliver_now and deliver_later?
deliver_now sends synchronously in the current thread. deliver_later enqueues the delivery as a background job. In web requests, always use deliver_later — SMTP connections can take seconds, and you don’t want user-facing requests waiting on third-party services.
Q3: How do I send emails with a dynamic from address?
Override from: per-mailer method: mail(to: user.email, from: "[email protected]", subject: "..."). For reply-to addresses (common for “contact support” patterns), use reply_to: instead of from:.
Q4: Can Action Mailer send emails in multiple languages?
Yes. Set I18n.locale in your mailer method before generating the mail, and use standard t() helpers in your templates. Many apps use I18n.with_locale(user.locale) { mail(...) } to send in the user’s preferred language.
Q5: How do I track email opens and click-throughs?
Use a transactional email provider (SendGrid, Postmark, Resend) that provides open and click tracking out of the box. They inject tracking pixels and link wrapping automatically. Avoid implementing this yourself in Rails — provider-level tracking is more reliable and handles opt-out compliance.
Check viewARU - Brand Newsletter!
Newsletter to DEVs by DEVs - boost your Personal Brand & career! 🚀