руководство · smtp в python3

Как продуктовой команде безопасно реализовать SMTP на Python 3?

Реализуйте SMTP на Python 3 в авторизованном серверном воркере, а не в браузере или коде, который контролирует пользователь. Собирайте письма с помощью EmailMessage, держите получателей конверта отдельно от видимых заголовков, создайте SSL-контекст с проверкой сертификатов, задайте конечные тайм-ауты подключения и используйте либо SMTP_SSL для TLS с самого начала соединения, либо SMTP.starttls() с последующим EHLO для явного перехода на TLS. Загружайте учётные данные из менеджера секретов, вызывайте send_message(), проверяйте результаты по отклонённым получателям и сохраняйте точный исход попытки. Повторяйте только временные сбои с ограниченной задержкой и никогда не считайте приём по SMTP доказательством попадания во «Входящие».

Определите одну авторизованную почтовую операцию

Отталкивайтесь от одобренного продуктового события — подтверждения аккаунта, чека, запрошенного оповещения или уведомления безопасности. Сохраните надёжную задачу исходящей отправки до открытия SMTP-соединения. Задача должна содержать стабильный ключ бизнес-события, тенанта, класс письма, ревизию шаблона, одобренных отправителя и получателей конверта, видимый адрес From, основание (согласие или необходимость) и текущий результат проверки стоп-листа. Ввод из браузера, мобильного приложения, шаблона или от пользователя не должен выбирать SMTP-хосты, учётные данные, отправителя конверта, произвольные заголовки или неограниченных получателей. Авторизуйте вызывающую сторону и тенанта, проверяйте адреса, ограничивайте число получателей и вложений и предотвращайте внедрение символов перевода строки. Захватывайте задачу один раз и ведите историю попыток только на добавление. smtplib в Python — это клиент протокола; он не обеспечивает идемпотентность на уровне бизнес-логики, изоляцию тенантов, согласие, стоп-лист или надёжную очередь. Эти механизмы должно обеспечивать приложение вокруг него.

Собирайте структурированные письма с помощью EmailMessage

Используйте email.message.EmailMessage вместо склеивания строк заголовков и MIME. Задайте From, To, Subject и стабильный заголовок корреляции приложения из проверенных значений, затем вызовите set_content для простого текста, add_alternative — для HTML-части при необходимости и add_attachment — только для явно поддерживаемых типов и размеров файлов. Генерируйте текст и HTML из одной одобренной ревизии шаблона. Экранируйте недоверенные значения с учётом контекста вывода и не рендерите необработанный пользовательский HTML. Не помещайте секреты, токены доступа, лишние персональные данные или внутренние ключи базы данных в заголовки, темы, поля отслеживания или имена вложений. Пакет email сериализует письмо согласно своей политике и может генерировать границы MIME при преобразовании, поэтому подписывайте или хешируйте итоговое сериализованное представление, если последующие проверки целостности зависят от точных байтов. Держите SMTP-конверт отдельно: видимые заголовки To и Cc адресованы читателям, а список получателей транспорта управляет командами RCPT TO.

Осознанно выбирайте неявный TLS или STARTTLS

Используйте SMTP_SSL, если сервер требует TLS с самого начала соединения. Используйте SMTP для открытого соединения, только если задокументированный процесс сервера требует немедленного перехода через STARTTLS. Согласно документации smtplib в Python, starttls переводит последующие SMTP-команды внутрь TLS, и после этого клиенту следует снова вызвать ehlo. Никогда не проходите аутентификацию до обязательного перехода на TLS. Создавайте контекст через ssl.create_default_context, чтобы проверка сертификата и имени хоста использовала безопасные клиентские настройки по умолчанию, и передавайте ожидаемое имя хоста сервера через обычное подключение библиотеки. Если шифрование обязательно, считайте отсутствие поддержки STARTTLS, ошибку сертификата, несовпадение имени хоста или сбой согласования TLS безусловной остановкой. Не отключайте проверку и не подставляйте непроверяющий контекст, чтобы заставить продакшен работать. TLS на участке передачи защищает SMTP-соединение, а не хранимое содержимое письма, обработку у провайдера, хранение у получателя или итоговый почтовый ящик.

Храните учётные данные на сервере с ограниченной областью действия

Загружайте имя пользователя SMTP, пароль или токен во время выполнения из управляемого сервиса секретов. Никогда не размещайте их в системе контроля версий, слоях Docker, конфигурации, закоммиченной в Git, URL, аргументах командной строки, отладочном выводе, аналитике, отчётах об исключениях, тестовых снимках, блокнотах, тикетах или промптах. Предпочитайте учётные данные, ограниченные одним окружением, доменом отправителя или разрешённой нагрузкой, административному секрету на весь аккаунт. Отделяйте продакшен от разработки и CI. Сделайте ротацию рутиной: выпустите замену, обновите воркер, проведите контрольный тест доставки, подтвердите данные об аутентификации и результате, а затем отзовите старые учётные данные. Ограничьте доступ к секрету процессом отправки и проводите аудит административных чтений. Метод login в Python перебирает механизмы аутентификации, объявленные сервером; приложение всё равно должно решать, допустимы ли сервер, защита соединения, аккаунт и механизм. Повторяющиеся ошибки аутентификации должны приостанавливать группу задач и запускать расследование, а не быстрые повторные попытки ввода пароля.

Используйте явные тайм-ауты и ограниченное время жизни соединения

Передавайте конечный тайм-аут в SMTP или SMTP_SSL, чтобы подключение и блокирующие операции не могли занимать воркер бесконечно. Применяйте внешний дедлайн задачи и политику отмены, потому что тайм-аут одного сокета — не полноценный контроль возраста очереди. Не используйте общий объект SMTP в параллельных задачах, если доступ к нему не сериализован и безопасность его состояния не доказана. Простая схема: открыть одно соединение для ограниченного пакета, поприветствовать сервер, установить TLS при необходимости, пройти аутентификацию, отправить небольшое число писем, вызвать quit и отбросить соединение после ошибок или по достижении предельного возраста. Повторное использование соединения снижает накладные расходы, но увеличивает неоднозначность после разрыва соединения сервером, тайм-аутов или частично изменённого состояния. Ограничивайте число писем на соединение и переподключайтесь осознанно. Отслеживайте задержку подключения, согласование TLS, аутентификацию, задержку команд, разрывы соединения сервером и возраст задач, не журналируя учётные данные и содержимое писем. SMTP-сервер может вводить ограничения, которые меняются независимо от Python.

Отправляйте одно письмо и сохраняйте частичные результаты по получателям

SMTP.sendmail использует from_addr и to_addrs для транспортного конверта и не переписывает заголовки письма. SMTP.send_message сериализует EmailMessage и выводит значения по умолчанию, если явные значения конверта не переданы. В коде для продакшена передавайте одобренного отправителя и список получателей конверта явно, чтобы обработка Bcc и авторизация тенанта оставались однозначными. Согласно документации Python, sendmail завершается нормально, если принят хотя бы один получатель, и возвращает словарь с записью для каждого отклонённого получателя. Поэтому отсутствие исключения — не то же самое, что успех для всех получателей. Сохраняйте наборы принятых и отклонённых получателей отдельно, включая код состояния и очищенную диагностику. Не повторяйте отправку принятым получателям, если отклонены лишь некоторые. Считайте результат по каждому получателю независимо авторизованным, сохраняя при этом общую попытку отправки письма. Исключение на этапе DATA отличается от отказа на RCPT и требует собственной классификации.

Классифицируйте исключения по этапу и постоянству

Обрабатывайте исключения smtplib явно и сохраняйте их SMTP-коды и очищенные сообщения сервера. SMTPConnectError и timeout могут быть временными, но могут указывать и на неверный хост, порт, файрвол или сбой. Исключение SMTPNotSupportedError после STARTTLS или SMTPUTF8 должно останавливать конфигурацию, которой нужна эта функция. SMTPAuthenticationError требует проверки учётных данных, аккаунта, механизма и TLS, а не слепого повтора. SMTPSenderRefused и SMTPRecipientsRefused требуют решений на уровне адреса отправителя или получателя. SMTPDataError описывает неожиданный ответ на DATA и в зависимости от расширенного статуса может означать проблему с содержимым, политикой, квотой или временное поведение получателя. Считайте ответы 4xx кандидатами на ограниченный повтор, а 5xx — постоянными для данной попытки, учитывая при этом документацию конкретного провайдера. Используйте экспоненциальную задержку, случайный разброс, лимиты числа попыток и возраста очереди и состояние dead-letter. Никогда не повторяйте отправку после добавления в стоп-лист, жалобы, отписки, отзыва авторизации или признаков некорректного получателя.

Сверяйте неоднозначные результаты отправки

Сетевой тайм-аут или разрыв соединения после того, как клиент передал данные письма, но до получения итогового ответа сервера, — неоднозначная ситуация. Сервер мог принять ответственность за письмо, даже если Python выбросил исключение. Не создавайте сразу новую логическую отправку. Пометьте попытку как неизвестную, сохраните её стабильные идентификаторы события и трассировки и при возможности запросите журналы провайдера или последующие события доставки. Если SMTP-сервис не предлагает идемпотентности или поиска по корреляции, определите продуктовое решение с учётом класса письма, его возраста, вреда от дубликата и пользовательского опыта. У оповещений безопасности и писем для сброса пароля риски дублирования иные, чем у чеков или финансовых уведомлений. Сохраняйте исходную попытку и ссылку на повтор в журнале. Никогда не заявляйте о доставке ровно один раз: SMTP не обеспечивает её от начала до конца. Тестируйте эту ветку на контролируемом тестовом сервере, который разрывает соединение на каждом этапе протокола, в том числе до и после приёма DATA.

Отделяйте приём по SMTP от доставки и вовлечённости

Успешный вызов send_message означает, что хотя бы один получатель был принят на наблюдаемом этапе SMTP в рамках задокументированной семантики Python. Это не доказывает, что приняты все получатели, что сервер назначения впоследствии сохранил письмо, что оно попало в папку «Входящие» или что его прочитал человек. Моделируйте как отдельные свидетельства отправку провайдеру, приём сервером получателя, временный или постоянный сбой, последующий отказ, жалобу, отписку, попадание в почтовый ящик и вовлечённость. Принимайте аутентифицированные события провайдера, если они есть, устраняйте дубликаты и храните время события отдельно от времени обработки. Применяйте постоянные отказы, жалобы и отписки непосредственно перед последующими отправками. Открытия и клики — не транспортное доказательство, и на них могут влиять технологии защиты приватности. Ведите агрегированные метрики с минимумом персональных данных по когортам, безопасным для тенантов, ревизии шаблона, домену отправителя, классу статуса и времени. Настройте оповещения о всплесках отклонений, неизвестных исходах, возрасте очереди, сбоях TLS, ошибках аутентификации и необычном разветвлении по получателям.

Тестируйте локально, не отправляя письма реальным клиентам

Покройте модульными тестами построение письма, отклонение внедрения заголовков, авторизацию получателей, удаление Bcc, текстовую и HTML-версии, обработку Unicode, ограничения вложений и проверки стоп-листа. Используйте контролируемый локальный SMTP-тестовый сервер или фикстуру протокола, чтобы моделировать сбои приветствия, отсутствие STARTTLS, сбой сертификата, ошибки аутентификации, частичное принятие RCPT, ответы DATA 4xx и 5xx, отключения и задержанные ответы. Не используйте устаревшие неаутентифицированные сервисы отладки для секретов, похожих на продакшен, или контента клиентов. Интеграционные тесты должны использовать выделенные аккаунты и контролируемых получателей с явными квотами и очисткой. Проверяйте необработанное полученное письмо, результаты аутентификации, видимые заголовки, поведение ответов и корреляцию событий. Выполняйте сканирование секретов в фикстурах и логах.

Как SendHQ вписывается в эту схему

SendHQ документирует email API в рамках рабочего пространства для отправки с подтверждённого домена, событий доставки и стоп-листов. Это руководство посвящено SMTP-клиенту Python из стандартной библиотеки; актуальные способы интеграции и контракт API смотрите в документации SendHQ.

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

Можно ли размещать учётные данные SMTP для Python в клиентском коде?

Нет. Храните их в серверном менеджере секретов с узкой областью действия по окружению и нагрузке, аудитом доступа, регулярной ротацией и без журналирования.

Когда в Python использовать SMTP_SSL?

Используйте SMTP_SSL, если TLS требуется с начала соединения. SMTP со starttls — только для задокументированного процесса с явным переходом на TLS, который прекращает соединение при сбое.

Нужно ли снова вызывать EHLO после starttls?

Да. Документация smtplib в Python предписывает снова вызывать ehlo после starttls, чтобы возможности сервера были заново получены внутри защищённого соединения.

Означает ли send_message, что все получатели приняты?

Нет. Python может завершиться нормально, если принят хотя бы один получатель, и возвращает отклонённых получателей отдельно. Сохраняйте и обрабатывайте результаты по получателям независимо.

Что делать после SMTPAuthenticationError?

Приостановите затронутую конфигурацию и проверьте TLS, сервер, аккаунт, секрет и объявленные механизмы. Слепые повторы с учётными данными могут усугубить блокировки или сигналы о компрометации.

Нужно ли повторять каждую SMTPDataError?

Нет. Сохраните точный статус и диагностику, а затем отличайте временные условия 4xx от постоянных сбоев 5xx, связанных с политикой, содержимым, квотой или конфигурацией.

Определяет ли приём по SMTP попадание во «Входящие»?

Нет. Это свидетельство только о транспортном этапе. Последующие релеи, фильтрация у получателя, отказы, правила почтового ящика, итоговая папка и реакция человека — отдельные результаты.

Охватывает ли это руководство интеграцию, специфичную для SendHQ?

Нет. Здесь рассматривается SMTP-клиент Python из стандартной библиотеки. Актуальные способы интеграции и контракт API смотрите в документации SendHQ.

Источники