mcpField
A
action-mailer

action-mailer

github.com/thinkoodle开发7190 次安装8 周前更新
代码仓库

关于 action-mailer

面向开发者和编码 Agent 的 Rails Action Mailer 技能,用于创建和维护应用邮件,涵盖邮件类、模板、投递配置、预览、测试和附件。

主要功能

  1. 01参数化邮件类模式
  2. 02HTML 与纯文本邮件模板
  3. 03队列及 SMTP 投递配置
  4. 04预览与投递测试
  5. 05附件和内嵌图片
  6. 06回调与邮件本地化

适用场景

  1. 01创建账户及密码邮件
  2. 02构建多部分邮件模板
  3. 03按环境配置邮件投递
  4. 04预览并测试邮件内容

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/thinkoodle/rails-skills --skill action-mailer

安装后在 Claude Code 中输入/action-mailer确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
configuration.md
delivery.md
interceptors.md
previews.md
templates.md
testing.md

详细介绍

Rails Action Mailer 专家

使用 Action Mailer 从 Rails 应用中创建、配置并发送电子邮件。

理念

核心原则:

  1. 默认使用 deliver_later — 通过 Active Job 将电子邮件加入队列。deliver_now 会阻塞 HTTP 请求,让用户等待 SMTP 往返。
  2. 同时创建 HTML 和文本模板 — 一些电子邮件客户端会屏蔽 HTML,垃圾邮件过滤器也会降低仅含 HTML 的电子邮件的评分。务必提供文本备用版本。
  3. 使用参数化邮件程序 — 通过 with() 传递上下文,而不是方法参数。这样回调和共享设置都能顺畅运行。
  4. 设置 default from: — 每个邮件程序都需要默认发件人。否则,电子邮件可能会静默失败或被邮件服务器拒绝。
  5. 编写预览 — 预览类让你无需发送真实电子邮件或手动编写测试数据,就能迭代调整电子邮件设计。
  6. 使用 _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.erb
  • app/views/user_mailer/password_reset.text.erb
  • test/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 常见错误

  1. 使用 deliver_now 而不是 deliver_later — 等待 SMTP 时会阻塞 HTTP 请求;用户会遇到页面加载缓慢的问题
  2. 只创建 HTML 模板 — 垃圾邮件过滤器会惩罚仅含 HTML 的电子邮件,而且有些客户端会完全屏蔽 HTML
  3. 缺少默认的 from: 地址 — 邮件服务器会拒绝没有发件人的电子邮件,而且通常不会给出提示
  4. 在模板中使用 _path 辅助方法 — 会生成相对 URL,在电子邮件客户端中无法正常使用(没有请求上下文可用于解析 URL)
  5. 传递参数而不使用 with() — 参数化 Mailer(Mailer.with(user: @user).action)可以与回调和共享设置顺畅配合
  6. 忘记设置 default_url_options — 未配置主机时,_url 辅助方法会报错,或在生产环境中生成指向 localhost 的链接
  7. 不编写预览 — 没有预览时,你只能发送真实电子邮件,或编写测试来查看电子邮件的外观
  8. 未按环境配置投递方式 — 测试环境使用 :test,开发环境使用 :letter_opener,生产环境使用 :smtp

应避免的反模式

  1. 臃肿的 Mailer — 将业务逻辑放在模型/服务中。Mailer 只应负责组装和发送;在 Mailer 中实现逻辑会导致测试和复用困难
  2. 在模板中使用内联样式 — 使用带有共享样式的 Mailer 布局;可以考虑使用 premailer-rails 自动将 CSS 转为内联样式,以兼容电子邮件客户端
  3. 硬编码 URL — 带 _url 后缀的路由辅助方法会在路由变更时保持正确;硬编码字符串则会过时
  4. 没有纯文本备用内容 — 仅含 HTML 的电子邮件会降低投递评分,且屏幕阅读器无法访问
  5. 在控制器中同步投递 — 在请求中调用 deliver_now 会让用户等待 SMTP 往返
  6. 未经测试的 Mailer — 测试内容、收件人、主题和投递数量
  7. 在 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

评论

支持 Markdown