Agent Skills: Rails Action Mailbox Expert

Expert guidance for receiving and processing inbound emails with Action Mailbox in Rails. Use when setting up inbound email processing, routing emails to mailboxes, configuring ingress providers (Mailgun, SendGrid, Postfix, etc.), handling attachments, bouncing emails, or testing with fixtures. Covers "action mailbox", "inbound email", "receive email", "process email", "email routing", "mailbox", "incoming email", "email processing", "ingress", "InboundEmail".

UncategorizedID: thinkoodle/rails-skills/action-mailbox

Install this agent skill to your local

pnpm dlx add-skill https://github.com/ThinkOodle/rails-skills/tree/HEAD/skills/action-mailbox

Skill Files

Browse the full folder contents for action-mailbox.

Download Skill

Loading file tree…

skills/action-mailbox/SKILL.md

Skill Metadata

Name
action-mailbox
Description
Expert guidance for receiving and processing inbound emails with Action Mailbox in Rails. Use when setting up inbound email processing, routing emails to mailboxes, configuring ingress providers (Mailgun, SendGrid, Postfix, etc.), handling attachments, bouncing emails, or testing with fixtures. Covers "action mailbox", "inbound email", "receive email", "process email", "email routing", "mailbox", "incoming email", "email processing", "ingress", "InboundEmail".

Rails Action Mailbox Expert

Route incoming emails to controller-like mailbox classes for processing in Rails applications.

Philosophy

Core Principles:

  1. Mailboxes are controllers for email — route by address pattern, process in dedicated classes
  2. Extract data early, incinerate later — persist domain data from emails immediately; raw emails are temporary
  3. Bounce explicitly — reject bad emails with informative bounce messages, don't silently swallow them
  4. Test with real email fixtures — use .eml files, not hand-built Mail objects
  5. One mailbox per concern — keep mailboxes focused; split routing rather than branching in process

When To Use This Skill

  • Setting up Action Mailbox in a Rails app
  • Configuring an ingress provider (Mailgun, SendGrid, Postmark, Postfix, etc.)
  • Writing mailbox classes to process inbound emails
  • Routing emails to the correct mailbox
  • Handling email attachments via Active Storage
  • Bouncing or rejecting invalid emails
  • Testing inbound email processing
  • Debugging email delivery/processing issues

Instructions

Step 1: Install Action Mailbox

bin/rails action_mailbox:install
bin/rails db:migrate

This creates:

  • app/mailboxes/application_mailbox.rb — the routing hub
  • Migration for action_mailbox_inbound_emails table
  • Active Storage tables (if not already present)

Verify setup:

# 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

Step 2: Configure Routing

ApplicationMailbox is the router. It matches email recipients against patterns and dispatches to mailbox classes.

# 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

Routing rules:

  • Patterns match against to, cc, and bcc fields
  • First match wins — order matters
  • Use all: as a catch-all only if every email must be processed
  • Unmatched emails stay pending and get incinerated

⚠️ Common mistake: Forgetting that routing matches ALL recipient fields, not just to. An email CC'd to a matching address will also route.

Step 3: Generate Mailbox Classes

bin/rails generate mailbox support

Creates app/mailboxes/support_mailbox.rb:

class SupportMailbox < ApplicationMailbox
  def process
    # Your processing logic here
  end
end

Step 4: Implement the Process Method

The process method is where you extract data from the email and persist it to your domain models.

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

Key accessors on mail (a Mail::Message):

  • mail.to — array of To addresses
  • mail.from — array of From addresses
  • mail.cc — array of CC addresses
  • mail.subject — subject line
  • mail.body.decoded / mail.decoded — decoded body text
  • mail.date — send date
  • mail.message_id — unique message ID
  • mail.in_reply_to — message ID this replies to
  • mail.attachments — array of attachments
  • mail.multipart? — whether email has multiple parts
  • mail.text_part / mail.html_part — specific MIME parts

The inbound_email accessor:

  • inbound_email — the ActionMailbox::InboundEmail Active Record object
  • inbound_email.message_id — the RFC 2822 Message-ID
  • inbound_email.source — raw email source (RFC 822)

Step 5: Use Callbacks for Guards and Side Effects

Action Mailbox provides before_processing, after_processing, and around_processing callbacks.

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

⚠️ Common mistake: Not understanding that bounce_with halts processing. It sets the status to bounced, sends the bounce email, and stops — the process method never runs.

Step 6: Bounce Correctly

bounce_with takes an Action Mailer message and:

  1. Sends the bounce email
  2. Sets inbound_email.status to bounced
  3. Halts further processing
# 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

Rules for bouncing:

  • Always bounce in before_processing callbacks, not inside process
  • Always provide a meaningful bounce email — silent drops confuse senders
  • Don't bounce spam — just mark as delivered or let it incenerate silently

Step 7: Configure Ingress Provider

Set the ingress in config/environments/production.rb:

config.action_mailbox.ingress = :mailgun  # or :sendgrid, :postmark, :mandrill, :relay

Credentials go in encrypted credentials:

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

Or use environment variables:

  • RAILS_INBOUND_EMAIL_PASSWORD — relay ingress password
  • MAILGUN_INGRESS_SIGNING_KEY — Mailgun signing key
  • MANDRILL_INGRESS_API_KEY — Mandrill API key

See reference.md for provider-specific endpoint URLs and configuration details.

Step 8: Test Mailboxes

Use ActionMailbox::TestCase and its helpers:

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: "support@example.com",
        from: "customer@example.com",
        subject: "Help needed",
        body: "I can't log in"
      )
    end

    ticket = SupportTicket.last
    assert_equal "Help needed", ticket.subject
    assert_equal "customer@example.com", ticket.from_email
  end

  test "bounces email from unknown sender" do
    inbound_email = receive_inbound_email_from_mail(
      to: "support@example.com",
      from: "stranger@unknown.com",
      subject: "Hello"
    )

    assert inbound_email.bounced?
  end

  test "routes to support mailbox" do
    assert_equal SupportMailbox, ApplicationMailbox.route(
      receive_inbound_email_from_mail(to: "support@example.com")
    )
  end
end

Testing with .eml fixture files:

# 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

⚠️ Common mistake: Testing with hand-crafted strings instead of realistic email fixtures. Real emails have headers, MIME boundaries, and encoding that simple strings miss. Save actual emails as .eml files.

Step 9: Use the Conductor for Local Testing

In development, visit:

http://localhost:3000/rails/conductor/action_mailbox/inbound_emails

The conductor lets you:

  • Create new inbound emails manually
  • View all inbound emails and their status
  • Redeliver emails for reprocessing
  • Test routing without configuring a real ingress

Also available via curl:

curl -X POST http://localhost:3000/rails/conductor/action_mailbox/inbound_emails \
  -F "inbound_email[raw_email_file]=@path/to/email.eml"

Step 10: Configure Incineration

Processed emails are incinerated (deleted) after 30 days by default.

# config/environments/production.rb
config.action_mailbox.incinerate_after = 14.days  # Override default

Important: Extract all data you need in process before relying on the raw email. After incineration, the original email and its Active Storage blob are gone.

Statuses that trigger incineration: delivered, failed, bounced

Quick Reference

InboundEmail Lifecycle

  pending → processing → delivered
                       → failed (exception raised)
                       → bounced (bounce_with called)

Email Address Matching Patterns

# Exact local part
routing("support@example.com" => :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 Template

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

Test Helpers

# 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?

Anti-Patterns to Avoid

  1. Giant process methods — Extract to service objects; mailbox should orchestrate, not implement
  2. No bounce handling — Always validate sender/content before processing
  3. Ignoring multipart emails — Use mail.text_part or mail.html_part, not just mail.body
  4. Relying on raw email after incineration — Extract data into domain models immediately
  5. Catch-all without filteringrouting(all:) catches spam too; filter aggressively
  6. Testing only happy path — Test bounces, unknown senders, missing fields, spam
  7. Hardcoding ingress passwords — Use credentials or environment variables
  8. Processing in callbacks — Use callbacks for guards only; business logic goes in process
  9. Not setting up Active Job — Action Mailbox routes asynchronously via Active Job; ensure your queue adapter works
  10. Forgetting Active Storage — Action Mailbox stores raw emails via Active Storage; both must be migrated