NYC
skills/dchuk/rails_ai_agents/action-mailer-patterns

action-mailer-patterns

SKILL.md

Action Mailer Patterns for Rails 8

Overview

Action Mailer handles transactional emails:

  • HTML and text email templates
  • Layouts for consistent styling
  • Previews for development
  • Background delivery via Active Job (Solid Queue)
  • Internationalized emails

Quick Start

bin/rails generate mailer User welcome password_reset

Configuration

# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.default_url_options = { host: "example.com" }

Application Mailer

# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
  default from: "noreply@example.com"
  layout "mailer"

  helper_method :app_name

  private

  def app_name
    Rails.application.class.module_parent_name
  end
end

TDD Workflow

Mailer Progress:
- [ ] Step 1: Write mailer test (RED)
- [ ] Step 2: Run test (fails)
- [ ] Step 3: Create mailer method
- [ ] Step 4: Create email templates
- [ ] Step 5: Run test (GREEN)
- [ ] Step 6: Create preview

Testing Mailers (Minitest)

Mailer Test

# test/mailers/user_mailer_test.rb
require "test_helper"

class UserMailerTest < ActionMailer::TestCase
  setup do
    @user = users(:one)
  end

  test "welcome email renders headers" do
    mail = UserMailer.welcome(@user)

    assert_equal I18n.t("user_mailer.welcome.subject"), mail.subject
    assert_equal [@user.email_address], mail.to
    assert_equal ["noreply@example.com"], mail.from
  end

  test "welcome email renders HTML body" do
    mail = UserMailer.welcome(@user)

    assert_includes mail.html_part.body.to_s, @user.name
    assert_includes mail.html_part.body.to_s, "Welcome"
  end

  test "welcome email renders text body" do
    mail = UserMailer.welcome(@user)

    assert_includes mail.text_part.body.to_s, @user.name
  end

  test "welcome email includes login link" do
    mail = UserMailer.welcome(@user)

    assert_includes mail.html_part.body.to_s, new_session_url
  end

  test "password_reset email includes token" do
    token = "reset-token-123"
    mail = UserMailer.password_reset(@user, token)

    assert_equal [@user.email_address], mail.to
    assert_includes mail.html_part.body.to_s, token
  end
end

Testing Delivery

# test/integration/registration_test.rb
require "test_helper"

class RegistrationTest < ActionDispatch::IntegrationTest
  test "registration sends welcome email" do
    assert_enqueued_email_with UserMailer, :welcome do
      post registrations_path, params: {
        registration: { email: "new@example.com", name: "Test", password: "password123" }
      }
    end
  end
end

Testing with perform_enqueued_jobs

# test/integration/notification_test.rb
require "test_helper"

class NotificationTest < ActionDispatch::IntegrationTest
  test "sends notification email" do
    assert_emails 1 do
      perform_enqueued_jobs do
        NotificationMailer.daily_digest(users(:one)).deliver_later
      end
    end
  end
end

Mailer Implementation

Basic Mailer

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def welcome(user)
    @user = user
    @login_url = new_session_url

    mail(to: @user.email_address, subject: t(".subject"))
  end

  def password_reset(user, token)
    @user = user
    @token = token
    @reset_url = edit_password_url(token: token)
    @expires_in = "24 hours"

    mail(to: @user.email_address, subject: t(".subject"))
  end
end

Mailer with Attachments

class ReportMailer < ApplicationMailer
  def monthly_report(user, report)
    @user = user
    @report = report

    attachments["report-#{Date.current}.pdf"] = report.to_pdf

    mail(to: @user.email_address, subject: t(".subject"))
  end
end

Bundled Notification Pattern

Send one email with multiple notifications instead of many emails:

class NotificationMailer < ApplicationMailer
  def daily_digest(user)
    @user = user
    @notifications = user.notifications.unread.today

    return if @notifications.empty?

    mail(to: @user.email_address, subject: t(".subject", count: @notifications.count))
  end
end

Email Templates

<%# app/views/user_mailer/welcome.html.erb %>
<h1><%= t(".greeting", name: @user.name) %></h1>
<p><%= t(".intro") %></p>
<p><%= link_to t(".login_button"), @login_url, class: "button" %></p>
<%# app/views/user_mailer/welcome.text.erb %>
<%= t(".greeting", name: @user.name) %>

<%= t(".intro") %>

<%= t(".login_prompt") %>: <%= @login_url %>

Delivery Methods

# Background delivery (preferred)
UserMailer.welcome(user).deliver_later

# With delay
UserMailer.welcome(user).deliver_later(wait: 5.minutes)

# Immediate (avoid in production)
UserMailer.welcome(user).deliver_now

Previews

# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
  def welcome
    user = User.first
    UserMailer.welcome(user)
  end

  def password_reset
    user = User.first
    UserMailer.password_reset(user, "preview-token-123")
  end
end

Access at: http://localhost:3000/rails/mailers

I18n for Emails

# config/locales/mailers/en.yml
en:
  user_mailer:
    welcome:
      subject: "Welcome to Our App!"
      greeting: "Hello %{name}!"
      intro: "Thanks for signing up."
      login_button: "Log In Now"
      login_prompt: "Log in here"
    password_reset:
      subject: "Reset Your Password"

Localized Delivery

class UserMailer < ApplicationMailer
  def welcome(user)
    @user = user
    I18n.with_locale(user.locale || I18n.default_locale) do
      mail(to: @user.email_address, subject: t(".subject"))
    end
  end
end

Checklist

  • Mailer test written first (RED)
  • Mailer method created
  • HTML template created
  • Text template created
  • Uses I18n for all text
  • Preview created
  • Uses deliver_later (not deliver_now)
  • All tests GREEN
Weekly Installs
2
First Seen
7 days ago
Installed on
opencode2
gemini-cli2
antigravity2
claude-code2
windsurf2
codex2