Rails Action Mailbox: Inkomende e-mails verwerken in productie met Postmark, Mailgun en SendGrid
Een logistieke startup huurde mij vorig voorjaar in om een feature live te krijgen die hun CEO al negen maanden aan klanten beloofde: stuur de orderbevestiging van je leverancier door naar orders@hunbedrijf.com, en het systeem maakt een conceptinkooporder aan met de regelitems al ingevuld. De vorige ontwikkelaar had dit aan elkaar gehangen via een Zapier-naar-Make.com-naar-een-webhook constructie die ongeveer één op de twintig e-mails verloor, andere op maandagen soms dubbel verwerkte, en meer kostte dan hun hele Postgres-rekening. Ik heb het in vier dagen herschreven op Rails Action Mailbox. Sindsdien is er geen e-mail meer kwijtgeraakt.
Na negentien jaar Rails heb ik dit patroon vaker gezien dan ik kan tellen. Inkomende e-mail is de saaie ruggengraat van de helft van de B2B-software die je gebruikt. Supporttickets, bonnen, replies op notificaties, document-uploads, nieuwsbrieven die naar een feed worden geparsed — het loopt allemaal door iemands iets@jouwdomein.com. Het Rails Action Mailbox framework, geleverd in Rails 6 en verfijnd in Rails 8, is de productie-grade primitive voor dit werk. Deze post is het draaiboek dat ik bij klanten gebruik: hoe je het opzet, hoe je het koppelt aan Postmark, Mailgun of SendGrid, hoe je berichten routeert, bijlagen en replies parseert, en hoe je spam, misbruik en je eigen bugs buiten de deur houdt.
Wat Rails Action Mailbox eigenlijk doet
Rails Action Mailbox ontvangt inkomende e-mails op dezelfde manier waarop Action Mailer ze verstuurt. Je provider — Postmark, Mailgun, SendGrid, Mandrill of Postfix op je eigen server — accepteert het SMTP-verkeer, zet elk bericht om naar een webhook-payload en POST’t dat naar één Rails-endpoint. Action Mailbox parseert de payload, slaat de ruwe e-mail en een gestructureerd ActionMailbox::InboundEmail record op, en stuurt het bericht naar de juiste mailbox-klasse op basis van routeringregels die jij definieert.
Je schrijft klassen die er zo uitzien:
class SupportMailbox < ApplicationMailbox
def process
Ticket.create_from_email!(mail)
end
end
En je beschrijft hoe e-mail ze vindt:
class ApplicationMailbox < ActionMailbox::Base
routing /^support@/i => :support
routing /^orders@/i => :orders
routing :all => :catch_all
end
Al het andere — webhook-signing, retries, bounce-handling, opslag van de ruwe e-mail, verwijdering na een instelbare bewaartermijn — wordt afgehandeld door Active Storage en Active Job. Het is een van die Rails-features die klein lijkt totdat je het zelf probeert te bouwen, en dan begrijp je opeens waarom het framework zijn prijs waard is.
Rails Action Mailbox installeren
Action Mailbox zit standaard in Rails. Je activeert het met één commando:
bin/rails action_mailbox:install
bin/rails db:migrate
Dit maakt app/mailboxes/application_mailbox.rb aan, voegt twee tabellen toe — action_mailbox_inbound_emails en de active_storage_blobs van Active Storage als die nog niet bestonden — en mount de routing-engine. De ruwe RFC 822 e-mail wordt opgeslagen als Active Storage blob, wat betekent dat hij staat waar je andere uploads ook staan (S3, GCS, de lokale schijf in development).
Configureer de ingress in config/environments/production.rb:
config.action_mailbox.ingress = :postmark
Of :mailgun, :sendgrid, :mandrill, :postfix, :relay. Elke ingress heeft zijn eigen webhook-signing schema. Zet daarna het gedeelde wachtwoord in encrypted credentials — nooit in environment variables die in een CI-dashboard slingeren:
bin/rails credentials:edit
action_mailbox:
ingress_password: een-lange-random-string-uit-secure-random
postmark:
api_token: server-token-uit-postmark
Als je je secrets nog niet naar encrypted credentials hebt verhuisd, loopt de post over Rails credentials en secrets management je er stap voor stap doorheen. Doe dat eerst; het is twintig minuten werk dat zich oneindig terugbetaalt.
Postmark koppelen voor inkomende e-mail
Postmark is wat ik bij negentig procent van mijn klanten installeer. De prijsstelling is eerlijk, de deliverability is uitstekend en inbound parsing zit gebundeld in dezelfde server die je voor transactionele uitgaande e-mail gebruikt. Zo zet je het op:
- Ga in het Postmark dashboard naar je Server, Settings, en zoek de inbound stream.
- Zet de inbound webhook URL op
https://jouwapp.com/rails/action_mailbox/postmark/inbound_emails. - Zet de HTTP basic auth gebruikersnaam op
actionmailboxen het wachtwoord op hetingress_passworddat je hebt gegenereerd. - Zet het inbound forwarding domein op iets als
inbound.jouwdomein.com— dit is het adres dat klanten e-mailen — of gebruik het automatisch gegenereerde…@inbound.postmarkapp.comtijdens development.
Dat is alles. Een testbericht naar support@inbound.jouwdomein.com komt binnen een paar seconden binnen in je SupportMailbox#process. Als dat niet gebeurt is het antwoord bijna altijd één van drie dingen: verkeerd webhook-wachtwoord, verkeerde DNS voor het inbound-domein (Postmark verwacht een MX-record dat naar inbound.postmarkapp.com wijst), of je firewall blokkeert de IP’s van Postmark.
Mailgun koppelen voor inkomende e-mail
Mailgun is de juiste keuze als je al op Mailgun zit voor uitgaande e-mail of als je flexibele regex-gebaseerde receive routes nodig hebt. Configuratie:
# config/environments/production.rb
config.action_mailbox.ingress = :mailgun
# encrypted credentials
action_mailbox:
mailgun_signing_key: <jouw mailgun http webhook signing key>
Maak in Mailgun een route met match_recipient(".*@inbound.jouwdomein.com") en een actie van forward("https://jouwapp.com/rails/action_mailbox/mailgun/inbound_emails/mime"). Let op het /mime achteraan — Mailgun heeft twee webhook-formaten en alleen de MIME-variant is wat Action Mailbox verwacht.
Mailgun ondertekent elke webhook met HMAC-SHA256 en Action Mailbox verifieert de timestamp, token en signature voor je. Als je 401’s in je logs ziet, klopt de signing key niet; rouleer hem voorzichtig, want Mailgun staat geen twee actieve keys tegelijk toe.
SendGrid koppelen voor inkomende e-mail
SendGrid Inbound Parse werkt prima maar is van de drie het meest pielerig. Je configureert het onder Settings → Inbound Parse, wijst een MX-record naar mx.sendgrid.net, en forward naar https://actionmailbox:WACHTWOORD@jouwapp.com/rails/action_mailbox/sendgrid/inbound_emails.
Twee SendGrid-eigenaardigheden om te kennen. Ten eerste: bijlagen komen in een aparte set multipart-velden, en zeer grote berichten (>30MB) worden zonder waarschuwing afgekapt tenzij je een enterprise-plan hebt. Ten tweede: SendGrid heeft geen echte signing voor inbound webhooks — het leunt op het basic auth wachtwoord — dus gebruik een lang random wachtwoord en rouleer het als het ooit lekt.
Documenteer voor alle drie providers je runbook in je repo: welke provider, waar de webhook staat, waar het wachtwoord zit en hoe je de credential rouleert. De post over een legacy Rails codebase erven behandelt het patroon.
Inkomende e-mails routeren naar de juiste mailbox
De routing-DSL handelt het meeste af van wat je nodig hebt:
class ApplicationMailbox < ActionMailbox::Base
routing /^support@/i => :support
routing /^orders@/i => :orders
routing /^(invoice|bill)@/i => :billing
routing /\+([0-9]+)@/i => :ticket_reply # matcht user+12345@jouwdomein.com
routing :all => :catch_all
end
Routes worden in volgorde geëvalueerd, dus zet de specifieke patronen boven de catch-all. De capture groups in de regex worden niet automatisch beschikbaar gemaakt — als je het ticket-id nodig hebt, doe het dan binnen de mailbox:
class TicketReplyMailbox < ApplicationMailbox
def process
ticket_id = mail.to.first[/\+([0-9]+)@/, 1]
ticket = Ticket.find_by(id: ticket_id)
return bounce_with(TicketMailer.unknown_ticket(mail)) unless ticket
ticket.add_reply!(
from: mail.from.first,
body: parse_reply(mail),
attachments: mail.attachments
)
end
private
def parse_reply(mail)
body = mail.html_part&.decoded || mail.text_part&.decoded || mail.body.decoded
EmailReplyParser.parse_reply(body)
end
end
De email_reply_parser gem (dezelfde die GitHub gebruikt) strijdt signatures en quoted reply-tekst eruit. Combineer dat met Loofah om de HTML te saneren voor opslag en je hebt een robuuste reply-pipeline voor supporttickets, comments-by-email of welke gespreksfunctie dan ook.
Bijlagen parseren en veilig opslaan
De mail gem binnen Action Mailbox biedt bijlagen aan als Mail::Part objecten. Ze naar Active Storage pushen is rechttoe rechtaan:
class OrdersMailbox < ApplicationMailbox
ALLOWED_TYPES = %w[application/pdf image/jpeg image/png application/vnd.ms-excel].freeze
MAX_BYTES = 25.megabytes
def process
order = Order.create!(supplier_email: mail.from.first, subject: mail.subject)
mail.attachments.each do |attachment|
next unless ALLOWED_TYPES.include?(attachment.mime_type)
next if attachment.body.encoded.bytesize > MAX_BYTES
order.documents.attach(
io: StringIO.new(attachment.body.decoded),
filename: attachment.filename,
content_type: attachment.mime_type
)
end
OrderExtractionJob.perform_later(order)
end
end
Drie dingen die dit doet die toy-voorbeelden missen. Het dwingt een MIME-type allowlist af op basis van het geparsete content type, niet de bestandsnaam — gebruikers sturen .pdf bestanden die in werkelijkheid executables, ZIPs of Word-documenten zijn vaker dan je zou geloven. Het begrenst de bijlagegrootte op controllerniveau in plaats van te leunen op de limiet van je provider. En het duwt het zware werk — het document door Claude of je OCR-dienst halen — naar een background job in plaats van de webhook-response te blokkeren.
Voor de logistieke startup roept de volgende stap in OrderExtractionJob de Anthropic Vision API aan. Het volledige patroon staat in Rails Claude Vision API voor PDF-bonnen extractie. Inkomende e-mail combineren met vision-extractie is in 2026 een van de hoogst renderende patronen die ik uitrol — het vervangt een hele data-entry afdeling met tweehonderd regels Ruby.
Bouncen, doorsturen en catch-all
Niet elke inkomende e-mail moet verwerkt worden. Action Mailbox geeft je bounce_with, bounced! en delivered! callbacks om de levenscyclus te modelleren:
class SupportMailbox < ApplicationMailbox
before_processing :ensure_known_sender
def process
SupportTicket.create_from_email!(mail)
end
private
def ensure_known_sender
return if Customer.exists?(email: mail.from.first)
bounce_with SupportMailer.unknown_sender(mail)
end
end
bounce_with stuurt een nette reply naar de oorspronkelijke afzender — meestal een Mailer die zegt “we kennen dit adres niet, e-mail vanaf je geregistreerde adres” — en markeert de inbound e-mail als :bounced. Het oorspronkelijke bericht blijft opgeslagen, dus je kunt het later onderzoeken. Catch-all routes zijn voor alles wat aan geen enkele andere regel voldoet. Ik laat die meestal doorsturen naar een human-only review queue in plaats van stilletjes droppen:
class CatchAllMailbox < ApplicationMailbox
def process
InboundEmailReview.create!(
from: mail.from.first,
to: mail.to.first,
subject: mail.subject,
raw_email: inbound_email
)
AdminMailer.unrouted_email(self).deliver_later
end
end
Dit heeft me gered bij drie verschillende klantopdrachten waar iemand het adres op een afgedrukt contract veranderde van support@ naar help@ en we anders een jaar klantmail stilletjes zouden hebben laten vallen.
Verdedigen tegen spam, misbruik en loops
Een publiek inbound e-mail endpoint is een magneet voor problemen. Drie verdedigingen horen in elke Action Mailbox app:
-
SPF/DKIM-verificatie bij de provider. Postmark, Mailgun en SendGrid doen dit allemaal en geven het resultaat door. Weiger in
before_processingals het bericht beide niet haalt:before_processing :require_authenticated_sender def require_authenticated_sender return if mail.header["Authentication-Results"]&.value&.match?(/dkim=pass|spf=pass/) bounced! end -
Rate-limit per afzender. Een spammy afzender raakt je duizend keer per uur. Gebruik Rack::Attack op route-niveau of een Redis-counter in
before_processing. De post over Rails rate limiting met Rack::Attack behandelt de patronen. -
Detecteer en breek loops. Auto-reply mailers die op auto-reply mailers reageren is een klassieke oneindige lus. Sla verwerking over als het bericht
Auto-Submitted: auto-repliedofPrecedence: bulkheaders heeft, en reageer nooit met een Mailer die zelf weer inbound-routed kan worden in je app.
Voeg ook een feature flag toe. De post over Rails feature flags met Flipper laat zien hoe je een kill switch bouwt die je in vijf seconden kunt omzetten als een aanvaller je adres vindt.
Action Mailbox testen in de praktijk
De receive_inbound_email_from_mail test helper maakt mailbox-tests snel en deterministisch:
require "test_helper"
class OrdersMailboxTest < ActionMailbox::TestCase
test "creates an order with attachments" do
inbound = receive_inbound_email_from_mail(
to: "orders@inbound.example.com",
from: "supplier@vendor.com",
subject: "PO #4421",
body: "See attached.",
attachments: { "po.pdf" => file_fixture("po.pdf").read }
)
assert_equal :delivered, inbound.status.to_sym
assert_equal 1, Order.count
assert_equal 1, Order.last.documents.count
end
test "rejects oversized attachments" do
inbound = receive_inbound_email_from_mail(
to: "orders@inbound.example.com",
from: "supplier@vendor.com",
subject: "PO #4421",
body: "See attached.",
attachments: { "huge.pdf" => "x" * 30.megabytes }
)
assert_equal 0, Order.last.documents.count
end
end
Test ook de routing, met assert_inbound_email_routes_to. En draai de volledige pipeline minstens één keer per week in CI door een echt bericht naar een sandbox-domein te sturen — provider-configuratie verschuift, MX-records verrotten en DNS-problemen komen pas aan het licht als er echt SMTP-verkeer binnenkomt. Synthetic checks zijn goedkope verzekering.
Wanneer je Action Mailbox niet moet gebruiken
Eerlijk zijn. Action Mailbox is het juiste gereedschap als e-mail een feature is, geen transportmechanisme. Gevallen waarin het verkeerd is:
- Bulk mailinglist-ingest. Miljoenen opt-in subscriptions per dag horen in een dedicated pipeline, niet in Active Job. Postmark, Mailgun en SendGrid rate-limiten je hoe dan ook.
- Real-time chat. De latency van SMTP naar webhook is twee tot dertig seconden. Gebruik een echt chatprotocol.
- Compliance-zware archivering. Als toezichthouders WORM-stijl retentie met volledige chain-of-custody eisen, wil je een dedicated e-mailarchiefproduct, niet Active Storage met Action Mailbox’ dertig dagen retentie.
Voor de logistieke startup paste het perfect, en we leverden in vier dagen op. Ze verwerken nu drieduizend inkomende e-mails per dag voor orders, support en replies-op-notificaties. De Action Mailbox tabel wordt na dertig dagen geschoond, de gestructureerde records blijven voor altijd, en de CEO is gestopt met excuses maken aan klanten over het “systeem dat soms e-mails kwijtraakt.” Dat is de winst.
FAQ
Hoe gaat Rails Action Mailbox om met bijlagen die groter zijn dan de body-limiet?
Action Mailbox slaat de ruwe e-mail op als Active Storage blob, dus de groottelimiet is de kleinste van twee: de per-object cap van je storage backend (5GB op S3, veel groter dan welke e-mail dan ook) en de limiet die je provider oplegt voor inkomende berichten. Postmark zit op 35MB, Mailgun op 25MB, SendGrid op 30MB op de meeste plannen. Forceer je eigen bijlage-groottelimiet in de mailbox vóór je naar Active Storage persisteert om opslagkosten voorspelbaar te houden.
Kan ik Rails Action Mailbox gebruiken zonder een derde-partij-provider?
Ja — de :postfix en :relay ingresses laten je rechtstreeks ontvangen vanaf je eigen SMTP-server. Ik raad het niet aan. Een inbound MX runnen met correcte SPF, DKIM, DMARC, IP-reputatie en abuse-handling is een fulltime baan. Betaal Postmark of Mailgun de paar euro per maand en steek je engineering-uren in je product.
Hoe parseer ik alleen de nieuwe content uit een reply-e-mail in Rails?
Gebruik de email_reply_parser gem. Het strijkt quoted text, signatures en Op di. ... schreef: headers uit een body, en geeft alleen de nieuwe content terug die de gebruiker heeft getypt. Combineer met Loofah om HTML te saneren en je hebt GitHub-niveau reply-parsing in vijf regels. Voor lastiger gevallen (Outlook’s quoting-stijl is uniek vreselijk) is de talon Python-service via een kleine HTTP-wrapper het overwegen waard.
Hoe lang bewaart Action Mailbox inkomende e-mails?
Standaard worden verwerkte e-mails na dertig dagen geïncinereerd, instelbaar via config.action_mailbox.incinerate_after. Gebouncede of mislukte e-mails worden onbeperkt bewaard zodat je ze kunt onderzoeken. Voor AVG- of CCPA-compliance zet je de retentie gelijk aan je privacybeleid en documenteer je de delete-job in je verwerkingsregister. Verleng de retentie nooit zonder reden — elke bewaarde e-mail is een aansprakelijkheid.
Hulp nodig met inkomende e-mail, AI-extractie of welke productie-Rails-feature dan ook in je product? TTB Software bouwt deze elke maand voor klanten in heel Europa. We doen Rails al negentien jaar en behandelen e-mail als de productie-kritische infrastructuur die het is.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation