Send email with Flask
Send transactional email from Flask through PostStack.
There are two ways to send email from Flask with PostStack. If your app already uses Flask-Mail — including a Flask-Mail setup pointed at another provider or Gmail — change four config values and keep every Message and mail.send() call as it is. If you are starting fresh or need more than SMTP can give you (the email id back, delivery and bounce webhooks, templates, idempotent retries), call the REST API with the PostStack Python SDK. Both are below, along with background sending with RQ or Celery and the errors people hit most often.
Quickstart#
Install Flask-Mail or the SDK
bashpip install Flask-Mail # SMTP path: keep your existing Flask-Mail code pip install poststack # REST path: the PostStack Python SDKConfigure Flask-Mail for PostStack
python# config.py — Flask-Mail pointed at PostStack's SMTP relay import os class Config: MAIL_SERVER = "smtp.poststack.dev" MAIL_PORT = 587 MAIL_USE_TLS = True # STARTTLS on 587. For port 465 use MAIL_USE_SSL = True instead — not both MAIL_USE_SSL = False MAIL_USERNAME = "poststack" # any non-empty value; the API key authenticates MAIL_PASSWORD = os.environ["POSTSTACK_API_KEY"] # sk_live_... / sk_test_... MAIL_DEFAULT_SENDER = ("Acme", "hello@yourdomain.com") # must be on a verified domain # app.py from flask import Flask from flask_mail import Mail mail = Mail() def create_app(): app = Flask(__name__) app.config.from_object("config.Config") mail.init_app(app) return appSend an email with Flask-Mail
pythonfrom flask import Blueprint from flask_mail import Message from .app import mail bp = Blueprint("auth", __name__) @bp.post("/welcome") def welcome(): msg = Message( subject="Welcome to Acme", recipients=["user@example.com"], body="Thanks for signing up.", # text/plain part html="<h1>Welcome!</h1><p>Thanks for signing up.</p>", ) mail.send(msg) return {"ok": True}Handle errors
pythonimport logging import smtplib from flask_mail import Message from .app import mail logger = logging.getLogger(__name__) def send_or_log(msg: Message) -> bool: try: mail.send(msg) return True except smtplib.SMTPAuthenticationError: # 535 — MAIL_PASSWORD is not a valid, unrevoked PostStack API key logger.exception("PostStack rejected the SMTP credentials") raise except smtplib.SMTPResponseException as exc: # e.g. "Domain 'example.com' not found for this team": the sender is # not on a verified domain. The reason is in exc.smtp_error. logger.error("PostStack refused the message: %s %s", exc.smtp_code, exc.smtp_error) return False except (smtplib.SMTPException, OSError): # connection refused / timed out — outbound 587 (or 465) blocked? logger.exception("Could not reach smtp.poststack.dev") raise
Migrating Flask-Mail from another SMTP provider#
Flask-Mail does not care which provider is on the other end, so switching is a config change. PostStack accepts any non-empty username and authenticates with the API key in the password. Remove your old provider’s Python package too if you only used it for sending.
# Before — your previous provider
MAIL_SERVER = "smtp.old-provider.example"
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = "old-username"
MAIL_PASSWORD = os.environ["OLD_PROVIDER_API_KEY"]
# After — PostStack
MAIL_SERVER = "smtp.poststack.dev"
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = "poststack"
MAIL_PASSWORD = os.environ["POSTSTACK_API_KEY"]
# Checklist:
# 1. Add and verify your sending domain in PostStack (SPF + DKIM records)
# 2. Make sure MAIL_DEFAULT_SENDER / every Message(sender=...) uses that domain
# 3. Send a test with an sk_test_ key, then switch to sk_live_Attachments, cc/bcc and custom headers with Flask-Mail#
Message.attach(filename, content_type, data) adds a file. PostStack accepts up to 10 attachments per message, 10 MB per file and 25 MB in total. X-PostStack-Tags and X-PostStack-Idempotency-Key headers are read by the relay; other X- headers pass through.
from flask import current_app
from flask_mail import Message
from .app import mail
msg = Message(
subject="Invoice #1042",
recipients=["customer@example.com"],
cc=["accounts@example.com"],
bcc=["archive@yourdomain.com"],
reply_to="billing@yourdomain.com",
body="Your invoice is attached.",
extra_headers={
"X-PostStack-Tags": "invoice,transactional",
"X-PostStack-Idempotency-Key": "invoice-1042",
},
)
msg.attach("invoice-1042.pdf", "application/pdf", pdf_bytes)
with current_app.open_resource("static/terms.pdf") as fp:
msg.attach("terms.pdf", "application/pdf", fp.read())
mail.send(msg)Send in the background (RQ) with an app context#
Flask-Mail reads its settings from current_app, which does not exist in an RQ or Celery worker. Create the app once in the worker module and push an app context around the send.
# tasks.py — executed by "rq worker"
from flask_mail import Message
from myapp.app import create_app, mail
app = create_app()
def send_welcome(email: str, name: str):
with app.app_context():
mail.send(Message(
subject="Welcome to Acme",
recipients=[email],
html=f"<h1>Welcome, {name}!</h1>",
))
# views.py
from redis import Redis
from rq import Queue
from .tasks import send_welcome
queue = Queue(connection=Redis.from_url("redis://localhost:6379"))
@bp.post("/signup")
def signup():
user = create_user(request.form)
queue.enqueue(send_welcome, user.email, user.name) # response stays fast
return {"ok": True}HTML email from Jinja templates#
Keep email bodies in templates/email/ and render them with the same render_template your views use, so autoescaping and template inheritance work. Render a plain-text twin too: mail with only an HTML part scores worse with spam filters. render_template needs an app context, which a view already has.
# templates/email/welcome.html → {% extends "email/base.html" %} … Hi {{ user.name }} …
# templates/email/welcome.txt → Hi {{ user.name }}, thanks for signing up.
from flask import render_template
from flask_mail import Message
from .app import mail
def send_welcome(user):
mail.send(Message(
subject="Welcome to Acme",
recipients=[user.email],
body=render_template("email/welcome.txt", user=user), # text/plain
html=render_template("email/welcome.html", user=user), # text/html
))Handle bounces and complaints with a webhook route#
Add a webhook endpoint in PostStack for email.bounced and email.complained. Verify X-PostStack-Signature against request.get_data() — the raw bytes; request.json re-serialises them and the HMAC will not match. PostStack suppresses the address itself, so the route only updates your own data and returns any 2xx (anything else is retried). verify is a local HMAC check in the SDK (pip install poststack); it makes no API call.
import os
from flask import Blueprint, current_app, request
hooks = Blueprint("hooks", __name__)
@hooks.post("/webhooks/poststack")
def poststack_webhook():
ps = current_app.extensions["poststack"] # SDK client, see the SDK section below
if not ps.webhooks.verify(
request.get_data(), # raw body
request.headers.get("X-PostStack-Signature", ""),
os.environ["POSTSTACK_WEBHOOK_SECRET"], # whsec_...
):
return "Invalid signature", 401
event = request.get_json()
data = event["data"] # email_id, from, subject, and "to": EVERY recipient of the message
if event["type"] == "email.bounced":
for address in data["to"]:
mark_undeliverable(address, reason=data.get("bounce_message"))
elif event["type"] == "email.complained":
for address in data["to"]:
unsubscribe_everywhere(address)
return "", 204Alternative: send through the REST API with the Python SDK#
Use the SDK when you want the email id back, per-send open/click tracking, stored templates with variables, or batch sends of up to 100 emails per request.
# app.py
import os
from flask import Flask, current_app
from poststack import PostStack, PostStackError
def create_app():
app = Flask(__name__)
app.extensions["poststack"] = PostStack(api_key=os.environ["POSTSTACK_API_KEY"])
return app
# views.py
@bp.post("/signup")
def signup():
user = create_user(request.form)
ps = current_app.extensions["poststack"]
try:
result = ps.emails.send({
"from": "Acme <hello@yourdomain.com>",
"to": [user.email], # always a list
"subject": "Welcome to Acme",
"html": f"<h1>Welcome, {user.name}!</h1>",
"text": f"Welcome, {user.name}!",
"tags": ["welcome"],
})
except PostStackError as exc:
# SDK already retried 408/429/5xx. 400 = invalid payload,
# 422 = unverified domain or suppressed recipient.
current_app.logger.error("PostStack %s: %s (req %s)", exc.status_code, exc.error, exc.request_id)
return {"ok": False}, 502
return {"ok": True, "email_id": result["id"]} # "em_…"Framework integrations#
Flask-Mail over SMTP
Flask-Mail is a thin wrapper around smtplib. Point it at smtp.poststack.dev and all existing Message objects — HTML bodies, attachments, cc/bcc, custom headers — send through PostStack unchanged. Custom X-PostStack-* headers add tags, idempotency keys and scheduled sends.
PostStack SDK on app.extensions
Create one PostStack client in create_app() and store it on app.extensions["poststack"]; views reach it through current_app. emails.send() returns an em_… id and the SDK retries 408/429/5xx for you with an automatic idempotency key.
RQ / Celery background jobs
Enqueue the send rather than calling it in the view. Workers run outside the request, so Flask-Mail needs with app.app_context(): there, while the SDK needs only the API key from the environment.
Blueprints
Keep sending helpers in one module (emails.py) imported by your auth, billing and account blueprints, so templates and sender addresses live in one place.
Common pitfalls#
SMTPAuthenticationError (535)
MAIL_PASSWORD must be a PostStack API key (sk_live_… or sk_test_…), not your account password. A stray newline from a secrets file or a revoked key produces the same error.
"Domain '…' not found for this team"
The sender must be on a domain you added and verified in PostStack. Check MAIL_DEFAULT_SENDER and any explicit sender= on Message — a leftover address from your previous provider is the usual cause.
MAIL_USE_TLS and MAIL_USE_SSL both True
Pick one: port 587 with MAIL_USE_TLS = True (STARTTLS), or port 465 with MAIL_USE_SSL = True (implicit TLS). Mixing them, or using 465 with STARTTLS, ends in a TLS handshake error or a timeout.
"Working outside of application context"
Flask-Mail needs current_app. In RQ/Celery workers, threads and scripts, wrap the send in with app.app_context():.
Nothing arrives, no error
MAIL_SUPPRESS_SEND (on automatically when TESTING = True) makes Flask-Mail skip delivery, and sk_test_ keys accept mail without delivering it. Check both before debugging DNS.
FAQ#
How do I configure Flask-Mail for PostStack?
Set MAIL_SERVER = "smtp.poststack.dev", MAIL_PORT = 587, MAIL_USE_TLS = True, MAIL_USERNAME = "poststack" and MAIL_PASSWORD to your API key. Port 465 with MAIL_USE_SSL = True also works. Your Message and mail.send() code does not change.
How do I switch Flask-Mail from another provider to PostStack?
Set MAIL_SERVER to smtp.poststack.dev, the username to any non-empty value such as poststack, and the password to a PostStack API key. Verify your sending domain in PostStack first so the From address is accepted.
Flask-Mail or the PostStack SDK?
Flask-Mail if you already use it or want provider-neutral SMTP code. The SDK if you need the email id back to match webhooks, stored templates, batch sends or per-send tracking settings. You can use both in one app.
Does Flask-Mail work with Flask 3?
Yes — Flask-Mail 0.10 supports current Flask releases. The SDK has no Flask dependency at all.
How do I know when a Flask email bounced?
SMTP accepts the message before the receiving server decides, so a bounce never shows up as an exception in mail.send(). Subscribe a webhook endpoint to email.bounced and verify its signature against request.get_data(). PostStack suppresses the address automatically after a hard bounce or complaint.
How do I send email in the background with Flask?
Enqueue a function with RQ or Celery from your view. In the worker, create the app and send inside with app.app_context(): (Flask-Mail) or construct a PostStack client from the environment (SDK).
Reference#
The docs this guide builds on:
- SDKs — every official client, its retries and idempotency keys
- SMTP relay — hosts, ports, TLS modes and the
X-PostStack-*headers - Emails API — every send field, attachments, scheduling and batch sends
- Templates — stored templates with variables
- Webhooks — event types, payloads and signature verification
- Suppressions — what happens to an address after a hard bounce or complaint
Ready to send from Flask?
Create a free account and an API key — no card needed.