关于 action-mailer
面向开发者和编码 Agent 的 Rails Action Mailer 技能,用于创建和维护应用邮件,涵盖邮件类、模板、投递配置、预览、测试和附件。
主要功能
- 01参数化邮件类模式
- 02HTML 与纯文本邮件模板
- 03队列及 SMTP 投递配置
- 04预览与投递测试
- 05附件和内嵌图片
- 06回调与邮件本地化
适用场景
- 01创建账户及密码邮件
- 02构建多部分邮件模板
- 03按环境配置邮件投递
- 04预览并测试邮件内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill action-mailer安装后在 Claude Code 中输入/action-mailer确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/configuration.mddelivery.mdinterceptors.mdpreviews.mdtemplates.mdtesting.md详细介绍
Rails Action Mailer 专家
使用 Action Mailer 从 Rails 应用中创建、配置并发送电子邮件。
理念
核心原则:
- 默认使用
deliver_later— 通过 Active Job 将电子邮件加入队列。deliver_now会阻塞 HTTP 请求,让用户等待 SMTP 往返。 - 同时创建 HTML 和文本模板 — 一些电子邮件客户端会屏蔽 HTML,垃圾邮件过滤器也会降低仅含 HTML 的电子邮件的评分。务必提供文本备用版本。
- 使用参数化邮件程序 — 通过
with()传递上下文,而不是方法参数。这样回调和共享设置都能顺畅运行。 - 设置
default from:— 每个邮件程序都需要默认发件人。否则,电子邮件可能会静默失败或被邮件服务器拒绝。 - 编写预览 — 预览类让你无需发送真实电子邮件或手动编写测试数据,就能迭代调整电子邮件设计。
- 使用
_url帮助方法,而不是_path— 电子邮件没有请求上下文,因此相对路径会生成失效链接。_url会生成绝对 URL。
何时使用此技能
- 生成新的邮件程序类
- 创建电子邮件模板(HTML + 文本)
- 配置 SMTP/发送设置
- 添加附件或内嵌图片
- 编写邮件程序预览和测试
- 设置拦截器或观察器
- 为电子邮件主题实现 I18n
- 调试电子邮件发送问题
说明
第 1 步:检查现有邮件程序模式
先检查项目 — 为保持一致,请遵循现有邮件程序模式:
# Find existing mailers
ls app/mailers/
# Check ApplicationMailer defaults
cat app/mailers/application_mailer.rb
# Find existing templates
ls app/views/layouts/mailer.*
find app/views -name "*.html.erb" -path "*/mailer*" -o -name "*.text.erb" -path "*/mailer*"
# Check delivery config
grep -r "action_mailer" config/environments/
# Find existing previews
ls test/mailers/previews/
与现有代码库保持一致,比理论上的最佳实践更重要。
第 2 步:生成或创建邮件程序
使用包含操作名称的生成器:
bin/rails generate mailer User welcome_email password_reset
这会创建:
app/mailers/user_mailer.rb— 邮件程序类app/views/user_mailer/welcome_email.html.erb— HTML 模板app/views/user_mailer/welcome_email.text.erb— 文本模板app/views/user_mailer/password_reset.html.erbapp/views/user_mailer/password_reset.text.erbtest/mailers/user_mailer_test.rb— 测试文件test/mailers/previews/user_mailer_preview.rb— 预览类
如果 ApplicationMailer 不存在,请创建它:
# app/mailers/application_mailer.rb
class ApplicationMailer < ActionMailer::Base
default from: "[email protected]"
layout "mailer"
end
第 3 步:编写邮件程序类
使用参数化邮件程序(with/params 模式):
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
default from: "[email protected]"
def welcome_email
@user = params[:user]
@login_url = login_url
mail(
to: email_address_with_name(@user.email, @user.name),
subject: "Welcome to #{app_name}"
)
end
def password_reset
@user = params[:user]
@token = params[:token]
@reset_url = edit_password_reset_url(token: @token)
mail(to: @user.email, subject: "Reset your password")
end
private
def app_name
Rails.application.class.module_parent_name
end
end
调用邮件程序 — 优先使用 deliver_later:
# Preferred — async via Active Job (user doesn't wait for SMTP)
UserMailer.with(user: @user).welcome_email.deliver_later
# With params
UserMailer.with(user: @user, token: @token).password_reset.deliver_later
# Synchronous — blocks the request while talking to SMTP server
UserMailer.with(user: @user).welcome_email.deliver_now
仅在以下情况使用 deliver_now: rake 任务/cron 作业、控制台调试,或需要立即确认的关键电子邮件。
第 4 步:创建 HTML 和文本模板
同时创建 HTML 和文本版本。 两者都存在时,Action Mailer 会自动生成 multipart/alternative 电子邮件。
HTML 模板(app/views/user_mailer/welcome_email.html.erb):
<h1>Welcome, <%= @user.name %>!</h1>
<p>Thanks for signing up. Your account is ready.</p>
<p>
<%= link_to "Log in to your account", @login_url %>
</p>
文本模板(app/views/user_mailer/welcome_email.text.erb):
Welcome, <%= @user.name %>!
Thanks for signing up. Your account is ready.
Log in: <%= @login_url %>
为什么要两种版本? 一些电子邮件客户端会屏蔽 HTML。垃圾邮件过滤器会降低仅含 HTML 的电子邮件的评分。文本备用版本是确保邮件送达的必要条件。
第 5 步:配置 URL 主机
电子邮件需要绝对 URL,因为没有浏览器请求上下文来解析相对路径:
# config/environments/development.rb
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
# config/environments/production.rb
config.action_mailer.default_url_options = { host: "www.example.com", protocol: "https" }
在模板中,使用 _url 帮助方法:
<%# CORRECT %>
<%= link_to "View order", order_url(@order) %>
<%# WRONG — will break in email %>
<%= link_to "View order", order_path(@order) %>
第 6 步:编写邮件程序预览
为每个邮件程序操作编写预览 — 这样你就可以在 /rails/mailers 中迭代调整设计,而无需发送真实电子邮件:
# test/mailers/previews/user_mailer_preview.rb
class UserMailerPreview < ActionMailer::Preview
def welcome_email
user = User.first || User.new(name: "Preview User", email: "[email protected]")
UserMailer.with(user: user).welcome_email
end
def password_reset
user = User.first || User.new(name: "Preview User", email: "[email protected]")
UserMailer.with(user: user, token: "preview-token-123").password_reset
end
end
预览提示:
- 使用
User.first,并提供回退选项User.new(...),这样即使数据库为空,预览也能正常运行 - 只需返回邮件对象 — 不要在预览中调用
deliver_later - 模板更改后,预览会自动重新加载
第 7 步:编写邮件程序测试
# test/mailers/user_mailer_test.rb
require "test_helper"
class UserMailerTest < ActionMailer::TestCase
test "welcome_email" do
user = users(:active_user)
email = UserMailer.with(user: user).welcome_email
assert_emails 1 do
email.deliver_now
end
assert_equal ["[email protected]"], email.from
assert_equal [user.email], email.to
assert_equal "Welcome to MyApp", email.subject
# Test HTML part
assert_match user.name, email.html_part.body.to_s
assert_match "Log in", email.html_part.body.to_s
# Test text part
assert_match user.name, email.text_part.body.to_s
end
end
关键测试断言:
# Count emails sent
assert_emails 1 do
UserMailer.with(user: user).welcome_email.deliver_now
end
# No emails sent
assert_no_emails do
# action that shouldn't send email
end
# Enqueued for later delivery
assert_enqueued_emails 1 do
UserMailer.with(user: user).welcome_email.deliver_later
end
# Check email content
assert_equal ["[email protected]"], email.to
assert_equal ["[email protected]"], email.from
assert_equal "Subject", email.subject
assert_match "expected text", email.body.encoded
第 8 步:配置发送方式
开发环境 — 使用 letter_opener 或 :test:
# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener # Opens in browser
# OR
config.action_mailer.delivery_method = :test # Stores in ActionMailer::Base.deliveries
生产环境 — SMTP:
# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "smtp.example.com",
port: 587,
domain: "example.com",
user_name: Rails.application.credentials.dig(:smtp, :user_name),
password: Rails.application.credentials.dig(:smtp, :password),
authentication: "plain",
enable_starttls: true,
open_timeout: 5,
read_timeout: 5
}
测试环境 — 始终使用 :test:
# config/environments/test.rb
config.action_mailer.delivery_method = :test
快速参考
mail() 方法选项
| 选项 | 描述 |
|---|---|
to: | 收件人——字符串或数组 |
from: | 发件人(覆盖默认值) |
cc: | 抄送收件人 |
bcc: | 密送收件人 |
reply_to: | 回复地址 |
subject: | 电子邮件主题 |
headers: | 自定义标头哈希 |
template_path: | 自定义视图目录 |
template_name: | 自定义模板名称 |
delivery_method_options: | 针对单封邮件的投递选项 |
附件
def invoice_email
@invoice = params[:invoice]
# Simple attachment
attachments["invoice.pdf"] = File.read("/path/to/invoice.pdf")
# Attachment with options
attachments["report.csv"] = {
mime_type: "text/csv",
content: generate_csv_data
}
mail(to: @invoice.customer_email, subject: "Your Invoice")
end
内嵌附件(电子邮件正文中的图片)
# In mailer
def newsletter
attachments.inline["logo.png"] = File.read("app/assets/images/logo.png")
mail(to: params[:user].email, subject: "Newsletter")
end
<%# In HTML template %>
<%= image_tag attachments["logo.png"].url, alt: "Company Logo" %>
回调
class ApplicationMailer < ActionMailer::Base
before_action :set_default_vars
after_action :log_delivery
after_deliver :record_sent
private
def set_default_vars
@company_name = "MyApp"
end
def log_delivery
Rails.logger.info("Preparing email: #{action_name}")
end
def record_sent
# Called after successful delivery
EmailLog.create!(mailer: self.class.name, action: action_name)
end
end
拦截器和观察器
# config/initializers/mail_interceptors.rb
Rails.application.configure do
if Rails.env.staging?
config.action_mailer.interceptors = %w[SandboxEmailInterceptor]
end
config.action_mailer.observers = %w[EmailDeliveryObserver]
end
# app/interceptors/sandbox_email_interceptor.rb
class SandboxEmailInterceptor
def self.delivering_email(message)
message.to = ["[email protected]"]
end
end
# app/observers/email_delivery_observer.rb
class EmailDeliveryObserver
def self.delivered_email(message)
EmailDelivery.log(message)
end
end
Mailer 中的 rescue_from
class NotifierMailer < ApplicationMailer
rescue_from ActiveJob::DeserializationError do |exception|
# Handle stale records gracefully
end
rescue_from Net::SMTPAuthenticationError do |exception|
# Handle SMTP auth failures
end
end
电子邮件主题的 I18n
# config/locales/en.yml
en:
user_mailer:
welcome_email:
subject: "Welcome to %{app_name}"
password_reset:
subject: "Reset your password"
# Mailer — omit subject: to auto-lookup from I18n
def welcome_email
@user = params[:user]
mail(to: @user.email) # Subject from en.user_mailer.welcome_email.subject
end
用于共享上下文的参数化 Mailer
使用 before_action + params 在多个操作之间共享上下文:
class InvitationsMailer < ApplicationMailer
before_action { @inviter = params[:inviter]; @invitee = params[:invitee] }
default to: -> { @invitee.email },
from: -> { email_address_with_name("[email protected]", @inviter.name) }
def account_invitation
mail subject: "#{@inviter.name} invited you to #{params[:inviter].account.name}"
end
end
# Call with: InvitationsMailer.with(inviter: user, invitee: other).account_invitation.deliver_later
详细参考
如需了解更深入的模式和示例,请参阅 references/ 目录:
references/templates.md— Mailer 类模式、HTML/文本模板、附件、I18n、布局references/delivery.md— SMTP 配置、按环境设置、错误处理references/testing.md— Mailer 测试模式、投递断言、集成测试references/interceptors.md— 回调、拦截器、观察器、投递生命周期references/previews.md— 预览类、备用数据、自定义预览路径references/configuration.md— 边缘情况、易错点、生产环境配置方案
Agent 常见错误
- 使用
deliver_now而不是deliver_later— 等待 SMTP 时会阻塞 HTTP 请求;用户会遇到页面加载缓慢的问题 - 只创建 HTML 模板 — 垃圾邮件过滤器会惩罚仅含 HTML 的电子邮件,而且有些客户端会完全屏蔽 HTML
- 缺少默认的
from:地址 — 邮件服务器会拒绝没有发件人的电子邮件,而且通常不会给出提示 - 在模板中使用
_path辅助方法 — 会生成相对 URL,在电子邮件客户端中无法正常使用(没有请求上下文可用于解析 URL) - 传递参数而不使用
with()— 参数化 Mailer(Mailer.with(user: @user).action)可以与回调和共享设置顺畅配合 - 忘记设置
default_url_options— 未配置主机时,_url辅助方法会报错,或在生产环境中生成指向localhost的链接 - 不编写预览 — 没有预览时,你只能发送真实电子邮件,或编写测试来查看电子邮件的外观
- 未按环境配置投递方式 — 测试环境使用
:test,开发环境使用:letter_opener,生产环境使用:smtp
应避免的反模式
- 臃肿的 Mailer — 将业务逻辑放在模型/服务中。Mailer 只应负责组装和发送;在 Mailer 中实现逻辑会导致测试和复用困难
- 在模板中使用内联样式 — 使用带有共享样式的 Mailer 布局;可以考虑使用
premailer-rails自动将 CSS 转为内联样式,以兼容电子邮件客户端 - 硬编码 URL — 带
_url后缀的路由辅助方法会在路由变更时保持正确;硬编码字符串则会过时 - 没有纯文本备用内容 — 仅含 HTML 的电子邮件会降低投递评分,且屏幕阅读器无法访问
- 在控制器中同步投递 — 在请求中调用
deliver_now会让用户等待 SMTP 往返 - 未经测试的 Mailer — 测试内容、收件人、主题和投递数量
- 在 Mailer 视图中放置机密信息 — 切勿在电子邮件中暴露令牌/密码;应改用有效期短且经过哈希处理的链接
文件结构
app/
mailers/
application_mailer.rb # Base class — default from, layout
user_mailer.rb # Mailer class
views/
layouts/
mailer.html.erb # HTML layout (wraps all mailer HTML views)
mailer.text.erb # Text layout
user_mailer/
welcome_email.html.erb # HTML template
welcome_email.text.erb # Text template
config/
environments/
development.rb # delivery_method, default_url_options
production.rb # SMTP settings
test.rb # delivery_method: :test
test/
mailers/
user_mailer_test.rb # Mailer tests
previews/
user_mailer_preview.rb # Email previews