Chapitres
Sur cette page
DOC-04 / Référence technique · Chapitre 07
Inbox hub et Atlas Inbox : deux pipelines IMAP distincts
Ce chapitre décrit les deux pipelines de collecte IMAP de Synedre, l'un alimentant la boîte personnelle vers `ps_ac_inbox_emails` via classification heuristique, l'autre traitant la boîte agentique CodeMyShop pour déclencher des actions via classification LLM.
Vue d'ensemble — deux pipelines de messagerie distincts
Le système de messagerie de Synedre repose sur deux pipelines entièrement séparés, sans lien de jointure entre eux. Chacun dispose de sa propre boîte de réception, de sa propre table de stockage et de sa propre logique de traitement. Les confondre est la première source d'erreur d'exploitation.
| Pipeline « Inbox hub » | Pipeline « Inbox agentique » | |
|---|---|---|
| Boîte IMAP | Boîte de contact personnelle (hébergeur mutualisé) | Boîte agentique dédiée (même hébergeur, dossier distinct) |
| Table cible | Table d'inbox principale (brute + classification) | Table de messages bruts et table de workflow agentique |
| Collecteur actif | Collecteur Python planifié à la minute | Collecteur Python planifié à la minute (dossier non-lus) |
| Implémentation désactivée | Ancienne façade serveur (désactivée, voir §2.1) | Aucune — le collecteur Python a toujours été direct |
| Classification | Heuristique statique (domaine expéditeur) | Modèle de langage — 6 intentions possibles |
| Finalité | Triage et affichage dans le hub (bugs, MRR, priorité) | Déclenchement d'actions : run, chantier, négociation, conseil… |
| Interface utilisateur | Cadran inbox du hub, skill /inbox |
Cadrans runs, négociations, etc. |
Boîte de contact (IMAP, dossier personnel)
│
│ cron toutes les minutes — collecteur inbox hub
│ (lecture seule, recherche des 7 derniers jours)
▼
Table inbox principale
(classification heuristique : client / priorité / MRR / spam)
│
▼ skill /inbox, cadran hub
[DÉSACTIVÉ] Ancienne façade serveur (POST sync)
désactivée le 2026-05-23 — saturait la boucle d'événements
Boîte agentique (IMAP, dossier dédié)
│ cron toutes les minutes — collecteur agentique
│ (recherche des messages non-lus)
▼
Table messages bruts ──1:1──► Table workflow agentique (statut='received')
│
├─ pièces jointes → scanner antivirus + heuristiques
│ └─ résultat : sûr / non sûr
▼
Classifieur LLM → intention détectée
│
┌──────────┬────┴─────┬───────────────┬──────────┐
run question chantier négociation conseil / bruit
Pipeline « Inbox hub » — boîte de contact vers table d'inbox principale
Un seul collecteur actif depuis mai 2026
Depuis le 23 mai 2026, un unique collecteur Python alimente la table d'inbox principale. L'ancienne façade serveur est définitivement désactivée.
-
Collecteur Python actif — interroge la boîte de contact en lecture seule via la façade de connexion IMAP interne. Il recherche les messages des 7 derniers jours, les traite du plus récent au plus ancien, et insère chaque message avec une clause
ON CONFLICT (imap_id) DO UPDATE: en cas de doublon, seuls les champs vides sont complétés (COALESCE), la classification existante n'est jamais écrasée. Le collecteur est déclenché par le planificateur système toutes les minutes. - Façade serveur historique — désactivée — l'ancienne implémentation, qui exposait un endpoint de synchronisation déclenché par un script shell planifié, a été arrêtée le 23 mai 2026. Elle provoquait des timeouts IMAP et saturait la boucle d'événements du serveur applicatif, rendant le hub indisponible. La ligne correspondante dans le planificateur système est commentée et accompagnée d'un avertissement explicite.
Il n'existe donc pas deux chemins co-actifs : un seul chemin est vivant (le collecteur Python), l'autre est mort. Le skill de secours en lecture seule (fallback IMAP d'urgence) ne touche jamais à la table principale afin d'éviter toute double-insertion.
Détection de vraie insertion : la clause RETURNING distingue une véritable insertion d'un backfill sur doublon, permettant de comptabiliser séparément les nouveaux messages et les doublons ignorés.
Classification heuristique — pas de modèle de langage ici
La classification repose sur une heuristique statique appliquée à l'adresse de l'expéditeur uniquement (le sujet n'est pas utilisé) :
- L'adresse est comparée à une table interne de domaines clients connus (correspondance exacte sur l'adresse ou sur le domaine) ; si elle correspond, les champs nom client, priorité et MRR sont renseignés.
- Sinon, l'adresse est testée contre une liste de marqueurs spam typiques (
newsletter,noreply,no-reply,mailer-daemon,postmaster,notifications@,updates@,marketing@…) ; si un marqueur correspond, le message est classé spam.
Le statut initial posé est soit spam, soit new.
Le corps du message — aux formats text/plain et text/html — est extrait de manière multipart, en ignorant explicitement les parties marquées comme pièces jointes. Les deux corps sont persistés intégralement (sans troncature).
Pièces jointes non capturées par le flux actif. Le collecteur Python actif ne persiste aucune pièce jointe. Cette fonctionnalité appartenait à l'ancienne façade désactivée ; en l'état, la table de stockage des pièces jointes de l'inbox hub n'est plus alimentée.
Gestion des credentials de connexion IMAP
Le collecteur actif ne lit pas directement les variables d'environnement IMAP : il délègue cette résolution à la façade de connexion interne, qui lit les credentials depuis le fichier d'environnement de la machine hôte. Ce fichier est la source unique de vérité et n'est jamais versionné dans le dépôt.
Attention : les différents composants du système (façade d'envoi sortant, scripts utilitaires, collecteur actif) utilisent des noms de variables légèrement divergents pour désigner le mot de passe IMAP. En cas de doute, le fichier d'environnement de la machine hôte fait foi. Le nom canonique documenté dans la référence de configuration centrale est distinct des alias utilisés dans certains scripts anciens.
Structure de la table d'inbox principale
| Colonne | Type | Rôle |
|---|---|---|
| Identifiant interne | integer PK | Clé primaire auto-incrémentée |
| Identifiant IMAP | varchar UNIQUE | Clé de déduplication — cible du ON CONFLICT |
| Expéditeur (adresse, nom), sujet | varchar | Métadonnées du message |
| Date de réception | timestamptz | |
| Nom client, priorité, MRR | varchar / int | Renseignés par la classification heuristique |
| Indicateur bug, mot-clé bug, sévérité IA, résumé IA | smallint / varchar / text | Triage bug — l'indicateur est posé à 0 à l'insertion initiale |
| Chemin d'archive | varchar | Non écrit par le flux actif |
| Statut | varchar | new, spam, etc. |
| Traitement (agent, date, notes) | divers | Annotations de traitement manuel ou agentique |
| Lien chantier | integer FK | Rattachement à un chantier existant |
| Corps texte, corps HTML | text | Corps complet non tronqué |
| Dates de création / mise à jour | timestamptz | Mises à jour à chaque insertion ou mise à jour |
Le cadran d'inbox du hub expose des endpoints permettant de lister les messages, consulter un message individuel, modifier son statut, consulter ses pièces jointes, le rattacher à un chantier, ou déclencher une synchronisation manuelle.
Pipeline « Atlas Inbox » — Boîte de réception intelligente
Ce pipeline traite automatiquement les emails entrants de la boîte centrale de la plateforme en trois étapes chaînées dans un seul cycle de scrutation : scrutation IMAP → analyse des pièces jointes → classification par IA.
3.1 Scrutation IMAP
Un automate de scrutation s'exécute toutes les minutes via le planificateur système. Un mécanisme de verrou exclusif garantit qu'une seule instance tourne à la fois ; un gardien secondaire, déclenché toutes les quinze minutes, nettoie les verrous orphelins laissés par des crashes précédents.
Le cycle de scrutation suit les étapes suivantes :
- Acquisition d'un verrou non-bloquant — si le verrou est déjà tenu, le cycle est silencieusement ignoré.
- Connexion à la boîte de réception via la façade email obligatoire (tout accès IMAP direct est interdit — voir §3.5). La connexion s'ouvre en mode lecture-écriture pour permettre la pose du drapeau
\Seenaprès traitement. - Recherche des messages non lus (
UNSEEN), plafonnée à 50 messages par cycle (configurable). - Récupération du corps de chaque message avec
BODY.PEEK[]— le modePEEKévite de marquer automatiquement le message comme lu ; le drapeau\Seenn'est posé qu'après un enregistrement confirmé en base, garantissant l'idempotence : « non-lu = pas encore traité », même en cas de crash post-récupération. - Insertion en base dans la table des messages email, avec résolution de doublon par
ON CONFLICT DO NOTHING. Le corps est tronqué (texte brut : 60 000 caractères ; HTML : 120 000 caractères) pour éviter les dépassements lors des échanges avec la base de données. - Insertion d'une ligne de suivi dans la table de workflow Atlas, avec le statut initial
received, également idempotente. - Chaînage automatique (niveau 1) : si le message contient des pièces jointes, l'analyse antivirus est déclenchée en premier (doctrine scan-first), puis la classification par IA est appelée dans tous les cas.
- Écriture d'un événement d'audit append-only — sans donnée personnelle en clair : seul un hash court (8 caractères) de l'adresse expéditrice est enregistré.
Anti-boucle interne : les emails envoyés depuis la boîte elle-même, ou dont le sujet trahit une origine interne au pipeline, sont marqués lus et ignorés sans insertion, pour éviter les boucles d'auto-spawn. Les re-forwards légitimes provenant du fondateur (emails clients transmis manuellement) ne sont pas concernés par cette règle — ce flux est intentionnel.
Les payloads volumineux (corps d'email) transitent via un fichier SQL temporaire plutôt que par injection directe dans la commande, pour contourner les limites de taille des arguments shell.
Interface en ligne de commande :
python3 -m <scrutation-inbox> # cycle complet
python3 -m <scrutation-inbox> --dry-run # scrutation sans insertion
python3 -m <scrutation-inbox> --max 10 # limite à 10 messages
3.2 Analyse des pièces jointes
Lorsqu'un message contient des pièces jointes, l'analyseur les extrait et les soumet à un scan antivirus avant toute classification. Le processus suit ces étapes :
- Récupération des métadonnées du message (identifiant RFC822, présence de pièces jointes) par jointure entre la table de workflow et la table des messages.
- Re-récupération du message brut via IMAP en mode lecture seule, par son identifiant
Message-ID. - Extraction des parties attachées vers un répertoire temporaire isolé, avec assainissement des noms de fichiers pour prévenir les attaques de type path traversal.
- Refus pré-scan : les fichiers dont l'extension est considérée exécutable ou dangereuse (
.exe,.scr,.bat,.cmd,.com,.vbs,.js,.jar,.msi,.dmg,.deb,.rpm,.ps1) ou dont la taille dépasse 25 Mo sont immédiatement refusés avec le verdictblocked. - Scan antivirus de chaque fichier via la façade de l'agent sécurité (voir §5).
- Le verdict global
attachments_safe = 1n'est accordé que si au moins un fichier a été scanné, et qu'aucun n'est suspect, infecté ou refusé. - En cas d'échec,
attachments_safe = 0, le statut passe àfailed, et les fichiers sont déplacés en quarantaine dans un répertoire isolé. - Le verdict de scan (JSONB) et un événement d'audit sont persistés en base.
3.3 Classification par IA
La classification analyse le contenu de l'email et lui attribue un intent déterminant son traitement downstream.
Garde-fou scan-first (bloquant)
Si un email contient des pièces jointes dont le verdict de scan est présent et indique que les pièces ne sont pas sûres, la classification est refusée et un événement d'audit classify_blocked_unsafe_attachments est émis. Tolérance : si le verdict est absent (pièces jointes pas encore scannées ou message sans pièce jointe), la classification peut continuer.
Protection contre l'injection de prompt
Le corps de l'email est encadré par des marqueurs délimiteurs explicites dans le prompt soumis au modèle de langage, accompagnés d'un avertissement indiquant que le contenu est une donnée email et non une instruction. Le corps est tronqué à 8 000 caractères. La sortie du modèle est contrainte par une liste d'intents valides — même si le modèle hallucine une valeur hors liste, la validation la rejette. Le scope et le destinataire ne sont jamais dérivés du corps de l'email.
Court-circuit pré-LLM — re-forward interne
Si le fondateur retransmet vers la boîte Atlas un récapitulatif dont l'origine est elle-même Atlas (détecté par l'adresse expéditrice et le préfixe du sujet), l'intent est forcé à noise avec une confiance de 1,0, avant tout appel au modèle de langage. Ce court-circuit évite le déclenchement coûteux d'un spawn sur un email auto-généré par le pipeline lui-même.
OCR local — enrichissement du prompt
Après validation du verdict antivirus (pièces jointes sûres), si des fichiers image sont présents, un moteur OCR local (Tesseract) est déclenché. Le texte extrait est ajouté au contexte du prompt entre des marqueurs dédiés. Deux règles de doctrine s'appliquent : (1) le texte OCR n'est jamais persisté en base (données personnelles potentielles — usage mémoire vive uniquement) ; (2) l'OCR est déclenché après le gate antivirus, pour ne jamais lire le contenu d'une pièce jointe non encore vérifiée.
Appel au modèle de langage
La configuration de routage IA détermine le fournisseur et le modèle à utiliser (par défaut : un modèle léger d'un fournisseur IA tiers). L'appel est effectué avec un timeout de 30 secondes et un retour JSON structuré. La clé API du fournisseur IA est injectée depuis la configuration sécurisée de l'environnement — elle n'apparaît jamais en clair dans le code.
Validation de la sortie
La sortie du modèle est validée : l'intent doit appartenir à la liste des valeurs autorisées, et le score de confiance doit être compris entre 0 et 1. Si la confiance est inférieure à 0,7, un événement d'audit classification_uncertain est émis. Un floor automatique force l'intent à noise si la confiance est inférieure à 0,5, indépendamment du seuil d'audit.
Garde-fous post-LLM — deux règles de surcharge
- Faux noise fondateur avec pièces jointes : si le modèle retourne
noisemais que l'email est un re-forward du fondateur contenant des pièces jointes, l'intent est requalifié enquestion— le contenu actionnable se trouve probablement dans une image que le modèle léger ne peut pas analyser directement (l'OCR local pallie partiellement ce point). Événement d'audit :noise_override_founder_attachment. - Client existant rerouté : si l'intent est
negociationouconseilmais que l'adresse expéditrice correspond à un tenant actif en base (matching sur les domaines email des clients), l'intent est requalifié enrunscopé sur ce tenant. Les prospects non encore clients conservent leur intentnegociation. Événement d'audit :existing_client_guard.
Persistance
La ligne de workflow Atlas est mise à jour avec l'intent classifié, le score de confiance, le nom du modèle, le raisonnement textuel, et le statut passe à classified.
Intents et matérialisation downstream
Six intents sont reconnus : run, chantier, question, noise, negociation, conseil. Chaque intent matérialise au plus une ligne dans une table dédiée, de façon idempotente :
| Intent | Action | Table cible | Déduplication | Lien tracé |
|---|---|---|---|---|
run |
UPSERT | Table des runs (source='atlas-inbox', trigger='email', scope optionnel si rerouté par le garde-fou client) |
Contrainte unique sur la paire (type de référence, id de référence) filtrée sur la source | Colonne id_run_linked |
question |
UPSERT | Table des questions (status='pending') |
Idem | Colonne id_question_linked |
negociation |
INSERT | Table des négociations (status='nouveau') |
Guard sur la colonne de lien non nulle | Colonne id_negociation_linked |
conseil |
INSERT | Table des conseils (status='received') |
Guard sur la colonne de lien non nulle | Colonne id_conseil_linked |
chantier |
Draft via CLI --link-chantier |
Table des chantiers (status='draft') |
— | Colonne id_chantier_linked |
noise |
No-op total | — | — | — |
Pour les intents negociation et conseil, la société est dérivée du domaine de l'adresse expéditrice, sauf pour les domaines génériques (messageries grand public telles que Gmail, Outlook, Laposte, etc.) pour lesquels aucune société n'est inférée.
Intent chantier — matérialisation manuelle : contrairement aux autres intents, la création d'un chantier n'est pas automatique à l'issue de la classification. Le statut reste classified avec intent='chantier'. La création du chantier en draft est déclenchée manuellement via la CLI, après revue humaine. Ce choix est volontaire (doctrine des procédures en 7 étapes).
Interface en ligne de commande du classifieur :
python3 -m <classifieur-atlas> --id-atlas-email 42
python3 -m <classifieur-atlas> --all-pending
python3 -m <classifieur-atlas> --reclassify <id>
python3 -m <classifieur-atlas> --link-chantier <id>
python3 -m <classifieur-atlas> --report-uncertain
3.5 Garde-fou façade email
Ce garde-fou est un mécanisme technique — pas une simple convention — qui bloque structurellement tout accès email en dehors de la façade autorisée. Il s'agit d'un hook branché sur l'agent de développement, interceptant chaque commande shell avant exécution.
Fonctionnement :
- Seules les commandes faisant appel aux scripts de façade autorisés (client email, synchronisation IMAP, accès direct IMAP, boîte SASU) passent sans blocage.
- Toute commande contenant des appels directs aux bibliothèques email bas niveau (
smtplib,imaplib,SMTP_SSL,IMAP4_SSL,MIMEText,MIMEMultipart, etc.) ou aux façades parallèles bannies (send_immediate,send_email_quick) est bloquée avec un message d'erreur explicite retourné à l'agent.
Ce garde-fou couvre notamment les tentatives d'envoi inline en Python ou via un script custom hors whitelist. Il a été instauré à la suite d'un incident de production : une fonction d'envoi direct bypassait l'IMAP, rendant les emails envoyés invisibles dans le dossier Envoyés et corrompant l'encodage des caractères spéciaux lors du passage par le shell. Le hook empêche structurellement la répétition de ce scénario.
3.4 Schéma de workflow Atlas
La table de workflow Atlas centralise l'état de traitement de chaque email. Les colonnes clés sont :
- Identifiants : clé primaire de la ligne Atlas, clé étrangère vers la table des messages email.
- Classification : intent classifié, score de confiance (numérique), nom du modèle, raisonnement textuel, statut global.
- Pièces jointes : verdict de scan (JSONB), indicateur booléen de sécurité.
- Liens downstream : colonnes de lien vers les tables chantier, run, question, négociation, conseil.
- Ship-via-email : jeton d'autorisation, horodatages d'émission et de consommation, identifiant du consommateur, identifiant du message de référence (
In-Reply-To). - Provenance : colonnes
source_typeetsource_metadata(JSONB) traçant l'origine de la ligne — poll IMAP automatique ou re-forward manuel — permettant notamment au court-circuit anti-boucle de distinguer un email légitime d'un récapitulatif auto-généré retransmis.
Audit forensic : une table d'audit append-only enregistre chaque événement significatif (type d'action, acteur, détails JSON, horodatage). Les détails JSON ne contiennent jamais de données personnelles en clair — uniquement des hash, identifiants internes, codes d'erreur et métriques.
Spawn et ship sont deux automates distincts :
- L'automate de spawn (cron toutes les 5 minutes, avec décalage de 2 minutes) déclenche un agent headless pour les intents
chantieretquestionuniquement — jamais pournoiseni pourrun. Lesrunsont matérialisés dans la table dédiée mais ne déclenchent pas de spawn via ce cron (traitement downstream hors périmètre).- L'automate de ship gère le pipeline d'autorisation par email : il génère un jeton à usage unique, envoie l'email d'autorisation via la façade, puis applique un triple contrôle anti-usurpation (liste blanche expéditeur + DKIM +
In-Reply-To+ consommation atomique du jeton) avant d'exécuter la commande./ship.Ces deux automates sont hors périmètre de cette section.
Extraction manuelle de pièces jointes
Un outil en ligne de commande permet d'extraire les pièces jointes d'un email par son identifiant de message (et non par position dans la boîte) :
node <outil-extraction-pj> <message-id> [dossier-de-sortie]
L'outil charge la configuration depuis les fichiers d'environnement (hôte IMAP, identifiant de connexion, mot de passe IMAP). Le dossier de sortie par défaut est un répertoire temporaire nommé d'après le préfixe du message. Le parseur MIME est multipart-aware et effectue une écriture binaire pour préserver l'intégrité des octets.
Doctrine : les fichiers extraits ne doivent pas être ouverts ni analysés avant que le scan antivirus ait rendu un verdict
clean(cf. section suivante). Le skill de recherche en boîte de réception liste les pièces jointes sans les extraire, afin de respecter ce principe de scan-first.
Façade d'envoi email
Une façade unique centralise tout envoi d'email professionnel. L'utilisation directe de la bibliothèque d'envoi SMTP standard est interdite en dehors de cette façade — toute communication sortante doit transiter par elle.
Cycle brouillon → validation → envoi
La façade implémente un flux en trois temps :
- Rédaction du brouillon (
--draft) : le brouillon est écrit sur disque au format JSON avec le statutdraft. Une copie de validation est automatiquement envoyée à l'opérateur (jamais au destinataire final). - Revue : l'opérateur liste les brouillons en attente (
--list), visualise un brouillon complet (--preview --draft-id <réf>) et peut redemander une copie de validation (--validate --draft-id <réf>). - Envoi réel (
--send --draft-id <réf>) : l'email part au destinataire et une copie est archivée dans le dossier « Envoyés » IMAP. La référence de brouillon (et non un numéro de position) doit être utilisée pour cibler un brouillon — un numéro de position change dès qu'un brouillon antérieur est expédié.
Arguments principaux
| Flag | Effet |
|---|---|
--to / --cc (répétable ou CSV) |
Destinataires |
--subject |
Objet de l'email |
--body |
Corps inline |
--body-file |
Corps depuis un fichier UTF-8 (recommandé, évite l'inline) |
--markdown |
Corps en Markdown — converti automatiquement en HTML avant envoi |
--html |
Corps déjà en HTML |
--attachment (répétable) |
Pièces jointes — échec immédiat si un fichier est absent |
--draft-id <réf> |
Cible un brouillon existant pour envoi, prévisualisation ou revalidation |
La commande --draft exige --to, --subject et au moins un corps (--body ou --body-file).
Format imposé : l'option --plain / --no-markdown est refusée. La façade convertit systématiquement le corps en HTML avant l'envoi, ce qui garantit que la signature canonique HTML (portant le logo) est toujours jointe.
Copie de validation automatique (show-before-send)
À chaque création de brouillon, la façade envoie automatiquement une copie à l'opérateur avec le sujet préfixé [À VALIDER → <destinataire>]. L'email client ne part pas à ce stade — un message console l'indique explicitement. Un garde-fou anti-boucle empêche l'envoi d'une copie de validation si le destinataire est déjà la boîte de validation.
C'est l'implémentation de la doctrine show-before-send : les étapes brouillon et envoi ne s'enchaînent jamais de façon automatique ; l'opérateur valide la copie reçue avant tout --send.
Avant l'affichage du brouillon, la façade interroge le moteur de tonalité afin d'afficher un rappel contextuel sur le registre à adopter pour ce destinataire. Cette consultation est en mode best-effort et ne bloque jamais le flux.
Envoi réel et archivage
L'envoi réel s'effectue via SMTP puis la façade copie le message dans le dossier « Envoyés » IMAP. La liste des noms de dossiers candidats couvre les variantes courantes des différents hébergeurs (OVH, Gmail, etc.). La copie n'est déclarée réussie qu'après un statut OK explicite du serveur IMAP ; en cas d'échec sur tous les dossiers, un avertissement non bloquant est remonté — l'email SMTP a bien été envoyé. La signature HTML canonique est automatiquement ajoutée à chaque envoi.
Configuration SMTP/IMAP
Toute la configuration (hôte SMTP, port, identifiant, mot de passe SMTP, adresse expéditeur, hôte IMAP, identifiant et mot de passe IMAP) est lue depuis les variables d'environnement au démarrage. Une vérification au lancement liste les variables manquantes. Le port 465 déclenche le mode SMTP over SSL.
Scan antivirus obligatoire avant ouverture (P0)
Le moteur d'analyse antivirus est opéré par l'agent Mitnick. Aucune pièce jointe extraite n'est ouverte ni analysée avant que le verdict soit clean. L'analyse en cloud (ex. VirusTotal) est exclue pour des raisons de confidentialité des données client. Tout contournement est interdit.
Couches d'analyse
- Refus brut des exécutables : toute pièce jointe dont l'extension correspond à un type exécutable connu (
.exe,.dll,.so,.bat,.cmd,.ps1,.sh,.vbs,.js,.jar,.msi) est immédiatement classifiéeinfected. - Analyse ClamAV : le binaire ClamAV est invoqué en mode non-interactif. Un exit code 1 (signature trouvée) entraîne un verdict
infectedet un arrêt immédiat de la chaîne. Si ClamAV n'est pas installé, le moteur remonte une erreur descriptive. - Heuristiques PDF : une analyse par expressions régulières sur les 4 premiers mégaoctets du fichier détecte les directives PDF à risque (JavaScript embarqué, actions automatiques, formulaires de soumission, médias enrichis…) et produit un verdict
suspicious. - Macros Office : les formats Office autorisant les macros sont contrôlés via l'outil
olevba(best-effort) à la recherche de déclencheurs automatiques et d'appels système — verdictsuspicioussi détectés. - Archives : les archives compressées ne font pas l'objet d'une descente récursive et sont classifiées
suspiciouspar défaut, nécessitant une revue manuelle.
Marqueur de résultat propre
Lorsque le verdict final est clean, le moteur écrit un fichier de marqueur JSON atomique (contenant l'empreinte SHA-256 et l'horodatage) dans un sous-répertoire dédié à côté de la pièce jointe. Les processus en aval opèrent en mode fail-close sur l'absence de ce marqueur : pas de marqueur = pas encore scanné = bloquer. L'écriture est atomique ; si elle échoue, le verdict de l'appel reste correct — seule la persistance du marqueur est optionnelle.
Verdicts
| Verdict | Signification | Code de sortie CLI |
|---|---|---|
clean |
Aucun signal — ouverture autorisée | 0 |
suspicious |
Signaux statiques — traitement en sandbox requis | 1 |
infected |
Correspondance ClamAV ou exécutable — NE PAS OUVRIR | 2 |
error |
Scan impossible (ClamAV indisponible, fichier absent…) | 3 |
Le code de sortie CLI reflète le pire verdict parmi tous les fichiers analysés. Le moteur de scan peut être invoqué en ligne de commande avec une option --json pour une sortie structurée, ou sur un répertoire entier.
Dans le pipeline de traitement des emails entrants, le moteur de scan fixe un indicateur de sécurité sur chaque pièce jointe. Le classifieur refuse de s'exécuter si cet indicateur signale des pièces jointes non sûres.
Doctrine — zéro communication client directe (P0)
Le système ne parle jamais directement aux clients. Toute communication externe transite par l'opérateur humain.
Garde-fous concrets :
- La façade d'envoi n'envoie jamais au client lors de la création d'un brouillon. Elle écrit le brouillon et envoie uniquement une copie de validation à l'opérateur. Seul un
--send --draft-id <réf>déclenché après validation explicite livre le message au destinataire final. - Un hook de garde bloque toute tentative d'envoi SMTP ou IMAP direct depuis un script quelconque, en dehors de la liste blanche des composants autorisés. Ce garde-fou technique rend la façade non contournable.
- Le classifieur de la boîte de réception ne dérive jamais le destinataire ni le périmètre à partir du corps d'un email (protection contre l'injection de prompt). Les brouillons de réponse restent en statut
pendingpour validation humaine. - Le vouvoiement est systématique pour tout contact externe. Les prises de rendez-vous se font via le lien Calendly, jamais avec un créneau inscrit en dur dans un email.
Tâches planifiées actives
| Fréquence | Rôle | Pipeline |
|---|---|---|
| Toutes les minutes | Synchronisation de la boîte de réception | Collecteur IMAP → table de stockage des emails entrants (seul collecteur actif) |
| Toutes les minutes | Interrogation du pipeline de traitement Atlas | Lecture IMAP → table de traitement Atlas |
| Toutes les 15 minutes | Surveillance des verrous Atlas orphelins | Watchdog de verrous |
| Toutes les 5 minutes (avec délai de démarrage) | Spawn de l'agent IA en mode autonome | Traitement des intents chantier et question uniquement — liste blanche figée |
Désactivée : l'ancienne tâche planifiée qui déclenchait la synchronisation via l'API web de la façade Nuxt est commentée depuis le 23 mai 2026. Elle saturait la boucle d'événements du serveur applicatif lors des timeouts IMAP/base de données. Elle est remplacée par le collecteur Python externe mentionné ci-dessus. Ne pas la réactiver sans avoir résolu la saturation de la boucle d'événements.
Pièges connus
- Deux espaces de stockage distincts : la table de collecte IMAP (clé : identifiant IMAP) et les tables du pipeline Atlas (clé : compte + identifiant de message) sont deux mondes séparés. Aucune jointure ne les relie.
- Flag
\Seenposé après insertion : un crash entre la récupération et l'insertion laisse l'email non lu, donc re-traité au cycle suivant. L'idempotence est assurée par une contrainteON CONFLICTen base. PEEKobligatoire lors de la récupération Atlas, pour ne pas marquer prématurément le message comme lu.--markdownrecommandé pour tout email client. Toujours--body-fileplutôt que--bodyinline.- Scan-first non négociable : ouvrir une pièce jointe avant le verdict
cleanconstitue une dette d'architecture de niveau P0. - Divergence docstring / réalité : le commentaire du collecteur Atlas mentionne « 5 minutes » mais la tâche planifiée tourne toutes les minutes.
- Anti-boucle email : l'adresse du fondateur transfère volontairement les messages vers la boîte Atlas. Seule la boîte Atlas est filtrée comme auto-émetteur. Ne pas ajouter l'adresse du fondateur dans la liste des auto-émetteurs.
- Intent
chantier: le classifieur ne crée pas de chantier automatiquement. C'est une action CLI explicite (--link-chantier). Cela évite la création de chantiers orphelins sans lettre de mission.