Chapitres

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.

  1. 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.
  2. 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 :

  1. Acquisition d'un verrou non-bloquant — si le verrou est déjà tenu, le cycle est silencieusement ignoré.
  2. 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 \Seen après traitement.
  3. Recherche des messages non lus (UNSEEN), plafonnée à 50 messages par cycle (configurable).
  4. Récupération du corps de chaque message avec BODY.PEEK[] — le mode PEEK évite de marquer automatiquement le message comme lu ; le drapeau \Seen n'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.
  5. 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.
  6. Insertion d'une ligne de suivi dans la table de workflow Atlas, avec le statut initial received, également idempotente.
  7. 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.
  8. É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 :

  1. 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.
  2. Re-récupération du message brut via IMAP en mode lecture seule, par son identifiant Message-ID.
  3. 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.
  4. 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 verdict blocked.
  5. Scan antivirus de chaque fichier via la façade de l'agent sécurité (voir §5).
  6. Le verdict global attachments_safe = 1 n'est accordé que si au moins un fichier a été scanné, et qu'aucun n'est suspect, infecté ou refusé.
  7. 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é.
  8. 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

  1. Faux noise fondateur avec pièces jointes : si le modèle retourne noise mais que l'email est un re-forward du fondateur contenant des pièces jointes, l'intent est requalifié en question — 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.
  2. Client existant rerouté : si l'intent est negociation ou conseil mais que l'adresse expéditrice correspond à un tenant actif en base (matching sur les domaines email des clients), l'intent est requalifié en run scopé sur ce tenant. Les prospects non encore clients conservent leur intent negociation. É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_type et source_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 chantier et question uniquement — jamais pour noise ni pour run. Les run sont 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 :

  1. Rédaction du brouillon (--draft) : le brouillon est écrit sur disque au format JSON avec le statut draft. Une copie de validation est automatiquement envoyée à l'opérateur (jamais au destinataire final).
  2. 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>).
  3. 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

  1. 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ée infected.
  2. Analyse ClamAV : le binaire ClamAV est invoqué en mode non-interactif. Un exit code 1 (signature trouvée) entraîne un verdict infected et un arrêt immédiat de la chaîne. Si ClamAV n'est pas installé, le moteur remonte une erreur descriptive.
  3. 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.
  4. 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 — verdict suspicious si détectés.
  5. Archives : les archives compressées ne font pas l'objet d'une descente récursive et sont classifiées suspicious par 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 pending pour 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 \Seen posé 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 contrainte ON CONFLICT en base.
  • PEEK obligatoire lors de la récupération Atlas, pour ne pas marquer prématurément le message comme lu.
  • --markdown recommandé pour tout email client. Toujours --body-file plutôt que --body inline.
  • Scan-first non négociable : ouvrir une pièce jointe avant le verdict clean constitue 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.