Оба режима соединения передают одно и то же сообщение на один и тот же аккаунт релея, и разница в том, где начинается TLS, а не в том, что отправляется. Один открывается открытым текстом и обновляется внутри соединения; другой шифруется с первого байта. Это единственное различие решает, что примет строгая политика релея, что сможет заметить промежуточное устройство и какое сообщение об ошибке вы проведёте полдня, читая. Вот как я выбираю между ними, когда приложение в частной сети передаёт сообщение внутренней службе отправки.

Где два режима действительно расходятся

Задача одинакова в обоих случаях: аутентифицироваться как одобренная учётная запись, назвать разрешённого отправителя, перечислить получателей и дождаться, пока релей возьмёт ответственность. Меняется порядок операций. Порт 587 начинается как сессия SMTP открытым текстом, объявляет STARTTLS в ответе EHLO и обновляется только если клиент попросит. Порт 465 ожидает рукопожатия TLS до того, как будет произнесена любая команда SMTP.

Отправка и релей — разные задачи, поэтому номера портов не являются взаимозаменяемыми привычками. RFC 6409 описывает отправку сообщений как отдельную службу, а RFC 8314 охватывает вариант неявного TLS, который большинство современных релеев предоставляют на 465. Если ваш релей документирует один из двух, эта документация важнее любого предпочтения, вынесенного от прежнего работодателя.

Что даёт STARTTLS на 587

Обновление происходит внутри разговора SMTP, поэтому релей может объявить его, а клиент может отказаться продолжать без него. В Python starttls() выбрасывает исключение, когда сервер не предлагает это расширение, что превращает попытку понижения в видимый сбой вместо тихого входа открытым текстом. Именно это поведение является главным аргументом за режим, и на него стоит полагаться намеренно, а не случайно.

Окно раскрытия невелико, но реально: начальный обмен EHLO читается, и ничто до обновления не несёт учётных данных. На частном пути между подсетью приложения и внутренним релеем это окно обычно приемлемо. На пути, который вы не контролируете, именно это заставляет некоторые команды настаивать на другом режиме.

Устройство перехвата, удаляющее возможность STARTTLS, даёт неудачную отправку, а не откат. Это и есть нужный вам результат, и это самый быстрый способ обнаружить, что что-то переписывает SMTP на пути.

Что меняет неявный TLS на 465

Фазы открытого текста нет вообще. Клиент выполняет рукопожатие TLS сразу после открытия сокета, проверяет сертификат по своему хранилищу доверия и только затем обменивается EHLO, AUTH и сообщением. Ничто не может понизить сессию, потому что нет состояния открытого текста, из которого можно было бы вести переговоры, и это устраняет целый класс сбоев перехвата.

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

Одна функция отправки, два транспорта

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

export SMTP_HOST=smtp.relay.internal
export SMTP_PORT=587
export SMTP_USER=svc-notify
export [email protected]
export [email protected]
export SMTP_PASSWORD="$(cat /run/secrets/smtp_password)"
import os, smtplib, ssl
from email.message import EmailMessage

ctx = ssl.create_default_context()
msg = EmailMessage()
msg["From"] = os.environ["MAIL_FROM"]
msg["To"] = os.environ["MAIL_TEST_TO"]
msg["Subject"] = "relay submission test"
msg.set_content("One authorized test message from the app.")

host = os.environ["SMTP_HOST"]
port = int(os.environ.get("SMTP_PORT", "587"))

if port == 465:
    client = smtplib.SMTP_SSL(host, port, context=ctx, timeout=15)
else:
    client = smtplib.SMTP(host, port, timeout=15)
    client.ehlo()
    client.starttls(context=ctx)
    client.ehlo()

with client:
    client.login(os.environ["SMTP_USER"], os.environ["SMTP_PASSWORD"])
    refused = client.send_message(msg)

print("refused:", refused)

create_default_context() проверяет цепочку и имя хоста, поэтому частный CA нужно установить в хранилище доверия, а не обходить непроверенным контекстом. send_message() возвращает словарь получателей, которых релей отклонил; пустой словарь означает, что он принял всех. Пятнадцатисекундный тайм-аут — отправная точка, а не настроенное значение для вашей сети.

Держите пароль вне образа и вне журналов. Секрет, смонтированный во время выполнения и прочитанный в окружение, — это нормально; выводить окружение в строку журнала — нет, как и отладочный след обмена AUTH.

Выбирайте по политике релея и пути, а не по привычке

Три условия решают. Первое — что релей действительно объявляет: служба, слушающая только 587 с STARTTLS, недоступна на 465, как бы вы ни предпочитали неявный TLS. Второе — достаточно ли доверенный путь между приложением и релеем, чтобы принять читаемый обмен EHLO. Третье — переписывает ли что-то посередине SMTP или проксирует его, потому что это решает, является ли риск понижения теоретическим или реальным.

Когда релей предлагает оба, я беру 465 для службы, работающей без присмотра, и 587 для той, где человек наблюдает за первым развёртыванием, просто потому что сообщения об ошибках указывают на разные уровни. Это предпочтение моё, и это не политика.

Что бы вы ни выбрали, запишите порт, материал доверия и домен отправителя вместе с развёртыванием. Затем отправьте одно разрешённое тестовое сообщение и подтвердите, что релей его принял, сохраняя результат отправки и любой последующий отчёт о доставке как отдельные записи. Успешная передача релею — это конец ответственности приложения, а не доказательство того, что сообщение дошло до почтового ящика.

Похожие статьи