Flask оставляет почту на усмотрение разработчика: это либо небольшое расширение, либо smtplib напрямую. Ниже — вариант с Flask-Mailman, развиваемым преемником Flask-Mail, на fmailer: шаблоны Jinja2, вложения и вынос отправки из запроса.
| Параметр | Значение |
|---|---|
| SMTP-хост | smtp.fmailer.ru |
| Порт | 587 |
| Шифрование | STARTTLS (соединение переводится в TLS до авторизации) |
| Логин | Логин SMTP-токена вашего домена |
| Пароль | Пароль этого токена — показывается один раз, при создании |
pip install flask-mailmanFlask-Mail, у которого несколько лет не было содержательных релизов и который ломается на свежих версиях Python. Flask-Mailman — замена почти без изменений в конфигурации. # 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.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 appfrom 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Шаблоны писем — обычные шаблоны Jinja2. Готовьте и HTML, и текстовую версию: письмо без текстовой части почтовые фильтры считают признаком спама:
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()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. Поток — быстрое решение, но ему нужен контекст приложения:
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.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 не требует. Если не хочется добавлять зависимость, хватит стандартной библиотеки:
# Без расширения вовсе — 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)# 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. |
| Включены оба флага TLS | MAIL_USE_TLS и MAIL_USE_SSL взаимоисключающие. |
| Запросы виснут под нагрузкой | Синхронная отправка занимает воркеры. Перейдите на Celery и задайте MAIL_TIMEOUT. |
| Таймаут подключения на сервере | Порт заблокирован. Используйте 8587 или 8465. |
SMTPSenderRefused (550) | Адрес в MAIL_DEFAULT_SENDER не на подтверждённом домене. |
Flask-Mailman. Flask-Mail фактически не поддерживается уже несколько лет и ломается на новых версиях Python. Flask-Mailman — его развиваемый преемник: он переносит во Flask проверенный почтовый интерфейс Django и сохраняет практически те же ключи конфигурации, поэтому переход сводится к переименованию.
Нет, они взаимоисключающие и одновременное включение вызывает ошибку. MAIL_USE_TLS = True вместе с портом 587 — это STARTTLS, MAIL_USE_SSL = True с портом 465 — шифрование с первого байта.
Поток не наследует контекст Flask. Возьмите настоящий объект приложения через current_app._get_current_object() и оберните отправку в with app.app_context(). Для всего важного лучше использовать Celery: поток умирает вместе с процессом и уносит неотправленное письмо.
Тем же render_template, что и для страниц, а результат передать в attach_alternative(html, 'text/html') у объекта EmailMultiAlternatives. Рядом держите шаблон .txt для текстового тела.
Нет. У Flask нет собственного мнения о почте, поэтому стандартный smtplib работает и обходится без лишней зависимости. Расширение в основном даёт готовую привязку к конфигурации, тестовый outbox и переиспользование соединения.
Большинство облачных провайдеров блокируют исходящие порты 25, 465 и 587. Поставьте MAIL_PORT = 8587 для STARTTLS или 8465 для шифрования с первого байта. И обязательно задайте MAIL_TIMEOUT, чтобы заблокированный порт не занимал воркер.