راهنما · SMTP در Python 3

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

SMTP در Python 3 را پشت یک worker مجاز سمت سرور پیاده‌سازی کنید، نه در کد مرورگر یا کدی که کاربر کنترل می‌کند. پیام‌ها را با EmailMessage بسازید، گیرندگان envelope را از هدرهای قابل‌مشاهده جدا نگه دارید، یک SSL context تأییدشده بسازید، timeoutهای محدود برای اتصال تعیین کنید و یا از SMTP_SSL برای TLS از ابتدای اتصال استفاده کنید یا از SMTP.starttls() و سپس EHLO برای ارتقای صریح. اعتبارنامه‌ها را از یک مدیر اسرار بارگذاری کنید، send_message() را فراخوانی کنید، نتایج گیرندگان ردشده را بررسی کنید و نتیجه دقیق تلاش را ذخیره کنید. فقط خطاهای گذرا را با backoff محدود دوباره امتحان کنید و هرگز پذیرش SMTP را دلیل رسیدن به صندوق ورودی ندانید.

یک عملیات ایمیل مجاز تعریف کنید

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

پیام‌های ساخت‌یافته را با EmailMessage بسازید

به‌جای الحاق رشته‌های هدر و MIME از email.message.EmailMessage استفاده کنید. From، To، Subject و یک هدر همبستگی پایدار اپلیکیشن را از مقادیر اعتبارسنجی‌شده تنظیم کنید، سپس set_content را برای متن ساده، در صورت نیاز add_alternative را برای بخش HTML و add_attachment را فقط برای انواع و اندازه‌های فایلی که صراحتاً پشتیبانی می‌شوند فراخوانی کنید. متن و HTML را هر دو از یک نسخه تأییدشده قالب تولید کنید. مقادیر غیرقابل‌اعتماد را برای زمینه خروجی‌شان escape کنید و از رندر HTML خام کاربر پرهیز کنید. اسرار، توکن‌های دسترسی، داده‌های شخصی غیرضروری یا کلیدهای داخلی پایگاه داده را در هدرها، موضوع‌ها، فیلدهای ردیابی یا نام پیوست‌ها قرار ندهید. بسته email طبق policy خود serialize می‌کند و ممکن است هنگام flatten کردن مرزهای MIME تولید کند، پس اگر کنترل‌های یکپارچگی بعدی به بایت‌های دقیق وابسته‌اند، نمایش serialize‌شده نهایی را امضا یا hash کنید. envelope در SMTP را جدا نگه دارید: هدرهای قابل‌مشاهده To و Cc برای خوانندگان است، در حالی که فهرست گیرندگان انتقال، فرمان‌های RCPT TO را کنترل می‌کند.

میان TLS ضمنی و STARTTLS آگاهانه انتخاب کنید

وقتی سرور از ابتدای اتصال TLS را الزامی می‌کند، از SMTP_SSL استفاده کنید. از SMTP برای اتصال متن آشکار فقط زمانی استفاده کنید که گردش‌کار مستند سرور ارتقای فوری STARTTLS را الزامی کند. مستندات smtplib در Python می‌گوید starttls فرمان‌های بعدی SMTP را درون TLS قرار می‌دهد و کلاینت باید پس از آن دوباره ehlo را فراخوانی کند. هرگز پیش از ارتقای لازم به TLS احراز هویت نکنید. context را با ssl.create_default_context بسازید تا اعتبارسنجی گواهی و بررسی نام هاست از پیش‌فرض‌های امن کلاینت استفاده کنند و نام هاست موردانتظار سرور را از طریق اتصال عادی کتابخانه بدهید. وقتی رمزنگاری الزامی است، نبود پشتیبانی STARTTLS، خطای گواهی، عدم تطابق نام هاست یا شکست مذاکره TLS را توقف قطعی بدانید. برای کار کردن محیط عملیاتی، تأیید را غیرفعال نکنید یا context تأییدنشده جایگزین نکنید. TLS در هر مرحله (hop) از اتصال SMTP محافظت می‌کند، نه از محتوای ذخیره‌شده پیام، پردازش ارائه‌دهنده، ذخیره‌سازی گیرنده یا صندوق ایمیل نهایی.

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

نام کاربری، رمز عبور یا توکن SMTP را در زمان اجرا از یک سرویس مدیریت اسرار بارگذاری کنید. هرگز آن را در کنترل نسخه، لایه‌های Docker، پیکربندی commit‌شده در Git، URLها، آرگومان‌های خط فرمان، خروجی debug، analytics، گزارش‌های استثنا، snapshotهای آزمون، notebookها، تیکت‌ها یا promptها قرار ندهید. اعتبارنامه‌ای با دامنه یک محیط، یک دامنه فرستنده یا یک بار کاری مجاز را به راز مدیریتی در سطح کل حساب ترجیح دهید. محیط عملیاتی را از توسعه و CI جدا کنید. چرخش را به کاری روزمره تبدیل کنید: جایگزین را فراهم کنید، worker را به‌روز کنید، یک آزمون تحویل کنترل‌شده اجرا کنید، شواهد احراز هویت و نتیجه را تأیید کنید و سپس اعتبارنامه قدیمی را ابطال کنید. دسترسی به راز را به فرایند ارسال محدود کنید و خواندن‌های مدیریتی را ممیزی کنید. متد login در Python سازوکارهای احراز هویتی را که سرور اعلام می‌کند امتحان می‌کند؛ اپلیکیشن همچنان باید تصمیم بگیرد که آیا سرور، امنیت اتصال، حساب و سازوکار قابل‌قبول‌اند. شکست‌های مکرر احراز هویت باید گروه ارسال را متوقف کنند و بررسی را آغاز کنند، نه اینکه به امتحان سریع رمز عبور منجر شوند.

از timeoutهای صریح و طول عمر محدود اتصال استفاده کنید

یک timeout محدود به SMTP یا SMTP_SSL بدهید تا اتصال و عملیات مسدودکننده نتوانند یک worker را بی‌پایان اشغال کنند. یک مهلت بیرونی برای job و سیاست لغو اعمال کنید، زیرا یک timeout سوکت کنترل کاملی بر سن صف نیست. یک شیء SMTP مشترک را میان وظایف هم‌زمان نگه ندارید، مگر اینکه دسترسی به آن سریالی باشد و ایمنی وضعیت آن ثابت شده باشد. یک طراحی ساده برای یک دسته محدود یک اتصال باز می‌کند، به سرور خوشامد می‌گوید، در صورت لزوم TLS برقرار می‌کند، احراز هویت می‌کند، تعداد کمی پیام ارسال می‌کند، quit را فراخوانی می‌کند و پس از خطا یا رسیدن به محدودیت سن، اتصال را کنار می‌گذارد. استفاده مجدد می‌تواند سربار را کاهش دهد، اما پس از قطع اتصال سرور، timeoutها یا وضعیت جزئی، ابهام را افزایش می‌دهد. تعداد پیام در هر اتصال را محدود کنید و آگاهانه دوباره متصل شوید. تأخیر اتصال، مذاکره TLS، احراز هویت، تأخیر فرمان‌ها، قطع اتصال سرور و سن job را بدون لاگ کردن اعتبارنامه‌ها یا محتوای پیام پایش کنید. سرور SMTP ممکن است محدودیت‌هایی اعمال کند که مستقل از Python تغییر می‌کنند.

یک پیام بفرستید و نتایج پذیرش جزئی گیرندگان را نگه دارید

SMTP.sendmail از from_addr و to_addrs برای envelope انتقال استفاده می‌کند و هدرهای پیام را بازنویسی نمی‌کند. SMTP.send_message یک EmailMessage را serialize می‌کند و اگر مقادیر envelope صریح داده نشود، پیش‌فرض‌ها را استخراج می‌کند. در کد محیط عملیاتی، فرستنده و فهرست گیرندگان envelope تأییدشده را صریحاً بدهید تا مدیریت Bcc و مجوز tenant بی‌ابهام بماند. Python مستند کرده است که sendmail وقتی دست‌کم یک گیرنده پذیرفته شود به‌طور عادی بازمی‌گردد و برای هر گیرنده ردشده یک دیکشنری برمی‌گرداند. بنابراین نبود استثنا به معنای موفقیت برای همه گیرندگان نیست. دامنه گیرندگان پذیرفته‌شده و ردشده را جداگانه، همراه با کد وضعیت و پیام عیب‌یابی پاک‌سازی‌شده، ذخیره کنید. وقتی فقط برخی گیرندگان رد شده‌اند، برای گیرندگان پذیرفته‌شده تلاش مجدد نکنید. هر گیرنده را نتیجه‌ای مستقلاً مجاز بدانید و در عین حال تلاش پیام مشترک را نگه دارید. استثنایی که بعداً در مرحله DATA رخ می‌دهد با رد شدن در RCPT فرق دارد و دسته‌بندی خاص خود را لازم دارد.

استثناها را بر اساس مرحله و دائمی بودن دسته‌بندی کنید

استثناهای smtplib را صریحاً مدیریت کنید و کدهای SMTP و پیام‌های پاک‌سازی‌شده سرور را نگه دارید. SMTPConnectError و timeout ممکن است گذرا باشند، اما ممکن است هاست، پورت یا فایروال اشتباه یا یک قطعی را نیز آشکار کنند. SMTPNotSupportedError پس از STARTTLS یا SMTPUTF8 باید پیکربندی‌ای را که به آن قابلیت نیاز دارد متوقف کند. SMTPAuthenticationError به بررسی اعتبارنامه، حساب، سازوکار و TLS نیاز دارد، نه تلاش مجدد کورکورانه. SMTPSenderRefused و SMTPRecipientsRefused به تصمیم‌هایی در سطح هویت یا گیرنده نیاز دارند. SMTPDataError یک پاسخ غیرمنتظره در DATA را توصیف می‌کند و بسته به وضعیت تکمیلی آن ممکن است نشان‌دهنده محتوا، سیاست، سهمیه یا رفتار موقت گیرنده باشد. پاسخ‌های 4xx را نامزد تلاش مجدد محدود و 5xx را برای آن تلاش دائمی بدانید و در عین حال مستندات مخصوص ارائه‌دهنده را رعایت کنید. از exponential backoff، jitter، سقف تعداد تلاش و سن صف و وضعیت dead-letter استفاده کنید. هرگز پس از توقف ارسال، گزارش اسپم، لغو اشتراک، ابطال مجوز یا شواهد گیرنده نامعتبر تلاش مجدد نکنید.

نتایج مبهم ارسال را تطبیق دهید

timeout شبکه یا قطع اتصال پس از اینکه کلاینت داده پیام را فرستاده اما پیش از مشاهده پاسخ نهایی سرور، مبهم است. ممکن است سرور مسئولیت را پذیرفته باشد، حتی اگر Python استثنا ایجاد کرده باشد. بی‌درنگ یک ارسال منطقی تازه نسازید. تلاش را نامعلوم علامت بزنید، شناسه‌های پایدار رویداد و ردگیری آن را نگه دارید و در صورت امکان لاگ‌های ارائه‌دهنده یا رویدادهای تحویل بعدی را جست‌وجو کنید. اگر سرویس SMTP هیچ idempotency یا همبستگی قابل‌جست‌وجو ارائه نمی‌دهد، یک تصمیم محصولی بر اساس دسته پیام، سن، آسیب تکرار و تجربه کاربر تعریف کنید. هشدارهای امنیتی و پیام‌های بازنشانی رمز عبور ریسک تکرار متفاوتی نسبت به رسیدها یا اطلاعیه‌های مالی دارند. تلاش اصلی و هر پیوند تلاش مجدد را در دفتر ثبت نگه دارید. هرگز ادعای تحویل دقیقاً یک‌باره نکنید، زیرا SMTP آن را به‌صورت سرتاسری فراهم نمی‌کند. این شاخه را با یک سرور آزمایشی کنترل‌شده که اتصال را در هر مرحله پروتکل، از جمله پیش و پس از پذیرش DATA، قطع می‌کند آزمایش کنید.

پذیرش SMTP را از تحویل و تعامل جدا کنید

فراخوانی موفق send_message یعنی طبق معنای مستند Python، دست‌کم یک گیرنده در مرحله مشاهده‌شده SMTP پذیرفته شده است. ثابت نمی‌کند همه گیرندگان پذیرفته شده‌اند، سرور مقصد بعداً پیام را نگه داشته، پیام به پوشه صندوق ورودی رسیده یا کسی آن را خوانده است. ارسال به ارائه‌دهنده، پذیرش توسط سرور گیرنده، خطای موقت یا دائمی، برگشت بعدی، گزارش اسپم، لغو اشتراک، قرار گرفتن در صندوق ایمیل و تعامل را شواهدی جداگانه مدل کنید. در صورت امکان رویدادهای احراز هویت‌شده ارائه‌دهنده را دریافت کنید، تکرارهایشان را حذف کنید و زمان وقوع را جدا از زمان پردازش نگه دارید. برگشت‌های دائمی، گزارش‌های اسپم و لغو اشتراک‌ها را درست پیش از ارسال‌های بعدی اعمال کنید. باز شدن‌ها و کلیک‌ها دلیل انتقال نیستند و ممکن است تحت تأثیر فناوری‌های حریم خصوصی قرار گیرند. متریک‌های تجمیعی با حداقل داده‌های شخصی را بر اساس گروه ایمن از نظر tenant، نسخه قالب، دامنه فرستنده، دسته وضعیت و زمان نگه دارید. برای جهش در رد شدن‌ها، نتایج نامعلوم، سن صف، خطاهای TLS، خطاهای احراز هویت و گسترش غیرعادی تعداد گیرندگان هشدار تنظیم کنید.

به‌صورت محلی و بدون ارسال ایمیل واقعی به مشتریان آزمایش کنید

ساخت پیام، رد injection هدر، مجوزدهی گیرنده، حذف Bcc، جایگزین‌های متن ساده و HTML، مدیریت Unicode، محدودیت پیوست و بررسی‌های توقف ارسال را unit-test کنید. از یک سرور آزمون SMTP محلی کنترل‌شده یا fixture پروتکل استفاده کنید تا خرابی greeting، نبود STARTTLS، خرابی گواهی، خطاهای احراز هویت، پذیرش جزئی RCPT، پاسخ‌های DATA ‏4xx و 5xx، قطع اتصال و پاسخ‌های تأخیری را شبیه‌سازی کنید. از سرویس‌های debugging احراز هویت‌نشده منسوخ برای secretهای شبیه محیط عملیاتی یا محتوای مشتری استفاده نکنید. آزمون‌های یکپارچه‌سازی باید از حساب‌های اختصاصی و گیرندگان کنترل‌شده، با سهمیه‌ها و پاک‌سازی صریح استفاده کنند. پیام خام دریافتی، نتایج احراز هویت، هدرهای قابل‌مشاهده، رفتار پاسخ و همبستگی رویداد را تأیید کنید. secret scanning را در fixtureها و logها اجرا کنید.

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

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

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

آیا اعتبارنامه‌های SMTP در Python باید در کد کلاینت قرار گیرند؟

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

Python چه زمانی باید از SMTP_SSL استفاده کند؟

وقتی TLS از ابتدای اتصال الزامی است از SMTP_SSL استفاده کنید. از SMTP همراه با starttls فقط برای گردش‌کار مستند ارتقای صریح استفاده کنید که در صورت خطا اتصال را قطع کند (fail closed).

آیا باید پس از starttls دوباره EHLO فراخوانی شود؟

بله. مستندات smtplib در Python می‌گوید پس از starttls دوباره ehlo را فراخوانی کنید تا قابلیت‌ها درون اتصال محافظت‌شده دوباره کشف شوند.

آیا send_message یعنی همه گیرندگان پذیرفته شده‌اند؟

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

پس از SMTPAuthenticationError چه باید کرد؟

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

آیا باید هر SMTPDataError را دوباره امتحان کرد؟

خیر. وضعیت و پیام عیب‌یابی دقیق را نگه دارید، سپس شرایط موقت 4xx را از خطاهای دائمی 5xx مربوط به سیاست، محتوا، سهمیه یا پیکربندی تمیز دهید.

آیا پذیرش SMTP رسیدن به صندوق ورودی را تعیین می‌کند؟

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

آیا این راهنما یکپارچه‌سازی ویژه SendHQ را پوشش می‌دهد؟

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

منابع