CeníkAPI & SMTPŘešeníSrovnáníNástrojeBlogFAQKontakt
PřihlásitRegistrovat
Tmavý režim
← Zpět na blog
27. října 2026·4 min čtení·Tým TopEmail

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_TLS i EMAIL_USE_SSL najednou, nebo TLS s portem 465.
  • SERVER_EMAIL zapomenutý na root@localhost: Django pak chybové reporty neodešle a v logu je 550 5.7.1.
  • Webhook za CsrfViewMiddleware bez @csrf_exempt: 403 a opakované doručení.
  • Ověření z request.data nebo json.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.

PythonDjangoapiSMTPwebhookyintegracenávod
Transakční e-maily z vaší domény

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 →