افتح صندوق بريد

الاختبار وCI

رموز OTP من البريد في الاختبارات الآلية دون تذبذب

رمز التحقق لمرة واحدة هو ست خانات داخل رسالة تحتوي أيضًا على سنة، وسعر، ورقم طلب، ورقم هاتف. واستخراج الست الصحيحة — في كل مرة، وفي كل مُشغِّل — انضباط بسيط: ثبِّت النمط، وأزل الترميز، وتجاهل الرسالة التي رأيتها من قبل، واحترم انتهاء الصلاحية. وهذا هو ذلك الانضباط، مع كود جاهز للطرفية، وPython، وTypeScript.

  • متوسط
  • 14 دقيقةً للقراءة
مظروف أزرق مفتوح وأمامه ستة مكعبات رمادية صغيرة مصطفة، على كل منها نقطة زرقاء، تحت عدسة مكبرة رمادية

أين يوجد الرمز فعلاً

يحتوي بريد التحقق على ما يصل إلى ثلاثة أماكن يمكن أن يعيش فيها الرمز، وأيّها يجب أن تقرأ يحدد كل ما يليه. وتمنحك رسالة JSON القادمة من API الأماكن الثلاثة دفعة واحدة: subject، وtext (الجزء النصي البسيط، أو null)، وhtml (جزء HTML، أو null).

أينكيف يبدوكيف تقرأه
الجزء النصي البسيط (text)Your code is 481920. It expires in 10 minutes.حلِّل هذا أولاً حين يوجد. بلا ترميز، ولا شيء لفك تشفيره، والصياغة ثابتة.
جزء HTML (html)الجملة نفسها داخل جدول، وغالبًا مع تنسيق كل خانة في خلية منفصلة، وكل & مكتوبة ككيان (entity).أزل الوسوم واستبدلها بمسافات، وفك ترميز الكيانات، واطوِ المسافات البيضاء، ثم طبِّق النمط. لا تُطبِّق regex على HTML الخام أبدًا.
سطر الموضوع481920 is your verification codeهدية حين يفعلها المرسِل: لا متن يجب تحليله على الإطلاق. طابِقه في الموضوع، وارجع إلى المتن عند الحاجة.
صورةالرمز مرسوم كصورة، لإحباط هذا النوع بالذات من السكربتات.نادر، وعلامة على أن المرسِل لا يريد الأتمتة. غيِّر قالب المرسِل إن كان قالبك أنت؛ ولا يوجد التفاف صادق عليه إن لم يكن كذلك.

الجزء النصي البسيط هو المفضَّل، وهو الذي تولِّده معظم أنظمة القوالب تلقائيًا من HTML — فهو موجود عادةً. وحين يكون null، يصبح جزء HTML هو المتن الوحيد، والقسمان التاليان عن قراءته بأمان.

ثبِّت النمط على صياغتك أنت

الغريزة الأولى هي \d{6}. فهو يطابق الرمز، ويطابق أيضًا السنة في التذييل، والرمز البريدي في كتلة العنوان، وآخر ست خانات من رقم هاتف، ورقم الطلب الذي يظهر قبل الرمز بسطرين. وأيّهما يظهر أولاً يفوز، ويكتبه الاختبار في النموذج بثقة تامة.

النمطيطابق أيضًاالحكم
\d{6}سنوات، ورموز بريدية، وأسعار بلا فاصل، وأرقام طلبات، وأرقام هواتف، وأرقام تتبع شحنات.أبدًا. فهذا ليس نمطًا، بل رمية عملة.
\b\d{6}\bكل ما سبق مما يصادف أن يكون بالضبط ست خانات ومسافة على كل جانب — أي معظمه أيضًا.أفضل بالكاد. فحدود الكلمات (word boundaries) لا تعرف ما هو الرمز.
code is\D{0,12}(\d{6})فقط الست خانات التي تلي الكلمات التي يضعها قالبك أنت قبل الرمز، مع مساحة لنقطتين رأسيتين، أو مسافة، أو ما يعادل وسمًا من المسافات البيضاء المتبقية.نعم. فهو يطابق الرمز ولا شيء غيره، ويفشل يوم يُعيد فيه أحدهم صياغة البريد — وهو فشل تريد أن تُخبَر به.

و\D{0,12} هو التفصيل العملي: فبعد استبدال الوسوم بمسافات، يمكن أن تُفصَل الكلمات عن الخانات بنقطتين رأسيتين، أو سلسلة مسافات، أو بقايا <strong> كانت تجلس بينهما. وحتى اثنا عشر حرفًا غير رقمي يغطي كل ذلك دون أن يسمح للنمط بالقفز إلى رقم مختلف.

قوالب تُقسِّم الخانات

يضع تصميم شائع كل خانة من الرمز في صندوقها الخاص، لتُقرأ جيدًا على الهاتف. وهذا في HTML يعني ست خلايا جدول، أو ست <span>، ولا يظهر الرقم أبدًا كست خانات متتالية في أي مكان في المصدر:

ما يحتويه جزء HTML فعلاً
<p>Your code is</p>
<table><tr>
  <td class="digit">4</td><td class="digit">8</td><td class="digit">1</td>
  <td class="digit">9</td><td class="digit">2</td><td class="digit">0</td>
</tr></table>

لا يجد regex على HTML الخام شيئًا. والحل ليس regex أذكى؛ بل تحويل HTML إلى نص أولاً، بترتيب ثابت:

  1. استبدل كل وسم بمسافة. مسافة، لا فراغًا تامًا — فيجب أن يصبح <td>4</td><td>8</td> هو 4 8، لا 48 ملتصقة بما تلاها.
  2. فُك ترميز الكيانات. &amp;، و&nbsp;، و&#39;. فالمسافة غير القابلة للفصل بين خانتين ليست مسافة بالنسبة لـregex إلى أن يُفَك ترميزها.
  3. اطوِ المسافات البيضاء، ثم طابِق مع السماح للخانات بأن تكون متباعدة. بالنسبة للتصميم المُقسَّم إلى صناديق، استخدم code is\D{0,12}(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d) ثم اجمع المجموعات (groups)؛ أما بالنسبة لقالب عادي فالنمط البسيط من القسم السابق يكفي.

تقوم المساعدات (helpers) أدناه بالخطوتين الأولى والثانية نيابة عنك، وتبحث في كلا الجزأين دفعة واحدة، بحيث لا يحتاج الاختبار إلى معرفة أي تصميم يستخدمه القالب هذا الشهر.

أحدث رسالة ليست دائمًا الصحيحة

كل سرد لصندوق بريد هنا يعود بترتيب الأحدث أولاً، وmessages[0] هو ما تقرؤه معظم المسودات الأولى. وثلاث حالات تجعل تلك الرسالة الخطأ:

رسالتان من إجراء واحد
يرسل التسجيل بريد ترحيب وبريد رمز، بأي ترتيب يُفرِّغ به طابور المرسِل. ونصف الوقت يكون الترحيب هو الأحدث. رشِّح على الموضوع، أو على المرسِل، قبل أخذ أي شيء.
إعادة إرسال
طلب الاختبار الرمز مرتين — مرة بالخطأ، ومرة عمدًا — ولا يقبل الخادم إلا الأحدث. وتبقى الرسالة الأقدم في صندوق البريد، وتظل تطابق النمط، وتظل تُحلَّل إلى ست خانات أصبحت الآن غير صالحة.
تشغيلة اختبار سابقة
فقط إن أُعيد استخدام العنوان، وهو ما يجب ألا يحدث أبدًا. فعنوان جديد لكل تشغيلة يجعل هذه الحالة مستحيلة؛ وإن تعذّر ذلك، فاللقطة (snapshot) أدناه هي الحل البديل.

الشكل المتين هو نفسه في كل مُشغِّل: انظر إلى ما في صندوق البريد قبل إطلاق البريد، ثم لا تقبل إلا رسالة لم تكن موجودة بعد وتطابق الموضوع الذي تتوقعه.

انتظار رسالة لم تكن موجودة قبل إعادة الإرسال
// Remember what is already there, THEN trigger the resend, THEN wait for something new.
const before = new Set((await listMailbox(address)).messages.map(m => m.id));

await page.getByRole('button', { name: 'Resend code' }).click();

const fresh = await waitFor(address, m => !before.has(m.id) && /code/i.test(m.subject));

رموز تنتهي صلاحيتها أثناء التشغيلة

تصلح معظم رموز التحقق من خمس إلى خمس عشرة دقيقة. ويبدو ذلك سخيًا إلى أن تضع مجموعة اختبارات عشرين اختبارًا في طابور، طلب كل منها رمزه في البداية وكتبه في النهاية. وثلاث قواعد تُبقي الرمز حيًا:

  • اطلب الرمز في آخر لحظة ممكنة. أطلِق الإرسال مباشرة قبل الانتظار، لا في خطوة إعداد تعمل بينما اختبارات أخرى في الطابور.
  • أبقِ مهلة الانتظار أقل بكثير من عمر الرمز. فمهلة ستين ثانية لرمز مدته عشر دقائق تترك تسع دقائق لكتابته. أما مهلة عشر دقائق فلا تترك شيئًا.
  • لا تُخزِّن رمزًا أبدًا لاختبار آخر. فالرموز تُستخدم مرة واحدة كما أن عمرها قصير؛ وأي fixture مشترك يوزِّع رمزًا هو سباق بين اختبارين على رقم واحد.

دوال استخراج جاهزة للاستخدام

ثلاث نسخ من الأسطر الأربعة نفسها: دمج الجزأين، وتحويل الوسوم إلى مسافات، وفك ترميز الكيانات، وطي المسافات البيضاء، ثم النمط المثبَّت. غيِّر النمط ليطابق صياغة قالبك، ولا شيء آخر يحتاج إلى لمس.

من الطرفية، بـjq

shell
curl -sG "https://grabmail.io/api/v1/message/$ID" --data-urlencode "mailbox=$ADDR" \
| jq -r '[.text, .html] | map(select(. != null)) | join(" ") | gsub("<[^>]*>"; " ")' \
| grep -oiE 'code is[^0-9]{0,12}[0-9]{6}' | grep -oE '[0-9]{6}' | head -1

Python

extract.py
import html
import re

TAGS = re.compile(r"<[^>]+>")
CODE = re.compile(r"code is\D{0,12}(\d{6})", re.I)      # anchored on YOUR template's wording


def text_of(message: dict) -> str:
    """Both parts as plain text: tags out, entities decoded, whitespace folded."""
    raw = f"{message.get('text') or ''}\n{message.get('html') or ''}"
    return re.sub(r"\s+", " ", html.unescape(TAGS.sub(" ", raw)))


def code_from(message: dict, pattern: re.Pattern = CODE) -> str:
    hit = pattern.search(text_of(message))
    if not hit:
        raise AssertionError(f"no code in message {message['id']!r} ({message['subject']!r})")
    return hit.group(1)

TypeScript

extract.ts
export type Message = { id: string; subject: string; text: string | null; html: string | null };

const TAGS = /<[^>]+>/g;
const ENTITIES: Record<string, string> = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'", '&nbsp;': ' ' };

/** Both parts as plain text: tags out, the common entities decoded, whitespace folded. */
export const textOf = (m: Message): string =>
  `${m.text ?? ''}\n${m.html ?? ''}`
    .replace(TAGS, ' ')
    .replace(/&(amp|lt|gt|quot|#39|nbsp);/g, e => ENTITIES[e])
    .replace(/\s+/g, ' ');

/** Anchored on your own wording. A reworded template fails loudly. */
export function codeFrom(m: Message, pattern = /code is\D{0,12}(\d{6})/i): string {
  const hit = textOf(m).match(pattern);
  if (!hit) throw new Error(`no code in message ${m.id} ("${m.subject}")`);
  return hit[1];
}

ورسالة JSON التي تقرؤها هذه الدوال تأتي من GET /api/v1/message/{id}، الموثَّق في مرجع API؛ والانتظار الذي يمنحك المعرِّف أصلاً موجود في دليل الاختبار من طرف إلى طرف، وكمساعدات جاهزة لـPlaywright وCypress وPython وNode.js.

قبل أن تعتبره منتهيًا

  • النمط مثبَّت على صياغة قالبك، ومحفوظ بجانب القالب.
  • البحث في كلا الجزأين، كنص: الوسوم إلى مسافات، والكيانات مفكوكة الترميز، والمسافات البيضاء مطوية.
  • مرشِّح على الموضوع أو المرسِل، بحيث لا يفوز بريد الترحيب أبدًا على بريد الرمز.
  • لقطة (snapshot) قبل أي إعادة إرسال، ولا تُقبل بعدها إلا الرسائل الجديدة.
  • مهلة الانتظار أقل بكثير من عمر الرمز، وإطلاق الإرسال مباشرة قبل الانتظار.
  • رسالة فشل تذكر معرِّف الرسالة والموضوع الذي بحثت فيه.

وهذا يغطي كل طريقة رُصد فيها نجاح مستخرِج ست خانات برقم خطأ. ولدى الوكيل الذي يقرأ البريد نفسه المشكلات نفسها، وأداة واحدة أقل لحلها، ولهذا يمنحه خادم MCP الرسالة كاملة بدل تخمين — ويستعرض دليل صندوق وارد يستطيع وكيل ذكاء اصطناعي قراءته ذلك بالتفصيل.

أسئلة

هل يجب أن أقرأ الجزء النصي أم جزء HTML؟

الجزء النصي حين يوجد: فهو ثابت ولا شيء فيه يحتاج فك ترميز. لكن ابحث في كليهما على أي حال، كما تفعل المساعدات، بحيث يعمل القالب الذي يرسل HTML فقط، ولا يتعثر القالب الذي يرسل نصًا فقط في HTML فارغ.

رمزي يحتوي على أحرف. هل يتغير النمط؟

فقط فئة الأحرف (character class): ([A-Z0-9]{6})، أو أي أبجدية يستخدمها المرسِل، مع بقائها مثبَّتة على الصياغة التي تسبقها. أضف علامة i إن لم تكن حالة الأحرف مضمونة، واحذر ألا تطابق الفئة أيضًا كلمة إنجليزية تلي نقطة التثبيت.

ماذا عن الروابط السحرية بدلاً من الرموز؟

الانضباط نفسه، بنمط مختلف: طابِق الرابط بجزء من المسار تعرفه — /confirm/، /auth/magic/ — بدلاً من «الرابط الأول»، لأن البريد المعاملاتي يحمل عادةً خمسة روابط ونادرًا ما يكون الذي تريده هو الأول. فُك ترميز &amp; قبل زيارته.

كم من الوقت تبقى الرسالة متاحة للقراءة؟

5 يومًا بعد وصولها، سواء قُرئت أم لا. وهذا أطول بكثير من مدة صلاحية أي رمز، فلا يحتاج الاختبار أبدًا إلى الإسراع في القراءة — بل في الكتابة فقط.

هل يمكنني الحصول على الرمز دون استطلاع صندوق البريد؟

عبر REST، لا: فأنت تستطلع مرة في الثانية بمهلة زمنية، وهذا هو الإيقاع الموثَّق ولا يُقيَّد أبدًا. أما عبر MCP فهناك أداة wait_for_message تُبقي النداء مفتوحًا حتى تصل الرسالة، وهذا هو الشكل الذي يحتاجه وكيل الذكاء الاصطناعي.

هل أحتاج إلى مفتاح API؟

لا. فالنطاقات العامة لا تحتاج مفتاحًا، ولا حسابًا، ولا ترويسة. ولا تستخدم رمز bearer إلا مجموعة النطاقات المدفوعة التي تبقى بعيدة عن قوائم حظر البريد المؤقت، وكود الاستخراج مطابق في الحالتين.

جرّبه وهو جديد

عنوان واحد يستغرق نقرة واحدة، بلا حساب وبلا بطاقة. كل ما في هذا الدليل يعمل عليه فورًا.

أهلًا بعودتك

صناديقك ونطاقاتك في مكان واحد.