وبهوکهای ورودی
دریافت دادههای ایمیل ورودی پردازششده از طریق اعلانهای وبهوک
هنگامی که یک ایمیل ورودی با مسیری تطبیق داشته باشد که در actions آن webhook وجود دارد،
MailLand ایمیل پردازششده را بهصورت یک درخواست POST با فرمت JSON به وبهوک همان مسیر ارسال
میکند. این صفحه قالب بار، تأیید امضا و رفتار تحویل را پوشش میدهد.
بار وبهوک
MailLand یک درخواست POST با بدنهٔ JSON زیر به آدرس وبهوک شما ارسال میکند:
{
"event": "inbound.received",
"event_id": "evt_a1b2c3d4e5f60718293a4b5c6d7e8f90",
"timestamp": "2026-02-17T14:30:00Z",
"occurred_at": "2026-02-17T14:29:58Z",
"schema_version": 1,
"data": {
"idempotency_key": "k7m2x9q4p1w8z3_r7k3m9q2p4w8x1",
"email": {
"id": "k7m2x9q4p1w8z3",
"received_at": "2026-02-17T14:29:58Z",
"size_bytes": 48213,
"envelope": {
"from": "[email protected]",
"to": ["[email protected]"]
},
"headers": {
"from": { "address": "[email protected]", "name": "Jane Customer" },
"to": [{ "address": "[email protected]", "name": null }],
"cc": [],
"subject": "Need help with my account",
"message_id": "<[email protected]>",
"in_reply_to": null,
"reply_to": "[email protected]",
"date": "2026-02-17T14:29:55Z"
},
"body": {
"text_preview": "I need assistance with my account settings.",
"has_text": true,
"has_html": true
},
"attachments": [
{
"id": "a3k7m2q9p4w1x8",
"file_name": "screenshot.png",
"content_type": "image/png",
"size_bytes": 34500,
"is_inline": false,
"content_id": null,
"download_url": "https://r2.example.com/screenshot.png?X-Amz-Signature=..."
}
],
"authentication": {
"spf": { "result": "pass" },
"dkim": { "result": "pass" },
"dmarc": { "result": "pass" }
},
"spam": {
"score": 1.2,
"is_spam": false,
"reason": null
},
"source": {
"ip": "203.0.113.42",
"hostname": "mail.example.com"
}
},
"matched_route": {
"id": "r7k3m9q2p4w8x1",
"name": "Support Inbox"
}
}
}متن کامل ایمیل هرگز درون بار قرار داده نمیشود: فقط یک پیشنمایش کوتاهشده در text_preview و
پرچمهای has_text/has_html. هیچ فیلد body.html وجود ندارد.
فیلدهایی که مقدار ندارند (name، in_reply_to، content_id، reason، data.matched_route و مانند
آنها) بهجای ارسال بهصورت null، کاملاً از JSON حذف میشوند. نمونهٔ بالا برای خوانایی
null را نشان میدهد؛ روی حضور کلید حساب نکنید.
فیلدهای بار
| فیلد | نوع | توضیحات |
|---|---|---|
event | string | همیشه inbound.received |
event_id | string | شناسهٔ رویداد، برابر با هدر X-MLND-Event-Id. این شناسه برای هر پیام ورودی و هر وبهوک ثابت است (evt_ و پس از آن ۳۲ نویسهٔ هگزادسیمال)؛ هر تلاش مجدد و انتشار دوبارهٔ همان تحویل، event_id، هدر و بدنهٔ یکسانی دارد. بازپخش دستی event_id جدیدی دارد و data.replay_of را مقدار میدهد. |
data.idempotency_key | string | زمانی که مسیری تطبیق داشته باشد {emailPublicId}_{routePublicId}، وگرنه فقط {emailPublicId} (یا وقتی مسیر دیگر وجود ندارد). در تلاشهای مجدد و بازپخشهای دستی ثابت میماند |
data.replay_of | string | فقط در بازپخش دستی وجود دارد: event_id تحویل اصلی. در تحویلهای عادی و در بازپخشِ پیامهایی که پیش از ثابتشدن شناسهٔ رویداد دریافت شدهاند، ارسال نمیشود |
timestamp | string | زمان ISO 8601 لحظهٔ تولید رویداد |
occurred_at | string | زمان دریافت ایمیل؛ همان لحظهٔ data.email.received_at |
schema_version | number | نسخهٔ طرحوارهٔ محموله، 1؛ بخش نسخهبندی را ببینید |
data.email.id | string | شناسهٔ عمومی ایمیل ورودی |
data.email.received_at | string | زمان دریافت ایمیل |
data.email.size_bytes | number | اندازهٔ پیام خام به بایت |
data.email.envelope.from | string | آدرس MAIL FROM در SMTP |
data.email.envelope.to | array | آدرسهای RCPT TO در SMTP |
data.email.headers.from | object | { address, name } استخراجشده از سرصفحهٔ From |
data.email.headers.to / .cc | array | آرایهای از { address, name } |
data.email.headers.subject | string | موضوع ایمیل |
data.email.headers.message_id | string | null | سرصفحهٔ Message-ID |
data.email.headers.in_reply_to | string | null | سرصفحهٔ In-Reply-To |
data.email.headers.reply_to | string | null | سرصفحهٔ Reply-To |
data.email.headers.date | string | null | سرصفحهٔ Date |
data.email.body.text_preview | string | null | پیشنمایش متنی کوتاهشده |
data.email.body.has_text / .has_html | boolean | اینکه بخش متنی یا HTML وجود داشته یا نه |
data.email.attachments | array | به ادامه مراجعه کنید |
data.email.authentication.spf/dkim/dmarc.result | string | نتیجه با حروف کوچک، مانند pass، fail، none |
data.email.spam.score | number | امتیاز اسپم |
data.email.spam.is_spam | boolean | اینکه ایمیل اسپم تشخیص داده شده یا نه |
data.email.spam.reason | string | null | دلیل طبقهبندی، در صورت وجود |
data.email.source.ip / .hostname | string | null | آیپی و نام میزبان DNS معکوس سرور فرستنده |
data.matched_route | object | null | { id, name } مسیر تطبیقیافته، یا null |
پیوستها
محتوای پیوستها هرگز درون بار قرار داده نمیشود. هر آیتم در data.email.attachments یک download_url
دارد:
| فیلد | نوع | توضیحات |
|---|---|---|
id | string | شناسهٔ پیوست. ثابت است: هر تلاش مجدد و هر بازپخش رویداد برای همان پیوست همان id را دارد؛ با آن پیوستی را که قبلاً دانلود کردهاید تشخیص دهید. download_url در هر تلاش از نو ساخته میشود |
file_name | string | نام فایل اصلی |
content_type | string | نوع MIME |
size_bytes | number | اندازه به بایت |
is_inline | boolean | اینکه پیوست بهصورت درونخطی ارجاع داده شده (مثلاً یک تصویر جاسازیشده) یا نه |
content_id | string | null | Content-ID برای پیوستهای درونخطی |
download_url | string | null | نشانی از پیش امضاشده، معتبر برای ۱۵ دقیقه از زمان تولید بار |
download_url نیازی به سرصفحهٔ Authorization ندارد: خود نشانی، اعتبارنامه است و ۱۵ دقیقه پس از
تولید بار وبهوک منقضی میشود. بلافاصله پس از دریافت وبهوک، پیوستها را دانلود کنید. اگر نشانی
منقضی شده، بهجای آن کل پیام خام را دریافت کنید:
GET https://api.mail.land/v1/transactional/inbound/emails/{emailId}/rawاین کار به سرصفحهٔ معمول Authorization شما نیاز دارد و فایل اصلی .eml را بازمیگرداند
(Content-Type: message/rfc822): هیچ نقطهٔ پایانی برای دانلود تکتک پیوستها وجود ندارد.
تأیید امضا
وبهوکهای ورودی دقیقاً مانند هر وبهوک دیگر در MailLand امضا میشوند: همان سرصفحه، همان طرح، همان مدیریت راز. مرجع اصلی، همراه با یک مثال کامل و کد تأیید به هشت زبان، صفحهٔ Signature Verification است؛ همهٔ آنچه در آنجا آمده، بدون تغییر اینجا نیز صدق میکند.
خلاصه:
X-MLND-Signature: t=1708171200,v1=<hex>tیک زمان یونیکس بر حسب ثانیه است وv1یک HMAC-SHA256 بهصورت هگزادسیمال با حروف کوچک است؛- رشتهٔ امضاشده بهصورت
{timestamp}.{raw_body}است: زمان، سپس یک نقطهٔ واقعی، و سپس بدنه دقیقاً همانطور که دریافت شده، پیش از تجزیهٔ JSON؛ - کلید، رازِ امضای وبهوک است که فقط یکبار هنگام ایجاد نمایش داده میشود؛
- مقایسه را در زمان ثابت انجام دهید و زمانی قدیمیتر از ۳۰۰ ثانیه را رد کنید.
هیچ مسیر تحویل بدون امضایی وجود ندارد: اگر وبهوکی رازِ قابلاستفاده نداشته باشد، MailLand بهجای
ارسال بدون امضا، تحویل را ناموفق میکند. هر درخواستی که بدون سرصفحهٔ X-MLND-Signature برسد را رد
کنید.
رفتار تحویل
کدهای پاسخ
نقطهٔ پایانی شما باید با یک کد وضعیت 2xx برای تأیید دریافت پاسخ دهد. هر چیز دیگری، وضعیتی غیر
از 2xx یا اتمام مهلت، بهعنوان یک تلاش تحویل ناموفق شمرده میشود.
| پاسخ شما | اقدام MailLand |
|---|---|
200–299 | تحویل موفق، بدون تلاش مجدد |
| هر وضعیت دیگر، یا نبود پاسخ در ۱۵ ثانیه | تحویل ناموفق، تلاش مجدد میشود (مشروط به سقف تلاش زیر) |
سیاست تلاش مجدد
یک تلاش ناموفق با همان زمانبندی سایر رویدادهای وبهوک دوباره انجام میشود، تا سقف ۶ تلاش در مجموع برای هر رویداد: تلاش اول، سپس تلاش مجدد بلافاصله، بعد از ۱ دقیقه، ۵ دقیقه، ۱۵ دقیقه و ۱ ساعت. پس از ششمین تلاش ناموفق، تحویل بهعنوان ناموفق دائمی علامتگذاری میشود. میتوانید آن را بهصورت دستی بازپخش کنید. برای زمانبندی کامل به منطق تلاش مجدد نگاه کنید.
نقطهٔ پایانیای که کمتر از حدود یک ساعت و نیم از دسترس خارج باشد در کل همین بازه دوباره امتحان میشود، اما قطعی طولانیتر به تحویل ناموفق دائمی ختم میشود. نقطهٔ پایانی خود را طوری طراحی کنید که حتی در حالت خطا هم سریع پاسخ دهد و برای قطعیهای طولانیتر به بازپخش دستی تکیه کنید.
هر تلاش تحویل ناموفق (نه هر رویداد) در مجموع شکستهای متوالی وبهوک محاسبه میشود، و وبهوک
بهمحض رسیدن این مجموع به pauseThreshold خودش (وقتی روی مقداری بزرگتر از ۰ تنظیم شده باشد)، وگرنه
مقدار پیشفرض یعنی ده شکست پیاپی، بهطور خودکار غیرفعال میشود: پس از آن، ایمیلهای ورودی جدید هیچ
تحویل وبهوکی برای آن در صف قرار نمیدهند، و چون همان ردیف وبهوک برای رویدادهای پیام هم استفاده
میشود، آنها هم متوقف میشوند. با مقدار پیشفرض، دو ایمیل ورودی که کاملاً شکست بخورند (هرکدام ۶
تلاش) برای غیرفعالشدن کافی است. وبهوک را در صفحهٔ Webhooks دوباره فعال کنید، سپس ایمیلهای ازدسترفته را
بازپخش کنید.
بازپخش یک وبهوک
تحویل مجدد وبهوک برای یک ایمیل ورودی مشخص:
POST https://api.mail.land/v1/transactional/inbound/emails/{id}/replay-webhookcurl -X POST https://api.mail.land/v1/transactional/inbound/emails/k7m2x9q4p1w8z3/replay-webhook \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"emailId": "k7m2x9q4p1w8z3",
"queued": true,
"webhookUrl": "https://yourapp.com/webhooks/support",
"message": "Webhook replay queued successfully"
}این درخواست یک تحویل پایدار را به وبهوک مسیر منطبق در صف قرار میدهد و پس از ثبتشدن همان سطر،
202 Accepted برمیگرداند؛ ارسال بهصورت همزمان انجام نمیشود. بازپخش یک شناسهٔ رویداد جدید میگیرد
(event_id و X-MLND-Event-Id تازه) و شناسهٔ تحویل اصلی را در data.replay_of همراه دارد؛ بنابراین حذف تکراریها در
سمت شما آن را رویدادی جدید میبیند، ولی همچنان میتوانید آن را به رویداد اصلی ربط دهید. data.idempotency_key
همان مقدار تحویل اصلی باقی میماند. درخواست هدرهای X-MLND-Source: inbound و
X-MLND-Inbound-Email-Id را دارد و هر download_url پیوست در محموله در لحظهٔ ارسال امضا میشود؛ اعتبار آن
۱۵ دقیقه از همان لحظه است، نه از لحظهٔ فراخوانی بازپخش.
حذف رویدادهای تکراری
تحویل وبهوک ورودی حداقل یکبار (at-least-once) انجام میشود: تلاش مجدد، تایماوت در سمت شما پس از پردازش درخواست، یا جابهجایی کارگر میتواند یک رویداد را بیش از یک بار تحویل دهد. گیرنده باید تکراریها را حذف کند.
- هر وبهوک ورودی یک شناسهٔ رویداد ثابت دارد که در
event_idو هدرX-MLND-Event-Idمیآید. این شناسه برای هر پیام ورودی و هر وبهوک قطعی است؛ پس همهٔ تلاشهای تحویل یک پیام به یک وبهوک شناسهٔ یکسان دارند. - بر اساس همین شناسه تکراریها را حذف کنید. شناسههای پردازششده را نگه دارید و شناسهای را که قبلاً دیدهاید رد کنید.
- بازپخش دستی یک رویداد جدید است.
event_idتازه دارد و فیلدdata.replay_ofشناسهٔ اصلی را نگه میدارد. اگر میخواهید بازپخشِ رویدادی که قبلاً پردازش شده نادیده گرفته شود،data.replay_ofرا هم با شناسههای ذخیرهشدهتان مقایسه کنید.
app.post("/webhooks/support", express.raw({ type: "application/json" }), async (req, res) => {
// 1. Verify X-MLND-Signature first (see Signature Verification).
const eventId = req.header("X-MLND-Event-Id");
const client = await db.connect();
try {
await client.query("BEGIN");
// 2. Record the event id; the unique constraint makes this atomic.
const inserted = await client.query(
"INSERT INTO processed_events (event_id) VALUES ($1) ON CONFLICT (event_id) DO NOTHING",
[eventId],
);
// 3. Only a first-time id enqueues work, in the SAME transaction (jobs table).
if (inserted.rowCount > 0) {
await client.query("INSERT INTO jobs (kind, payload) VALUES ($1, $2)", [
"process-inbound-email",
req.body.toString(),
]);
}
await client.query("COMMIT");
res.sendStatus(200); // new or duplicate: acknowledge
} catch (err) {
await client.query("ROLLBACK"); // id and job are both undone, so the retry is not a "duplicate"
res.sendStatus(500);
} finally {
client.release();
}
});ثبت شناسه و صفگذاری کار در یک تراکنش اهمیت دارد: اگر شناسه اول ذخیره شود و صفگذاری شکست بخورد، تلاش مجدد MailLand بهعنوان تکراری رد میشود و ایمیل از دست میرود.
شناسههای پردازششده را دستکم بهاندازهٔ طولانیترین بازهٔ تلاش مجدد، بهعلاوهٔ هر بازپخشی که ممکن است خودتان بزنید، نگه دارید.
سرصفحههای وبهوک
هر درخواست وبهوک شامل سرصفحههای زیر است:
| سرصفحه | نمونه | توضیحات |
|---|---|---|
Content-Type | application/json | |
X-MLND-Signature | t=1708171200,v1=abc123... | زمان و امضای HMAC-SHA256 |
X-MLND-Event | inbound.received | نوع رویداد |
X-MLND-Event-Id | evt_a1b2c3d4... | شناسهٔ یکتای رویداد |
X-MLND-Event-Version | 1 | نسخهٔ شمای بار |
X-MLND-Webhook-Id | w4k8m2q7p9x3z1 | شناسهٔ عمومی وبهوک |
X-MLND-Attempt | 1 | شمارهٔ تلاش تحویل (از ۱ شروع میشود) |
X-MLND-Source | inbound | خطی که رویداد را تولید کرده |
X-MLND-Inbound-Email-Id | k7m2x9q4p1w8z3 | ایمیل ورودی مربوط به این تحویل |
بهترین شیوهها
- سریع پاسخ دهید: وبهوک را بلافاصله با
200تأیید کنید و ایمیل را بهصورت ناهمزمان در یک کار پسزمینه پردازش کنید. - امضاها را تأیید کنید: همیشه سرصفحهٔ
X-MLND-Signatureرا برای جلوگیری از بارهای جعلی اعتبارسنجی کنید. - موارد تکراری را مدیریت کنید: تحویل حداقل یکبار است؛ پس بر اساس
X-MLND-Event-Id(همانevent_idمحموله) تکراریها را حذف کنید. بازپخش دستی شناسهٔ جدید دارد وdata.replay_ofآن شناسهٔ اصلی است؛ بخش حذف رویدادهای تکراری را ببینید. - پیوستها را فوراً دانلود کنید: هر
download_url۱۵ دقیقه پس از تولید بار منقضی میشود؛ پس از آن پیام خام را دریافت کنید. - از HTTPS استفاده کنید: آدرسهای وبهوک باید در محیط تولید از HTTPS برای تحویل امن بار استفاده کنند.
پاسخهای خطا
| وضعیت | کد | توضیحات |
|---|---|---|
400 | INVALID_REQUEST | هیچ مسیری برای این ایمیل منطبق نشد |
400 | INVALID_REQUEST | مسیر وبهوکی پیکربندیشده ندارد |
400 | INVALID_REQUEST | وبهوک مسیر غیرفعال است؛ پیش از بازپخش آن را فعال کنید |
404 | RESOURCE_NOT_FOUND | ایمیل موردنظر برای بازپخش یافت نشد |
گامهای بعدی
- Inbound Overview: پیکربندی تنظیمات ورودی و مشاهدهٔ ایمیلها
- Inbound Routes: تنظیم قوانین مسیریابی برای تحویل وبهوک
- Webhooks: پیکربندی وبهوکهای خروجی برای رویدادهای ایمیل