Processing incoming emails opens powerful workflows: support tickets from customer replies, document uploads via email attachments, or automated data imports. Rails 8 Action Mailbox provides a complete framework for receiving, routing, and processing inbound emails with the same elegance Rails brings to everything else.
Setting Up Action Mailbox
Action Mailbox requires a few migrations and configuration steps. Start by installing the framework:
# Terminal
bin/rails action_mailbox:install
bin/rails db:migrateThis creates two tables: action_mailbox_inbound_emails for storing raw emails and tracking processing status, and the Active Storage tables for attachments. Configure the ingress (how emails arrive) in the environment file:
# config/environments/production.rb
config.action_mailbox.ingress = :mailgunAvailable ingress options include :mailgun, :sendgrid, :postmark, :mandrill, and :relay for direct SMTP. For development, use the conductor interface at /rails/conductor/action_mailbox/inbound_emails to test with manually crafted emails.
Routing Incoming Emails
Action Mailbox routes emails to specific mailboxes based on rules defined in the ApplicationMailbox. The routing DSL matches against recipient addresses:
# app/mailboxes/application_mailbox.rb
class ApplicationMailbox < ActionMailbox::Base
# Route support emails to the support mailbox
routing /^support@/i => :support
# Route replies with tokens to the reply mailbox
routing /^reply\+(.+)@/i => :reply
# Route document submissions
routing "[email protected]" => :documents
# Route everything else to a catch-all
routing :all => :default
endRoutes are evaluated in order, so place specific patterns before catch-alls. The routing accepts strings for exact matches, regular expressions for patterns, and procs for complex logic:
# app/mailboxes/application_mailbox.rb
class ApplicationMailbox < ActionMailbox::Base
routing ->(inbound_email) {
inbound_email.mail.to.any? { |addr| addr.include?("urgent") }
} => :urgent
routing :all => :default
endBuilding a Support Ticket Mailbox
Create mailboxes to handle specific email types. A support ticket mailbox might create tickets from customer emails and attach any included files:
# app/mailboxes/support_mailbox.rb
class SupportMailbox < ApplicationMailbox
before_processing :ensure_valid_sender
def process
ticket = create_or_update_ticket
attach_files(ticket)
notify_support_team(ticket)
end
private
def create_or_update_ticket
existing_ticket = find_existing_ticket
if existing_ticket
existing_ticket.replies.create!(
body: email_body,
sender_email: sender_email
)
existing_ticket
else
SupportTicket.create!(
subject: mail.subject || "No Subject",
body: email_body,
sender_email: sender_email,
sender_name: sender_name,
status: :open
)
end
end
def find_existing_ticket
# Check for In-Reply-To header to find existing conversation
if mail.in_reply_to.present?
message_id = mail.in_reply_to.gsub(/[<>]/, "")
SupportTicket.find_by(message_id: message_id)
end
end
def attach_files(ticket)
mail.attachments.each do |attachment|
ticket.files.attach(
io: StringIO.new(attachment.decoded),
filename: attachment.filename,
content_type: attachment.content_type
)
end
end
def notify_support_team(ticket)
SupportMailer.new_ticket_notification(ticket).deliver_later
end
def email_body
# Prefer plain text, fall back to stripped HTML
if mail.multipart?
mail.text_part&.decoded || strip_html(mail.html_part&.decoded) || ""
else
mail.decoded
end
end
def strip_html(html)
return nil unless html
ActionController::Base.helpers.strip_tags(html).squish
end
def sender_email
mail.from&.first
end
def sender_name
mail[:from]&.display_names&.first || sender_email
end
def ensure_valid_sender
bounced! unless sender_email.present?
end
endThe mail object is a Mail::Message instance providing access to all email headers, body parts, and attachments. The lifecycle callbacks (before_processing, after_processing) and status methods (bounced!, delivered!) control how emails move through the system.
Processing Reply Tokens
A common pattern uses tokenized reply addresses to associate responses with specific records. When sending outbound emails, include a reply token in the from address:
# app/mailers/comment_mailer.rb
class CommentMailer < ApplicationMailer
def notification(comment)
@comment = comment
@post = comment.post
mail(
to: @post.author.email,
subject: "New comment on #{@post.title}",
reply_to: reply_address(@post)
)
end
private
def reply_address(post)
token = post.signed_id(purpose: :email_reply, expires_in: 30.days)
"reply+#{token}@example.com"
end
endThe reply mailbox extracts and validates the token:
# app/mailboxes/reply_mailbox.rb
class ReplyMailbox < ApplicationMailbox
before_processing :set_post
before_processing :set_user
def process
@post.comments.create!(
user: @user,
body: email_body
)
end
private
def set_post
token = recipient_token
@post = Post.find_signed(token, purpose: :email_reply)
bounced! unless @post
rescue ActiveSupport::MessageVerifier::InvalidSignature
bounced!
end
def set_user
@user = User.find_by(email: mail.from&.first)
bounced! unless @user
end
def recipient_token
# Extract token from [email protected]
mail.to.find { |addr| addr.start_with?("reply+") }
&.then { |addr| addr[/reply\+(.+)@/, 1] }
end
def email_body
# Strip quoted reply text
body = mail.text_part&.decoded || mail.decoded
EmailReplyTrimmer.trim(body)
end
endUsing signed_id with a purpose and expiration ensures tokens cannot be forged or reused for unintended purposes.
Testing Action Mailbox
Action Mailbox includes testing helpers for RSpec and Minitest. Test mailboxes by creating inbound emails and asserting on outcomes:
# spec/mailboxes/support_mailbox_spec.rb
require "rails_helper"
RSpec.describe SupportMailbox, type: :mailbox do
include ActionMailbox::TestHelper
describe "#process" do
it "creates a support ticket from incoming email" do
expect {
receive_inbound_email_from_mail(
from: "[email protected]",
to: "[email protected]",
subject: "Help needed",
body: "Cannot login to my account"
)
}.to change(SupportTicket, :count).by(1)
ticket = SupportTicket.last
expect(ticket.sender_email).to eq("[email protected]")
expect(ticket.subject).to eq("Help needed")
expect(ticket.body).to include("Cannot login")
end
it "attaches files from the email" do
receive_inbound_email_from_mail(
from: "[email protected]",
to: "[email protected]",
subject: "Screenshot attached",
body: "See attached"
) do |mail|
mail.add_file(filename: "screenshot.png", content: file_fixture("test.png").read)
end
ticket = SupportTicket.last
expect(ticket.files).to be_attached
expect(ticket.files.first.filename.to_s).to eq("screenshot.png")
end
it "bounces emails without a sender" do
inbound_email = receive_inbound_email_from_mail(
from: "",
to: "[email protected]",
subject: "Spam"
)
expect(inbound_email).to have_been_bounced
end
end
endThe receive_inbound_email_from_mail helper creates and processes an inbound email in one step. For more control, use create_inbound_email_from_mail to create without processing.
Handling Edge Cases
Production email processing encounters malformed messages, spam, and unexpected encodings. Build defensive mailboxes:
# app/mailboxes/application_mailbox.rb
class ApplicationMailbox < ActionMailbox::Base
rescue_from ActiveRecord::RecordInvalid, with: :handle_invalid_record
rescue_from StandardError, with: :handle_unexpected_error
private
def handle_invalid_record(exception)
Rails.logger.error("Mailbox validation failed: #{exception.message}")
bounced!
end
def handle_unexpected_error(exception)
Rails.logger.error("Mailbox processing failed: #{exception.message}")
Rails.error.report(exception, handled: true)
bounced!
end
endConsider rate limiting by sender, scanning attachments for malware before processing, and setting maximum attachment sizes in Active Storage configuration.
Summary
Action Mailbox transforms Rails applications into email-processing powerhouses. The routing DSL directs emails to specialized mailboxes, callbacks control the processing lifecycle, and signed tokens enable secure reply handling. Combined with Active Storage for attachments and Solid Queue for async processing, email becomes just another input channel for Rails applications.
For applications already using Action Mailer for outbound email, Action Mailbox completes the circleβenabling true two-way email communication with minimal configuration and maximum Rails conventions.