Надсилання пошти Python через SMTP: STARTTLS на 587 чи TLS на 465?
STARTTLS на 587 і неявний TLS на 465 обидва передають відправлення з Python, але лише один відповідає вашому релею. Порівняйте рукостискання та режими відмови.
Обидва режими з'єднання передають те саме повідомлення на той самий обліковий запис релея, і різниця в тому, де починається 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 для тієї, де людина спостерігає за першим розгортанням, просто тому що повідомлення про помилки вказують на різні рівні. Ця перевага моя, і це не політика.
Що б ви не вибрали, запишіть порт, матеріал довіри й домен відправника разом із розгортанням. Потім надішліть одне дозволене тестове повідомлення і підтвердьте, що релей його прийняв, зберігаючи результат надсилання та будь-який пізніший звіт про доставку як окремі записи. Успішна передача релею — це кінець відповідальності застосунку, а не доказ того, що повідомлення дійшло до скриньки.