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

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

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

أولا التشخيص: الرسالة لم تُرسَل أم أُرسلت ووقعت في البريد المزعج؟#

هناك ثلاث حالات ممكنة، وهي كلها متطابقة من وجهة نظر المستخدم: «الرسالة لم تصل».

  1. التطبيق لم يسلّم الرسالة إلى خادم البريد أصلا.
  2. سلّمها، لكن خادم المستقبِل رفضها أو ردّها مؤقتا.
  3. قبلها المستقبِل لكنه وضعها في مجلد البريد المزعج.

للتمييز بين الثلاث ابدأ من جهتك. انظر أولا إلى سجل لارافيل. إذا فشل الإرسال فهناك عادة استثناء متعلق بنقل البريد:

Bash
tail -n 300 storage/logs/laravel.log | grep -i -E 'mail|smtp|transport'

إذا كان السجل فارغا فأنت لا تعرف بعد هل نجح الإرسال أم لم يحدث أصلا. لذلك لدينا اختبار آمن: اجعل مشغّل البريد مؤقتا على log، وامسح كاش الإعدادات، ونفّذ العملية نفسها مرة أخرى.

INI
MAIL_MAILER=log
Bash
php artisan config:clear

إذا كُتب النص الكامل للرسالة في storage/logs/laravel.log فهذا يعني أن التطبيق يبني الرسالة فعلا و«يرسلها»، وأن المشكلة تقع بعد لارافيل. وإذا لم يُكتب شيء فالمشكلة داخل التطبيق نفسه، ولا علاقة لسجلات DNS بها.

نقطة أوقعتنا: لا تنسَ config:clear. إذا كنت قد نفّذت config:cache فلن يكون لتغيير .env أي أثر حتى يُمسح الكاش. وإذا نسخت القيم من ويندوز فتحقق من نهايات الأسطر؛ نهايات الأسطر على طريقة ويندوز في ملف env فخ تُعد كلمة مرور SMTP من ضحاياه الكلاسيكيين.

وللاختبار اليدوي يكفي tinker:

PHP
Mail::raw('اختبار إرسال من الخادم', function ($m) {
    $m->to('you@example.com')->subject('اختبار الإرسال');
});

من جهة الخادم، في cPanel صفحة اسمها Track Delivery تعرض تقرير تسليم الرسائل الصادرة. إذا لم تكن رسالتك هناك فهذا يعني أنها لم تُسلَّم قط إلى خادم البريد. وإذا كانت هناك وعادت بخطأ فسترى نص الخطأ في المكان نفسه — وهذا النص يقول عادة بدقة ما الذي اعترض عليه المستقبِل.

إذا بقيت الرسالة في الطابور فلن تُرسَل أبدا#

هذه أشيع حالة رأيناها، وأطرف ما فيها أنها لا تولّد أي خطأ.

إذا كان صنف البريد لديك ينفّذ ShouldQueue وكان QUEUE_CONNECTION على database فإن لارافيل لا يرسل الرسالة؛ بل يضع سجلا في جدول jobs ويردّ بنجاح فورا. وفي الاستضافة المشتركة لا توجد عملية دائمة، فإذا لم يشغّل كرون العامل (worker) فسيكبر جدول jobs فقط ولن تُرسل أي رسالة.

انظر أولا كم مهمة عالقة في الطابور وكم مهمة فشلت:

SQL
SELECT COUNT(*) FROM jobs;
Bash
php artisan queue:failed

إذا كان عدد jobs كبيرا ومتزايدا فمشكلتك ليست في البريد أصلا، بل في الطابور. في cPanel أسهل طريقة هي مهمة كرون قصيرة تشغّل العامل وتفرّغ الطابور وتخرج:

Bash
*/5 * * * * /usr/local/bin/ea-php84 /home/user/app/artisan queue:work --stop-when-empty --max-time=280 >> /home/user/logs/queue.log 2>&1

اكتب المسار المطلق لمفسّر PHP حتما؛ فصدفة كرون غير تفاعلية ولا تعرف الأسماء المستعارة (aliases) في حسابك — وفخ الـ alias في مهمة كرون في cPanel مقال مستقل بذاته.

إذا نجح اختبار MAIL_MAILER=log إلى هنا وكان الطابور يفرغ، فتطبيقك سليم. ومن هنا فصاعدا السؤال هو: لماذا لا يثق بك المستقبِل؟

ثلاث طبقات للتحقق من المرسل: SPF وDKIM وDMARC#

لا يملك بروتوكول البريد الأساسي أي تصور عن «المرسل الحقيقي». يستطيع أي خادم إرسال رسالة مكتوب في حقل From فيها اسم نطاقك. وطبقات SPF وDKIM وDMARC الثلاث صُنعت لسد هذه الثغرة بالضبط، وكل منها يثبت شيئا مختلفا.

مسار البريد من خادم الموقع إلى صندوق المستقبِل ونقاط التفتيش الثلاث SPF وDKIM وDMARC

SPF قائمة تُنشر في DNS الخاص بنطاقك وتحدد الخوادم المسموح لها بإرسال البريد باسم هذا النطاق. يقارن المستقبِل عنوان الخادم المتصل به بهذه القائمة. لا يقول SPF شيئا عن محتوى الرسالة.

DKIM توقيع تشفيري على الرسالة نفسها. يوقّع الخادم المرسل أجزاء من الترويسة والمتن بمفتاح خاص، ويضع المفتاح العام في DNS. يتحقق المستقبِل من التوقيع فيعرف أن الرسالة لم تُعدَّل في الطريق وأنها فعلا من صاحب ذلك المفتاح. وبخلاف SPF ينجو DKIM من إعادة التوجيه (forwarding) أيضا.

DMARC هي الطبقة الثالثة وتؤدي مهمتين. الأولى: تقول إن نتيجة الطبقتين السابقتين يجب أن تتوافق مع النطاق الذي يراه المستخدم في From. والثانية: تقول للمستقبِل ماذا يفعل إذا لم يتحقق هذا التوافق — لا شيء، أو حجر، أو رفض. كما تتيح الحصول على تقارير دورية.

نقطة مهمة يتجاهلها كثيرون: يُفحص SPF على عنوان المغلّف (العنوان المعلن في حوار SMTP)، لا على ما يراه المستخدم في From. قد يختلف الاثنان، وهنا يدخل DMARC ويجعل التوافق إلزاميا.

أنشئ سجل SPF في Zone Editor في cPanel#

في cPanel اذهب إلى Zone Editor، واختر النطاق، وانظر السجلات الموجودة. SPF سجل من نوع TXT على النطاق نفسه، وليس نوع سجل مستقلا.

الاسمالنوعقيمة نموذجية
example.comTXTv=spf1 +mx +a +ip4:198.51.100.7 ~all
default._domainkeyTXTv=DKIM1; k=rsa; p=...
_dmarcTXTv=DMARC1; p=none; rua=mailto:dmarc@example.com

عنوان IP أعلاه مجرد مثال. ضع عنوان IP الصادر لخادمك أنت؛ وإن لم يكن لديك فعادة يكفي +a و+mx في الاستضافة المشتركة لأن خادم الإرسال هو الخادم نفسه الذي يعمل عليه الموقع.

قاعدتان يجعل انتهاكهما سجل SPF لديك بلا أثر أو حتى ضارا:

يُسمح بسجل SPF واحد فقط. إذا كان لديك سجلا TXT يبدأ كلاهما بـ v=spf1 فالنتيجة خطأ دائم، ويعد المستقبِل SPF لديك بأكمله غير صالح — ولا يختار أحدهما. يحدث هذا عادة حين ينشئ cPanel سجلا مرة، ثم تضيف أنت يدويا سجلا لخدمة أخرى. الحل الصحيح هو الدمج: كل الآليات داخل السجل الواحد نفسه.

أكثر من عشر عمليات بحث في DNS تعني الفشل. كل include وa وmx يحتاج إلى عملية بحث في DNS، والسقف الكلي عشر. إن تجاوزت هذا الرقم فستحصل على خطأ دائم أيضا وكأنك لا تملك SPF أصلا. لا تُحتسب ip4 وip6 عمليات بحث، لذا فاستبدال عدة include بعناوين IP مباشرة هو طوق النجاة.

أما نهاية السجل: ~all تعني «أي خادم آخر مشبوه» و-all تعني «غير مسموح قطعا». إذا لم تكن متأكدا أنك أحصيت كل مسارات الإرسال لديك فابدأ بـ ~all؛ يمكن التشديد لاحقا، أما استرجاع الرسائل الضائعة فلا.

تنبيه: تغيير سجل DNS ليس فوريا. بحسب قيمة TTL والذاكرات المؤقتة الوسيطة قد يستغرق الأمر عدة ساعات حتى ترى كل الجهات المستقبِلة القيمة الجديدة. بعد كل تغيير انتظر ثم اختبر؛ وإلا فأنت ترى نتيجة القيمة القديمة وتغيّر الإعدادات الصحيحة بلا سبب.

فعّل DKIM من Email Deliverability في cPanel#

في cPanel صفحة اسمها Email Deliverability صُنعت لهذا الغرض: قائمة بالنطاقات مع حالة كل منها، وزر للإصلاح التلقائي ينشئ السجلات اللازمة بنفسه إذا كان DNS يُدار على الخادم نفسه. بالدخول إلى Manage ترى القيمة الكاملة لسجلي DKIM وSPF المقترحين. ينشر DKIM في cPanel عادة بالمحدِّد (selector) default، أي أن اسم السجل هو default._domainkey.

وهنا فخ شائع يكون كثيرا هو السبب الحقيقي: إذا كان DNS نطاقك يُدار في مكان آخر فإن cPanel ينشئ السجل لكنه لا يُنشر أبدا. عليك نسخ القيمة من الصفحة نفسها وإنشاؤها يدويا في لوحة إدارة DNS الفعلية لديك. وإلا ستعرض cPanel الحالة ناقصة إلى الأبد وتبحث أنت عن مشكلة في الخادم ليست هناك.

للتأكد، افحص نشر السجلات الثلاثة من الخارج:

Bash
dig +short TXT example.com
dig +short TXT default._domainkey.example.com
dig +short TXT _dmarc.example.com

إذا لم يكن dig متاحا فالأمر host -t TXT example.com يؤدي الغرض نفسه. أجرِ هذا الفحص من خارج الخادم؛ فقد يرى خادمك النسخة المحلية من المنطقة (zone) لا ما يراه العالم.

ابدأ DMARC بـ p=none لا بالرفض#

الإغراء الطبيعي هو وضع p=reject من البداية لينتهي الأمر. هذا خطير: ما لم تتأكد أن كل مسارات إرسالك (الموقع، لوحة الدعم، النشرة البريدية، نموذج الاتصال) تُوقَّع وتتوافق بشكل صحيح، فإن p=reject يعني أن المستقبِلين سيتخلصون من رسائلك الحقيقية أيضا ولن تعلم بشيء.

المسار الصحيح من ثلاث مراحل. ابدأ أولا بسياسة «لا شيء» واكتفِ بالتقارير:

INI
v=DMARC1; p=none; rua=mailto:dmarc@example.com; fo=1

هذا السجل من نوع TXT واسمه _dmarc على نطاقك. بهذا الإعداد لن تُرفض أي رسالة بسبب DMARC، لكن المستقبِلين سيرسلون تقارير دورية بعدد الرسائل التي رأوها باسم نطاقك وكم منها اجتاز SPF وDKIM. التقارير ملفات XML وقراءتها يدويا تحتاج إلى صبر، لكن الأسابيع القليلة الأولى تكشف أي مسار إرسال نسيته.

بعد أن تُظهر التقارير أن كل حركتك الحقيقية تقريبا تجتاز الفحص، انتقل إلى p=quarantine ثم، عند الاطمئنان، إلى p=reject. ويتيح المعامل pct تطبيق السياسة على نسبة من الرسائل فقط. وإذا أردت توافقا صارما أضف adkim=s وaspf=s، لكن الوضع الافتراضي المتساهل يكفي لمعظم المتاجر.

الإعداد الصحيح لإرسال البريد في لارافيل: smtp أم sendmail#

في الاستضافة المشتركة لديك خياران، والاختيار الخاطئ قد يستهلك ساعات.

يستخدم MAIL_MAILER=sendmail الملف التنفيذي المحلي في الخادم. وهو بسيط، لكن يجب أن يستطيع PHP لتشغيله إطلاق عملية خارجية. في كثير من الاستضافات المشتركة تكون دوال تشغيل العمليات مغلقة في disable_functions والنتيجة خطأ مبهم لا علاقة له بالبريد. وإذا صادفت هذه الحالة فإن قيود disable_functions في الاستضافة المشتركة هي أول ما يجب فحصه.

أما MAIL_MAILER=smtp فيمكن التنبؤ به أكثر لأنه يستخدم حساب بريد حقيقيا على الخادم نفسه، وأخطاؤه مقروءة:

INI
MAIL_MAILER=smtp
MAIL_HOST=mail.example.com
MAIL_PORT=465
MAIL_USERNAME=no-reply@example.com
MAIL_PASSWORD="كلمة مرور حساب البريد"
MAIL_SCHEME=smtps
MAIL_FROM_ADDRESS="no-reply@example.com"
MAIL_FROM_NAME="${APP_NAME}"

بشأن المنفذ والتشفير: المنفذ 465 يُنشئ التشفير منذ بداية الاتصال، أما المنفذ 587 فيبدأ باتصال عادي ثم يرقّيه. الاثنان غير قابلين للتبديل؛ وخلط الاثنين ينتهي عادة بقطع صامت للاتصال أو بانتهاء المهلة. واسم المفتاح نفسه يختلف بحسب الإصدار: في الإصدارات الأحدث من لارافيل يقرأ الإعداد الافتراضي MAIL_SCHEME (القيمة smtps للمنفذ 465 وتركه فارغا للمنفذ 587)، وفي الإصدارات الأقدم MAIL_ENCRYPTION بقيمة ssl أو tls. والمرجع القاطع هو ملف config/mail.php في مشروعك — انظر أي متغير بيئة يقرؤه فعلا وضعه في .env؛ فالمفتاح غير الموجود في الإعدادات يُتجاهل بصمت.

بعد كل تغيير لا تنسَ php artisan config:clear.

عنوان المرسل يجب أن يكون على نطاقك، لا على Gmail#

كانت هذه الحالة الوحيدة التي ساء مزاجنا حين اكتشفناها. كانت قيمة MAIL_FROM_ADDRESS في المشروع مضبوطة على عنوان Gmail — ربما بقيت من الأيام الأولى للتطوير.

انظر كيف يبدو هذا المزيج من وجهة نظر المستقبِل. يتصل خادم استضافتك ويسلّم رسالة مكتوب في From فيها عنوان Gmail. يطرح المستقبِل ثلاثة أسئلة ويحصل على جواب سيئ لكل منها. هل هذا الخادم في قائمة SPF لذلك النطاق؟ لا، لأنك لا تملك ذلك النطاق ولا تستطيع تغيير سجلاته. هل وُقّعت الرسالة بمفتاح ذلك النطاق؟ لا. هل التوافق متحقق؟ قطعا لا.

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

الحل سطر واحد: ضع عنوان المرسل على نطاقك. وإذا أردت أن تصل الردود إلى صندوقك الشخصي فاستخدم Reply-To — هذا الحقل لا دور له في التحقق من الهوية وهو حر:

PHP
public function envelope(): Envelope
{
    return new Envelope(
        subject: 'فاتورة مشترياتك',
        replyTo: ['shop.support@gmail.com'],
    );
}

وكذلك أنشئ صندوقا حقيقيا لعنوان المرسل. فعنوان مرسل لا وجود له ولا يقبل الرسائل المرتدة هو بحد ذاته إشارة سلبية.

أرسل البريد عبر الطابور لكي لا ينتظر المستخدم SMTP#

حين تُجري الإرسال مباشرة داخل دورة الطلب يبقى المستخدم منتظرا حتى نهاية حوار SMTP. وإذا كان خادم البريد بطيئا أو انتهت مهلة الاتصال، فإن دفعة المستخدم الناجحة تماما تنتهي بخطأ 500.

الطريقة الصحيحة هي إسناد الإرسال إلى الطابور:

PHP
class InvoiceMail extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;
}

أو استخدم في موضع الاستدعاء Mail::to($user)->queue(new InvoiceMail($order)).

لكن التحذير نفسه من القسم الثاني ينطبق هنا: طابور بلا عامل يعني لا رسائل. أنشئ مهمة كرون للعامل في اللحظة نفسها وتأكد أنها تعمل بملاحظة امتلاء جدول jobs وتفريغه. وقد جمعنا بقية هذه الإعدادات الأولية في قائمة التحقق للاستضافة المشتركة مع لارافيل.

عوامل أخرى تدفع البريد إلى المزعج#

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

العامللماذا يهم
غياب النسخة النصيةالرسالة بصيغة HTML فقط نمط شائع في الرسائل الدعائية
الرابط المختصرتبقى الوجهة الحقيقية مخفية فتحصل على نقاط سلبية
عنوان IP مشترك سيئ السمعةجيرانك على الخادم نفسه أرسلوا رسائل مزعجة
محتوى كله صورلا يبقى نص للتحليل فتُعد مشبوهة
غياب رابط إلغاء الاشتراكشبه حتمي في الرسائل الجماعية

النسخة النصية في لارافيل عملها سطر واحد:

PHP
public function content(): Content
{
    return new Content(
        view: 'emails.invoice',
        text: 'emails.invoice_plain',
    );
}

في البريد التعاملي لا يوجد أي سبب لاختصار العناوين؛ ضع العنوان الكامل على نطاقك. وإذا كان نطاق الرابط هو نفسه نطاق المرسل فستحصل على نقاط أفضل أيضا.

سمعة عنوان IP الصادر في الاستضافة المشتركة ليست بيدك. فإذا كان كل شيء صحيحا ومع ذلك تقع الرسائل باستمرار في المزعج فهذا احتمال جدي، وحله ليس في الإعدادات؛ فإما أن تتحدث مع دعم الاستضافة أو تسند الإرسال إلى خدمة إرسال بريد مخصصة — وفي هذه الحالة أيضا يجب أن تنشر سجلات SPF وDKIM الخاصة بتلك الخدمة على نطاقك لكي يبقى التوافق قائما.

ولكل رسالة جماعية — نشرة بريدية أو إعلان خصم أو تذكير بسلة المشتريات — ضع رابط إلغاء الاشتراك. غيابه يزيد شكاوى المستخدمين ويؤثر مباشرة في سمعة النطاق. أما الفاتورة واستعادة كلمة المرور فلا ينطبق عليهما هذا الشرط.

قِس النتيجة: اقرأ ترويسة Authentication-Results#

بعد كل تغيير لا تخمّن. أرسل رسالة حقيقية إلى صندوق حقيقي وافتح ترويسات الرسالة المستلمة. في Gmail اختر من قائمة الرسالة نفسها خيار عرض الأصل (Show original)؛ وفي معظم العملاء الآخرين خيار بالمعنى نفسه.

ابحث عن ترويسة اسمها Authentication-Results. يضيف هذه الترويسة خادم المستقبِل وتذكر بدقة نتيجة الفحوص الثلاثة.

مقارنة بين ترويستين Authentication-Results، إحداهما بنتيجة نجاح والأخرى بنتيجة فشل

انظر إلى ثلاثة أشياء: يجب أن تكون نتيجة SPF هي pass؛ ونتيجة DKIM pass ونطاق الموقِّع هو نطاقك؛ ونتيجة DMARC pass والنطاق بجانبها هو النطاق نفسه الذي يراه المستخدم في From.

إذا أعطى DKIM النتيجة none فالرسالة لم تُوقَّع أو لم يُعثر على المفتاح العام في DNS. وإذا أعطى SPF النتيجة fail فالخادم الذي سلّم الرسالة ليس في قائمتك. وإذا فشل DMARC بينما نجح الاثنان الآخران فالمسألة شبه دائما مسألة توافق.

تذكير آخر: هناك فاصل بين حفظ السجل ورؤية أثره. إذا كانت الترويسة ما زالت تقول fail فانتظر بضع ساعات قبل تغيير آخر وكرر الاختبار نفسه.

الدرس الأساسي#

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

وإذا أخذت من هذا المقال شيئا واحدا فقط: ضع عنوان المرسل على نطاقك. فـ SPF وDKIM وDMARC لها معنى حين يكون النطاق الذي يراه المستخدم هو النطاق الذي تملكه. وبدون ذلك تكون بقية الأعمال مجرد سجلات لا ينظر إليها أحد.