Inkomende e-mail · 22 september 2026

E-mail ontvangen met Amazon SES: S3 en Lambda

Bouw een productierijpe pipeline voor inkomende e-mail met Amazon SES, S3 en Lambda, inclusief MIME-veiligheid, tenantroutering, idempotentie, retries en threading.

Het kortste betrouwbare pad voor inkomende e-mail via Amazon SES is: verifieer een domein, laat een MX-record naar een SES-ontvangstendpoint wijzen, sla elk geaccepteerd bericht op in S3 en roep daarna Lambda asynchroon aan om het te parsen en op te slaan. Zet de S3-actie vóór de Lambda-actie. Behandel het SES-bericht-ID als idempotentiesleutel, gebruik de ontvanger uit de SMTP-envelope voor routering en zet onveilige content in quarantaine in plaats van headers of bijlagen te vertrouwen.

De architectuur die je bouwt

Gebruik een apart subdomein zoals inbound.example.com, tenzij SES alle e-mail voor je hoofddomein moet ontvangen. Zo blijft applicatiemail gescheiden van de inboxen van medewerkers en is terugdraaien een DNS-wijziging in plaats van een mailmigratie.

De productieflow is:

sender -> SES inbound SMTP endpoint -> active SES receipt rule -> S3 raw-message object -> asynchronous Lambda action -> MIME parser and policy checks -> application database and private attachment storage

Receipt rules van Amazon SES voeren hun acties in volgorde uit. AWS documenteert expliciet het patroon met eerst S3 en dan Lambda voor gevallen waarin code de body van het bericht nodig heeft. Een directe Lambda-actie ontvangt metadata en een selectie van headers, niet de volledige body. De body blijft als ruw MIME-object in S3 staan (AWS SES: concepten voor ontvangen).

Die scheiding is nuttig. De stap die met SMTP praat, slaat het originele bericht snel op, terwijl parsen, indexeren, notificaties en bedrijfslogica na de acceptatie plaatsvinden. Een tijdelijke databasestoring hoort er niet toe te leiden dat de mailserver van een afzender de SMTP-transactie moet herhalen.

1. Kies een ondersteunde regio en verifieer het domein

E-mail ontvangen met SES is alleen beschikbaar in bepaalde AWS-regio's. Kies er een uit de actuele lijst met SES-ontvangstendpoints en houd je SES-, Lambda-, SNS- en KMS-resources in die regio, tenzij de betreffende AWS-documentatie uitdrukkelijk iets anders toestaat.

Maak een SES-domeinidentiteit aan voor precies het hoofddomein of subdomein dat e-mail ontvangt. Voor domeinverificatie publiceer je de DNS-records die SES aanlevert. Verificatie voor ontvangen bewijst dat je het domein beheert; dat staat los van het instellen van de MX-route die inkomend verkeer naar SES stuurt (AWS-gids voor domeinverificatie).

Voor een apart subdomein zien de DNS-records er conceptueel zo uit:

inbound.example.com. MX 10 inbound-smtp.us-east-1.amazonaws.com.

Vervang us-east-1 door de regio die je hebt gekozen. AWS documenteert de MX-waarde als 10 inbound-smtp.<region>.amazonaws.com (AWS-gids voor MX-records). Laat je hoofddomein niet naar SES wijzen als mensen daar nog e-mail ontvangen via Google Workspace, Microsoft 365 of een andere mailboxprovider.

Controleer het gepubliceerde record via meer dan één resolver voordat je gaat testen:

dig MX inbound.example.com +short

Zichtbaarheid in DNS bewijst alleen dat de route is gepubliceerd. Stuur een gecontroleerd bericht naar een testadres en bevestig dat SES het heeft opgeslagen voordat je de configuratie als voltooid beschouwt.

2. Sla het ruwe bericht op voordat je het verwerkt

Maak een privé-S3-bucket aan met geblokkeerde publieke toegang, een lifecyclebeleid en zo beperkt mogelijke IAM-rechten. Maak daarna een SES-receipt rule aan waarvan de ontvangervoorwaarde overeenkomt met je inkomende domein of met specifieke adressen.

De eerste actie hoort het ruwe bericht in S3 af te leveren. Een objectprefix zoals inbound/ maakt het eenvoudiger om bewaarregels en toegangsbeleid af te bakenen. SES slaat ruwe, onbewerkte MIME-content op. AWS documenteert momenteel een standaardmaximum van 40 MB wanneer berichten in S3 worden opgeslagen, terwijl de SNS-actie die het volledige bericht bevat een veel kleiner maximum van 150 KB heeft (AWS S3-receipt action). Door dat verschil in grootte is S3 de veiligere standaardkeuze voor echte antwoorden en bijlagen.

Schakel je de optionele KMS-instelling van SES op de receipt action in, lees dan de details over versleuteling goed door. SES gebruikt voor die functie client-side versleuteling, geen gewone server-side versleuteling van S3, dus je lezer moet het object met een compatibele client ontsleutelen. Zet het niet zomaar aan om dan tijdens een incident te ontdekken dat je Node-parser de opgeslagen bytes niet kan lezen.

Geef SES alleen toestemming om naar de bedoelde bucket en prefix te schrijven. Geef Lambda s3:GetObject alleen voor diezelfde locatie. De functie heeft geen rechten nodig om de bucket te beheren.

3. Voeg een asynchrone Lambda-actie toe

Zet de Lambda-actie na de S3-actie in de receipt rule en gebruik asynchrone aanroep, tenzij de functie moet beslissen of SES de rule verder moet evalueren. AWS raadt asynchrone uitvoering aan voor normale verwerking en reserveert synchrone uitvoering voor beslissingen over de mailflow (AWS Lambda-receipt action).

De door SES toegekende mail.messageId is ook de sleutel van het S3-object als er geen prefix is ingesteld. Met een prefix zet je die ervoor. Het volgende Node.js-skelet haalt het ruwe bericht op en parset het. Bundel @aws-sdk/client-s3 en een onderhouden MIME-parser zoals mailparser in het deploymentartefact en pin hun versies.

import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; import { simpleParser } from "mailparser"; const s3 = new S3Client({}); const bucket = process.env.INBOUND_BUCKET; const prefix = process.env.INBOUND_PREFIX || "inbound/"; export async function handler(event) { for (const record of event.Records || []) { const ses = record.ses; const messageId = ses?.mail?.messageId; const recipients = ses?.receipt?.recipients || []; if (!messageId || !/^[A-Za-z0-9._-]+$/.test(messageId)) { throw new Error("Missing or invalid SES message ID"); } // claimOnce must be an atomic insert with a unique constraint. if (!(await claimOnce(messageId))) continue; try { const object = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: `${prefix}${messageId}`, })); const raw = Buffer.from(await object.Body.transformToByteArray()); const parsed = await simpleParser(raw, { skipHtmlToText: true, skipTextToHtml: true, }); await saveInboundMessage({ providerMessageId: messageId, envelopeRecipients: recipients, envelopeFrom: ses.mail.source, headerMessageId: parsed.messageId || null, inReplyTo: parsed.inReplyTo || null, references: parsed.references || [], subject: parsed.subject || "", text: parsed.text || "", html: parsed.html || null, attachments: parsed.attachments, receivedAt: ses.mail.timestamp, }); await markComplete(messageId); } catch (error) { await releaseOrMarkFailed(messageId, String(error)); throw error; } } }

De placeholderfuncties staan voor applicatiespecifieke opslag, maar hun contract is belangrijk. claimOnce moet een uniciteitsconstraint in de database of een conditionele schrijfactie op het bericht-ID van de provider gebruiken. Een read gevolgd door een insert is gevoelig voor race conditions. Sla de verwerkingsstatus op, zodat een beheerder onderscheid kan maken tussen processing, complete, quarantined en failed.

Routeer op de envelope-ontvanger, niet op de To-header

De zichtbare velden To en Cc zijn berichtcontent die de afzender aanlevert. Ze kunnen de werkelijke bestemming weglaten door BCC, doorsturen of bewuste manipulatie. Voorwaarden in SES-receipt rules gebruiken de ontvangers uit de SMTP-envelope, en AWS adviseert downstream verwerkers om de ontvangers uit de SES-notificatie te gebruiken om te bepalen waar het bericht is afgeleverd (AWS: concepten voor ontvangen).

Dat onderscheid voorkomt een bug tussen tenants. Ontvangt reply+tenant-a@inbound.example.com een bericht waarvan de zichtbare To tenant-b@example.com vermeldt, routeer het dan via de geauthenticeerde applicatiekoppeling van het envelope-adres en nooit via de weergaveheader.

Gebruik een willekeurig, niet te raden antwoordtoken wanneer een adres een klant of gesprek identificeert. Sla het token gehasht op, laat het verlopen waar dat zinvol is en weiger adressen die niet aan een actieve workspace gekoppeld zijn. Een voorspelbaar lokaal deel zoals ticket-42 is een uitnodiging om berichten in de thread van een andere gebruiker te injecteren.

Parse MIME als vijandige input

E-mail is een genest invoerformaat van tientallen jaren oud. RFC 5322 definieert berichtheaders en -body's, terwijl MIME multipart-content en transfer encodings toevoegt (RFC 5322, RFC 2045). Gebruik een onderhouden parser in plaats van zelf te splitsen op lege regels of boundaries.

Pas limieten toe voordat je content beschikbaar maakt voor het product:

  • Begrens het totale aantal gedecodeerde bytes, het aantal bijlagen, de grootte per bijlage, de MIME-nestingdiepte en de parseertijd.
  • Sla bijlagen privé op met gegenereerde objectnamen. Gebruik de bestandsnaam van de afzender nooit als pad.
  • Behandel het opgegeven contenttype en de bestandsnaam als hints. Bepaal het type waar mogelijk op basis van de content.
  • Voer de content van bijlagen nooit uit. Scan bijlagen of zet ze in quarantaine voordat ze gedownload kunnen worden.
  • Sanitize HTML met een strikte allowlist, blokkeer externe afbeeldingen standaard en render de HTML in een geïsoleerde context. Gebruik bij voorkeur plaintext voor geautomatiseerde analyse.
  • Zet geen ruwe body's, adressen, tokens of bijlagecontent in gewone applicatielogs.

SES kan uitslagen voor SPF, DKIM, DMARC, spam en virussen rapporteren, maar AWS merkt op dat SES deze resultaten alleen beschikbaar stelt en je bedrijfsbeleid niet automatisch toepast. Bepaal zelf of mislukte controles moeten worden geweigerd, in quarantaine gezet of met een waarschuwing getoond. Een geslaagde authenticatie identificeert een domein onder een specifiek mechanisme; het maakt de content niet veilig en bewijst niet dat een mens het bericht heeft geschreven.

Maak retries saai

Bij asynchrone aanroep kan Lambda mislukte functies opnieuw proberen, en AWS waarschuwt dat dubbele bezorging mogelijk is, zelfs als de functie geen fout retourneert. Stel een on-failure-bestemming of dead-letter queue in en laat een alarm afgaan bij verwerkingsfouten (retrygedrag van AWS Lambda).

Idempotentie moet elk neveneffect verderop in de keten dekken:

  1. Sla het SES-bericht-ID op onder een uniciteitsconstraint.
  2. Sla geparste content en threadkoppelingen waar mogelijk in één transactie op.
  3. Zet notificaties, het aanmaken van tickets of werk voor agents in een outbox met als sleutel het bericht-ID plus het actietype.
  4. Markeer het record pas als voltooid nadat duurzame schrijfacties zijn geslaagd.
  5. Verwerk opnieuw vanaf het originele S3-object, niet vanaf een logregel waarin informatie verloren is gegaan.

Start je de verwerking via S3-notificaties in plaats van een Lambda-actie in SES, dan geldt dezelfde regel. Notificaties van Amazon S3 zijn ontworpen voor at-least-once-bezorging en komen niet gegarandeerd in volgorde aan (AWS S3-eventnotificaties).

Berichten in threads zetten zonder het onderwerp te vertrouwen

Gebruik de geparste velden Message-ID, In-Reply-To en References om een threadmatch voor te stellen. Zet berichten niet in een thread alleen op basis van een onderwerp dat begint met Re:. Controleer ook of het envelope-adres of het antwoordtoken bij dezelfde workspace en hetzelfde gesprek hoort voordat je iets koppelt.

Automatische antwoorden hebben een apart beleid nodig. Detecteer signalen zoals Auto-Submitted en voorkom dat er antwoordlussen ontstaan. RFC 3834 raadt duidelijke identificatie en terughoudend gedrag aan voor automatische antwoorden (RFC 3834). Stelt een AI-agent een antwoord op, houd verzenden dan een expliciet, idempotent neveneffect. Vereis goedkeuring van de gebruiker bij onverwachte ontvangers, gevoelige content of acties buiten de oorspronkelijke support- of productworkflow. Een bericht ontvangen is geen algemene toestemming voor ongerelateerde marketing.

Productiechecklist

  • De ontvangende regio ondersteunt inkomende e-mail via SES.
  • De domeinidentiteit is geverifieerd en het MX-record wordt correct opgelost.
  • De receipt rule heeft een beperkte voorwaarde voor ontvangers en de bedoelde rule set is actief.
  • De S3-actie wordt uitgevoerd vóór asynchrone Lambda-verwerking.
  • De bucket is privé, toegang volgt least privilege en retentie is gedocumenteerd.
  • De SES-bericht-ID heeft een uniekheidsbeperking in de database.
  • Routing gebruikt envelope-ontvangers, niet zichtbare To- of Cc-headers.
  • MIME, HTML, links en bijlagen worden als niet-vertrouwde invoer behandeld.
  • Mislukte events bereiken een gemonitorde bestemming en kunnen opnieuw worden afgespeeld.
  • Threadmatches dwingen workspace-eigenaarschap af.
  • Geautomatiseerde antwoorden hebben luspreventie, toestemmingsgrenzen en verzend-idempotentie.
  • Een gecontroleerde test omvat plaintext, HTML, BCC, dubbele bezorging, grote bijlagen, ongeldige MIME en parserfouten.

Direct met SES werken past goed als je team AWS-native controle wil en bereid is zelf DNS, IAM, MIME-parsing, tenantisolatie, bewaartermijnen, retryafhandeling en operationele meldingen te beheren. Wil je die applicatiebouwstenen achter een compactere e-mail-API, dan biedt SendHQ inkomende adressen, bewaarde berichten, threads en toegang per workspace naast uitgaande transactionele e-mail. Hoe dan ook: zorg dat het ruwe bericht herstelbaar blijft en dat elke actie verderop in de keten veilig opnieuw kan worden uitgevoerd.