Python: odeslání e-mailu přes API s requests, Django EMAIL_HOST, Flask-Mail a ověření webhooku
Minimální funkční příklady pro Python 3.12: odeslání přes REST API knihovnou requests s Idempotency-Key, nastavení Django SMTP backendu na smtp.topemail.cz, Flask-Mail, značky přes hlavičky a ověření HMAC podpisu webhooku pomocí hmac.compare_digest.
Pythonová aplikace má k odesílání e-mailů dvě cesty: REST API přes requests (ID e-mailu v odpovědi, idempotence, strukturované chyby), nebo standardní SMTP backend frameworku (nulová změna kódu, jen settings.py). Oba způsoby končí ve stejném účtu se stejnými stavy a webhooky. Článek ukazuje API klienta, nastavení Django 5 a Flask-Mail, a endpoint pro webhook email.status v Django i Flasku. Které zvolit, rozebírá srovnání SMTP a API.
REST API přes requests
# topemail.py
import os
import requests
API_URL = "https://topemail.cz/api/email/send"
TIMEOUT = 15
class TopEmailError(Exception):
def __init__(self, code: str, message: str, status: int):
super().__init__(f"{code}: {message}")
self.code = code
self.status = status
def send_email(payload: dict, idempotency_key: str | None = None) -> dict:
headers = {
"Authorization": f"Bearer {os.environ['TOPEMAIL_KEY']}", # "CLIENT_ID:SECRET"
"Content-Type": "application/json",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
resp = requests.post(API_URL, json=payload, headers=headers, timeout=TIMEOUT)
data = resp.json()
if not resp.ok:
raise TopEmailError(data.get("code", "unknown"), data.get("error", "Unknown error"), resp.status_code)
return data # {"ok": True, "emails": [{"id", "to", "status"}], "price": 0.06}
Použití:
from topemail import send_email, TopEmailError
try:
result = send_email(
{
"from": "Můj e-shop <objednavky@muj-eshop.cz>",
"to": [order.email],
"subject": f"Potvrzení objednávky {order.code}",
"html": render_to_string("emails/confirm.html", {"order": order}),
"text": render_to_string("emails/confirm.txt", {"order": order}),
"replyTo": "podpora@muj-eshop.cz",
"tags": {"order": order.code, "type": "confirm"},
},
idempotency_key=f"order-{order.code}-confirm",
)
order.confirm_email_id = result["emails"][0]["id"]
order.save(update_fields=["confirm_email_id"])
except TopEmailError as e:
if e.code == "invalid_recipient":
order.flag_bad_email() # neopakovat
elif e.status == 429 or e.status >= 500:
raise # Celery/RQ úlohu nechat opakovat se stejným klíčem
else:
logger.error("TopEmail config error: %s", e) # invalid_from, domain_not_verified…
except requests.RequestException:
raise # síť, opakovat
Idempotency-Key odvozujte z události (order-1042-confirm), ne z uuid4(); platí 24 hodin a chrání před dvojím odesláním při opakování Celery úlohy. Přílohu přidáte jako "attachments": [{"filename": "faktura.pdf", "content": base64.b64encode(pdf).decode(), "contentType": "application/pdf"}], celkem do 10 MB.
Django: SMTP backend
Nejmenší možná změna. Django posílá přes smtplib, stačí settings.py:
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.topemail.cz"
EMAIL_PORT = 587
EMAIL_USE_TLS = True # STARTTLS; pro 465 dejte EMAIL_PORT = 465 a EMAIL_USE_SSL = True
EMAIL_USE_SSL = False
EMAIL_HOST_USER = os.environ["TOPEMAIL_CLIENT_ID"]
EMAIL_HOST_PASSWORD = os.environ["TOPEMAIL_SECRET"]
EMAIL_TIMEOUT = 15
DEFAULT_FROM_EMAIL = "Můj e-shop <objednavky@muj-eshop.cz>"
SERVER_EMAIL = "server@muj-eshop.cz" # odesílatel chybových reportů ADMINS
EMAIL_USE_TLS a EMAIL_USE_SSL nesmí být zapnuté obě; Django při startu vyhodí chybu. DEFAULT_FROM_EMAIL i SERVER_EMAIL musí být na doméně ověřené v TopEmail, jinak relay odpoví 550 5.7.1 a Django vyhodí SMTPDataError.
Od té chvíle send_mail(), EmailMessage, reset hesla z django.contrib.auth i e-maily z balíčků třetích stran (allauth, django-rest-passwordreset) chodí přes relay. Značky přidáte přes headers:
from django.core.mail import EmailMultiAlternatives
msg = EmailMultiAlternatives(
subject=f"Potvrzení objednávky {order.code}",
body=text_version,
to=[order.email],
reply_to=["podpora@muj-eshop.cz"],
headers={"X-TopEmail-Tag-order": order.code, "X-TopEmail-Tag-type": "confirm"},
)
msg.attach_alternative(html_version, "text/html")
msg.send()
Relay odpoví 250 2.0.0 Queued as <id>; smtplib tuto odpověď nevrací, ale stav dohledáte v dashboardu podle značek, nebo vám ho pošle webhook. Pokud posíláte víc zpráv za sebou, použijte get_connection() a send_messages(), aby Django nedělalo nové TLS spojení pro každou.
Rychlý test: python manage.py sendtestemail vas@gmail.com, pak v Gmailu „Zobrazit originál" a zkontrolovat SPF, DKIM a DMARC PASS.
Flask-Mail
from flask import Flask
from flask_mail import Mail, Message
app = Flask(__name__)
app.config.update(
MAIL_SERVER="smtp.topemail.cz",
MAIL_PORT=587,
MAIL_USE_TLS=True,
MAIL_USE_SSL=False,
MAIL_USERNAME=os.environ["TOPEMAIL_CLIENT_ID"],
MAIL_PASSWORD=os.environ["TOPEMAIL_SECRET"],
MAIL_DEFAULT_SENDER=("Můj e-shop", "objednavky@muj-eshop.cz"),
)
mail = Mail(app)
msg = Message(
subject="Ověřovací kód",
recipients=[user.email],
body=f"Váš kód je {code}.",
extra_headers={"X-TopEmail-Tag-type": "otp"},
)
mail.send(msg)
Pro Flask bez rozšíření funguje přímo smtplib.SMTP("smtp.topemail.cz", 587) + starttls() + login("CLIENT_ID", "SECRET").
Webhook email.status
Při změně stavu pošleme POST s JSON (event, id, to, status, detail, tags, occurredAt) a hlavičkou X-TopEmail-Signature: sha256=<HMAC-SHA256 těla s webhook secretem>. Ověření musí probíhat z nezparsovaného těla a v konstantním čase.
Django:
# views.py
import hmac, hashlib, json, os
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def topemail_webhook(request):
expected = "sha256=" + hmac.new(
os.environ["TOPEMAIL_WEBHOOK_SECRET"].encode(), request.body, hashlib.sha256
).hexdigest()
received = request.headers.get("X-TopEmail-Signature", "")
if not hmac.compare_digest(expected, received):
return HttpResponse(status=401)
event = json.loads(request.body)
process_email_status.delay(event) # Celery; zpracovat na pozadí
return HttpResponse(status=200)
@csrf_exempt je nutný: webhook nemá CSRF token a Django by ho bez něj odmítl s 403. request.body jsou syrové bajty, přesně to, z čeho se podpis počítá.
Flask:
@app.post("/webhooks/topemail")
def topemail_webhook():
expected = "sha256=" + hmac.new(
os.environ["TOPEMAIL_WEBHOOK_SECRET"].encode(), request.get_data(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-TopEmail-Signature", "")):
abort(401)
event = request.get_json(force=True)
queue.enqueue(process_email_status, event)
return "", 200
request.get_data() vrátí syrové tělo; volejte ho před get_json(), jinak už může být stream přečtený.
Zpracování na pozadí musí být idempotentní, protože při pomalé odpovědi doručení opakujeme. Ukládejte události s unikátním klíčem (id, status, occurred_at) a IntegrityError ignorujte; postup s pořadím stavů je v článku o webhookách. Reagujte na bounced (adresa neplatná), complained (zkontrolovat obsah) a delivered (označit v objednávce).
Stav e-mailu na vyžádání
resp = requests.get(
f"https://topemail.cz/api/email/{email_id}",
headers={"Authorization": f"Bearer {os.environ['TOPEMAIL_KEY']}"},
timeout=TIMEOUT,
)
email = resp.json() # status, statusDetail, events[]
Časté chyby
EMAIL_USE_TLSiEMAIL_USE_SSLnajednou, nebo TLS s portem 465.SERVER_EMAILzapomenutý naroot@localhost: Django pak chybové reporty neodešle a v logu je550 5.7.1.- Webhook za
CsrfViewMiddlewarebez@csrf_exempt: 403 a opakované doručení. - Ověření z
request.datanebojson.dumps(request.json): podpis nikdy nesedí. - Náhodný Idempotency-Key v Celery úloze: při retry e-mail odejde dvakrát.
Shrnutí
Pro stávající Django nebo Flask stačí šest řádků konfigurace SMTP a odesílatel na ověřené doméně. Pro nový kód requests.post na API s Idempotency-Key a chybami podle code. Webhook ověřte hmac.compare_digest z request.body, odpovězte 200 a zpracujte na pozadí. Pole a stavy jsou v dokumentaci API, cena 0,06 Kč za e-mail, prvních 1 000 měsíčně zdarma.
Transakční e-maily z vaší domény přes API nebo SMTP, od 0,025 do 0,06 Kč za e-mail podle objemu, prvních 1 000 e-mailů měsíčně zdarma. Registrace zdarma, bez platební karty.
Registrovat zdarma →