Вложения и массовая рассылка писем

Отчёт ценен с вложением. Модуль email умеет прикреплять файлы любого типа, а шаблонизация позволяет разослать персональные письма всему списку получателей.
Суть: msg.add_attachment(данные, тип, имя) прикрепляет файл. Массовая рассылка = цикл по получателям с персональным телом из шаблона.

В прошлом уроке мы отправили текстовое письмо. Но обычно цель — доставить файл: Excel-отчёт, PDF-счёт, договор. EmailMessage добавляет вложение методом add_attachment, которому нужны три вещи: байты файла, его MIME-тип (например application/pdf) и имя, под которым получатель его увидит.

Массовая рассылка соединяет это с шаблонами из урока про Word: для каждого получателя подставляем его данные в текст письма. Логику персонализации и подготовки рассылки соберём в браузере на stdlib.

Попробуй сам ▶

from email.message import EmailMessage

# Список рассылки
recipients = [
    {'email': '[email protected]', 'name': 'Анна', 'debt': 0},
    {'email': '[email protected]', 'name': 'Борис', 'debt': 4200},
    {'email': '[email protected]', 'name': 'Вика', 'debt': 1500},
]

body_tpl = 'Здравствуйте, {name}! {tail}'

queue = []
for r in recipients:
    if r['debt'] > 0:
        tail = f"Напоминаем о задолженности {r['debt']} руб."
    else:
        tail = 'Спасибо, у вас нет задолженностей.'
    msg = EmailMessage()
    msg['To'] = r['email']
    msg['Subject'] = 'Состояние счёта'
    msg.set_content(body_tpl.format(name=r['name'], tail=tail))
    queue.append(msg)

print(f'Подготовлено писем: {len(queue)}')
for m in queue:
    print('->', m['To'], '|', m.get_content().strip())

Каждое письмо получает персональный текст в зависимости от данных получателя — это и есть суть умной рассылки. Прикрепление реального файла и отправка трогают диск и сеть, поэтому показаны как нерабочая в браузере врезка.

from pathlib import Path
from email.message import EmailMessage
import smtplib, os

msg = EmailMessage()
msg['From'] = '[email protected]'
msg['To'] = '[email protected]'
msg['Subject'] = 'Ваш счёт'
msg.set_content('Счёт во вложении.')

data = Path('invoice.pdf').read_bytes()
msg.add_attachment(data, maintype='application',
                   subtype='pdf', filename='invoice.pdf')

with smtplib.SMTP_SSL('smtp.company.ru', 465) as s:
    s.login('[email protected]', os.environ['EMAIL_PASSWORD'])
    s.send_message(msg)
МАССОВАЯ РАССЫЛКА

  список получателей
       |  для каждого:
       v
  шаблон + данные -> персональное письмо
       |  + вложение (add_attachment)
       v
  send_message  --пауза-->  следующий

  пауза между письмами спасает от спам-лимита

Для серьёзных рассылок одного цикла по адресам мало — нужна идемпотентность. Представьте: скрипт разослал половину писем и упал на сбое сети. При повторном запуске наивный цикл отправит первую половину заново, и часть людей получит письмо дважды. Решение — вести журнал уже отправленных адресов (хоть в простом файле) и перед отправкой проверять, не ушло ли письмо этому получателю. Тогда повторный запуск спокойно «доберёт» оставшихся, не беспокоя тех, кто уже получил. Это тот самый принцип идемпотентности из начала курса, применённый к рассылке: повторный запуск не должен ломать уже сделанную работу.

Как работает под капотом

Письмо с вложением — это multipart-сообщение: оно состоит из частей (тело + каждый файл), разделённых служебными границами. add_attachment сам создаёт нужную структуру и кодирует бинарный файл в base64, потому что по протоколу почты можно передавать только текст. Получатель декодирует обратно. Вам всё это знать необязательно — метод прячет сложность.

MIME-тип (maintype/subtype) подсказывает почтовому клиенту, чем открыть вложение: application/pdf — PDF, image/png — картинка. Неверный тип не сломает доставку, но клиент может не предложить правильное приложение для открытия.

Частые ошибки

  • Слать всё одним залпом. Сотни писем за секунду — прямой путь в спам-фильтр. Делайте паузы.
  • Один сбой роняет всю рассылку. Оборачивайте каждое письмо в try, чтобы плохой адрес не остановил остальные.
  • Видимые адреса всех получателей. Не кладите весь список в To; для рассылки шлите по одному письму на адрес.

Best practices

  • Читайте файл как байты (read_bytes()) и указывайте корректный MIME-тип.
  • Делайте небольшую паузу между письмами и логируйте каждый результат.
  • Ведите учёт отправленных, чтобы при сбое не разослать дубли при повторном запуске.

Итоги. add_attachment прикрепляет файлы, а шаблон по списку получателей даёт персональную рассылку. Паузы и обработка ошибок отличают надёжную рассылку от спама. Дальше научимся забирать данные из веба.

Проверьте себя
1. Что нужно передать в add_attachment, чтобы прикрепить файл?
AТолько путь к файлу
BБайты файла, его MIME-тип и имя для получателя
CТолько имя файла
DСсылку на файл в облаке
2. Почему массовую рассылку делают с паузами между письмами?
AЧтобы письма дошли в правильном порядке
BЧтобы не сработали спам-лимиты сервера
CЧтобы уменьшить размер писем
DТак требует модуль email