Python SMTP-verzending: STARTTLS op 587 of TLS op 465?
STARTTLS op 587 en impliciete TLS op 465 dragen beide een Python-verzending, maar slechts één past bij jouw relay. Vergelijk de handshake en de faalmodi.
Beide verbindingsmodi dragen hetzelfde bericht naar hetzelfde relayaccount, en het verschil zit erin waar TLS begint, niet wat er wordt verzonden. De ene opent in platte tekst en wordt binnen de verbinding geüpgraded; de andere is vanaf de eerste byte versleuteld. Dat ene verschil bepaalt wat een strikt relaybeleid accepteert, wat een tussenliggend apparaat kan waarnemen en welk foutbericht je een middag lang leest. Zo kies ik tussen beide wanneer een applicatie op een privénetwerk een bericht aan een interne verzenddienst overhandigt.
Waar de twee modi echt uiteengaan
De taak is in beide gevallen identiek: authenticeren als een goedgekeurd account, een toegestane afzender noemen, ontvangers opgeven en wachten tot de relay de verantwoordelijkheid overneemt. Wat verandert, is de volgorde van de handelingen. Poort 587 begint als een SMTP-sessie in platte tekst, adverteert STARTTLS in het EHLO-antwoord en wordt alleen geüpgraded als de client daarom vraagt. Poort 465 verwacht een TLS-handshake voordat er ook maar een SMTP-opdracht wordt uitgesproken.
Verzending en relay zijn verschillende taken, daarom zijn de poortnummers geen uitwisselbare gewoonten. RFC 6409 beschrijft berichtverzending als een eigen dienst, en RFC 8314 behandelt de impliciete-TLS-variant die de meeste moderne relays op 465 aanbieden. Als jouw relay een van de twee documenteert, gaat die documentatie voor op elke voorkeur die je bij een vorige werkgever hebt opgedaan.
Wat STARTTLS op 587 je oplevert
De upgrade gebeurt binnen het SMTP-gesprek, dus de relay kan hem aankondigen en de client kan weigeren verder te gaan zonder. In Python gooit starttls() een uitzondering wanneer de server de uitbreiding niet aanbiedt, wat een downgradepoging omzet in een zichtbare fout in plaats van een stille login in platte tekst. Dat gedrag is het belangrijkste argument voor de modus, en het is de moeite waard er bewust op te vertrouwen en niet per ongeluk.
Het blootstellingsvenster is klein maar reëel: de eerste EHLO-uitwisseling is leesbaar, en niets vóór de upgrade draagt inloggegevens. Op een privépad tussen een applicatiesubnet en een interne relay is dat venster meestal aanvaardbaar. Op een pad dat je niet beheert, is dat de reden waarom sommige teams op de andere modus staan.
Een onderscheppingsapparaat dat de STARTTLS-mogelijkheid weghaalt, levert een mislukte verzending op, geen terugval. Dat is het gewenste resultaat, en het is ook de snelste manier om te ontdekken dat iets onderweg SMTP herschrijft.
Wat impliciete TLS op 465 verandert
Er is helemaal geen fase in platte tekst. De client voert een TLS-handshake uit zodra de socket opengaat, valideert het certificaat tegen zijn truststore en wisselt pas daarna EHLO, AUTH en het bericht uit. Niets kan de sessie downgraden omdat er geen platte-teksttoestand is om over te onderhandelen, en dat verwijdert een hele klasse van onderscheppingsfouten.
De ruil is diagnosticeerbaarheid. Een relay die op 465 met platte SMTP antwoordt, of een loadbalancer die TLS beëindigt met de verkeerde certificaatnaam, faalt bij de handshake en geeft je een SSL-fout in plaats van een mailfout. Wanneer dat gebeurt, zit het probleem onder de applicatie, en geen enkele berichtconstructie lost dat op.
Eén verzendfunctie, twee transporten
De code verschilt nauwelijks tussen de modi, daarom moet de keuze uit het relaybeleid komen en niet uit het gemak van de bibliotheek. Exporteer eerst de instellingen en voer daarna één script uit dat het transport op basis van de geconfigureerde poort kiest.
export SMTP_HOST=smtp.relay.internal export SMTP_PORT=587 export SMTP_USER=svc-notify export [email protected] export [email protected] export SMTP_PASSWORD="$(cat /run/secrets/smtp_password)"
import os, smtplib, ssl
from email.message import EmailMessage
ctx = ssl.create_default_context()
msg = EmailMessage()
msg["From"] = os.environ["MAIL_FROM"]
msg["To"] = os.environ["MAIL_TEST_TO"]
msg["Subject"] = "relay submission test"
msg.set_content("One authorized test message from the app.")
host = os.environ["SMTP_HOST"]
port = int(os.environ.get("SMTP_PORT", "587"))
if port == 465:
client = smtplib.SMTP_SSL(host, port, context=ctx, timeout=15)
else:
client = smtplib.SMTP(host, port, timeout=15)
client.ehlo()
client.starttls(context=ctx)
client.ehlo()
with client:
client.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
refused = client.send_message(msg)
print("refused:", refused)
create_default_context() verifieert de keten en de hostnaam, dus een privé-CA moet in de truststore worden geïnstalleerd in plaats van omzeild met een niet-geverifieerde context. send_message() retourneert een woordenboek met ontvangers die de relay weigerde; een leeg woordenboek betekent dat hij ze allemaal accepteerde. De time-out van vijftien seconden is een vertrekpunt, geen op jouw netwerk afgestemde waarde.
Houd het wachtwoord buiten de image en buiten de logs. Een geheim dat tijdens runtime wordt gemount en in de omgeving wordt gelezen, is prima; de omgeving in een logregel echoën is dat niet, en een debugspoor van de AUTH-uitwisseling ook niet.
Kies op basis van relaybeleid en pad, niet uit gewoonte
Drie voorwaarden beslissen het. Ten eerste, wat de relay werkelijk adverteert: een dienst die alleen op 587 met STARTTLS luistert, is op 465 niet bereikbaar, hoe graag je impliciete TLS ook wilt. Ten tweede, of het pad tussen de applicatie en de relay vertrouwd genoeg is om een leesbare EHLO-uitwisseling te accepteren. Ten derde, of iets tussen beide SMTP herschrijft of proxyt, want dat bepaalt of het downgraderisico theoretisch of reëel is.
Als de relay beide aanbiedt, neem ik 465 voor een dienst die onbeheerd draait en 587 voor een dienst waarbij iemand de eerste uitrol volgt, puur omdat de foutberichten op verschillende lagen wijzen. Die voorkeur is de mijne en het is geen beleid.
Wat je ook kiest, leg de poort, het vertrouwensmateriaal en het afzenderdomein vast samen met de uitrol. Stuur daarna één geautoriseerd testbericht en bevestig dat de relay het heeft geaccepteerd, waarbij je het verzendresultaat en elk later afleverrapport als afzonderlijke registraties bewaart. Een geslaagde overdracht aan de relay is het einde van de verantwoordelijkheid van de applicatie, niet het bewijs dat het bericht een inbox heeft bereikt.