ارسال SMTP با Python: STARTTLS روی 587 یا TLS روی 465؟
STARTTLS روی 587 و TLS ضمنی روی 465 هر دو ارسال یک پیام Python را حمل میکنند، اما تنها یکی با رلهٔ شما سازگار است. دستدادن و حالتهای خرابی را مقایسه کنید.
هر دو حالت اتصال همان پیام را به همان حساب رله میرسانند و تفاوت در جایی است که TLS آغاز میشود، نه در آنچه فرستاده میشود. یکی در متن آشکار باز میشود و درون باند ارتقا مییابد؛ دیگری از نخستین بایت رمزگذاری شده است. همین یک تفاوت تعیین میکند که سیاست سختگیرانهٔ رله چه چیزی را میپذیرد، یک دستگاه میانی چه چیزی را میتواند ببیند، و کدام پیام خطا را یک بعدازظهر کامل میخوانید. در ادامه میگویم وقتی برنامهای در شبکهٔ خصوصی پیامی را به سرویس ارسال داخلی میسپارد، چگونه بین این دو انتخاب میکنم.
دو حالت در کجا واقعاً از هم جدا میشوند
کار در هر دو حالت یکسان است: احراز هویت بهعنوان حسابی تأییدشده، معرفی یک فرستندهٔ مجاز، فهرستکردن گیرندگان و انتظار برای اینکه رله مسئولیت را بپذیرد. آنچه تغییر میکند ترتیب عملیات است. پورت 587 بهصورت نشست SMTP با متن آشکار آغاز میشود، STARTTLS را در پاسخ EHLO اعلام میکند و تنها اگر کلاینت بخواهد ارتقا مییابد. پورت 465 پیش از گفتن هر فرمان SMTP انتظار دستدادن TLS دارد.
ارسال و رله دو کار متفاوتاند، و به همین دلیل شمارهٔ پورتها عادتهای قابلتعویض نیستند. 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 را، فقط به این دلیل که پیامهای خرابی به لایههای متفاوتی اشاره میکنند. این ترجیح از من است و سیاست نیست.
هر چه برگزینید، پورت، مادهٔ اعتماد و دامنهٔ فرستنده را همراه با استقرار ثبت کنید. سپس یک پیام آزمون مجاز بفرستید و تأیید کنید که رله آن را پذیرفته است، و نتیجهٔ ارسال و هر گزارش تحویل بعدی را بهصورت سوابق جداگانه نگه دارید. تحویل موفق به رله پایان مسئولیت برنامه است، نه مدرکی بر اینکه پیام به صندوق ورودی رسیده است.