É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.
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.
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
consolepar 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.
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 webPuis 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.
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.
C'est l'étape qui coûte le plus de temps, autant la faire dans l'ordre.
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.
| 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 |
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.
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.
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.
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 "" # retirerTrois 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-24rouvre 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.
- Créer une campagne : segment visé, ce que vous proposez en une phrase, ce que vous voulez obtenir, consignes de rédaction.
- 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.
- Rédiger : un brouillon par destinataire.
- Relire et valider. Rien ne part sans ce clic.
- 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. - 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.
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.
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.
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.
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.
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.
python -m pytest117 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.
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.
MIT. Faites-en ce que vous voulez, y compris commercialement, en gardant la mention de copyright. Le logiciel est fourni sans garantie.