action_mailbox.cr
action_mailbox.cr
A 1:1 Crystal port of actionmailbox 8.1.4 (the Rails inbound email component) — receive raw RFC822 messages, store them as InboundEmail records with a raw_email Active Storage blob, route them to mailbox classes via address rules, and track the processing lifecycle (pending → processing → delivered / failed / bounced). Mailbox classes are a compile-time registry (macro inherited + the routing DSL), and incoming mail is parsed by a Mail::Message.from_source reopen in the port itself. Suite: 29/29 oracle cases captured from the real gem (SQLite in-memory, Disk service, ActiveJob :test adapter) and replayed without edits.
⚠️ EXPERIMENTAL FRAMEWORK, DO NOT USE IN PRODUCTION YET.
Installation
dependencies:
action_mailbox_cr:
github: OrelSokolov/action_mailbox.cr
shards install
Depends on active_record.cr, active_storage.cr and action_mailer.cr.
Usage
require "action_mailbox_cr"
class ApplicationMailbox < ActionMailbox::Base
routing({/^replies@/i => :replies,
"help@example.com" => :help,
:all => :backstop})
end
class RepliesMailbox < ApplicationMailbox
before_processing :check_sender # halts the chain by bouncing
def process
inbound_email.mail.subject
inbound_email.delivered!
end
def check_sender
inbound_email.bounced! if mail.from.nil?
end
end
email = ActionMailbox::InboundEmail.create_and_extract_message_id!(rfc822_source)
ActionMailbox.enqueued_jobs # => ["ActionMailbox::RoutingJob"]
email.route # dispatch via ::ApplicationMailbox
email.reload.status # => "delivered"
Feature comparison vs actionmailbox 8.1.4
InboundEmail model & storage
| Feature | Original (actionmailbox) | Port (action_mailbox.cr) | Status |
|---|---|---|---|
create_and_extract_message_id! (checksum, Message-ID extraction, missing-ID generation <checksum>@<hostname>.mail) |
✅ | Same (SHA1 checksum) | ✅ |
has_one_attached :raw_email |
✅ | Active Storage blob + attachment (message.eml, message/rfc822) |
✅ |
Status enum pending/processing/delivered/failed/bounced |
✅ | Same, stored as the Rails integer enum (0..4) | ✅ |
#mail, #source, #processed?, status predicates/bangers |
✅ | Same | ✅ |
InboundEmail < ActiveRecord::Base |
✅ | Hand-rolled row class over Base.connection |
⚠️ partial |
Incineration (IncinerationJob, incinerate_after 30 days) |
✅ | Not implemented (P1) | ❌ |
Routing & mailbox lifecycle
| Feature | Original | Port (action_mailbox.cr) | Status |
|---|---|---|---|
routing address => :mailbox — String (case-insensitive) / Regex / Proc / :all over mail.recipients |
✅ | Same | ✅ |
mailbox_for (first matching route) |
✅ | Same | ✅ |
perform_processing: processing! → before_processing → process → delivered!; exception → failed! |
✅ | Same | ✅ |
before_processing halting via bounced! (finished_processing?) |
✅ | Same | ✅ |
Unrouted email → bounced! + RoutingError |
✅ | Same | ✅ |
after_processing / around_processing / rescue_from |
✅ | Not implemented (P1) | ❌ |
bounce_with / bounce_now_with |
✅ | Not implemented (P1) | ❌ |
Ingress & configuration
| Feature | Original | Port (action_mailbox.cr) | Status |
|---|---|---|---|
RoutingJob (ActiveJob) |
✅ | In-memory ActionMailbox.enqueued_jobs register, #route dispatch |
⚠️ partial |
HTTP ingress: relay (basic auth + ingress password), postmark, sendgrid, mailgun, mandrill |
✅ | Not implemented (P1) | ❌ |
config.action_mailbox.* (incinerate, queues, ingress, logger) |
✅ | Not implemented (P1) | ❌ |
Deviations & Crystal-specific notes
- Mailbox name → class resolution is a compile-time registry (
macro inheritedcollects subclasses; theroutingmacro registers:name → NameMailboxand adds routes) — Ruby's runtimeconstantizeis impossible in Crystal. RoutingJobis a register of enqueued job names instead of an ActiveJob integration; the oracle itself runs the gem under the ActiveJob:testadapter withrouteinvoked explicitly.InboundEmailis a hand-rolled row class overBase.connection(same layout asRichTextin action_text.cr) so status helpers stay overridable; the table matches the gem's migration (integer status,message_id,message_checksum).- The reading side of
Mail::Message(from_source: unfolded headers with downcased keys, to/cc/bcc address lists,recipients = to + cc + bcc,message_idwithout angle brackets) is a reopen of theMail::Messagebuilt by action_mailer.cr. - P1 (not yet ported): ingress controllers (
relay/postmark/sendgrid/mailgun/mandrill),bounce_with/bounce_now_with,IncinerationJob/incinerate_after,after/around_processing.
Full list with rationale: PLAN.md (Russian).
Testing
crystal spec # needs a local PostgreSQL (AMB_TEST_URL, default
# postgres://root@/crails_as_test?host=/var/run/postgresql)
Oracle: 29 cases recorded with the real actionmailbox 8.1.4 (oracle/cases.rb → oracle/expected.yml; SQLite in-memory, Disk service, ActiveJob :test adapter); spec/oracle_spec.cr replays them 1:1 without edits — 29/29.
action_mailbox.cr
- 0
- 0
- 0
- 0
- 0
- about 5 hours ago
- October 12, 2026
Mon, 12 Oct 2026 06:14:31 GMT