/ Tags: RAILS / Categories: RAILS

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.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More