35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails Action Mailbox: Inkomende e-mails verwerken in productie met Postmark, Mailgun en SendGrid

Rails Action Mailbox: Inkomende e-mails verwerken in productie met Postmark, Mailgun en SendGrid

Roger Heykoop
Ruby on Rails, DevOps
Rails Action Mailbox gids: verwerk inkomende e-mails in productie met Postmark, Mailgun, SendGrid; route per domein, bijlagen, replies parsen, spam tegengaan.

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:

  1. Ga in het Postmark dashboard naar je Server, Settings, en zoek de inbound stream.
  2. Zet de inbound webhook URL op https://jouwapp.com/rails/action_mailbox/postmark/inbound_emails.
  3. Zet de HTTP basic auth gebruikersnaam op actionmailbox en het wachtwoord op het ingress_password dat je hebt gegenereerd.
  4. 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.com tijdens 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:

  1. SPF/DKIM-verificatie bij de provider. Postmark, Mailgun en SendGrid doen dit allemaal en geven het resultaat door. Weiger in before_processing als 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
    
  2. 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.

  3. 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-replied of Precedence: bulk headers 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.

#rails-action-mailbox #rails-inbound-email #action-mailbox-postmark #action-mailbox-mailgun #action-mailbox-sendgrid #ruby-on-rails
R

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 Touch

Share this article

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