Автоматическая рассылка отчётов на Python: ошибки новичков
Скрипт собирает Excel-отчёт и шлёт его на почту, а через неделю падает: письма не уходят, вложения битые, пароль в коде утёк. Разбираем, что ломается и как сделать надёжно.

Задача звучит просто: раз в сутки собрать отчёт, приложить файл и отправить письмо себе и коллегам. На локальной машине всё работает с первого запуска. Через неделю на сервере скрипт молча перестаёт отправлять письма, вложения приходят пустыми, а Gmail блокирует вход. Ниже — пять типичных ошибок, из-за которых рассылка ломается, с рабочим кодом на Python 3.11+ и стандартными модулями smtplib и email.
Разбираем именно отправку и типовые грабли новичка. Формирование самого отчёта (pandas, openpyxl) и планировщик (cron, systemd timer) — смежные темы, их упоминаем одной строкой, но не разворачиваем.
Что понадобится до начала
- Python 3.9 или новее. Примеры проверены на 3.11, модули
smtplibиemailвходят в стандартную библиотеку — ставить ничего не нужно. - Учётная запись почты с доступом по SMTP и адрес SMTP-сервера с портом. Для большинства провайдеров это порт 587 (STARTTLS) или 465 (SSL).
- Пароль приложения, а не пароль от аккаунта. У Gmail и Яндекса вход по основному паролю из скриптов заблокирован — нужен отдельный пароль приложения в настройках безопасности.
- Готовый файл отчёта на диске или в памяти.
Ошибка 1. Пароль прямо в коде
Первое, что делает новичок, — вписывает логин и пароль строками в скрипт. Дальше файл уезжает в git, и учётка утекает в публичный репозиторий. GitHub сканирует такие утечки автоматически, но провайдер почты заблокирует ящик быстрее.
Держите секреты в переменных окружения. Скрипт читает их через os.environ, а сами значения задаются вне кода — в файле окружения сервиса или в настройках CI.
import os
# Читаем секреты из окружения. Если переменной нет — падаем сразу,
# а не отправляем письмо с пустым паролем.
SMTP_HOST = os.environ["SMTP_HOST"]
SMTP_PORT = int(os.environ["SMTP_PORT"])
SMTP_USER = os.environ["SMTP_USER"]
SMTP_PASSWORD = os.environ["SMTP_PASSWORD"]
Проверка: запустите python -c "import os; print(os.environ.get('SMTP_USER'))" в той же сессии — должен вывестись ваш логин, а не None.
Если пароль хоть раз попал в git — считайте его скомпрометированным. Удалить строку из последнего коммита мало: значение остаётся в истории. Меняйте пароль приложения, а не переписывайте файл.
Ошибка 2. Кодировка темы и вложений
Русская тема письма превращается в =?utf-8?... или вовсе в кракозябры, а имя файла Отчёт.xlsx приходит как ?????.xlsx. Причина — ручная сборка заголовков строками. Не собирайте MIME-письмо вручную: класс EmailMessage из модуля email кодирует заголовки и вложения сам.
from email.message import EmailMessage
def build_message(subject: str, body: str, to_addrs: list[str]) -> EmailMessage:
msg = EmailMessage()
msg["Subject"] = subject # кириллица кодируется автоматически
msg["From"] = SMTP_USER
msg["To"] = ", ".join(to_addrs)
msg.set_content(body)
return msg
Проверка: отправьте письмо с темой «Отчёт за сегодня» и откройте его в почтовом клиенте — тема читается по-русски.
Как правильно приложить Excel-файл
Вложение должно читаться в бинарном режиме, а MIME-тип — соответствовать формату. Для .xlsx это application/vnd.openxmlformats-officedocument.spreadsheetml.sheet. Ошибка новичка — открыть файл в текстовом режиме: тогда бинарные байты Excel портятся и файл не открывается на той стороне.
from pathlib import Path
def attach_xlsx(msg: EmailMessage, file_path: str) -> None:
path = Path(file_path)
data = path.read_bytes() # именно байты, не текст
msg.add_attachment(
data,
maintype="application",
subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
filename=path.name, # имя с кириллицей кодируется корректно
)
Проверка: полученный файл открывается в Excel без предупреждения о повреждении.
Ошибка 3. Порт и шифрование перепутаны
Самая частая причина «письмо не уходит и ошибки нет» — несоответствие порта и способа шифрования. На 587 работает STARTTLS: подключаетесь по обычному сокету и повышаете соединение до TLS. На 465 — сразу SSL. Если открыть SSL-соединение на 587-м порту, скрипт зависнет до таймаута.
| Порт | Шифрование | Класс smtplib | Что вызвать |
|---|---|---|---|
| 587 | STARTTLS | SMTP | .starttls() после connect |
| 465 | SSL/TLS | SMTP_SSL | шифрование сразу, starttls не нужен |
| 25 | нет или STARTTLS | SMTP | часто закрыт хостерами, не используйте |
Вариант под порт 587 со STARTTLS:
import smtplib
def send_starttls(msg: EmailMessage) -> None:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as server:
server.starttls() # повышаем соединение до TLS
server.login(SMTP_USER, SMTP_PASSWORD)
server.send_message(msg)
Вариант под порт 465 с SSL отличается только классом — smtplib.SMTP_SSL(...) без вызова starttls(). Проверка: в логе почтового сервиса появляется запись об успешной аутентификации, а письмо приходит в течение минуты.
Ошибка 4. Нет таймаута и обработки сбоев
SMTP-сервер может не ответить: сеть моргнула, провайдер временно недоступен. Без параметра timeout вызов SMTP() зависает на минуты, а cron запускает следующую копию поверх зависшей. Всегда задавайте таймаут и ловите ошибки отдельными типами — тогда в логе видно причину.
import logging
import smtplib
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("report_mailer")
def send_safe(msg: EmailMessage) -> bool:
try:
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=30) as server:
server.starttls()
server.login(SMTP_USER, SMTP_PASSWORD)
server.send_message(msg)
log.info("Письмо отправлено")
return True
except smtplib.SMTPAuthenticationError:
log.error("Аутентификация отклонена: проверьте пароль приложения")
except smtplib.SMTPRecipientsRefused as exc:
log.error("Адрес получателя отвергнут: %s", exc.recipients)
except (smtplib.SMTPException, OSError) as exc:
log.error("Сбой отправки: %s", exc)
return False
Отдельная обработка SMTPAuthenticationError избавляет от гадания: если пароль неверный, вы увидите это в логе, а не будете искать проблему в кодировке.
Ошибка 5. Молчаливый провал в планировщике
Под cron у скрипта другое окружение: нет переменных из вашего профиля, другой рабочий каталог, часто урезанный PATH. Скрипт, который в терминале работает, под cron падает на первой же строке os.environ["SMTP_USER"] с KeyError — и вы об этом не узнаете, потому что stdout уходит в никуда.
- Передавайте переменные окружения явно — в строке cron, в
EnvironmentFileдля systemd или через файл, который скрипт читает сам. - Пишите лог в файл, а не в stdout: тогда после сбоя есть что читать.
- Указывайте абсолютные пути к файлу отчёта. Относительный путь резолвится от рабочего каталога планировщика, а не от папки скрипта.
- Возвращайте ненулевой код выхода при сбое (
sys.exit(1)), чтобы планировщик или мониторинг видел провал.
Если не получилось
smtplib.SMTPAuthenticationError: 535— сервер не принял логин/пароль. Почти всегда: используете основной пароль вместо пароля приложения, либо в аккаунте не включён доступ по SMTP.- Скрипт висит и завершается по
TimeoutError— перепутаны порт и шифрование (SSL на 587 или STARTTLS на 465). Сверьтесь с таблицей выше. KeyError: 'SMTP_USER'— переменная окружения не задана в среде, где запущен скрипт. Под cron это отдельное окружение, см. ошибку 5.- Файл-вложение не открывается на той стороне — читаете файл в текстовом режиме или указали неверный MIME-подтип. Читайте
read_bytes()и задавайте подтип по формату. - Тема или имя файла в кракозябрах — собираете заголовки вручную. Переходите на
EmailMessage, он кодирует UTF-8 сам.
Что делать дальше
Когда отправка стабильно работает, добавьте наблюдаемость: пусть скрипт при сбое пишет в лог и, например, шлёт короткое уведомление в мессенджер. Настройте systemd timer вместо cron — он даёт свой лог через journalctl и не запускает копию поверх зависшей. Секреты по мере роста проекта переносите из переменных окружения в менеджер секретов. Точный синтаксис методов EmailMessage и smtplib для вашей версии Python сверяйте в официальной документации — сигнатуры между 3.x версиями стабильны, но подтвердить нелишне.
Магистр Excel и Google Sheets: ВПР, СУММЕСЛИМН, макросы VBA и скрипты Apps Script.
Спросить за 20 ₽Источники: Python docs — email: Examples, Python docs — smtplib
Частые вопросы
У меня Python 3.8, примеры подойдут?
Почему Gmail и Яндекс отклоняют мой пароль?
Письмо уходит с локальной машины, но падает на сервере — почему?
Как приложить сразу несколько файлов?
Стоит ли использовать сторонние библиотеки вместо smtplib?
Провайдер режет большие вложения — что делать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.