راهنما · SMTP در Python

یک تیم محصول چگونه باید SMTP را در Python به‌صورت امن پیاده‌سازی کند؟

SMTP را در Python از یک worker پس‌زمینه مجاز پیاده‌سازی کنید، نه مستقیماً از یک درخواست وب. پیام را با EmailMessage بسازید، برای TLS ضمنی از SMTP_SSL استفاده کنید یا وقتی قرارداد فعلی ارائه‌دهنده الزام می‌کند اتصال را با STARTTLS به‌صورت صریح ارتقا دهید، با یک راز سمت سرور احراز هویت کنید و send_message را با timeoutهای محدود فراخوانی کنید. job را پیش از اتصال ذخیره کنید، شواهد رد شدن در سطح هر گیرنده را ثبت کنید، قطع‌شدن‌های مبهم را تطبیق دهید و پذیرش SMTP را از تحویل بعدی و رسیدن به صندوق ورودی تفکیک کنید.

پیش از SMTP، ارسال را مجوزدهی و ذخیره کنید

با یک رویداد مشروع برنامه مانند رسید، هشدار امنیتی، تأیید درخواست‌شده یا اطلاعیه حساب شروع کنید. فراخواننده را احراز هویت کنید و tenant، نوع پیام، هویت From قابل‌مشاهده، گیرنده و نسخه قالب را مجوزدهی کنید. پیش از باز کردن هر اتصال SMTP، یک job خروجی پایدار با کلید پایدار رویداد کسب‌وکار بنویسید. این کلید باید مانع شود دو worker به‌طور مستقل یک پیام منطقی واحد را بسازند. ورودی مرورگر نباید میزبان SMTP، پورت، نام کاربری، فرستنده envelope، گیرندگان دلخواه، هدرها یا سیاست TLS را انتخاب کند. این مقادیر را در پیکربندی بازبینی‌شده سرور نگه دارید. worker صف باید یک job را claim کند، موارد توقف ارسال و مجوز را در زمان ارسال دوباره بررسی کند، هر تلاش را ثبت کند و job را از طریق وضعیت‌های صریح آزاد یا نهایی کند. کتابخانه SMTP در Python پیام آماده‌شده را منتقل می‌کند؛ مجوزدهی tenant، رضایت، idempotency یا سیاست توقف ارسال را فراهم نمی‌کند.

پیام را با EmailMessage بسازید

به‌جای الحاق هدرها و بدنه خام، از email.message.EmailMessage استفاده کنید. From، To، Subject، Date و یک Message-ID تولیدشده را طبق مدل تأییدشده برنامه تنظیم کنید و سپس در صورت نیاز از set_content برای متن و از add_alternative برای HTML استفاده کنید. اشیای نشانی را اعتبارسنجی کنید، تعداد گیرندگان و پیوست‌ها را محدود کنید، تزریق newline در مقادیر را رد کنید و داده‌های قالب را برای بافت خروجی escape کنید. هر دو نسخه متن و HTML را از یک نسخه تغییرناپذیر قالب تولید کنید. اسرار و داده‌های شخصی غیرضروری را از موضوع، هدرهای سفارشی، نام فایل‌ها، فیلدهای عیب‌یابی و لاگ‌ها دور نگه دارید. هدر From قابل‌مشاهده را آگاهانه از فرستنده envelope در SMTP جدا کنید، چون احراز هویت و پردازش برگشت ایمیل ممکن است به هویت‌های متفاوتی وابسته باشند. به‌جای نگه داشتن بدنه کامل پیام‌ها بدون نیاز تعریف‌شده، یک نسخه محتوا یا hash امن از نظر حریم خصوصی را برای ممیزی ذخیره کنید.

TLS ضمنی یا STARTTLS را صریحاً انتخاب کنید

Python، SMTP_SSL را برای اتصال‌هایی که از ابتدا رمزنگاری می‌شوند و SMTP.starttls را برای ارتقای یک اتصال برقرارشده مستند کرده است. به‌جای حدس زدن از یک فهرست عمومی پورت‌ها، از hostname، پورت، گواهی و قرارداد ارسال فعلی ارائه‌دهنده پیروی کنید. یک SSL context پیش‌فرض تأییدشده بسازید و بررسی گواهی یا hostname را غیرفعال نکنید. برای STARTTLS، وصل شوید، در صورت نیاز EHLO بفرستید، starttls را با context فراخوانی کنید و دوباره EHLO بفرستید، چون extensionهای اعلام‌شده ممکن است پس از ارتقا تغییر کنند. هرگز اعتبارنامه‌ها یا محتوای پیام مشتری را روی اتصال متن ساده ارسال نکنید. RFC 8314 ارسال محافظت‌شده با TLS را توصیه می‌کند و دسترسی متن ساده را منسوخ می‌داند. خطای گواهی، عدم تطابق hostname، نبود STARTTLS الزامی یا تغییرات غیرمنتظره در قابلیت‌ها را خطاهای قطعی بدانید که نیاز به بررسی دارند، نه اینکه بی‌صدا به حالت ناامن برگردید.

اعتبارنامه‌های SMTP را در محدوده‌ای محدود و امن نگه دارید

نام کاربری و رمز عبور یا token را در زمان اجرا از یک سامانه مدیریت‌شده اسرار در سمت سرور بارگذاری کنید. اعتبارنامه‌ها را در کد منبع، bundleهای کلاینت، dumpهای محیط، URLها، traceهای استثنا، analytics، notebookها، اسکرین‌شات‌ها، promptها یا fixtureهای commit‌شده قرار ندهید. هر اعتبارنامه را به کوچک‌ترین محیط و بار کاری که ارائه‌دهنده پشتیبانی می‌کند محدود کنید و development را از production جدا کنید. فقط پس از برقراری وضعیت TLS الزامی احراز هویت کنید. چرخش را با گیرندگان کنترل‌شده تمرین کنید: جایگزین را از طریق مدیریت تأییدشده فراهم کنید، worker را به‌روز کنید، احراز هویت و یک چرخه کامل رویداد را تأیید کنید و سپس مقدار قدیمی را باطل کنید. خطای مکرر احراز هویت باید مسیر آسیب‌دیده را متوقف کند، نه اینکه یک حلقه تلاش مجدد سریع راه بیندازد. متد login در Python بین سازوکارهای اعلام‌شده توسط سرور مذاکره می‌کند، اما سازوکار واقعی ارائه‌دهنده، سیاست حساب، مجوزهای token و رفتار چرخش به شواهد به‌روز نیاز دارند.

از یک تابع ارسال محدود در Python استفاده کنید

adapter ارائه‌دهنده را کوچک نگه دارید و شواهد ساختاریافته را به ماشین وضعیت job برگردانید. یک جریان معمول، یک SSL context می‌سازد، برای TLS ضمنی SMTP_SSL(host, port, timeout=10) را به‌صورت smtp باز می‌کند، smtp.login(username, secret) را فراخوانی می‌کند و سپس smtp.send_message(message, from_addr=envelope_from, to_addrs=recipients) را فراخوانی می‌کند. برای ارائه‌دهنده‌ای که ارتقای صریح را الزامی می‌کند، از SMTP با timeout، سپس ehlo، starttls(context=context)، دوباره ehlo و در پایان login استفاده کنید. hostnameها یا پورت‌های نمونه را پیش‌فرض همگانی معرفی نکنید. به‌جای اتکا به parse هدرهای غیرقابل‌اعتماد، یک فهرست گیرنده نرمال‌شده ارسال کنید. کلاس استثنا، کد پاسخ SMTP و متن عیب‌یابی محدود را در صورت وجود ثبت کنید، اما نشانی‌ها، اعتبارنامه‌ها و محتوای پیام را حذف (redact) کنید. مراحل اتصال، TLS، احراز هویت، envelope، داده و quit را جداگانه اندازه بگیرید تا خطاهای عملیاتی قابل‌عیب‌یابی بمانند.

نتایج گیرندگان send_message را دقیق تفسیر کنید

Python مستند کرده است که sendmail و send_message وقتی ایمیل دست‌کم برای یک گیرنده پذیرفته شود به‌طور عادی برمی‌گردند و یک دیکشنری برای گیرندگان ردشده برمی‌گردانند؛ دیکشنری خالی یعنی در آن مرحله هیچ گیرنده‌ای رد نشده است. این نتیجه در سطح گیرنده را نگه دارید، نه اینکه کل job را تحویل‌شده علامت بزنید. اگر همه گیرندگان رد شوند، کتابخانه استثنای SMTPRecipientsRefused را برمی‌انگیزد. استثناهای دیگر رد فرستنده، رد DATA، احراز هویت، اتصال، پروتکل و خطاهای مرتبط را از هم تفکیک می‌کنند. شواهد دقیق را به وضعیت‌های برنامه نگاشت کنید: پذیرفته‌شده توسط سرور ارسال، ردشده دائمی، ردشده موقت یا نامشخص. بازگشت عادی فقط نتیجه ارسال SMTP در همان محدوده را ثابت می‌کند. پذیرش توسط سرور مقصد، قرار گرفتن نهایی در صندوق، خوانده شدن یا تعامل را ثابت نمی‌کند. اعلان‌های وضعیت تحویل (DSN) یا رویدادهای ارائه‌دهنده در مراحل بعد باید جداگانه تطبیق داده شوند.

فقط وقتی ریسک ارسال تکراری کنترل شده است دوباره تلاش کنید

پیش از زمان‌بندی تلاش دیگر، خطاها را دسته‌بندی کنید. خطاهای دائمی نشانی، فرستنده، احراز هویت، سیاست یا محتوا معمولاً به اصلاح یا توقف ارسال نیاز دارند، نه تکرار خودکار. پاسخ‌های موقت 4xx را می‌توان با exponential backoff، jitter، سقف تعداد تلاش، زمان انقضا و بودجه به ازای هر مقصد دوباره تلاش کرد. reset شدن اتصال یا timeout پس از ارسال داده پیام ممکن است مبهم باشد: سرور ممکن است پیام را پذیرفته باشد در حالی که کلاینت پاسخ نهایی را از دست داده است. آن تلاش را نامشخص نگه دارید، فعالیت ارائه‌دهنده یا رویدادهای بعدی را از طریق تطبیق امن از نظر حریم خصوصی بررسی کنید و از ارسال مجدد فوری و کورکورانه پرهیز کنید. SMTP هیچ کلید idempotency همگانی در سطح برنامه ندارد. کلید پایدار رویداد کسب‌وکار از تلاش‌های هم‌زمان برنامه جلوگیری می‌کند، اما نمی‌تواند یک سرور SMTP راه دور را وادار کند دو ارسال پذیرفته‌شده را یکی کند. نتایج مبهم مکرر را ارجاع دهید و شواهد دقیقی را که مبنای تصمیم بوده نگه دارید.

گیرندگان جزئی و موارد توقف ارسال را مدیریت کنید

وقتی پیامی چند گیرنده دارد، SMTP ممکن است برخی را بپذیرد و برخی را رد کند. پاسخ هر گیرنده را ذخیره کنید و فقط زیرمجموعه پذیرفته‌شده را به وضعیت بعدی ببرید. صرفاً چون یک نشانی رد موقت دریافت کرده، کل فهرست اصلی را دوباره ارسال نکنید. موارد توقف ارسال ناشی از برگشت دائمی، گزارش اسپم، لغو اشتراک، الزامات قانونی، tenant و مدیر را پیش از هر تلاش، از جمله تلاش‌های مجدد، اعمال کنید. انواع پیام را فقط از طریق یک سیاست صریح و مستند از هم جدا کنید؛ برچسب تراکنشی زدن روی یک پیام، ایمنی گیرنده یا محدودیت‌های ارائه‌دهنده را از بین نمی‌برد. در گردش‌کارهای حساس، وقتی حریم خصوصی و وضعیت اختصاصی هزینه را توجیه می‌کنند، jobهای تک‌گیرنده را ترجیح دهید. از افشای فهرست گیرندگان از طریق To یا Cc پرهیز کنید و هرگز از رفتار Bcc به‌عنوان جایگزین مجوزدهی استفاده نکنید. متن عیب‌یابی را محدود و redact کنید، چون پاسخ‌های SMTP ممکن است شامل نشانی گیرندگان یا جزئیات مخصوص گیرنده باشند.

مسیرهای خطا را با سیستم‌های کنترل‌شده آزمایش کنید

ساخت پیام، Unicode، نسخه‌های جایگزین متن و HTML، پیوست‌ها، رد هدرها، نرمال‌سازی گیرندگان، تأیید TLS، نبود STARTTLS، اعتبارنامه‌های نامعتبر، رد فرستنده، رد یک گیرنده و همه گیرندگان، رد DATA، timeout پیش و پس از پذیرش احتمالی، قطع اتصال، پاسخ‌های محدودیت نرخ، انقضای تلاش مجدد، workerهای تکراری، تغییرات فهرست توقف ارسال و چرخش راز را آزمایش کنید. برای آزمون‌های واحد و یکپارچه‌سازی قطعی، از یک سرویس SMTP آزمایشی کنترل‌شده یا یک شبیه‌ساز محلی استفاده کنید؛ هرگز ترافیک ناخواسته از محیط‌های غیرعملیاتی را به نشانی‌های مشتریان هدایت نکنید. در canaryهای production، از گیرندگان مجاز استفاده کنید و هدرهای خام را از نظر From قابل‌مشاهده، مسیر envelope، Message-ID، DKIM، SPF، هم‌راستایی DMARC و شواهد ارائه‌دهنده بررسی کنید. تأیید کنید که لاگ‌ها و معیارها هیچ نشتی از اعتبارنامه یا بدنه پیام ندارند. اگر worker می‌تواند مجوزدهی tenant را دور بزند، TLS را تنزل دهد، بدون محدودیت دوباره تلاش کند، رد جزئی را نادیده بگیرد یا نمی‌تواند مسیر ارسال را متوقف کند، راه‌اندازی را رد کنید.

SendHQ چگونه جای می‌گیرد

SendHQ یک API ایمیل در سطح فضای کاری برای ارتباط موردانتظار محصول است. مستندات آن ارسال، دامنه‌های تأییدشده، رویدادهای تحویل و موارد توقف ارسال را پوشش می‌دهد. هنگام یکپارچه‌سازی SendHQ با Python از API ‏HTTP مستندشده آن استفاده کنید.

پرسش‌های متداول

در Python از SMTP_SSL استفاده کنم یا STARTTLS؟

از حالتی استفاده کنید که قرارداد فعلی ارسال (submission) ارائه‌دهنده الزام می‌کند. SMTP_SSL از ابتدای اتصال رمزنگاری می‌کند؛ STARTTLS به‌صورت صریح ارتقا می‌دهد و به TLS تأییدشده و یک EHLO تازه نیاز دارد.

آیا بازگشت عادی send_message تحویل را ثابت می‌کند؟

خیر. یعنی دست‌کم یک گیرنده در همان مرحله ارسال SMTP پذیرفته شده است. پذیرش مقصد، قرار گرفتن در صندوق و تعامل به شواهد بعدی و مشخص نیاز دارند.

دیکشنری بازگشتی send_message چه معنایی دارد؟

گیرندگانی را که سرور SMTP رد کرده به شواهد پاسخ نگاشت می‌کند. دیکشنری خالی یعنی در آن مرحله هیچ گیرنده‌ای رد نشده است، نه اینکه همه پیام‌ها به صندوق ورودی رسیده‌اند.

آیا می‌توان timeout را فوراً دوباره تلاش کرد؟

اگر پس از ارسال احتمالی رخ داده باشد، خیر؛ انجام این کار ایمن نیست. تلاش را مبهم نگه دارید، شواهد ارائه‌دهنده یا رویدادهای بعدی را تطبیق دهید و فقط طبق یک سیاست محدود برای ریسک ارسال تکراری دوباره ارسال کنید.

رمز عبور SMTP را کجا باید ذخیره کرد؟

از یک سامانه مدیریت‌شده اسرار در سمت سرور استفاده کنید که دسترسی محدود به بار کاری و محیط، بازیابی ممیزی‌شده، چرخش آزموده‌شده و هیچ افشایی به کلاینت‌ها، لاگ‌ها، promptها یا fixtureها نداشته باشد.

آیا هرگز باید تأیید گواهی را در production غیرفعال کرد؟

خیر. خطای گواهی یا hostname نشانه پیکربندی ناامن یا نادرست است. به‌جای تضعیف بی‌صدای تأیید TLS، مسیر را متوقف و عیب‌یابی کنید.

رد شدن بخشی از گیرندگان را چگونه باید مدیریت کرد؟

نتیجه هر گیرنده را ذخیره کنید، زیرمجموعه پذیرفته‌شده را جلو ببرید و فقط ردهای موقت واجد شرایط را دوباره تلاش کنید. گیرندگانی را که قبلاً پذیرفته شده‌اند همراه با کل فهرست اصلی دوباره ارسال نکنید.

آیا این صفحه ثابت می‌کند SendHQ از SMTP پشتیبانی می‌کند؟

خیر. این راهنما SMTP ‏Python را به‌طور کلی پوشش می‌دهد؛ برای API ایمیل SendHQ از مستندات کنونی آن استفاده کنید.

منابع