Урок 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"]. Так переменная живёт, пока открыт терминал. Как задать её постоянно, подскажет поиск по запросу «переменные среды» для вашей системы.
Скрипт рассылки
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сам закроет соединение, даже если по дороге случится ошибка.
Как не навредить
Рассылка — действие, которое нельзя отменить. Письмо, ушедшее ста людям с ошибкой в имени, уже не вернуть. Поэтому порядок такой:
- Пробный запуск с
DRY_RUN = True: проверить список адресов и темы. - Письмо себе: временно поставить в таблицу только свой адрес и посмотреть, как письмо выглядит в почте, открывается ли вложение.
- Рассылка всем.
И ещё несколько правил:
- пишите только тем, кто ждёт ваших писем: коллегам, клиентам, которые согласились на рассылку. Массовые письма незнакомым людям почтовые сервисы считают спамом и блокируют ящик;
- у почтовых сервисов есть ограничения на число писем в сутки. Для рабочих отчётов коллегам их хватает с запасом, для больших рассылок используют специальные сервисы;
- если писем много, делайте паузу между отправками:
time.sleep(1).
Попробуйте сами. Измените скрипт так, чтобы каждый менеджер получал не общий отчёт, а файл своего магазина: Светлане — магазин_1.xlsx, Дмитрию — магазин_2.xlsx. Подсказка: номер магазина есть в столбце «магазин».
Решение
Функция получает путь к файлу параметром:
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
А путь к файлу собираем из номера магазина:
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 вопроса
Проверьте себя
Выберите вариант, который кажется вам правильным. В конце вы увидите свой результат и объяснение к каждому вопросу.
-
Где безопаснее хранить пароль от почты для скрипта?
Код часто пересылают коллегам и выкладывают в Git. Пароль из переменной окружения не попадёт туда вместе со скриптом.
-
Почта отказывает скрипту во входе с основным паролем. Что делать?
Крупные почтовые сервисы пускают программы по отдельному паролю приложения. Его можно отозвать в любой момент, не меняя основной пароль.
-
Что делает msg.add_attachment(...)?
add_attachment добавляет вложение: содержимое файла, его тип и имя, под которым файл увидит получатель.
-
В каком порядке безопаснее запускать новую рассылку?
Пробный запуск покажет список и темы писем, письмо себе — как оно выглядит у получателя. Ошибку в письме, ушедшем сотне людей, уже не исправить.