SMTP for Python — mail from your script or Django app that arrives
Python has SMTP built in: with smtplib you send a message in a handful of lines, and Django wraps that in its own mail layer with EMAIL settings. What neither provides is deliverability — that depends on the server you send through.
Whether you have a standalone script, a Flask app or a Django project: the settings are the same five details. You point them at your relay and the code that builds the messages stays untouched.
Why it goes wrong without a relay
Python mail often comes from the background: a nightly report, a cron job that sends invoices, a script that warns when a reading falls outside the margin. Nobody is watching — so nobody notices that the receiving server quietly refuses them. With Django there is the classic go-live surprise on top: in development everything goes to the console, in production to real providers that do check reputation.
Setting up SMTP in Python
Collect your details
Log in to your 2mail account and retrieve your SMTP server, username and password there.
Fill them in inside Python
In a Django project you set the EMAIL settings in the settings: the SMTP backend as the email backend, the relay's host, port 587, the STARTTLS option on, the username and password, and a default sender address on your own domain. If you work directly with smtplib, you connect to the host on port 587, call starttls, log in with username and password and then send the message. In both cases, read the details from environment variables, not from the code.
Authenticate your domain
Make sure SPF, DKIM and DMARC are set correctly for your domain, so recipients can verify that the mail comes from you.
Test
From your platform, send a test message to an address at a major provider and check that it arrives in the inbox and not in the spam folder.
The fields Python asks for
Every platform names them slightly differently, but it is always the same five. The actual values are tied to your account and can be found in your 2mail account — take them from there, so you are sure to use the right ones.
| Field | What you enter |
|---|---|
| SMTP server (host) | The server name from your 2mail account. |
| Port | The port that matches your chosen encryption; your account shows which one. |
| Encryption | TLS. Never send unencrypted — your credentials would otherwise travel in plain text. |
| Username | The SMTP login of your 2mail account. |
| Password | The matching password. Never use the password of an employee's mailbox. |
SMTP configuration for Python in code
Working examples for port 587 with STARTTLS, ready to copy. Replace the sender address with an address on your own domain and check the server name and login in your 2mail account. The password belongs in an environment variable — never in the code and never in version control.
smtplib: connecting on 587 with starttls()
The standard library is enough. The order matters: starttls() first, and only then login() — otherwise your password crosses the wire unencrypted, or the relay refuses the sign-in. Use smtplib.SMTP, not SMTP_SSL: the latter belongs with port 465. Login and password come from environment variables.
import os
import smtplib
import ssl
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "Example <noreply@example.com>"
msg["To"] = "you@example.org"
msg["Subject"] = "SMTP test"
msg.set_content("Sent through the 2mail SMTP relay.")
context = ssl.create_default_context()
with smtplib.SMTP("relay.2mail.eu", 587, timeout=10) as server:
server.starttls(context=context) # upgrade to TLS first, log in afterwards
server.login(os.environ["SMTP_USERNAME"], os.environ["SMTP_PASSWORD"])
server.send_message(msg) # call again for the next message, same connection Django: the EMAIL_* settings
Django needs no extra package. Set the SMTP backend explicitly: in development it is often set to the console backend, and then production sends nothing while everything appears to work. sendtestemail is a built-in management command that lets you check the settings without having to call a view or a task.
# settings.py
import os
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "relay.2mail.eu"
EMAIL_PORT = 587
EMAIL_USE_TLS = True # STARTTLS - never together with EMAIL_USE_SSL
EMAIL_HOST_USER = os.environ["SMTP_USERNAME"]
EMAIL_HOST_PASSWORD = os.environ["SMTP_PASSWORD"]
EMAIL_TIMEOUT = 10
DEFAULT_FROM_EMAIL = "Example <noreply@example.com>"
SERVER_EMAIL = "noreply@example.com" # sender of error reports to ADMINS
# Test from the shell:
# python manage.py sendtestemail you@example.org Flask-Mail: the same five details
If you work with Flask, Flask-Mail asks for the same details under different names. The same rule applies: MAIL_USE_TLS for port 587, and MAIL_USE_SSL stays off.
import os
from flask import Flask
from flask_mail import Mail
app = Flask(__name__)
app.config.update(
MAIL_SERVER="relay.2mail.eu",
MAIL_PORT=587,
MAIL_USE_TLS=True, # STARTTLS
MAIL_USE_SSL=False,
MAIL_USERNAME=os.environ["SMTP_USERNAME"],
MAIL_PASSWORD=os.environ["SMTP_PASSWORD"],
MAIL_DEFAULT_SENDER="noreply@example.com",
)
mail = Mail(app) Which emails Python sends
As soon as the relay is set up, all of these run through 2mail — you do not have to configure them one by one.
Pitfalls with Python
TLS option and port have to match
Django has two separate TLS settings: one for STARTTLS and one for TLS from the start. Switch on only the STARTTLS setting; that is the one for port 587. Both at once produces a configuration error; neither sends your login unencrypted, which a relay rightly refuses.
One connection per batch, not per message
A script that opens a new SMTP connection for each of a thousand messages behaves like an attacker. Open one connection, send the batch, close it. Django has a function for sending several messages over a single connection.
Silent failures in background jobs
An exception in a cron job disappears into a log file nobody reads. Catch errors when sending and report them somewhere they get noticed — and hook up 2mail's webhooks for delivery and bounce, so you also see what goes wrong after handover.
Common error messages with Python
The messages you get when host, port, encryption or login are wrong — quoted literally, so you recognise them in your log, with the cause and the fix next to them.
| Message | Cause | Fix |
|---|---|---|
smtplib.SMTPAuthenticationError: (535, b'5.7.8 Error: authentication failed: …') | The relay rejects the login: a wrong username or password, or an environment variable that has a different value in the cron or service context than in your shell. | Print what the process actually reads (the username, never the password) and use the SMTP login from your 2mail account — not the password of a mailbox. |
smtplib.SMTPNotSupportedError: SMTP AUTH extension not supported by server. | login() is called before starttls(). A relay only offers authentication once the connection is encrypted; on the still unencrypted connection, AUTH simply does not exist. | Call server.starttls(context=context) first and only then server.login(…). In Django: set EMAIL_USE_TLS = True. |
ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:…) | The code uses smtplib.SMTP_SSL (or, in Django, EMAIL_USE_SSL) on port 587. Python then starts with a TLS handshake right away, whereas the relay expects plain SMTP first on that port and only switches after STARTTLS. The classic 465-versus-587 mix-up. | Use smtplib.SMTP(…, 587) followed by starttls(), or in Django EMAIL_USE_TLS = True with port 587. |
smtplib.SMTPServerDisconnected: Connection unexpectedly closed | The connection dropped before or during the conversation. Usually a script that keeps a connection open and reuses it after a long pause, while the server has closed it for inactivity in the meantime — or a plain SMTP connection to a port that expects TLS from the first byte. | Open one connection per batch of messages and close it afterwards; catch the exception and reconnect. Check that you are using port 587 with starttls(). |
ValueError: EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set one of those settings to True. | Both TLS options are switched on in the Django settings — often because one comes from a shared base file and the other from the production settings. | Keep only EMAIL_USE_TLS = True and set EMAIL_USE_SSL = False. |
A different platform?
SMTP is a standard protocol: any system that speaks SMTP — a CRM, an ERP, a custom application or a server — can send through 2mail using the same five fields.