Trackily Docs

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.

Email — SMTP

Concept

Pourquoi pas du SMTP direct vers gmail.com / outlook.com ? Trois raisons :

  1. 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é.
  2. 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.
  3. 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ès last_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 :

  1. 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.
  2. 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_default pour les séquences et les confirmations : ce repli ne vaut que pour les envois transactionnels.
  3. 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 en queued, 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.
  4. Envoie, marque le send sent, puis incrémente les deux compteurs et note last_success_at dans 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 failed avec smtp_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érifie is_active=true, la connexion depuis l'écran, et que le from_email est sur un domaine vérifié SPF/DKIM par le provider.
  • Compteur send_count_today qui ne repart pas — c'est normal : la colonne ne repart qu'au premier envoi du nouveau jour UTC. L'écran et list_email_smtp_servers affichent 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