Les flux d’inscription semblent simples jusqu’à ce qu’on les automatise. C’est alors qu’on découvre une réalité frustrante : l’email d’inscription est la partie la plus bruyante du pipeline. Les messages arrivent en retard, arrivent deux fois, ou arrivent après que votre test soit déjà passé à autre chose. Si vous ajoutez des agents LLM par-dessus, vous pouvez aussi obtenir des “boucles de bots”, où un agent redéclenche l’inscription ou rejoue un lien de vérification jusqu’à ce que les limites de taux ou les verrouillages se déclenchent.
Ce guide se concentre sur deux tueurs de fiabilité dans les tests d’emails d’inscription :
- Doublons (même événement email traité plusieurs fois)
- Boucles de bots (automatisation déclenchant répétitivement le même email, ou consommant répétitivement le même email)
L’objectif n’est pas “faire passer le test une fois”, mais rendre l’étape email déterministe, idempotente et sûre à réessayer.
Pourquoi les doublons se produisent dans les tests d’emails d’inscription (ce n’est pas juste votre fournisseur de messagerie)
Les doublons viennent typiquement d’un comportement “au-moins-une-fois” quelque part dans la chaîne. Il est utile de nommer la couche, pour pouvoir dédupliquer au bon niveau.
| D’où viennent les doublons | Cause commune | À quoi ça ressemble dans les tests | Meilleure correction |
|---|---|---|---|
| Votre application | “Renvoyer l’email de vérification” déclenché deux fois, tentatives sans idempotence, doubles soumissions de formulaire | Deux emails avec des tokens différents | Ajouter une clé d’idempotence par tentative d’inscription, imposer un token actif |
| Votre file de tâches | Le worker réessaie sans clé de déduplication | Même modèle, même token envoyé deux fois | Rendre le job d’envoi idempotent (attempt_id) |
| Chemin de livraison SMTP | Greylisting, échecs transitoires, tentatives en amont | Deux messages quasi-identiques, possiblement même Message-ID
|
Dédupliquer par identifiant de message stable et artefact |
| Livraison de webhook | Votre endpoint timeout, le fournisseur réessaie | Même message livré plusieurs fois | Vérifier les signatures et implémenter l’idempotence webhook |
| Consommateur de polling | Bugs de curseur, cohérence finale, récupération répétitive du “dernier” | Même message traité à chaque poll | Utiliser un curseur ou stocker les “ids de messages vus” |
| Orchestration CI / agent | Les réessais de test relancent la même tentative logique | Plus d’emails qu’attendu, assertions instables | Isoler la boîte par tentative, corréler les run ids |
Point clé à retenir : vous ne pouvez pas “prévenir” de manière fiable les doublons dans les systèmes distribués. Vous ne pouvez que concevoir pour que les doublons soient inoffensifs.
Pourquoi les boucles de bots se produisent (et pourquoi elles sont pires que les doublons)
Un doublon est un événement répété. Une boucle de bot est un cycle de rétroaction.
Boucles communes dans l’automatisation d’inscription :
- Boucle de réessai : l’agent timeout en attendant l’email, réessaie l’inscription, déclenchant un autre email, puis répète.
- Boucle de rejeu : l’agent reçoit l’email de vérification, clique le lien magique, obtient une erreur, et clique à nouveau indéfiniment.
- Boucle de parser : l’agent échoue à extraire l’OTP, demande un renvoi, et continue d’accumuler des emails tout en lisant toujours le plus ancien.
- Boucle de rejeu webhook (sécurité + fiabilité) : si vous ne vérifiez pas les payloads webhook signés (et la tolérance timestamp / rejeu), un payload capturé peut être rejoué et causer un traitement répété.
La correction est de traiter la vérification d’inscription comme une petite machine à états avec des budgets :
- Un id de tentative unique
- Un scope de boîte unique
- Une attente bornée
- Une consommation unique de l’artefact de vérification
- Un arrêt ferme quand les budgets sont dépassés

Le pattern déterministe : boîte-par-tentative plus consommation idempotente
Si vous utilisez encore des boîtes partagées (ou l’adressage plus dans une boîte), vous vous battez contre le mauvais combat. Le pattern propre pour les tests d’emails d’inscription est :
- Créer une boîte de réception jetable fraîche par tentative d’inscription
- Envoyer l’email de vérification d’inscription à cette adresse
- Attendre de manière déterministe (webhook d’abord, polling en fallback)
- Extraire un artefact minimal (OTP ou URL)
- Le consommer exactement une fois
Mailhook est conçu pour ce style d’automatisation : vous créez des boîtes de réception jetables via API et recevez les messages entrants comme JSON structuré, livré via webhooks temps réel et/ou récupéré via polling. Pour les endpoints exacts et champs de payload, utilisez la référence canonique sur Mailhook llms.txt.
Dédupliquer correctement : choisir les bonnes clés (id de message vs id d’artefact)
Pour arrêter les doublons, vous avez besoin d’une clé stable pour “cet événement email” et une clé stable pour “cette action de vérification”. Elles ne sont pas toujours identiques.
Clés de déduplication recommandées
| Scope de déduplication | Ce que vous prévenez | Clé suggérée | Notes |
|---|---|---|---|
| Niveau message | Traiter le même email plus d’une fois | Id de message du fournisseur (préféré), ou header Message-ID normalisé |
RFC 5322 définit Message-ID, mais ce n’est pas garanti unique en pratique, traiter comme best-effort |
| Niveau artefact | Cliquer le même lien de vérification deux fois, ou réutiliser un OTP | Hash de l’artefact extrait (valeur OTP, token, ou URL canonicalisée) | Canonicaliser l’URL (supprimer les paramètres de tracking) avant le hash |
| Niveau tentative | Créer plusieurs tentatives “actives” qui font la course |
attempt_id que vous générez avant d’envoyer l’email |
Stocker ceci dans votre DB et logs |
| Livraison webhook | Exécuter votre gestionnaire webhook deux fois |
delivery_id ou id de message du payload |
Retourner 2xx seulement après écriture durable |
Si vous ne pouvez implémenter qu’une chose : idempotence niveau artefact. Même si vous recevez trois emails, seul le premier artefact devrait être consommé.
Webhooks : assumez une livraison au-moins-une-fois et construisez l’idempotence
Les réessais de webhook sont normaux, pas exceptionnels. Les fournisseurs réessaient quand :
- Votre endpoint timeout
- Vous retournez un non-2xx
- Votre load balancer ferme la connexion
Donc votre gestionnaire de webhook doit être :
- Authentifié (vérifier les payloads signés)
- Résistant au rejeu (tolérance timestamp, nonce si disponible)
- Idempotent (même événement peut arriver deux fois)
Mailhook supporte les payloads signés pour la sécurité, ce qui vous permet de vérifier que le webhook vient vraiment de Mailhook et n’a pas été altéré. Suivez la procédure de vérification décrite dans llms.txt.
Forme minimale du gestionnaire webhook (pseudocode)
handleWebhook(request):
payload = request.body
assert verify_signature(request.headers, payload)
event_id = payload.event_id OR payload.message.id
if db.exists("webhook_events", event_id):
return 200
db.insert("webhook_events", {event_id, received_at: now()})
enqueue("process_message", {message_id: payload.message.id, inbox_id: payload.inbox.id})
return 200
Note de conception : écrire l’enregistrement d’idempotence d’abord, puis enqueuer. Si l’enqueue échoue, vous pouvez réessayer en sécurité.
Pour le comportement général des réessais webhook et les patterns de vérification de signature, les docs webhook de Stripe sont une bonne référence, même si vous n’utilisez pas Stripe : webhook best practices.
Polling : arrêter les bugs “le dernier message gagne” avec curseurs et budgets temps
Le polling est un fallback parfaitement valide, mais “récupérer le dernier et parser” est une source commune de doublons et boucles de bots.
Un contrat de polling plus sûr :
- Poller jusqu’à une deadline
- Filtrer étroitement (destinataire + corrélation de tentative)
- Suivre un curseur ou stocker les ids de messages traités
- Sélectionner le premier message qui correspond à la tentative, pas “peu importe ce qui est arrivé le plus récemment”
Boucle de polling minimale (pseudocode)
waitForSignupEmail(inbox_id, attempt_id, deadline):
seen = set()
while now() < deadline:
messages = api.list_messages(inbox_id)
for m in messages:
if m.id in seen:
continue
seen.add(m.id)
if not matches_attempt(m, attempt_id):
continue
artifact = extract_verification_artifact(m)
return {message_id: m.id, artifact}
sleep(backoff())
throw Timeout("No matching signup email")
Ce seul changement, “se souvenir de ce que vous avez déjà regardé”, prévient une quantité surprenante d’instabilité.
Corrélation : rendre le bon email facile à identifier
Les doublons deviennent dangereux quand vous ne pouvez pas dire quel email appartient à quelle tentative.
Options de corrélation, de la plus forte à la plus faible :
- Isolation de boîte : une boîte de réception jetable par tentative (meilleure)
-
Token de tentative explicite dans le contenu email : inclure
attempt_iddans le template (fonctionne bien pour les systèmes internes) -
Header personnalisé : ajouter
X-Correlation-Id: <attempt_id>lors de l’envoi - Tags de sujet : utile, mais le plus facile à casser avec la localisation ou les changements de template
Si vous contrôlez l’expéditeur, un header personnalisé est généralement le plus propre, car il évite le parsing HTML fragile. Si vous ne contrôlez pas l’expéditeur (SaaS tiers), l’isolation de boîte et les matchers étroits sont vos meilleurs outils.
Pour un plongeon profond sur quels headers valent la peine d’être fiables, voir le RFC qui définit le format de message : RFC 5322.
Règles “consommer une fois” qui arrêtent les boucles de rejeu
Une fois que vous extrayez un lien de vérification ou OTP, votre automatisation doit le traiter comme une capacité à usage unique.
Implémentez ces règles :
-
Stocker un marqueur consommé clé par
artifact_hash - Ne pas cliquer ou soumettre un OTP deux fois même si l’UI dit “réessayer”
- Si la rédemption échoue, s’arrêter et exposer une erreur debuggable (ne pas réessayer aveuglément)
Une simple table de base de données suffit :
| Colonne | But |
|---|---|
artifact_hash |
Clé d’idempotence, prévient la double-consommation |
attempt_id |
Lie la consommation à l’exécution |
consumed_at |
Debuggabilité et audit |
result |
Success, already_used, expired, invalid |
C’est comme ça qu’on transforme une boucle potentiellement non bornée en workflow fini.
Agents LLM : prévenir le comportement “renvoi autonome” avec des contraintes d’outils
Les agents LLM sont excellents pour improviser, ce qui est exactement ce qu’on ne veut pas dans les flux d’auth.
Si un agent est autorisé à :
- déclencher l’inscription
- demander un renvoi
- lire les emails
- cliquer les liens
alors un petit problème de parsing peut le faire spammer les renvois et produire une boucle auto-entretenue.
La correction est de donner à l’agent des outils contraints et des budgets explicites :
-
create_signup_attempt()retourne{attempt_id, email, inbox_id, expires_at} -
wait_for_signup_email(attempt_id)retourne un seul message ou timeout -
extract_verification_artifact(message)retourne une seule URL ou OTP -
redeem_artifact_once(attempt_id, artifact)impose l’idempotence et retourne un statut final
Ne donnez pas à l’agent une instruction générique “ouvrir le navigateur et cliquer n’importe quoi dans l’HTML de l’email”. Préférez l’extraction de texte depuis des champs JSON structurés, puis validez l’URL contre une allowlist avant toute navigation.
Observabilité : logger les identifiants qui rendent les doublons explicables
Quand un test d’inscription échoue, vous voulez répondre à ces questions en une minute :
- Quelle était cette tentative ?
- Quelle boîte a été utilisée ?
- Combien de messages sont arrivés, et quand ?
- Quel message a été sélectionné ?
- Quel artefact a été extrait ?
- L’artefact a-t-il été consommé avant ?
Un schéma de logging pratique :
attempt_idinbox_idmessage_idartifact_hash-
delivery_method(webhook ou polling) -
latency_ms(envoi à réception)
Si vous utilisez Mailhook, vous pouvez construire ceci sans parser le MIME brut, car les messages sont livrés comme JSON structuré et peuvent être traités de manière déterministe (voir llms.txt pour le contrat canonique).
Une courte checklist pour arrêter les doublons et boucles de bots
Utilisez ceci comme gate pre-merge pour les tests d’inscription dépendants des emails :
- Utilisez boîte-par-tentative, pas des boîtes partagées
- Attendez via webhook-first, gardez le polling comme fallback
- Implémentez l’idempotence webhook et vérifiez les payloads signés
- Implémentez la sémantique consommer-une-fois niveau artefact
- Ajoutez des budgets (max renvois, max temps d’attente, max tentatives de rédemption)
- Loggez
attempt_id,inbox_id,message_id, etartifact_hash
Où Mailhook s’intègre
Si votre approche actuelle dépend du scraping d’une UI de boîte partagée ou du parsing d’emails HTML imprévisibles, les doublons et boucles sont presque garantis avec le temps.
Mailhook fournit les primitives qui rendent l’automatisation d’inscription à nouveau ennuyeuse :
- Créer des boîtes de réception jetables via API
- Recevoir les emails comme JSON structuré
- Obtenir des notifications webhook temps réel (avec payloads signés)
- Utiliser le polling comme chemin de récupération fallback
- Scaler avec traitement par lots, domaines partagés, ou support de domaine personnalisé
Pour intégrer contre la vraie sémantique API et champs de payload, commencez avec Mailhook llms.txt, puis explorez le produit sur Mailhook.