Aller au contenu

Note d'ingénierie

Prospection email : envoyer sans griller ses domaines

Par Alexandre Kaczor7 min de lecture
  • Email
  • Architecture
  • IA
Illustration de l'article « Prospection email : envoyer sans griller ses domaines »

Plafonds par boîte, fenêtre d'envoi, arrêt dès la première réponse, SPF, DKIM et DMARC vérifiés : les garde-fous d'un outil de séquences d'emails.

Un domaine d'envoi se grille en quelques jours et se répare en plusieurs semaines. Une boîte neuve qui envoie deux cents messages le premier jour finit en indésirables, et ce sont alors les emails légitimes du produit, factures et réinitialisations de mot de passe comprises, qui cessent d'arriver.

Pour prospecter pour mes produits, j'ai écrit KMail, un outil interne de séquences d'emails. Il envoie depuis la vraie boîte de contact de chaque produit, et non depuis un domaine secondaire acheté pour l'occasion. C'est un choix assumé : les réponses arrivent là où je les lis, et la réputation qui se construit est celle du domaine qui compte. Il impose en contrepartie de ne jamais mettre ce domaine en danger. Voici les garde-fous, tels qu'ils sont codés.

Une séquence, pas une campagne

KMail ne fait pas de « campagnes » envoyées d'un coup à une liste. Il gère des séquences : une suite d'étapes espacées de quelques jours, dans laquelle chaque contact entre individuellement. Un modèle typique compte un premier contact et deux relances, à trois puis cinq jours d'intervalle.

Les relances partent par défaut dans le même fil : même objet précédé de « Re: », et en-têtes In-Reply-To et References qui pointent vers le message précédent. Pour le destinataire, c'est une conversation, pas trois messages publicitaires. Pour l'outil, c'est aussi ce qui permettra plus tard de rattacher une réponse à l'envoi qui l'a provoquée.

Avant qu'une séquence puisse être activée, des contrôles bloquants s'appliquent : chaque étape doit contenir le lien de désinscription, aucune variable ne doit être inconnue, au moins une étape doit être active. D'autres contrôles ne font qu'avertir : objet de plus de 60 caractères, corps trop court ou trop long, plus de trois liens, trop de majuscules dans l'objet, formules connues pour déclencher les filtres.

Des plafonds à trois niveaux

Le cœur de la protection est un ensemble de limites vérifiées avant chaque envoi, dans cet ordre :

if (await this.repository.isSuppressed(contact.email)) {
  await this.repository.stopEnrollment(
    enrollment.id,
    EnrollmentStatus.STOPPED_SUPPRESSED,
    'Adresse présente dans la liste de suppression',
  );
  return 'contact en liste de suppression : inscription arrêtée';
}

const window = checkSendWindow(now, sequence);
if (!window.ok) return window.reason;

const lastSent = this.lastSentAtByApp.get(app.slug);
if (lastSent !== undefined && now.getTime() - lastSent < MIN_GAP_PER_MAILBOX_MS) {
  return 'lissage : un envoi vient de partir de cette boîte (< 20 s)';
}

const caps = checkDailyCaps({
  mailboxSentToday: await this.repository.mailboxSentToday(app.slug, parisDayKey(now)),
  mailboxCap: app.dailyCap,
  sequenceSentToday: await this.repository.sequenceSentSince(sequence.id, parisDayStart(now)),
  sequenceCap: sequence.dailyCap,
});
if (!caps.ok) return caps.reason;

Concrètement :

  • Une fenêtre d'envoi : de 9 h à 18 h, heure de Paris, du lundi au vendredi par défaut. Un email de prospection reçu à 3 h du matin est lu comme un envoi automatique, ce qu'il est.
  • Un lissage par boîte : au moins 20 secondes entre deux envois d'une même boîte. Le planificateur passant une fois par minute, cela revient en pratique à un envoi par boîte et par minute au plus. Les serveurs d'envoi tolèrent mal les rafales.
  • Un plafond journalier par boîte : 40 messages par jour. Chaque produit n'ayant qu'une boîte, c'est aussi le plafond par domaine.
  • Un plafond journalier par séquence : 30 par défaut, pour qu'une séquence ne consomme pas à elle seule tout le budget de la boîte.

Les jours sont comptés en jours civils de Paris, et les emails de test entrent dans le compte : un plafond qui ignore une partie des envois n'est pas un plafond.

Je note honnêtement ce qui manque. Le code prévoit l'idée d'une montée progressive pour une boîte neuve, mais elle n'est pas implémentée : le plafond est fixe à 40 dès le premier jour. C'est une valeur prudente pour une boîte qui envoie déjà du courrier normal, pas pour une boîte créée la veille. Il n'y a pas non plus d'aléa dans l'espacement des envois.

S'arrêter dès que quelqu'un répond

La règle la plus importante d'une séquence n'est pas d'envoyer, c'est de s'arrêter. Relancer quelqu'un qui a déjà répondu est la façon la plus sûre de se faire signaler comme indésirable.

KMail lit les boîtes de réception en IMAP toutes les deux minutes, sans rien marquer comme lu ni déplacer. Chaque message entrant passe par une fonction de décision pure :

export function decide(mail: InboundMail, candidates: Candidates, lookup: Lookup): Decision {
  if (mail.isAutoReply) return { kind: 'auto-reply' };

  if (mail.isBounce) {
    // ... rattachement à l'envoi d'origine, code d'erreur, rejet définitif ou non
  }

  // Le fil d'abord : c'est la preuve la plus forte. Sinon l'expéditeur seul
  // suffit — une réponse tapée depuis un autre client garde son auteur.
  if (lookup.outboundByThread && lookup.contactOfOutbound) {
    return { kind: 'reply', outbound: lookup.outboundByThread, contact: lookup.contactOfOutbound };
  }
  if (lookup.contactByFrom) return { kind: 'reply', outbound: null, contact: lookup.contactByFrom };
  return { kind: 'ignored', reason: 'no-contact' };
}

Comme KMail génère lui-même le Message-ID de chaque envoi, une réponse se rattache d'abord par le fil. À défaut, l'adresse de l'expéditeur suffit si c'est un contact connu. Une réponse arrête toutes les séquences actives du contact (c'est le réglage par défaut de chaque séquence) et crée une tâche « lire la réponse ».

Les réponses automatiques d'absence sont reconnues par leurs en-têtes (Auto-Submitted, Precedence) ou leur objet, et ignorées : un message d'absence n'est pas une conversation. Un rejet définitif, lui, place l'adresse dans une liste de suppression commune à tous les produits et arrête toutes ses séquences. Au-delà de 5 % de rejets sur sept jours, l'outil affiche une alerte.

La désinscription suit la RFC 8058 : chaque envoi porte List-Unsubscribe et List-Unsubscribe-Post, ce qui permet aux messageries d'afficher leur propre bouton de désinscription en un clic. Le lien du corps de l'email désinscrit aussi immédiatement, sans page de confirmation. Les grands fournisseurs de messagerie exigent désormais ce mécanisme des expéditeurs réguliers.

SPF, DKIM, DMARC : vérifier plutôt que supposer

KMail interroge le DNS de chaque domaine d'envoi et affiche l'état de quatre enregistrements : SPF (avec l'inclusion attendue de l'hébergeur de messagerie), DKIM (en testant les sélecteurs courants), DMARC (avec sa politique) et MX. Chaque recommandation donne l'enregistrement exact à poser, par exemple une politique DMARC en p=none pour commencer, à durcir en p=quarantine une fois SPF et DKIM en place.

Ce contrôle a servi dès le premier jour. Sur les cinq domaines, SPF, MX et DMARC étaient en place, mais DKIM était absent partout. Rien ne le signalait : les emails partaient, et la plupart arrivaient. Un domaine sans signature DKIM reste pourtant pénalisé par les filtres, et la politique DMARC n'a alors qu'un seul mécanisme sur lequel s'appuyer. C'est typiquement le défaut qu'on ne voit qu'en le cherchant.

L'IA classe, l'humain décide

Quand les réponses arrivent, un modèle de langage peut les classer en cinq catégories : intéressé, pas maintenant, pas intéressé, question, incertain. La consigne se termine par une phrase à laquelle je tiens : en cas de doute, répondre « incertain », jamais « intéressé » par optimisme. Toute valeur hors de la liste est ramenée à « incertain ».

Le classement se lance à la demande, et son résultat est stocké comme une suggestion, jamais comme un statut appliqué. C'est moi qui qualifie la réponse. Une erreur de classement coûte une relance mal placée ou un prospect intéressé oublié ; ni l'un ni l'autre ne se délègue à un modèle sans relecture. Le modèle ne reçoit que l'objet de l'email envoyé et le début de la réponse, et chaque appel est comptabilisé.

Ce que j'en retiens

  • La réputation d'un domaine se protège avant chaque envoi, pas dans un tableau de bord consulté après coup. Fenêtre, lissage et plafonds se vérifient dans le chemin d'envoi lui-même.
  • S'arrêter compte plus qu'envoyer. Rattacher les réponses par Message-ID et arrêter toutes les séquences du contact évite l'erreur la plus coûteuse.
  • La configuration DNS se vérifie, elle ne se suppose pas. Un DKIM manquant ne produit aucune erreur visible.
  • L'IA propose, l'humain qualifie. Sur une relation commerciale, une suggestion prudente vaut mieux qu'une automatisation confiante.

Un projet similaire, un outil d'emailing ou d'automatisation commerciale à construire proprement ? Parlons-en.