Chapitres

DOC-05 / Référence technique · Chapitre 07

Inbox hub & Atlas Inbox — deux pipelines email

Décrit les deux pipelines IMAP→DB du harness (contact@ trié pour le hub, atlas@ déclencheur d'actions agentiques), la façade d'envoi sortant et les doctrines bloquantes associées (scan AV, zéro contact client direct).

Vue d'ensemble — deux pipelines distincts

Le système héberge deux boîtes mail et deux espaces de stockage d'emails qui n'ont aucun lien de jointure entre eux. C'est la première source de confusion ; à retenir avant tout :

Pipeline « Inbox hub » Pipeline « Atlas Inbox »
Boîte IMAP Boîte de contact principale (hébergeur mutualisé) Boîte agentique dédiée (hébergeur mutualisé, boîte distincte)
Stockage cible Table d'emails du hub Table de messages bruts + table de workflow Atlas
Collecteur actif Script de collecte Python (actif depuis mai 2026) Script de poll Python direct (sans façade intermédiaire)
Implémentation désactivée Ancienne façade Nuxt désactivée mai 2026 (aucune façade Nuxt — poll Python direct)
Classification Heuristique statique (domaine expéditeur → client / MRR / spam) LLM, intention parmi 6 valeurs possibles
Finalité Triage et affichage dans le hub (bugs clients, MRR, priorité) Déclencher des actions : run, chantier, question, négociation, conseil

Le schéma de flux général est le suivant :

Boîte de contact principale (IMAP)
              │
              │ cron toutes les minutes — collecteur Python inbox hub
              │ (lecture seule, fenêtre glissante 7 jours, dédup par UID)
              ▼
    Stockage inbox hub
    (classification heuristique : client / priorité / MRR / spam)
              │
              ▼  interface hub

  [DÉSACTIVÉ mai 2026] ancienne façade Nuxt / tâche planifiée Nitro
  — saturait la boucle d'événements sur timeouts IMAP

Boîte agentique Atlas (IMAP)
              │ cron toutes les minutes — collecteur Python Atlas
              │ (lecture des messages non lus)
              ▼
  Messages bruts ──1:1──▶ Entrées workflow Atlas (statut « reçu »)
              │
              ├─ pièces jointes présentes → analyse antivirus + heuristiques
              │        └─ pièces jointes sûres : oui / non
              ▼
  Classification LLM → intention détectée
              │
  ┌──────┬──────────┬──────────┬──────────────┐
 run  question  chantier  négociation  conseil / bruit

Pipeline « Inbox hub » — boîte de contact principale

Un seul collecteur actif (depuis mai 2026)

Cette table d'emails n'a qu'un seul chemin d'alimentation câblé en production : le collecteur Python dédié à l'inbox hub. L'ancienne façade Nuxt est hors service.

  1. Collecteur Python actif — effectue un poll IMAP direct de la boîte de contact principale via la façade de connexion inbox (mode lecture seule). Il interroge les messages des 7 derniers jours, traite les UIDs du plus récent au plus ancien, et insère chaque message avec une logique upsert : en cas de conflit sur l'identifiant IMAP, les champs déjà renseignés ne sont jamais écrasés. Il tourne en cron toutes les minutes. Ce collecteur remplace l'ancienne tâche planifiée Nitro.
  2. Implémentation historique — désactivée : l'ancienne façade Nuxt (endpoint de synchronisation → tâche Nitro), déclenchée par un script cron dédié, a été désactivée en mai 2026. Elle saturait la boucle d'événements du serveur Nuxt en raison de timeouts IMAP répétés qui bloquaient également la connexion à la base de données. La ligne cron correspondante est commentée dans le crontab.

Il n'existe donc pas deux chemins co-actifs : un chemin live (collecteur Python) et un chemin mort (ancienne façade Nuxt). Le skill de lecture d'urgence (fallback IMAP, lecture seule) précise explicitement qu'il ne touche jamais au stockage principal pour éviter toute double insertion.

Détection d'une vraie insertion : la requête d'upsert utilise un mécanisme permettant de distinguer une vraie insertion d'un backfill sur doublon — ce qui permet de comptabiliser séparément les nouveaux messages et les mises à jour silencieuses.

Classification heuristique (sans LLM)

La classification de l'inbox hub repose sur une heuristique statique appliquée à l'adresse expéditeur uniquement (le sujet n'est pas utilisé) :

  • L'adresse est comparée à un référentiel de domaines et d'emails clients connus → enrichit le message avec le nom du client, sa priorité et son MRR.
  • Sinon, des marqueurs de spam sont testés sur l'adresse (newsletter, noreply, no-reply, mailer-daemon, postmaster, notifications@, updates@, marketing@) → le message est marqué spam.

Le statut initial posé est spam ou new. Le corps du message est extrait en deux variantes (texte brut et HTML) de façon multipart, en ignorant explicitement les parties marquées comme pièces jointes. Les deux corps sont persistés intégralement, sans troncature. Sur ré-sync d'un message déjà connu, l'upsert ne backfille les corps que s'ils sont vides (via COALESCE) — la classification existante reste intacte.

Pièces jointes : non capturées par le flux actif. Le collecteur Python inbox hub ne persiste aucune pièce jointe — les parties Content-Disposition: attachment sont explicitement ignorées lors de l'extraction du corps. Le stockage des pièces jointes (une ligne par fichier en données binaires) appartenait à l'ancienne façade désactivée ; en l'état actuel, la table de pièces jointes de l'inbox hub n'est plus alimentée par ce pipeline.

Gestion des credentials de connexion IMAP

Le collecteur actif ne lit pas les variables de connexion IMAP directement : il délègue à une façade de connexion qui résout les credentials de la boîte de contact. Ces valeurs résident uniquement dans le fichier d'environnement du serveur hôte, jamais dans le dépôt.

Plusieurs composants du système (client d'envoi sortant, scripts utilitaires, collecteur principal) référencent le mot de passe IMAP sous des noms de variables légèrement différents selon leur origine. En cas de divergence, le fichier d'environnement hôte fait foi et définit le mapping réel.

Structure du stockage inbox hub

Champ Type Rôle
Identifiant interne entier, clé primaire
Identifiant IMAP chaîne, unique Clé de déduplication (upsert ON CONFLICT)
Expéditeur (adresse, nom), sujet chaîne
Date de réception horodatage avec fuseau
Nom client, priorité client, MRR chaîne / entier Rempli par la classification heuristique
Marqueur bug, mot-clé bug, sévérité IA, résumé IA entier / chaîne / texte Triage bug (posé à 0 à l'insertion initiale)
Chemin d'archive chaîne Référence au fichier .eml archivé (non écrit par le flux actif)
Statut chaîne new, spam, …
Traité par, traité le, notes divers Annotations internes
Lien vers chantier entier, clé étrangère Rattachement à un chantier
Corps texte brut, corps HTML texte Corps complet non tronqué
Créé le, mis à jour le horodatage Écrits à chaque insertion ou mise à jour

L'interface hub expose plusieurs endpoints pour lister les messages, accéder au détail d'un message, le mettre à jour, consulter ses pièces jointes, le lier à un chantier, ou déclencher manuellement une synchronisation.

Pipeline de réception et classification des emails entrants

Ce pipeline traite en trois étapes automatiquement enchaînées les emails reçus sur la boîte de réception centrale de CodeMyShop : collecte → analyse des pièces jointes → classification.

Collecte des messages

Un planificateur déclenche le cycle de collecte toutes les minutes via un mécanisme de verrou non-bloquant : si un cycle est déjà en cours, le déclenchement suivant est silencieusement ignoré. Un chien de garde vérifie périodiquement (toutes les quinze minutes) les verrous orphelins et les libère si nécessaire.

Le cycle de collecte suit les étapes suivantes :

  1. Connexion IMAP via la façade obligatoire — l'accès à la boîte mail passe exclusivement par la couche d'abstraction email de Synedre OS (voir la section sur le garde-fou de façade ci-dessous). Toute connexion IMAP directe est bloquée structurellement.
  2. Recherche des messages non lus — la requête porte sur les messages marqués UNSEEN, avec une limite configurable (50 par défaut). Le fetch utilise le mode PEEK pour ne pas marquer les messages comme lus automatiquement.
  3. Persistance en base — chaque message est inséré de façon idempotente : un conflit sur l'identifiant de message est ignoré silencieusement. Le corps du message est plafonné (60 000 caractères en texte brut, 120 000 en HTML) pour éviter des dépassements de capacité. Le marquage \Seen n'est posé qu'après confirmation de l'insertion, garantissant que tout message non marqué sera retraité en cas de crash.
  4. Enregistrement dans le registre Atlas — une seconde insertion, également idempotente, crée l'entrée de suivi avec le statut initial received.
  5. Enchaînement automatique — si le message comporte des pièces jointes, l'analyse antivirus est déclenchée avant la classification (doctrine scan-first), puis la classification est lancée dans tous les cas.

Anti-boucle interne : les emails dont l'expéditeur est la boîte Atlas elle-même, ou dont le sujet contient un identifiant interne de suivi, sont marqués comme lus et ignorés sans insertion, afin d'éviter les boucles d'auto-spawn. Les re-forwards manuels d'un expéditeur fondateur identifié sont en revanche traités normalement, car ils constituent un flux de travail légitime.

Audit : chaque cycle est tracé dans un journal forensic append-only. Par conception, aucune donnée personnelle n'est enregistrée en clair : seul un condensé cryptographique tronqué de l'adresse expéditeur est conservé.

Analyse des pièces jointes

Lorsqu'un message comporte des pièces jointes, le module d'analyse est invoqué avant toute classification. Le mécanisme est le suivant :

  1. Récupération des métadonnées du message (identifiant IMAP, présence de pièces jointes) par jointure entre les tables de suivi.
  2. Re-fetch du message en lecture seule par son identifiant IMAP.
  3. Extraction des pièces jointes vers un répertoire temporaire isolé, avec protection anti-traversal de chemin sur les noms de fichiers.
  4. Refus pré-scan : les extensions exécutables connues (.exe, .scr, .bat, .cmd, .com, .vbs, .js, .jar, .msi, .dmg, .deb, .rpm, .ps1) et les fichiers dépassant 25 Mo sont immédiatement rejetés avec le verdict blocked.
  5. Analyse antivirus de chaque pièce jointe via la façade de l'agent de sécurité (voir §5).
  6. Verdict global : attachments_safe = 1 uniquement si au moins un fichier a été scanné et qu'aucun fichier suspect, infecté ou refusé n'a été détecté. Dans le cas contraire, les pièces jointes sont déplacées en quarantaine et le statut du message est basculé en failed.
  7. Persistance du verdict détaillé (format structuré JSONB) et enregistrement de l'audit.

Classification par intelligence artificielle

La classification détermine la nature de chaque email entrant et déclenche la matérialisation des objets métier correspondants.

Garde-fous pré-classification

Blocage sur pièces jointes non sûres : si un message comporte des pièces jointes, que le verdict d'analyse est disponible, et que ce verdict est négatif (attachments_safe = 0), la classification est refusée et l'événement est audité. Tolérance : un verdict absent (message sans pièce jointe ou scan non encore effectué) laisse passer la classification.

Court-circuit pré-LLM — re-forward fondateur vers Atlas : si l'expéditeur est le fondateur et que le sujet indique un re-forward d'un récapitulatif Atlas, l'intention est forcée à noise sans appel au modèle de langage. Ce court-circuit évite de déclencher un spawn coûteux sur un contenu déjà traité.

Protection contre l'injection de prompt

  • Le corps de l'email est encadré par des marqueurs explicites de début et de fin.
  • Un avertissement explicite précise au modèle que le contenu encadré est de la donnée email, non des instructions.
  • Le corps est tronqué à 8 000 caractères avant d'être soumis au modèle.
  • La sortie est contrainte par une énumération stricte d'intentions valides — toute hallucination hors-enum est rejetée par la validation.
  • Le périmètre et le destinataire ne sont jamais dérivés du contenu du message.

OCR local — enrichissement du contexte

Après validation du verdict antivirus (pièces jointes sûres), si des pièces jointes sont présentes, un moteur OCR local (Tesseract) analyse les images pour en extraire le texte. Ce texte est ajouté au contexte du prompt entre marqueurs dédiés. Deux règles de doctrine s'appliquent :

  • Le texte OCR n'est jamais persisté en base de données (données personnelles potentielles — usage en mémoire vive uniquement).
  • L'OCR est déclenché après le garde-fou antivirus, afin de ne jamais lire le contenu d'une image non encore analysée.

Appel au modèle de langage

Si aucun court-circuit ne s'est déclenché, le système interroge un modèle de langage via la façade IA unifiée de Synedre OS. Le fournisseur et le modèle sont définis par la configuration de routage (par défaut : Mistral, modèle mistral-small-latest). La sortie est demandée en JSON structuré avec un délai d'expiration de 30 secondes.

Validation de la sortie : l'intention doit appartenir à l'ensemble des valeurs valides et le score de confiance doit être compris entre 0 et 1. Un score inférieur à 0,7 déclenche un audit classification_uncertain. Un score inférieur à 0,5 entraîne un plancher automatique à noise, indépendamment du seuil d'audit.

Garde-fous post-LLM

  1. Re-forward fondateur avec pièces jointes classifié à tort en noise : si le modèle retourne noise alors que l'expéditeur est le fondateur et que le message comporte des pièces jointes, l'intention est requalifiée en question. Ce cas couvre les situations où le contenu actionnable se trouve dans une image que le modèle de petite taille ne peut pas analyser directement.
  2. Client existant rerouté : si l'intention est negociation ou conseil mais que l'adresse expéditrice correspond à un tenant actif (vérification par domaine email dans les tables de référence client), l'intention est requalifiée en run scopé sur ce tenant. Les prospects restent en negociation.

Intents valides et matérialisation

Les six intentions valides sont : run, chantier, question, noise, negociation, conseil. Chaque intention matérialise au plus une entrée dans la table métier correspondante, de façon idempotente.

Intent Action Objet créé Déduplication Lien tracé
run UPSERT Travail (source='atlas-inbox', trigger='email', scope optionnel si rerouté) Contrainte unique sur la référence source Identifiant du travail lié
question UPSERT Question (status='pending') Contrainte unique sur la référence source Identifiant de la question liée
negociation INSERT Négociation (status='nouveau') Garde sur lien déjà présent Identifiant de la négociation liée
conseil INSERT Conseil (status='received') Garde sur lien déjà présent Identifiant du conseil lié
chantier Draft manuel (CLI) Chantier (status='draft') Identifiant du chantier lié
noise No-op total

Pour les intentions negociation et conseil, le nom de l'entreprise est dérivé du domaine expéditeur, sauf pour les domaines génériques (gmail.com, outlook.com, laposte.net, etc.).

Cas particulier de l'intent chantier : contrairement aux autres intentions, la création d'un chantier n'est pas automatique à la classification. Le message reste en statut classified avec l'intention chantier. La création du chantier en mode brouillon est déclenchée manuellement par le fondateur via l'option --link-chantier de l'interface CLI, conformément à la doctrine des procédures en sept étapes.

Interface en ligne de commande — classification

# Classifier un email par son identifiant
python3 -m un composant interne --id-atlas-email 42

# Classifier tous les emails en attente (reçus, pièces jointes sûres ou absentes)
python3 -m un composant interne --all-pending

# Reclassifier un email déjà traité
python3 -m un composant interne --reclassify 42

# Créer un chantier brouillon depuis un email classifié chantier
python3 -m un composant interne --link-chantier 42

# Rapport des classifications à faible confiance (< 0.7)
python3 -m un composant interne --report-uncertain

Garde-fou de façade email

Ce garde-fou est un mécanisme technique, non une simple convention. Il s'agit d'un hook de pré-exécution branché sur les commandes shell lancées par les agents. Son rôle est d'intercepter toute tentative d'accès direct à la messagerie (envoi ou lecture IMAP/SMTP en Python inline ou via un script non référencé) et de la bloquer avec un code d'erreur dur.

Fonctionnement :

  • Une liste blanche recense les modules email autorisés de Synedre OS (client email, synchronisation IMAP, accès direct). Toute commande faisant référence à l'un de ces modules est laissée passer.
  • Les commandes non listées sont analysées à la recherche de motifs d'accès direct : imports smtplib, imaplib, classes SMTP_SSL/IMAP4_SSL, méthodes d'envoi de message MIME, ou appels à des façades parallèles bannies (send_immediate, send_email_quick).
  • En cas de correspondance, la commande est bloquée et un message d'erreur explicatif est affiché à l'agent.

Ce garde-fou a été introduit suite à un incident de production où une façade non officielle contournait l'appui sur le dossier /Sent IMAP et corrompait l'encodage des caractères accentués lors de l'échappement shell. Le hook empêche structurellement toute rechute.

Schéma de la table de suivi Atlas

La table de suivi centrale stocke l'ensemble des informations de cycle de vie d'un email entrant :

  • Identité : clé primaire de l'entrée Atlas, clé étrangère vers la table de messages email.
  • Classification : intention classifiée, score de confiance (numérique), nom du modèle utilisé, justification textuelle.
  • Statut : progression dans le pipeline (receivedclassified ou failed).
  • Pièces jointes : verdict détaillé (JSONB), indicateur binaire de sécurité.
  • Liens vers les objets métier : identifiants des chantiers, travaux, questions, négociations et conseils créés.
  • Bloc d'autorisation d'expédition : jeton d'expédition, horodatages d'émission et de consommation, identifiant du consommateur, identifiant du message de réponse.
  • Provenance : type de source et métadonnées associées (JSONB), permettant de distinguer un email collecté par poll IMAP d'un re-forward manuel. C'est cette information qui permet au court-circuit de re-forward fondateur de fonctionner sans gaspiller un spawn.

Journal d'audit forensic : une table d'audit append-only enregistre chaque action significative (type d'action, acteur, détails structurés en JSONB, horodatage). Par conception, les détails ne contiennent jamais de données personnelles en clair : uniquement des condensés, identifiants, codes d'erreur et métriques.

Note — Spawn et expédition sont deux pipelines distincts :

  • Le module de spawn (planificateur toutes les 5 minutes) déclenche un agent en mode autonome pour les seules intentions chantier et question. Les intentions noise et run ne déclenchent jamais de spawn via ce planificateur.
  • Le module d'expédition gère le pipeline d'autorisation par email : génération d'un jeton d'expédition, envoi de l'email d'autorisation via la façade officielle, puis triple vérification anti-usurpation (liste blanche expéditeur + DKIM + correspondance du fil de discussion + jeton à usage unique atomique) avant d'exécuter la commande d'expédition.

Ces deux modules sont hors périmètre du présent chapitre.

Extraction manuelle de pièces jointes

Un outil en ligne de commande permet d'extraire les pièces jointes d'un email par identifiant de message (et non par identifiant de boîte). Il accepte un répertoire de sortie optionnel ; par défaut, les fichiers sont déposés dans un dossier temporaire dédié.

  • Les identifiants de connexion à la boîte mail (serveur, utilisateur, mot de passe) sont lus depuis les fichiers d'environnement du dépôt — la valeur effective fait foi.
  • Le parser est multipart-aware et préserve les octets binaires à l'écriture.
Doctrine : les fichiers extraits ne doivent jamais être ouverts ni parsés avant que le scanner antivirus ait rendu un verdict clean (voir section suivante). La commande de recherche d'emails liste les pièces jointes sans les extraire, précisément pour respecter ce principe scan-first.

Façade d'envoi d'e-mails professionnels

Une façade unique centralise tous les envois d'e-mails professionnels. L'utilisation directe de la bibliothèque SMTP bas niveau est interdite hors de cette façade. C'est la règle absolue de ce périmètre.

Cycle brouillon → validation → envoi

Le flux de travail suit trois étapes distinctes :

  1. Brouillon (--draft) : écrit un fichier JSON horodaté dans le répertoire des brouillons avec le statut draft, et envoie automatiquement une copie de validation au responsable (jamais au client). Le message ne part pas au client à cette étape — un message console l'indique explicitement.
  2. Validation (--validate) : renvoie la copie de validation au responsable pour relecture. La commande --list liste les brouillons en attente ; --preview affiche le contenu complet d'un brouillon identifié.
  3. Envoi réel (--send --draft-id N) : envoie le message au destinataire final et archive une copie dans le dossier « Envoyés » de la boîte IMAP. Seul ce déclenchement explicite, après validation humaine, livre le message au client.

Les sous-commandes draft et send ne s'enchaînent jamais automatiquement : c'est l'implémentation de la doctrine show before send.

Arguments disponibles

Flag Effet
--to / --cc / --subject Destinataires (répétable ou CSV) et objet du message
--body Corps du message en ligne
--body-file Corps lu depuis un fichier UTF-8 (recommandé, évite l'inline)
--markdown Convertit le corps Markdown en HTML avant envoi (recommandé pour tout e-mail client)
--html Corps déjà au format HTML
--attachment Pièce jointe (répétable) — échoue immédiatement si le fichier est absent
--draft-id Cible un brouillon existant pour envoi, prévisualisation ou validation

L'option --draft exige obligatoirement --to, --subject et un corps (--body ou --body-file).

Copie de validation automatique

À chaque création de brouillon, le système envoie une copie au responsable avec le sujet préfixé [À VALIDER → <destinataire>] <objet>. Un mécanisme anti-boucle empêche l'envoi de cette copie si le destinataire est déjà la boîte de validation elle-même.

Avant l'affichage du brouillon, le système interroge la façade de gestion du ton rédactionnel pour rappeler le registre approprié selon le destinataire — opération best-effort, non bloquante.

Envoi réel et archivage

L'envoi réel passe par SMTP puis archive une copie dans le dossier « Envoyés » de la boîte IMAP. Plusieurs variantes de nommage de ce dossier sont supportées (conventions OVH, Gmail, encodage UTF-7). Si l'archivage IMAP échoue, l'e-mail est tout de même parti — c'est un avertissement non bloquant. La signature HTML est ajoutée automatiquement à l'envoi.

Configuration SMTP/IMAP

Les paramètres de connexion (serveur SMTP, port, identifiants, boîte d'expédition, serveur IMAP) sont lus depuis les fichiers d'environnement du dépôt. Une vérification au démarrage liste toute variable manquante. L'utilisation du port 465 active le mode SMTP over SSL.

Scan antivirus obligatoire avant ouverture (P0)

La façade de scan antivirus est sous la responsabilité de l'agent Mitnick. Aucune pièce jointe extraite n'est ouverte ni parsée avant un verdict clean. Le recours à un service cloud externe (de type analyse en ligne) est exclu pour protéger les données des clients. Ce garde-fou est absolu et non contournable.

Couches d'analyse

Le scanner applique cinq couches successives :

  1. Refus des exécutables : toute extension exécutable reconnue (binaires Windows/Linux, scripts shell, PowerShell, JavaScript, VBScript, archives Java…) déclenche immédiatement un verdict infected.
  2. ClamAV : analyse par signature via le binaire système. Une correspondance positive déclenche infected en fail-fast. Si ClamAV n'est pas installé sur la machine, le scan renvoie une erreur explicite.
  3. Heuristiques PDF : détection par expressions régulières de constructions PDF dangereuses (JavaScript embarqué, actions automatiques, formulaires soumis, médias enrichis…) sur les premiers mégaoctets du fichier → verdict suspicious.
  4. Macros Office : pour les formats Office à macros activées, recherche de patterns d'exécution automatique et d'appels système via l'outil olevba (best-effort) → suspicious.
  5. Archives : aucune descente dans les archives ; verdict suspicious par défaut, revue manuelle requise.

Marqueur de verdict propre

Sur verdict clean, un fichier de marqueur JSON atomique (empreinte SHA-256 + horodatage) est écrit dans un sous-répertoire de la pièce jointe. Les processus en aval opèrent en fail-close : l'absence du marqueur signifie « pas encore scanné » et bloque le traitement. L'écriture est atomique (fichier temporaire + remplacement) ; si elle échoue, le verdict reste correct — seule la persistance du marqueur est optionnelle.

Tableau des verdicts

Verdict Signification Code de sortie CLI
clean Aucun signal détecté — ouverture autorisée 0
suspicious Signaux statiques présents — revue manuelle ou sandbox 1
infected Correspondance ClamAV ou exécutable — NE PAS OUVRIR 2
error Scan impossible (ClamAV indisponible, fichier absent…) 3

L'interface en ligne de commande accepte un ou plusieurs chemins et renvoie le pire verdict parmi tous les fichiers analysés. Le pipeline de traitement des e-mails entrants consomme ce scanner et refuse de progresser si les pièces jointes ne sont pas déclarées sûres.

Doctrine — zéro communication client directe (P0)

Le système ne parle jamais directement aux clients. Toute communication externe transite par le responsable humain.

Plusieurs garde-fous techniques concrétisent cette règle dans ce périmètre :

  • La façade d'envoi n'envoie jamais au client lors de la création d'un brouillon ; elle écrit le brouillon et envoie une copie de validation au responsable. Seul l'envoi explicite post-validation livre le message au client.
  • Un hook de prévention intercepte toute tentative d'utilisation directe des bibliothèques mail bas niveau hors de la liste blanche des façades autorisées. Ce garde-fou rend la façade non contournable.
  • Le moteur de classification des e-mails entrants ne dérive jamais le destinataire ni la portée d'une réponse depuis le corps du message (protection anti-injection). Les brouillons de réponse restent en statut pending jusqu'à validation humaine.
  • Le vouvoiement est systématique pour tout contact externe. Les rendez-vous se fixent via un lien de réservation en ligne uniquement — jamais de créneau proposé en dur dans un e-mail.

Tâches planifiées actives

Fréquence Rôle Pipeline
Chaque minute Synchronisation de la boîte de réception Collecteur IMAP → table de stockage des e-mails entrants (seul collecteur live)
Chaque minute Interrogation du pipeline de traitement Détection des nouveaux e-mails → file de traitement
Toutes les 15 minutes Surveillance des verrous orphelins Libération automatique des verrous bloquants du pipeline
Toutes les 5 minutes (avec délai initial de 2 min) Spawn agentique Lancement d'instances headless pour les intents chantier et question uniquement (liste blanche fixe)
Collecteur désactivé : l'ancien collecteur qui appelait la façade applicative web via une route HTTP est commenté depuis mai 2026. Il causait une saturation de la boucle d'événements du serveur applicatif (timeouts IMAP/base de données en cascade). Il est remplacé par le worker Python externe listé ci-dessus. Ne pas le réactiver sans corriger la cause racine.

Pièges connus

  • Deux tables distinctes, deux mondes étanches. La table de stockage des e-mails du hub (indexée par identifiant IMAP) et les tables du pipeline de traitement (indexées par compte et identifiant de message) ne sont pas joignables entre elles.
  • Marquage « lu » posé après insertion : un crash entre la récupération et l'insertion laisse l'e-mail non lu, donc retraité au cycle suivant. L'idempotence est assurée par une contrainte de conflit à l'insertion.
  • Récupération silencieuse obligatoire (PEEK) : le fetch du pipeline de traitement doit utiliser le mode PEEK pour ne pas consommer le drapeau « non lu » prématurément.
  • --markdown recommandé pour tout e-mail client (sinon le corps apparaît brut, non formaté). Préférer toujours --body-file à --body inline.
  • Scan-first non négociable : ouvrir une pièce jointe avant verdict clean constitue une dette d'architecture de niveau P0.
  • Décalage un composant interne : la description interne du collecteur d'e-mails mentionne un intervalle de 5 minutes, mais il tourne effectivement chaque minute.
  • Anti-boucle e-mail : la boîte de contact du fondateur transfère volontairement vers la boîte du pipeline. Seule cette dernière est filtrée comme expéditeur automatique. Ne pas ajouter la boîte du fondateur dans la liste des expéditeurs à ignorer.
  • Intent chantier : le moteur de classification ne crée jamais de chantier automatiquement — c'est une action CLI explicite. Cela évite la création de chantiers orphelins sans lettre de mission.