الأدلة التقنية

كيف يعمل البريد الإلكتروني؟ SMTP وIMAP للمبتدئين

ماذا يحدث بين ضغطك على «إرسال» ووصول الرسالة، ولماذا ترتد بعض الرسائل، وأي منفذ تفتح لكل غرض.

كيف يعمل البريد الإلكتروني؟ SMTP وIMAP للمبتدئين

تكتب رسالة في برنامج البريد وتضغط «إرسال»، فتصل إلى صاحبها في Gmail خلال ثوان، وفي يوم آخر تعود إليك بعد ساعات برسالة إنجليزية طويلة فيها أرقام مثل 550 5.1.1، أو تبقى «معلقة» دون أن تعرف أين هي. ثم تقرأ دليل تثبيت خادم بريد فيطلب منك أن تفتح المنافذ 25 و587 و993، وأن تتأكد من أن مزود السيرفر لا يحجب المنفذ 25، وأن تضبط سجل MX، وكل ذلك دون أن يشرح لماذا يحتاج البريد كل هذه الأبواب. وهذا المقال هو الأساس الذي تبنى عليه بقية مقالات البريد على الموقع: كيف تنتقل الرسالة فعلاً من جهاز إلى جهاز، وما الذي يقوله كل طرف للآخر، وأي منفذ لأي غرض، ثم نرى ذلك كله بأعيننا في مختبر صغير على جهازك لا تخرج منه أي رسالة إلى الإنترنت. وإذا كنت تبني بريد مؤسستك فالصورة الكاملة والقرار في دليل كيف تستضيف بريد مؤسستك بنفسك.

وسوف نناقش في هذا المقال ما يلي:

  • الطريق الذي تقطعه رسالة واحدة من المرسل إلى المستلم، والبرامج التي تمر بها وأسماؤها: MUA وMSA وMTA وMDA، ودور سجل MX.
  • بروتوكول SMTP كمحادثة نصية: الأوامر EHLO وMAIL FROM وRCPT TO وDATA، وأرقام الردود، ولماذا يعني الرد 4xx «حاول لاحقاً» والرد 5xx «لا تحاول أبداً».
  • الطابور وإعادة المحاولة Queue and Retry، والرسائل المرتدة Bounces، والفرق بين عنوان الظرف Envelope وعنوان الرسالة Header.
  • المنافذ 25 و587 و465 و993 و995 و143 و110، والفرق بين STARTTLS والتشفير المباشر Implicit TLS، ولماذا يحجب مزودو السيرفرات المنفذ 25.
  • قراءة البريد عبر IMAP وPOP3 وJMAP، والفرق بينها في الواقع اليومي.
  • إعادة استخدام اتصال SMTP لإرسال رسائل كثيرة، في كود تطبيقك وفي Postfix نفسه، وحدود السيرفرات المستقبلة التي تجعل الاتصالات الكثيرة مشكلة.
  • مختبر حقيقي بـ Postfix وDovecot وMailpit، ثم أدوات الفحص، والأخطاء الشائعة، وقائمة تحقق.

ماذا يحدث بعد أن تضغط «إرسال»؟

البريد الإلكتروني يشبه البريد العادي أكثر مما تتوقع، فأنت لا تسلم الرسالة لصاحبها يداً بيد، وإنما تضعها في مكتب البريد القريب منك، وهذا المكتب يعرف من الدليل أين مكتب البريد في مدينة المستلم فيرسلها إليه، ثم يضعها ذلك المكتب في صندوق المستلم، وصاحب الصندوق يفتحه متى شاء. وكل خطوة من هذه الخطوات يقوم بها برنامج مختلف له اسم في المعيار RFC 5598 الذي يصف بنية البريد على الإنترنت، والصورة التالية تبين الطريق الذي تقطعه رسالة من Alice في النطاق example.org إلى Bob في النطاق example.net:

مخطط من اليمين إلى اليسار: 1 جهاز Alice يرسل عبر المنفذ 587 مع كلمة المرور وSTARTTLS إلى سيرفر mx.example.org، 2 السيرفر يسأل DNS عن سجل MX للنطاق example.net فيعرف أنه mx.example.net، 3 يتصل به على المنفذ 25 دون كلمة مرور، 4 سيرفر المستلم يسلم الرسالة عبر LMTP إلى صندوق Bob في Dovecot، 5 هاتف Bob يقرأ الصندوق عبر IMAP على المنفذ 993، وتحتها بطاقات الأدوار MUA وMSA وMTA وMDA وبروتوكولات القراءة
خمس خطوات وخمسة أدوار، وفي المؤسسة الصغيرة يقوم برنامج واحد بأكثر من دور

وفيما يلي الأدوار بالترتيب كما في الصورة:

  • برنامج البريد Message User Agent، واختصاراً MUA: البرنامج الذي يكتب فيه الإنسان رسالته ويقرأ بريده، مثل Thunderbird وOutlook وتطبيق البريد في الهاتف والبريد عبر الويب Webmail. ولاحظ أن تطبيقك الذي يرسل رسالة «تأكيد الطلب» يقوم بنفس الدور تماماً.
  • وكيل الاستلام من المستخدمين Mail Submission Agent، واختصاراً MSA: البوابة التي يسلم إليها برنامج البريد رسالته، ويعرفها المعيار RFC 6409 على المنفذ 587، وأهم ما يميزها أنها تطلب تسجيل الدخول Authentication قبل أن تقبل أي رسالة، والسبب أنها تقبل رسائل إلى أي مكان في العالم، فلو قبلت من أي شخص لأصبح سيرفرك مرحلاً مفتوحاً Open Relay يرسل منه كل مرسلي الرسائل المزعجة Spam.
  • وكيل نقل البريد Mail Transfer Agent، واختصاراً MTA: البرنامج الذي ينقل الرسالة من سيرفر إلى سيرفر عبر بروتوكول نقل البريد البسيط Simple Mail Transfer Protocol، واختصاراً SMTP، على المنفذ 25، مثل Postfix وExim. وفي أغلب الخوادم يكون ال MSA وال MTA برنامجاً واحداً يستمع على عدة منافذ، كما في المختبر الذي سوف نبنيه.
  • وكيل التسليم Mail Delivery Agent، واختصاراً MDA: البرنامج الذي يكتب الرسالة في صندوق المستخدم على القرص، وفي المختبر هو Dovecot الذي يستلم الرسالة من Postfix عبر بروتوكول LMTP، وهو نسخة من SMTP مخصصة للتسليم المحلي.
  • بروتوكولات القراءة IMAP وPOP3 وJMAP: الطريقة التي يقرأ بها برنامج البريد عند Bob صندوقه من السيرفر، وسوف نخصص لها قسماً كاملاً.

كيف يعرف السيرفر أين يرسل؟ سجل MX

في الخطوة 2 يحتاج سيرفر Alice أن يعرف أي سيرفر يستقبل بريد example.net، فيسأل نظام DNS عن سجل MX لهذا النطاق، وقد شرحنا هذا السجل وطريقة نشره في مقال شرح DNS وسجلاته. والمثال التالي يبين سجلات MX للنطاق gmail.com كما يراها أي سيرفر يرسل إلى Gmail:

dig +short MX gmail.com

والمخرج سوف يكون كما يلي:

5 gmail-smtp-in.l.google.com.
40 alt4.gmail-smtp-in.l.google.com.
10 alt1.gmail-smtp-in.l.google.com.
20 alt2.gmail-smtp-in.l.google.com.
30 alt3.gmail-smtp-in.l.google.com.

في المخرج أعلاه لاحظ التالي:

  • الرقم قبل كل اسم هو الأولوية Preference، والسيرفر المرسل يبدأ بالرقم الأصغر، أي gmail-smtp-in.l.google.com، فإذا لم يرد ينتقل إلى الذي بعده، وبهذا الشكل يكون للنطاق سيرفرات احتياطية دون أي إعداد في الطرف المرسل.
  • الترتيب الذي يطبعه dig ليس مهماً، فالمهم هو الرقم.
  • إذا لم يكن للنطاق سجل MX أصلاً، فإن المعيار RFC 5321 يجعل السيرفر المرسل يعامل سجل A أو AAAA للنطاق نفسه كأنه سجل MX ضمني Implicit MX، لذلك فالنطاق الذي لا يستقبل بريداً أبداً ينشر ما يسمى Null MX، وهو سجل MX بأولوية 0 واسمه نقطة فقط .، فيعرف كل مرسل أنه لا يوجد سيرفر بريد لهذا النطاق ولا يحاول.

نبني نسخة صغيرة من هذا الطريق على جهازك

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

  • lab-mail-mta: سيرفر Postfix للنطاق example.org، يستقبل من السيرفرات على المنفذ 25، ومن المستخدمين على 587 و465.
  • lab-mail-dovecot: سيرفر Dovecot يحفظ صناديق example.org، ويستلم من Postfix عبر LMTP، ونقرأ منه عبر IMAP وPOP3.
  • lab-mail-pit: برنامج Mailpit، وهو سيرفر SMTP للاختبار يستقبل كل رسالة ويعرضها في واجهة ويب دون أن يرسلها لأحد، ويقوم هنا بدور «السيرفر الآخر على الإنترنت» للنطاق example.net، وفيه ميزة Chaos التي تجعله يرد بأخطاء نختارها، وهذا ما نحتاجه لنرى الطابور والرسائل المرتدة.
  • lab-mail-client: جهازك أو تطبيقك، وفيه أدوات الفحص swaks وopenssl وdig وnc وPython.

أنشئ مجلداً للمختبر وشهادة ذاتية التوقيع Self-signed Certificate للسيرفر الذي يمثله Mailpit:

mkdir -p mail-lab/{postfix,client,scripts,certs} && cd mail-lab
openssl req -x509 -newkey rsa:2048 -nodes -days 30 -subj /CN=mx.example.net \
  -keyout certs/key.pem -out certs/cert.pem

ثم الملف compose.yaml:

name: lab-mail

services:
  mailpit:                         # stands in for "the other server on the internet"
    image: axllent/mailpit:v1.31.4
    container_name: lab-mail-pit
    hostname: mx.example.net
    command:
      - --smtp-tls-cert=/certs/cert.pem
      - --smtp-tls-key=/certs/key.pem
      - --smtp-auth-accept-any
      - --smtp-allowed-recipients=^[a-z0-9]+@example\.net$$
      - --enable-chaos
      - --smtp-disable-rdns
    volumes:
      - ./certs:/certs:ro
    ports:
      - "127.0.0.1:18680:8025"     # web UI and API
    networks:
      lab:
        aliases: [mx.example.net]

  dovecot:                         # mailboxes of example.org: LMTP in, IMAP and POP3 out
    image: dovecot/dovecot:2.4.5-dev
    container_name: lab-mail-dovecot
    hostname: imap.example.org
    environment:
      USER_PASSWORD: labpass       # lab only: any user, this password
    volumes:
      - ./dovecot-lab.conf:/etc/dovecot/conf.d/lab.conf:ro
    networks:
      lab:
        aliases: [imap.example.org]

  postfix:                         # the MTA of example.org: port 25, 587 and 465
    build: ./postfix
    container_name: lab-mail-mta
    hostname: mx.example.org
    depends_on: [dovecot, mailpit]
    networks:
      lab:
        aliases: [mx.example.org]

  client:                          # your laptop or your application
    build: ./client
    container_name: lab-mail-client
    hostname: laptop.example.org
    command: sleep infinity
    volumes:
      - ./scripts:/lab:ro
    networks:
      - lab

networks:
  lab:
    name: lab-mail-net

في الإعداد أعلاه لاحظ التالي:

  • كل Container له اسم مستعار Alias على الشبكة باسم سيرفر حقيقي الشكل، مثل mx.example.net، فيتصل Postfix به بالاسم كما يتصل بأي سيرفر على الإنترنت، والفرق الوحيد أننا نوجهه إليه مباشرة بدل البحث عن سجل MX كما سيأتي.
  • الخيار --smtp-allowed-recipients يجعل Mailpit يرفض أي مستلم لا يطابق النمط، فالعنوان [email protected] مقبول والعنوان [email protected] مرفوض لأن فيه نقطة، وهكذا نحصل على رفض دائم حقيقي عندما نحتاجه. والعلامة $$ لأن Compose يعامل $ كبداية متغير.
  • كلمة المرور labpass في Dovecot تقبل أي اسم مستخدم، وهذا إعداد اختبار فقط تقدمه وثائق Docker Image الرسمية لـ Dovecot، ولا يصلح لأي سيرفر حقيقي.

ال Docker Image الرسمية لـ Dovecot 2.4 تعمل دون صلاحيات root وتستمع على منافذ غير معتادة مثل 31143 لـ IMAP و31993 لـ IMAPS، ولا تفعل POP3 افتراضياً، ونضيف لها الملف dovecot-lab.conf:

# Plain LMTP for Postfix inside the lab network (the image only offers LMTP over TLS on 31024)
service lmtp {
  inet_listener lmtp {
    port = 24
  }
}
# Postfix asks Dovecot to check passwords on 587 and 465 (SASL)
service auth {
  inet_listener sasl {
    port = 12345
  }
}
# POP3 is not enabled by default in the image
protocols {
  pop3 = yes
}

أما Postfix فنبنيه من حزمة Debian 13، والملف postfix/Dockerfile كما يلي:

FROM debian:13
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      postfix ssl-cert ca-certificates && rm -rf /var/lib/apt/lists/*
COPY start.sh /start.sh
CMD ["sh", "/start.sh"]

والإعداد كله في الملف postfix/start.sh الذي يعمل عند تشغيل ال Container:

#!/bin/sh
set -e
make-ssl-cert generate-default-snakeoil --force-overwrite   # self-signed certificate for mx.example.org
postconf -e \
  'myhostname = mx.example.org' 'mydomain = example.org' 'myorigin = example.org' \
  'mydestination = localhost' 'mynetworks_style = host' \
  'smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination' \
  'virtual_mailbox_domains = example.org' \
  'virtual_transport = lmtp:inet:imap.example.org:24' \
  'transport_maps = inline:{ example.net=smtp:[mx.example.net]:1025 }' \
  'default_transport = error:5.7.1 lab: mail never leaves this machine' \
  'smtpd_sasl_type = dovecot' 'smtpd_sasl_path = inet:imap.example.org:12345' \
  'smtp_tls_security_level = may' 'smtp_tls_loglevel = 1' \
  'maillog_file = /dev/stdout' 'smtp_host_lookup = native' 'lmtp_host_lookup = native'
# 587: STARTTLS required before AUTH, then relay only for logged-in users
postconf -M submission/inet='submission inet n - n - - smtpd'
postconf -P 'submission/inet/syslog_name=postfix/submission' \
  'submission/inet/smtpd_tls_security_level=encrypt' 'submission/inet/smtpd_sasl_auth_enable=yes' \
  'submission/inet/smtpd_relay_restrictions=permit_sasl_authenticated,reject'
# 465: the same, but TLS starts with the connection (implicit TLS)
postconf -M submissions/inet='submissions inet n - n - - smtpd'
postconf -P 'submissions/inet/syslog_name=postfix/submissions' \
  'submissions/inet/smtpd_tls_wrappermode=yes' 'submissions/inet/smtpd_sasl_auth_enable=yes' \
  'submissions/inet/smtpd_relay_restrictions=permit_sasl_authenticated,reject'
# tlsproxy is needed for TLS connection reuse (smtp_tls_connection_reuse), Debian does not list it
postconf -M tlsproxy/unix='tlsproxy unix - - n - 0 tlsproxy'
postconf -F '*/*/chroot = n'
exec postfix start-fg

في الإعداد أعلاه لاحظ التالي:

  • virtual_mailbox_domains وvirtual_transport: بريد example.org يسلم إلى Dovecot عبر LMTP، فهذا السيرفر هو ال MTA الذي يستقبل بريد النطاق.
  • transport_maps: بريد example.net يذهب إلى Mailpit مباشرة، والأقواس المربعة حول الاسم تعني «لا تبحث عن سجل MX لهذا الاسم»، وهذا السطر هو الشيء الوحيد الذي يختلف عن سيرفر حقيقي يبحث عن MX في الخطوة 2.
  • smtp_tls_security_level = may: عندما يرسل Postfix إلى سيرفر آخر يستخدم STARTTLS إذا عرضه السيرفر الآخر، ويرسل دون تشفير إذا لم يعرضه، وهذا ما يسمى التشفير الانتهازي Opportunistic TLS، وسوف نعود إليه في قسم المنافذ.
  • المنفذان 587 و465 يقبلان الإرسال إلى الخارج للمستخدم الذي سجل دخوله فقط، وكلمات المرور يتحقق منها Dovecot لصالح Postfix، وهو نفس التصميم الذي شرحناه في دليل خادم Postfix وDovecot يدوياً، والفرق أن الاتصال بينهما هنا عبر الشبكة وليس عبر ملف Socket.
  • default_transport: أي نطاق غير example.org وexample.net يذهب إلى ناقل الخطأ error، فلو كتبت في المختبر عنواناً حقيقياً بالخطأ يرفضه Postfix فوراً ولا يحاول أن يسلمه، حتى للمستخدم الذي سجل دخوله على 587، والرد يكون كما يلي: 550 5.7.1 <[email protected]>: Recipient address rejected: lab: mail never leaves this machine.
  • هذا الإعداد للمختبر فقط، فلا توجد فيه سجلات DNS ولا DKIM ولا حماية من الرسائل المزعجة، وإذا كنت تريد خادماً حقيقياً فابدأ من الدليل السابق.

وأخيراً الملف client/Dockerfile:

FROM ubuntu:24.04
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      swaks libnet-ssleay-perl libio-socket-ssl-perl libauthen-sasl-perl \
      openssl netcat-openbsd bind9-dnsutils curl python3 ca-certificates && rm -rf /var/lib/apt/lists/*

ولاحظ الحزم الثلاث التي تبدأ بـ lib، فبدونها يعمل swaks ولكنه يرفض STARTTLS بالرسالة TLS not available: requires Net::SSLeay، ولا يستطيع تسجيل الدخول. الآن نشغل المختبر:

docker compose up -d --build
docker compose ps --format 'table {{.Name}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'

والمخرج سوف يكون كما يلي:

NAME               IMAGE                       STATUS                    PORTS
lab-mail-client    lab-mail-client             Up 20 minutes
lab-mail-dovecot   dovecot/dovecot:2.4.5-dev   Up 20 minutes             8080/tcp, 9090/tcp, 31024/tcp, 31110/tcp, 31143/tcp, 31587/tcp, 31993/tcp, 31995/tcp, 34190/tcp
lab-mail-mta       lab-mail-postfix            Up 20 minutes
lab-mail-pit       axllent/mailpit:v1.31.4     Up 20 minutes (healthy)   127.0.0.1:18680->8025/tcp

لاحظ أن منافذ Dovecot مكتوبة دون عنوان، أي أنها معروضة داخل شبكة Docker فقط ولم تنشر على جهازك، وأن Mailpit وحده منشور على 127.0.0.1:18680، فتستطيع أن تفتح http://127.0.0.1:18680 في المتصفح وترى الرسائل التي تصل إليه. وكل الأوامر التالية ننفذها داخل ال Container الذي يمثل جهازك، فادخل إليه بالأمر docker exec -it lab-mail-client bash.

SMTP: محادثة نصية بأوامر قصيرة وردود بأرقام

بروتوكول SMTP الذي يعرفه المعيار RFC 5321 هو محادثة نصية بين برنامجين، يرسل فيها المرسل أمراً قصيراً في سطر، ويرد السيرفر برقم من ثلاث خانات وجملة قصيرة، وهي نفس فكرة HTTP الذي شرحنا فيه أن الطلب والرد رسالتان نصيتان، والفرق أن SMTP محادثة طويلة بعدة أسطر ذهاباً وإياباً وليست طلباً واحداً ورداً واحداً. والصورة التالية تبين المحادثة كاملة كما سوف نراها بعد قليل:

محادثة بين سيرفرين: السيرفر يرحب بالرقم 220، ثم يرسل الطرف الآخر EHLO فيرد 250 مع قائمة الإمكانيات STARTTLS وDSN وCHUNKING، ثم MAIL FROM فيرد 250، ثم RCPT TO لعنوان مقبول فيرد 250، ثم RCPT TO لعنوان في gmail.com فيرد بالأحمر 554 Relay access denied، ثم DATA فيرد 354، ثم الرؤوس والنص وسطر فيه نقطة فيرد 250 queued، ثم QUIT فيرد 221، مع قوس يجمع MAIL FROM وRCPT TO باسم الظرف envelope وقوس على المحتوى content، وتحتها مفتاح الألوان 2xx و3xx و4xx و5xx
أوامر المرسل على اليمين وردود السيرفر على اليسار، والظرف يرسل قبل المحتوى وبأوامر منفصلة

والجدول التالي يجمع الأوامر التي تراها في كل محادثة تقريباً:

الأمرماذا يعني
EHLOالمرسل يعرف بنفسه باسم سيرفره، والسيرفر يرد بقائمة الإمكانيات التي يدعمها، مثل STARTTLS وAUTH وSIZE. والأمر الأقدم HELO يعرف بالنفس دون قائمة.
STARTTLSطلب تحويل الاتصال نفسه إلى اتصال مشفر قبل إكمال المحادثة، كما في RFC 3207.
AUTHتسجيل الدخول باسم مستخدم وكلمة مرور، ويستخدم على منافذ الإرسال 587 و465 وليس بين السيرفرات.
MAIL FROMعنوان المرسل على الظرف، وإليه تعود رسائل الفشل.
RCPT TOعنوان مستلم على الظرف، ويتكرر لكل مستلم، ولكل واحد رده الخاص.
DATAبداية محتوى الرسالة: الرؤوس Headers ثم سطر فارغ ثم النص، وينتهي المحتوى بسطر فيه نقطة واحدة فقط.
RSETإلغاء الرسالة الحالية والبدء من جديد على نفس الاتصال.
QUITإنهاء المحادثة وإغلاق الاتصال.

الآن نرى ذلك على المختبر، فنرسل رسالة إلى [email protected] كأننا سيرفر بريد آخر اسمه mx.example.net، والأداة swaks تكتب كل سطر يذهب وكل سطر يعود:

swaks --server mx.example.org --port 25 --helo mx.example.net \
  --from [email protected] --to [email protected] \
  --h-From "Finance <[email protected]>" --h-Subject "Invoice 1042" \
  --body "Hello Alice, the invoice is attached."

والمخرج سوف يكون كما يلي:

=== Trying mx.example.org:25...
=== Connected to mx.example.org.
<-  220 mx.example.org ESMTP Postfix (Debian)
 -> EHLO mx.example.net
<-  250-mx.example.org
<-  250-PIPELINING
<-  250-SIZE 10240000
<-  250-VRFY
<-  250-ETRN
<-  250-STARTTLS
<-  250-ENHANCEDSTATUSCODES
<-  250-8BITMIME
<-  250-DSN
<-  250-SMTPUTF8
<-  250 CHUNKING
 -> MAIL FROM:<[email protected]>
<-  250 2.1.0 Ok
 -> RCPT TO:<[email protected]>
<-  250 2.1.5 Ok
 -> DATA
<-  354 End data with <CR><LF>.<CR><LF>
 -> Date: Tue, 06 Oct 2026 04:59:46 +0000
 -> To: [email protected]
 -> From: Finance <[email protected]>
 -> Subject: Invoice 1042
 -> Message-Id: <[email protected]>
 -> X-Mailer: swaks v20240103.0 jetmore.org/john/code/swaks/
 ->
 -> Hello Alice, the invoice is attached.
 ->
 ->
 -> .
<-  250 2.0.0 Ok: queued as AFA1DB069F
 -> QUIT
<-  221 2.0.0 Bye
=== Connection closed with remote host.

في المخرج أعلاه لاحظ التالي:

  • الأسطر التي تبدأ بـ -> أرسلها swaks، والتي تبدأ بـ <- ردود Postfix. والسيرفر هو الذي يبدأ الكلام بالرقم 220 قبل أن يرسل المرسل أي شيء، وإذا اتصلت بسيرفر ولم تر هذا السطر فالمشكلة في الشبكة وليست في البريد.
  • رد EHLO عدة أسطر، وكل سطر غير الأخير فيه شرطة بعد الرقم 250-، والسطر الأخير فيه مسافة 250 CHUNKING، وهكذا يعرف المرسل أن الرد انتهى. ولاحظ أن STARTTLS في القائمة، أي أن السيرفر مستعد للتشفير، وأن AUTH ليس فيها لأن المنفذ 25 لا يقبل تسجيل الدخول في إعدادنا.
  • الرد 354 ليس نجاحاً نهائياً، وإنما «أكمل»، ثم يرسل swaks الرؤوس وسطراً فارغاً والنص، وينتهي المحتوى بسطر فيه نقطة وحدها، فيرد السيرفر 250 2.0.0 Ok: queued as AFA1DB069F. وهذه النقطة البسيطة لها تاريخ أمني، فاختلاف السيرفرات في فهم نهاية المحتوى هو ما استغله هجوم تهريب SMTP الذي شرحناه في دليل Postfix وDovecot.
  • الكلمة queued مهمة، فالسيرفر لم يسلم الرسالة بعد، وإنما قبل مسؤوليتها ووضعها في طابوره برقم AFA1DB069F، وبهذا الرقم تجدها في سجلاته.

وفي سجل Postfix نرى ما حدث بعد ذلك:

docker logs lab-mail-mta 2>&1 | grep AFA1DB069F
Oct 06 04:59:46 mx postfix/smtpd[191]: AFA1DB069F: client=lab-mail-client.lab-mail-net[192.168.80.3]
Oct 06 04:59:46 mx postfix/cleanup[196]: AFA1DB069F: message-id=<[email protected]>
Oct 06 04:59:46 mx postfix/qmgr[189]: AFA1DB069F: from=<[email protected]>, size=478, nrcpt=1 (queue active)
Oct 06 04:59:46 mx postfix/lmtp[197]: AFA1DB069F: to=<[email protected]>, relay=imap.example.org[192.168.80.2]:24, delay=0.06, delays=0.01/0.01/0.02/0.02, dsn=2.0.0, status=sent (250 2.0.0 <[email protected]> XxzyLEKAxGoQAAAA68ePFA Saved)
Oct 06 04:59:46 mx postfix/qmgr[189]: AFA1DB069F: removed

وهذه هي الخطوة 4 من الصورة الأولى: مدير الطابور qmgr أخذ الرسالة، وسلمها البرنامج lmtp إلى Dovecot الذي رد بنفس لغة الأرقام 250 2.0.0 ... Saved، ثم حذفت من الطابور. وإذا أردت أن تكتب المحادثة بيدك حرفاً حرفاً بدل swaks، فالأمر nc -C mx.example.org 25 يفتح الاتصال، والخيار -C يرسل نهاية السطر بالشكل CRLF الذي يطلبه المعيار، ثم تكتب الأوامر نفسها سطراً سطراً.

ماذا تعني أرقام الردود؟

الخانة الأولى من الرد هي أهم ما فيه، فالمرسل يقرر ما يفعله بناء عليها وحدها تقريباً، والمعيار RFC 5321 يقسمها إلى أربعة أنواع:

  • 2xx نجاح Positive Completion: الأمر نفذ، مثل 250 و221.
  • 3xx أكمل Positive Intermediate: السيرفر ينتظر بقية البيانات، والمثال المشهور 354 بعد DATA.
  • 4xx فشل مؤقت Transient Negative: «ليس الآن، حاول لاحقاً»، فالمرسل يحتفظ بالرسالة ويعيد المحاولة، مثل 421 عندما يكون السيرفر مشغولاً و451 عند خطأ مؤقت.
  • 5xx فشل دائم Permanent Negative: «لا تحاول مرة أخرى بنفس الشكل»، فالمرسل يتوقف ويخبر صاحب الرسالة، مثل 550 عندما لا يوجد صندوق بهذا الاسم و554 عند رفض لأسباب السياسة.

وبعد الرقم ترى غالباً رقماً ثانياً بنقطتين مثل 5.7.1، وهو رمز الحالة المفصل Enhanced Status Code من RFC 3463، وقد أعلن عنه السيرفر في السطر ENHANCEDSTATUSCODES. وشكله «النوع.الموضوع.التفصيل»، فالخانة الأولى تكرر النوع نفسه، والثانية موضوع الخطأ: 1 للعنوان Addressing، و2 للصندوق Mailbox، و4 للشبكة والتوجيه Routing، و7 للأمان والسياسة Security or Policy. لذلك عندما ترى 5.1.1 فابحث في العنوان، وعندما ترى 5.7.x فابحث في الصلاحيات أو في سجلات المصادقة مثل SPF وDMARC التي شرحناها في مقال شرح SPF وDKIM وDMARC.

الآن نكسر شيئاً عن قصد، فنطلب من سيرفر example.org، ونحن غير مسجلين، أن يمرر رسالة إلى عنوان في gmail.com، أي أن يكون مرحلاً لنا:

swaks --server mx.example.org --port 25 --helo mx.example.net \
  --from [email protected] --to [email protected] --quit-after RCPT
 -> MAIL FROM:<[email protected]>
<-  250 2.1.0 Ok
 -> RCPT TO:<[email protected]>
<** 554 5.7.1 <[email protected]>: Relay access denied
 -> QUIT
<-  221 2.0.0 Bye

اختصرنا أسطر EHLO لأنها نفسها، والمهم أن السيرفر رفض المستلم برفض دائم 554 5.7.1، وهذا هو السلوك الصحيح للمنفذ 25، فهو يقبل البريد لنطاقاته فقط. وقد يتساءل البعض: لماذا وضعنا في الإعداد السطر smtpd_relay_restrictions ما دام Postfix يرفض أصلاً؟ والإجابة أننا عندما شغلنا المختبر أول مرة دون هذا السطر كان الرد نفسه مؤقتاً:

454 4.7.1 <[email protected]>: Relay access denied

والسبب أن القيمة الافتراضية لهذا الإعداد في وثائق Postfix تنتهي بـ defer_unauth_destination، وهي ترفض نفس الطلبات التي يرفضها reject_unauth_destination ولكن برمز مؤقت، وهذا الرمز المؤقت مفيد وأنت تجهز سيرفراً جديداً، فإذا أخطأت في الإعداد ورفض السيرفر بريداً كان يجب أن يقبله، فالمرسلون يعيدون المحاولة ولا تضيع رسائل، ولكنه على سيرفر يعمل فعلاً تجعل كل من يحاول استغلالك كمرحل يعيد المحاولة أياماً، لذلك بعد أن تتأكد من إعدادك اجعلها رفضاً دائماً كما في دليل Postfix وDovecot.

الطابور وإعادة المحاولة Queue and Retry

هنا الفرق الكبير بين البريد وبين صفحة الويب، فإذا لم يرد سيرفر الويب يظهر لك خطأ وينتهي الأمر، أما البريد فقد صمم ليصل حتى لو كان السيرفر المستقبل متوقفاً ساعات، والسبب أن كل MTA يحتفظ بطابور Queue على القرص، وعندما يتلقى رداً مؤقتاً 4xx أو لا يستطيع الاتصال أصلاً، يبقي الرسالة في الطابور ويعيد المحاولة لاحقاً. ويقول المعيار RFC 5321 في القسم 4.5.4.1 إن الفترة بين المحاولات ينبغي ألا تقل عن 30 دقيقة في العادة، وإن مدة الاستسلام تحتاج غالباً أن تكون 4 إلى 5 أيام على الأقل. والصورة التالية تبين الطريقين، المؤقت والدائم:

صفان: في الأعلى سيرفر mx.example.org يرسل RCPT TO إلى mx.example.net فيرد 451، فتذهب الرسالة إلى الطابور deferred، ثم يعيد المحاولة بين 300 و4000 ثانية ويستسلم بعد 5 أيام، ثم يرد السيرفر 250 فتصل الرسالة دون أن تضيع، وفي الأسفل يرد السيرفر 550 لعدم وجود الصندوق، فينشئ السيرفر المرسل رسالة مرتدة DSN بعنوان MAIL FROM فارغ ويرسلها إلى Return-Path فتصل إلى صندوق Alice بعنوان Undelivered Mail Returned to Sender مع Status 5.1.0 وAction failed
الرد المؤقت يبقي الرسالة في الطابور، والرد الدائم ينشئ رسالة مرتدة إلى صاحبها

ولنرى الطابور بأعيننا نطلب من Mailpit أن يرد على كل مستلم بالرمز المؤقت 451 عبر واجهته البرمجية API:

curl -s -X PUT -H 'Content-Type: application/json' \
  -d '{"Recipient":{"ErrorCode":451,"Probability":100}}' http://mx.example.net:8025/api/v1/chaos
{"Sender":{"ErrorCode":451,"Probability":0},"Recipient":{"ErrorCode":451,"Probability":100},"Authentication":{"ErrorCode":535,"Probability":0}}

ثم نرسل 50 رسالة من «التطبيق» إلى عناوين في example.net عبر منفذ الإرسال 587 بالسكربت send.py الذي سوف نشرحه في قسم إعادة استخدام الاتصال، ونعرض الطابور من داخل Postfix بالأمر postqueue -p، وهو نفس الأمر القديم mailq:

python3 /lab/send.py mx.example.org 587 reuse 50
docker exec lab-mail-mta sh -c 'postqueue -p | head -4; postqueue -p | tail -1'
reuse 50 messages in 0.52 s (10.4 ms per message)
-Queue ID-  --Size-- ----Arrival Time---- -Sender/Recipient-------
28795B0985      403 Tue Oct  6 05:00:55  [email protected]
(host mx.example.net[192.168.80.4] said: 451 Chaos recipient error (in reply to RCPT TO command))
                                         [email protected]
-- 19 Kbytes in 50 Requests.

الأمر docker exec ننفذه من جهازك وليس من داخل lab-mail-client، لأنه يدخل إلى Container آخر. وفي المخرج لاحظ أن التطبيق انتهى في نصف ثانية وكأن كل شيء نجح، فهو سلم الرسائل لسيرفر example.org وأخذ الرد 250، أما الرسائل نفسها فما زالت كلها في الطابور، وبجانب كل واحدة سبب التأجيل كما قاله السيرفر الآخر بالحرف. وفي السجل تجد السطر نفسه بالحالة status=deferred:

Oct 06 05:00:55 mx postfix/smtp[209]: A0EE3B19B3: to=<[email protected]>, relay=mx.example.net[192.168.80.4]:1025, delay=0.06, delays=0.01/0/0.05/0.01, dsn=4.0.0, status=deferred (host mx.example.net[192.168.80.4] said: 451 Chaos recipient error (in reply to RCPT TO command))

وفي Postfix تبدأ الفترة بين المحاولات من minimal_backoff_time وقيمتها الافتراضية 300 ثانية، وتكبر حتى maximal_backoff_time وقيمتها 4000 ثانية، أي حوالي 67 دقيقة، والرسالة التي تبقى مؤجلة أكثر من maximal_queue_lifetime وقيمتها 5 أيام تعتبر غير قابلة للتسليم وترتد إلى صاحبها. ولكي لا ننتظر خمس دقائق نلغي الخطأ ونطلب من Postfix أن يحاول الآن بالأمر postqueue -f:

curl -s -X PUT -H 'Content-Type: application/json' \
  -d '{"Recipient":{"ErrorCode":451,"Probability":0}}' http://mx.example.net:8025/api/v1/chaos
docker exec lab-mail-mta postqueue -f
sleep 5; docker exec lab-mail-mta postqueue -p
Mail queue is empty

وصلت الرسائل الخمسون كلها دون أن يعيد التطبيق إرسال أي شيء، وهذه هي الفائدة الحقيقية من وجود MTA بين تطبيقك والعالم. ويجدر الإشارة هنا إلى أن بعض السيرفرات ترد بالرمز المؤقت عن قصد على كل مرسل لا تعرفه، وهي تقنية التأجيل الرمادي Greylisting التي يصفها RFC 6647، والفكرة أن السيرفر الحقيقي سوف يعيد المحاولة كما يطلب المعيار، بينما كثير من برامج الرسائل المزعجة لا تعيد، لذلك إذا كان تطبيقك يرسل مباشرة إلى سيرفرات المستلمين دون طابور، فكل رد 4xx يعني رسالة ضائعة.

الرسائل المرتدة Bounces وتقارير DSN

الآن الحالة الثانية في الصورة: نرسل من حساب Alice إلى عنوان غير موجود عند Mailpit:

swaks --server mx.example.org --port 587 --tls --auth PLAIN \
  --auth-user [email protected] --auth-password labpass --helo laptop.example.org \
  --from [email protected] --to [email protected] \
  --h-Subject "Meeting tomorrow" --body "See you at 10."

سوف يرد Postfix على swaks بالرد 250 2.0.0 Ok: queued as B647CB1997، أي أن الإرسال «نجح» من وجهة نظر Alice، ثم في السجل نرى ما حدث بعد ذلك:

Oct 06 05:02:12 mx postfix/smtp[291]: B647CB1997: to=<[email protected]>, relay=mx.example.net[192.168.80.4]:1025, delay=0.07, delays=0.01/0/0.05/0.01, dsn=5.1.0, status=bounced (host mx.example.net[192.168.80.4] said: 550 5.1.0 Requested action not taken: mailbox unavailable (in reply to RCPT TO command))
Oct 06 05:02:12 mx postfix/cleanup[280]: C74FDB199C: message-id=<[email protected]>
Oct 06 05:02:12 mx postfix/bounce[317]: B647CB1997: sender non-delivery notification: C74FDB199C
Oct 06 05:02:12 mx postfix/qmgr[273]: C74FDB199C: from=<>, size=2504, nrcpt=1 (queue active)
Oct 06 05:02:12 mx postfix/qmgr[273]: B647CB1997: removed
Oct 06 05:02:12 mx postfix/lmtp[318]: C74FDB199C: to=<[email protected]>, relay=imap.example.org[192.168.80.2]:24, delay=0.05, delays=0.01/0.01/0.02/0.02, dsn=2.0.0, status=sent (250 2.0.0 <[email protected]> UEhaMtSAxGoTAAAA68ePFA Saved)

في السجل أعلاه لاحظ التالي:

  • الرد 550 دائم، فلم يبق Postfix الرسالة في الطابور لإعادة المحاولة، وإنما أنشأ مباشرة رسالة جديدة برقم C74FDB199C تسمى إشعار عدم التسليم Non-Delivery Notification، أو الرسالة المرتدة Bounce.
  • المرسل في الرسالة المرتدة فارغ from=<>، وهذا ما يسمى المرسل الفارغ Null Reverse-Path، ويذكر RFC 5321 أن المعايير تطلب إرسال إشعارات الفشل وتقارير DSN به، والسبب أن الرسالة المرتدة إذا ارتدت هي الأخرى فلا يوجد عنوان تعود إليه، وبالتالي لا تدخل السيرفرات في حلقة من الرسائل المرتدة بلا نهاية.
  • الرسالة المرتدة ذهبت إلى عنوان الظرف [email protected] وسلمت إلى صندوقها، وسوف نقرؤها عبر IMAP في قسم القراءة.

والرسالة المرتدة ليست نصاً حراً، وإنما لها شكل قياسي يسمى إشعار حالة التسليم Delivery Status Notification، واختصاراً DSN، يعرفه RFC 3464، حتى تستطيع البرامج قراءتها وليس الإنسان فقط. وهذه أهم أسطرها كما وصلت إلى صندوق Alice:

docker exec lab-mail-dovecot doveadm fetch -u [email protected] text mailbox INBOX HEADER Subject "Undelivered" \
  | grep -E "^(Return-Path|From|Auto-Submitted|Content-Type|Reporting-MTA|Final-Recipient|Action|Status|Remote-MTA|Diagnostic-Code):"
Return-Path: <>
From: Mail Delivery System <[email protected]>
Auto-Submitted: auto-replied
Content-Type: multipart/report; report-type=delivery-status;
Content-Type: text/plain; charset=utf-8
Content-Type: message/delivery-status
Reporting-MTA: dns; mx.example.org
Final-Recipient: rfc822; [email protected]
Action: failed
Status: 5.1.0
Remote-MTA: dns; mx.example.net
Diagnostic-Code: smtp; 550 5.1.0 Requested action not taken: mailbox
Content-Type: message/rfc822
Return-Path: <[email protected]>
From: [email protected]

الرسالة من ثلاثة أجزاء: نص يقرؤه الإنسان، ثم جزء message/delivery-status بالحقول Action وStatus وDiagnostic-Code التي تقرؤها البرامج، ثم الرسالة الأصلية أو رؤوسها في الجزء message/rfc822. وهذه الحقول هي ما تقرؤه منصات الإرسال لتوقف الإرسال إلى العناوين الميتة، كما شرحنا في دليل Amazon SES.

⚠️
ارفض الرسالة أثناء محادثة SMTP بدلاً من قبولها ثم إرجاعها، والسبب أن الرسالة المرتدة تذهب إلى عنوان الظرف، وهذا العنوان يكتبه المرسل بنفسه، فإذا قبل سيرفرك رسالة مزعجة بعنوان مزور ثم أرجعها، فأنت ترسل رسالة مرتدة لشخص بريء لم يرسل شيئاً، وهذا ما يسمى Backscatter، وقد يضع سيرفرك في قوائم الحظر. لذلك يتحقق السيرفر الجيد من وجود الصندوق عند RCPT TO ويرد بـ 550 مباشرة كما فعل Mailpit هنا.

الظرف والرؤوس: عنوانان للمرسل وعنوانان للمستلم

لاحظت في المحادثة أن العناوين تظهر مرتين: مرة في الأوامر MAIL FROM وRCPT TO، ومرة في الرؤوس From: وTo: بعد DATA، وهذا ليس تكراراً، فالأولى هي الظرف Envelope الذي يعرفه RFC 5321 وتقرؤه السيرفرات لتعرف أين تسلم الرسالة وأين تعيدها إذا فشلت، والثانية هي رؤوس الرسالة التي يعرفها RFC 5322 ويعرضها برنامج البريد للإنسان. والأهم أن البروتوكول لا يطلب أن يتطابقا، والصورة التالية تبين ذلك بقيم حقيقية من المختبر:

ظرف كبير عليه عنوان الظرف حسب RFC 5321: MAIL FROM bounces@example.org وRCPT TO ops@example.net، ويرسل كأوامر SMTP ولا يراه القارئ، وفي داخله ورقة الرسالة برؤوس RFC 5322: From Alice alice@example.org وTo Team team@example.net وSubject Envelope vs headers ثم النص، وسهم من الظرف إلى السيرفرات التي تسلم إلى RCPT TO وترجع إلى MAIL FROM، وسهم من الورقة إلى القارئ الذي يرى From وTo
السيرفرات تسلم حسب الظرف، والقارئ يرى الرؤوس، والاثنان مستقلان تماماً

والمثال التالي يرسل رسالة ظرفها إلى [email protected]، بينما السطر To: فيها يقول [email protected]، وعنوان المرسل على الظرف يختلف عن السطر From:، كما يلي:

swaks --server mx.example.net --port 1025 --helo laptop.example.org \
  --from [email protected] --to [email protected] \
  --h-From "Alice <[email protected]>" --h-To "Team <[email protected]>" \
  --h-Subject "Envelope vs headers" --body "Who is this really for?"
curl -s http://mx.example.net:8025/api/v1/message/latest/raw | grep -E "^(Return-Path|From|To|Subject):"
curl -s http://mx.example.net:8025/api/v1/message/latest | python3 -c "import sys,json;m=json.load(sys.stdin);print('ReturnPath:',m['ReturnPath']);print('From:',m['From']);print('To:',m['To']);print('Bcc:',m['Bcc'])"
Return-Path: <[email protected]>
To: Team <[email protected]>
From: Alice <[email protected]>
Subject: Envelope vs headers
ReturnPath: [email protected]
From: {'Name': 'Alice', 'Address': '[email protected]'}
To: [{'Name': 'Team', 'Address': '[email protected]'}]
Bcc: [{'Name': '', 'Address': '[email protected]'}]

في المخرج أعلاه لاحظ التالي:

  • الرسالة وصلت إلى ops لأنه في الظرف، مع أن اسمه لا يظهر في أي رأس، لذلك اعتبره Mailpit نسخة مخفية Bcc، وهذه بالضبط طريقة عمل النسخة المخفية Blind Carbon Copy في كل برامج البريد: العنوان في RCPT TO فقط، ولا يوجد في الرؤوس، كما يصف RFC 5322.
  • السيرفر الأخير حفظ عنوان الظرف في الرأس Return-Path عند التسليم النهائي كما يطلب RFC 5321، فإذا أردت أن تعرف أين تعود رسائل الفشل لرسالة وصلتك، فانظر إلى هذا الرأس وليس إلى From:.
  • هذا الاستقلال مفيد، فمنصات الإرسال تضع في الظرف عنواناً خاصاً بها لتستقبل الرسائل المرتدة وتعالجها، ولكنه أيضاً ما يجعل انتحال الاسم الظاهر ممكناً، ولهذا جاء DMARC ليربط النطاق في From: بنتائج الفحص، كما شرحنا في مقال SPF وDKIM وDMARC مع مثال انتحال كامل.

وهناك رأس ثالث يضيفه كل سيرفر تمر به الرسالة، وهو Received:، فالمعيار يطلب من كل سيرفر أن يضيف سطراً في أعلى الرسالة فيه اسم من سلمها إليه وعنوانه والوقت. وهذه رؤوس رسالة أرسلتها Alice من 587 إلى [email protected] كما وصلت إلى Mailpit:

Return-Path: <[email protected]>
Received: from mx.example.org (unknown [192.168.80.5])
        by mx.example.net (Mailpit) with SMTP
        for <[email protected]>; Tue, 6 Oct 2026 05:00:07 +0000 (UTC)
Received: from laptop.example.org (lab-mail-client.lab-mail-net [192.168.80.3])
	by mx.example.org (Postfix) with ESMTPSA id 1A4FEB095A
	for <[email protected]>; Tue, 06 Oct 2026 05:00:07 +0000 (UTC)

اقرأ الأسطر Received: من الأسفل إلى الأعلى، فالأسفل هو أول خطوة: من جهاز laptop.example.org إلى mx.example.org، ثم الأعلى منه من mx.example.org إلى mx.example.net. والكلمة ESMTPSA من RFC 3848 تعني أن الخطوة الأولى كانت مشفرة S ومسجلة الدخول A، وهذه الأسطر هي أول ما تقرؤه عندما تتأخر رسالة، لأن الوقت في كل سطر يبين أين انتظرت.

المنافذ والتشفير: أي باب لأي غرض؟

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

مخطط في وسطه سيرفر mx.example.org، على يساره سحابة سيرفرات البريد الأخرى على المنفذ 25 مع قفل أصفر يعني STARTTLS إذا عرضه الطرفان، وملاحظة أن MTA-STS وDANE تجعله إلزامياً، وبطاقة حمراء أن كثيراً من مزودي السيرفرات يحجبون المنفذ 25 الصادر، وعلى يمينه منافذ المستخدمين: 465 للإرسال بتشفير مباشر توصي به RFC 8314، و587 للإرسال مع STARTTLS قبل تسجيل الدخول، و993 IMAPS، و143 IMAP مع STARTTLS، و995 POP3S، و443 JMAP، وفي الأسفل مقارنة بين STARTTLS الذي يبدأ بتحية مكشوفة ثم يتحول إلى TLS وبين التشفير المباشر من أول بايت
منفذ واحد للسيرفرات، ومنافذ للإرسال والقراءة لمستخدميك، والفرق في متى يبدأ التشفير
المنفذالاسممن يستخدمهالتشفيرتسجيل الدخول
25smtpالسيرفرات فيما بينهاSTARTTLS إذا عرضه الطرفانلا يوجد
587submissionبرامج البريد والتطبيقات للإرسالSTARTTLS، وسيرفرك يجب أن يطلبهإلزامي
465submissionsبرامج البريد والتطبيقات للإرسالتشفير مباشر Implicit TLSإلزامي
993imapsالقراءة عبر IMAPتشفير مباشرإلزامي
143imapالقراءة عبر IMAP للبرامج القديمةSTARTTLSإلزامي
995pop3sالتنزيل عبر POP3تشفير مباشرإلزامي
110pop3التنزيل عبر POP3 للبرامج القديمةSTARTTLSإلزامي
443httpsJMAP والبريد عبر الويبHTTPSإلزامي

الأسماء في العمود الثاني هي أسماء الخدمات المسجلة في سجل IANA للمنافذ، ومنها submission وsubmissions اللذان كتبناهما لـ Postfix في start.sh.

STARTTLS أم التشفير المباشر Implicit TLS؟

هناك طريقتان لتشفير اتصال البريد. في الطريقة الأولى STARTTLS يبدأ الاتصال مكشوفاً، ويرسل الطرفان التحية وEHLO نصاً واضحاً، ثم يطلب المرسل STARTTLS فيتحول الاتصال نفسه إلى اتصال مشفر، وفي الطريقة الثانية يبدأ التشفير مع أول بايت على منفذ مخصص لذلك، تماماً كما يحدث في HTTPS الذي شرحناه في مقال HTTP وHTTPS. والمثال التالي يبين STARTTLS على المنفذ 587، فنحاول أولاً أن نرسل دون تشفير:

swaks --server mx.example.org --port 587 --helo laptop.example.org \
  --from [email protected] --to [email protected] --quit-after MAIL
 -> MAIL FROM:<[email protected]>
<** 530 5.7.0 Must issue a STARTTLS command first

اختصرنا أسطر EHLO مرة أخرى، والمهم الرد: رفض دائم، وهذا ما نريده تماماً من منفذ الإرسال، لأن كلمة المرور سوف تمر عليه. والآن مع --tls وتسجيل الدخول:

swaks --server mx.example.org --port 587 --tls --auth PLAIN \
  --auth-user [email protected] --auth-password labpass --helo laptop.example.org \
  --from [email protected] --to [email protected] \
  --h-From "Alice <[email protected]>" --h-Subject "Weekly report" --body "The weekly report is ready."

والمخرج سوف يكون كما يلي، واختصرنا منه سطري الاتصال الأولين وأسطر المحتوى بعد DATA:

<-  220 mx.example.org ESMTP Postfix (Debian)
 -> EHLO laptop.example.org
<-  250-mx.example.org
<-  250-PIPELINING
<-  250-SIZE 10240000
<-  250-VRFY
<-  250-ETRN
<-  250-STARTTLS
<-  250-ENHANCEDSTATUSCODES
<-  250-8BITMIME
<-  250-DSN
<-  250-SMTPUTF8
<-  250 CHUNKING
 -> STARTTLS
<-  220 2.0.0 Ready to start TLS
=== TLS started with cipher TLSv1.3:TLS_AES_256_GCM_SHA384:256
=== TLS client certificate not requested and not sent
=== TLS no client certificate set
=== TLS peer[0]   subject=[/CN=mx.example.org]
===               commonName=[mx.example.org], subjectAltName=[DNS:mx.example.org] notAfter=[2036-10-03T04:59:13Z]
=== TLS peer certificate failed CA verification (self-signed certificate), passed host verification (using host mx.example.org to verify)
 ~> EHLO laptop.example.org
<~  250-mx.example.org
<~  250-PIPELINING
<~  250-SIZE 10240000
<~  250-VRFY
<~  250-ETRN
<~  250-AUTH PLAIN
<~  250-ENHANCEDSTATUSCODES
<~  250-8BITMIME
<~  250-DSN
<~  250-SMTPUTF8
<~  250 CHUNKING
 ~> AUTH PLAIN AGFsaWNlQGV4YW1wbGUub3JnAGxhYnBhc3M=
<~  235 2.7.0 Authentication successful
 ~> MAIL FROM:<[email protected]>
<~  250 2.1.0 Ok
 ~> RCPT TO:<[email protected]>
<~  250 2.1.5 Ok
 ~> DATA
<~  354 End data with <CR><LF>.<CR><LF>
<~  250 2.0.0 Ok: queued as 1A4FEB095A
 ~> QUIT
<~  221 2.0.0 Bye

في المخرج أعلاه لاحظ التالي:

  • بعد STARTTLS تتغير الأسهم من -> إلى ~>، أي أن ما بعدها مشفر، ويرسل swaks الأمر EHLO مرة ثانية لأن المعيار يلغي كل ما عرفه الطرفان قبل التشفير.
  • AUTH PLAIN لم يظهر في القائمة قبل التشفير، وظهر بعده، فسيرفر الإرسال الصحيح لا يعرض تسجيل الدخول على اتصال مكشوف أصلاً. والنص بعد AUTH PLAIN ليس تشفيراً وإنما ترميز Base64 لاسم المستخدم وكلمة المرور، ويستطيع أي شخص أن يفكه، ولهذا بالضبط يجب أن يكون داخل TLS.
  • swaks فشل في التحقق من الشهادة لأنها ذاتية التوقيع في المختبر، وأكمل لأنه أداة فحص، أما برنامج البريد الحقيقي فيجب أن يرفض، والشهادة الحقيقية تصدرها من Let's Encrypt كما في أدلة الخوادم.

أما التشفير المباشر على المنفذ 465 فيظهر بوضوح مع openssl s_client، فالتحية 220 نفسها لا تصل إلا بعد اكتمال TLS:

openssl s_client -connect mx.example.org:465 -brief -crlf
depth=0 CN = mx.example.org
verify error:num=18:self-signed certificate
CONNECTION ESTABLISHED
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Peer certificate: CN = mx.example.org
Hash used: SHA256
Signature type: RSA-PSS
Verification error: self-signed certificate
Server Temp Key: X25519, 253 bits
220 mx.example.org ESMTP Postfix (Debian)
250-mx.example.org
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH PLAIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING

كتبنا EHLO laptop.example.org بعد أن ظهر سطر 220، ولاحظ أن STARTTLS غير موجود في القائمة، فالاتصال مشفر من البداية ولا حاجة له. وللمنفذ 587 تستخدم الأمر نفسه مع -starttls smtp، فيقوم openssl بالخطوات المكشوفة بنفسه ثم يعرض لك الشهادة.

وقد يتساءل البعض: ما دامت الطريقتان تنتهيان باتصال مشفر، فما الفرق؟ والإجابة أن الخطوات المكشوفة في STARTTLS هي نقطة ضعفه، فمن يقف في منتصف الطريق يستطيع أن يحذف السطر 250-STARTTLS من القائمة فيظن البرنامج أن السيرفر لا يدعم التشفير، كما يذكر RFC 3207 نفسه في قسم الأمان، والبرنامج الذي يكمل دون تشفير يرسل كلمة المرور نصاً واضحاً. لذلك جاء RFC 8314 في يناير 2018 ليوصي بالتشفير المباشر Implicit TLS لكل البروتوكولات بين برنامج البريد ومزود الخدمة: 465 للإرسال، و993 لـ IMAP، و995 لـ POP3، مع إبقاء 587 بـ STARTTLS للبرامج التي تحتاجه.

📌
في مارس 2011 نشر Wietse Venema، مطور Postfix، ثغرة CVE-2011-0411: كثير من تطبيقات STARTTLS، ومنها Postfix نفسه، كانت تقرأ الأوامر التي يرسلها المهاجم في نفس حزمة الأمر STARTTLS ثم تنفذها بعد بدء التشفير وكأنها جاءت مشفرة. وبعد عشر سنوات نشر باحثون من جامعة Münster للعلوم التطبيقية في مؤتمر USENIX Security 2021 دراسة NO STARTTLS، وجدوا فيها أكثر من 40 ثغرة في تطبيقات STARTTLS في برامج وخوادم البريد، ووجدوا أن ثغرة 2011 نفسها ما زالت في 15 تطبيقاً وفي حوالي 2% من خوادم البريد التي فحصوها، وكانت توصيتهم تجنب STARTTLS حيث أمكن. والدرس: لمستخدميك فضل 465 و993 و995، وأبق 587 و143 للبرامج التي لا تعرف غيرها.

التشفير بين السيرفرات على المنفذ 25

المنفذ 25 مختلف، فلا يوجد عليه تسجيل دخول لأن أي سيرفر في العالم يحق له أن يسلمك بريداً لنطاقك، والتشفير عليه بـ STARTTLS انتهازي Opportunistic، أي أن المرسل يشفر إذا عرض المستقبل التشفير، ويرسل نصاً واضحاً إذا لم يعرضه، ولا يتحقق عادة من الشهادة، والسبب أن رفض التسليم لسيرفر بلا شهادة صحيحة كان سيوقف جزءاً كبيراً من البريد في العالم. وهذا ما فعله Postfix في المختبر عندما أرسل إلى Mailpit، وتراه في السجل:

Oct 06 05:00:07 mx postfix/smtp[199]: Untrusted TLS connection established to mx.example.net[192.168.80.4]:1025: TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange x25519 server-signature RSA-PSS (2048 bits) server-digest SHA256
Oct 06 05:00:07 mx postfix/smtp[199]: 1A4FEB095A: to=<[email protected]>, relay=mx.example.net[192.168.80.4]:1025, delay=0.09, delays=0.01/0.03/0.05/0, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 1EXgYuKrMHjhn7STTBMCLs)

الكلمة Untrusted تعني أن الاتصال مشفر ولكن الشهادة لم يتحقق منها، وأكمل Postfix التسليم لأن المستوى may لا يطلب أكثر من ذلك. ولكي يطلب النطاق المستقبل تشفيراً بشهادة صحيحة يوجد معياران: MTA-STS الذي ينشر سياسة عبر HTTPS وDNS، وقد شرحناه في مقال SPF وDKIM وDMARC، وDANE لـ SMTP في RFC 7672 الذي ينشر بصمة الشهادة في سجل TLSA ويحتاج DNSSEC على النطاق، وهذا هو سبب التحذير الذي يظهر في سجلات Stalwart كما في دليل Stalwart.

لماذا يحجب مزود السيرفر المنفذ 25؟

لأن المنفذ 25 لا يحتاج تسجيل دخول، فأي برنامج خبيث على أي سيرفر مخترق يستطيع أن يتصل مباشرة بسيرفرات Gmail وOutlook ويرسل منها آلاف الرسائل المزعجة، وعنوان IP الذي يرسل منها يدخل قوائم الحظر، ثم يؤجره المزود لعميل جديد فيجد بريده مرفوضاً من أول يوم. لذلك يحجب كثير من المزودين الاتصال الصادر على المنفذ 25 افتراضياً، مثل Hetzner وDigitalOcean وAWS، ويفتحه بعضهم بطلب، ولاحظ أن الحجب على الاتصال الصادر فقط، فسيرفرك يستقبل على 25 عادة ولكنه لا يستطيع أن يسلم. وقد شرحنا كيف تتأكد من ذلك قبل الشراء في دليل Postfix وDovecot، وإذا كان المنفذ محجوباً فالحل أن ترسل عبر خادم وسيط SMTP Relay على المنفذ 587 أو 465 كما في دليل الإرسال عبر Amazon SES، وهذا ممكن لأن منافذ الإرسال لا يحجبها أغلب المزودين. وقد جمعنا سياسات المزودين بتواريخ صفحاتهم الرسمية، ومنهم من يحجب 465 و587 أيضاً، مع أشكال الترحيل Relay كلها في مقال الفرق بين SMTP والترحيل Relay.

قراءة البريد: IMAP وPOP3 وJMAP

وصلت الرسالة إلى صندوق Bob على السيرفر، والآن يحتاج برنامج البريد أن يقرأها، وهنا ثلاثة بروتوكولات تختلف في سؤال واحد بسيط: أين يعيش البريد، على السيرفر أم على الجهاز؟ والصورة التالية تبين الفرق:

ثلاثة أعمدة: POP3 على المنفذ 995 ينزل صندوق INBOX فقط إلى جهاز واحد بالأمرين RETR وDELE فيجد الهاتف الصندوق فارغاً، ولا توجد مجلدات ولا حالة قراءة مشتركة، وIMAP على المنفذ 993 يبقي الصندوق على السيرفر بمجلداته INBOX وSent وDrafts وTrash وJunk وعلامة Seen، والجهاز والهاتف يتزامنان فما يقرأ في أحدهما يظهر مقروءاً في الآخر، وJMAP على المنفذ 443 يعمل بنفس نموذج IMAP عبر JSON وHTTPS مع الدفع الفوري ويدعمه Stalwart
POP3 ينقل البريد إلى جهاز واحد، وIMAP وJMAP يبقيانه على السيرفر لكل الأجهزة
  • POP3: البروتوكول الأقدم في RFC 1939، صمم لزمن كان فيه الاتصال بالإنترنت مؤقتاً، فيتصل البرنامج وينزل الرسائل من الصندوق الرئيسي ثم يحذفها من السيرفر عادة، ولا يعرف شيئاً عن المجلدات ولا عن حالة القراءة. وهو مناسب لحالة واحدة اليوم: جهاز واحد يريد نسخة محلية من البريد ولا يهمه أن يراه في مكان آخر.
  • IMAP: البروتوكول الذي يستخدمه أغلب الناس دون أن يعرفوا، وإصداره الحالي IMAP4rev2 في RFC 9051 منذ أغسطس 2021. البريد يبقى على السيرفر بمجلداته، وكل رسالة لها علامات Flags مثل \Seen للمقروءة و\Flagged للمهمة، فإذا قرأت رسالة في الحاسوب ظهرت مقروءة في الهاتف، والبحث يتم على السيرفر، والأمر IDLE يجعل السيرفر يخبر البرنامج بالرسالة الجديدة فوراً.
  • JMAP: البروتوكول الأحدث، وهو JSON عبر HTTPS في RFC 8620 للأساس وRFC 8621 للبريد، بنفس نموذج IMAP من صناديق وعلامات، ولكنه يجمع تغييرات كثيرة في طلب HTTP واحد ويدعم الدفع Push دون اتصال دائم، كما في موقع JMAP. ويدعمه Stalwart بجانب IMAP وPOP3، ولكن تأكد أن برنامج البريد عند مستخدميك يدعمه قبل أن تعتمد عليه.

والآن نقرأ صندوق Alice في المختبر عبر IMAP بالكتابة اليدوية، فنفتح اتصالاً مشفراً مباشراً بالمنفذ 31993، وهو المقابل لـ 993 في ال Docker Image الرسمية:

openssl s_client -connect imap.example.org:31993 -crlf -quiet 2>/dev/null

ثم نكتب الأوامر التالية سطراً سطراً، وكل أمر في IMAP يبدأ بوسم Tag نختاره، مثل a1، يعيده السيرفر في نهاية الرد لنعرف أي رد لأي أمر:

a1 LOGIN [email protected] labpass
a2 LIST "" "*"
a3 SELECT INBOX
a4 FETCH 1:* (FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (FROM SUBJECT)])
a5 STORE 1 +FLAGS (\Seen)
a6 LOGOUT

والمخرج سوف يكون كما يلي:

* OK [CAPABILITY IMAP4rev1 IMAP4rev2 LOGIN-REFERRALS ID ENABLE IDLE SASL-IR LITERAL+ AUTH=PLAIN] Dovecot ready.
a1 OK [CAPABILITY IMAP4rev1 IMAP4rev2 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE REPLACE SNIPPET=FUZZY PREVIEW=FUZZY PREVIEW SPECIAL-USE STATUS=SIZE SAVEDATE COMPRESS=DEFLATE INPROGRESS NOTIFY LITERAL+ UTF8=ACCEPT FILTER=SIEVE] Logged in
* LIST (\HasNoChildren \Drafts) "/" Drafts
* LIST (\HasNoChildren \Junk) "/" Junk
* LIST (\HasNoChildren \Sent) "/" Sent
* LIST (\HasNoChildren \Trash) "/" Trash
* LIST (\HasNoChildren) "/" INBOX
a2 OK List completed (0.001 + 0.000 secs).
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted.
* 2 EXISTS
* 2 RECENT
* OK [UNSEEN 1] First unseen.
* OK [UIDVALIDITY 1791262786] UIDs valid
* OK [UIDNEXT 3] Predicted next UID
a3 OK [READ-WRITE] Select completed (0.001 + 0.000 secs).
* 1 FETCH (FLAGS (\Recent) RFC822.SIZE 754 BODY[HEADER.FIELDS (FROM SUBJECT)] {62}
From: Finance <[email protected]>
Subject: Invoice 1042

)
* 2 FETCH (FLAGS (\Recent) RFC822.SIZE 2742 BODY[HEADER.FIELDS (FROM SUBJECT)] {104}
From: Mail Delivery System <[email protected]>
Subject: Undelivered Mail Returned to Sender

)
a4 OK Fetch completed (0.001 + 0.000 secs).
* 1 FETCH (FLAGS (\Seen \Recent))
a5 OK Store completed (0.002 + 0.000 + 0.001 secs).
* BYE Logging out
a6 OK Logout completed (0.001 + 0.000 secs).

في المخرج أعلاه لاحظ التالي:

  • في الصندوق رسالتان: الفاتورة التي أرسلناها على المنفذ 25، والرسالة المرتدة من قسم الرسائل المرتدة، وكلاهما وصل عبر LMTP من Postfix.
  • الأمر FETCH طلب رأسين فقط وحجم كل رسالة، ولم ينزل الرسالة كاملة، وهذا ما يجعل IMAP سريعاً مع صندوق فيه آلاف الرسائل، فالبرنامج يعرض القائمة أولاً وينزل الرسالة عندما تفتحها. والكلمة PEEK تعني «اقرأ دون أن تجعلها مقروءة».
  • الأمر STORE وضع العلامة \Seen على الرسالة الأولى على السيرفر نفسه، فأي برنامج آخر يفتح هذا الصندوق الآن سوف يراها مقروءة، وهذا هو الفرق الجوهري عن POP3.
  • المجلدات Sent وDrafts وJunk وTrash لها علامات استخدام خاصة Special-Use مثل \Sent، وبها يعرف برنامج البريد أين يحفظ الرسائل المرسلة مهما كانت لغة اسم المجلد.

وللمقارنة، نفس الصندوق عبر POP3 على المنفذ 31995، وهو المقابل لـ 995، بالأمر openssl s_client -connect imap.example.org:31995 -crlf -quiet 2>/dev/null ثم الأوامر USER [email protected] وPASS labpass وSTAT وLIST وTOP 1 0 وQUIT:

+OK Dovecot ready.
+OK
+OK Logged in.
+OK 2 3496
+OK 2 messages:
1 754
2 2742
.
+OK
Return-Path: <[email protected]>
Delivered-To: [email protected]
Received: from mx.example.org ([192.168.80.5])
	by imap.example.org with LMTP
	id XxzyLEKAxGoQAAAA68ePFA
	(envelope-from <[email protected]>)
	for <[email protected]>; Tue, 06 Oct 2026 04:59:46 +0000
Received: from mx.example.net (lab-mail-client.lab-mail-net [192.168.80.3])
	by mx.example.org (Postfix) with ESMTP id AFA1DB069F
	for <[email protected]>; Tue, 06 Oct 2026 04:59:46 +0000 (UTC)
Date: Tue, 06 Oct 2026 04:59:46 +0000
To: [email protected]
From: Finance <[email protected]>
Subject: Invoice 1042
Message-Id: <[email protected]>
X-Mailer: swaks v20240103.0 jetmore.org/john/code/swaks/

.
+OK Logging out.

لاحظ أن POP3 لا يعرف إلا صندوقاً واحداً برسائل مرقمة وأحجام، فلا توجد مجلدات ولا علامات، والردود ليست أرقاماً بل +OK و-ERR فقط. ولاحظ أيضاً في رؤوس الرسالة أن Dovecot أضاف سطر Received: جديداً بالكلمة with LMTP، وأضاف Return-Path وDelivered-To عند التسليم النهائي، فهذه الرسالة تحمل الآن الطريق الذي قطعته كاملاً. والقاعدة العملية لمستخدميك: IMAP على 993 لكل الأجهزة، وPOP3 فقط لمن يطلبه بسبب واضح، وJMAP عندما يكون الخادم والبرامج تدعمه.

إعادة استخدام اتصال SMTP: رسائل كثيرة على اتصال واحد

نعود الآن إلى الطرف المرسل، فقد رأيت في محادثة المنفذ 587 أن الرسالة الواحدة تحتاج قبل أن يبدأ إرسالها: اتصال TCP، ثم انتظار التحية، ثم EHLO، ثم STARTTLS ومصافحة TLS، ثم EHLO مرة ثانية، ثم AUTH، وكل خطوة تنتظر رداً من السيرفر قبل الخطوة التالية. فإذا كان تطبيقك يرسل 50 رسالة «تأكيد طلب» ويفتح اتصالاً جديداً لكل رسالة، فهو يدفع هذا الثمن خمسين مرة، والحل أن تبقي الاتصال مفتوحاً وترسل عليه رسالة بعد رسالة، فالمعيار يسمح بعدة رسائل في جلسة واحدة، وكل MAIL FROM جديد يبدأ رسالة جديدة. وهذه هي نفس فكرة تجميع الاتصالات Connection Pooling التي شرحناها بالتفصيل لقواعد البيانات في مقال تجميع الاتصالات، والصورة التالية تبينها في SMTP:

صفان: في الأعلى اتصال لكل رسالة، فكل رسالة تمر بالخطوات TCP وEHLO وSTARTTLS وTLS وEHLO وAUTH ثم الرسالة ثم QUIT، وتتكرر 50 مرة، ونتيجة Python smtplib في المختبر 2.5 إلى 2.7 ثانية لخمسين رسالة، وPostfix فتح 50 اتصال TLS، وفي الأسفل اتصال واحد يدفع الخطوات مرة واحدة ثم يرسل الرسائل 1 و2 و3 حتى 50 ثم QUIT، والنتيجة 0.7 إلى 1.2 ثانية، وPostfix استخدم 14 اتصال TLS، وفي الأسفل تنبيه أن تبقي الاتصالات المتوازية قليلة لأن السيرفرات المستقبلة تعدها، وأن Postfix رد في المختبر 421 4.7.0 too many connections
الخطوات قبل الرسالة تدفع مرة واحدة بدل خمسين مرة، والأرقام من المختبر على جهاز واحد

في كود تطبيقك

السكربت التالي scripts/send.py يرسل عدداً من الرسائل بطريقتين: اتصال جديد لكل رسالة، أو اتصال واحد لكل الرسائل، ويستخدم المكتبة smtplib المدمجة في Python:

import smtplib, ssl, sys, time
from email.message import EmailMessage

HOST, PORT = sys.argv[1], int(sys.argv[2])
MODE, COUNT = sys.argv[3], int(sys.argv[4])   # MODE: "new" = one connection per message, "reuse" = one connection for all
USER, PASSWORD = "[email protected]", "labpass"
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE                # lab only: self-signed certificate

def message(i):
    m = EmailMessage()
    m["From"], m["To"], m["Subject"] = USER, f"user{i}@example.net", f"Order #{i} confirmed"
    m.set_content(f"Your order #{i} is confirmed.")
    return m

def connect():
    s = smtplib.SMTP(HOST, PORT, local_hostname="app.example.org")
    s.starttls(context=ctx)
    s.login(USER, PASSWORD)
    return s

start = time.perf_counter()
if MODE == "new":
    for i in range(COUNT):
        with connect() as s:                   # TCP + EHLO + STARTTLS + EHLO + AUTH for every message
            s.send_message(message(i))
else:
    with connect() as s:                       # paid once
        for i in range(COUNT):
            s.send_message(message(i))         # MAIL FROM, RCPT TO, DATA on the same session
elapsed = time.perf_counter() - start
print(f"{MODE:5} {COUNT} messages in {elapsed:.2f} s ({elapsed / COUNT * 1000:.1f} ms per message)")

في الكود أعلاه لاحظ التالي:

  • الدالة connect تقوم بكل الخطوات المكلفة، والفرق بين الطريقتين هو فقط مكان with: داخل الحلقة أو خارجها. والكلاس SMTP يرسل QUIT تلقائياً عند الخروج من with كما تذكر الوثائق.
  • السطران اللذان يلغيان التحقق من الشهادة للمختبر فقط بسبب الشهادة الذاتية، فلا تنسخهما إلى تطبيق حقيقي، والسبب أنك بدونهما تعطي كلمة مرور SMTP لأي شخص يقف في منتصف الطريق.

نشغله ثلاث مرات بكل طريقة على Mailpit مباشرة، أي على المنفذ 1025 مع STARTTLS وتسجيل الدخول:

for r in 1 2 3; do
  python3 /lab/send.py mx.example.net 1025 new 50
  python3 /lab/send.py mx.example.net 1025 reuse 50
done
new   50 messages in 2.66 s (53.2 ms per message)
reuse 50 messages in 1.16 s (23.2 ms per message)
new   50 messages in 2.54 s (50.9 ms per message)
reuse 50 messages in 0.71 s (14.3 ms per message)
new   50 messages in 2.54 s (50.8 ms per message)
reuse 50 messages in 0.69 s (13.9 ms per message)

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

وفي Node.js تقوم المكتبة Nodemailer بذلك بالخيار pool: true، وتقول وثائقها إنها تبقي حتى maxConnections من الاتصالات مفتوحة، وقيمته الافتراضية 5، وترسل حتى maxMessages رسالة على الاتصال الواحد، وقيمته الافتراضية 100، ثم تفتح اتصالاً جديداً:

import nodemailer from 'nodemailer';

// Create ONE transporter when the app starts and reuse it for every message.
export const mailer = nodemailer.createTransport({
  host: 'smtp.example.org',
  port: 465,
  secure: true,          // implicit TLS
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
  pool: true,
  maxConnections: 5,     // default 5: parallel connections to this server
  maxMessages: 100,      // default 100: messages per connection before reconnecting
  rateDelta: 1000,       // optional: at most rateLimit messages per rateDelta ms
  rateLimit: 10,
});

والقاعدة نفسها في كل لغة: أنشئ كائن الإرسال مرة واحدة عند تشغيل التطبيق ولا تنشئه داخل دالة الطلب، فتقول الوثائق صراحة إن إنشاء Transporter جديد لكل رسالة يلغي فائدة التجميع. أما متى ترسل عبر SMTP ومتى عبر HTTP API، وأين ترسل البريد التفاعلي من تطبيقك، فقد شرحناه في دليل البريد التفاعلي.

في Postfix نفسه

وماذا عن السيرفر عندما يسلم الرسائل من طابوره إلى سيرفر آخر؟ يقوم Postfix بنفس الشيء عبر ذاكرة الاتصالات Connection Cache، وتشرح وثيقتها الرسمية أن الإعداد smtp_connection_cache_on_demand مفعل افتراضياً، فيحفظ Postfix الاتصال ليعيد استخدامه عندما يكون لوجهة واحدة عدد كبير من الرسائل في الطابور النشط، ويمكنك أن تفرضه دائماً لوجهات معينة بالإعداد smtp_connection_cache_destinations، مثل الخادم الوسيط relayhost. ولكن هناك تفصيل لا تجده إلا إذا قرأت الوثائق كاملة: هذا يعمل على الاتصالات المكشوفة، أما الاتصالات المشفرة بـ TLS فتحتاج الإعداد smtp_tls_connection_reuse، وقيمته الافتراضية no، وهو متاح منذ Postfix 3.4، ويعمل عبر الخدمة tlsproxy التي أضفناها في start.sh.

ولكي نرى ذلك نعيد تجربة الطابور مرتين: 50 رسالة مؤجلة إلى example.net ثم postqueue -f، ونعد في السجل الرسائل التي وصلت، والرسائل التي أرسلت على اتصال مستخدم من قبل، ويكتب Postfix لهذه الأخيرة conn_use= برقم الاستخدام، فالرسالة التي لا يوجد في سطرها conn_use فتحت اتصالاً جديداً. في المرة الأولى بالإعداد الافتراضي smtp_tls_connection_reuse=no:

docker logs --since 10s lab-mail-mta 2>&1 | grep -c "status=sent"
docker logs --since 10s lab-mail-mta 2>&1 | grep "status=sent" | grep -c "conn_use="
50
0

خمسون رسالة ولا توجد أي رسالة على اتصال مستخدم، أي 50 اتصال TCP وTLS كاملاً. ثم نفعل إعادة استخدام اتصالات TLS ونكرر نفس التجربة:

docker exec lab-mail-mta postconf -e smtp_tls_connection_reuse=yes
docker exec lab-mail-mta postfix reload
# ... the same 50 deferred messages, then postqueue -f ...
docker logs --since 10s lab-mail-mta 2>&1 | grep -c "status=sent"
docker logs --since 10s lab-mail-mta 2>&1 | grep "status=sent" | grep -c "conn_use="
docker logs --since 10s lab-mail-mta 2>&1 | grep "status=sent" | grep -o "conn_use=[0-9]*" | sort -t= -k2 -n | uniq -c
50
36
      8 conn_use=2
      8 conn_use=3
      6 conn_use=4
      5 conn_use=5
      4 conn_use=6
      3 conn_use=7
      1 conn_use=8
      1 conn_use=9

36 رسالة من 50 ذهبت على اتصال مفتوح من قبل، أي أن Postfix فتح 14 اتصالاً فقط بدل 50، واستخدم أطولها لتسع رسائل، وفي تشغيل آخر كانت النتيجة 15 اتصالاً. ويظهر الفرق في سطر التسليم نفسه، فهذا سطر رسالة فتحت اتصالاً جديداً ثم سطر رسالة على اتصال مستخدم:

Oct 06 05:28:16 mx postfix/smtp[481]: 28C6EB2405: to=<[email protected]>, relay=mx.example.net[192.168.80.4]:1025, delay=10, delays=10/0/0.05/0, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 6nV0jRISuLifZ2WtWnHEl5)
Oct 06 05:28:16 mx postfix/smtp[472]: 3D312B240E: to=<[email protected]>, relay=mx.example.net[192.168.80.4]:1025, conn_use=2, delay=10, delays=10/0.15/0/0, dsn=2.0.0, status=sent (250 2.0.0 Ok: queued as 4QOBU4yxRBMnB0aeihxR88)

والحقل delays فيه أربعة أزمنة كما تشرح وثائق Postfix: من وصول الرسالة حتى دخولها الطابور النشط، ثم حتى بداية الاتصال، ثم إعداد الاتصال، وهو مع الاتصال الجديد يشمل DNS وTCP وEHLO وSTARTTLS، ثم نقل الرسالة. ولاحظ أن الزمن الثالث 0.05 في السطر الأول و0 في الثاني لأن الاتصال كان جاهزاً، فالعشر ثوان في أول الحقل هي فقط وقت انتظار الرسالة في الطابور أثناء التجربة. وتذكر الوثيقة أن الفائدة الحقيقية تظهر في حالتين: الوجهة التي لها عدة سيرفرات بعضها لا يرد، والسيرفرات التي تحدد عدد الاتصالات الجديدة، وهذه الحالة الثانية هي موضوع القسم التالي.

لماذا لا تفتح اتصالات كثيرة بالتوازي؟

قد تفكر أن الحل الأسرع ليس إعادة الاستخدام، وإنما أن تفتح 50 اتصالاً معاً وترسل الرسائل كلها في نفس اللحظة، وعلى جهازك سوف ينجح هذا وقد يكون أسرع. ولكن السيرفر المستقبل لا يرى «تطبيقاً سريعاً»، وإنما يرى عنوان IP واحداً فتح فجأة عشرات الاتصالات، وهذا بالضبط شكل البرامج المزعجة، لذلك تضع السيرفرات حدوداً على عدد الاتصالات المتزامنة وعلى معدلها. وفي Postfix مثلاً يحدد smtpd_client_connection_count_limit عدد الاتصالات المتزامنة من كلاينت واحد، وقيمته الافتراضية 50، ولنرى الرد عندما تتجاوزه جعلناه 3 في المختبر وفتحنا خمسة اتصالات:

docker exec lab-mail-mta sh -c 'postconf -e smtpd_client_connection_count_limit=3 && postfix reload'
python3 -c "
import smtplib
conns = []
for i in range(1, 6):
    try:
        s = smtplib.SMTP('mx.example.org', 25, local_hostname='mx.example.net')
        code, msg = s.ehlo()
        print(i, code, msg.split(b'\n')[0].decode())
        conns.append(s)
    except Exception as e:
        print(i, type(e).__name__, e)
"
1 250 mx.example.org
2 250 mx.example.org
3 250 mx.example.org
4 SMTPConnectError (421, b'4.7.0 mx.example.org Error: too many connections from 192.168.80.3')
5 SMTPConnectError (421, b'4.7.0 mx.example.org Error: too many connections from 192.168.80.3')

وفي سجل Postfix:

Oct 06 05:15:50 mx postfix/smtpd[348]: warning: Connection concurrency limit exceeded: 4 from lab-mail-client.lab-mail-net[192.168.80.3] for service smtp

الرد 421 مؤقت، فالسيرفر المرسل الجيد يعيد المحاولة لاحقاً، ولكن التطبيق الذي يرسل مباشرة دون طابور يفقد الرسالة، والسيرفرات الكبيرة أشد من ذلك، فقائمة أخطاء SMTP في Gmail فيها الرد 421 4.7.28 الذي يقول إن Gmail لاحظ معدلاً غير معتاد من البريد من عنوانك أو نطاقك فحد منه مؤقتاً، والرد 450 4.2.1 عندما يستقبل المستلم بريداً بمعدل أكبر مما يسمح به. وبالتالي فالمعدل والتوازي يؤثران في سمعة عنوانك وفي وصول بريدك، وليس في السرعة فقط. لذلك القواعد العملية:

  • أعد استخدام الاتصال بدل أن تفتح اتصالاً لكل رسالة، فهذا أسرع لك وأخف على السيرفر الآخر.
  • أبق الاتصالات المتوازية قليلة، فالقيمة الافتراضية 5 في Nodemailer معقولة، واستخدم rateLimit إذا كانت خدمة الإرسال تحدد معدلاً.
  • لا ترسل من التطبيق مباشرة إلى سيرفرات المستلمين، وإنما إلى MTA أو خدمة إرسال لها طابور تعيد المحاولة عند 4xx، فهذا ما رأيته في قسم الطابور.

أعد القيمة إلى 50 بالأمر postconf -e smtpd_client_connection_count_limit=50 ثم postfix reload، وعندما تنتهي من المختبر احذفه كاملاً من مجلد mail-lab:

docker compose down --rmi local

الخيار --rmi local يحذف ال Images التي بناها Compose لـ Postfix والكلاينت فقط، ويترك Images Mailpit وDovecot التي نزلها من Docker Hub.

أدوات الفحص التي تحتاجها

كل الأدوات التي استخدمناها في المختبر هي نفسها التي تستخدمها على سيرفر حقيقي عندما لا تصل رسالة، وهذه خلاصتها:

  • swaks: سكين الجيش السويسري لـ SMTP، يرسل رسالة اختبار ويكتب المحادثة كاملة، ويدعم STARTTLS والتشفير المباشر بالخيار --tlsc وتسجيل الدخول والرؤوس المخصصة، وهو أول ما تجربه بعد أي تغيير في إعداد الإرسال. ولا تنس الحزم التي تضيف له TLS وتسجيل الدخول.
  • openssl s_client: يفتح اتصالاً مشفراً ويعرض الشهادة والإصدار، بالخيار -starttls smtp أو -starttls imap للمنافذ التي تبدأ مكشوفة، ودونه للمنافذ 465 و993 و995، وهو الطريقة الأسرع لترى هل الشهادة منتهية أو لاسم آخر.
  • nc: لكتابة المحادثة بيدك على المنافذ المكشوفة 25 و587 قبل STARTTLS، مع الخيار -C لنهاية السطر الصحيحة، وأبسط استخدام له أن ترى هل يصل سطر التحية 220 أصلاً، فإذا لم يصل فالمشكلة في جدار الحماية أو في حجب المنفذ وليست في إعداد البريد.
  • dig: لسجلات MX وA وPTR، وقد جمعنا أوامره لكل سجل بريد في مقال SPF وDKIM وDMARC.
  • Mailpit: سيرفر SMTP للاختبار يمسك كل رسالة يرسلها تطبيقك أثناء التطوير ويعرضها في المتصفح، فلا تصل رسالة تجريبية إلى عميل حقيقي، وتستطيع أن تختبر به رد تطبيقك على أخطاء 4xx و5xx بميزة Chaos كما فعلنا.
  • سجل الخادم والطابور: في Postfix الأمر postqueue -p للطابور وpostqueue -f لإعادة المحاولة الآن، والسجل بالبحث عن رقم الرسالة، وأهم ما فيه الحقلان status= وdsn= والرد الذي قاله السيرفر الآخر بين قوسين.
  • الفحص من الخارج: خدمات مثل mail-tester.com وinternet.nl ترسل لها رسالة حقيقية أو تفحص نطاقك، فتخبرك عن سجلات المصادقة وعن STARTTLS وغيرها، وقد شرحنا ما تفحصه كل خدمة في قسم الاختبار في مقال SPF وDKIM وDMARC.

أخطاء شائعة

  • إعداد برنامج البريد أو التطبيق على المنفذ 25: المنفذ 25 للسيرفرات، وأغلب الخوادم لا تقبل عليه تسجيل الدخول، وكثير من المزودين يحجبونه صادراً، فتبدو المشكلة وكأنها «السيرفر لا يرد». استخدم 465 أو 587.
  • اعتبار 250 queued وصولاً: هذا الرد يعني أن سيرفرك قبل الرسالة فقط، وقد تبقى في طابوره أياماً أو ترتد، لذلك عندما يقول المستخدم «أرسلت ولم تصل» ابدأ بسجل سيرفرك والطابور قبل أي شيء آخر.
  • إعادة المحاولة عند 5xx أو تجاهل 4xx: التطبيق الذي يعيد إرسال رسالة رفضت بـ 550 يضر سمعة عنوانه، والذي يتجاهل 421 أو 451 يفقد رسائل كان يمكن أن تصل، والحل أن ترسل إلى MTA أو خدمة لها طابور.
  • قبول الرسالة ثم إرجاعها: يرسل رسائل مرتدة إلى عناوين مزورة لأشخاص أبرياء، فتحقق من المستلم عند RCPT TO.
  • الخلط بين الظرف والرؤوس: البحث عن سبب الرسالة المرتدة في From: بدل Return-Path، أو الظن أن To: هو من استلم الرسالة فعلاً.
  • إطفاء التحقق من الشهادة في كود الإرسال: سطرا المختبر في send.py يجعلان كلمة مرور SMTP تذهب لأي سيرفر يدعي أنه سيرفرك، فلا تنقلهما إلى تطبيق حقيقي.
  • اتصال جديد لكل رسالة، أو عشرات الاتصالات المتوازية: الأول بطيء ويرهق سيرفر الإرسال بتسجيل الدخول، والثاني يجلب ردود 421 وحدود المعدل، والصحيح اتصالات قليلة يعاد استخدامها.
  • POP3 لمستخدمين لديهم أكثر من جهاز: ينزل البريد إلى أول جهاز يتصل ويحذفه من السيرفر عادة، فيجد الهاتف صندوقاً فارغاً، وتضيع الرسائل إذا تعطل ذلك الجهاز.

قائمة تحقق

  1. هل تعرف أي برنامج في منظومتك يقوم بكل دور: من يستقبل من المستخدمين، ومن ينقل بين السيرفرات، ومن يكتب في الصناديق، ومن يخدم القراءة؟
  2. هل سجل MX لنطاقك يشير إلى اسم سيرفر له سجل A، وهل تعرف ماذا يرى dig +short MX لنطاقك؟
  3. هل المنفذ 25 مفتوح للاستقبال، وهل الاتصال الصادر على 25 مسموح من مزودك، أو لديك خادم وسيط؟
  4. هل منفذ الإرسال 465 أو 587 يطلب TLS قبل تسجيل الدخول، ويرفض الإرسال إلى الخارج دون تسجيل دخول؟
  5. هل المنفذ 25 يرفض التمرير إلى نطاقات غيرك برفض دائم بعد أن تأكدت من إعدادك؟
  6. هل مستخدموك على 993 و465، ومنافذ STARTTLS وهي 587 و143 و110 فقط لمن يحتاجها؟
  7. هل تتحقق من المستلم أثناء المحادثة بدل قبول الرسالة ثم إرجاعها؟
  8. هل تطبيقك يرسل إلى MTA أو خدمة لها طابور، ويعيد استخدام الاتصال، ويبقي الاتصالات المتوازية قليلة؟
  9. هل تعرف أين تجد الطابور والسجل على سيرفرك، وكيف تتبع رسالة برقمها؟

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • الرسالة تمر بأدوار واضحة: برنامج البريد MUA، ثم بوابة الإرسال MSA بتسجيل دخول على 587 أو 465، ثم MTA يسأل DNS عن سجل MX ويسلم على المنفذ 25، ثم MDA يكتبها في الصندوق، ثم يقرؤها البرنامج عبر IMAP أو POP3 أو JMAP.
  • SMTP محادثة نصية، والخانة الأولى من الرد هي القرار: 2xx نجاح، و3xx أكمل، و4xx حاول لاحقاً من الطابور، و5xx لا تحاول وأرسل رسالة مرتدة بمرسل فارغ إلى عنوان الظرف.
  • الظرف MAIL FROM وRCPT TO تقرؤه السيرفرات، والرؤوس From: وTo: يقرؤها الإنسان، والاثنان مستقلان، ومنه جاءت النسخة المخفية Bcc وجاء الانتحال الذي يعالجه DMARC.
  • المنفذ 25 للسيرفرات بتشفير انتهازي، و465 و993 و995 بتشفير مباشر هي الأفضل لمستخدميك كما يوصي RFC 8314، و587 مع STARTTLS للبرامج التي تحتاجه.
  • أعد استخدام اتصال SMTP في تطبيقك وفي Postfix، فقد أرسلنا 50 رسالة على اتصال واحد أسرع عدة مرات، وأرسل Postfix 50 رسالة على 14 اتصال TLS بدل 50 بعد تفعيل smtp_tls_connection_reuse، وأبق الاتصالات المتوازية قليلة لأن السيرفرات المستقبلة ترد بـ 421 وتحد من معدلك.

وهذه المقالات تكمل الصورة بحسب ما تريد أن تفعله بعد ذلك:

دليلكيف تستضيف بريد مؤسستك بنفسك، من القرار إلى التشغيلمتى تستضيف بريد شركتك بنفسك ومتى يكون البريد المستضاف أنسب، وكيف تنقل المستخدمين على مراحل دون أن تضيع رسالة.دليلشرح DNS وسجلاته للمبتدئين بمثال حقيقيكيف يتحول اسم نطاقك إلى عنوان سيرفرك، وما وظيفة كل سجل تضيفه في لوحة النطاق، والأخطاء التي تضيع فيها الساعات عادة.دليلشرح SPF وDKIM وDMARC للمبتدئين: كيف تثبت أن رسائلك منك فعلاًلماذا يستطيع أي شخص أن يرسل باسم نطاقك، وكيف يسد كل سجل من هذه السجلات الثلاثة ثغرة تركها الذي قبله.دليلبناء خادم بريد بيدك مع Postfix وDovecot على Ubuntuنبني خادم بريد كاملاً من حزم Ubuntu دون Docker، فتعرف دور كل مكون وأين تبحث عندما تتوقف رسالة.دليلTransactional Email: كيف تصل رسائل تطبيقك في وقتهالماذا تفصل رسائل تطبيقك مثل استعادة كلمة المرور عن النشرات البريدية، وكيف ترسلها بحيث تصل في وقتها ولا تذهب إلى Spam.دليلConnection Pooling: لماذا الاتصال بقاعدة البيانات مكلفلماذا تظهر رسالة too many clients عندما تزيد نسخ تطبيقك، وكيف يحل مجمع الاتصالات وPgBouncer هذه المشكلة.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال واختباره على Postfix 3.10.13 من حزمة Debian 13 وDovecot 2.4.5 وMailpit 1.31.4 وswaks 20240103.0 مع Docker Engine 29.8.0.
  • أكتوبر 2026: إضافة رابط إلى مقال الفرق بين SMTP والترحيل Relay في قسم حجب المنفذ 25، وتصحيح جملة منافذ الإرسال لأن بعض المزودين يحجبونها أيضاً.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.