Документация / Flask

Отправка email во Flask через SMTP

Flask оставляет почту на усмотрение разработчика: это либо небольшое расширение, либо smtplib напрямую. Ниже — вариант с Flask-Mailman, развиваемым преемником Flask-Mail, на fmailer: шаблоны Jinja2, вложения и вынос отправки из запроса.

Логин и пароль можно получить на странице управления токенами домена.

Параметры подключения

ПараметрЗначение
SMTP-хостsmtp.fmailer.ru
Порт587
ШифрованиеSTARTTLS (соединение переводится в TLS до авторизации)
ЛогинЛогин SMTP-токена вашего домена
ПарольПароль этого токена — показывается один раз, при создании
Каждый режим доступен и на высоком порту: 8465 (SSL/TLS), 8587 (STARTTLS) и 8025 (без шифрования). Многие хостинги и облака блокируют исходящие 25, 465 и 587 — если соединение отваливается по таймауту, используйте соответствующий высокий порт

Установка

bash
pip install flask-mailman
Большинство руководств до сих пор используют Flask-Mail, у которого несколько лет не было содержательных релизов и который ломается на свежих версиях Python. Flask-Mailman — замена почти без изменений в конфигурации.

Настройка

config.pypython
# config.py
import os

class Config:
    MAIL_SERVER = "smtp.fmailer.ru"
    MAIL_PORT = 587
    MAIL_USE_TLS = True          # STARTTLS на 587
    MAIL_USE_SSL = False         # нельзя вместе с MAIL_USE_TLS
    MAIL_USERNAME = os.environ["SMTP_USER"]
    MAIL_PASSWORD = os.environ["SMTP_PASS"]
    MAIL_DEFAULT_SENDER = ("Магазин", "noreply@mail.example.ru")
    MAIL_TIMEOUT = 10
app.pypython
# 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

Первое письмо

python
from flask_mailman import EmailMessage

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

    EmailMessage(
        subject=f"Подтверждение заказа №{order.id}",
        body="Заказ принят. Доставка — завтра до 18:00.",
        to=[order.customer_email],
    ).send()

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

HTML-письмо из шаблона Jinja2

Шаблоны писем — обычные шаблоны Jinja2. Готовьте и HTML, и текстовую версию: письмо без текстовой части почтовые фильтры считают признаком спама:

python
from flask import render_template
from flask_mailman import EmailMultiAlternatives

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

msg = EmailMultiAlternatives(
    subject=f"Подтверждение заказа №{order.id}",
    body=render_template("email/order_confirmed.txt", order=order),
    to=[order.customer_email],
)
msg.attach_alternative(html, "text/html")
msg.send()

Вложение

python
msg = EmailMessage(
    subject="Счёт на оплату",
    body="Счёт за март во вложении.",
    to=["client@example.ru"],
)
msg.attach_file("/srv/invoices/2026-03.pdf")
msg.attach("report.csv", csv_bytes, "text/csv")
msg.send()

Фоновая отправка

Отправка внутри view держит ответ открытым на всё время обмена с SMTP. Поток — быстрое решение, но ему нужен контекст приложения:

python
from threading import Thread
from flask import current_app

def send_async(app, msg):
    # У потока нет контекста запроса — контекст приложения нужно создать вручную.
    with app.app_context():
        msg.send()

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

Для всего, от чего зависит пользователь, используйте Celery: поток умирает вместе с процессом и не даёт повторов:

tasks.pypython
# tasks.py — надёжная фоновая отправка
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"Подтверждение заказа №{order.id}",
            body=render_confirmation(order),
            to=[order.customer_email],
        ).send()
    except Exception as exc:
        raise self.retry(exc=exc)

Без расширения

Никакого почтового расширения Flask не требует. Если не хочется добавлять зависимость, хватит стандартной библиотеки:

python
# Без расширения вовсе — Flask его не требует.
import smtplib
from email.message import EmailMessage

def send_email(to, subject, text):
    msg = EmailMessage()
    msg["From"] = "Магазин <noreply@mail.example.ru>"
    msg["To"] = to
    msg["Subject"] = subject
    msg.set_content(text)

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

Тесты

python
# Flask-Mailman в тестах собирает письма вместо отправки.
def test_order_sends_confirmation(app, client):
    with mail.record_messages() as outbox:
        client.post("/orders", json={"email": "client@example.ru"})

    assert len(outbox) == 1
    assert "Подтверждение" in outbox[0].subject

Разбор ошибок

ОшибкаПричина и решение
Working outside of application context Отправка из потока. Оберните в with app.app_context().
SMTPAuthenticationError Неверные MAIL_USERNAME или MAIL_PASSWORD.
Включены оба флага TLSMAIL_USE_TLS и MAIL_USE_SSL взаимоисключающие.
Запросы виснут под нагрузкой Синхронная отправка занимает воркеры. Перейдите на Celery и задайте MAIL_TIMEOUT.
Таймаут подключения на сервере Порт заблокирован. Используйте 8587 или 8465.
SMTPSenderRefused (550) Адрес в MAIL_DEFAULT_SENDER не на подтверждённом домене.

Частые вопросы

Что выбрать — Flask-Mail или Flask-Mailman?

Flask-Mailman. Flask-Mail фактически не поддерживается уже несколько лет и ломается на новых версиях Python. Flask-Mailman — его развиваемый преемник: он переносит во Flask проверенный почтовый интерфейс Django и сохраняет практически те же ключи конфигурации, поэтому переход сводится к переименованию.

Можно ли включить MAIL_USE_TLS и MAIL_USE_SSL одновременно?

Нет, они взаимоисключающие и одновременное включение вызывает ошибку. MAIL_USE_TLS = True вместе с портом 587 — это STARTTLS, MAIL_USE_SSL = True с портом 465 — шифрование с первого байта.

Почему отправка из потока падает с «Working outside of application context»?

Поток не наследует контекст Flask. Возьмите настоящий объект приложения через current_app._get_current_object() и оберните отправку в with app.app_context(). Для всего важного лучше использовать Celery: поток умирает вместе с процессом и уносит неотправленное письмо.

Как отрисовать HTML-шаблон письма во Flask?

Тем же render_template, что и для страниц, а результат передать в attach_alternative(html, 'text/html') у объекта EmailMultiAlternatives. Рядом держите шаблон .txt для текстового тела.

Нужно ли расширение, чтобы отправлять письма из Flask?

Нет. У Flask нет собственного мнения о почте, поэтому стандартный smtplib работает и обходится без лишней зависимости. Расширение в основном даёт готовую привязку к конфигурации, тестовый outbox и переиспользование соединения.

Почему на продакшене письма отваливаются по таймауту?

Большинство облачных провайдеров блокируют исходящие порты 25, 465 и 587. Поставьте MAIL_PORT = 8587 для STARTTLS или 8465 для шифрования с первого байта. И обязательно задайте MAIL_TIMEOUT, чтобы заблокированный порт не занимал воркер.

Что дальше