mcpField
A
action-mailbox

action-mailbox

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

关于 action-mailbox

为 Rails 开发者提供 Action Mailbox 实践指导,涵盖入站邮件路由、处理、拒收和测试。重点强调及时提取业务数据、明确退信及使用真实邮件样本。

主要功能

  1. 01按收件地址模式路由邮件
  2. 02编写专注的邮箱处理类
  3. 03通过回调校验并退信
  4. 04处理附件及多部分邮件
  5. 05使用 .eml 样本测试

适用场景

  1. 01为 Rails 应用配置入站邮件
  2. 02将支持邮件转为业务记录
  3. 03拒绝无效发件人并发送说明
  4. 04排查邮件路由和处理问题

安装

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

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

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

附带文件reference.md

详细介绍

Rails Action Mailbox 专家

将收到的电子邮件路由到类似控制器的邮箱类,以便在 Rails 应用中进行处理。

理念

核心原则:

  1. 邮箱是处理电子邮件的控制器 — 根据地址模式进行路由,并在专用类中处理
  2. 尽早提取数据,稍后销毁原始邮件 — 立即将电子邮件中的领域数据持久化;原始邮件仅作临时保存
  3. 明确退信 — 拒绝不合规的电子邮件时发送内容清晰的退信,不要悄悄丢弃
  4. 使用真实的电子邮件样例进行测试 — 使用 .eml 文件,而非手动构造 Mail 对象
  5. 每个关注点对应一个邮箱 — 保持邮箱职责专一;拆分路由,而不是在 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 — 唯一消息 ID
  • mail.in_reply_to — 此邮件回复的消息 ID
  • mail.attachments — 附件数组
  • mail.multipart? — 电子邮件是否包含多个部分
  • mail.text_part / mail.html_part — 指定的 MIME 部分

inbound_email 访问器:

  • inbound_email — ActionMailbox::InboundEmail Active Record 对象
  • inbound_email.message_id — RFC 2822 Message-ID
  • inbound_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 消息,并会:

  1. 发送退信
  2. 将 inbound_email.status 设为 bounced
  3. 中止后续处理
# 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?

应避免的反模式

  1. 庞大的 process 方法 — 提取到服务对象中;mailbox 应负责协调,而非实现
  2. 未处理退信 — 处理前始终验证发件人和内容
  3. 忽略多部分邮件 — 使用 mail.text_part 或 mail.html_part,不要只用 mail.body
  4. 在自动清除后仍依赖原始邮件 — 立即将数据提取到领域模型中
  5. 使用未经过滤的全捕获规则 — routing(all:) 也会捕获垃圾邮件;应严格过滤
  6. 只测试正常情况 — 测试退信、未知发件人、缺失字段和垃圾邮件
  7. 将入口密码硬编码 — 使用凭据或环境变量
  8. 在回调中进行处理 — 回调仅用于守卫逻辑;业务逻辑应放在 process 中
  9. 未设置 Active Job — Action Mailbox 通过 Active Job 异步路由;请确保队列适配器可正常工作
  10. 忘记 Active Storage — Action Mailbox 通过 Active Storage 存储原始邮件;两者都必须完成迁移

评论

支持 Markdown