Урок 6 из 8 · 20 мин · обновлено

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

Как отправлять письма из Python: модули email и smtplib, письмо с вложением, рассылка по списку из CSV, пароль приложения вместо основного пароля, пробный запуск и правила, чтобы письма не попали в спам.

Задача

Отчёт из урока 4 готов, теперь его нужно отправить. Каждому менеджеру магазина — своё письмо с обращением по имени и названием магазина в теме. Список менеджеров лежит в таблице:

Текст
имя;почта;магазин
Светлана;svetlana@example.ru;Магазин 1
Дмитрий;dmitry@example.ru;Магазин 2
Елена;elena@example.ru;Магазин 3

Python умеет отправлять почту встроенными модулями: email собирает письмо, smtplib отправляет его через почтовый сервер.

Пароль приложения

Скрипт отправляет письма от вашего ящика, поэтому ему нужен вход в почту. Основной пароль для этого не используют. Крупные почтовые сервисы — Яндекс Почта, Mail.ru и другие — дают создать в настройках безопасности отдельный пароль приложения: он подходит только для программ, и его можно отозвать в любой момент.

Адреса серверов отправки:

Почта Сервер Порт
Яндекс Почта smtp.yandex.ru 465
Mail.ru smtp.mail.ru 465

Для корпоративной почты адрес сервера подскажет администратор.

Пароль никогда не пишут в коде: скрипты пересылают коллегам и выкладывают в Git. Его кладут в переменную окружения — настройку системы, которую скрипт читает при запуске. В терминале Windows (PowerShell):

Терминал
$env:MAIL_PASSWORD = "пароль-приложения"

На macOS и Linux:

Терминал
export MAIL_PASSWORD="пароль-приложения"

В скрипте его читают через os.environ["MAIL_PASSWORD"]. Так переменная живёт, пока открыт терминал. Как задать её постоянно, подскажет поиск по запросу «переменные среды» для вашей системы.

Скрипт рассылки

Python
import csv
import os
import smtplib
from email.message import EmailMessage
from pathlib import Path

DRY_RUN = True                      # пока True — письма только печатаются
SENDER = "otchety@kancmarket.ru"
REPORT = Path("Отчёт за неделю.xlsx")

def make_letter(name, email, shop):
    msg = EmailMessage()
    msg["From"] = SENDER
    msg["To"] = email
    msg["Subject"] = f"Продажи за неделю — {shop}"
    msg.set_content(f"{name}, добрый день!\n\nВо вложении отчёт о продажах за неделю.\n\nОтдел аналитики")
    msg.add_attachment(
        REPORT.read_bytes(), maintype="application",
        subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        filename=REPORT.name,
    )
    return msg

with open("менеджеры.csv", encoding="utf-8-sig", newline="") as f:
    letters = [make_letter(r["имя"], r["почта"], r["магазин"]) for r in csv.DictReader(f, delimiter=";")]

if DRY_RUN:
    for msg in letters:
        print(msg["To"], "|", msg["Subject"])
else:
    with smtplib.SMTP_SSL("smtp.yandex.ru", 465) as server:
        server.login(SENDER, os.environ["MAIL_PASSWORD"])
        for msg in letters:
            server.send_message(msg)
            print("Отправлено:", msg["To"])

Пробный запуск печатает, кому и с какой темой уйдут письма:

Результат
svetlana@example.ru | Продажи за неделю — Магазин 1
dmitry@example.ru | Продажи за неделю — Магазин 2
elena@example.ru | Продажи за неделю — Магазин 3

Разберём главное:

  • EmailMessage() — пустое письмо. Заголовки From, To, Subject задаются как ключи словаря, текст — через set_content();
  • add_attachment() прикрепляет файл: содержимое, тип и имя. Длинный subtype — официальный тип файлов Excel .xlsx. Для PDF было бы maintype="application", subtype="pdf";
  • SMTP_SSL(сервер, 465) открывает защищённое соединение с почтовым сервером, login() входит в ящик, send_message() отправляет письмо;
  • with сам закроет соединение, даже если по дороге случится ошибка.

Как не навредить

Рассылка — действие, которое нельзя отменить. Письмо, ушедшее ста людям с ошибкой в имени, уже не вернуть. Поэтому порядок такой:

  1. Пробный запуск с DRY_RUN = True: проверить список адресов и темы.
  2. Письмо себе: временно поставить в таблицу только свой адрес и посмотреть, как письмо выглядит в почте, открывается ли вложение.
  3. Рассылка всем.

И ещё несколько правил:

  • пишите только тем, кто ждёт ваших писем: коллегам, клиентам, которые согласились на рассылку. Массовые письма незнакомым людям почтовые сервисы считают спамом и блокируют ящик;
  • у почтовых сервисов есть ограничения на число писем в сутки. Для рабочих отчётов коллегам их хватает с запасом, для больших рассылок используют специальные сервисы;
  • если писем много, делайте паузу между отправками: time.sleep(1).

Попробуйте сами. Измените скрипт так, чтобы каждый менеджер получал не общий отчёт, а файл своего магазина: Светлане — магазин_1.xlsx, Дмитрию — магазин_2.xlsx. Подсказка: номер магазина есть в столбце «магазин».

Решение

Функция получает путь к файлу параметром:

Python
def make_letter(name, email, shop, report):
    msg = EmailMessage()
    msg["From"] = SENDER
    msg["To"] = email
    msg["Subject"] = f"Продажи за неделю — {shop}"
    msg.set_content(f"{name}, добрый день!\n\nВо вложении продажи вашего магазина за неделю.")
    msg.add_attachment(
        report.read_bytes(), maintype="application",
        subtype="vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        filename=report.name,
    )
    return msg

А путь к файлу собираем из номера магазина:

Python
for r in csv.DictReader(f, delimiter=";"):
    number = r["магазин"].split()[-1]            # «Магазин 1» → «1»
    report = Path(f"магазин_{number}.xlsx")
    letters.append(make_letter(r["имя"], r["почта"], r["магазин"], report))

Перед этим циклом создайте пустой список letters = []. Если файла магазина нет, read_bytes() остановит скрипт с ошибкой FileNotFoundError ещё до отправки — это хорошо: лучше не отправить ничего, чем отправить часть писем.

Мини-тест · 4 вопроса

Проверьте себя

Выберите вариант, который кажется вам правильным. В конце вы увидите свой результат и объяснение к каждому вопросу.

  1. Где безопаснее хранить пароль от почты для скрипта?

  2. Почта отказывает скрипту во входе с основным паролем. Что делать?

  3. Что делает msg.add_attachment(...)?

  4. В каком порядке безопаснее запускать новую рассылку?