ایمیل ورودی · 22 سپتامبر 2026
دریافت ایمیل با Amazon SES: با S3 و Lambda
یک pipeline ایمیل ورودی آماده محیط عملیاتی با Amazon SES، S3 و Lambda بسازید؛ شامل ایمنی MIME، مسیریابی بر اساس tenant، idempotency، تلاش مجدد و رشته گفتگو.
کوتاهترین مسیر قابلاعتماد برای ایمیل ورودی در Amazon SES این است: دامنه را تأیید کنید، یک رکورد MX را به endpoint دریافت SES اشاره دهید، هر پیام پذیرفتهشده را در S3 ذخیره کنید و سپس Lambda را بهصورت ناهمگام فراخوانی کنید تا پیام را parse و ذخیره کند. اقدام S3 را پیش از اقدام Lambda قرار دهید. شناسه پیام SES را کلید idempotency در نظر بگیرید، برای مسیریابی از گیرنده envelope در SMTP استفاده کنید و بهجای اعتماد به هدرها یا پیوستها، محتوای ناامن را قرنطینه کنید.
معماریای که باید بسازید
از یک زیردامنه اختصاصی مانند inbound.example.com استفاده کنید، مگر اینکه SES قرار باشد همه ایمیلهای دامنه اصلی شما را دریافت کند. این کار ایمیل برنامه را از صندوقهای ورودی کارکنان جدا نگه میدارد و بازگشت به وضعیت قبل را به یک تغییر DNS تبدیل میکند، نه یک مهاجرت ایمیل.
جریان محیط عملیاتی به این شکل است:
sender
-> SES inbound SMTP endpoint
-> active SES receipt rule
-> S3 raw-message object
-> asynchronous Lambda action
-> MIME parser and policy checks
-> application database and private attachment storage
قوانین دریافت (receipt rule) در Amazon SES اقدامات خود را به ترتیب اجرا میکنند. AWS بهطور مشخص الگوی «اول S3، بعد Lambda» را برای زمانی که کد به بدنه پیام نیاز دارد مستند کرده است. اقدام مستقیم Lambda فقط metadata و برخی هدرها را دریافت میکند، نه بدنه کامل را. بدنه بهصورت شیء MIME خام در S3 باقی میماند (مفاهیم دریافت در AWS SES).
این جداسازی مفید است. مرحلهای که با SMTP سروکار دارد پیام اصلی را سریع ذخیره میکند، در حالی که parse کردن، نمایهسازی، اعلانها و منطق کسبوکار پس از پذیرش انجام میشوند. یک خطای موقت پایگاه داده نباید سرور ایمیل فرستنده را مجبور به تکرار تراکنش SMTP کند.
1. یک region پشتیبانیشده انتخاب و دامنه را تأیید کنید
دریافت ایمیل در SES فقط در برخی regionهای AWS در دسترس است. یکی را از فهرست فعلی endpointهای دریافت SES انتخاب کنید و منابع SES، Lambda، SNS و KMS را در همان region نگه دارید، مگر اینکه مستندات مربوط AWS صراحتاً حالت دیگری را مجاز بداند.
برای دقیقاً همان دامنه اصلی یا زیردامنهای که ایمیل دریافت خواهد کرد، یک domain identity در SES بسازید. تأیید دامنه مستلزم انتشار رکوردهای DNS است که SES ارائه میدهد. تأیید برای دریافت، کنترل شما بر دامنه را ثابت میکند؛ این کار از پیکربندی مسیر MX که ترافیک ورودی را به SES میفرستد جداست (راهنمای تأیید دامنه در AWS).
برای یک زیردامنه اختصاصی، رکوردهای DNS از نظر مفهومی چنین هستند:
inbound.example.com. MX 10 inbound-smtp.us-east-1.amazonaws.com.
us-east-1 را با region انتخابی خود جایگزین کنید. AWS مقدار MX را 10 inbound-smtp.<region>.amazonaws.com مستند کرده است (راهنمای رکورد MX در AWS). اگر افراد هنوز از طریق Google Workspace، Microsoft 365 یا ارائهدهنده صندوق ایمیل دیگری روی دامنه اصلی شما ایمیل دریافت میکنند، آن را به SES اشاره ندهید.
پیش از آزمایش، رکورد منتشرشده را از بیش از یک resolver بررسی کنید:
dig MX inbound.example.com +short
قابلمشاهده بودن در DNS فقط ثابت میکند که مسیر منتشر شده است. پیش از اینکه راهاندازی را کامل بدانید، یک پیام کنترلشده به یک نشانی آزمایشی بفرستید و مطمئن شوید SES آن را ذخیره کرده است.
2. پیام خام را پیش از پردازش ذخیره کنید
یک bucket خصوصی در S3 با دسترسی عمومی مسدود، یک lifecycle policy و محدودترین مجوزهای IAM عملی بسازید. سپس یک قانون دریافت SES بسازید که شرط گیرنده آن با دامنه ورودی یا نشانیهای مشخص شما مطابقت داشته باشد.
اقدام اول باید پیام خام را به S3 تحویل دهد. یک پیشوند شیء مانند inbound/ محدود کردن قوانین نگهداری و سیاستهای دسترسی را آسانتر میکند. SES محتوای MIME خام و دستنخورده را ذخیره میکند. AWS در حال حاضر حداکثر پیشفرض 40 MB را برای ذخیره پیام در S3 مستند کرده است، در حالی که اقدام SNS که پیام کامل را شامل میشود حداکثر بسیار کمتری معادل 150 KB دارد (اقدام دریافت S3 در AWS). همین تفاوت اندازه دلیل آن است که S3 پیشفرض امنتری برای پاسخها و پیوستهای دنیای واقعی است.
اگر تنظیم اختیاری KMS در SES را روی اقدام دریافت فعال میکنید، جزئیات رمزنگاری را با دقت بخوانید. SES برای این قابلیت از رمزنگاری سمت کلاینت استفاده میکند، نه رمزنگاری معمول سمت سرور S3؛ بنابراین خواننده شما باید شیء را با یک کلاینت سازگار رمزگشایی کند. آن را سرسری فعال نکنید تا مبادا در میانه یک رخداد متوجه شوید parser شما در Node نمیتواند بایتهای ذخیرهشده را بخواند.
به SES فقط اجازه نوشتن در bucket و پیشوند موردنظر را بدهید. به Lambda فقط برای همان مکان مجوز s3:GetObject بدهید. این تابع به مجوزهای مدیریت bucket نیازی ندارد.
3. یک اقدام Lambda ناهمگام اضافه کنید
اقدام Lambda را پس از اقدام S3 در قانون دریافت قرار دهید و از فراخوانی ناهمگام استفاده کنید، مگر اینکه تابع باید تصمیم بگیرد SES ارزیابی قانون را ادامه دهد یا نه. AWS اجرای ناهمگام را برای پردازش عادی توصیه میکند و اجرای همگام را به تصمیمگیری درباره جریان ایمیل اختصاص میدهد (اقدام دریافت Lambda در AWS).
وقتی پیشوندی پیکربندی نشده باشد، mail.messageId که SES اختصاص میدهد همان کلید شیء در S3 است. اگر پیشوند دارید، آن را به ابتدای شناسه اضافه کنید. اسکلت Node.js زیر پیام خام را دریافت و parse میکند. @aws-sdk/client-s3 و یک parser نگهداریشده MIME مانند mailparser را همراه artifact استقرار بستهبندی کنید و نسخههای آنها را ثابت کنید.
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { simpleParser } from "mailparser";
const s3 = new S3Client({});
const bucket = process.env.INBOUND_BUCKET;
const prefix = process.env.INBOUND_PREFIX || "inbound/";
export async function handler(event) {
for (const record of event.Records || []) {
const ses = record.ses;
const messageId = ses?.mail?.messageId;
const recipients = ses?.receipt?.recipients || [];
if (!messageId || !/^[A-Za-z0-9._-]+$/.test(messageId)) {
throw new Error("Missing or invalid SES message ID");
}
// claimOnce must be an atomic insert with a unique constraint.
if (!(await claimOnce(messageId))) continue;
try {
const object = await s3.send(new GetObjectCommand({
Bucket: bucket,
Key: `${prefix}${messageId}`,
}));
const raw = Buffer.from(await object.Body.transformToByteArray());
const parsed = await simpleParser(raw, {
skipHtmlToText: true,
skipTextToHtml: true,
});
await saveInboundMessage({
providerMessageId: messageId,
envelopeRecipients: recipients,
envelopeFrom: ses.mail.source,
headerMessageId: parsed.messageId || null,
inReplyTo: parsed.inReplyTo || null,
references: parsed.references || [],
subject: parsed.subject || "",
text: parsed.text || "",
html: parsed.html || null,
attachments: parsed.attachments,
receivedAt: ses.mail.timestamp,
});
await markComplete(messageId);
} catch (error) {
await releaseOrMarkFailed(messageId, String(error));
throw error;
}
}
}
توابع placeholder نماینده ذخیرهسازی خاص برنامهاند، اما قرارداد آنها مهم است. claimOnce باید از قید یکتایی پایگاه داده یا نوشتن شرطی روی شناسه پیام ارائهدهنده استفاده کند. خواندن و سپس درج، دچار شرایط رقابتی میشود. وضعیت پردازش را ذخیره کنید تا اپراتور بتواند میان processing، complete، quarantined و failed تمایز قائل شود.
مسیریابی را با گیرنده envelope انجام دهید، نه هدر To
فیلدهای قابلمشاهده To و Cc محتوای پیاماند که فرستنده تعیین میکند. این فیلدها ممکن است بهدلیل BCC، forward یا دستکاری عمدی، مقصد واقعی را نشان ندهند. شرطهای دریافت SES از گیرندگان envelope در SMTP استفاده میکنند و AWS به پردازشگرهای پاییندستی توصیه میکند برای تعیین مقصد تحویل پیام، از گیرندگان موجود در اعلان SES استفاده کنند (مفاهیم دریافت در AWS).
این تمایز از یک باگ میان tenantها جلوگیری میکند. اگر reply+tenant-a@inbound.example.com پیامی دریافت کند که To قابلمشاهده آن tenant-b@example.com است، آن را با نگاشت احرازشده برنامه برای نشانی envelope مسیریابی کنید، هرگز با هدر نمایشی.
وقتی نشانیای یک مشتری یا گفتگو را مشخص میکند، از یک توکن پاسخ تصادفی و غیرقابلحدس استفاده کنید. توکن را در حالت ذخیره هش کنید، در زمان مناسب منقضی کنید و نشانیهایی را که به یک فضای کاری فعال نگاشت نمیشوند رد کنید. بخش محلی قابلپیشبینی مانند ticket-42 دعوتی است برای تزریق پیام به رشته گفتگوی کاربر دیگر.
MIME را ورودی خصمانه در نظر بگیرید
ایمیل یک قالب ورودی تودرتو و چنددههای است. RFC 5322 هدرها و بدنه پیام را تعریف میکند و MIME محتوای multipart و کدگذاریهای انتقال را اضافه میکند (RFC 5322، RFC 2045). بهجای اینکه خودتان پیام را بر اساس خطوط خالی یا boundaryها تقسیم کنید، از یک parser نگهداریشده استفاده کنید.
پیش از در دسترس قرار دادن محتوا برای محصول، محدودیتها را اعمال کنید:
- برای کل بایتهای رمزگشاییشده، تعداد پیوستها، اندازه هر پیوست، عمق تودرتویی MIME و زمان parse سقف تعیین کنید.
- پیوستها را بهصورت خصوصی و با نامهای شیء تولیدشده ذخیره کنید. هرگز از نام فایل فرستنده بهعنوان مسیر استفاده نکنید.
- نوع محتوا و نام فایل اعلامشده را فقط سرنخ بدانید. در صورت امکان، نوع را از روی محتوا تشخیص دهید.
- هرگز محتوای پیوست را اجرا نکنید. پیوستها را پیش از دانلود اسکن یا قرنطینه کنید.
- HTML را با یک allowlist سختگیرانه پاکسازی کنید، تصاویر راه دور را بهطور پیشفرض مسدود کنید و آن را در یک بستر ایزوله رندر کنید. برای تحلیل خودکار، متن ساده را ترجیح دهید.
- بدنههای خام، نشانیها، توکنها یا محتوای پیوستها را در لاگهای معمولی برنامه قرار ندهید.
SES میتواند نتایج SPF، DKIM، DMARC، اسپم و ویروس را گزارش دهد، اما AWS یادآوری میکند که SES این نتایج را فقط در اختیار شما میگذارد و سیاست کسبوکار شما را خودکار اعمال نمیکند. تصمیم بگیرید موارد ناموفق رد شوند، قرنطینه شوند یا با هشدار نمایش داده شوند. موفقیت در احراز هویت، یک دامنه را تحت سازوکاری مشخص شناسایی میکند؛ محتوا را ایمن نمیکند و ثابت نمیکند که انسانی آن را نوشته است.
تلاشهای مجدد را بیدردسر کنید
فراخوانی ناهمگام Lambda میتواند توابع ناموفق را دوباره اجرا کند و AWS هشدار میدهد که تحویل تکراری حتی زمانی که تابع خطایی برنمیگرداند هم ممکن است. یک مقصد on-failure یا dead-letter queue پیکربندی کنید و برای خطاهای پردازش هشدار تنظیم کنید (رفتار تلاش مجدد در AWS Lambda).
Idempotency باید همه اثرات جانبی پاییندستی را پوشش دهد:
- شناسه پیام SES را با یک قید یکتایی درج کنید.
- در صورت امکان، محتوای parseشده و پیوندهای رشته گفتگو را در یک تراکنش ذخیره کنید.
- اعلانها، ایجاد تیکت یا کار ایجنت را در یک outbox با کلید «شناسه پیام بهعلاوه نوع اقدام» قرار دهید.
- رکورد را فقط پس از موفقیت نوشتنهای ماندگار، کامل علامت بزنید.
- پردازش مجدد را از شیء اصلی S3 انجام دهید، نه از یک ورودی لاگ ناقص.
اگر پردازش را بهجای اقدام Lambda در SES از طریق اعلانهای S3 فعال میکنید، همین قاعده صدق میکند. اعلانهای Amazon S3 برای تحویل حداقل یکباره طراحی شدهاند و رسیدن آنها به ترتیب تضمین نمیشود (اعلانهای رویداد S3 در AWS).
رشته گفتگو را بدون اعتماد به موضوع ایمیل بسازید
از فیلدهای parseشده Message-ID، In-Reply-To و References برای پیشنهاد تطبیق رشته گفتگو استفاده کنید. رشته گفتگو را صرفاً بر اساس موضوعی که با Re: شروع میشود نسازید. همچنین پیش از پیوند دادن هر چیزی، مطمئن شوید نشانی envelope یا توکن پاسخ به همان فضای کاری و گفتگو تعلق دارد.
پاسخهای خودکار به سیاست جداگانهای نیاز دارند. سیگنالهایی مانند Auto-Submitted را تشخیص دهید و از ایجاد حلقههای پاسخ پرهیز کنید. RFC 3834 شناسایی روشن و رفتار محافظهکارانه را برای پاسخهای خودکار توصیه میکند (RFC 3834). اگر یک ایجنت هوش مصنوعی پاسخی را پیشنویس میکند، ارسال را بهصورت یک اثر جانبی صریح و idempotent نگه دارید. برای گیرندگان غیرمنتظره، محتوای حساس یا اقداماتی خارج از گردشکار اصلی پشتیبانی یا محصول، تأیید کاربر را الزامی کنید. دریافت یک پیام به معنای رضایت کلی برای بازاریابی نامرتبط نیست.
چکلیست محیط عملیاتی
- region دریافت از ایمیل ورودی SES پشتیبانی میکند.
- هویت دامنه تأیید شده و رکورد MX بهدرستی resolve میشود.
- قانون receipt شرط گیرنده محدودی دارد و rule set موردنظر فعال است.
- عمل S3 پیش از پردازش ناهمگام Lambda اجرا میشود.
- Bucket خصوصی است، دسترسی بر اساس حداقل سطح دسترسی است و نگهداری مستند شده است.
- برای شناسه پیام SES یک محدودیت یکتای پایگاه داده وجود دارد.
- مسیریابی از گیرندگان envelope استفاده میکند، نه هدرهای قابلمشاهده
ToیاCc. - با MIME، HTML، لینکها و پیوستها بهعنوان ورودی غیرقابلاعتماد رفتار میشود.
- رویدادهای ناموفق به یک مقصد تحت پایش میرسند و قابل replay هستند.
- تطبیق رشتههای گفتگو، مالکیت فضای کاری را اعمال میکند.
- پاسخهای خودکار دارای جلوگیری از loop، مرزهای رضایت و idempotency ارسال هستند.
- یک آزمون کنترلشده متن ساده، HTML، BCC، تحویل تکراری، پیوستهای بزرگ، MIME بدشکل و خرابی parser را پوشش میدهد.
استفاده مستقیم از SES زمانی مناسب است که تیم شما کنترل بومی AWS را میخواهد و آماده است مسئولیت DNS، IAM، parse کردن MIME، جداسازی tenantها، نگهداری، مدیریت تلاش مجدد و هشدارهای عملیاتی را بر عهده بگیرد. اگر میخواهید این اجزای پایه برنامه پشت یک API ایمیل محدودتر قرار بگیرند، SendHQ نشانیهای ورودی، پیامهای نگهداریشده، رشتههای گفتگو و دسترسی در سطح فضای کاری را در کنار ایمیل تراکنشی خروجی ارائه میدهد. در هر حال، پیام خام را قابل بازیابی نگه دارید و هر اقدام پاییندستی را در برابر بازپخش ایمن کنید.