2mail — SMTP relay from Belgian infrastructure

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

1

Collect your details

Log in to your 2mail account and retrieve your SMTP server, username and password there.

2

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.

3

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.

4

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.
PortThe port that matches your chosen encryption; your account shows which one.
EncryptionTLS. Never send unencrypted — your credentials would otherwise travel in plain text.
UsernameThe SMTP login of your 2mail account.
PasswordThe 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.

Password resets and email verification from Django
Scheduled reports and exports from cron jobs
Alerts from monitoring and data processing scripts
Invoices and confirmations from a web application
Error reports to administrators

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.
SPF, DKIM & DMARC explained About deliverability

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.

Frequently asked questions about SMTP for Python

smtplib and Django send, but do not provide deliverability: that depends on the server you send through. A local mail function or a fresh server has no authentication and no reputation. An SMTP relay with correct SPF, DKIM and DMARC records fixes that.
The SMTP backend as the email backend, the relay's host, port 587, the STARTTLS option on, your username and password, and a default sender address on your own domain. Keep the login in environment variables, not in the settings themselves.
Connect to the host on port 587, call starttls, log in with username and password and send the message. For several messages, use the same connection instead of opening a new one per message.
In your 2mail account, per application. They are account-specific, so we do not publish them on the website. Preferably create a separate login per script or application, so you can revoke it independently and track that traffic on its own.
Call us
Send an email