ثلاثة استدعاءات، ولا شيء يُجهَّز
الواجهة كلها ثلاث نقاط طرفية تحت https://grabmail.io/api/v1، بالإضافة إلى عنوان واحد للمرفقات تسلّمه لك الاستدعاءات الأخرى جاهزًا. لا يوجد استدعاء لإنشاء صندوق بريد، وغيابه ليس إغفالاً: يبدأ وجود العنوان لحظة وصول بريد إليه، فلا شيء يبقى لمثل هذا الاستدعاء ليفعله.
| الاستدعاء | بمَ يُجيب | ما تُمرّره |
|---|---|---|
GET /mailbox | كل ما ينتظر عند عنوان، الأحدث أولاً. | address، وlimit وbefore اختياريًا |
GET /message/{id} | رسالة واحدة كاملة: الجزء النصي، وجزء HTML، وكل مرفق برابط جاهز مسبقًا. | mailbox |
DELETE /message/{id} | يزيلها الآن بدل انتظار انتهاء مدة الاحتفاظ. | mailbox |
GET /attachment/{id} | بايتات ملف واحد، تمامًا كما وصلت. | mailbox |
كل استجابة هي JSON، بما في ذلك كل خطأ. وكل وقت بتوقيت UTC بصيغة RFC 3339. معرّفات الرسائل معتمة: أعدها كما تلقّيتها، ولا تحاول تفكيكها أبدًا.
الاستدعاء الأول، وبم يُجيب عنوان فارغ
اختر اسمًا، وضع أحد النطاقات العامة خلفه، ثم اقرأه. لا شيء يحتاج إلى الوجود مسبقًا، ولا شيء يُنشأ بمجرد السؤال.
$ curl -sG https://grabmail.io/api/v1/mailbox \
--data-urlencode "address=k7fq2m@grabmail.io"{
"address": "k7fq2m@grabmail.io",
"alias": "q4v8n2mt7xkd@example.net",
"count": 1,
"next": null,
"messages": [
{
"id": "3QK7ZB5M9WVXR2HD4TNFJ0PC6A",
"from": "no-reply@example.com",
"from_name": "Example",
"subject": "Your verification code",
"preview": "Your code is 481920. It expires in 10 minutes.",
"has_html": false,
"date": "2026-08-29T09:14:02Z",
"seen": false,
"attachments": 0,
"expires_at": "2026-09-03T09:14:02Z"
}
]
}خمسة حقول، اثنان منها أهم مما يبدوان:
count- عدد الرسائل في هذه الإجابة — لا عدد ما يحمله الصندوق كله. فبمجرد أن تمرّر
limit، يصبح هذان رقمين مختلفين. next- مؤشر الصفحة التي تلي هذه، أو
nullحين لا يوجد شيء بعدها. إنه معرّف آخر رسالة أُعطيتها للتو، ولهذا لا يكلّف التصفح استدعاءً إضافيًا لاكتشافه. messages- القائمة نفسها، الأحدث أولاً. كل عنصر فيها يحمل مسبقًا
subjectوfromوdateوseen، وpreviewقصيرًا للنص، وما إذا كان هناك جزء HTML، وعدد المرفقات. alias- عنوان ثانٍ يوصل الرسائل إلى هنا ولا يكشف شيئًا عن هذا العنوان. أعطه لنموذج بدل العنوان الحقيقي؛ فمن ينتهي به الأمر يكتبه في هذه الخدمة يجد صندوق بريد فارغًا.
address- العنوان كما فُهم، بأحرف صغيرة وبلا فراغات زائدة. قارنه بما أرسلته إن كنت تبني العنوان من أجزاء.
القراءة بعد الخمسين الأولى
يُجيب الاستدعاء الواحد بخمسين رسالة كحد افتراضي، ومئتين عند أقصى الحدود. وعنوان جامع نشط يتجاوز الاثنين في عصر واحد، والجزء الذي يخطئ القراء فهمه هو ما يأتي بعده — لأنه ليس رقم صفحة.
limit- عدد ما يُعاد في هذا الاستدعاء، من 1 إلى 200. القيم الخارجة عن النطاق تُضبط عند أقرب حد بدل أن تُرفض، فـ
limit=5000تمنحك 200 بصمت. before- معرّف أقدم رسالة لديك بالفعل. تحصل على ما يليها. مرّر ما وضعته الإجابة السابقة في
next. nextnullيعني أنك بلغت نهاية الصندوق. إنها الإشارة الموثوقة الوحيدة على نهاية القائمة: فالصفحة القصيرة ليست كذلك، لأن الصفحة لا تقصر إلا حين يقرر الخادم ذلك.
limit يحدّ سقف إجابة واحدة، وnext يسمّي حيث توقفت تلك الإجابة، وbefore يطلب ما يقع بعدها.ADDR="k7fq2m@grabmail.io"
CURSOR=""
while :; do
PAGE=$(curl -fsG https://grabmail.io/api/v1/mailbox \
--data-urlencode "address=$ADDR" \
--data-urlencode "limit=200" \
${CURSOR:+--data-urlencode "before=$CURSOR"})
printf '%s' "$PAGE" | jq -c '.messages[]'
CURSOR=$(printf '%s' "$PAGE" | jq -r '.next // empty')
[ -n "$CURSOR" ] || break
sleep 1
doneكرّر طالما أن next ليست null، وستحصل على الصندوق كاملاً، مهما كبر حجمه. كل استدعاء هو قراءة نطاق فوق فهرس لا إزاحة رقمية، فتكلفة الصفحة الألف هي تكلفة الصفحة الأولى نفسها.
مؤشر من صندوق بريد آخر، أو مؤشر انتهت صلاحيته منذ ذلك الحين، ليس خطأً: تحصل على صفحة فارغة وnext: null. وهذه هي الإجابة الصحيحة — فتكرار أحدث صفحة بدلاً من ذلك كان سيمنح السكربت بريدًا سبق أن عالجه — لكن معنى ذلك أن المؤشر المنتهي يبدو تمامًا كنهاية القائمة.
فتح رسالة واحدة، ومتى لا تحتاج إلى ذلك
المعرّف من القائمة مع صندوق البريد الذي وصلت إليه الرسالة يمنحانك الرسالة نفسها. كلاهما مطلوب: فمعرّف تسرّب من صندوق بريد لا يمكن استخدامه لقراءة صندوق آخر، لأن كل عملية بحث محصورة بالعنوان أيضًا.
$ curl -sG https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
--data-urlencode "mailbox=k7fq2m@grabmail.io"{
"id": "3QK7ZB5M9WVXR2HD4TNFJ0PC6A",
"from": "no-reply@example.com",
"to": "k7fq2m@grabmail.io",
"subject": "Your verification code",
"date": "2026-08-29T09:14:02Z",
"expires_at": "2026-09-03T09:14:02Z",
"text": "Your code is 481920. It expires in 10 minutes.",
"html": null,
"attachments": []
}text- الجزء النصي الصرف. حلّل هذا الجزء حين يكون موجودًا: فهو ثابت، لا يحمل أي وسم، والرمز المكوّن من ستة أرقام فيه هو رمز من ستة أرقام فعلاً.
html- جزء HTML، أو
nullحين لا يرسل المرسل واحدًا. غالبًا ما توجد روابط التأكيد هنا فقط. attachments- عنصر واحد لكل ملف، ولكل منها رابط جلب جاهز مسبقًا. قائمة فارغة، لا
null، حين لا يوجد أي منها. expires_at- متى تُحذف هذه الرسالة، بصيغة RFC 3339 نفسها التي يستخدمها
date. اقرأه بدل حسابه — فمدة الاحتفاظ ليست إعدادًا يمكنك التأكد منه من الخارج.
غالبًا ما تستطيع تجاوز هذا الاستدعاء كليًا. فالقائمة تعيد أصلاً الموضوع والمرسِل والتاريخ ومعاينة قصيرة للنص، وهذا يكفي لتقرر أن رسالة ما ليست ما تنتظره. وجلب كل رسالة في صندوق بريد لتكتشف أنك لم ترغب في أي منها هو أكثر طريقة شائعة يبطئ بها السكربت.
استخراج ملف
يحمل كل مرفق url خاصًا به، والتفصيل الذي يستحق معرفته قبل كتابة الحلقة هو أنه مسار على هذا المصدر لا عنوان مطلق — ومعامل mailbox موجود فيه مسبقًا. ضع المصدر في المقدمة، واجلبه، ولا شيء آخر يلزم تمريره ولا شيء يستوجب تفويضًا.
ADDR="k7fq2m@grabmail.io"
ID="3QK7ZB5M9WVXR2HD4TNFJ0PC6A"
curl -fsG https://grabmail.io/api/v1/message/$ID \
--data-urlencode "mailbox=$ADDR" \
| jq -r '.attachments[] | "\(.url)\t\(.filename)"' \
| while IFS=$'\t' read -r path name; do
curl -fs "https://grabmail.io$path" -o "$name"
doneيُجيب دائمًا بـapplication/octet-stream مع Content-Disposition: attachment، أيًا كان ما وسم به المرسل الملف. وهذا مقصود — فإعادة text/html الذي وضعه غريب كانت ستسمح لمرفق بالعمل كصفحة على هذا المصدر — لذا فإن السكربت الذي يهتم بالنوع يقرؤه من JSON الرسالة، حيث يكون بيانات لا تعليمة.
الرسالة كاملة، بملفاتها جميعًا، سقفها 5 MB. أما ماذا يعنيه ذلك السقف فعليًا بعد أن يُضخّم base64 ملفًا ثنائيًا فموضوع قائم بذاته، وله دليل عن المرفقات.
حصتان لمعدل الطلبات، لا حصة واحدة
هذا هو الجزء الذي يستحق معرفته ويسهل إغفاله: عرض قائمة عنوان وقراءة منه يُقاسان بشكل منفصل، لأنهما ليسا الخطر نفسه. أي شخص يعرف عنوانًا يستطيع استطلاع قائمته؛ أما قراءة رسالة فتتطلب معرّفًا، ولا شيء فيه للتخمين.
| ما الذي تستدعيه | الحصة | ماذا يعني ذلك عمليًا |
|---|---|---|
GET /mailbox | طلب واحد في الثانية، لكل عنوان | هذه هي وتيرة الاستطلاع المقصودة، ولا تُكبَح أبدًا عند هذه السرعة. الأسرع من ذلك يُرفض، ولم يكن ليفيد أصلاً. |
GET /message/{id}، GET /attachment/{id}، DELETE | أسخى بكثير، لكل عنوان | أفرغ صفحة رسائل دفعة واحدة دون توقف بينها. ولهذا تستطيع الواجهة فتح رسالة في الثانية نفسها التي جرى فيها استطلاع. |
| كل شيء، مجتمعًا | 1200 طلب في الدقيقة، لكل عميل | عشرون عنوانًا يُستطلَع كل واحد منها مرة في الثانية — رقم يتجاوز بارتياح أي أتمتة حقيقية، وهو ما يوقف مضيفًا واحدًا يجول على عشرة آلاف عنوان. |
تجاوز أيٍّ منها يمنحك 429 مع مدة الانتظار، بالثواني، في ترويسة Retry-After. التزم بها بدل التراجع بعدد اخترعته أنت: فهي الخادم يخبرك بالضبط متى سيقول نعم.
read_box() {
local wait
while :; do
BODY=$(curl -s -D /tmp/gm.h -G https://grabmail.io/api/v1/mailbox \
--data-urlencode "address=$1")
grep -qi '^HTTP/[0-9.]* 429' /tmp/gm.h || { printf '%s' "$BODY"; return 0; }
wait=$(awk 'tolower($1) == "retry-after:" { print $2 + 0 }' /tmp/gm.h)
sleep "${wait:-1}"
done
}أما كيف تنتظر رسالة لم تصل بعد — بموعد نهائي لا بعدد محاولات، وماذا تفعل حين يفوت ذلك الموعد — فهو موضوع دليل اختبار تدفقات التحقق. والحلقة هناك هي الحلقة نفسها التي تحتاجها مهمة مجدولة.
الحذف، والقاع الذي يقوم عليه كل شيء
يمكن التخلص من رسالة انتهيت منها فورًا بدل تركها تنتظر انتهاء مدة الاحتفاظ بها. أثر الاستدعاء لا يتغيّر بتكراره: حذف المعرّف نفسه مرتين يُجيب بـ200 في الحالتين، فلا يبدو طلب أُعيد إرساله فشلاً أبدًا.
$ curl -s -X DELETE -G https://grabmail.io/api/v1/message/3QK7ZB5M9WVXR2HD4TNFJ0PC6A \
--data-urlencode "mailbox=k7fq2m@grabmail.io"- احذف حين تحصل على ما جئت من أجله
- السكربت الذي يعالج رسالة ويتركها في مكانها سيعالجها مرة أخرى في التشغيل التالي، ما لم يحتفظ بقائمته الخاصة لما رآه. والحذف هو المحاسبة الأرخص.
- لا تعتمد عليه من أجل الخصوصية
- بين الوصول والحذف، كان بإمكان أي شخص يعرف العنوان أن يقرأها. الحذف يُغلق النافذة؛ ولا يُلغي ما حدث خلالها.
- كل شيء يزول عند 5 يومًا مهما حدث
- مقروءة أو غير مقروءة، محذوفة أو لا، تختفي الرسالة بعد 5 يومًا من وصولها. إنه حد صارم لا إعداد، ولا معامل يمدّده.
المعرّفات التي تتفرّع عندها، والحقل الذي لا تقرأه أبدًا
كل فشل هو JSON بالحقلين نفسيهما. error معرّف ثابت مقروء آليًا؛ أما message فللبشر وقد تُعاد صياغته في أي وقت. والتفرّع بناءً على الثاني هو كيف ينكسر سكربت في يوم لم يتغيّر فيه شيء.
| الحالة والمعرّف | ما الذي حدث | ما الذي ينبغي أن يفعله السكربت |
|---|---|---|
400 invalid_address | العنوان مفقود، أو لا يشبه عنوانًا أصلاً. | توقف فورًا. لا عدد من المحاولات يُصلح خطأ إملائيًا. |
400 bad_cursor | before ليس معرّف رسالة. | توقف فورًا، وتحقق من أنك تُعيد next لا شيئًا بنيته بنفسك. |
404 unknown_domain | ذلك النطاق غير مستضاف هنا. | توقف فورًا. على نطاقك الخاص، هذا يعني سجل MX — انظر توصيل نطاق. |
404 not_found | لا رسالة كهذه في ذلك الصندوق، أو أنها تجاوزت مدة الاحتفاظ بها. | عاملها كأنها زالت. وهذا أيضًا ما تحصل عليه عند قراءة معرّف صالح مقابل صندوق بريد خاطئ. |
429 rate_limited | إحدى الحصتين أعلاه. | انتظر عدد الثواني المذكور في Retry-After ثم تابع. لا تعدّها أبدًا تشغيلاً فاشلاً. |
مهمة تُفرغ عنوانًا كل ساعة
اجمع القطع معًا وستكون المهمة المجدولة قصيرة. هذه تأخذ كل رسالة تنتظر عند عنوان، وتكتبها على القرص بصيغة JSON، وتحذفها — فيبدأ التشغيل التالي من صندوق بريد فارغ، ولا يمكنه أبدًا معالجة الرسالة نفسها مرتين.
#!/usr/bin/env bash
set -euo pipefail
ADDR="orders@example.com"
OUT="/var/lib/mailsink"
API="https://grabmail.io/api/v1"
mkdir -p "$OUT"
while :; do
page=$(curl -fsG "$API/mailbox" \
--data-urlencode "address=$ADDR" \
--data-urlencode "limit=200")
ids=$(printf '%s' "$page" | jq -r '.messages[].id')
[ -n "$ids" ] || break
for id in $ids; do
curl -fsG "$API/message/$id" \
--data-urlencode "mailbox=$ADDR" > "$OUT/$id.json"
curl -fs -X DELETE -G "$API/message/$id" \
--data-urlencode "mailbox=$ADDR" > /dev/null
done
sleep 1
done17 * * * * /usr/local/bin/drain.shأربع خصائص تستحق أن تُسمّى، لأنها ما يفصل مهمة تستطيع تركها تعمل عن أخرى عليك مراقبتها:
- آمنة عند تشغيلها مرتين. نسختان بدأتا معًا تؤديان العمل نفسه بترتيب مختلف وتحذفان الرسائل نفسها؛ والثانية تجد صندوق بريد فارغًا فتتوقف.
- تكتب قبل أن تحذف. فإن كان القرص ممتلئًا أو أُنهيت العملية، تبقى الرسالة في الصندوق عند التشغيل التالي. أما الترتيب المعاكس فيُفقِد بريدًا في اليوم الذي يهم فيه ذلك فعلاً.
- تُفرغ لا تقرأ فقط. ولأن كل رسالة تزول بمجرد أن تصبح آمنة على القرص، تعيد القائمة التالية المئتين اللاحقتين — فصندوق بريد استقبل أربعمئة رسالة بين تشغيلين يُفرَّغ كليًا، لا حتى أحدث خمسين منها فقط.
- تفشل بصوت مرتفع. الخروج برمز غير صفري هو ما يجعل cron يرسل لك المخرجات. والمهمة التي تبتلع أخطاءها الخاصة هي مهمة ظلت معطوبة طوال شهر.
ما لن تفعله واجهة برمجة التطبيقات هذه من أجلك
أربعة أشياء لا تفعلها، كل واحد منها عن قصد، ولن يُضاف أي منها لاحقًا. من الأفضل أن تصمم حول هذه الحدود الآن بدل اكتشافها من سكربت ظل يعمل جزئيًا بصمت:
- لا ترسل أبدًا
- استقبال فقط. لا توجد نقطة طرفية تضع رسالة على السلك، ولهذا لا يمكن استخدام أي شيء هنا للإرسال من عنوان لا تملكه.
- لا تدفع أبدًا
- لا webhooks ولا callbacks: أنت تسأل، وهي تُجيب. أما وكيل ذكاء اصطناعي يفضّل الانتظار الحاجب حتى يصل البريد فله بدلاً من ذلك
wait_for_messageعبر MCP — انظر دليل الوكلاء. - لا تبحث أبدًا
- لا معامل استعلام لمرسِل أو لموضوع. يجري الترشيح من جهتك، فوق القائمة — وهذا أحد أسباب حمل القائمة معاينةً.
- لا تُصادِق أبدًا، على نطاق عام
- أي شخص يعرف العنوان يقرأ الصندوق. العنوان هو السر كله، فعامله كسرّ: لا تشتقه أبدًا من اسم عميل، ولا تُوجّه إلى نطاق مشترك أي شيء يزعجك أن يُقرأ بصوت عالٍ.
الحل للأخيرة هو نطاق تملكه أنت. وجّه سجل MX الخاص به إلى smtp.grabmail.io ليجيب كل عنوان عليه على النقاط الطرفية الثلاث نفسها، بلا واجهة برمجة تطبيقات ثانية تتعلمها ولا مفتاح تُدَوِّره — وعند الطلب، يُغلَق بحيث لا يفتحه إلا مفتاح Bearer. وتوصيل نطاق لا يتطلب أكثر من سجل DNS واحد.
10 smtp.grabmail.io
قبل أن تتركها تعمل
ستة أمور تستحق التحقق منها في مهمة ستعمل دون أن تراقبها:
- لا تستطلع أسرع من مرة في الثانية لكل عنوان، والتزم بـ
Retry-Afterحين يُطلب منك الانتظار. - اتبع
nextحتى النهاية، بدل افتراض أن استدعاءً واحدًا هو الصندوق كله. - تفرّع بناءً على رمز الحالة و
error، لا علىmessageأبدًا. - اكتب كل ما تحتاج إلى الاحتفاظ به قبل أن تحذفه، وتذكّر أن 5 يومًا قاع لا يمكنك تحريكه.
- اربط كل ما تستخرجه بقالبك الخاص. فنمط مكوّن من ستة أرقام مجردة سيطابق بارتياح سنةً، أو سعرًا، أو رقم طلب وصل أولاً.
- افترض أن العنوان عام ما لم يكن على نطاق تتحكم فيه، وضع كل ما يهم على نطاق كهذا.
لا شيء من هذا يحتاج إلى حساب. وإن تجاوزت النطاقات العامة احتياجك، فما يتغيّر هو النطاق في العنوان — أما الاستدعاءات الثلاثة أعلاه فتبقى كما هي تمامًا.
أسئلة
هل أحتاج إلى مفتاح API؟
لا. لا حساب على النطاقات العامة، ولا رمز، ولا شيء يستوجب التسجيل، والنطاق الذي تُوجّهه إلى هنا يُجيب على النقاط الطرفية نفسها بلا مفتاح أيضًا. الاستثناء الوحيد نطاق أغلقناه بناءً على طلب، ويُقرأ بترويسة Authorization: Bearer.
ما أسرع وتيرة يمكنني الاستطلاع بها؟
مرة في الثانية لكل عنوان بالنسبة لعرض القائمة، وهذه هي الوتيرة المقصودة ولا تُكبَح أبدًا. أما قراءة رسالة أو مرفق فتُقاس بشكل منفصل وأسخى بكثير، فتستطيع إفراغ صفحة رسائل دفعة واحدة. وكل شيء مجتمعًا سقفه 1200 طلب في الدقيقة لكل عميل.
كيف أعرف أنني قرأت الصندوق كاملاً؟
حين يعود next بقيمة null. لا تستنتج ذلك من صفحة قصيرة: فالخادم هو من يقرر ماهية الصفحة، وصفحة أقصر من limit ليست نهاية بحد ذاتها.
هل يمكنني استدعاء هذا من متصفح؟
نعم. تحمل الاستجابات Access-Control-Allow-Origin: *، فتستطيع صفحة من أي مصدر استدعاء النقاط الطرفية مباشرة دون وسيط خاص بك في المنتصف. التفويض هنا ليس cookie أبدًا، فالانفتاح إلى هذا الحد لا يكلّف شيئًا.
ماذا يحدث إن طلبت رسالة انتهت صلاحيتها؟
404 مع not_found، تمامًا كما مع معرّف لم يوجد قط. يُحذف كل شيء بعد 5 يومًا من وصوله، قُرئ أم لا، ولا معامل يمدّد ذلك.
هل يمكنني الحصول على webhook حين يصل بريد؟
لا — REST API تسأل فتُجاب، بلا callbacks. وإن كان ما تريده كودًا ينتظر انتظارًا حاجبًا حتى تصل الرسالة، فخادم MCP لديه wait_for_message، الذي يفعل ذلك بالضبط ومُخصَّص للوكلاء.
هل استخدام عنوان عام في بيئة الإنتاج آمن؟
فقط لأشياء لا تمانع أن يقرأها غريب. أي شخص يعرف العنوان يستطيع قراءة صندوقه، عبر واجهة برمجة التطبيقات تمامًا كما عبر الموقع. أما أي شيء آخر، فوجّه إلى هنا نطاقًا تملكه — فالاستدعاءات لا تتغيّر.
لماذا تظهر رسالة لم أفتحها قط كمقروءة؟
لأن شيئًا ما فتحها. قراءة رسالة عبر واجهة برمجة التطبيقات تضبط علامة seen فيها، وهذه العلامة مشتركة مع كل من ينظر إلى ذلك العنوان. سيستمر سكربت وشخص يراقبان الصندوق نفسه في مفاجأة أحدهما الآخر، لذا رشِّح حسب معرّفات عالجتها بدل seen.
هل يجب عليّ حذف الرسائل؟
لا — كل شيء تنتهي صلاحيته من تلقاء نفسه بعد 5 يومًا. لكن الحذف يستحق فعله على أي حال في مهمة مجدولة، لأن صندوق البريد المُفرَّغ هو أبسط سجل ممكن لما سبق أن عالجته.


