关于 action-mailbox
为 Rails 开发者提供 Action Mailbox 实践指导,涵盖入站邮件路由、处理、拒收和测试。重点强调及时提取业务数据、明确退信及使用真实邮件样本。
主要功能
- 01按收件地址模式路由邮件
- 02编写专注的邮箱处理类
- 03通过回调校验并退信
- 04处理附件及多部分邮件
- 05使用 .eml 样本测试
适用场景
- 01为 Rails 应用配置入站邮件
- 02将支持邮件转为业务记录
- 03拒绝无效发件人并发送说明
- 04排查邮件路由和处理问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ thinkoodle/ rails-skills --skill action-mailbox安装后在 Claude Code 中输入/action-mailbox确认已加载。
附带文件reference.md
详细介绍
Rails Action Mailbox 专家
将收到的电子邮件路由到类似控制器的邮箱类,以便在 Rails 应用中进行处理。
理念
核心原则:
- 邮箱是处理电子邮件的控制器 — 根据地址模式进行路由,并在专用类中处理
- 尽早提取数据,稍后销毁原始邮件 — 立即将电子邮件中的领域数据持久化;原始邮件仅作临时保存
- 明确退信 — 拒绝不合规的电子邮件时发送内容清晰的退信,不要悄悄丢弃
- 使用真实的电子邮件样例进行测试 — 使用
.eml文件,而非手动构造 Mail 对象 - 每个关注点对应一个邮箱 — 保持邮箱职责专一;拆分路由,而不是在
process中分支处理
何时使用此技能
- 在 Rails 应用中设置 Action Mailbox
- 配置入口服务商(Mailgun、SendGrid、Postmark、Postfix 等)
- 编写邮箱类以处理收到的电子邮件
- 将电子邮件路由到正确的邮箱
- 通过 Active Storage 处理电子邮件附件
- 退回或拒绝无效的电子邮件
- 测试收到的电子邮件处理
- 调试电子邮件投递或处理问题
说明
步骤 1:安装 Action Mailbox
bin/rails action_mailbox:install
bin/rails db:migrate
这会创建:
app/mailboxes/application_mailbox.rb— 路由中心action_mailbox_inbound_emails表的迁移- Active Storage 表(如果尚未创建)
验证设置:
# Check the migration ran
bin/rails runner "puts ActionMailbox::InboundEmail.table_exists?"
# Check conductor is available (dev only)
# Visit http://localhost:3000/rails/conductor/action_mailbox/inbound_emails
步骤 2:配置路由
ApplicationMailbox 是路由器。它会将电子邮件收件人与模式进行匹配,并分派到邮箱类。
# app/mailboxes/application_mailbox.rb
class ApplicationMailbox < ActionMailbox::Base
# Route by To/CC/BCC address patterns (regex or string)
routing(/^support@/i => :support)
routing(/^reply\+(.+)@/i => :replies)
routing(/@invoices\./i => :invoices)
routing(all: => :catch_all) # Fallback — use sparingly
end
路由规则:
- 模式会匹配
to、cc和bcc字段 - 首个匹配项优先 — 顺序很重要
- 仅当必须处理每封电子邮件时,才使用
all:作为兜底规则 - 未匹配的电子邮件会保持
pending状态,之后被销毁
⚠️ 常见错误: 忘记路由匹配的是所有收件人字段,而不仅仅是 to。抄送到匹配地址的电子邮件也会被路由。
步骤 3:生成邮箱类
bin/rails generate mailbox support
这会创建 app/mailboxes/support_mailbox.rb:
class SupportMailbox < ApplicationMailbox
def process
# Your processing logic here
end
end
步骤 4:实现 Process 方法
process 方法用于从电子邮件中提取数据,并将其持久化到领域模型中。
class SupportMailbox < ApplicationMailbox
def process
ticket = SupportTicket.create!(
subject: mail.subject,
body: mail.decoded,
from_email: mail.from.first,
from_name: mail.from_address&.display_name
)
# Handle attachments
mail.attachments.each do |attachment|
ticket.files.attach(
io: StringIO.new(attachment.decoded),
filename: attachment.filename,
content_type: attachment.content_type
)
end
end
end
mail(一个 Mail::Message)上的关键访问器:
mail.to— To 地址数组mail.from— From 地址数组mail.cc— CC 地址数组mail.subject— 主题行mail.body.decoded/mail.decoded— 解码后的正文文本mail.date— 发送日期mail.message_id— 唯一消息 IDmail.in_reply_to— 此邮件回复的消息 IDmail.attachments— 附件数组mail.multipart?— 电子邮件是否包含多个部分mail.text_part/mail.html_part— 指定的 MIME 部分
inbound_email 访问器:
inbound_email—ActionMailbox::InboundEmailActive Record 对象inbound_email.message_id— RFC 2822 Message-IDinbound_email.source— 原始电子邮件来源(RFC 822)
步骤 5:使用回调执行前置检查和副作用
Action Mailbox 提供 before_processing、after_processing 和 around_processing 回调。
class SupportMailbox < ApplicationMailbox
before_processing :ensure_known_sender
before_processing :reject_spam
after_processing :notify_team
def process
create_ticket_from_email
end
private
def ensure_known_sender
unless User.exists?(email_address: mail.from.first)
bounce_with SupportMailer.unknown_sender(inbound_email)
end
end
def reject_spam
if SpamDetector.spam?(mail)
bounce_with SupportMailer.spam_rejected(inbound_email)
end
end
def notify_team
SupportNotificationJob.perform_later(@ticket)
end
end
⚠️ 常见错误: 不理解 bounce_with 会中止处理。它会将状态设为 bounced、发送退信,然后停止 — process 方法不会运行。
步骤 6:正确退信
bounce_with 接受一条 Action Mailer 消息,并会:
- 发送退信
- 将
inbound_email.status设为bounced - 中止后续处理
# In a before_processing callback:
bounce_with UserMailer.not_authorized(inbound_email)
# The mailer is a normal Action Mailer:
class UserMailer < ApplicationMailer
def not_authorized(inbound_email)
@email = inbound_email
mail(
to: @email.mail.from.first,
subject: "Unable to process your email"
)
end
end
退信规则:
- 始终在
before_processing回调中退信,不要在process内退信 - 始终提供有意义的退信邮件 — 悄悄丢弃会让发件人感到困惑
- 不要对垃圾邮件退信 — 只需将其标记为已投递,或让它悄悄销毁
步骤 7:配置入口服务提供商
在 config/environments/production.rb 中设置入口:
config.action_mailbox.ingress = :mailgun # or :sendgrid, :postmark, :mandrill, :relay
凭据应放在加密凭据中:
bin/rails credentials:edit
action_mailbox:
ingress_password: "strong-random-password" # For relay/postmark/sendgrid
mailgun_signing_key: "your-mailgun-key" # For Mailgun
mandrill_api_key: "your-mandrill-key" # For Mandrill
或使用环境变量:
RAILS_INBOUND_EMAIL_PASSWORD— 中继入口密码MAILGUN_INGRESS_SIGNING_KEY— Mailgun 签名密钥MANDRILL_INGRESS_API_KEY— Mandrill API 密钥
请参阅 reference.md,了解特定提供商的端点 URL 和配置详情。
步骤 8:测试邮箱
使用 ActionMailbox::TestCase 及其辅助方法:
require "test_helper"
class SupportMailboxTest < ActionMailbox::TestCase
test "creates a support ticket from inbound email" do
assert_difference "SupportTicket.count", 1 do
receive_inbound_email_from_mail(
to: "[email protected]",
from: "[email protected]",
subject: "Help needed",
body: "I can't log in"
)
end
ticket = SupportTicket.last
assert_equal "Help needed", ticket.subject
assert_equal "[email protected]", ticket.from_email
end
test "bounces email from unknown sender" do
inbound_email = receive_inbound_email_from_mail(
to: "[email protected]",
from: "[email protected]",
subject: "Hello"
)
assert inbound_email.bounced?
end
test "routes to support mailbox" do
assert_equal SupportMailbox, ApplicationMailbox.route(
receive_inbound_email_from_mail(to: "[email protected]")
)
end
end
使用 .eml 测试夹具文件进行测试:
# Save real emails as test/fixtures/files/welcome_email.eml
test "processes a real email fixture" do
receive_inbound_email_from_source(
file_fixture("welcome_email.eml").read
)
end
⚠️ 常见错误: 使用手工编写的字符串进行测试,而不是使用真实的电子邮件夹具。真实邮件包含标头、MIME 分隔符和编码,简单字符串无法体现这些内容。请将实际邮件保存为 .eml 文件。
步骤 9:使用 Conductor 进行本地测试
在开发环境中,访问:
http://localhost:3000/rails/conductor/action_mailbox/inbound_emails
Conductor 可以:
- 手动创建新的入站邮件
- 查看所有入站邮件及其状态
- 重新投递邮件以进行处理
- 无需配置真实入口即可测试路由
也可通过 curl 使用:
curl -X POST http://localhost:3000/rails/conductor/action_mailbox/inbound_emails \
-F "inbound_email[raw_email_file]=@path/to/email.eml"
步骤 10:配置自动清除
处理后的邮件默认会在 30 天后自动清除(删除)。
# config/environments/production.rb
config.action_mailbox.incinerate_after = 14.days # Override default
重要提示: 在依赖原始邮件之前,先在 process 中提取所需的所有数据。自动清除后,原始邮件及其 Active Storage blob 都会被删除。
会触发自动清除的状态: delivered、failed、bounced
快速参考
InboundEmail 生命周期
pending → processing → delivered
→ failed (exception raised)
→ bounced (bounce_with called)
邮件地址匹配模式
# Exact local part
routing("[email protected]" => :support)
# Regex on local part
routing(/^reply\+(.+)@/i => :replies)
# Domain matching
routing(/@billing\./i => :billing)
# Catch-all (last resort)
routing(all: => :catch_all)
Mailbox 类模板
class ExampleMailbox < ApplicationMailbox
before_processing :validate_sender
def process
# 1. Find or create related records
user = User.find_by!(email_address: mail.from.first)
# 2. Extract and persist data
record = user.messages.create!(
subject: mail.subject,
body: mail.decoded,
received_at: mail.date
)
# 3. Handle attachments
mail.attachments.each do |attachment|
record.files.attach(
io: StringIO.new(attachment.decoded),
filename: attachment.filename,
content_type: attachment.content_type
)
end
# 4. Trigger side effects
NotificationJob.perform_later(record)
end
private
def validate_sender
unless User.exists?(email_address: mail.from.first)
bounce_with ExampleMailer.unknown_sender(inbound_email)
end
end
end
测试辅助方法
# From ActionMailbox::TestCase / ActionMailbox::TestHelper
# Create and process an inbound email from params
receive_inbound_email_from_mail(to:, from:, subject:, body:, **headers)
# Create and process from raw RFC 822 source
receive_inbound_email_from_source(source)
# Create without processing (for routing tests)
create_inbound_email_from_mail(to:, from:, subject:, body:)
create_inbound_email_from_source(source)
# Status checks on returned InboundEmail
inbound_email.delivered?
inbound_email.bounced?
inbound_email.failed?
inbound_email.pending?
inbound_email.processing?
应避免的反模式
- 庞大的 process 方法 — 提取到服务对象中;mailbox 应负责协调,而非实现
- 未处理退信 — 处理前始终验证发件人和内容
- 忽略多部分邮件 — 使用
mail.text_part或mail.html_part,不要只用mail.body - 在自动清除后仍依赖原始邮件 — 立即将数据提取到领域模型中
- 使用未经过滤的全捕获规则 —
routing(all:)也会捕获垃圾邮件;应严格过滤 - 只测试正常情况 — 测试退信、未知发件人、缺失字段和垃圾邮件
- 将入口密码硬编码 — 使用凭据或环境变量
- 在回调中进行处理 — 回调仅用于守卫逻辑;业务逻辑应放在
process中 - 未设置 Active Job — Action Mailbox 通过 Active Job 异步路由;请确保队列适配器可正常工作
- 忘记 Active Storage — Action Mailbox 通过 Active Storage 存储原始邮件;两者都必须完成迁移