تكتب رسالة في برنامج البريد وتضغط «إرسال»، فتصل إلى صاحبها في 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:

وفيما يلي الأدوار بالترتيب كما في الصورة:
- برنامج البريد 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 محادثة طويلة بعدة أسطر ذهاباً وإياباً وليست طلباً واحداً ورداً واحداً. والصورة التالية تبين المحادثة كاملة كما سوف نراها بعد قليل:

والجدول التالي يجمع الأوامر التي تراها في كل محادثة تقريباً:
| الأمر | ماذا يعني |
|---|---|
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 AFA1DB069FOct 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 أيام على الأقل. والصورة التالية تبين الطريقين، المؤقت والدائم:

ولنرى الطابور بأعيننا نطلب من 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 -pMail 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.
RCPT TO ويرد بـ 550 مباشرة كما فعل Mailpit هنا.الظرف والرؤوس: عنوانان للمرسل وعنوانان للمستلم
لاحظت في المحادثة أن العناوين تظهر مرتين: مرة في الأوامر MAIL FROM وRCPT TO، ومرة في الرؤوس From: وTo: بعد DATA، وهذا ليس تكراراً، فالأولى هي الظرف Envelope الذي يعرفه RFC 5321 وتقرؤه السيرفرات لتعرف أين تسلم الرسالة وأين تعيدها إذا فشلت، والثانية هي رؤوس الرسالة التي يعرفها RFC 5322 ويعرضها برنامج البريد للإنسان. والأهم أن البروتوكول لا يطلب أن يتطابقا، والصورة التالية تبين ذلك بقيم حقيقية من المختبر:

والمثال التالي يرسل رسالة ظرفها إلى [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؟ والإجابة أن كل منفذ باب لنوع مختلف من الزوار، فالسيرفرات الأخرى تدخل من باب، ومستخدموك الذين يرسلون من باب، والذين يقرؤون من باب ثالث، ولكل باب قواعده في التشفير وتسجيل الدخول، وإذا كان مصطلح المنفذ جديداً عليك فقد شرحناه في مقال مصطلحات الاستضافة الذاتية. والصورة التالية تبين المنافذ ونوع التشفير على كل منها:

| المنفذ | الاسم | من يستخدمه | التشفير | تسجيل الدخول |
|---|---|---|---|---|
| 25 | smtp | السيرفرات فيما بينها | STARTTLS إذا عرضه الطرفان | لا يوجد |
| 587 | submission | برامج البريد والتطبيقات للإرسال | STARTTLS، وسيرفرك يجب أن يطلبه | إلزامي |
| 465 | submissions | برامج البريد والتطبيقات للإرسال | تشفير مباشر Implicit TLS | إلزامي |
| 993 | imaps | القراءة عبر IMAP | تشفير مباشر | إلزامي |
| 143 | imap | القراءة عبر IMAP للبرامج القديمة | STARTTLS | إلزامي |
| 995 | pop3s | التنزيل عبر POP3 | تشفير مباشر | إلزامي |
| 110 | pop3 | التنزيل عبر POP3 للبرامج القديمة | STARTTLS | إلزامي |
| 443 | https | JMAP والبريد عبر الويب | 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 -crlfdepth=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 للبرامج التي تحتاجه.
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: البروتوكول الأقدم في 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:

في كود تطبيقك
السكربت التالي 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
donenew 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 -c50
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=936 رسالة من 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 لمستخدمين لديهم أكثر من جهاز: ينزل البريد إلى أول جهاز يتصل ويحذفه من السيرفر عادة، فيجد الهاتف صندوقاً فارغاً، وتضيع الرسائل إذا تعطل ذلك الجهاز.
قائمة تحقق
- هل تعرف أي برنامج في منظومتك يقوم بكل دور: من يستقبل من المستخدمين، ومن ينقل بين السيرفرات، ومن يكتب في الصناديق، ومن يخدم القراءة؟
- هل سجل MX لنطاقك يشير إلى اسم سيرفر له سجل A، وهل تعرف ماذا يرى
dig +short MXلنطاقك؟ - هل المنفذ 25 مفتوح للاستقبال، وهل الاتصال الصادر على 25 مسموح من مزودك، أو لديك خادم وسيط؟
- هل منفذ الإرسال 465 أو 587 يطلب TLS قبل تسجيل الدخول، ويرفض الإرسال إلى الخارج دون تسجيل دخول؟
- هل المنفذ 25 يرفض التمرير إلى نطاقات غيرك برفض دائم بعد أن تأكدت من إعدادك؟
- هل مستخدموك على 993 و465، ومنافذ STARTTLS وهي 587 و143 و110 فقط لمن يحتاجها؟
- هل تتحقق من المستلم أثناء المحادثة بدل قبول الرسالة ثم إرجاعها؟
- هل تطبيقك يرسل إلى MTA أو خدمة لها طابور، ويعيد استخدام الاتصال، ويبقي الاتصالات المتوازية قليلة؟
- هل تعرف أين تجد الطابور والسجل على سيرفرك، وكيف تتبع رسالة برقمها؟
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- الرسالة تمر بأدوار واضحة: برنامج البريد 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، وتصحيح جملة منافذ الإرسال لأن بعض المزودين يحجبونها أيضاً.