Regex برای اعتبارسنجی ایمیل: یک الگوی کاربردی و محدودیتهای آن
هیچ regexی نمیتواند بگوید یک آدرس واقعاً کار میکند یا نه. یک الگوی کوتاه غلطهای تایپی را میگیرد؛ بقیه را ایمیل تأیید ثابت میکند.
اگر دنبال regex ایمیل بگردید، از الگوهای هشتنویسهای تا الگوهای چندهزارنویسهای پیدا میکنید. الگوهای طولانی تلاش میکنند دقیقاً از استانداردها پیروی کنند و باز هم نمیتوانند بگویند یک صندوق پستی وجود دارد یا نه. الگوهای کوتاه راحت خوانده میشوند و اگر برای کار درست به کار بروند، کافیاند. آن کار درست، گرفتن اشتباههای آشکار است، مثل نبودن @ یا وجود فاصله، پیش از اینکه یک ایمیل تأیید بفرستید که بررسی واقعی را انجام میدهد.
چرا گرامر کامل RFC 5322 هدف اشتباهی است
RFC 5322 نحو آدرسها را در پیامهای ایمیل تعریف میکند و بسیار بیشتر از آنچه هر فرم ثبتنامی بخواهد مجاز میداند. همه این آدرسها از نظر نحوی معتبرند:
"john doe"@example.com: بخش محلی (local part) داخل گیومه که یک فاصله دارد.john(comment)@example.com: یک توضیح (comment) داخل آدرس.user@[192.168.0.1]: یک آدرس IP بهجای نام دامنه.admin@localhost: دامنهای بدون نقطه.
regexی که کل این گرامر را، از جمله توضیحها و فاصلههای تاخورده (folding whitespace)، پیادهسازی کند، عظیم است، بازبینیاش دشوار است و آدرسهایی را میپذیرد که بسیاری از سیستمهای ایمیل رد میکنند. تازه jane@gmial.com را هم قبول میکند. SMTP (RFC 5321) محدودیتهای طول اضافه میکند: ۶۴ اکتت برای بخش پیش از @ و ۲۵۵ اکتت برای دامنه، که همراه با نحو مسیر، سقف عملی ۲۵۴ نویسه را میدهد. بررسی طول با کد سادهتر است تا با regex.
یک الگوی کاربردی
این همان الگویی است که بیشتر اپلیکیشنها واقعاً لازم دارند: چیزی بدون فاصله و بدون @، یک @، و بعد دامنهای که دستکم یک نقطه داشته باشد.
^[^\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 | پذیرفته | پذیرفته | پذیرفته |
هیچکدام از این سه کامل نیست و لازم هم نیست باشد. نقطههای پشتسرهم در بخش محلی و دامنههای سطح بالای تکحرفی از همه آنها رد میشوند؛ ایمیل تأیید این موارد را میگیرد. آنچه اهمیت دارد این است که هیچ آدرس واقعی و رایجی رد نشود.
یک دلیل مربوط به کارایی هم برای ترجیح نسخه برچسبی وجود دارد. روی یک ورودی دستساز ۴۰٬۰۰۰ نویسهای که به یک فاصله ختم میشود، الگوی کوتاه در Node به خاطر عقبگرد (backtracking) حدود ۲٫۹ ثانیه طول کشید، در حالی که الگوی برچسبی در کمتر از یک میلیثانیه تمام شد. اگر پیش از اجرای هر الگویی سقف ۲۵۴ نویسه را بررسی کنید، این مشکل برای هر دو از بین میرود.
الگوی input type=email در HTML5
مرورگر یک <input type="email"> را بر اساس تعریفی در استاندارد HTML گروه WHATWG اعتبارسنجی میکند. خود مشخصات این تعریف را «نقض عمدی» (willful violation) 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])?)*$/
این الگو علامتهای نگارشی رایج را پیش از @ مجاز میداند، برچسبهای دامنه را به حداکثر ۶۳ حرف، رقم و خط تیره محدود میکند که با خط تیره شروع یا تمام نشوند، و نقطه را الزامی نمیداند؛ پس 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) پشتیبانی میکنند میتوانند به چنین آدرسهایی نامه برسانند.
تصمیم را بر اساس سرویسدهنده ایمیل خودتان بگیرید، نه بر اساس یک regex. الگوهایی که بر پایه [^\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) در بخش محلی و یک آدرس ۲۵۶ نویسهای.
// 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 هم همین رفتار را دارد، مگر اینکه modifier D را اضافه کنید. modifier u هم در PHP مهم است: بدون آن، \s با فاصله نشکن تطبیق پیدا نکرد و user\u{00A0}x@example.com پذیرفته شد. در JavaScript، وقتی فقط test() را صدا میزنید پرچم g را نگذارید.
اعتبارسنجی واقعی، ایمیل تأیید است
یک الگو فقط میتواند بگوید آدرس معقول به نظر میرسد. نمیتواند بگوید صندوق پستی وجود دارد، ایمیل دریافت میکند یا متعلق به کسی است که آن را تایپ کرده. بررسی رکوردهای MX نشان میدهد که یک دامنه برای دریافت ایمیل تنظیم شده، اما نشان نمیدهد یک صندوق پستی مشخص وجود دارد. تنها آزمون مطمئن، فرستادن یک پیام است:
- آدرس را بهعنوان تأییدنشده ذخیره کنید.
- لینکی حاوی یک توکن تصادفی و یکبارمصرف که منقضی میشود، مثلاً بعد از ۲۴ ساعت، ایمیل کنید.
- آدرس را فقط وقتی تأییدشده علامت بزنید که لینک باز شود.
- تا آن زمان، از فرستادن هر چیز دیگری به آن آدرس خودداری کنید.
یکسانسازی را سبک نگه دارید: فاصلههای اضافه را حذف کنید و دامنه را به حروف کوچک تبدیل کنید. RFC 5321 بخش پیش از @ را حساس به بزرگی و کوچکی حروف میداند، هرچند بیشتر سرویسدهندهها این تفاوت را نادیده میگیرند؛ پس آن بخش را همانطور که وارد شده ذخیره کنید.
الگوها را روی فهرست خودتان امتحان کنید
یکی از الگوها را بدون اسلشهای دو طرفش در تستکننده Regex بچسبانید، پرچمها را روی gm بگذارید و آدرسهای نمونهتان را هرکدام در یک خط وارد کنید. با پرچم m، ^ و $ روی هر خط جداگانه اعمال میشوند، پس هر آدرس پذیرفتهشده بهطور مستقل هایلایت میشود. ما بررسی کردیم که این نمای خطبهخط برای هر سه الگوی بالا همان نتیجهای را میدهد که آزمایش تکتک آدرسها. در کد اپلیکیشن، هر بار فقط یک آدرس را بدون m آزمایش کنید؛ وگرنه یک مقدار چندخطی ممکن است فقط به این دلیل که یکی از خطهایش تطبیق دارد پذیرفته شود.