AnnonceNouveautés34 lectures
Un rebond dur chez un aérostier, et une veille qui mesurait tout sauf ce qui charge
LeTock.fréditeur 10 pts ·
Publié le 25 septembre 2026.
Nous avons alerté un site parce qu’un mot accentué avait disparu. Il était là.
Trouvé en cherchant l’origine des noms de tâches abîmés. Et la piste qui y menait était fausse, ce qui vaut d’être dit : un incident du 19 annonçait « la page répond 200 mais “D□couvre ce qui te revient” n’y est plus », et il était tentant d’y voir ce défaut-ci. C’en était un autre — le texte attendu du contrôle portait lui-même le caractère perdu, et ce trou-là avait déjà été bouché le 22 après le signalement d’un client. Le défaut réel a donc été vérifié à part, directement : un corps latin-1 servi avec charset=iso-8859-1 ressort en « D□couvre ». La lecture du corps d’une réponse décode toujours en UTF-8 et ignore le jeu de caractères annoncé — c’est la spécification, et c’est un piège. Une page servie en latin-1 nous arrivait avec un caractère perdu à la place de chaque lettre accentuée, donc un contrôle de contenu portant sur un mot accentué y échouait pour toujours, quoi que fasse le propriétaire du site. Une fausse alerte coûte plus cher qu’une alerte absente : elle apprend à ne plus regarder le rouge. Les sondes lisent désormais le jeu annoncé dans l’en-tête, puis dans le document, et vérifient le résultat — parce qu’un serveur qui annonce UTF-8 en servant du latin-1 est un classique, et qu’un caractère perdu prouve à lui seul que la déclaration mentait. Le garde a d’ailleurs été pris en défaut par son propre test : windows-1252 ne rate jamais, il traduit n’importe quelle suite d’octets, même une image. Ce sont les caractères de contrôle qu’il produit alors qui font la preuve, pas l’absence d’erreur.
Huit de nos tâches portaient des noms cassés depuis neuf jours, et rien ne s’en plaignait.
Signalé depuis l’écran : « �a me revient � d�couverte hebdomadaire ». Le � n’est pas un caractère, c’est une perte : il apparaît quand des octets ont été décodés avec le mauvais jeu de caractères, et ce qu’ils portaient a déjà disparu — on ne remonte pas de « d�couverte » à « découverte ». Vérifié octet par octet : la base contenait bien EF BF BD, donc la casse datait de l’enregistrement, pas de l’affichage. Chaque lettre accentuée était devenue exactement un caractère perdu, signature d’un texte écrit dans l’encodage par défaut de Windows puis relu comme de l’UTF-8 — par une commande lancée à la main le 16, une fois de plus hors du dépôt. Le nom d’une tâche refuse désormais ces caractères, à la création comme à la modification : le garde posé sur un seul des deux chemins laisse la porte ouverte, et c’est le test qui l’a dit. Refuser coûte un message d’erreur à celui qui envoie, pendant qu’il a encore le texte intact sous la main ; accepter grave dans la base un nom que plus personne ne pourra réparer.
Deux journées de correctifs n’avaient été annoncées nulle part.
Le journal des versions est lu à quatre endroits, et le forum en est un : une version publiée doit y devenir un fil auquel on peut répondre, parce qu’un journal ne se discute pas et qu’un fil, si. C’était l’intention — en pratique, chaque annonce était un script jetable de plus, et le jour où personne n’en a écrit un, rien n’a été publié. Mesuré : trente-huit fils, dont le dernier datait de l’avant-veille, pour deux journées publiées depuis — dont la correction d’un classificateur qui faisait supprimer des adresses parfaitement valides. Ceux qui en subissaient l’effet n’avaient nulle part où le lire. Une habitude qui tient tant que quelqu’un y pense n’est pas une habitude, c’est une chance. La mise en ligne publie désormais elle-même ce qui n’a pas encore de fil, en reprenant le texte du journal plutôt qu’un résumé — deux versions du même texte, une seule serait relue. Les ruptures d’API passent en tête, parce que c’est la seule information qui arrête du code qui tourne chez quelqu’un.
Un changement posé à la main se dit maintenant à celui qui le subit.
Une exception de quota se pose d’une commande, et la personne concernée ne voit rien : ni sur son écran, qui continue d’afficher son forfait, ni dans sa boîte. Un compte dont les règles changent sans que personne ne le dise est un compte dont on ne peut plus rien croire à l’écran. Prévenir fait donc partie du geste, et pas de la bonne volonté de celui qui l’a fait. Le détail qui a coûté le plus : la première version de cet envoi avait été écrite directement sur le serveur, pour un seul message — et le déploiement suivant l’a effacée, puisque la mise en ligne republie l’arbre du dépôt et que ce qui n’y est pas n’existe pas. Un outil qu’on réécrit à chaque fois finit par être réécrit mal, un soir, sur une adresse réelle. Il passe par la même porte que n’importe quel client — mêmes quotas, même palier de chauffe, même journal des remises — et n’envoie rien sans qu’on le lui demande explicitement.
Le plafond du jour a refusé un message attendu, parce que la campagne avait tout consommé.
Posée le matin, mesurée l’après-midi. La chauffe par compte existe pour une seule raison : empêcher un compte inconnu d’expédier mille messages le jour de son inscription, sur une adresse partagée par tous. Elle a fait autre chose. Elle a refusé une notification personnelle — un message attendu, adressé à quelqu’un qui venait d’être inscrit — parce que la prospection froide avait déjà consommé les cent messages de la journée. Un plafond qui bloque le transactionnel pour du froid déjà parti punit le mauvais message : le tort était fait avant, et c’est le message innocent qui paie. Le plafond du compte ne s’applique donc plus aux équipes marquées nommément, qui sont l’exact contraire d’un inconnu. Ce qui protège vraiment l’adresse partagée ne bouge pas : le palier de la plateforme s’applique toujours, et c’est lui qui borne le volume réel. Une règle de sûreté qu’on ne mesure jamais contre un cas légitime finit par coûter plus cher que ce qu’elle évite.
L’illimité existe, et il n’est pas à vendre.
Quelques équipes ne comptent pas : la nôtre, et quelques proches. La façon évidente de le faire — un quatrième forfait « illimité » — est la mauvaise : la page Tarifs lit la même source que le code, et c’est précisément ce qui empêche un quota annoncé quelque part d’être appliqué ailleurs. Y ajouter un palier que personne ne peut acheter mentirait à la page pour arranger deux comptes. Ce dont il s’agit n’est pas un forfait, c’est une exception nominative : elle se pose ligne par ligne, se révoque d’une commande, et ne raconte rien au public. Un test vérifie d’ailleurs qu’aucun forfait ne s’ajoute à la vente. Elle lève les quotas de comptage — tâches, domaines, audits — et la chauffe du compte, mais pas le palier de la plateforme ni la part de courrier froid : ceux-là protègent la réputation d’une adresse d’expédition partagée par tous les domaines, et une exception sur ce terrain-là ne se ferait pas à nos dépens mais à ceux des autres. La frontière a bougé le jour même, et pour une bonne raison : voir ci-dessous.
La mesure traitait quarante prospects par passage, la collecte en rentrait quatre mille cinq cents.
Signalé depuis l’écran : « que 127 de prêt ? ». Le chiffre ne montait pas, et ce n’était pas la collecte. La mesure se faisait un domaine après l’autre, quarante par passage — deux cent quarante par jour, quoi qu’on collecte. Le stock des « mesurés et prêts » ne pouvait donc pas dépasser quelques centaines, et l’écran le montrait sans dire pourquoi. Une mesure coûte quelques requêtes DNS et parfois une connexion : bien moins qu’une visite de collecte, qui en fait cinq. Rien ne justifiait de les faire une par une. Mesuré après correction : 300 prospects en 42 secondes, soit sept par seconde. Le passage en traite désormais 1 200, c’est-à-dire 7 200 par jour — au-dessus de tout ce que la chauffe autorisera à envoyer. Le goulot repasse là où il doit être : la réputation de l’adresse d’expédition.
Deux collectes tournaient en même temps sur le même curseur — le châtiment qu’on détecte chez les autres.
Vu à la charge de la machine, passée de 0,35 à 1,63 : un passage lancé à la main pendant que le minuteur en démarrait un autre. Les deux lisent et écrivent le même curseur, prennent donc la même tranche de domaines, frappent deux fois aux mêmes portes, et le second écrase le curseur du premier. C’est exactement le « chevauchement » que ce produit détecte chez ses clients, et qu’il serait gênant de ne pas s’appliquer. Un verrou le ferme désormais — avec la précaution qui le rend utilisable : un processus tué laisse son verrou derrière lui, et le lendemain plus rien ne collecterait. Une panne silencieuse posée pour éviter un doublon serait un mauvais marché. Le verrou porte donc le numéro du processus, et celui qui le trouve vérifie que son propriétaire vit encore. La première version de cette vérification était fausse, et l’essai l’a montré : elle déclarait mort le processus 1, qui ne meurt jamais. La cause est précise — demander « peux-tu signaler ce processus » répond « permission refusée » quand il existe mais appartient à quelqu’un d’autre, et « introuvable » seulement quand il n’existe pas. Seule la seconde réponse dit la mort ; tout le reste laisse le doute, et le doute garde le verrou — un verrou repris à tort fait exactement le chevauchement qu’il existe pour empêcher.
« sort -u » tenait quatre mille domaines distincts pour des doublons.
Pour tenir des mois, la réserve doit écarter ce qu’elle connaît déjà : le tirage est aléatoire dans les 3,2 millions de .fr actifs, donc deux tirages se recouvrent. Mesuré après deux passages seulement : 410 577 lignes pour un nombre inférieur de domaines distincts. Sur des mois, la moitié de la réserve serait des domaines déjà visités — et on frapperait deux fois chez les mêmes gens, ce qui est pire que du gaspillage. Le piège était dans l’outil de mesure. sort -u est celui qu’on prend d’instinct, et il est faux ici : sous une locale UTF-8, sa comparaison suit les règles de collation de la langue, qui tiennent pour égales des chaînes différentes. Sur notre réserve, il ne voyait que 363 125 domaines là où il y en a 367 216 : s’en servir pour dédoublonner aurait supprimé 4 091 domaines parfaitement distincts, sans un mot. Le dédoublonnage se fait donc à l’égalité d’octets, avant la résolution DNS — ce qui économise autant de requêtes que de visites. Et quand un tirage rapportera moins d’un dixième de nouveaux, le passage le dira : ce ne sera pas une panne, mais le signe que le .fr est connu et qu’il faut une autre source.
La campagne mesure ce qu’elle rapporte — et refuse de classer tant qu’il n’y a rien à départager.
« Qu’elle s’améliore de jour en jour » demande de savoir ce qui marche ; savoir ce qui marche demande des résultats. L’écran montre désormais, par segment et par constat mesuré : écrits, remis, rebonds durs, réponses, désabonnés. Le cœur du module n’est pas le comptage, c’est le refus de conclure trop tôt. Classer sur peu produit un ordre — les ordres sortent toujours — et cet ordre serait du bruit. Pire : il serait suivi. On écarterait un segment pour deux rebonds sur douze envois, et on ne saurait jamais qu’on s’est trompé, puisqu’on ne lui écrirait plus. Une décision prise sur trop peu se protège elle-même de la contradiction, et c’est la pire espèce d’erreur. Sous trente messages écrits, un groupe garde donc son compte brut et pas de taux. Un défaut est tombé en mesurant la production : la première version annonçait « le meilleur : boutique, 0 % de réponses » — le volume suffisait, mais il n’y avait rien à départager. Un classement où tout le monde est à zéro n’est pas un classement, c’est un ordre alphabétique déguisé. L’écran dit maintenant ce qui est vrai : « ce n’est pas un segment qui marche moins bien qu’un autre, c’est la campagne qui n’a pas encore de retour ».
Notre écran affichait nos propres messages en un seul pavé.
Signalé depuis la boîte : un message de prospection s’affichait tout collé, paragraphes fondus, signature au milieu d’une phrase. Le texte, lui, est parfaitement replié à soixante-dix-huit colonnes — et le destinataire le voit ainsi, parce que sa messagerie rend le text/plain comme il faut. C’était notre visionneuse qui mentait sur ce qu’on avait envoyé, et c’est le pire endroit où mentir puisque c’est celui où l’on vérifie. La cause tient en une ligne absente : le rendu échappait le texte et rendait les liens cliquables, sans toucher aux sauts de ligne — or en HTML, un saut de ligne est un espace. Une balise plutôt qu’un style, parce que le corps est servi dans un cadre isolé dont la politique de sécurité peut refuser le style en ligne.
La collecte tourne désormais jour et nuit ; l’envoi reste aux heures ouvrées.
Les deux marchaient au même minuteur, calé sur la journée — ce qui revenait à ne pas collecter la moitié du temps pour une raison qui ne concerne que l’envoi. Visiter un domaine est une requête HTTP vers un serveur : personne ne la lit, personne ne s’en offusque, et l’heure n’y change rien. Envoyer, si. Un message froid reçu à trois heures du matin ressemble exactement à ce qu’il n’est pas, et l’heure d’arrivée est une des rares choses qu’un destinataire remarque avant même d’ouvrir. La collecte passe donc toutes les deux heures, vingt-quatre heures sur vingt-quatre, en priorité basse ; l’envoi garde ses six passages de 8 h 40 à 18 h 40. La réserve double sans qu’un seul message parte à une heure indéfendable.
Chaque message promettait une relance. Aucune n’est jamais partie.
Le pied de tous nos messages froids porte, en toutes lettres : « Une seule relance, puis plus rien. » La fonction qui écrit cette relance existait, avec ses tests. Elle n’était appelée nulle part, et la colonne qui l’enregistre n’a jamais été écrite. Ne pas relancer ne fait de tort à personne — mais annoncer un geste qu’on ne fait pas est exactement ce que ce produit reproche aux autres à longueur de page, et c’est renoncer à la moitié de ce qu’une campagne froide rapporte. Elle part désormais sept jours après le premier message, dans la même part froide — une relance est un message froid de plus, et le mélange que voient les serveurs d’en face ne fait pas la différence. Le domaine est remesuré avant. C’est ce qui distingue une relance d’un renvoi : si le constat a été corrigé entre-temps, le message le dit et s’arrête là. Deux défauts corrigés dans le texte en le branchant. Il annonçait « le constat ci-dessous » et n’en mettait aucun — celui qui lit cherche, ne trouve rien, et conclut à un modèle mal rempli, ce qui détruit précisément ce que la relance essaie de prouver. Et aucune des deux variantes ne portait le lien de désabonnement : une relance sans ce lien laisse le bouton « indésirable » comme seul moyen de nous faire taire, et c’est justement sur la relance qu’on l’utilise.
La prospection a son tableau de suivi — réserve, rendement, segments, constats.
Tout était déjà quelque part : le nombre de domaines visités dans un fichier, le rendement dans la sortie d’un passage, l’état des prospects dans une table, les envois dans une autre. Pour répondre à « où on en est », il fallait ouvrir quatre choses et faire une soustraction — et une information qui demande quatre gestes n’est pas consultée, sinon le jour où quelque chose a déjà mal tourné. L’écran d’exploitation montre désormais la réserve de domaines qualifiés et ce qu’il en reste, le rendement mesuré, les prospects par état, la courbe des collectés et des écrits, et deux colonnes qui disent à qui on écrit : les segments reconnus et les constats mesurés. Deux précautions. Le rendement se calcule sur ce qui a été ouvert, pas sur la réserve entière — diviser par des domaines qu’on n’a pas encore visités rendrait un chiffre qui baisse à chaque regarnissage, alors que rien n’a changé. Et la réserve vit dans des fichiers à côté de l’open data : quand ils sont illisibles, l’écran écrit « pas mesuré » plutôt que zéro, parce que « la réserve est vide » est l’inverse de « je ne sais pas ». L’écran nomme aussi ce qui n’a reçu aucun message : les domaines bien tenus, chez qui la sonde n’a rien trouvé de vrai à dire. C’est le prix de la règle, et c’est ce qui la rend défendable.
La part du palier dépensée en courrier froid devient un réglage, borné à la moitié.
Elle était fixée au quart, en dur. La nuance qui a fait la changer : elle porte sur le palier, pas sur le trafic réel. Une vraie journée le montre — cent dix-neuf remises dont cinquante froides font déjà 42 % de froid, là où la règle disait vingt-cinq. Les serveurs d’en face ne voient pas notre palier ; ils voient le mélange. Le réglage existe parce que l’arbitrage n’est pas technique : sous-utiliser le palier ralentit la chauffe de l’adresse, le sur-utiliser en froid abîme sa réputation. Le défaut reste le quart, qui est le choix prudent ; l’élever se relit dans un fichier plutôt que dans un commit. Borné à la moitié : au-delà, la campagne froide deviendrait le courrier principal de la plateforme, ce qu’aucun réglage ne devrait permettre sans qu’on réécrive la raison d’être de ce plafond. Le filtre DNS de la collecte se règle aussi : vingt résolutions de front tenaient pour vingt mille candidats, pas pour quatre cent mille — et ce n’est pas la politesse qui borne là, une résolution est un paquet UDP vers un résolveur public, pas une visite chez quelqu’un.
La campagne s’est arrêtée deux heures — et c’est notre propre produit qui l’a dit.
En scindant le frein en deux mesures — les plaintes sur la plateforme, les rebonds sur notre liste — l’appel dans le script d’envoi n’a pas suivi. Il passait encore l’ancienne forme, et TypeScript n’a rien dit : le script est un .mjs, donc hors du typage. Les passages de 14 h 40 et 16 h 40 se sont arrêtés sur Cannot read properties of undefined. Ce qui compte est la suite : personne ne regardait, et pourtant la panne s’est annoncée toute seule. La tâche surveillée dans LeTock a enregistré une exécution missed à l’heure où le battement aurait dû arriver, et l’écran l’a portée en retard. Le produit a attrapé sa propre campagne. Le correctif ne se contente pas de la ligne : la requête et la décision vivent maintenant ensemble dans un module vérifié, et le script n’appelle plus qu’une fonction sans argument. Tant qu’un appel traverse la frontière du typage, une signature n’est pas un contrat mais une convention — et une convention se rompt en silence.
Onze chiffres de la notice sont maintenant tenus par un test — dont six qui sont des contrats d’API.
Après les paliers, le balayage : « 50 destinataires au plus », « 200 caractères », « 200 Ko », « 10 pièces jointes, 10 Mo bruts », « cinq crochets », « 30 secondes de délai ». Tous étaient retapés à la main, tous étaient justes, et aucun n’était tenu. Ceux-là ne décrivent pas un comportement interne : ils disent à du code client ce qu’il a le droit d’envoyer. Une limite fausse dans la notice fait écrire une boucle qui découpe par cinquante quand le serveur en accepte vingt — et l’erreur se découvre en production, chez quelqu’un d’autre, sur un message qui ne part pas. Chaque garde a été mise en défaut exprès avant d’être conservée : sans ça, « c’est gardé » est une croyance.
Les chiffres de la notice sont désormais ceux du code — un test l’exige.
« Ça sera toujours vrai ? » — la bonne question, et la réponse était non. La notice porte des tableaux de paliers retapés à la main en markdown. Ils étaient justes le jour où on les a écrits, et ils le seraient restés jusqu’au jour où quelqu’un aurait changé une constante — sans que rien ne le dise. La notice aurait continué de s’afficher, parfaitement mise en page, en mentant. C’est la panne que ce produit vend, appliquée à sa propre documentation : un client qui lit « 50 le premier jour » et qui se fait refuser à 30 ne conclut pas que la notice a vieilli — il conclut que le produit est cassé, et il a raison. Un test compare maintenant chaque tableau de la notice aux constantes du code : les paliers de l’adresse, ceux du compte, ceux du mérite, et jusqu’aux bornes d’âge écrites dans les intitulés. Il a été mis en défaut exprès avant d’être gardé. Pourquoi un test plutôt qu’une génération : engendrer les tableaux supprimerait le risque, mais aussi la prose autour — or ces tableaux sont commentés, nuancés, et ces nuances sont ce qui les rend utiles. On garde l’écriture à la main, et on interdit qu’elle diverge.
Un compte créé ce matin pouvait expedier mille messages dans la journée.
« Comment on se protège si un nouveau client arrive et envoie avec de mauvaises intentions tout de suite ? » — la question a trouvé un vrai trou. Il fallait déjà prouver la possession du domaine (DNS ou fichier), publier DKIM et SPF, et passer le contrôle de forme ; mais une fois ces cinq minutes passées, rien ne distinguait un compte de trois heures d’un compte de trois mois. Or l’adresse d’expédition est commune à toute la plateforme : un seul compte malveillant, une seule journée, et c’est la remise de tous les clients qui s’effondre — y compris celle de ceux qui n’ont rien envoyé. Le dégât ne se répare pas en fermant le compte. Un compte chauffe donc lui aussi : 50 messages le premier jour, puis un palier par semaine. Pas une validation à la main — elle dort la nuit, elle dort le week-end, et elle transforme l’inscription en demande d’autorisation. Le mérite accélère : mille remises acceptées en disent plus qu’une semaine de calendrier passée à ne rien faire, donc l’âge sert de plancher à l’inconnu et le volume prouvé ouvre les paliers de celui qu’on connaît.
Et la sanction sur faute a été retirée avant d’être livrée — elle coupait les factures d’un client.
La première version faisait redescendre d’un palier en cas de plaintes ou d’adresses inexistantes. Mesuré sur la production avant de publier : notre propre compte, 7 % d’adresses inexistantes sur sept jours — une faute réelle, venue de la campagne froide — serait retombé à cinquante messages par jour alors qu’il en avait déjà remis cinquante-sept. Le courrier transactionnel de deux clients de cette équipe se serait arrêté dans la minute, pour une faute qui n’était pas la leur. Une même équipe mélange du courrier attendu — factures, liens de connexion — et du courrier froid ; un plafond ne sait pas les distinguer, et couper les deux pour punir l’un est hors de proportion. La chauffe garde donc le seul rôle pour lequel elle existe : empêcher un compte inconnu d’expédier mille messages le jour de son inscription. Le ralentissement sur faute est assuré ailleurs, par des mécanismes qui savent de quoi ils parlent — l’alerte « trop de rebonds » qui part au client, et le frein de la campagne froide qui lit sa propre liste. La faute reste écrite dans le verdict : un plafond qui ne dit pas ce qu’il a vu laisserait croire que personne ne regarde.
Un domaine vérifié, tout vert — et qui n’existe pas pour le DNS.
Vingt-cinq messages refusés en trente jours, mot pour mot : « 4.1.8 <messages@mail.faktify.fr>: Sender address rejected: Domain not found ». Le sous-domaine d’expédition n’a ni MX ni A. Beaucoup de serveurs vérifient que l’adresse de retour existe avant d’accepter un message — c’est banal — et refusent avant même de lire. Ce qui rend le cas intéressant, c’est qu’il ne se voyait nulle part : la vérification d’un domaine d’envoi regarde DKIM et SPF, qui sont des enregistrements TXT, et les deux peuvent être parfaits sur un nom qui, par ailleurs, ne mène à rien. Le domaine passait donc « vérifié », et ses messages se faisaient refuser chez un destinataire sur trois. Tout était vert, et rien n’arrivait — la panne exacte que ce produit vend. Une veille horaire résout désormais chaque domaine qui expédie et signale celui qui ne mène nulle part, en disant le geste plutôt que le symptôme : poser un MX ou un A, un seul des deux suffit. Elle ne bloque pas l’envoi : certains destinataires acceptent très bien un expéditeur qui ne résout pas, et couper le courrier d’un client sur une règle que tout le monde n’applique pas serait pire que le mal. Le signal le dit aussi.
Trois chiffres faux avant de trouver que la bonne réponse était déjà en base.
Le taux de rebonds a été compté de trois façons successives, toutes écrites à la main : « tous les refus » donnait 26 % sur le plus gros expéditeur, « ce qui ressemble à une adresse inexistante » donnait 3,8 %, « tout ce qui n’est pas clairement un blocage » donnait 13 %. La bonne réponse est 5,2 % — et elle n’a jamais eu besoin d’être calculée. La table mail_remises porte le verdict du classificateur, adresse par adresse, écrit au moment du refus par le code qui décide aussi de supprimer ou de réessayer : accepte, differe, rebond_dur, rebond_doux, refus, plainte. Écrire un quatrième motif aurait donné un quatrième chiffre. Tout se lit désormais dans cette colonne — l’écran d’exploitation, l’alerte envoyée au client, et le frein de la campagne — et la lecture a gagné autre chose au passage : mail_remises compte par destinataire, là où les événements comptaient par message. Un message à trois destinataires dont un seul rebondit y valait « un rebond » comme un message à un seul.
Nous allions écrire à un client qu’il avait 26 % de rebonds, pour une panne qui était la nôtre.
L’alerte « trop de rebonds » part dès 5 % sur une journée, et elle comptait tous les refus. Sur le client concerné : 102 rebonds, dont 85 % étaient des blocages de notre propre adresse IP, hérités d’un épisode de liste noire qui touchait toute la plateforme. Son vrai taux était de 3,8 %. Accuser quelqu’un de la qualité de sa liste pour une panne qui est la nôtre est la pire alerte possible : elle est fausse, elle est humiliante, et elle lui fait nettoyer une liste qui n’avait rien. Le jugement ne porte donc plus que sur les adresses inexistantes — la seule chose qu’il puisse corriger — et le libellé dit quoi faire plutôt que ce qui s’est passé : « 8 rebonds » ne se corrige pas, « 8 adresses inexistantes à retirer de ta liste » oui. Ce qui n’est pas de son fait est mentionné à part, sans reproche. Et un piège évité en chemin, que les tests ont attrapé : la première version décrivait ce qui ressemble à une adresse inexistante, si bien qu’un rebond sans motif connu — ceux qu’un fournisseur signale sans détail — ne déclenchait plus rien. Une alerte qui marchait serait devenue muette. Le motif décrit donc ce qui disculpe : le doute reste un rebond, et ne sert qu’à écarter ce qui est clairement de notre côté.
Le tableau accusait le mauvais domaine — et j’avais corrigé les jauges sans le corriger, lui.
La colonne « Échec » comptait encore tous les refus, alors que les jauges venaient d’apprendre à les distinguer : deux chiffres pour la même idée sur le même écran, exactement ce qu’on venait de reprocher ailleurs. En triant par domaine, la conclusion s’inverse. eclatevents.fr était désigné à 26 % : 85 % de ses rebonds étaient des blocages d’adresse IP, hérités d’un épisode de liste noire qui touchait toute la plateforme et dont il n’était pas responsable. Son vrai taux est de 3,8 %. Le domaine réellement au-dessus du seuil était l’autre, à 6,7 % — celui que la colonne ne désignait pas. Un tableau qui nomme un coupable doit nommer le bon : sinon il coûte une conversation désagréable avec quelqu’un qui n’y était pour rien. La colonne sépare désormais les rebonds durs — des adresses qui n’existent pas — des bloqués, qui coûtent une remise et pas une réputation.
La campagne s’arrête toute seule si les plaintes montent — et le premier frein posé était inutilisable.
Une alerte suppose quelqu’un pour la lire. La campagne part six fois par jour, sans personne devant, et le dégât d’une journée de trop se compte en semaines : au-delà de trois plaintes pour mille, Gmail filtre en masse. Le seul mécanisme qui tienne à cette cadence est celui qui refuse d’envoyer. La première version lisait le taux de rebonds de toute la plateforme — et s’enclenchait immédiatement. En mesurant : 30 rebonds durs sur sept jours, dont quinze chez un client, douze chez un autre, et zéro dans la prospection. Le frein aurait arrêté notre campagne pour la qualité d’une liste qui n’est pas la nôtre, et qu’arrêter la campagne ne corrige en rien : il serait resté serré en permanence, et un frein toujours serré est un frein qu’on desserre. Les deux mesures sont donc séparées, chacune sur sa base : la plainte abîme la réputation de l’adresse que tous les domaines partagent et se lit sur la plateforme entière ; le rebond dur mesure la fraîcheur d’une liste et se lit sur la nôtre. Avec deux volumes minimaux différents, parce qu’à cinquante-huit messages remis, trois rebonds font 5,2 % et un message de plus déplace le résultat de deux points : ce n’est pas une mesure, c’est un tirage au sort.
Un domaine n’a qu’un propriétaire prouvé — sinon deux comptes, deux quotas.
Signalé depuis l’écran : le même domaine déclaré dans deux comptes, « ça ne double pas les quotas ? ». Vérification faite : si. Le quota d’envoi se compte par équipe, donc deux comptes qui prouvent le même domaine obtiennent deux quotas quotidiens pour un seul domaine — un contournement par construction, et il suffit de créer un second compte. Pour le courrier c’est pire : deux propriétaires prouvés veulent dire deux clés DKIM et deux SPF attendus sur le même nom, et plus personne ne sait qui a le droit d’écrire. La déclaration reste libre, et c’est délibéré : la réserver au premier arrivé permettrait de bloquer le domaine de quelqu’un d’autre en le déclarant avant lui, et tant qu’elle n’est pas prouvée elle ne fait rien — ni surveillance, ni envoi. C’est la preuve qui devient exclusive. Le refus nomme le cas plutôt que de dire « impossible », parce que le cas légitime existe — on déménage d’un compte à l’autre — et qu’il se règle en retirant le domaine de l’ancien.
La jauge disait 20 % d’échecs. En triant les refus un par un, il y en avait 3,3 %.
À la question « pour être un expéditeur de qualité, quel est l’objectif », il a fallu commencer par admettre que notre propre jauge ne mesurait pas la bonne chose. Elle comptait tous les refus. En les triant sur trente jours : sur 233 refus, 93 étaient des blocages d’adresse IP ou de cadence (l’épisode d’une liste noire, depuis levé), 29 des serveurs injoignables, 25 un domaine d’expédition qui ne résout pas — et 22 seulement des adresses inexistantes. Un blocage d’IP ne dit rien de la qualité d’une liste : il dit que ce serveur-là ne nous aimait pas ce jour-là. Les mélanger donnait un chiffre quatre fois trop noir, c’est-à-dire un chiffre qu’on cesse de regarder. L’écran montre désormais ce sur quoi les fournisseurs nous jugent vraiment : rebonds durs, objectif < 2 %, et surtout plaintes, objectif < 0,1 % — le seuil de Gmail est à 0,3 %, au-delà duquel il filtre en masse et où le retour en grâce se compte en semaines. C’est le seul indicateur dont on ne se relève pas en corrigeant une liste ; nous en avons zéro. Et l’objectif est devenu l’échelle de la jauge plutôt que cent : à deux pour cent, l’arc est plein et la couleur a basculé. Une jauge sans son objectif est une décoration — « 3,3 % » ne dit rien à qui ne sait pas où est la limite.
Sept recettes officielles de plus, et un garde-fou qui a eu raison contre moi.
Le prélèvement du mois, les impayés jamais relancés, le fichier du fournisseur qui n’arrive plus, le flux produit qui fige les annonces, le partenaire qui a cessé d’appeler, le rapport du lundi, la recherche interne qui ne trouve plus les nouveautés. Vingt recettes officielles au total. Chacune porte sa panne silencieuse, et c’est le critère de sélection : un import qui ne trouve aucun fichier se termine en succès — il a bien traité les zéro lignes du fichier absent — et le site vend au prix de la semaine dernière ce qu’il n’a plus. Un test a refusé deux d’entre elles, et il avait raison : une recette en mode « monitor » doit s’installer en un clic, sans rien à saisir. Les miennes demandaient le nom du fournisseur et celui du partenaire pour les mettre dans le titre d’une tâche — un formulaire avant la valeur, pour un mot que la personne change ensuite en deux secondes. Un champ à remplir avant de pouvoir commencer apprend à remplir sans lire.
Le recollage ne connaîssait pas la plus grosse des frontières — celle qui porte la page entière.
Correctif du correctif, trouvé en allant compter les marques dans le HTML réellement servi plutôt qu’en supposant leur forme. Le recollage cherchait des emplacements nommés P:n ; la frontière de plus haut niveau, celle qui contient toute la page, s’appelle B:0. Elle restait donc dans son conteneur caché. Et ce n’est pas une liste mais une cascade : le contenu d’un conteneur porte lui-même les emplacements des suivants — sur une page de forum, S:0 contient le P:1 que S:1 viendra remplir. Un seul parcours ne pouvait pas suffire. On repasse donc jusqu’à ce que plus rien ne bouge, avec une borne pour qu’un HTML inattendu ne fige pas l’onglet : mieux vaut renoncer à un rafraîchissement que bloquer la page. Un seul emplacement vide restant annule le remplacement — une page à moitié recollée est une page qui a perdu la moitié de son contenu.
Notre propre tâche de prospection était en retard de vingt-huit minutes. Tous les jours, par construction.
Signée par l’écran d’exploitation : « tâches en retard de plus de cinq minutes : 1 ». L’alerte avait raison, le réglage avait tort. Le minuteur part à 8 h 40, 10 h 40… et le battement n’arrive qu’au bout du passage — vingt-huit minutes plus tard, le temps de visiter neuf mille domaines. Attendre ce battement à l’heure du départ rendait la tâche en retard chaque jour. La grâce l’empêchait d’alerter, mais elle s’affichait quand même dans le compte des tâches en retard : un avertissement permanent qui ne veut rien dire est exactement ce qui apprend à ne plus regarder les avertissements, et c’est ce que nous reprochons aux autres. L’heure attendue est donc celle de la fin — 9 h 10, 11 h 10…, deux minutes après l’heure où le battement arrive d’habitude — et la grâce descend à quarante-cinq minutes : un passage qui déborde de ce quart d’heure supplémentaire est vraiment anormal. Elle reste sous l’intervalle de deux heures, sans quoi un battement manquant serait couvert par le suivant et l’absence ne se verrait jamais.
Le rafraîchissement automatique vidait la page qu’il devait mettre à jour.
Trois écrans tronqués de la même façon — deux modules sur huit, un jour sur sept, un cadran sur quatre — et aucun défaut dans les trois composants : rendus à part avec les données de production, ils sortaient tous leurs éléments. Le mot qui a mis sur la piste est « des fois », qui ne décrit pas une donnée mais une course. Nos pages sont rendues en flux : le serveur envoie d’abord une coquille avec des emplacements vides, puis le contenu, garé dans des conteneurs cachés à la fin du corps, qu’un petit script remet à sa place au chargement (/forum/tock en compte quatorze). Le script qui rafraîchit l’écran d’exploitation toutes les trente secondes refait la requête, reparse le HTML et réinjecte la zone — or ni DOMParser ni importNode n’exécutent le moindre script. Le contenu restait donc dans son conteneur caché, hors de la zone remplacée : avant le premier rafraîchissement tout était là, après il ne restait que ce que la coquille portait. Le recollage se fait maintenant explicitement, et un garde-fou s’ajoute qui aurait suffi à lui seul, quelle qu’ait été la cause : si la zone qui arrive porte moins de contenu que celle qui est affichée, on garde celle qui est affichée. Une page qui se vide toute seule est la pire des mises à jour — le lecteur croit que les données ont disparu.
Trois jauges pour le courrier de la plateforme — et « 0 % d’échecs » ne s’affiche plus en rouge.
Remis, Échoués, Ouverts, sur trente jours et tous domaines confondus, là où l’écran ne comptait que sa file d’attente. Sous les jauges, le tableau par domaine répond à la seule question qui compte pour l’exploitant : l’adresse d’expédition est commune, donc les rebonds de l’un pèsent sur la remise de tous, y compris de ceux qui n’ont rien envoyé. La mesure d’aujourd’hui nomme le responsable sans ambiguïté. Deux règles tenues au passage. La jauge a appris de quel côté se trouve la bonne nouvelle : elle colorait toujours haut = vert, ce qui est juste pour un taux de remise et exactement faux pour un taux d’échec — « 0 % » sortait en rouge vif, c’est-à-dire que la couleur disait le contraire du chiffre, sur le même dessin. Et sous vingt remises, on écrit le compte et pas le taux : un rebond sur trois envois affiche « 33 % », ce qui est arithmétiquement exact et parfaitement trompeur. Une dernière précision qui évite un contresens : les ouvertures ne se mesurent que sur les messages en HTML, par une image d’un pixel. La prospection part en texte brut, délibérément — son taux d’ouverture est nul par construction, pas par désintérêt, et l’écran le dit.
Le graphe de la machine ne montrait que la charge — les trois autres courbes étaient là, et illisibles.
Signalé depuis l’écran : « il affiche souvent uniquement la charge ». Vérification faite, les quatre séries étaient bien tracées — le composant a été rendu à part pour s’en assurer, et les données relues en production : charge 4 à 166 %, mémoire 22 à 84 %, disque 12 à 25 %, base 26 à 132 Mo. Ce n’était pas un bogue, c’est une question d’amplitude. La charge monte à 166 et redescend à 4 sur deux cent dix points : elle remplit le cadre d’un gribouillis qui occupe toute la hauteur. La mémoire vit à l’intérieur de ce gribouillis, où un trait fin ne se distingue plus de rien ; le disque rampe écrasé en bas. Chacune était là, aucune n’était lisible. Une échelle commune ne se justifie que quand la comparaison des hauteurs dit quelque chose — et savoir si la mémoire passe au-dessus de la charge n’apprend rien à personne. Ce non-fait coûtait la lecture des trois autres mesures. La machine a donc un cadran par mesure, chacun à son échelle, et chacun en dit davantage que la tuile qu’il remplace : la valeur du moment, le minimum, le maximum avec son heure, et le seuil qui compte tracé en pointillé — 100 % pour la charge (« autant de travail que de cœurs »), 85 % pour la mémoire, 80 % pour le disque. Les quatre tuiles du haut ont disparu dans les cadrans : écrire le même chiffre à deux endroits, c’est garantir qu’un jour l’un bougera sans l’autre.
« Sur une liste noire », disait l’alerte. Vérification faite : « not listed, no action required ».
La veille des listes noires a deux sources : une requête DNS qui CONSTATE, et un refus d’un serveur d’en face qui CITE une liste en nous bloquant. Elle les affirmait de la même façon — « l’adresse d’expédition est sur une liste noire », en sérieux. Ce n’est pas la même chose, et la différence s’est vue en production : notre adresse a porté ce rouge pendant trois jours — la fenêtre pendant laquelle une citation compte — alors qu’une vérification directe répondait « not listed, no action required ». Une alerte qui affirme plus que ce qu’elle a mesuré coûte exactement ce qu’elle prétend protéger : celui qui la lit une fois pour rien apprend à ne plus regarder le rouge, et la fois suivante elle aura raison. Les deux verdicts sont donc séparés. Constaté par requête : sérieux, et le titre l’affirme. Seulement cité dans un refus : attention, et le titre dit ce qui s’est réellement passé — « un serveur a refusé cette adresse en citant une liste noire ». Le détail explique pourquoi on ne peut pas trancher : les grandes listes (Spamhaus, Abusix) refusent les résolveurs publics, donc la citation est tout ce qu’on peut obtenir, et le lien de vérification est celui que le refus donnait lui-même. La décision vit maintenant dans une fonction pure, éprouvée par des tests plutôt que par la production.
Personne ne pouvait répondre à nos messages : l’adresse d’expédition n’avait pas de boîte.
Quarante-cinq messages froids sont partis de benoit@letock.fr. Cette adresse n’avait aucune boîte ouverte, alors que le domaine reçoit chez nous. Notre propre serveur a donc refusé, en 550 « boîte inconnue », trois avis de rebond envoyés par Gandi et OVH — dont deux une minute après l’envoi du matin. Le refus des rebonds est le moindre mal. Le vrai dégât est ailleurs : quiconque répondait à notre message recevait un refus. Un message signé d’un prénom, qui invite à répondre, et dont la réponse rebondit — c’est exactement ce qu’un destinataire attend d’un publipostage, et c’est aussi un signal que les filtres regardent. La boîte est ouverte, et la campagne refuse désormais de partir tant que son expéditeur ne peut pas recevoir de réponse. La règle est posée là où elle vaut, et nulle part ailleurs : un client a parfaitement le droit d’expédier depuis un domaine dont le courrier entrant est ailleurs. Elle ne s’applique que lorsque nous sommes le MX, c’est-à-dire quand le refus viendrait de nous. Un défaut de plus est tombé en écrivant le test : sans arobase, lastIndexOf rend −1 et slice(0, −1) ne se plaint pas — « benoit » devenait la boîte « benoi » du domaine « benoit », donc un domaine inconnu, donc « ça ne nous regarde pas ». Une adresse malformée passait pour une adresse étrangère, en silence.
Nous avons écrit à « contact@www.… » — et ça a rebondi en dur.
Première nuit de la campagne automatique, et le premier vrai défaut sort tout seul. L’adresse était lue telle quelle sur leur page : contact@www.montgolfiere-centreatlantique.fr. Le filtre acceptait toute adresse finissant par le domaine, sous-domaines compris — ce qui est juste pour contact@mail.exemple.fr, et faux pour www., qui est un nom de site et n’a jamais eu de MX. Le message est revenu en « Recipient address rejected: User unknown ». Un rebond dur sur une adresse inexistante est ce qui coûte le plus cher : il compte contre la réputation de l’adresse d’expédition que tous les domaines de la plateforme partagent, et il venait d’une faute de frappe dans laquelle nous n’avions aucune raison d’entrer. On ne la réécrit pas en retirant le www. : ce serait deviner une adresse, et c’est précisément ce que ce module refuse de faire. Un domaine qui ne publie que ça est un domaine qu’on laisse. Les vrais sous-domaines de courrier, eux, restent acceptés.
La veille de voisinage mesurait quatre-vingt-dix secondes sur vingt-huit minutes.
Elle compare la durée des exécutions des clients pendant les passages de prospection et en dehors, pour vérifier que la priorité basse tient ses promesses. Elle annonçait « indéterminé », et pour une raison invisible : la fenêtre était calculée depuis l’ouverture du script d’envoi, alors que le gros du travail — visiter neuf mille domaines, vingt-sept minutes — se fait avant, dans un autre processus. Mesuré : 2 observations dans la fenêtre contre 9 890 en dehors. La fenêtre couvrait donc tout sauf la partie qui charge le serveur, et le verdict serait resté « indéterminé » indéfiniment. Le passage exporte désormais son heure de départ, et la fenêtre couvre le passage entier. C’est la deuxième fois en deux jours qu’une surveillance de notre cru avait l’air de marcher sans rien regarder ; les deux ont été trouvées en allant lire ce qui était réellement enregistré, jamais en relisant le code.
Première nuit en autonomie : 85 397 domaines qualifiés, 177 prospects, 25 messages.
Le regarnissage nocturne a tiré 110 000 candidats de l’open data AFNIC et en a gardé 85 397 après filtre DNS, en trente-neuf minutes : la réserve passe de 15 619 à 101 016 domaines. Le passage de 8 h 40 a visité neuf mille domaines en vingt-sept minutes, rendu 177 prospects, puis remis 25 messages — exactement le quart du palier de chauffe disponible, ni plus ni moins. Bilan des envois depuis le début : 45 écrits, 37 livrés, 8 en échec, dont cinq serveurs injoignables, une boîte pleine, un www. (ci-dessus) et un refus de remise lié à l’inscription connue de notre adresse sur une liste noire. Rendement réel sur le plus gros échantillon à ce jour : 1,97 %. Le premier chiffre annoncé était 6 %, mesuré sur 134 domaines ; il a fondu à chaque fois que l’échantillon a grandi.
Un détail vous a gêné, ou vous a coûté du temps ? Répondez ici plutôt
que par courrier : la réponse servira aussi aux suivants.