Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mailing

Licence MIT Python 3.10+ Texte brut, sans pixel Paire : leadgen

Écrire, envoyer et suivre une campagne de prospection par courriel, sans se mettre hors la loi ni se faire classer en spam.

mailing prend une liste de contacts professionnels, écrit un message personnalisé pour chacun, attend votre relecture, envoie à cadence humaine, relance les silencieux, puis relève et trie les réponses.

Il fait partie d'une paire :

leadgen trouve et qualifie les contacts, à partir de registres publics
mailing (ici) rédige, envoie, relance, trie les réponses

Chacun s'utilise seul. mailing accepte aussi un simple CSV. Voir Utiliser les deux ensemble.


Le principe

contacts  →  rédaction par modèle  →  VOTRE RELECTURE  →  envoi cadencé
                                                            ↓
              réponses triées  ←  relevé IMAP  ←  relances J+3, J+7

Rien ne part sans que vous l'ayez lu. C'est le seul garde-fou qui compte quand un modèle écrit à votre place, et il est dans le code : la rédaction ne produit que des brouillons, et l'envoi ne retient que ce qui est passé en « validé ».

Brique Ce qu'elle fait
Rédaction le modèle écrit chaque message à partir de la fiche du contact : ville, métier, titre du site, mots-clés relevés. Deux moteurs au choix, Claude ou Mistral. Sans clé API, un gabarit déterministe prend le relais
Séquences relance à J+3 puis J+7. Une réponse, une désinscription ou un rebond arrête tout immédiatement
Tri des réponses rebonds, absences du bureau et demandes de désinscription détectés par règles sûres. Le reste est classé par le modèle en interesse / pas_interesse / inconnu, et vous pouvez corriger
A/B des objets chaque objet reçoit d'abord 10 envois, ensuite le meilleur taux de réponse est privilégié, avec 15 % d'exploration pour ne pas enterrer une variante malchanceuse

L'A/B s'arbitre au taux de réponse, jamais au taux d'ouverture : suivre les ouvertures demande un pixel de traçage, qui dégrade la délivrabilité et n'apprend rien de solide en prospection froide.


Ce qui est verrouillé dans le code

Pas des recommandations, des refus.

  • Chaque message porte votre identité et un moyen de s'y opposer. Ce bloc est ajouté par l'outil, jamais par le modèle. Un message qui l'a perdu est bloqué avant l'envoi, y compris après une modification à la main.
  • La liste d'opposition est définitive, et vérifiée à l'import, à la rédaction, puis une dernière fois juste avant l'envoi.
  • Une désinscription exclut toute la structure, pas seulement la boîte qui a répondu : sinon la collecte suivante remonte accueil@ là où contact@ avait dit stop. ⚠️ Sauf sur un domaine partagé (@notaires.fr, @gmail.com…) : bannir le domaine rayerait des milliers de structures sans lien entre elles.
  • Un rebond n'exclut que l'adresse : une boîte pleine ne dit rien des autres adresses du même cabinet.
  • Les boîtes techniques (postmaster@, noreply@, abuse@) ne sont jamais contactées.
  • Quota quotidien, fenêtre horaire ouvrée, verrou anti-double-envoi.
  • Pendant une fermeture déclarée par la campagne, rien n'est rédigé ni envoyé, y compris un premier contact déjà validé.
  • Transport console par défaut : rien ne part tant que vous n'avez pas configuré l'envoi, et toute configuration incomplète y retombe.

Un journal horodaté enregistre chaque envoi, chaque réponse, chaque désinscription. C'est le registre à montrer en cas de réclamation.


Installation

Python 3.10 ou plus.

git clone https://github.com/Maxyull/mailing.git
cd mailing
python -m venv .venv
.venv\Scripts\activate          # Linux/macOS : source .venv/bin/activate
pip install -r requirements.txt
python -m pytest                # 117 tests, aucun acces reseau
python -m mailing web

Puis http://127.0.0.1:8770.

À ce stade tout fonctionne en essai à blanc : les messages sont écrits dans data/boite-test/*.eml au lieu d'être envoyés. C'est le bon moment pour lire une dizaine de messages en entier.

Pour envoyer pour de vrai

Copiez config/reglages-exemple.json en config/reglages.json, ou réglez tout depuis l'onglet Réglages de l'interface. Puis créez ../secrets/mailing.env, à côté du dépôt et non dedans :

MAILING_SMTP_MDP=...       # mot de passe de la boite d'envoi
MAILING_IMAP_MDP=...       # mot de passe de la boite de reception
MISTRAL_API_KEY=...        # ou ANTHROPIC_API_KEY, pour la redaction
BREVO_API_KEY=...          # seulement si vous passez par Brevo

Le fichier de réglages ne contient que le nom des variables, jamais leur valeur. Il peut désigner d'autres fichiers de secrets avec SECRETS_SUPPLEMENTAIRES=chemin1;chemin2, utile quand une clé est déjà posée ailleurs sur la machine : recopier un secret dans deux fichiers, c'est en oublier un le jour où on le révoque.

Le bandeau en haut de l'interface dit en permanence où vous en êtes : envoi réel ou essai à blanc, quota restant, fenêtre ouverte ou non, moteur de rédaction actif, relevé des réponses prêt ou non.


Préparer la boîte d'envoi

C'est l'étape qui coûte le plus de temps, autant la faire dans l'ordre.

⚠️ Une redirection ne peut pas envoyer. Une adresse comme contact@ peut exister et recevoir tout en n'étant qu'une redirection : aucune boîte derrière, donc aucun mot de passe, donc aucun SMTP. Il faut un compte réel.

⚠️ Les messageries chiffrées (Proton et assimilées) n'offrent pas de SMTP hors offre payante.

Les trois enregistrements DNS à vérifier

SPF doit autoriser les serveurs d'envoi de votre hébergeur
DKIM à activer chez l'hébergeur ; se vérifie sur un vrai message reçu (« signé par votre domaine »)
DMARC v=DMARC1;p=none;rua=mailto:dmarc@votre-domaine.fr; pour commencer

⚠️ L'adresse rua doit rester sur votre propre domaine. La pointer sur un domaine tiers exige une autorisation publiée dans la zone de ce tiers, que vous ne contrôlez pas. Pour recevoir les rapports ailleurs, utilisez une redirection depuis une adresse de votre domaine.

Où arrivent réellement les messages

Sur un échantillon B2B français de 55 cabinets : 23 chez Microsoft, 19 derrière une passerelle anti-spam d'entreprise (Retarus, Hornetsecurity/Vade, Mimecast…), 8 chez des hébergeurs français divers, 4 ailleurs. Deux conséquences :

  • Microsoft est le juge principal. Il accorde sa confiance progressivement à un domaine sans historique. C'est la vraie raison de démarrer bas.
  • ⚠️ Ces passerelles ne rebondissent presque jamais, elles mettent en quarantaine en silence. Un garde-fou « N rebonds et on arrête » ne se déclenchera donc pas. Le signal d'alerte réel est le silence total : aucune réponse, pas même un refus poli, au bout d'une trentaine d'envois.

C'est aussi pourquoi les messages restent en texte brut, sans image, sans HTML, sans pixel de traçage et avec un seul lien.

Montée en charge

Une adresse neuve qui envoie beaucoup part en spam.

Période quota_jour
semaine 1 10
semaine 2 20
ensuite 40

Le quota est global, partagé entre les campagnes actives. Une campagne peut avoir son propre plafond en plus, jamais un plafond supérieur.

Quand le segment n'est pas là

Un cabinet français ferme trois semaines en août. Un message envoyé pendant ce temps n'abîme rien, il attend, mais il arrive sous la pile du retour de congés et ne sera jamais lu. C'est un envoi perdu, et il consomme le quota d'une boîte qu'on monte en charge lentement.

Chaque campagne déclare donc ses périodes de fermeture, dans le champ fermetures : des intervalles MM-JJ:MM-JJ séparés par des virgules.

python -m mailing fermetures 1                              # afficher
python -m mailing fermetures 1 --valeur "08-01:08-24"       # août
python -m mailing fermetures 1 --valeur "08-01:08-24,12-20:01-05"
python -m mailing fermetures 1 --valeur ""                  # retirer

Trois choses à savoir :

  • Les périodes sont annuelles, elles se réappliquent chaque année sans qu'on ait à rééditer la campagne. Le phénomène l'est aussi. Une date absolue deviendrait un réglage mort qui cesse de s'appliquer sans le dire.
  • La borne de fin est incluse : 08-01:08-24 rouvre le 25 août. C'est ce qui permet d'écrire « évite aussi le lundi de retour de congés » en allongeant la fin, plutôt que d'avoir une règle cachée dans le moteur.
  • Une période peut franchir le 1er janvier (12-20:01-05).

Pendant une fermeture, deux choses se produisent, et il faut les deux :

Ce que ça couvre
Aucun brouillon n'est rédigé, aucun message n'est envoyé le premier contact, dont l'échéance est vide et donc toujours due
Les échéances de relance sont repoussées juste après la période les relances, y compris le report de sept jours après une absence du bureau

La première garde est indispensable : sans elle, tout le stock déjà validé partirait pendant la fermeture, puisqu'un premier contact n'a pas d'échéance à repousser. python -m mailing etat affiche FERMEE jusqu'au ... à côté du statut, et un lot bloqué rend fermeture: True.

Changer les fermetures ne recalcule pas les échéances déjà programmées : elles ont été calculées avec les fermetures du moment, et les déplacer en masse bougerait des relances réglées à la main. La garde d'envoi les retient de toute façon.


Le parcours, dans l'ordre

  1. Créer une campagne : segment visé, ce que vous proposez en une phrase, ce que vous voulez obtenir, consignes de rédaction.
  2. Importer les contacts, depuis leadgen ou un CSV. Une seule adresse par structure, jamais une adresse en liste d'opposition, jamais une adresse déjà présente dans une autre campagne.
  3. Rédiger : un brouillon par destinataire.
  4. Relire et valider. Rien ne part sans ce clic.
  5. Passer la campagne en active, puis envoyer un lot. Le lot part en tâche de fond, un message à la fois, espacé de 45 à 240 secondes.
  6. Relever les réponses, une fois la boîte IMAP configurée.

Les relances suivent le même chemin : le brouillon de l'étape 2 apparaît trois jours après l'envoi de l'étape 1, à relire comme le premier.

En ligne de commande

Pour une tâche planifiée, ou pour tout piloter sans l'interface :

python -m mailing etat
python -m mailing campagne "Notaires - vague 1" --segment notaires \
       --proposition "..." --objectif "un echange par mail"
python -m mailing importer 1 --segments notaires --score-min 60 --limite 50
python -m mailing rediger 1
python -m mailing envoyer 1 --limite 10
python -m mailing reponses --jours 14
python -m mailing reprendre 1        # sans numéro : toutes les campagnes
python -m mailing fermetures 1 --valeur "08-01:08-24"

routine-quotidienne.py enchaîne la préparation des relances et le relevé des réponses, sans jamais envoyer. C'est ce qu'on met dans une tâche planifiée.

Quand un envoi échoue

Un échec technique passe le message en statut erreur, et le lot suivant ne regarde que les valide : sans rien faire, le destinataire sort de la campagne en silence. reprendre remet ces messages en file. Il ne rédige rien et n'envoie rien, ces messages ont déjà été relus ; ils repartent au prochain lot, quota et fenêtre d'envoi compris. Les messages rejeté ne sont pas touchés : une désinscription ou une réponse reçue est une décision, pas un incident.


Le tableau de bord

La page d'accueil montre quatre chiffres en gros : envoyés du jour rapporté au quota, réponses et leur taux, intéressés, brouillons à relire. En dessous, une ligne serrée pour le reste, ce qui attend votre réponse en évidence, et l'activité des 14 derniers jours en histogramme.

Elle se rafraîchit seule chaque minute, sauf pendant que vous remplissez un champ.

La création de campagne vit sur sa propre page, /campagnes/nouvelle, pour que l'accueil ne montre que des chiffres. Elle pré-remplit les consignes de la dernière campagne : elles portent des arbitrages déjà tranchés, et repartir d'un champ vide les fait reperdre à chaque fois.


Ce que « personnalisé » veut dire, et ses limites

Le modèle ne reçoit que des faits publics : nom, ville, métier, titre du site, mots-clés relevés. Aucune donnée sensible.

⚠️ Un modèle invente des faits plausibles si on le laisse faire. Constaté en réel : « votre activité en droit immobilier et familial » écrit à une étude dont la fiche ne disait rien de tel. Trois garde-fous : la consigne l'interdit explicitement, un contrôle signale les domaines d'activité absents de la fiche, et votre relecture reste le passage obligé. Le contrôle avertit, il ne bloque pas : l'affirmation peut être vraie, c'est à vous de trancher.

⚠️ La personnalisation ne vaut que ce que vaut la fiche. Si dix contacts ont les mêmes mots-clés, ils recevront dix messages très proches. Chacun ne voit que le sien, mais la variable réellement testée devient alors l'objet, pas le corps.


Utiliser les deux ensemble

mailing lit la base de leadgen en lecture seule et partage sa liste d'opposition. C'est ce partage qui compte : sans lui, la personne qui a répondu « stop » serait recontactée à la collecte suivante.

Posez les deux dépôts côte à côte :

votre-dossier/
├── leadgen/     ← collecte et qualifie
├── mailing/     ← rédige, envoie, relance, trie les réponses
└── secrets/     ← vos clés, hors des deux dépôts

mailing trouvera ../leadgen/data/leads.db tout seul.

Sans leadgen, importez un CSV avec au minimum une colonne email, et optionnellement nom, ville, segment, site, titre, accroche, signaux, angle, score.


Tests

python -m pytest

117 tests, aucun accès au réseau, à une boîte mail ou à une clé API. tests/test_regression.py rejoue un scénario complet et le compare à une référence figée : toute modification du gabarit, du bloc légal, des filtres d'import ou du moteur de séquence le fait échouer, volontairement.

Ce que le dépôt ne contient pas

Aucune adresse réelle, aucun secret, aucune donnée de prospect. La base, les messages et les réglages restent hors du dépôt.

Licence

MIT. Faites-en ce que vous voulez, y compris commercialement, en gardant la mention de copyright. Le logiciel est fourni sans garantie.

⚠️ La licence couvre le code, pas votre usage : la conformité de vos envois reste votre responsabilité, et les garde-fous décrits plus haut ne remplacent pas une lecture attentive avant d'envoyer.

About

Outil local de prospection par courriel B2B : redaction assistee par modele, validation humaine obligatoire, envoi cadence, relances, tri des reponses. Conformite verrouillee dans le code.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages