skills/action-mailer-patterns/SKILL.md
Implements transactional emails with Action Mailer and TDD. Use when creating email templates, notification emails, password resets, email previews, or when user mentions mailer, email, notifications, or transactional emails.
npx skillsauth add fernandezbaptiste/rails_ai_agents action-mailer-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Action Mailer handles transactional emails in Rails:
# Generate mailer
bin/rails generate mailer User welcome password_reset
# This creates:
# - app/mailers/user_mailer.rb
# - app/views/user_mailer/welcome.html.erb
# - app/views/user_mailer/welcome.text.erb
# - spec/mailers/user_mailer_spec.rb (if using RSpec)
app/
├── mailers/
│ ├── application_mailer.rb # Base mailer
│ └── user_mailer.rb
├── views/
│ ├── layouts/
│ │ └── mailer.html.erb # Email layout
│ └── user_mailer/
│ ├── welcome.html.erb
│ ├── welcome.text.erb
│ ├── password_reset.html.erb
│ └── password_reset.text.erb
spec/
├── mailers/
│ ├── user_mailer_spec.rb
│ └── previews/
│ └── user_mailer_preview.rb
Mailer Progress:
- [ ] Step 1: Write mailer spec (RED)
- [ ] Step 2: Run spec (fails)
- [ ] Step 3: Create mailer method
- [ ] Step 4: Create email templates
- [ ] Step 5: Run spec (GREEN)
- [ ] Step 6: Create preview
- [ ] Step 7: Test delivery integration
# 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" }
config.action_mailer.smtp_settings = {
address: "smtp.example.com",
port: 587,
user_name: Rails.application.credentials.smtp[:user_name],
password: Rails.application.credentials.smtp[:password],
authentication: "plain",
enable_starttls_auto: true
}
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "[email protected]"
layout "mailer"
# Shared helper for all mailers
helper_method :app_name
private
def app_name
Rails.application.class.module_parent_name
end
end
# spec/mailers/user_mailer_spec.rb
require "rails_helper"
RSpec.describe UserMailer, type: :mailer do
describe "#welcome" do
let(:user) { create(:user, email_address: "[email protected]", name: "John") }
let(:mail) { described_class.welcome(user) }
it "renders the headers" do
expect(mail.subject).to eq(I18n.t("user_mailer.welcome.subject"))
expect(mail.to).to eq(["[email protected]"])
expect(mail.from).to eq(["[email protected]"])
end
it "renders the HTML body" do
expect(mail.html_part.body.to_s).to include("John")
expect(mail.html_part.body.to_s).to include("Welcome")
end
it "renders the text body" do
expect(mail.text_part.body.to_s).to include("John")
expect(mail.text_part.body.to_s).to include("Welcome")
end
it "includes login link" do
expect(mail.html_part.body.to_s).to include(new_session_url)
end
end
describe "#password_reset" do
let(:user) { create(:user) }
let(:token) { "reset-token-123" }
let(:mail) { described_class.password_reset(user, token) }
it "renders the headers" do
expect(mail.subject).to eq(I18n.t("user_mailer.password_reset.subject"))
expect(mail.to).to eq([user.email_address])
end
it "includes reset link with token" do
expect(mail.html_part.body.to_s).to include(token)
end
it "expires link information" do
expect(mail.html_part.body.to_s).to include("24 hours")
end
end
end
# spec/services/user_registration_service_spec.rb
RSpec.describe UserRegistrationService do
describe "#call" do
it "sends welcome email" do
expect {
described_class.new.call(user_params)
}.to have_enqueued_mail(UserMailer, :welcome)
end
end
end
# Integration test
RSpec.describe "User Registration", type: :request do
it "sends welcome email after registration" do
expect {
post registrations_path, params: valid_params
}.to have_enqueued_mail(UserMailer, :welcome)
end
end
# 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
class ReportMailer < ApplicationMailer
def monthly_report(user, report)
@user = user
@report = report
# Attach file
attachments["report-#{Date.current}.pdf"] = report.to_pdf
# Attach inline image
attachments.inline["logo.png"] = File.read(Rails.root.join("app/assets/images/logo.png"))
mail(to: @user.email_address, subject: t(".subject"))
end
end
class NotificationMailer < ApplicationMailer
def notify(recipient, sender, message)
@recipient = recipient
@sender = sender
@message = message
mail(
to: @recipient.email_address,
from: "#{@sender.name} <[email protected]>",
reply_to: @sender.email_address,
subject: t(".subject", sender: @sender.name)
)
end
end
<%# app/views/user_mailer/welcome.html.erb %>
<h1><%= t(".greeting", name: @user.name) %></h1>
<p><%= t(".intro") %></p>
<p><%= t(".getting_started") %></p>
<ul>
<li><%= t(".step_1") %></li>
<li><%= t(".step_2") %></li>
<li><%= t(".step_3") %></li>
</ul>
<p>
<%= link_to t(".login_button"), @login_url, class: "button" %>
</p>
<p><%= t(".help_text_html", support_email: mail_to("[email protected]")) %></p>
<%# app/views/user_mailer/welcome.text.erb %>
<%= t(".greeting", name: @user.name) %>
<%= t(".intro") %>
<%= t(".getting_started") %>
* <%= t(".step_1") %>
* <%= t(".step_2") %>
* <%= t(".step_3") %>
<%= t(".login_prompt") %>: <%= @login_url %>
<%= t(".help_text", support_email: "[email protected]") %>
<%# app/views/layouts/mailer.html.erb %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #0066cc;
color: #ffffff;
text-decoration: none;
border-radius: 4px;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #eee;
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<%= yield %>
<div class="footer">
<p><%= t("mailer.footer.company_name") %></p>
<p><%= t("mailer.footer.address") %></p>
</div>
</body>
</html>
# spec/mailers/previews/user_mailer_preview.rb
# OR test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome
user = User.first || FactoryBot.build(:user, name: "Preview User")
UserMailer.welcome(user)
end
def password_reset
user = User.first || FactoryBot.build(:user)
UserMailer.password_reset(user, "preview-token-123")
end
end
Access previews at: http://localhost:3000/rails/mailers
class OrderMailerPreview < ActionMailer::Preview
def confirmation
order = Order.last || build_preview_order
OrderMailer.confirmation(order)
end
def confirmation_with_discount
order = build_preview_order
order.discount_cents = 1000
OrderMailer.confirmation(order)
end
def confirmation_multiple_items
order = build_preview_order
3.times { order.line_items.build(product: Product.first, quantity: 2) }
OrderMailer.confirmation(order)
end
private
def build_preview_order
Order.new(
user: User.first,
total_cents: 5000,
created_at: Time.current
)
end
end
# config/locales/mailers/en.yml
en:
user_mailer:
welcome:
subject: "Welcome to Our App!"
greeting: "Hello %{name}!"
intro: "Thanks for signing up."
getting_started: "Here's how to get started:"
step_1: "Complete your profile"
step_2: "Explore features"
step_3: "Connect with others"
login_button: "Log In Now"
login_prompt: "Log in here"
help_text: "Questions? Email us at %{support_email}"
help_text_html: "Questions? Email us at %{support_email}"
password_reset:
subject: "Reset Your Password"
mailer:
footer:
company_name: "Example Company"
address: "123 Main St, City, Country"
# config/locales/mailers/fr.yml
fr:
user_mailer:
welcome:
subject: "Bienvenue sur notre application !"
greeting: "Bonjour %{name} !"
# ...
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
UserMailer.welcome(user).deliver_now
# Uses Active Job
UserMailer.welcome(user).deliver_later
# With options
UserMailer.welcome(user).deliver_later(wait: 5.minutes)
UserMailer.welcome(user).deliver_later(wait_until: Date.tomorrow.noon)
UserMailer.welcome(user).deliver_later(queue: :mailers)
class UserRegistrationService
def call(params)
user = User.create!(params)
UserMailer.welcome(user).deliver_later
success(user)
end
end
class NotificationMailer < ApplicationMailer
def daily_digest(user)
@user = user
@notifications = user.notifications.unread.today
# Don't send if nothing to report
return if @notifications.empty?
mail(to: @user.email_address, subject: t(".subject"))
end
end
class NewsletterJob < ApplicationJob
def perform
User.subscribed.find_each(batch_size: 100) do |user|
NewsletterMailer.weekly(user).deliver_later
end
end
end
class ApplicationMailer < ActionMailer::Base
after_action :log_delivery
private
def log_delivery
Rails.logger.info("Sending #{action_name} to #{mail.to}")
end
end
deliver_later (not deliver_now)development
Creates ViewComponents for reusable UI elements with TDD. Use when building reusable UI components, extracting complex partials, creating cards/tables/badges/modals, or when user mentions ViewComponent, components, or reusable UI.
development
Guides Test-Driven Development workflow with Red-Green-Refactor cycle. Use when the user wants to implement a feature using TDD, write tests first, follow test-driven practices, or mentions red-green-refactor.
data-ai
Configures Solid Queue for background jobs in Rails 8. Use when setting up background processing, creating background jobs, configuring job queues, or migrating from Sidekiq to Solid Queue.
testing
Creates service objects following single-responsibility principle with comprehensive specs. Use when extracting business logic from controllers, creating complex operations, implementing interactors, or when user mentions service objects or POROs.