Tester SMTP sous Linux : de la connexion TCP à l’e-mail délivré
Lorsqu’une appliance ne délivre plus d’e-mails, un test SMTP manuel est plus utile que n’importe quel journal. Comment vérifier couche par couche avec les outils intégrés, ce que signifient les différents symptômes d’erreur et pourquoi un équilibreur de charge fausse le diagnostic.
Lorsqu’une passerelle de messagerie cesse soudainement de délivrer des messages, les journaux de l’appliance n’affichent souvent que le résultat final : une livraison échoue, la file d’attente grossit, un message d’erreur indique un délai d’expiration. Seul un test manuel depuis la ligne de commande permet d’en déterminer la véritable cause. SMTP est un protocole en texte clair qui peut entièrement être utilisé à la main, ce qui en fait un outil de diagnostic disponible partout sans installation supplémentaire.
Deuxième raison de recourir à un test manuel : il est généralement impossible d’installer quoi que ce soit sur les appliances. Pas de gestionnaire de paquets, pas de droits root, pas de swaks. Toutes les étapes suivantes fonctionnent donc avec ce qui est déjà présent sur pratiquement tous les systèmes Linux.
Distinguer les couches
Un échec d’envoi d’e-mail peut survenir à cinq niveaux différents, chacun produisant un symptôme distinct :
- Résolution de noms : L’hôte cible ne peut pas être traduit en adresse IP.
- Connexion TCP : La connexion au port ne s’établit pas ou est réinitialisée.
- Dialogue SMTP : La connexion est établie, mais le serveur refuse l’expéditeur, le destinataire ou le contenu.
- Chiffrement du transport : STARTTLS est absent, le certificat est invalide ou la version TLS ne convient pas.
- Vérification de l’expéditeur : L’e-mail est accepté puis rejeté chez le destinataire en raison de SPF, DKIM ou DMARC.
Le diagnostic gagne énormément en efficacité lorsque vous vérifiez ces niveaux l’un après l’autre et séparément, au lieu d’envoyer immédiatement un e-mail de test complet. Un essai global échoué vous dit seulement que quelque chose ne fonctionne pas. La vérification par couches vous dit quoi.
Étape 1 : résolution de noms
getent hosts relay.example.com
Si la sortie reste vide, aucun serveur de noms n’est accessible depuis cet hôte ou il ne répond pas aux noms externes. Cela arrive régulièrement en pratique : les appliances situées dans des zones isolées ne reçoivent souvent qu’un résolveur interne qui ne connaît que ses propres zones.
cat /etc/resolv.conf; grep hosts: /etc/nsswitch.conf
Si la résolution est absente, testez directement avec l’adresse IP dans les étapes suivantes. C’est parfaitement suffisant pour le diagnostic et cela sépare clairement le problème DNS du problème de transport. En production, l’absence de résolution reste naturellement un constat distinct à corriger.
Étape 2 : accessibilité du port
Pour un simple test TCP, bash suffit. Le pseudo-périphérique /dev/tcp ouvre une connexion sans nécessiter l’installation de nc ou de telnet :
timeout 10 bash -c 'exec 3<>/dev/tcp/192.0.2.25/25' ; echo "exit=$?"
Le code de sortie est ici l’information essentielle :
| sortie | Signification |
|---|---|
0 | La connexion est établie, le port est ouvert |
124 | Délai d’expiration : les paquets sont rejetés, typiquement par une règle de pare-feu DROP |
1 | Refus immédiat (RST) ou absence de route |
En pratique, la différence entre 124 et 1 est l’indice le plus important. Un délai d’expiration signifie que quelqu’un rejette silencieusement les paquets sur le trajet, ce qui est presque toujours une règle de pare-feu. Un RST immédiat provient en revanche d’un système qui répond mais n’offre pas le service.
Vérifiez immédiatement les deux ports concernés, ainsi qu’une autre destination quelconque afin de déterminer si l’hôte est autorisé à établir des connexions sortantes :
for t in "192.0.2.25 25" "192.0.2.25 587" "1.1.1.1 443"; do
set -- $t
timeout 8 bash -c "exec 3<>/dev/tcp/$1/$2" 2>/dev/null
echo "$1:$2 -> exit=$?"
done
Si le test de contrôle échoue également, le système ne dispose généralement d’aucune sortie directe et le trafic doit passer par un relais interne ou un proxy. Nous verrons plus bas pourquoi ce cas est particulièrement délicat.
Si /dev/tcp est absent, le shell n’est pas bash. Sous sh, ash ou ksh, cette fonctionnalité n’existe pas, ce qui est souvent interprété à tort comme un problème réseau :
ps -p $$ -o comm= ; echo "BASH_VERSION=${BASH_VERSION:-keine bash}"
Étape 3 : écouter d’abord, ne rien envoyer
Un serveur SMTP envoie spontanément une bannière 220. Le test isolé le plus révélateur consiste donc à ouvrir une connexion et à ne rien faire :
{ exec 3<>/dev/tcp/192.0.2.25/25 && timeout 15 cat <&3; echo "[ende exit=$?]"; }
Ces quelques caractères distinguent deux situations entièrement différentes. Si une réponse 220 mail.example.com ESMTP arrive, l’hôte distant parle et toutes les erreurs ultérieures se situent dans le dialogue. Si rien n’arrive, ce n’est pas dû à une commande mal formulée de votre part, puisque vous n’en avez envoyé aucune.
Le descripteur de fichier reste ensuite ouvert dans le shell. Fermez-le avant de lancer le test suivant, sans quoi vous risquez de continuer à travailler avec une ancienne connexion qui n’est plus intacte :
exec 3<&- 3>&-
Étape 4 : le dialogue SMTP à la main
Une fois la bannière affichée, effectuez le dialogue complet. Il est important qu’un processus de lecture s’exécute en parallèle afin de voir chaque réponse au moment où elle arrive. Un script qui envoie d’abord tout puis lit ensuite ne vous montrera rien si l’échange s’interrompt en plein milieu du dialogue :
{
exec 3<>/dev/tcp/192.0.2.25/25
cat <&3 & R=$!
sleep 1; printf 'EHLO host.example.com\r\n' >&3
sleep 2; printf 'MAIL FROM:<absender@example.com>\r\n' >&3
sleep 2; printf 'RCPT TO:<empfaenger@example.net>\r\n' >&3
sleep 2; printf 'DATA\r\n' >&3
sleep 2; printf 'From: absender@example.com\r\nTo: empfaenger@example.net\r\nSubject: Relay-Test\r\n' >&3
printf 'Date: %s\r\nMessage-ID: <%s@example.com>\r\n\r\nTestnachricht\r\n.\r\n' "$(date -R)" "$(date +%s).$" >&3
sleep 3; printf 'QUIT\r\n' >&3
sleep 2; kill $R 2>/dev/null
}
Deux détails déterminent la réussite ou l’échec. SMTP exige CRLF comme fin de ligne, d’où printf avec \r\n et non echo. Et le point sur une ligne à lui seul termine la partie message ; il doit être envoyé sous la forme \r\n.\r\n.
Déroulement attendu : 220 à l’établissement de la connexion, 250 après EHLO, 250 2.1.0 après MAIL FROM, 250 2.1.5 après RCPT TO, 354 après DATA et enfin 250 2.0.0 Ok: queued as <id>. Notez l’identifiant de file d’attente. Il permet au fournisseur exploitant le service de suivre le message s’il n’arrive jamais chez le destinataire.
Le nom EHLO mérite votre attention : certains relais le vérifient par rapport aux DNS direct et inverse et répondent sinon par 501 ou 504. Utilisez le FQDN réel du système émetteur, et non son nom court.
Étape 5 : STARTTLS et certificat
Pour la connexion chiffrée, openssl s_client effectue lui-même la négociation STARTTLS puis transmet le canal à l’entrée standard :
openssl s_client -connect 192.0.2.25:25 -starttls smtp -tls1_2 -brief </dev/null
Si vous vous connectez par adresse IP parce que le DNS est indisponible, la vérification du nom d’hôte ne peut pas fonctionner. Le nom du certificat ne correspond alors pas à l’adresse numérique. SNI et le nom de vérification peuvent être définis explicitement, sans aucune requête DNS :
openssl s_client -connect 192.0.2.25:25 \
-servername mail.example.com -verify_hostname mail.example.com \
-starttls smtp -tls1_2 -brief </dev/null
Deux symptômes d’erreur apparaissent régulièrement ici et sont souvent mal interprétés.
« Didn’t find STARTTLS in server response, trying anyway » signifie que le serveur n’a pas proposé STARTTLS dans sa réponse EHLO. openssl envoie tout de même un ClientHello TLS, le serveur y voit des données de protocole invalides et la connexion se termine par wrong version number ou write:errno=32 (EPIPE). Ces deux messages sont des erreurs consécutives. L’information essentielle est : pas de STARTTLS. Vérifiez avec le dialogue en texte clair de l’étape 4 quelles capacités le serveur annonce réellement.
L’absence de STARTTLS sur un saut interne est souvent tout à fait normale. Si un équilibreur de charge transmet la connexion à la couche 4, ce n’est pas lui qui négocie TLS, mais le système situé derrière lui avec la destination réelle. Tester en texte clair sur le segment interne n’est alors pas un défaut de sécurité, mais simplement l’architecture.
Étape 6 : Python comme alternative
Si Python est disponible, vous évitez la gestion manuelle du temps avec sleep. La bibliothèque standard suffit, rien ne doit être installé :
#!/usr/bin/env python3
import smtplib, ssl
from email.message import EmailMessage
from email.utils import formatdate, make_msgid
msg = EmailMessage()
msg["From"] = "absender@example.com"
msg["To"] = "empfaenger@example.net"
msg["Subject"] = "Relay-Test"
msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid(domain="example.com")
msg.set_content("Testnachricht\n")
ctx = ssl.create_default_context()
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
s = smtplib.SMTP("192.0.2.25", 25, timeout=30, local_hostname="host.example.com")
s.set_debuglevel(1)
s.ehlo()
if s.has_extn("starttls"):
s.starttls(context=ctx, server_hostname="mail.example.com")
s.ehlo()
print("TLS:", s.sock.version(), s.sock.cipher()[0])
s.send_message(msg)
s.quit()
set_debuglevel(1) enregistre le dialogue complet, y compris tous les codes de réponse, et smtplib lit chaque réponse de manière synchrone. Une interruption apparaît sous la forme SMTPServerDisconnected, accompagnée de la dernière ligne reçue, plutôt que comme un Broken Pipe silencieux.
Deux points échouent souvent ici : server_hostname est indispensable lorsque vous vous connectez via une adresse IP, sinon Python vérifie le certificat par rapport à l’adresse numérique. Et si vous désactivez délibérément la vérification, check_hostname = False doit précéder verify_mode = ssl.CERT_NONE, sinon Python lève une ValueError.
Adresse d’expéditeur, SPF et alignement
Un test échoue étonnamment souvent non pas au niveau du transport, mais à cause de l’adresse d’expéditeur choisie. Trois points doivent être vérifiés au préalable.
Le domaine de l’expéditeur doit être un FQDN. Une adresse telle que test@meine-testdomain sans domaine de premier niveau est déjà refusée par de nombreux MTA lors de MAIL FROM, avec 501 ou 553.
Le domaine doit autoriser le chemin d’envoi utilisé. Un regard sur l’enregistrement SPF permet de voir si l’adresse sortante est couverte :
dig +short TXT example.com | grep spf1
Et lorsque DMARC est actif, c’est l’alignement qui décide. Si l’enregistrement contient aspf=s, le domaine dans l’enveloppe (MAIL FROM) et le domaine dans l’en-tête From: doivent correspondre exactement, et pas seulement être apparentés :
dig +short TXT _dmarc.example.com
Avec p=reject, un e-mail de test dont l’alignement ne convient pas disparaît silencieusement chez le destinataire, bien que votre relais l’ait accepté avec 250 queued. C’est la cause la plus fréquente des messages considérés comme envoyés avec succès côté expéditeur mais qui n’arrivent jamais.
Lorsqu’un équilibreur de charge s’intercale
Dans les environnements plus importants, une appliance envoie rarement directement sur Internet. Il est courant d’utiliser un serveur virtuel sur un équilibreur de charge, qui accepte la connexion, réécrit l’adresse vers une adresse définie via source NAT, puis la transmet vers l’extérieur. Cela a une conséquence fâcheuse pour le diagnostic.
Un serveur virtuel fonctionnant à la couche 4 confirme immédiatement la négociation TCP, avant même d’avoir lui-même établi une connexion vers la cible. Si cette seconde connexion échoue, le client voit une connexion établie avec succès puis immédiatement réinitialisée : Connection reset by peer, sans aucune bannière SMTP. L’erreur ne se situe alors ni chez vous ni chez la cible, mais dans le pool derrière le serveur virtuel, par exemple parce qu’un membre est marqué comme indisponible ou que le FQDN configuré ne peut pas être résolu.
Cela explique aussi pourquoi un test direct vers la cible Internet doit échouer lorsque la règle de transfert n’accepte que le trafic provenant de l’adresse SNAT déjà réécrite. Les connexions avec l’adresse source d’origine ne correspondent à aucune règle et sont rejetées. Dans de tels environnements, testez toujours le serveur virtuel prévu, et non la cible réelle.
Une seule ligne indique quelle adresse source votre système utilise pour une destination donnée. La valeur après src est précisément celle dont l’équipe réseau a besoin pour l’autorisation :
ip route get 192.0.2.25
Si le système se trouve derrière NAT, l’hôte distant ne voit pas cette adresse, mais l’adresse publique du périmètre. Il est impossible de la déterminer depuis l’intérieur tant qu’aucun trafic ne passe ; elle figure dans la règle NAT.
Symptômes d’erreur en un coup d’œil
| Observation | Cause probable |
|---|---|
Name or service not known | Aucune résolution de noms sur l’hôte |
| Délai d’expiration, sortie 124 | Le pare-feu rejette silencieusement (DROP) |
Connection refused | Aucun service sur le port ou règle REJECT |
| Connexion établie, pas de bannière, puis RST | L’équilibreur de charge accepte, mais le backend est inaccessible |
Didn't find STARTTLS | Le serveur ne propose pas de chiffrement du transport |
wrong version number, errno=32 | Erreurs consécutives après TLS forcé sans STARTTLS |
501 / 553 à MAIL FROM | Domaine expéditeur non FQDN ou non autorisé |
554 relay access denied | Adresse IP source non autorisée sur le relais |
250 queued, mais aucune livraison | Alignement SPF, DKIM ou DMARC chez le destinataire |
Tests de charge et limites de débit
Pour les tests de volume, une règle est souvent négligée au quotidien : le problème n’est pas le nombre de messages, mais le nombre de connexions. Les relais typiques autorisent quelques centaines de connexions par minute, mais des dizaines de milliers de messages. Gardez donc une session ouverte et envoyez-y de nombreuses enveloppes, plutôt que de vous reconnecter pour chaque message.
Dans smtplib, cela signifie simplement réutiliser plusieurs fois le même objet de connexion et rétablir la session de façon contrôlée après un nombre fixe de messages. En revanche, celui qui ouvre une nouvelle connexion pour chaque e-mail dépasse la limite de connexions bien avant la limite de messages et provoque des refus qui ressemblent à un problème du côté distant.
Conclusion
Le test SMTP manuel n’est pas une solution de secours pour les environnements dépourvus d’outils, mais le diagnostic le plus précis disponible dans l’exploitation de la messagerie. Il distingue clairement la résolution de noms, l’accessibilité, le dialogue protocolaire et le chiffrement, et fournit un résultat univoque à chaque niveau. En écoutant d’abord, puis en menant le dialogue à la main et en prenant les codes de réponse au sérieux, vous obtenez en quelques minutes un constat permettant d’étayer un ticket auprès de l’équipe réseau ou du fournisseur : avec l’adresse source, le port cible, le comportement observé et le code de sortie.

Commentaires
Les commentaires sont chargés depuis GitHub / Giscus.