Regex للتحقق من البريد الإلكتروني: نمط عملي وحدوده
لا يوجد تعبير نمطي يخبرك بأن العنوان يعمل فعلًا. النمط القصير يلتقط الأخطاء المطبعية، ورسالة التأكيد تثبت الباقي.
ابحث عن تعبير نمطي (regex) للتحقق من البريد الإلكتروني وستجد كل شيء، من ثمانية محارف إلى عدة آلاف. الأنماط الطويلة تحاول اتباع المعايير حرفيًا، ومع ذلك لا تستطيع أن تخبرك إن كان صندوق البريد موجودًا. أما القصيرة فسهلة القراءة، وتكفي إذا استُخدمت في مهمتها الصحيحة: التقاط الأخطاء الواضحة، مثل @ مفقودة أو مسافة زائدة، قبل أن ترسل رسالة التأكيد التي تتولى الفحص الحقيقي.
لماذا لا ينبغي أن تستهدف قواعد RFC 5322 كاملة
يعرّف RFC 5322 صيغة العناوين في رسائل البريد الإلكتروني، ويسمح بأكثر بكثير مما يحتاجه أي نموذج تسجيل. كل العناوين التالية صالحة من حيث الصيغة:
"john doe"@example.com: جزء محلي بين علامتي اقتباس يحتوي مسافة.john(comment)@example.com: تعليق داخل العنوان.user@[192.168.0.1]: عنوان IP بدل اسم نطاق.admin@localhost: نطاق بلا نقطة.
التعبير النمطي الذي يطبّق القواعد كلها، بما فيها التعليقات والمسافات البيضاء المطوية، ضخم وصعب المراجعة، ويقبل عناوين ترفضها أنظمة بريد كثيرة. وهو مع ذلك يمرّر jane@gmial.com دون اعتراض. ويضيف SMTP (المعرَّف في RFC 5321) حدودًا للطول: 64 ثُمانية (octet) للجزء الذي يسبق @ و255 للنطاق، وهي مع صيغة المسار تعطي حدًا أقصى عمليًا قدره 254 محرفًا. وفحص الطول أبسط حين يُكتب شفرةً عادية منه حين يُحشر في تعبير نمطي.
نمط عملي
هذا هو النمط الذي تحتاجه معظم التطبيقات فعلًا: شيء لا يحتوي مسافات ولا @، ثم @ واحدة، ثم نطاق يحتوي نقطة واحدة على الأقل.
^[^\s@]+@[^\s@]+\.[^\s@]+$
في هذا النمط نقطة ضعف واحدة تستحق الإصلاح. فلأن [^\s@] يطابق النقاط أيضًا، يقبل النمط مقاطع نطاق فارغة مثل user@example..com. واستبعاد النقطة من كل مقطع يسدّ هذه الثغرة:
^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$
وهذا ما فعله كل نمط بمجموعة من عناوين الاختبار في Node 24، إلى جانب نمط مواصفة HTML الذي يشرحه القسم التالي:
| العنوان | النمط القصير | نمط المقاطع | HTML type=email |
|---|---|---|---|
first.last+news@example.co.uk | مقبول | مقبول | مقبول |
o'brien@example.ie | مقبول | مقبول | مقبول |
user@bücher.de | مقبول | مقبول | مرفوض |
δοκιμή@παράδειγμα.δοκιμή | مقبول | مقبول | مرفوض |
"john doe"@example.com | مرفوض | مرفوض | مرفوض |
admin@localhost | مرفوض | مرفوض | مقبول |
user @example.com | مرفوض | مرفوض | مرفوض |
user@@example.com | مرفوض | مرفوض | مرفوض |
user@example..com | مقبول | مرفوض | مرفوض |
user@example.com. | مقبول | مرفوض | مرفوض |
us..er@example.com | مقبول | مقبول | مقبول |
user@-example.com | مقبول | مقبول | مرفوض |
user@example.c | مقبول | مقبول | مقبول |
لا أحد من الأنماط الثلاثة مثالي، ولا يلزم أن يكون كذلك. النقاط المتتالية في الجزء المحلي ونطاقات المستوى الأعلى المكوّنة من حرف واحد تمرّ منها جميعًا، ورسالة التأكيد تتكفّل بها. المهم ألا يُرفض أي عنوان حقيقي شائع الاستخدام.
وهناك سبب يتعلق بالأداء لتفضيل نمط المقاطع. فعلى مدخل مصمَّم خصيصًا طوله 40,000 محرف وينتهي بمسافة، استغرق النمط القصير نحو 2.9 ثانية في Node بسبب التراجع (backtracking)، بينما انتهى نمط المقاطع في أقل من مللي ثانية. وفحص حد الـ 254 محرفًا قبل تشغيل أي نمط يزيل المشكلة في الحالتين.
نمط HTML5 لحقل input type=email
يتحقق المتصفح من الحقل <input type="email"> وفق تعريف وارد في معيار HTML الصادر عن WHATWG. تصف المواصفة هذا التعريف بأنه مخالفة متعمَّدة لـ RFC 5322، وتقدّم التعبير النمطي التالي بلغة JavaScript طريقةً لتطبيقه:
/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
يسمح هذا النمط بعلامات الترقيم المعتادة قبل @، ويشترط أن يتكوّن كل مقطع من مقاطع النطاق من 63 حرفًا أو رقمًا أو شرطة على الأكثر، وألا يبدأ بشرطة أو ينتهي بها، ولا يشترط وجود نقطة، لذلك يمرّ admin@localhost. ويرفض أي محرف خارج ASCII. يمكنك إعادة استخدام فحص المتصفح في السكربت عبر input.checkValidity() أو input.validity.typeMismatch، لكن كل ما يفحصه المتصفح يمكن تجاوزه، فتحقّق مرة أخرى على الخادم.
العناوين الدولية
يمكن أن تحتوي أسماء النطاقات محارف خارج ASCII. فالنطاق bücher.de ينتقل عبر DNS بالشكل xn--bcher-kva.de، وكلٌّ من new URL('http://bücher.de').hostname في JavaScript وidn_to_ascii() في PHP و'bücher.de'.encode('idna') في Python ينتج هذا الشكل. ويمكن أن يحتوي الجزء الذي يسبق @ محارف خارج ASCII أيضًا، لكن خوادم البريد التي تدعم SMTPUTF8 (المعرَّف في RFC 6531) هي وحدها القادرة على التسليم إلى مثل هذه العناوين.
اتخذ القرار بناءً على مزوّد البريد لديك، لا على تعبير نمطي. الأنماط المبنية على [^\s@] تقبل العناوين الدولية، بينما يرفضها نمط HTML. وكذلك يفعل filter_var($email, FILTER_VALIDATE_EMAIL) في PHP: ففي اختباراتنا رفض user@bücher.de وδοκιμή@example.com كليهما، ومع الراية FILTER_FLAG_EMAIL_UNICODE قبِل الثاني لكنه ظل يرفض النطاق المكتوب بمحارف يونيكود.
الفحص نفسه في JavaScript وPython وPHP
كل دالة من الدوال التالية تحذف المسافات البيضاء المحيطة بالقيمة، وتفرض حد الطول، وتشترط أن يطابق النص كله. وقد أعادت الثلاث نتائج متطابقة للعناوين السابقة ولبضع حالات حدّية إضافية: مسافات بيضاء محيطة، وسطر جديد في منتصف القيمة، ومسافة غير قابلة للفصل (non-breaking space) في الجزء المحلي، وعنوان طوله 256 محرفًا.
// JavaScript
function isValidEmail(value) {
const email = value.trim();
return email.length <= 254 && /^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/.test(email);
}
# Python
import re
EMAIL_RE = re.compile(r"[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+")
def is_valid_email(value: str) -> bool:
email = value.strip()
return len(email) <= 254 and EMAIL_RE.fullmatch(email) is not None
// PHP
function isValidEmail(string $value): bool
{
$email = trim($value);
return strlen($email) <= 254
&& preg_match('/^[^\s@]+@[^\s@.]+(?:\.[^\s@.]+)+$/Du', $email) === 1;
}
في هذه الشفرات تفصيلتان تمنعان أخطاء حقيقية. في Python قبِل re.match() مع $ القيمة "user@example.com\n"، لأن $ يطابق أيضًا قبل سطر جديد في النهاية، بينما رفضها re.fullmatch(). ويتصرف PHP بالطريقة نفسها ما لم تضف المعدِّل D. والمعدِّل u مهم في PHP أيضًا: فمن دونه لم يطابق \s المسافة غير القابلة للفصل، فقُبل العنوان user\u{00A0}x@example.com. أما في JavaScript فلا تضع الراية g إن كنت تستدعي test() فقط.
التحقق الحقيقي هو رسالة التأكيد
كل ما يستطيع النمط قوله هو أن العنوان يبدو معقولًا. لا يستطيع أن يعرف إن كان صندوق البريد موجودًا، أو إن كان يستقبل الرسائل، أو إن كان يخص الشخص الذي يكتبه. والاستعلام عن سجلات MX يبيّن أن النطاق مهيَّأ لاستقبال البريد، لكنه لا يثبت وجود صندوق بريد بعينه. الاختبار الموثوق الوحيد هو إرسال رسالة:
- خزّن العنوان على أنه غير مؤكَّد.
- أرسل إليه رابطًا يحتوي رمزًا عشوائيًا يُستخدم مرة واحدة وتنتهي صلاحيته، بعد 24 ساعة مثلًا.
- لا تعلّم العنوان مؤكَّدًا إلا عند فتح الرابط.
- وحتى ذلك الحين، تجنّب إرسال أي شيء آخر إلى هذا العنوان.
اجعل التطبيع خفيفًا: احذف المسافات البيضاء وحوّل النطاق إلى أحرف صغيرة. فـ RFC 5321 يعامل الجزء الذي يسبق @ على أنه حساس لحالة الأحرف، مع أن معظم المزوّدين يتجاهلونها، لذا خزّنه كما أُدخل.
جرّب الأنماط على قائمتك
الصق أحد الأنماط في مختبِر التعابير النمطية من دون الشرطتين المائلتين المحيطتين، واضبط الرايات على gm، وضع عناوين الاختبار كل عنوان في سطر. مع الراية m ينطبق ^ و$ على كل سطر، فيُبرَز كل عنوان مقبول وحده. وقد تحققنا من أن هذا العرض سطرًا سطرًا يعطي النتائج نفسها التي يعطيها اختبار كل عنوان منفردًا، وذلك للأنماط الثلاثة السابقة جميعًا. أما في شفرة التطبيق فاختبر عنوانًا واحدًا في كل مرة من دون m، وإلا فقد تمرّ قيمة متعددة الأسطر لمجرد أن أحد أسطرها يطابق.