SMTP Servers
Un SMTP server, c'est le relais qui livre tes emails. Trackily ne route pas directement vers les MX des destinataires — il déléguer à un MTA tiers (Mailgun, SendGrid, Postmark, SES, Gmail) via SMTP authentifié. Tu configures un ou plusieurs serveurs, tu les attaches à des listes, et le moteur d'envoi s'occupe du reste.

Concept
Pourquoi pas du SMTP direct vers gmail.com / outlook.com ? Trois raisons :
- Réputation IP. Les MTA pros maintiennent des IPs warmed-up, signent SPF + DKIM + DMARC pour toi, et négocient avec les opérateurs (Gmail, Yahoo, Outlook) bien mieux qu'un VPS isolé.
- Feedback loop. Mailgun / SES te renvoient des webhooks sur les bounces / complaints — sans ça, tu n'as aucun moyen d'apprendre que tu as cramé un email et tu pollues
email_suppressionà l'aveugle. - Délivrabilité. Un envoi via Postmark depuis une IP partagée mais bien notée passe en inbox ; le même contenu depuis ton VPS résidentiel atterrit en spam.
Choisir un provider
| Provider | Quand l'utiliser | Notes |
|---|---|---|
| Mailgun | Cas le plus courant. Bon compromis prix/délivrabilité. Webhooks bounce/complaint propres. | Plan free 100/jour ; payant à partir de ~35$/mois. |
| SendGrid | Volume élevé (>100k/mois). API riche, dashboard correct. | Plan free 100/jour ; payant rapidement cher au-delà. |
| Postmark | Transactionnel pur (reçus, magic-links, notifications). Délivrabilité inbox excellente. | Refuse les listes marketing — ne JAMAIS l'utiliser pour des sequences. |
| Amazon SES | Tu as déjà un compte AWS et tu veux le prix au plus bas. | Setup plus rude (SPF/DKIM à faire toi-même), mais $0.10/1000 emails. |
| Gmail / Workspace | Très faible volume (<50/jour), strictement personnel. | Bloque à 500/jour ; les comptes "envoi de masse" se font suspendre. |
Recommandation par défaut : Mailgun pour les sequences, Postmark pour le magic-link et les reçus de commande. Deux SMTP server rows séparées, deux rôles bien isolés.
Le modèle de données
Schema email_smtp_servers :
| Colonne | Type | Note |
|---|---|---|
id |
SERIAL PK | |
name |
TEXT | label humain ("Mailgun production") |
host |
TEXT | ex: smtp.mailgun.org |
port |
INTEGER | 587 (STARTTLS) ou 465 (TLS) |
secure |
BOOLEAN | true pour TLS direct (port 465), false pour STARTTLS (port 587) |
auth_user |
TEXT | nom d'utilisateur SMTP (souvent apikey ou postmaster@…) |
auth_pass |
TEXT | mot de passe / API key. Chiffré au repos via AES-256-GCM (SECRETS_MASTER_KEY). Jamais retourné dans les API responses. |
from_email |
TEXT NOT NULL | adresse expéditeur par défaut (peut être écrasée par la liste) |
from_name |
TEXT | nom d'expéditeur par défaut |
reply_to |
TEXT | reply-to par défaut |
daily_limit |
INTEGER | plafond par jour UTC (par défaut 5000), appliqué par le moteur. NULL = pas de plafond ; 0 ou un nombre négatif est refusé (HTTP 400, et par l'outil MCP) |
hourly_limit |
INTEGER | plafond par fenêtre d'une heure (par défaut 500), appliqué par le moteur. NULL = pas de plafond ; 0 ou un nombre négatif est refusé |
send_count_today |
INTEGER | compteur brut : remis à 1 au premier envoi d'un nouveau jour UTC. Tant que rien n'est parti, il montre la veille — l'écran et l'outil MCP le remettent à zéro à la lecture (capacite.aujourdhui) |
send_count_hour |
INTEGER | compteur brut de la fenêtre glissante ouverte au premier envoi (last_reset_hour) ; remis à 1 au premier envoi après last_reset_hour + 1 h, pas à l'heure pleine |
is_active |
BOOLEAN | si false, rien ne part par ce serveur : ses envois en file sont retenus 24 h après leur échéance, puis échouent en smtp_indisponible |
is_default |
BOOLEAN | serveur des envois transactionnels (reçu de commande, lien de connexion, send_test_email sans list_id) : le premier actif, is_default d'abord. Un seul à la fois : en marquer un retire la marque des autres |
notes |
TEXT | libre |
last_test_at, last_test_ok, last_test_error |
le dernier test (vérification ou e-mail d'essai) et son résultat | |
last_error, last_error_at, last_error_kind |
la dernière erreur de serveur (identifiant, connexion, relais) vue par le moteur ou par un test |
|
last_success_at |
TIMESTAMPTZ | le dernier envoi réussi (séquence, confirmation ou e-mail d'essai) |
updated_at date la dernière modification par l'opérateur : le moteur ne l'écrit plus à chaque envoi (il sert aussi de clé au cache des connexions SMTP).
Créer un SMTP server
Depuis l'écran
Email Marketing → Serveurs SMTP, puis « Nouveau serveur » : nom, adresse d'expédition, hôte, port, identifiant et mot de passe, plafonds du jour et de l'heure. Enregistré, le serveur s'ouvre dans son panneau : « Vérifier la connexion » s'y connecte, et s'identifie si un identifiant est saisi, sans envoyer d'e-mail (un refus de relais ne se voit qu'à l'envoi) ; « Envoyer un essai » envoie un vrai e-mail à l'adresse saisie en bas du panneau. La vérification porte toujours sur la configuration enregistrée : dans la boîte, le bouton dit « Enregistrer et vérifier » dès qu'un champ a changé.
Chaque ligne affiche le verdict rendu par le serveur (voir la section « Peut envoyer ? » plus bas) et, à droite, le seul geste qui répare : « Réactiver » (désactivé), « Corriger » (mot de passe, identifiant ou relais refusés : la boîte s'ouvre sur le champ fautif) ou « Vérifier » (jamais vérifié, injoignable). Un plafond atteint n'a pas de geste : les envois attendent.
Via MCP (Mailgun)
// First call — Tier-2 → returns confirmation_required
{
"name": "create_email_smtp_server",
"arguments": {
"name": "Mailgun production",
"host": "smtp.mailgun.org",
"port": 587,
"secure": false,
"auth_user": "postmaster@mg.brandfr.com",
"auth_pass": "redacted-api-key-from-mailgun",
"from_email": "marie@brandfr.com",
"from_name": "Marie de Brand FR",
"reply_to": "marie@brandfr.com",
"daily_limit": 10000,
"hourly_limit": 1000
}
}
// Response (first call)
{
"status": "confirmation_required",
"tool": "create_email_smtp_server",
"summary": "Will create SMTP relay 'Mailgun production' (smtp.mailgun.org:587). Password is sensitive — confirm to proceed.",
"preview": { "name": "Mailgun production", "host": "smtp.mailgun.org", ... },
"confirm_token": "cfm_a1b2c3d4e5f6g7h8i9j0k1l2"
}
// Second call — same args + confirm_token
{
"name": "create_email_smtp_server",
"arguments": {
"name": "Mailgun production",
"host": "smtp.mailgun.org",
"port": 587,
"secure": false,
"auth_user": "postmaster@mg.brandfr.com",
"auth_pass": "redacted-api-key-from-mailgun",
"from_email": "marie@brandfr.com",
"from_name": "Marie de Brand FR",
"reply_to": "marie@brandfr.com",
"daily_limit": 10000,
"hourly_limit": 1000,
"confirm_token": "cfm_a1b2c3d4e5f6g7h8i9j0k1l2"
}
}
// Response (second call)
{ "status": "ok", "smtp_server": { "id": 1, "name": "Mailgun production", ... } }
C'est un tool Tier-2 parce que le password est sensible et qu'on veut éviter qu'un LLM hallucinant créé 10 relays avec des credentials random. Scopes requis : email:write ET settings:write.
Recettes par provider
Mailgun
host smtp.mailgun.org
port 587
secure false
auth_user postmaster@mg.tondomaine.com ← vu dans Mailgun Dashboard → Domains
auth_pass <SMTP password> ← Dashboard → Domains → Domain settings → SMTP credentials
Vérifie que le domaine d'envoi est vérifié SPF + DKIM dans Mailgun avant de balancer du trafic. Sinon délivrabilité catastrophique.
SendGrid
host smtp.sendgrid.net
port 587
secure false
auth_user apikey ← littéralement la chaîne "apikey"
auth_pass SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxx ← API key SendGrid avec permission "Mail Send"
Postmark
host smtp.postmarkapp.com
port 587
secure false
auth_user <Server Token> ← Postmark → Servers → API Tokens → Server Token
auth_pass <same Server Token> ← oui, user et pass sont identiques chez Postmark
Réserve Postmark au transactionnel. Si tu envoies une newsletter via un Server transactionnel, ton compte est suspendu dans les 24h.
Amazon SES
host email-smtp.<region>.amazonaws.com (ex: email-smtp.eu-west-1.amazonaws.com)
port 587
secure false
auth_user AKIA... ← SMTP credentials générées dans la console SES
auth_pass <SMTP password> ← pas l'access key — un mot de passe dédié SMTP
Avant de scaler : sors du sandbox SES (Request production access) sinon tu ne peux envoyer qu'à des emails préalablement vérifiés.
Gmail / Workspace
host smtp.gmail.com
port 465
secure true
auth_user ton.email@gmail.com
auth_pass <App Password> ← Google Account → Security → App passwords (2FA requis)
À éviter pour du sérieux. Gmail bloque autour de 500 envois/jour et n'envoie pas de feedback loop propre — tes bounces n'arrivent jamais dans email_suppression.
Tester un SMTP
Vérifier la connexion, sans rien envoyer
Depuis l'écran (bouton « Vérifier la connexion ») ou par l'API :
POST /admin/api/email/smtp-servers/:id/test corps : {} (sans `to`)
Trackily ouvre la connexion, négocie le TLS et s'identifie si un identifiant est configuré (transport.verify() de nodemailer) — aucun e-mail ne part. Sans identifiant, rien n'est éprouvé au-delà de la connexion, et la réponse le dit (« Connexion réussie (aucun identifiant configuré : l'identification n'a pas été testée) »). Une vérification ne fait ni MAIL FROM ni RCPT TO : elle ne voit pas un refus de relais ou d'expéditeur, et ne le lève pas. Avec { "to": "moi@example.com" }, la même route envoie un vrai e-mail d'essai, qui, lui, éprouve tout.
Réponse : { ok, type, message, erreur, message_id? } — type vaut verification ou envoi, message est une phrase en français, erreur vaut null ou { categorie, detail } (categorie : identifiant, connexion, relais, destinataire, autre ; detail : la réponse brute du serveur). Un échec répond en HTTP 400. Si le mot de passe ne se déchiffre plus (clé de chiffrement changée), rien n'est tenté.
Dans les deux cas le résultat est noté sur le serveur (last_test_*) : c'est ce qui fait passer le verdict de « Jamais vérifié » à « Peut envoyer », ou à « Le serveur refuse l'identifiant ». Un test ne compte pas dans les plafonds.
Par MCP
Avant de lier un SMTP à une liste live, envoie un test :
{
"name": "send_test_email",
"arguments": {
"to": "moi@example.com",
"subject": "[Trackily test] Mailgun production",
"body_html": "<p>Si tu lis ceci, le SMTP marche.</p>"
}
}
Sans list_id, le tool prend le premier SMTP actif, is_default=true d'abord. Avec list_id, il prend le SMTP de cette liste. Le mail part synchrone, sans passer par la queue, sans logger dans email_sends, et n'est pas noté sur le serveur.
Quotas et compteurs
Chaque SMTP server stocke deux compteurs bruts :
send_count_today— le jour est le jour UTC : le compteur repart à 1 au premier envoi après minuit UTC ;send_count_hour— une fenêtre glissante d'une heure, ouverte au premier envoi (last_reset_hour) : le compteur repart à 1 au premier envoi aprèslast_reset_hour + 1 h. Ce n'est pas l'heure pleine.
Tant qu'aucun envoi ne part, ces colonnes montrent la veille ou l'heure précédente. L'écran et l'outil MCP appliquent la remise à zéro à la lecture (capacite) avec la même fonction que le moteur.
À chaque tick (toutes les 60 s), le moteur :
- Une fois toutes les 10 minutes, fait échouer les envois retenus faute de serveur depuis plus de 24 h après leur échéance (
smtp_indisponible, voir plus bas). Un tick ne démarre pas tant que le précédent tourne encore. - Choisit les envois dus dont le serveur effectif peut partir. Le serveur effectif est celui figé sur l'envoi à sa mise en file s'il est actif, sinon celui de la liste s'il l'est. Il n'y a aucun repli sur le serveur
is_defaultpour les séquences et les confirmations : ce repli ne vaut que pour les envois transactionnels. - Avant chaque envoi, vérifie les deux plafonds : le jour (
daily_limit) et l'heure (hourly_limit), avec la même fonction que le verdict de l'écran. Plafond atteint : l'envoi reste enqueued, sans consommer de tentative, et repart à la fin de la fenêtre horaire ou le lendemain à minuit UTC. Même règle pour un mot de passe qui ne se déchiffre plus, ou absent derrière un identifiant : rien n'est tenté, l'envoi attend qu'il soit corrigé. Les envois qui attendent ne bloquent pas ceux des autres serveurs. Un serveur dont une erreur de serveur tient encore (identifiant ou relais refusé, connexion en panne — la règle du verdict) ne reçoit qu'un envoi-sonde par tick ; les autres attendent sans tentative consommée. Un serveur qui échoue pendant un tick n'est pas retenté avant le tick suivant. Le premier envoi qui passe lève l'erreur, et le serveur repart en entier. - Envoie, marque le send
sent, puis incrémente les deux compteurs et notelast_success_atdans la même requête.
Les envois transactionnels (reçus, liens de connexion, send_test_email, tests) ne sont pas comptés dans ces plafonds.
Les limites évitent de griller ta réputation : Mailgun te coupe à ~10k/heure sur un compte fresh, SES te restreint dans les 200/sec en sandbox. Configure ces colonnes pour rester sous la limite officielle de ton provider.
Serveur désactivé, supprimé, ou liste sans SMTP
L'envoi n'échoue plus sur-le-champ : il est retenu, sans consommer de tentative, jusqu'à 24 h après son échéance (l'heure où il devait partir). Pendant ce délai, réactiver le serveur, ou rattacher un serveur actif à la liste, le fait partir au tick suivant. Passé ce délai, il échoue avec le motif smtp_indisponible. Un envoi échoué n'est jamais reprogrammé : la déduplication abonné + étape l'en empêche.
Erreurs de serveur et rebonds
Chaque échec d'envoi est classé en lisant le code nodemailer (err.code, err.responseCode, err.command) et le message :
| Catégorie | Exemples | Ce que fait le moteur |
|---|---|---|
identifiant |
EAUTH, 535 5.7.8, 530 Authentication required |
Erreur de serveur : jamais un rebond, jamais de suppression. Notée sur le serveur (last_error*), l'envoi est retenté pendant environ 24 h (5 min, 15 min, 30 min, 1 h, 2 h, 4 h, 6 h, 10 h), puis échoue en smtp_indisponible (<catégorie>) |
connexion |
ECONNREFUSED, ETIMEDOUT, Greeting never received, certificat |
idem |
relais |
Relay access denied, Sender address rejected, identité SES non vérifiée |
idem : un refus de relais n'est plus pris pour un rebond (avant, chaque destinataire finissait en suppression globale) |
destinataire |
550 5.1.1 User unknown à l'étape RCPT TO |
5xx : rebond dur, l'adresse entre dans email_suppression. 4xx (greylisting, boîte pleine) : retenté comme un rebond temporaire |
autre |
contenu refusé, quota du fournisseur | retenté 5 fois (1 min → 6 h), puis failed |
« Peut envoyer ? » — le verdict
GET /admin/api/email/smtp-servers?detail=1 et l'outil MCP list_email_smtp_servers rendent, pour chaque serveur, un verdict calculé par la même fonction que le moteur (lib/email-smtp-sante.js). Sans detail, la route rend le tableau d'avant, inchangé.
Les cas sont évalués dans l'ordre : le premier qui s'applique l'emporte.
motif |
etat |
Peut envoyer ? | Quand |
|---|---|---|---|
desactive |
inactif |
non | is_active = false |
mot_de_passe_illisible |
bloque |
non | le mot de passe ne se déchiffre plus (clé de chiffrement changée) |
mot_de_passe_absent |
bloque |
non | un identifiant sans mot de passe |
identifiant_refuse |
bloque |
non | la dernière erreur de serveur est un refus d'identifiant, plus récente que le dernier succès |
relais_refuse |
bloque |
non | idem pour un refus de relais ou d'expéditeur |
injoignable |
bloque |
non | erreur de connexion, et aucun succès depuis plus d'une heure — ou aucun succès connu (le libellé ne dit alors pas « depuis plus d'une heure ») |
plafond_heure |
attente |
non | plafond de l'heure atteint ; reprend_a = fin de la fenêtre |
plafond_jour |
attente |
non | plafond du jour atteint ; reprend_a = minuit UTC. Si les deux plafonds sont atteints, c'est celui-ci |
jamais_verifie |
a_verifier |
null (« — ») |
aucun envoi ni vérification réussis connus, même après un test raté (le libellé cite alors la catégorie du test) |
null |
pret |
oui | sinon |
« Dernier succès » veut dire : le plus récent entre un envoi réussi et une vérification réussie — mais une vérification ne lève que ce qu'elle éprouve. Elle lève une erreur de connexion ; un refus d'identifiant seulement si un identifiant est configuré (sinon elle ne s'identifie pas) ; un refus de relais jamais : seul un envoi réussi (moteur ou e-mail d'essai) le lève. Trois avertissements ne bloquent pas : rejets_eleves (au moins 5 % de refus immédiats sur 7 jours, sur au moins 20 envois — les rebonds différés et les plaintes ne remontent pas jusqu'ici), erreur_recente (une erreur de serveur récente que le moteur est en train de retenter) et echecs_sans_envoi (des envois ont échoué sur 24 h et aucun n'est parti, par exemple un compte mis en pause par le fournisseur).
Les échecs et les refus comptés sur 24 h et sur 7 jours sont datés de leur fin de vie : chaque passage en failed ou bounced pose scheduled_at à l'heure de l'événement. Une expiration (24 h après l'échéance) et le rebond d'un envoi parti en retard y figurent donc. derniere_erreur porte tient (l'erreur retient-elle encore le serveur ?) et reglee_a (la preuve qui l'a levée) ; dernier_test.type est la catégorie notée au moment du test.
La réponse porte aussi une bande sante : le compte des serveurs par état, la capacité restante des serveurs prêts, l'activité sur 24 h, la file, et une seule alerte. Elle se déclenche quand aucun serveur n'est actif, quand des listes utilisent un serveur désactivé ou bloqué, ou quand des envois attendent sans serveur. Un plafond atteint ne déclenche jamais d'alerte : c'est de l'attente, pas une panne.
Lister les SMTP
// Request
{ "name": "list_email_smtp_servers", "arguments": {} }
// Response (excerpt) — passwords NEVER returned
{
"status": "ok",
"smtp_servers": [
{
"id": 1,
"name": "Mailgun production",
"host": "smtp.mailgun.org",
"port": 587,
"auth_user": "postmaster@mg.brandfr.com",
"from_email": "marie@brandfr.com",
"from_name": "Marie de Brand FR",
"daily_limit": 10000,
"hourly_limit": 1000,
"send_count_today": 1247,
"send_count_hour": 89,
"is_active": true,
"is_default": true,
"capacite": { "aujourdhui": 1247, "reste_jour": 8753, "heure": 89, "reste_heure": 911, "plafond": null, "disponible": true, "reprend_a": null },
"mot_de_passe": "present",
"verdict": { "peut_envoyer": true, "etat": "pret", "motif": null, "libelle": "Peut envoyer : encore 8 753 aujourd’hui, 911 cette heure.", "avertissements": [] }
}
],
"sante": { "serveurs": { "total": 1, "peuvent_envoyer": 1 }, "alerte": null }
}
Note : auth_pass n'est jamais retourné — seulement mot_de_passe (present, absent, illisible). Le moteur le déchiffre en interne juste avant nodemailer.createTransport(). send_count_* sont les compteurs bruts de la table ; capacite donne les compteurs effectifs.
DNS — SPF, DKIM, DMARC
Branche ces 3 enregistrements DNS sur le domaine d'envoi AVANT de commencer à envoyer en volume :
- SPF :
TXT @ "v=spf1 include:mailgun.org ~all"(adapte selon le provider) - DKIM : enregistrement fourni par le provider lors du setup du domaine
- DMARC :
TXT _dmarc "v=DMARC1; p=quarantine; rua=mailto:dmarc@tondomaine.com"
Sans ça, Gmail / Outlook envoient direct en spam. Et leur DMARC report te dira combien d'emails sont rejetés.
Erreurs courantes
Invalid login: 535-5.7.8 Username and Password not accepted— credentials faux ou App Password manquant (Gmail).Greeting never received— port bloqué par le firewall du host. Ouvre 587 et 465 en sortant.- Des envois en
failedavecsmtp_indisponible— aucun serveur actif pendant 24 h après leur échéance (serveur désactivé, supprimé, liste sans SMTP), ou une erreur de serveur (smtp_indisponible (identifiant),(connexion),(relais)) qui a duré environ 24 h. Vérifieis_active=true, la connexion depuis l'écran, et que lefrom_emailest sur un domaine vérifié SPF/DKIM par le provider. - Compteur
send_count_todayqui ne repart pas — c'est normal : la colonne ne repart qu'au premier envoi du nouveau jour UTC. L'écran etlist_email_smtp_serversaffichent le compteur effectif (capacite). - Emails arrivent en spam alors que tout est vert chez le provider — body HTML trop lourd (>100 KB), trop de liens externes, ratio image/text trop élevé, ou subject keyword-spammy ("FREE", "100% guaranteed"…).
Voir aussi
- Lists — où on attache un SMTP à une liste
- Sequences — ce qui passe par le SMTP
- Suppression — où atterrissent les bounces du SMTP
- Magic Link — utilise aussi le SMTP
is_default=true