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

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

Всё нужное для почты есть в стандартной библиотеке: smtplib говорит по SMTP, email.message собирает письмо. Зависимостей не требуется. Ниже — подключение к fmailer, HTML-письма, вложения, картинки в теле, массовая рассылка и асинхронный вариант.

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

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

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

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

python
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)
На порту 587 соединение открывается нешифрованным. Если авторизоваться первым, пароль токена уйдёт по сети открытым текстом — и сервер всё равно его отклонит.

На порту 465 соединение шифруется с самого начала:

python
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-письмо

Сначала текстовое тело, затем HTML как альтернатива. Порядок важен: почтовый клиент показывает последнюю часть, которую понимает:

python
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",
)

Вложение

python
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",
)

Массовая рассылка

Переиспользуйте одно авторизованное соединение и отправляйте по одному письму на получателя:

python
# Одно соединение на всю рассылку вместо переподключения на каждое письмо.
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)
При использовании CC и BCC на каждого получателя внутри системы создаётся отдельное письмо, и лимиты расходуются соответственно.

Асинхронная отправка

smtplib блокирует цикл событий asyncio на всё время обмена с сервером. Используйте aiosmtplib — он принимает те же объекты:

python
# 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"],
)

Отладка

python
smtp.set_debuglevel(1)   # печатать весь SMTP-диалог в stderr

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

ОшибкаПричина и решение
SMTPAuthenticationError Неверные доступы либо login() вызван до starttls().
SMTPNotSupportedError: STARTTLS extension not supportedstarttls() на порту 465. Там нужен SMTP_SSL.
ssl.SSLError: wrong version numberSMTP_SSL на порту 587. Нужен обычный SMTP плюс starttls().
Зависает навсегда Не передан timeout, порт заблокирован. Используйте 8587.
SMTPSenderRefused (550)Домен отправителя не подтверждён.
Тема выглядит как =?utf-8?B?… Так и должно быть — это кодирование по RFC 2047, почтовые клиенты его раскрывают.

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

Что использовать — smtplib.SMTP со starttls() или smtplib.SMTP_SSL?

SMTP со starttls() на порту 587: соединение открывается незашифрованным и переводится в TLS до авторизации. SMTP_SSL на порту 465: сокет шифруется с первого байта и вызывать starttls() уже нельзя. Вызов starttls() на SMTP_SSL приведёт к ошибке, а его пропуск на 587 отправит пароль открытым текстом.

Почему стоит использовать EmailMessage, а не MIMEText?

EmailMessage — современный интерфейс модуля email, рекомендованный начиная с Python 3.6. Он сам собирает multipart, заголовки, кодировки и вложения, тогда как старые MIMEMultipart и MIMEText требуют строить дерево вручную и легко ломают юникод в заголовках.

Почему Python выбрасывает SMTPAuthenticationError?

Неверный логин или пароль, либо login() вызван до starttls(): учётные данные, отправленные по нешифрованному соединению, отклоняются. На порту 587 starttls() должен идти первым.

Как не дать smtplib зависнуть?

Передавайте timeout в конструктор. Без него smtplib берёт глобальное значение сокета, а оно обычно не ограничено, поэтому при заблокированном порте процесс ждёт бесконечно. Если после этого начинается таймаут — хостинг блокирует исходящий 587, используйте 8587.

Как отправить HTML-письмо в Python?

Сначала set_content() с текстовой версией, затем add_alternative(html, subtype='html'). Порядок важен: почтовые клиенты показывают последнюю часть, которую умеют отрисовать. HTML без текстовой части почтовые фильтры считают признаком спама.

Можно ли отправлять письма асинхронно?

smtplib синхронный и блокирует цикл событий, поэтому в асинхронном приложении используйте aiosmtplib: он принимает те же объекты EmailMessage и даёт awaitable send(). Либо отдавайте письмо фоновому воркеру вроде Celery.

Что дальше