E-mails entrants · 22 septembre 2026
Comment recevoir des e-mails avec Amazon SES : S3 et Lambda
Construisez un pipeline de réception d’e-mails prêt pour la production avec Amazon SES, S3 et Lambda : sécurité MIME, routage par tenant, idempotence, nouvelles tentatives et fils de discussion.
Le chemin fiable le plus court pour la réception avec Amazon SES est le suivant : vérifiez un domaine, pointez un enregistrement MX vers un endpoint de réception SES, enregistrez chaque message accepté dans S3, puis invoquez Lambda de manière asynchrone pour l’analyser et le persister. Placez l’action S3 avant l’action Lambda. Utilisez l’identifiant de message SES comme clé d’idempotence, routez d’après le destinataire de l’enveloppe SMTP et mettez en quarantaine le contenu dangereux au lieu de faire confiance aux en-têtes ou aux pièces jointes.
L’architecture à construire
Utilisez un sous-domaine dédié comme inbound.example.com, sauf si SES doit recevoir tout le courrier de votre domaine racine. Vous séparez ainsi les e-mails applicatifs des boîtes des employés, et un retour arrière se résume à une modification DNS plutôt qu’à une migration de messagerie.
Le flux de production est le suivant :
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
Les règles de réception d’Amazon SES exécutent leurs actions dans l’ordre. AWS documente explicitement le schéma S3 d’abord, Lambda ensuite, lorsque le code a besoin du corps du message. Une action Lambda directe reçoit les métadonnées et une sélection d’en-têtes, pas le corps complet. Le corps reste l’objet MIME brut dans S3 (concepts de réception AWS SES).
Cette séparation est utile. L’étape exposée au SMTP stocke rapidement le message d’origine, tandis que l’analyse, l’indexation, les notifications et la logique métier interviennent après l’acceptation. Une panne temporaire de la base de données ne doit pas obliger le serveur de messagerie de l’expéditeur à répéter la transaction SMTP.
1. Choisissez une région prise en charge et vérifiez le domaine
La réception d’e-mails SES n’est disponible que dans certaines régions AWS. Choisissez-en une dans la liste actuelle des endpoints de réception SES, puis gardez les ressources SES, Lambda, SNS et KMS dans cette région, sauf si la documentation AWS concernée l’autorise explicitement.
Créez une identité de domaine SES pour le domaine racine ou le sous-domaine exact qui recevra le courrier. La vérification du domaine exige de publier les enregistrements DNS fournis par SES. La vérification pour la réception prouve que vous contrôlez le domaine ; elle est distincte de la configuration de la route MX qui envoie le trafic entrant vers SES (guide AWS de vérification de domaine).
Pour un sous-domaine dédié, les enregistrements DNS ressemblent conceptuellement à ceci :
inbound.example.com. MX 10 inbound-smtp.us-east-1.amazonaws.com.
Remplacez us-east-1 par la région choisie. AWS documente la valeur MX sous la forme 10 inbound-smtp.<region>.amazonaws.com (guide AWS sur l’enregistrement MX). Ne pointez pas votre domaine racine vers SES si des personnes y reçoivent encore du courrier via Google Workspace, Microsoft 365 ou un autre fournisseur de messagerie.
Vérifiez l’enregistrement publié depuis plusieurs résolveurs avant de tester :
dig MX inbound.example.com +short
La visibilité DNS prouve seulement que la route est publiée. Envoyez un message contrôlé à une adresse de test et vérifiez que SES l’a stocké avant de considérer la configuration comme terminée.
2. Stockez le message brut avant de le traiter
Créez un bucket S3 privé avec l’accès public bloqué, une politique de cycle de vie et les permissions IAM les plus restreintes possible. Créez ensuite une règle de réception SES dont la condition de destinataire correspond à votre domaine de réception ou à des adresses précises.
La première action doit déposer le message brut dans S3. Un préfixe d’objet comme inbound/ facilite le ciblage des règles de rétention et des politiques d’accès. SES stocke le contenu MIME brut, non modifié. AWS documente actuellement un maximum par défaut de 40 Mo lorsque les messages sont enregistrés dans S3, alors que l’action SNS qui inclut le message complet a un maximum bien plus faible de 150 Ko (action de réception S3 d’AWS). Cette différence de taille explique pourquoi S3 est le choix par défaut le plus sûr pour les réponses et pièces jointes réelles.
Si vous activez le paramètre KMS facultatif de SES sur l’action de réception, lisez attentivement les détails du chiffrement. SES utilise pour cette fonctionnalité un chiffrement côté client, et non le chiffrement côté serveur habituel de S3 : votre lecteur doit donc déchiffrer l’objet avec un client compatible. Ne l’activez pas à la légère pour découvrir en plein incident que votre parseur Node ne sait pas lire les octets stockés.
Autorisez SES à écrire uniquement dans le bucket et le préfixe prévus. Accordez à Lambda s3:GetObject uniquement pour ce même emplacement. La fonction n’a pas besoin de permissions d’administration du bucket.
3. Ajoutez une action Lambda asynchrone
Placez l’action Lambda après l’action S3 dans la règle de réception et utilisez l’invocation asynchrone, sauf si la fonction doit décider si SES poursuit l’évaluation de la règle. AWS recommande l’exécution asynchrone pour le traitement normal et réserve l’exécution synchrone aux décisions sur le flux de messagerie (action de réception Lambda d’AWS).
Le mail.messageId attribué par SES est aussi la clé de l’objet S3 lorsqu’aucun préfixe n’est configuré. Avec un préfixe, ajoutez-le devant. Le squelette Node.js suivant récupère le message brut et l’analyse. Empaquetez @aws-sdk/client-s3 et un parseur MIME maintenu comme mailparser avec l’artefact de déploiement, et figez leurs versions.
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;
}
}
}
Les fonctions de substitution représentent un stockage propre à l’application, mais leur contrat est important. claimOnce doit s’appuyer sur une contrainte d’unicité en base ou sur une écriture conditionnelle portant sur l’identifiant de message du fournisseur. Une lecture suivie d’une insertion est sujette aux conditions de concurrence. Stockez l’état de traitement pour qu’un opérateur puisse distinguer processing, complete, quarantined et failed.
Routez d’après le destinataire de l’enveloppe, pas l’en-tête To
Les champs visibles To et Cc font partie du contenu du message fourni par l’expéditeur. Ils peuvent omettre la destination réelle à cause d’un BCC, d’un transfert ou d’une manipulation délibérée. Les conditions de réception SES utilisent les destinataires de l’enveloppe SMTP, et AWS demande aux traitements en aval d’utiliser les destinataires indiqués dans la notification SES pour déterminer où le message a été livré (concepts de réception AWS).
Cette distinction évite un bug entre tenants. Si reply+tenant-a@inbound.example.com reçoit un message dont le champ To visible indique tenant-b@example.com, routez-le selon la correspondance applicative authentifiée de l’adresse d’enveloppe, jamais selon l’en-tête affiché.
Utilisez un jeton de réponse aléatoire et impossible à deviner lorsqu’une adresse identifie un client ou une conversation. Stockez le jeton sous forme hachée, faites-le expirer le moment venu et rejetez les adresses qui ne correspondent à aucun espace de travail actif. Une partie locale prévisible comme ticket-42 est une invitation à injecter des messages dans le fil d’un autre utilisateur.
Traitez le MIME comme une entrée hostile
L’e-mail est un format d’entrée imbriqué, vieux de plusieurs décennies. La RFC 5322 définit les en-têtes et le corps des messages, tandis que MIME ajoute le contenu multipart et les encodages de transfert (RFC 5322, RFC 2045). Utilisez un parseur maintenu au lieu de découper vous-même sur les lignes vides ou les délimiteurs.
Appliquez des limites avant de mettre le contenu à disposition du produit :
- Plafonnez le nombre total d’octets décodés, le nombre de pièces jointes, la taille de chaque pièce jointe, la profondeur d’imbrication MIME et le temps d’analyse.
- Stockez les pièces jointes en privé sous des noms d’objets générés. N’utilisez jamais le nom de fichier de l’expéditeur comme chemin.
- Considérez le type de contenu et le nom de fichier déclarés comme de simples indications. Détectez le type à partir du contenu lorsque c’est possible.
- N’exécutez jamais le contenu d’une pièce jointe. Analysez ou mettez en quarantaine les pièces jointes avant tout téléchargement.
- Assainissez le HTML avec une liste d’autorisation stricte, bloquez les images distantes par défaut et affichez-le dans un contexte isolé. Privilégiez le texte brut pour l’analyse automatisée.
- Ne placez pas les corps bruts, les adresses, les jetons ni le contenu des pièces jointes dans les logs applicatifs ordinaires.
SES peut fournir des verdicts SPF, DKIM, DMARC, spam et virus, mais AWS précise que SES expose ces résultats sans appliquer automatiquement votre politique métier. Décidez si les échecs doivent être rejetés, mis en quarantaine ou affichés avec un avertissement. Un résultat d’authentification positif identifie un domaine selon un mécanisme précis ; il ne rend pas le contenu sûr et ne prouve pas qu’un humain l’a rédigé.
Rendez les nouvelles tentatives sans surprise
L’invocation asynchrone de Lambda peut relancer les fonctions en échec, et AWS avertit qu’une livraison en double est possible même lorsque la fonction ne renvoie pas d’erreur. Configurez une destination en cas d’échec ou une file de lettres mortes (dead-letter queue), et déclenchez une alarme sur les échecs de traitement (comportement des nouvelles tentatives AWS Lambda).
L’idempotence doit couvrir chaque effet de bord en aval :
- Insérez l’identifiant de message SES sous une contrainte d’unicité.
- Persistez le contenu analysé et les liens de fil dans une seule transaction lorsque c’est possible.
- Placez les notifications, la création de tickets ou le travail des agents dans une outbox indexée par l’identifiant de message et le type d’action.
- Ne marquez l’enregistrement comme terminé qu’une fois les écritures durables réussies.
- Retraitez à partir de l’objet S3 d’origine, pas d’une entrée de log incomplète.
Si vous déclenchez le traitement à partir des notifications S3 plutôt que d’une action Lambda SES, la même règle s’applique. Les notifications Amazon S3 sont conçues pour une livraison at-least-once et ne sont pas garanties d’arriver dans l’ordre (notifications d’événements AWS S3).
Regroupez les messages en fils sans vous fier à l’objet
Utilisez les champs analysés Message-ID, In-Reply-To et References pour proposer un rattachement à un fil. Ne regroupez pas en fil sur la seule base d’un objet commençant par Re:. Vérifiez aussi que l’adresse d’enveloppe ou le jeton de réponse appartient au même espace de travail et à la même conversation avant de relier quoi que ce soit.
Les réponses automatiques nécessitent une politique distincte. Détectez des signaux comme Auto-Submitted et évitez de générer des boucles de réponses. La RFC 3834 recommande une identification claire et un comportement prudent pour les réponses automatiques (RFC 3834). Si un agent IA rédige une réponse, gardez l’envoi comme un effet de bord explicite et idempotent. Exigez l’approbation de l’utilisateur pour les destinataires inattendus, les contenus sensibles ou les actions sortant du workflow support ou produit d’origine. Recevoir un message ne vaut pas consentement général à du marketing sans rapport.
Checklist de production
- La région de réception prend en charge les e-mails entrants SES.
- L’identité de domaine est vérifiée et l’enregistrement MX est correctement résolu.
- La règle de réception a une condition de destinataire restrictive et le jeu de règles prévu est actif.
- L’action S3 s’exécute avant le traitement asynchrone Lambda.
- Le bucket est privé, l’accès applique le principe du moindre privilège et la conservation est documentée.
- L’ID de message SES a une contrainte d’unicité dans la base de données.
- Le routage utilise les destinataires de l’enveloppe, et non les en-têtes visibles
ToouCc. - MIME, HTML, les liens et les pièces jointes sont traités comme des entrées non fiables.
- Les événements en échec atteignent une destination surveillée et peuvent être rejoués.
- Les correspondances de fil de discussion assurent l’appartenance à l’espace de travail.
- Les réponses automatisées intègrent une prévention des boucles, des limites de consentement et l’idempotence d’envoi.
- Un test contrôlé couvre le texte brut, le HTML, le BCC, la livraison en double, les pièces jointes volumineuses, le MIME malformé et l’échec de l’analyseur.
SES en direct convient bien lorsque votre équipe veut un contrôle natif AWS et est prête à assumer le DNS, l’IAM, l’analyse MIME, l’isolation des tenants, la rétention, la gestion des nouvelles tentatives et les alertes opérationnelles. Si vous préférez disposer de ces primitives applicatives derrière une API e-mail plus restreinte, SendHQ fournit des adresses de réception, des messages conservés, des fils de discussion et un accès limité à l’espace de travail, en plus de l’e-mail transactionnel sortant. Dans tous les cas, gardez le message brut récupérable et faites en sorte que chaque action en aval puisse être rejouée sans risque.