Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Email в Python: SMTP и IMAP на практике

Письма из Python: SMTP-отправка, шаблоны и локальный dev-почтовик

Практика работы с email в Python: smtplib и готовые обёртки, MIME-структура письма, вложения и HTML, аутентификация и STARTTLS, ловушки доставки, локальный MailHog для разработки.

Прямой ответ

Отправка письма из Python — задача из двух слоёв: собрать корректное MIME-письмо (стандартная email.message.EmailMessage: тема, текст+HTML, вложения, кодировки) и доставить его через SMTP (smtplib.SMTP с STARTTLS и аутентификацией, или HTTP-API почтового провайдера — SendGrid/SES/почтовик хостера). Три практических правила: в разработке письма не уходят наружу — их перехватывает локальный SMTP-ловушка (MailHog/Mailpit в docker-стеке); отправка — фоновая задача с ретраями (SMTP медленный и падающий, в обработчике запроса ему не место); а «доходимость» до инбокса решается не кодом, а DNS-записями домена (SPF/DKIM/DMARC) — без них технически идеальное письмо улетит в спам.

Сборка письма: EmailMessage против ручной MIME

Современный способ — email.message.EmailMessage (msg["Subject"] = ..., set_content(text), add_alternative(html, subtype="html"), add_attachment(data, maintype=..., subtype=..., filename=...)): библиотека сама разруливает MIME-структуру (multipart/alternative для text+html, multipart/mixed для вложений) и заголовки кодировки (кириллица в теме/именах файлов — RFC 2047, руками это боль; кодировки в Python — в статье про Unicode). Обязательные поля помимо From/To/Subject: Message-ID (уникальность — дедупликация на принимающей стороне), Date; для рассылок — List-Unsubscribe (требование почтовиков для массовых отправителей). Ручная сборка MIME через MIMEMultipart — легаси-путь, нужен только для экзотики (календарные приглашения, подписи S/MIME).

Отправка: SMTP и его капризы

smtplib.SMTP(host, 587) → starttls() → login(user, password) → send_message(msg) — каноническая последовательность для хостерского почтовика; порт 465 — SMTP_SSL (implicit TLS, старый стиль); 25 — почти везде заблокирован провайдерами (борьба со спам-ботнетами). Капризы, о которые спотыкаются: таймауты обязательны (SMTP(host, timeout=10) — зависший SMTP без таймаута держит поток вечно); провайдеры требуют app-пароли (Gmail с обычным паролем не пустит — нужен пароль приложения при включённой 2FA); лимиты отправок (почтовик хостера — десятки-сотни в сутки, дальше — блок; для продукта — специализированный провайдер по HTTP-API). Секреты SMTP — в переменных окружения (правила), никогда в коде.

Локальная разработка: письма без отправки

Письма из dev-окружения не должны уходить клиентам — и не должны требовать реального SMTP. Стандарт связки: Mailpit/MailHog — контейнер-ловушка SMTP (принимает всё на 1025, показывает веб-мордой на 8025) в docker-compose (шаблон стека); настройки приложения — через env (EMAIL_HOST=localhost, EMAIL_PORT=1025) — те же переменные, что на проде, другое значение. Альтернативы: файловый бэкенд (письма в директорию — для тестов), консольный (в stdout — для отладки шаблонов), in-memory в юнит-тестах (assert на содержимое, без инфраструктуры). Проверка шаблонов глазами в Mailpit — часть workflow: HTML-письма ломаются регулярно и незаметно.

Фоновая отправка и надёжность

SMTP-вызов в обработчике HTTP-запроса — антипаттерн (секунды ожидания, отказ SMTP = отказ вашего API): отправка уходит в фоновую задачу (Celery с ретраями — практика; лёгкие случаи — очередь приложения). Надёжность доставки: идемпотентность (повтор ретрая не должен слать дубль — Message-ID/dedup-ключ), батчинг рассылок (провайдеры режут частоту), учёт отказов (bounce-адрес и обработка недоставленных — для продуктовых рассылок обязательно). IMAP-сторона (чтение почты) нужна реже — для ботов/обработчиков входящих; там те же принципы (imaplib/imap-tools, явная аутентификация, осторожность с парсингом — MIME-письма разнообразны до безумия).

Доходимость: то, что решает на самом деле

Технически корректное письмо с «голого» домена/сервера попадёт в спам: почтовики проверяют SPF (какие серверы вправе слать от домена), DKIM (подпись письма ключом домена), DMARC (политика при непроверке) — это DNS-записи домена, настраиваются один раз у провайдера рассылок. Плюс репутация: отдельный IP/домен для транзакционных и маркетинговых писем, стабильный объём, List-Unsubscribe, живой контент без «спам-слов». Для транзакционных писем (сброс пароля, чек) надёжнее всего — проверенный провайдер (SES/Postmark/SendGrid) с настроенным доменом: код остаётся простым, а доставляемость — их профессия.

Практический сценарий: письмо о сбросе пароля

Соберём самое частое транзакционное письмо целиком. Данные: в фоне (Celery-задача) формируется одноразовый токен сброса — случайные 32 байта, hash в БД с TTL пятнадцать минут, сам токен — только в письме. Письмо собирается EmailMessage: multipart/alternative — текстовая версия и HTML (табличная вёрстка, инлайн-стили, одна кнопка-ссылка, текстовый дубль URL под кнопкой — почтовики с заблокированными изображениями должны оставаться рабочими); тема конкретная («Сброс пароля — запрос получен»), From — адрес с настроенным DKIM-доменом, List-Unsubscribe для массовых шаблонов. Отправка: smtplib.SMTP(host, 587, timeout=10) → starttls → login (app-пароль из env) → send_message; на SMTPConnectError/Timeout — ретрай задачи с backoff (задача идемпотентна: повтор генерирует новый токен и инвалидирует старый, два живых токена не висят). Локальная разработка — тот же код с EMAIL_HOST=localhost:1025 и письмо видно в веб-морде Mailpit: верстка, ссылки, текстовая альтернатива проверяются глазами за секунды. Продакшн-гигиена: домен с SPF/DKIM/DMARC настроен у провайдера, отправка — с отдельного поддомена рассылок (репутация не тянет основной домен), bounce-адрес живой (недошедшие письма видны и разбираются). И обязательный тест в сюите: in-memory-бэкенд, assert на наличие токена-ссылки и отсутствие самого пароля в любом виде — письма умеют «случайно» включать лишнее.

FAQ

smtplib или HTTP-API провайдера? Для продукта — API провайдера (доставляемость, аналитика, вебхуки bounce); SMTP — когда провайдер только SMTP (хостерский ящик) или для внутренних уведомлений. Код письма (EmailMessage) не меняется — меняется только транспорт.

Как отправить HTML-письмо, чтобы оно выглядело нормально? Табличная вёрстка, инлайн-стили (стили в <head> режутся почтовиками), width 600px, текстовая альтернатива обязательна (multipart/alternative), изображения с absolute-URL и alt. Тест — в Mailpit и хотя бы одном реальном почтовике.

Почему письмо с вложением не открывается? Обычно — неверные maintype/subtype вложения (двоичный файл как text/plain) или не-RFC-имя файла с кириллицей; EmailMessage.add_attachment с явными типами решает оба.

Нужен ли свой почтовый сервер (Postfix)? Почти никогда: свой IP с чистой репутацией — отдельная профессия. Свой SMTP-релей уместен для внутренних уведомлений в изолированном контуре, всё продуктовое — через провайдера.

Источники: документация email/smtplib (docs.python.org), RFC 5322 — ссылки в шапке.