Всё нужное для почты есть в стандартной библиотеке: smtplib говорит по SMTP, email.message собирает письмо. Зависимостей не требуется. Ниже — подключение к fmailer, HTML-письма, вложения, картинки в теле, массовая рассылка и асинхронный вариант.
| Параметр | Значение |
|---|---|
| SMTP-хост | smtp.fmailer.ru |
| Порт | 587 |
| Шифрование | STARTTLS (соединение переводится в TLS до авторизации) |
| Логин | Логин SMTP-токена вашего домена |
| Пароль | Пароль этого токена — показывается один раз, при создании |
import os
import smtplib
from email.message import EmailMessage
msg = EmailMessage()
msg["From"] = "Магазин <noreply@mail.example.ru>"
msg["To"] = "client@example.ru"
msg["Subject"] = "Подтверждение заказа №4417"
msg.set_content("Заказ принят. Доставка — завтра до 18:00.")
with smtplib.SMTP("smtp.fmailer.ru", 587, timeout=10) as smtp:
smtp.starttls() # шифрование до авторизации
smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
smtp.send_message(msg)На порту 465 соединение шифруется с самого начала:
import ssl
# Порт 465 — соединение шифруется сразу, starttls() вызывать не нужно.
context = ssl.create_default_context()
with smtplib.SMTP_SSL("smtp.fmailer.ru", 465, context=context, timeout=10) as smtp:
smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
smtp.send_message(msg)Сначала текстовое тело, затем HTML как альтернатива. Порядок важен: почтовый клиент показывает последнюю часть, которую понимает:
msg = EmailMessage()
msg["From"] = "Магазин <noreply@mail.example.ru>"
msg["To"] = "client@example.ru"
msg["Subject"] = "Подтверждение заказа №4417"
# Сначала текстовая часть...
msg.set_content("Заказ принят. Доставка — завтра до 18:00.")
# ...затем HTML-альтернатива. Так письмо становится multipart/alternative.
msg.add_alternative(
"""\
<html>
<body>
<h1>Заказ принят</h1>
<p>Доставка — завтра до 18:00.</p>
</body>
</html>
""",
subtype="html",
)from pathlib import Path
import mimetypes
path = Path("/srv/invoices/2026-03.pdf")
mime, _ = mimetypes.guess_type(path.name)
maintype, subtype = (mime or "application/octet-stream").split("/", 1)
msg.add_attachment(
path.read_bytes(),
maintype=maintype,
subtype=subtype,
filename="invoice.pdf",
)Переиспользуйте одно авторизованное соединение и отправляйте по одному письму на получателя:
# Одно соединение на всю рассылку вместо переподключения на каждое письмо.
with smtplib.SMTP("smtp.fmailer.ru", 587, timeout=10) as smtp:
smtp.starttls()
smtp.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
for user in users:
msg = EmailMessage()
msg["From"] = "Магазин <noreply@mail.example.ru>"
msg["To"] = user.email # один получатель на письмо
msg["Subject"] = "Еженедельный отчёт"
msg.set_content(render_report(user))
try:
smtp.send_message(msg)
except smtplib.SMTPRecipientsRefused:
log.warning("Адрес отклонён: %s", user.email)smtplib блокирует цикл событий asyncio на всё время обмена с сервером. Используйте aiosmtplib — он принимает те же объекты:
# aiosmtplib — то же самое для asyncio: те же объекты, но с await.
import aiosmtplib
await aiosmtplib.send(
msg,
hostname="smtp.fmailer.ru",
port=587,
start_tls=True,
username=os.environ["SMTP_USER"],
password=os.environ["SMTP_PASS"],
)smtp.set_debuglevel(1) # печатать весь SMTP-диалог в stderr| Ошибка | Причина и решение |
|---|---|
SMTPAuthenticationError | Неверные доступы либо login() вызван до starttls(). |
SMTPNotSupportedError: STARTTLS extension not supported | starttls() на порту 465. Там нужен SMTP_SSL. |
ssl.SSLError: wrong version number | SMTP_SSL на порту 587. Нужен обычный SMTP плюс starttls(). |
| Зависает навсегда | Не передан timeout, порт заблокирован. Используйте 8587. |
SMTPSenderRefused (550) | Домен отправителя не подтверждён. |
Тема выглядит как =?utf-8?B?… | Так и должно быть — это кодирование по RFC 2047, почтовые клиенты его раскрывают. |
SMTP со starttls() на порту 587: соединение открывается незашифрованным и переводится в TLS до авторизации. SMTP_SSL на порту 465: сокет шифруется с первого байта и вызывать starttls() уже нельзя. Вызов starttls() на SMTP_SSL приведёт к ошибке, а его пропуск на 587 отправит пароль открытым текстом.
EmailMessage — современный интерфейс модуля email, рекомендованный начиная с Python 3.6. Он сам собирает multipart, заголовки, кодировки и вложения, тогда как старые MIMEMultipart и MIMEText требуют строить дерево вручную и легко ломают юникод в заголовках.
Неверный логин или пароль, либо login() вызван до starttls(): учётные данные, отправленные по нешифрованному соединению, отклоняются. На порту 587 starttls() должен идти первым.
Передавайте timeout в конструктор. Без него smtplib берёт глобальное значение сокета, а оно обычно не ограничено, поэтому при заблокированном порте процесс ждёт бесконечно. Если после этого начинается таймаут — хостинг блокирует исходящий 587, используйте 8587.
Сначала set_content() с текстовой версией, затем add_alternative(html, subtype='html'). Порядок важен: почтовые клиенты показывают последнюю часть, которую умеют отрисовать. HTML без текстовой части почтовые фильтры считают признаком спама.
smtplib синхронный и блокирует цикл событий, поэтому в асинхронном приложении используйте aiosmtplib: он принимает те же объекты EmailMessage и даёт awaitable send(). Либо отдавайте письмо фоновому воркеру вроде Celery.