Règles de nettoyage

Pipeline déterministe appliqué à chaque email ingéré. Les règles sont exécutées dans l'ordre ci-dessous. Toute modification de leur comportement s'accompagne d'un bump de CLEANING_VERSION— ce qui permet de rejouer le nettoyage sur l'ensemble des emails bruts déjà en base et de comparer les deux versions.

Version active :v1.0.0
Ordre d'exécution
  1. R-01

    Sélection de la partie MIME

    étape 1 / 10

    On choisit d'abord la partie texte de l'email à nettoyer. Si la version `text/plain` existe et fait au moins 200 caractères, on la prend telle quelle (chemin minimal). Sinon, on bascule sur la version `text/html` — ce qui active les règles suivantes de suppression HTML.

    Seuil de bascule
    • text/plain ≥ 200 caractères → chemin texte
    • sinon → chemin HTML
  2. R-02

    Décodage

    étape 2 / 10

    Les emails reçus peuvent être encodés en quoted-printable ou base64, et contiennent des entités HTML (`&`, `'`, etc.). Cette étape est effectuée en amont par `mailparser` au moment de l'ingestion : le `html_body` et le `text_body` stockés en base sont déjà décodés en UTF-8 et prêts à consommer. Aucune passe supplémentaire n'est nécessaire ici.

    Délégué à

    mailparser (étape d'ingestion, pas de nettoyage).

  3. R-03

    Suppression structurelle HTML

    étape 3 / 10

    On enlève les balises qui n'ont pas de sens dans un texte de lecture : scripts, styles, métadonnées, liens CSS, commentaires HTML. Cela élimine aussi les éventuels JS embarqués qui pourraient s'activer si l'email était rendu sans sandbox.

    Balises supprimées
    • <script>
    • <style>
    • <head>
    • <meta>
    • <link>
    • <noscript>
    • commentaires HTML (<!-- ... -->)
  4. R-04

    Suppression des éléments cachés

    étape 4 / 10

    Beaucoup de newsletters insèrent des éléments invisibles (pixels de tracking, contenu de précédent A/B test). On les détecte via leur style inline ou leurs dimensions 1×1 et on les retire.

    Critères de détection (style inline)
    • display:none
    • visibility:hidden
    • opacity:0
    • dimensions explicites width="1" height="1"
  5. R-05

    Suppression des images de tracking

    étape 5 / 10

    Les expéditeurs mesurent l'ouverture des emails via des pixels invisibles hébergés sur des domaines dédiés (Mailchimp, SendGrid, Substack…). On supprime tout `<img>` dont le `src` contient un pattern connu — liste ci-dessous, versionnée avec `CLEANING_VERSION`.

    Patterns traqués
    • list-manage.com
    • list-manage1.com
    • mailchimp.com/track
    • sendgrid.net/wf/open
    • sendgrid.net/tracking
    • beehiiv.net/open
    • beehiv.net/open
    • substack.com/pixel
    • substackcdn.com/pixel
    • email.substack.com/tracking
    • convertkit.com/open
    • kit.com/open
    • customer.io/e/o/
    • /open.gif
    • /pixel.gif
    • /tracking.gif

    Match par sous-chaîne (case-sensitive). Ajouts → bump de CLEANING_VERSION.

  6. R-06

    Nettoyage des URLs

    étape 6 / 10

    Les liens des newsletters sont truffés de paramètres de tracking (utm_*, fbclid, mc_cid…). On les supprime sans toucher au reste de l'URL : le lien reste fonctionnel, mais on ne balance plus l'identifiant d'ouverture / de campagne à la page de destination. Appliqué sur les `href` des `<a>` **et** sur les URLs trouvées dans le corps texte.

    Paramètres supprimés
    • utm_source
    • utm_medium
    • utm_campaign
    • utm_term
    • utm_content
    • utm_id
    • mc_cid
    • mc_eid
    • fbclid
    • gclid
    • gclsrc
    • dclid
    • msclkid
    • _hsenc
    • _hsmi
    • __hstc
    • __hssc
    • __hsfp
  7. R-07

    Conversion en Markdown

    étape 7 / 10

    Le HTML nettoyé est converti en Markdown via `turndown` + plugin GFM. On préserve la structure sémantique (titres h1–h6, paragraphes, listes, liens, citations, emphases, tables) tout en écartant les styles. C'est le format le plus digeste pour un LLM en aval.

    Options turndown
    • headingStyle: atx (# titres)
    • bulletListMarker: - (listes)
    • codeBlockStyle: fenced (```)
    • plugin GFM activé (tables, strikethrough, task lists)
  8. R-08

    Extraction du footer

    étape 8 / 10

    Le bas de la plupart des newsletters contient un bloc boilerplate : lien unsubscribe, gestion des préférences, adresse postale de l'expéditeur. Ce n'est pas du bruit exploitable pour la synthèse, mais on ne le supprime pas : on le détecte par regex et on le stocke séparément dans `footer_extracted`. Le `cleaned_markdown` ne l'inclut plus.

    Patterns de détection
    • unsubscribe
    • manage preferences / manage your preferences
    • view in browser / view this email in
    • se désabonner

    Détection best-effort. Les faux négatifs (footer non reconnu) laissent le footer dans le corps — c'est accepté pour la v1.

  9. R-09

    Normalisation des espaces

    étape 9 / 10

    Cosmétique final : on trim les espaces en fin de ligne, et on collapse les enchaînements de plus de 2 retours à la ligne en 2 retours. Résultat : un Markdown propre, sans blocs de blancs inutiles qui gonfleraient la consommation de tokens en aval.

  10. R-10

    Versionnement

    étape 10 / 10

    Ce n'est pas une transformation : c'est la discipline qui permet de tout rejouer. Chaque résultat de nettoyage est tagué avec la valeur de `CLEANING_VERSION` au moment du run. Si on modifie une règle, on bump la version, on relance le pipeline sur `emails_raw`, et on obtient une nouvelle série de `emails_cleaned` comparable à la précédente sans avoir perdu aucune donnée brute.

    Contrat
    • Même (raw_email_id, cleaning_version) → même sortie, toujours (idempotence)
    • Le brut (emails_raw) n'est jamais modifié par le nettoyage
    • En cas d'erreur : row emails_cleaned avec status='failed' + error_message (pas de fail silencieux)

Invariants

  • Idempotence. Même entrée + même cleaning_version donnent toujours la même sortie.
  • Le brut est immuable. La table emails_rawn'est jamais modifiée par le nettoyage — on peut toujours repartir du MIME original.
  • Fail-loud.En cas d'erreur pendant le nettoyage, une row emails_cleaned avec status='failed'est créée avec le message d'erreur — jamais d'échec silencieux.