Docs / Flask

Send email in Flask over SMTP

Flask leaves email to you, which means either a small extension or smtplib directly. This guide uses Flask-Mailman — the maintained successor to Flask-Mail — against Postwing, and covers Jinja2 HTML templates, attachments and moving the send off the request.

You can get them on the token management page. For security reasons, a token is shown only once — at the moment it is created.

SMTP connection settings

SettingValue
SMTP hostsmtp.postwing.app
Port587
EncryptionSTARTTLS (the connection is upgraded to TLS before login)
UsernameThe login of an SMTP token for your domain
PasswordThe password of that token — shown once, when the token is created
Every mode is also available on a high port: 8465 (SSL/TLS), 8587 (STARTTLS) and 8025 (plain). Many hosting providers and clouds block outbound 25, 465 and 587 — if the connection times out, switch to the matching high port.

Install Flask-Mailman

bash
pip install flask-mailman
Most Flask email tutorials still use Flask-Mail, which has not had a meaningful release in years and breaks on recent Python versions. Flask-Mailman is a drop-in replacement with essentially the same configuration keys.

Configure the app

config.py
# config.py
import os

class Config:
    MAIL_SERVER = "smtp.postwing.app"
    MAIL_PORT = 587
    MAIL_USE_TLS = True          # STARTTLS on 587
    MAIL_USE_SSL = False         # mutually exclusive with MAIL_USE_TLS
    MAIL_USERNAME = os.environ["SMTP_USER"]
    MAIL_PASSWORD = os.environ["SMTP_PASS"]
    MAIL_DEFAULT_SENDER = ("Acme", "noreply@your-domain.com")
    MAIL_TIMEOUT = 10
app.py
# app.py
from flask import Flask
from flask_mailman import Mail
from config import Config

mail = Mail()

def create_app():
    app = Flask(__name__)
    app.config.from_object(Config)
    mail.init_app(app)
    return app

Send your first email

python
from flask import current_app
from flask_mailman import EmailMessage

@app.post("/orders")
def create_order():
    order = save_order(request.json)

    EmailMessage(
        subject=f"Your order #{order.id} is confirmed",
        body="Thanks! Your order ships tomorrow.",
        to=[order.customer_email],
    ).send()

    return {"id": order.id}, 201

Send an HTML email from a Jinja2 template

Email templates are ordinary Jinja2 templates. Render both an HTML and a text version — a message with no plain-text part is a spam signal:

python
from flask import render_template
from flask_mailman import EmailMultiAlternatives

html = render_template("email/order_confirmed.html", order=order)

msg = EmailMultiAlternatives(
    subject=f"Your order #{order.id} is confirmed",
    body=render_template("email/order_confirmed.txt", order=order),
    to=[order.customer_email],
)
msg.attach_alternative(html, "text/html")
msg.send()

Send an attachment

python
msg = EmailMessage(
    subject="Your invoice",
    body="The invoice for March is attached.",
    to=["customer@example.com"],
)
msg.attach_file("/srv/invoices/2026-03.pdf")
msg.attach("report.csv", csv_bytes, "text/csv")
msg.send()

Send in the background

Sending inside the view holds the response open for the whole SMTP round trip. A thread is the quick fix, but it needs an application context:

python
from threading import Thread

def send_async(app, msg):
    # A thread has no request context — push an application context manually.
    with app.app_context():
        msg.send()

def queue_email(msg):
    Thread(target=send_async, args=(current_app._get_current_object(), msg)).start()

For anything a user depends on, use Celery — a thread dies with the process and gives you no retries:

tasks.py
# tasks.py — durable background sending
from celery import shared_task
from flask_mailman import EmailMessage

@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_order_confirmation(self, order_id):
    order = Order.query.get(order_id)
    try:
        EmailMessage(
            subject=f"Your order #{order.id} is confirmed",
            body=render_confirmation(order),
            to=[order.customer_email],
        ).send()
    except Exception as exc:
        raise self.retry(exc=exc)

Without an extension

Flask requires no mail extension at all. If you would rather not add a dependency, the standard library covers it:

python
# No extension at all — Flask does not require one.
import smtplib
from email.message import EmailMessage

def send_email(to, subject, text):
    msg = EmailMessage()
    msg["From"] = "Acme <noreply@your-domain.com>"
    msg["To"] = to
    msg["Subject"] = subject
    msg.set_content(text)

    with smtplib.SMTP("smtp.postwing.app", 587, timeout=10) as smtp:
        smtp.starttls()
        smtp.login(current_app.config["MAIL_USERNAME"],
                   current_app.config["MAIL_PASSWORD"])
        smtp.send_message(msg)

Testing

python
# Flask-Mailman collects messages instead of sending them in tests.
def test_order_sends_confirmation(app, client):
    with mail.record_messages() as outbox:
        client.post("/orders", json={"email": "customer@example.com"})

    assert len(outbox) == 1
    assert "confirmed" in outbox[0].subject

Troubleshooting

ErrorCause and fix
Working outside of application context Sending from a thread. Wrap it in with app.app_context().
SMTPAuthenticationError Wrong MAIL_USERNAME / MAIL_PASSWORD.
Both TLS flags setMAIL_USE_TLS and MAIL_USE_SSL are exclusive — pick one.
Requests hang under load Synchronous sends occupying workers. Move to Celery and set MAIL_TIMEOUT.
Connection times out on the server Outbound port blocked. Use 8587 or 8465.
SMTPSenderRefused (550)MAIL_DEFAULT_SENDER is not on a verified domain.

Frequently asked questions

Should I use Flask-Mail or Flask-Mailman?

Flask-Mailman. Flask-Mail has been effectively unmaintained for years and breaks on newer Python releases. Flask-Mailman is the maintained successor, ports Django's well-tested mail API to Flask, and keeps a largely compatible configuration surface so migrating is mostly a rename.

Can MAIL_USE_TLS and MAIL_USE_SSL both be True?

No — they are mutually exclusive and setting both raises an error. Use MAIL_USE_TLS = True with port 587 for STARTTLS, or MAIL_USE_SSL = True with port 465 for implicit TLS.

Why does sending email in a background thread raise 'Working outside of application context'?

The thread does not inherit Flask's context. Capture the real app object with current_app._get_current_object() and wrap the send in with app.app_context(). For anything that matters, use Celery instead — a thread dies with the process and takes the unsent email with it.

How do I render an HTML email template in Flask?

Use render_template exactly as you would for a page, then pass the result to attach_alternative(html, 'text/html') on an EmailMultiAlternatives message. Keep a .txt template alongside it for the plain-text body.

Do I need an extension to send email from Flask?

No. Flask has no opinion about email, so smtplib from the standard library works fine and is one less dependency. An extension mainly buys you configuration wiring, a testing outbox and connection reuse.

Why do Flask emails time out in production?

Most cloud providers block outbound ports 25, 465 and 587. Set MAIL_PORT to 8587 for STARTTLS or 8465 for implicit TLS. Always set MAIL_TIMEOUT so a blocked port fails fast instead of tying up a worker.

Next steps